Linux 进程调度与内存管理详解

1,696 阅读6分钟

1. PCB 进程控制器 task_struct

对于 Linux 操作系统而言,每一个进程都对应一个 task_struct 结构体(Linux 中的线程也是由 task_struct 控制的,本文主要讨论进程调动的内容,就不展开说明了,在内存管理章节会介绍)。

在 task_struct 中维护了进程的 PID,进程状态以及堆栈信息等进程的几乎所有信息。

我们常常提到的“僵尸进程”,就是进程的堆栈信息被回收了,但是其 task_struct 却依旧存活在操作系统中。 “僵尸进程”往往是因为父进程处理子进程的销毁信号错误导致的。也是因为 task_struct 留存在操作系统中,所以“僵尸进程”是会消耗操作系统资源的。

与“僵尸进程”常常一起提及的是“孤儿进程”,“孤儿进程”是父进程先于子进程销毁导致的,不过“孤儿进程”会进行“寻找养父”的过程,先找“亲生父亲”的进程组中的兄弟进程,找不到的话就会认“0”号进程为父亲,所以“孤儿进程”的销毁并不会受阻,不会消耗操作系统过多的资源。

2. 进程调度算法

Linux 标准内核有两类调度类:CFS 调度算法的默认调度类和实时调度类。下面我们分类来讨论这两种不同的调度算法。

2.1 完全公平调度算法 CFS

一句话总结,CFS 会根据权重为各进程尽量“公平”的分配时间片。虽然叫做完全公平,但 CFS 算法并不是完全相同的未每个进程分配同样的时间片来运行,导致其不公平的原因有以下两点:

  • CFS 是根据权重为每个进程分配时间片的(体现在其具体实现的红黑树上就是vruntime)。

  • 为了避免进程的过度频繁切换,时间片的分配有一个最小值。

CFS 算法的底层实现是靠维护一棵进程vruntime的红黑树实现的,每一次选取vruntime最小的进程占据CPU。

下面分析一下 CFS 调度程序是如何工作的。假设有两个任务,它们具有相同的友好值。一个任务是 I/O 密集型而另一个为 CPU 密集型。通常,I/O 密集型任务在运行很短时间后就会阻塞以便等待更多的 I/O;而 CPU 密集型任务只要有在处理器上运行的机会,就会用完它的时间片。

因此,I/O 密集型任务的虚拟运行时间最终将会小于 CPU 密集型任务的,从而使得 I/O 密集型任务具有更高的优先级。这时,如果 CPU 密集型任务在运行,而 I/O 密集型任务变得有资格可以运行(如该任务所等待的 I/O 已成为可用),那么 I/O 密集型任务就会抢占 CPU 密集型任务。

2.2 实时进程调度算法

Linux 底层实现了SCHED_FIFO 或 SCHED_RR 两种实时进程调度策略。

  • SCHED_FIFO 先到先服务,所有进程排队,先到的进程先执行,执行完到下一个进程。

  • SCHED_RR 时间片轮转,所有进程排队,但是一次只能领取一定的时间片,如果时间片用完,进程任务还没执行完,就会到队尾排队。

2.3 调度算法的选择

Linux 采用两个单独的优先级范围,一个用于实时任务,另一个用于正常任务。实时任务分配的静态优先级为 0〜99,而正常任务分配的优先级为 100〜139。

这两个值域合并成为一个全局的优先级方案,其中较低数值表明较高的优先级。正常任务,根据它们的友好值,分配一个优先级;这里 -20 的友好值映射到优先级 100,而 +19 的友好值映射到 139。图 1 显示了这个方案。

image.png

3. mm_struct 结构体

对于 Linux 进程而言,使用 mm_struct 结构体来进行堆栈的内存管理,之前我们提到,Linux 中的每个线程都有一个 task_struct 作为PCB,mm_struct 结构体的指针就维护在 task_struct 中。

另外,我们知道,Linux 中的进程与线程都是靠 task_struct 管理的,所以正好可以通过 mm_struct 结构体来共享堆内存。

但是线程的栈是要分开的,所以当 fork() 创建线程时,会在进程栈中开辟一块固定大小的空间(2M)作为线程栈,并维护在线程的 task_struct 中。

4. slab 高速缓存

slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。

5. buddy 伙伴系统

一言以蔽之,伙伴系统就是空闲列表+内存块的合并与拆分机制。

空闲列表是操作系统内维护的几个由空闲内存块连接成的链表,每个列表上的内存块大小都相同,不同的列表都不同,当需要进行内存分配时,会从大于所需内存的链表上找一个内存快分配,以避免外部碎片的产生。当所需的空闲列表中的内存块不够用时,会在大一倍的列表中拿一个内存块拆成两块来使用,被拆开的内存块在空闲之后会合并还原。使用结束的内存块也会还回原空闲列表。

6. 内存置换算法

内存空间不足时会使用页面置换算法将暂时不需要的数据置换到磁盘的虚拟内存上,常用的是 LRU,CLOCK,二次机会法等等。

  • FIFO 将最先进入的数据置换出
  • LRU 将最近最久未使用的数据置换出
  • 二次机会法 改良的时钟算法

7. 分区

Linux 的内存有三个分区,DMA 区提供给外设直接访问,Normal 区是内核空间可以直接访问,用户空间用三级页表的方式来管理。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿