操作系统(中)

参考自:小林coding的图解系统

书接上回,讲完了硬件结构,该轮到内核了。内核主要负责沟通应用于硬件设备。内核的存在可以让应用程序无需关注硬件的运行,应用程序只需要进行一些系统调用与内核打交道就能运行。
内核主要负责进程/线程的调度、内存管理、硬件设备管理以及给提供系统调用。

内存管理

虚拟内存

为了能够在内存中同时运行多个程序,必须为程序提供虚拟的内存地址供其访问,以防止一个程序访问到了另一个程序的内存地址。
内核提供一种机制,将物理内存映射到虚拟地址,并通过分段与分页进行管理。

内存分段

程序有若干逻辑段,如数据段、代码段、堆栈空间等等。在32系统中,有着4GB的虚拟地址空间,其中1GB供内核态使用,剩下3GB留给用户态使用。
这些段在虚拟地址空间里按一定的逻辑顺序排列,比如栈空间在高地址,从高往低延伸,且有大小限制(在我的云主机里是8M),堆空间在低地址,从低往高延伸,且能够动态扩展(当然还是受限于3G的用户态空间)。但是映射到物理地址时顺序就有可能变化。
在这里插入图片描述
内存分段的不足是内存碎片问题和内存交换效率低。
1、内存碎片
在这里插入图片描述
此处产生了两块128MB的空闲内存,但任意一块都放不下200MB的内存,因此产生了百分之25的内存浪费(1G空间有256的空闲)。这些碎片称为外部内存碎片,是由于无法加载新的程序进来导致的。
内部内存碎片是指,程序本身就存在部分内存是不常用的,但还是要占着位置(占着图书馆座位不看书)。

2、内存交换
内存交换可用于解决外部内存碎片,根据上面那个例子,我们把占用256G的那个音乐程序先换到硬盘上,再换回内存中紧跟着游戏内存的屁股后面,那么就得到了256M的连续内存空间。
硬盘里也专门有一块用于内存与硬盘空间交换的空间,称为swap空间
但是硬盘与内存的读写速度不完全不匹配的,硬盘的读写速度比内存至少慢10倍,如果交换一个很大的程序,就会卡死。

内存分页

为了解决内存分段的带来的问题,我们选择把整个内存空间切成一段段固定大小的,linux下一页大小为4K。页表中的页通过MMU(Memory Management Unit)转换为物理地址。
如果内存访问的虚拟地址在页表中查不到,就会触发缺页异常,把缺少的页换入内存。
页表的结构如下所示:
在这里插入图片描述
查询时需要:
1、把虚拟内存地址切分为页号和偏移量
2、根据页号查页表得到物理页号
3、物理页号加上偏移量就得到了物理内存地址

question

分页如何解决内存碎片和交换效率低下?
1、页是提前划分好的,内核用页来管理内存,释放的时候也是以页为单位,因此不会出现太大的内存浪费。
2、不必把程序整个装载到内存进来,只需要装入其中的部分页,等到要访问的时候再换入内存。这样一次需要交换的空间就比较少。

多级页表

多级页表解决了内存空间的缺陷。32位系统下的虚拟地址空间有4G,可分为一百多万个页,每个页表项需要4字节存储,那么一个进程就需要4M的空间存储页表,但一个系统可以开启的进程可达上百个,这意味着系统需要用至少400M的空间存储页表。
我们可以把这100万个页表项先进行初步分页,即一级页表有1024个页表项,一级页表的每一个页表项都有1024个二级页表项。一级页表的页表项起初是空的,访问到了才会进行二级页表项的映射。
按照我的理解,应该是这样的:
1、进程想要运行,是一定要有页表管理整个4G的地址空间。
2、如果用单级页表,页表就得保存100万个页表项。
3、如果采用二级页表,我们就可以用一级页表先囊括一大段地址内的页表项,等进程访问到这段数据中的一页后再到二级表里查询。一级页表只是囊括了地址,并不一定保存了页表项,也已可以延后分配。
在这里插入图片描述

段页式内存管理

内存分段与分页都有各自的有点,段页式管理各取所长。
实现为:
1、先把程序划分了多个有意义的段(堆栈、代码段、数据段等等)
2、每个段再细分为多个页

因此要访问一块物理地址需要:
1、访问段表,得到页表地址
2、访问页表,得到物理页号
3、物理页加上页偏移,得到物理地址

linux内存管理

linux的每个进程都有着4G地址空间(从0地址开始),也就是说,每个进程看到的地址空间其实没有区别。
linux采用段页式管理,地址空间分布如下:
在这里插入图片描述1、内核空间只有进入内核态(系统调用)才会访问。
2、栈段包括函数内的局部变量和函数调用上下文(函数的栈帧,包含了函数内的局部变量、返回地址等等),有大小限制(8M),但可以通过ulimit -s命令改变栈大小。由系统自行管理。
3、文件映射段包括共享内存、动态库等。
4、堆段包含了动态分配的内存,可动态扩展大小,内存泄漏就发生在这块。
5、数据段包括了初始化和未经初始化的静态变量。

进程与线程

进程状态

一个进程至少有三种状态:
1、运行状态:占用cpu。
2、就绪状态:可以运行,但需要等待调度以得到cpu控制权。
3、阻塞状态:进程在等待某一事件发生而停止运行,在这件事发生以前,不会回到运行状态。

系统会把处于阻塞状态的进程的地址空间换出到硬盘,那么没有占用内存空间的进程就有了一个新状态,也就是挂起状态
挂起又分为阻塞挂起状态就绪挂起状态,阻塞挂起的进程在硬盘一直等待事件发生,就绪挂起的进程一换入内存就可以运行。
导致挂起的原因不只是进程的内存空间不在内存中,还有可能是因为进程内调用了sleep函数、或者被linux的Ctrl+Z命令丢到了后台。

进程状态转移图:
在这里插入图片描述

进程控制块PCB

进程存在的标志,包含了:
1、进程描述信息:包括进程标识符(PID)、用户标识符(进程归属的用户)
2、进程控制和管理信息:进程状态(运行、就就绪等)、进程优先级
3、资源分配清单:打开的文件描述符列表等
4、cpu内各寄存器的值

系统会将进程状态相同的进程的PCB用链表形式连接在一起。

进程控制

1、创建进程
操作系统允许一个进程创建子进程,并且子进程会继承父进程的资源(准确来说,子进程会继承父进程的部分地址空间,在子进程没有修改这部分数据时,父子进程共享这块空间,子进程修改之后才会创建自己的地址空间来使用,这就是copy on write技术)。
系统会先为新进程创建PCB并为其分配足够的资源,然后把其放入就绪队列等待调度。

2、终止进程
程序正常结束、异常、收到进程终止信号都会导致进程的终止。
进程终止时会先把cpu资源还回给系统并等待子进程终止(否则子进程会变成孤儿进程),把相应的PCB从队列中删除,然后把占用的所有资源归还。

3、阻塞进程
保护现场,让出cpu,并把PCB插到阻塞队列。

4、唤醒进程
进程不会自己脱离阻塞状态,因此需要去唤醒它,所谓的唤醒就是将其PCB从阻塞队列转移到就绪队列。

CPU上下文切换

又分为进程上下文切换、线程上下文切换、中断上下文切换,本质上是把CPU中的寄存器和程序计数器等数据重新加载。

进程上下文切换

包含了虚拟内存、栈、全局变量等用户空间的资源和寄存器等内核空间的资源的交换,交换出来的信息会保存在进程的PCB。
进程上下文切换有以下场景:
1、时间片耗尽。
2、系统资源不足。
3、通过sleep主动挂起。
4、被优先级更高的进程抢占。
5、发生硬件中断。
进程上下文切换的开销是比较大的。

线程

同一个进程中的多个线程共享代码段、数据段、打开的文件资源等,但每个线程都有自己的寄存器和栈。这些私有资源保存在线程控制块(TCB)中。
linux里面的线程都是轻量级进程(LWP),在linux里面真正执行程序的都是LWP,进程只不过是提供给线程运行资源罢了。就像进程提供了水泥给线程砌墙…

线程与进程比较:
1、进程是资源分配的单位(内存等),线程是CPU调度的单位。
2、进程有着一整块虚拟内存空间,线程只是独享寄存器和栈。
3、线程同样有就绪、运行、阻塞等状态。
4、线程的开销更小。体现在线程创建时间和终止事件更快、上下文切换更快、线程间通讯也不需要像进程间那么麻烦。

线程上下文切换

如果俩线程不归属同一个进程,那么就跟进程上下文切换一样。
如果归属同一进程,就会切换线程独享的寄存器、栈等。

线程的实现

线程有三种实现方法:
一、用户线程:在用户态实现,由用户态的线程库管理
**加粗样式**
优点:
1、每个进程都有它私有的TCB,TCP完全由线程库管理,因此在不支持线程的系统中也能使用。
2、用户线程的切换无需切换至内核态,速度快

缺点:
1、如果一个线程调用系统调用而阻塞了,所属的进程的其他线程也跟着阻塞。
2、没有办法打断运行中的线程,除非它主动让出cpu,因为调度问题不归用户太管。
3、每个线程都得分配一些进程的时间片,因此各个线程的执行时间会比较短。

二、内核线程:由内核实现和管理
**加粗样式**优点:
1、一个进程的其中一个线程阻塞不会影响其他内核线程。
2、时间片直接分配给线程,线程执行时间更长。

缺点:
1、线程的创建、切换等都需要通过系统调用,系统开销比较大。
2、由内核维护PCB和TCP。

三、轻量级进程:在内核中实现了用户线程

在这里插入图片描述也就是用LWP作为桥梁沟通了用户线程与内核线程。
用M个用户线程对应N个LWP,再对应到一个内核线程中。这种做法使得大部分线程上下文切换发生在用户空间,并且多个线程又能利用多核cpu。

进程调度算法

1、先来先服务
非抢占式算法,维护一个就绪进程队列,每次从队头取出进程运行。适用于计算密集型的程序。

2、最短作业优先调度
选择运行时间最短的先运行,不利于长作业,并且如果频繁的有短作业到来,长作业可能长时间内都不会运行。

3、高响应比优先调度
设定一个响应比优先级,计算公式是:优先级=(等待时间+要求服务时间)/ 要求服务时间。那么等待时间越长,任务的优先级就越高,也就越有可能得到运行机会。这种调度兼顾了长作业进程。

4、时间片轮转调度
为每个进程分配时间片,如果时间片用完了进程还没执行完毕,就切换到下一个进程,并把该进程放入队列尾部等待下一次运行。如果途中进程运行完毕则立刻进行进程切换。时间片设为20-50ms较为合理。

5、最高优先级调度
为进程分配优先级,静态优先级在分配之后就不会改变,动态优先级随着等待时间而逐渐提高。

6、多级反馈队列调度
糅合了优先级调度和时间片调度,我们根据优先级把进程分成多个队列,优先级高的先执行但分配的时间片较小,优先级低的后执行但分配的时间片较大。如果一个进程在时间片内没有运行完,就会分配到优先级低一级的队列的队尾。
这种调度兼顾了长调作业,又有很好的响应时间。
在这里插入图片描述

进程间通信

管道

在linux命令行中的 '|'就是一个管道,其作用是把管道前的命令输出作为后一个命令的输入,在此过程中伴随着子进程的创建。这种管道没有名字,称为匿名管道,只能用于父子进程间通信。
通过mkfifo命令可以创建一个命名管道,实则是一个文件,文件类型为p,可用于任意进程间通信。
管道的通信效率很低,且只有等管道内的数据被读走后才能退出,不适合频繁地交换数据。

消息队列

一种通信模式,如果要给另一个进程发信息,只需要把信息放到对应的消息队列就可以返回。
消息队列是保存在内核的消息链表,每个节点都是固定大小的数据块。
但是缺点是通信不及时,也无法传输大数据。

共享内存

将两个进程的部分虚拟地址空间映射到同一块物理内存上,省去了数据拷贝的过程,一个进程进行数据修改,另一个进程可以直接看到。

信号量

实际上是一个整型数字,用于进程间的同步和互斥。通过PV原语控制信号量大小,P操作将信号量减1,如果信号量小于0,进程将阻塞,如果大于0就正常运行。V操作将信号量加1,如果信号量<=0就会唤醒阻塞的进程。

信号

最古老的进程通信方式,是一种异步通信机制。可以在任何时候给某个进程发送信号,进程收到信号后有以下三种行为:
1、执行默认操作(比如进程终止等等)
2、捕捉信号,调用信号处理函数
3、忽略,但无法忽视SIGKILL和SEGSTOP,它们用于终止进程。

socket

可用于不同主机不同进程、同主机不同进程间通信。通过socket系统调用创建一个文件描述符。

同步与互斥

1、互斥:我们把需要修改多线程共享变量的代码段称为临界区,在临界区内,需要保证同一时间只有一个线程在临界区内。
2、同步:多个进程/线程间某些时候需要相互等待。

进程/线程必须获取锁才能进入临界区,离开临界区时,要释放锁。

自旋锁

线程一直等待,直到获取锁为止(通过CAS实现),此时线程不干别的事却一直占用cpu资源,造成极大的资源浪费。

无等待锁

线程若取不到锁,就置为等待状态,让出cpu,等待下一次调度再次尝试获取锁。

互斥锁

当有一个线程获取锁成功后,其他线程再获取锁后都会失败并阻塞。

读写锁

1、允许多个线程读,有线程读数据时,排斥所有写线程。
2、只允许单个线程写,有线程修改数据时,排斥所有其他线程。

悲观锁

认为多线程同时修改共享数据概率较高,因此在访问共享数据前,先上锁。但是由于写数据时排斥所有其他线程,因此在共享文档的修改等场景就不合适。

乐观锁

认为多线程同时修改共享数据的概率较低,因此全程不加锁,也就是无锁编程,每次修改数据时更新版本号,通过版本号来验证数据是否修改成功。实现难度很高,只有在加锁成本很高的时候才考虑用它。

死锁

由于代码编写不合理,导致多个线程都停在获取锁这一步骤上,无法继续运行。
需要同时满足以下四个条件:
1、互斥条件:多个线程不能同时使用同一个资源。
2、持有并等待条件:线程不会放弃已经持有的资源,并且会一直等待获取新的资源。
3、不可剥夺条件:在使用完资源以前,不允许其他线程获取该资源。
4、环路等待条件:线程1获取了资源1,又想获取资源2;线程2获取资源2,又想获取资源1。结果只能是无限等待。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值