Linux进程管理

操作系统的重要任务之一是管理计算机的软、硬件资源。现代操作系统的主要特点在于程序的并发执行,由此引出系统的资源被共享和用户随机使用系统。因而操作系统最核心的概念就是进程:即正在运行的程序。操作系统借助于进程来管理计算机的软、硬件资源,支持多任务的并发。操作系统的其他内容都是围绕进程展开的。所以进程管理是 Linux 操作系统内核的主要内容之一,它对整个操作系统的执行效率至关重要。

Linux 进程管理
原文出处:《操作系统实验指导-基于Linux内核》第2版
原文作者:徐虹 何嘉 张钟澍

Linux 进程管理

进程是一个动态的实体,是一个可并发执行的程序在一个数据集合上的运行过程,它是一个正在执行的程序,是操作系统分片资源的基本单位。进程不仅包含指令和数据,也包含程序计数器和所有 CPU 寄存器的值,同时它的堆栈中存储着如子程序参数、返回地址以及变量之类的临时数据。

Linux 的进程具有独立的权限与职责。如果系统中某个进程崩溃,它不会影响到其余的进程。每个进程运行在其各自的虚拟地址空间中,进程之间发生联系只能通过核心控制下的可靠通信机制来完成。

进程在生命周期内将使用系统中的资源。它利用系统中的 CPU 来执行命令,在物理内存中放置指令和数据,使用文件系统提供的功能打开并使用文件,同时直接或间接地使用物理设备。Linux 必须跟踪系统中每个进程以及资源,以便在进程间实现资源的公平分配。

Linux 支持多种类型的可执行文件格式,如 ELF、Java 等。由于这些进程必须使用系统共享库,所以对它们的管理要具有透明性。

  • 描述进程的数据结构
  • 进程调度
  • 创建进程
  • 进程通信机制

描述进程的数据结构

Linux 用 task_struct 数据结构来表示每个进程,在 Linux 中任务与进程表示的意义是一样的。系统维护一个名为 task 的数组,task 包含指向系统中所有进程的 task_struct 结构的指针。这意味着系统中的最大进程数目受 task 数组大小的限制,默认值一般为 512。创建新进程时,Linux 将从系统内存中分配一个 task_struct 结构并将其加入 task 数组。当前运行进程的结构用 current 指针来指示。

Linux 还支持实时进程。这些进程必须对外部事件作出快速反应,系统将区分对待这些进程和其他进程。虽然 task_struct 数据结构庞大而复杂,但可以归纳为如下几类:

进程的状态信息(state)

进程在执行过程中会根据环境来改变进程的状态,Linux 进程有以下状态。

  • Running

    进程处于运行(它是系统的当前进程)或者准备运行状态(它在等待系统将 CPU 分配给它)。

  • Waiting

    进程在等待一个事件或者资源。Linux 将等待进程分成两类:可中断与不可中断。可中断等待进程可以被信号中断;不可中断等待进程是由于硬件原因而等待,例如进程打开某个设备文件,在得到设备的回应前处于等待状态,因此在任何情况下都不可中断。

  • Stopped

    进程被停止,通常是通过接收一个信号。正在被调试的进程可以处于停止状态。

  • Zombie

    这是因为某些原因而被终止的进程,但是在 task 数组中仍然保留其 task_struct 结构。它像一个已经死亡的进程。

调度信息(scheduling information)

Linux 调度进程所需要的信息,包括进程的类型(普通或实时)和优先级,计数器中记录允许进程执行的时间量。

进程标识信息(identifiers)

系统中每个进程都有进程标志。进程标志并不是 task 数组的索引,它仅仅是个数字。每个进程还有一个用户与组标志,它们用来控制进程对系统中的文件和设备的存取权限。

进程的通信信息(inter-process communication)

Linux 支持经典的 UNIX IPC 机制,如信号、管道和命名管道以及 System V 中 IPC 机制,包括共享内存、信号量和消息队列。

Linux 系统中所有进程都是相互联系的。除了初始化进程外,所有进程都有一个父进程。新进程不是被创建,而是被复制,都是从以前的进程克隆而来。每个进程对应的 task_struct 结构中包含有指向其父进程和兄弟进程(具有相同父进程的进程)以及子进程的指针。

系统中所有进程都用一个双向链表连接起来,而它们的根是 init 进程的 task_struct 数据结构。这个链表被 Linux 核心用来寻找系统中所有进程,它为 ps 或者 kill 命令提供了支持。

时间和定时器信息(times and timers)

核心需要记录进程的创建时间以及在其生命期消耗的 CPU 时间。适中每跳动一次,核心就要更新保存在 jiffies 变量中,记录进程在系统和用户模式下消耗的时间量。Linux 支持与进程相关的 interval 定时器,进程可以通过系统调用来设定定时器以便在定时器到时后向它发送信号。这些定时器可以是一次性的或者周期性的。

有关文件系统的信息(file system)

进程可以自由地打开或关闭文件,进程的 task_struct 结构中包含一个指向每个打开文件描述符的指针以及指向两个 VFS inode 的指针。每个 VFS inode 唯一地标记文件中的一个目录或者文件,同时还对底层文件系统提供统一的接口。这两个指针,一个指向进程的根目录,另一个指向其当前或者 pwd 目录。pwd 从 UNIX 命令 pwd 中派生出来,用来显示当前工作目录。这两个 VFS inode 包含一个 count 域,当多个进程引用它们时,它的值将增加。这就是为什么不能删除进程当前目录或者其子目录的原因。

虚拟内存信息(virtual memory)

多数进程都有一些虚拟内存(核心线程和后台进程没有),Linux 核心必须跟踪虚拟内存与系统物理内存的映射关系。

进程上下文信息(processor specific context)

进程可以认为是系统当前状态的总和。进程运行时,它将使用处理器的寄存器以及堆栈等。进程被挂起时,进程的上下文中所有与 CPU 相关的状态必须保存在它的 task_struct 结构内。当调度器重新调度该进程时,所有上下文被重新设定。

其它信息

Linux 支持 SMP 多 CPU 结构,在 task_struct 中有相应的描述信息。此外还包括资源使用、进程终止信号、描述可执行的文件格式的信息等。

进程调度

Linux 能让多个进程并发执行,由此必然会产生资源争夺的情况,而 CPU 是系统中最重要的资源。进程调度就是进程调度程序按照一定的策略,动态地把 CPU 分配给处于就绪队列中的某一个进程,使之执行。进程调度的目的是使处理机资源得到最高效的利用。进程调度的策略要考虑“高效”、“公平”、“周转时间”、“吞吐量”、“响应时间”等原则,并且在一定的调度时机,通过合适的调度算法来完成进程的调度。

进程调度的时机

在 Linux 中采用的是 非剥夺调度 的机制,进程一旦运行就不能被停止,当前进程必须等待某个系统事件时,它才释放 CPU。例如进程可能需要写数据到某个文件。一般等待发生在系统调用的过程中,此时进程处于系统模式;处于等待状态的进程将被挂起而其他的进程被调度管理器选出来执行。系统为进程设置相应的时间片,当这个时间用完之后,再选择另一个进程来运行。Linux 调度时机有以下几种。

(1)时间片完

(2)进程状态转换

(3)执行设备驱动程序

(4)进程从中断、异常或系统调用返回到用户态

进程调度的功能

(1)允许进程建立自己的新备份

(2)决定哪一个进程将占用 CPU,使得可运行进程之间进行有效的转移

(3)接收中断并把它们发送到合适的内核子系统

(4)发送信号给用户进程

(5)管理定时器硬件

(6)当进程结束后,释放进程所占用的资源

(7)支持动态装入模块,这些模块代表着内核启动以后所增加的内核功能,这种可装入的模块将由虚拟文件系统和网络接口使用。

进程调度的数据结构

Linux 使用基于优先级的简单调度算法来选择下一个运行进程。当选定新进程后,系统必须将当前进程的状态、处理器中的寄存器以及上下文状态保存到 task_struct 结构中。同时它将重新设置新进程的状态并将系统控制权交给此进程。为了将 CPU 时间合理地分配给系统中每个可执行进程,调度管理器必须将这些时间信息也保存在 task_struct 中。

(1)调度策略(policy)

Linux 系统中存在普遍与实时两种进程。实时进程的优先级要高于其他进程。根据调度策略,Linux 将进程分为以下三种类型。

  • SCHED_FIFO:先进先出实时进程。只有当前进程执行完毕再调度下一优先级最高的进程。
  • SCHED_RR:循环实时进程。在此策略下,每个进程执行完一个时间片后,会被挂起,然后选择另一具有相同或更高优先级的进程执行。
  • SCHED_OTHER:普通进程。

(2)优先级(priority)

调度管理器分配给进程的优先级,同时也是进程允许运行的时间(jiffies)。系统调度 renice 可以改变进程的优先级。

(3)实时进程的优先级(rt_priority)

Linux 支持实时进程,且它们的优先级要高于非实时进程。调度器使用这个域给每个实时进程一个相对优先级。同样可以通过系统调用来改变实时进程的优先级。

(4)当前执行进程剩余的时间(counter)

进程首次运行时为进程优先级的数值,它随时间变化递减。普通进程的 counter 值是其优先级权值,而实时进程的则是 counter 加上 1000。

(5)当前进程(current process)

当调度其他进程占用 CPU 时,根据调度策略对当前进程进行一些处理,修改其状态,并插入相应的队列。

进程调度的依据

调度程序运行时,要在所有处于可运行状态的进程中选择最值得运行的进程投入运行。上面所介绍的 policy、priority、counter 和 rt_priority 4 项是调度程序选择的依据。

Linux 操作系统用函数 goodness() 来衡量一个处于可运行状态的进程值得运行的程度。该函数综合了上面4项依据,给每个处于可运行状态的进程赋予一个权值(weight),调度程序以这个权值作为选择进程的唯一依据。函数流程如下:

goodnees-flowchart

进程队列的组织方式

(1)可运行队列(runnable queue)

操作系统中所有处于可运行状态的进程链成一个队列,该队列就称作可运行队列。调度程序直接的操作对象就是可运行队列。

可运行队列容纳了系统中所有可运行进程,它是一个双向循环队列,其结构如图所示:

process-runnable-queue

该队列通过 task_struct 结构中的两个指针 next_run 和 prev_run 来维持。队列的标志有两个:一个是空进程 idle_task 即 task[0],另一个是队列的长度(即系统中处于可运行状态的进程数目,用全局整形变量 nr_running 表示)。

(2)pidhash 表及链表 pidhash

为了快速根据进程的 pid 找到该进程的 task_struct 结构,系统使用链式结构的 Hash 表对进程的 task_struct 结构进行管理,Hash 表的默认长度为 128。

1
2
3
# define PIDHASH_SZ (NR_TASKS>>2)
struct task_struct *pidhash[PIDHASH_SZ];
# define pidhash(x) (((x)>>8)^(x))&(PIDHASH_SZ-1)

新创建进程时,系统根据其 pid 值将其结构插入到 pidhash 表中,具有相同 hash 值的进程构成一个双向链表。

(3)空闲 task_struct 双向循环链表 tarray_freelist

Linux 操作系统将所有的空闲 task_struct 通过双向循环链表进行链接,方便对新创建的进程快速分配 task_struct 结构,以及对撤销的进程回收 task_struct 结构。

1
struct task_struct **tarray_freelist;

(4)等待队列

进程经常要等待某个系统资源。例如某个进程可能需要描述文件系统中某个目录的 VFS inode,但是此 inode 可能不在 buffer cache 中。此时这个进程必须等到该 inode 从包含次文件系统的物理介质中取出来才可以继续运行。Linux 核心使用一个非常简单的队列:等待队列。它包含一个指向进程 task_struct 结构的指针以及等待队列中下一个元素的指针。加入到等待队列中的进程既可以是可中断也可以是不可中断的。

进程调度的工作流程

进程调度的工作流程比较简单:遍历可运行队列,从中选择一个权值最大的进程;如果可运行队列中所有进程的时间片都用完了,则要给系统中所有进程的时间片重新赋值。Linux 操作系统中的调度程序比较简单,它可以分为如下 5 个部分。

  • 第一部分:看是否有中断在运行。当中断运行时,是不允许调度程序执行的。
  • 第二部分:处理内核例程。
  • 第三部分:对当前进程做相关处理,为选择下一个进程做好准备。
  • 第四部分:选择下一个可运行进程,即进程调度。
  • 第五部分:进程切换,使 current 指向选定的进程,并建立新进程的运行环境。

创建进程

Linux 启动后经过一系列的初始化操作,系统由 init() 函数创建系统的第一个进程 init,其标志符为 1。init 进程将完成系统的一些初始化设置任务(如打开系统控制台、安装根文件系统及启动系统的守护进程等),以及执行系统初始化程序,如 /etc/init,/bin/init 或者 /sbin/init。init 进程使用 /etc/inittab 作为脚本文件来创建系统中的新进程。这些新进程又创建各自的新进程。例如 getty 进程将在用户试图登录时创建一个 login 进程。系统中所有进程都是从 init 核心进程中派生出来。

新进程通过复制老进程或当前进程来创建。系统调用 fork 或 clone 可以创建新任务,复制发生在核心状态下的核心中。系统从物理内存中分配出来一个新的 task_struct 数据结构,同时还有一个或多个包含被复制进程堆栈(用户与核心)的物理页面。然后创建唯一的标记此新任务的进程标志符。新创建的 task_struct 将被放入 task 数组中,另外将被复制进程的 task_struct 中的内容页表拷入新的 task_struct 中。

复制完成后,Linux 允许两个进程共享资源而不是复制各自的备份。这些资源包括文件、信号处理过程和虚拟内存。 进程对共享资源用各自的 count 来记数。在两个进程对资源的使用完毕之前,Linux 绝不会释放此资源。

进程间通信机制

进程间通信机制