深入分析Linux内核源代码4-进程描述(1)
每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!

————零声学院

操作系统中最核心的概念是进程,本章将对进程进行全面的描述。首先从程序的角度引
入进程,接着对 Linux 中的进程进行了概要描述,在此基础上,对 Linux 中核心数据结构
task_struct 进行了较全面的介绍。另外,详细描述了内核对进程的 4 种组织方式,最后介
绍了系统中一种特殊的进程—内核线程。

4.1 进程和程序(Process and Program)

首先我们对进程作一明确定义:所谓进程是由正文段(Text)、用户数据段(User Segment)
以及系统数据段(System Segment)共同组成的一个执行环境。

程序只是一个普通文件,是一个机器代码指令和数据的集合,这些指令和数据存储在磁
盘上的一个可执行映像(Executable Image)中,所以,程序是一个静态的实体。这里,对
可执行映像做进一步解释,可执行映像就是一个可执行文件的内容,例如,你编写了一个 C
源程序,最终这个源程序要经过编译、连接成为一个可执行文件后才能运行。源程序中你要
定义许多变量,在可执行文件中,这些变量就组成了数据段的一部分;源程序中的许多语句,
例如“ i++; for(i=0; i<10; i++); ”等,在可执行文件中,它们对应着许多不同的机器
代码指令,这些机器代码指令经 CPU 执行,就完成了你所期望的工作。可以这么说:程序代
表你期望完成某工作的计划和步骤,它还浮在纸面上,等待具体实现。而具体的实现过程就
是由进程来完成的,进程可以认为是运行中的程序,它除了包含程序中的所有内容外,还包
含一些额外的数据。程序和进程的组成如图 4.1 所示。
深入分析Linux内核源代码4-进程描述(1)
程序装入内存后就可以运行了:在指令指针寄存器的控制下,不断地将指令取至 CPU 运
行。这些指令控制的对象不外乎各种存储器(内存、外存和各种 CPU 寄存器等),这些存储
器中保存有待运行的指令和待处理的数据,当然,指令只有到 CPU 才能发挥其作用。可见,
在计算机内部,程序的执行过程实际上就是一个执行环境的总和,这个执行环境包括程序中
各种指令和数据,还有一些额外数据,比如说寄存器的值、用来保存临时数据(例如传递给
某个函数的参数、函数的返回地址、保存变量等)的堆栈(包括程序堆栈和系统堆栈)、被
打开文件的数量及输入输出设备的状态等。这个执行环境的动态变化表征程序的运行。我们
就把这个环境称作“进程”,它代表程序的执行过程,是一个动态的实体,它随着程序中指
令的执行而不断地变化。在某个特定时刻的进程的内容被称为进程映像(Process Image)。
Linux 是一个多任务操作系统,也就是说,可以有多个程序同时装入内存并运行,操作
系统为每个程序建立一个运行环境即创建进程,每个进程拥有自己的虚拟地址空间,它们之
间互不干扰,即使要相互作用(例如多个进程合作完成某个工作),也要通过内核提供的进
程间通信机制(IPC)。Linux 内核支持多个进程虚拟地并发执行,这是通过不断地保存和切
换程序的运行环境而实现的,选择哪个进程运行是由调度程序决定的。注意,在一些 UNIX
书籍中,又把“进程切换”(Process Switching)称为“环境切换”或“上下文切换”(Context
Switching),这些术语表达的意思是相同的。

进程运行过程中,还需要其他的一些系统资源,例如,要用 CPU 来运行它的指令、要用
系统的物理内存来容纳进程本身和它的有关数据、要在文件系统中打开和使用文件、并且可
能直接或间接的使用系统的物理设备,例如打印机、扫描仪等。由于这些系统资源是由所有
进程共享的,所以 Linux 必须监视进程和它所拥有的系统资源,使它们们可以公平地拥有系
统资源以得到运行。

系统中最宝贵的资源是 CPU,通常来说,系统中只有一个 CPU,当然也可以有多个 CPU
(Linux 支持 SMP___对称多处理机)。Linux 作为多任务操作系统,其目的就是让 CPU 上一直
都有进程在运行,以最大限度地利用这一宝贵资源。通常情况下,进程数目是多于 CPU 数目
的,这样其他进程必须等待 CPU 这一资源。操作系统通过调度程序来选择下一个最应该运行
的进程,并使用一系列的调度策略来确保公平和高效。

进程是一个动态实体,如图 4.1 所示,进程实体由 3 个独立的部分组成。

(1)正文段(Text):存放被执行的机器指令。这个段是只读的(所以,在这里不能写
自己能修改的代码),它允许系统中正在运行的两个或多个进程之间能够共享这一代码。例
如,有几个用户都在使用文本编辑器,在内存中仅需要该程序指令的一个副本,他们全都共
享这一副本。

(2)用户数据段(User Segment):存放进程在执行时直接进行操作的所有数据,包括
进程使用的全部变量在内。显然,这里包含的信息可以被改变。虽然进程之间可以共享正文
段,但是每个进程需要有它自己的专用用户数据段。例如同时编辑文本的用户,虽然运行着
同样的程序编辑器,但是每个用户都有不同的数据:正在编辑的文本。

(3)系统数据段(System Segment):该段有效地存放程序运行的环境。事实上,这正
是程序和进程的区别所在。如前所述,程序是由一组指令和数据组成的静态事物,它们是进
程最初使用的正文段和用户数据段。作为动态事物,进程是正文段、用户数据段和系统数据
段的信息的交叉综合体,其中系统数据段是进程实体最重要的一部分,之所以说它有效地存
放程序运行的环境,是因为这一部分存放有进程的控制信息。系统中有许多进程,操作系统
要管理它们、调度它们运行,就是通过这些控制信息。Linux 为每个进程建立了 task_struct
数据结构来容纳这些控制信息。

总之,进程是一个程序完整的执行环境。该环境是由正文段、用户数据段、系统数据段
的信息交织在一起组成的。

4.2 Linux 中的进程概述

Linux 中的每个进程由一个 task_struct 数据结构来描述,在 Linux 中,任务(Task)
和进程(Process)是两个相同的术语,task_struct 其实就是通常所说的“进程控制块”即
PCB。task_struct 容纳了一个进程的所有信息,是系统对进程进行控制的唯一手段,也是最
有效的手段。

在 Linux 2.4 中,Linux 为每个新创建的进程动态地分配一个 task_struct 结构。系统
所允许的最大进程数是由机器所拥有的物理内存的大小决定的,例如,在 IA32 的体系结构中,
一个 512MB 内存的机器,其最大进程数可以达到 32KB,这是对旧内核(2.2 以前)版本的极
大改进①。

Linux 支持多处理机(SMP),所以系统中允许有多个 CPU。Linux 作为多处理机操作系
统时,系统中允许的最大 CPU 个数为 32。很显然,Linux 作为单机操作系统时,系统中只有
一个 CPU,本书主要讨论单处理机的情况。

和其他操作系统类似,Linux 也支持两种进程:普通进程和实时进程。实时进程具有一
定程度上的紧迫性,要求对外部事件做出非常快的响应;而普通进程则没有这种限制。所以,
调度程序要区分对待这两种进程,通常,实时进程要比普通进程优先运行。这两种进程的区
分也反映在 task_struct 数据结构中了。

总之,包含进程所有信息的 task_struct 数据结构是比较庞大的,但是该数据结构本身
并不复杂,我们将它的所有域按其功能可做如下划分:
• 进程状态(State);
• 进程调度信息(Scheduling Information);
• 各种标识符(Identifiers);
• 进程通信有关信息(IPC,Inter_Process Communication);
• 时间和定时器信息(Times and Timers);
• 进程链接信息(Links);
• 文件系统信息(File System);
• 虚拟内存信息(Virtual Memory);
• 页面管理信息(page);
• 对称多处理器(SMP)信息;
• 和处理器相关的环境(上下文)信息(Processor Specific Context);
• 其他信息。
下面我们对 task_struct 结构进行具体描述。

4.3 task_struct 结构描述

1.进程状态(State)
进程执行时,它会根据具体情况改变状态。进程状态是调度和对换的依据。Linux 中的
进程主要有如下状态,如表 4.1 所示
深入分析Linux内核源代码4-进程描述(1)
(1)可运行状态
处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程
(由 current 所指向的进程),而准备运行的进程只要得到 CPU 就可以立即投入运行,CPU 是
这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可
运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度
的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current
总是指向运行队列中的某个元素,只是具体指向谁由调度程序决定。
(2)等待状态
处于该状态的进程正在等待某个事件(Event)或某个资源,它肯定位于系统中的某个
等待队列(wait_queue)中。Linux 中处于等待状态的进程分为两种:可中断的等待状态和
不可中断的等待状态。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就
从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态
的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被
打断,只能用特定的方式来唤醒它,例如唤醒函数 wake_up()等。
(3)暂停状态
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到 SIGSTOP、SIGTSTP、
SIGTTIN 或 SIGTTOU 信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
(4)僵死状态
进程虽然已经终止,但由于某种原因,父进程还没有执行 wait()系统调用,终止进程的
信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的
垃圾,必须进行相应处理以释放其占用的资源。

2.进程调度信息
调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证
系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进
程的优先级等,如表 4.2 所示。
深入分析Linux内核源代码4-进程描述(1)
在下一章的进程调度中我们会看到,当 need_resched 被设置时,在“下一次的调度机
会”就调用调度程序 schedule()counter 代表进程剩余的时间片,是进程调度的主要依据,
也可以说是进程的动态优先级,因为这个值在不断地减少;nice 是进程的静态优先级,同时
也代表进程的时间片,用于对 counter 赋值,可以用 nice()系统调用改变这个值;policy
是适用于该进程的调度策略,实时进程和普通进程的调度策略是不同的;rt_priority 只对
实时进程有意义,它是实时进程调度的依据。
进程的调度策略有 3 种,如表 4.3 所示。
深入分析Linux内核源代码4-进程描述(1)
只有 root 用户能通过 sched_setscheduler()系统调用来改变调度策略。
3.标识符(Identifiers)
每个进程有进程标识符、用户标识符、组标识符,如表 4.4 所示。
不管对内核还是普通用户来说,怎么用一种简单的方式识别不同的进程呢?这就引入了
进程标识符(PID,process identifier),每个进程都有一个唯一的标识符,内核通过这个
标识符来识别不同的进程,同时,进程标识符 PID 也是内核提供给用户程序的接口,用户程
序通过 PID 对进程发号施令。PID 是 32 位的无符号整数,它被顺序编号:新创建进程的 PID
通常是前一个进程的 PID 加 1。然而,为了与 16 位硬件平台的传统 Linux 系统保持兼容,在
Linux 上允许的最大 PID 号是 32767,当内核在系统中创建第 32768 个进程时,就必须重新开
始使用已闲置的 PID 号。
深入分析Linux内核源代码4-进程描述(1)
另外,每个进程都属于某个用户组。task_struct 结构中定义有用户标识符和组标识符。
它们同样是简单的数字,这两种标识符用于系统的安全控制。系统通过这两种标识符控制进
程对系统中文件和设备的访问,其他几个标识符将在文件系统中讨论。

4.进程通信有关信息(IPC,Inter_Process Communication)
为了使进程能在同一项任务上协调工作,进程之间必须能进行通信即交流数据。
Linux 支持多种不同形式的通信机制。它支持典型的 UNIX 通信机制(IPC Mechanisms):
信号(Signals)、管道(Pipes),也支持 System V 通信机制:共享内存(Shared Memory)、
信号量和消息队列(Message Queues),如表 4.5 所示。
深入分析Linux内核源代码4-进程描述(1)
这些域的具体含义将在进程通信一章进行讨论。
5.进程链接信息(Links)
程序创建的进程具有父/子关系。因为一个进程能创建几个子进程,而子进程之间有兄
弟关系,在 task_struct 结构中有几个域来表示这种关系。
在 Linux 系统中,除了初始化进程 init,其他进程都有一个父进程(Parent Process)
或称为双亲进程。可以通过 fork()或 clone()系统调用来创建子进程,除了进程标识符(PID)
等必要的信息外,子进程的 task_struct 结构中的绝大部分的信息都是从父进程中拷贝,或
说“克隆”过来的。系统有必要记录这种“亲属”关系,使进程之间的协作更加方便,例如
父进程给子进程发送杀死(kill)信号、父子进程通信等,就可以用这种关系很方便地实现。
每个进程的 task_struct 结构有许多指针,通过这些指针,系统中所有进程的
task_struct结构就构成了一棵进程树,这棵进程树的根就是初始化进程 init的 task_struct
结构(init 进程是 Linux 内核建立起来后人为创建的一个进程,是所有进程的祖先进程)。
表 4.6 是进程所有的链接信息。
深入分析Linux内核源代码4-进程描述(1)
6.时间和定时器信息(Times and Timers)
一个进程从创建到终止叫做该进程的生存期(lifetime)。进程在其生存期内使用 CPU
的时间,内核都要进行记录,以便进行统计、计费等有关操作。进程耗费 CPU 的时间由两部
分组成:一是在用户模式(或称为用户态)下耗费的时间、一是在系统模式(或称为系统态)
下耗费的时间。每个时钟滴答,也就是每个时钟中断,内核都要更新当前进程耗费 CPU 的时
间信息。

“时间”对操作系统是极其重要的 。读者可能了解计算机时间的有关知识,例如
8353/8254 这些物理器件,INT 08、INT 1C 等时钟中断等,可能有过编程序时截获时钟中断
的成就感,不管怎样,下一章我们将用较大的篇幅尽可能向读者解释清楚操作系统怎样建立
完整的时间机制、并在这种机制的激励下进行调度等活动。
建立了“时间”的概念,“定时”就是轻而易举的了,无非是判断系统时间是否到达某
个时刻,然后执行相关的操作而已。Linux 提供了许多种定时方式,用户可以灵活使用这些
方式来为自己的程序定时。
表 4.7 是和时间有关的域,上面所说的 counter 是指进程剩余的 CPU 时间片,也和时间
有关,所以这里我们再次提及它。表 4.8 是进程的所有定时器。
深入分析Linux内核源代码4-进程描述(1)
进程有 3 种类型的定时器:实时定时器、虚拟定时器和概况定时器。这 3 种定时器的特
征共有 3 个:到期时间、定时间隔和要触发的事件。到期时间就是定时器到什么时候完成定
时操作,从而触发相应的事件;定时间隔就是两次定时操作的时间间隔,它决定了定时操作
是否继续进行,如果定时间隔大于 0,则在定时器到期时,该定时器的到期时间被重新赋值,
使定时操作继续进行下去,直到进程结束或停止使用定时器,只不过对不同的定时器,到期
时间的重新赋值操作是不同的。在表 4.8 中,每个定时器都有两个域来表示到期时间和定时
间隔:value 和 incr,二者的单位都是时钟滴答,和 jiffies 的单位是一致的,Linux 所有
的时间应用都建立在 jiffies 之上。虚拟定时器和概况定时器到期时由内核发送相应的信号,
而实时定时器比较特殊,它由内核机制提供支持,我们将在后面讨论这个问题。

每个时钟中断,当前进程所有和时间有关的信息都要更新:当前进程耗费的 CPU 时间要
更新,以便于最后的计费;时间片计数器 counter 要更新,如果 counter<=0,则要执行调度
程序;进程申请的延时要更新,如果延时时间到了,则唤醒该进程;所有的定时器都要更新,
Linux 内核检测这些定时器是否到期,如果到期,则执行相应的操作。在这里,“更新”的
具体操作是不同的:对 counter,内核要对它减值,而对于所有的定时器,就是检测它的值,
内核把系统当前时间和其到期时间作一比较,如果到期时间小于系统时间,则表示该定时器
到期。但为了方便,我们把这些操作一概称为“更新”,请读者注意。

请特别注意上面 3 个定时器的更新时间。实时定时器不管其所属的进程是否运行都要更
新,所以,时钟中断来临时,系统中所有进程的实时定时器都被更新,如果有多个进程的实
时定时器到期,则内核要一一处理这些定时器所触发的事件。而虚拟定时器和概况定时器只
在进程运行时更新,所以,时钟中断来临时,只有当前进程的概况定时器得到更新,如果当
前进程运行于用户态,则其虚拟定时器也得到更新。
此外,Linux 内核对这 3 种定时器的处理是不同的,虚拟定时器和概况定时器到期时,
内核向当前进程发送相应的信号:SIGVTALRM 、SIGPROF ;而实时定时器要执行的操作由
real_timer 决定, real_time 是 timer_list 类型的变量(定义: struct timer_list
real_timer),其中容纳了实时定时器的到期时间、定时间隔等信息,我们将在下一章详细
讨论这些内容。

7.文件系统信息(File System)
进程可以打开或关闭文件,文件属于系统资源,Linux 内核要对进程使用文件的情况进
行记录。task_struct 结构中有两个数据结构用于描述进程与文件相关的信息。其中,
fs_struct 中描述了两个 VFS 索引节点(VFS inode),这两个索引节点叫做 root 和 pwd,分
别指向进程的可执行映像所对应的根目录(Home Directory)和当前目录或工作目录。
file_struct 结构用来记录了进程打开的文件的描述符(Descriptor)。如表 4.9 所示。
深入分析Linux内核源代码4-进程描述(1)
在文件系统中,每个 VFS 索引节点唯一描述一个文件或目录,同时该节点也是向更低层
的文件系统提供的统一的接口。
8.虚拟内存信息(Virtual Memory)
除了内核线程(Kernel Thread),每个进程都拥有自己的地址空间(也叫虚拟空间),
用 mm_struct 来描述。另外 Linux 2.4 还引入了另外一个域 active_mm,这是为内核线程而
引入的。因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文
切换方式,当内核线程进行上下文切换时,让切换进来的线程的 active_mm 指向刚被调度出
去的进程的 active_mm(如果进程的 mm 域不为空,则其 active_mm 域与 mm 域相同)。内存
信息如表 4.10 所示。
在文件系统中,每个 VFS 索引节点唯一描述一个文件或目录,同时该节点也是向更低层
的文件系统提供的统一的接口。
深入分析Linux内核源代码4-进程描述(1)
9.页面管理信息
当物理内存不足时,Linux 内存管理子系统需要把内存中的部分页面交换到外存,其交
换是以页为单位的。有关页面的描述信息如表 4.11。
深入分析Linux内核源代码4-进程描述(1)
10.对称多处理机(SMP)信息
Linux 2.4 对 SMP 进行了全面的支持,表 4.12 是与多处理机相关的几个域。
深入分析Linux内核源代码4-进程描述(1)
11.和处理器相关的环境(上下文)信息(Processor Specific Context)
这里要特别注意标题:和“处理器”相关的环境信息。进程作为一个执行环境的综合,
当系统调度某个进程执行,即为该进程建立完整的环境时,处理器(Processor)的寄存器、
堆栈等是必不可少的。因为不同的处理器对内部寄存器和堆栈的定义不尽相同,所以叫做“和
处理器相关的环境”,也叫做“处理机状态”。当进程暂时停止运行时,处理机状态必须保
存在进程的 task_struct 结构中,当进程被调度重新运行时再从中恢复这些环境,也就是恢
复这些寄存器和堆栈的值。处理机信息如表 4.13 所示。
深入分析Linux内核源代码4-进程描述(1)
12.其他
(1)struct wait_queue *wait_chldexit
在进程结束时,或发出系统调用 wait4 时,为了等待子进程的结束,而将自己(父进程)
睡眠在该等待队列上,设置状态标志为 TASK_INTERRUPTIBLE,并且把控制权转给调度程序。
(2)Struct rlimit rlim[RLIM_NLIMITS]
每一个进程可以通过系统调用 setlimit 和 getlimit 来限制它资源的使用。
(3)Int exit_code exit_signal
程序的返回代码以及程序异常终止产生的信号,这些数据由父进程(子进程完成后)轮
流查询。
(4)Char comm[16]
这个域存储进程执行的程序的名字,这个名字用在调试中。
(5)Unsigned long personality
Linux 可以运行 X86 平台上其他 UNIX 操作系统生成的符合 iBCS2 标准的程序,
personality 进一步描述进程执行的程序属于何种 UNIX 平台的“个性”信息。通常有
PER_Linux,PER_Linux_32BIT,PER_Linux_EM86,PER_SVR4,PER_SVR3,PER_SCOSVR3,PER_WYSEV
386,PER_ISCR4,PER_BSD,PER_XENIX 和 PER_MASK 等,参见 include/Linux/personality.h>。
(6) int did_exec:1
按 POSIX 要求设计的布尔量,区分进程正在执行老程序代码,还是用系统调用 execve
()装入一个新的程序。
(7)struct linux_binfmt *binfmt
指向进程所属的全局执行文件格式结构,共有 a.out、script、elf、java 等 4 种。
综上所述,我们对进程的 task_struct 结构进行了归类讨论,还有一些域没有涉及到,
在第六章进程的创建与执行一节几乎涉及到所有的域,在那里可以对很多域有更深入一步的
理解。task_struct 结构是进程实体的核心,Linux 内核通过该结构来控制进程:首先通过其
中的调度信息决定该进程是否运行;当该进程运行时,根据其中保存的处理机状态信息来恢
复进程运行现场,然后根据虚拟内存信息,找到程序的正文和数据;通过其中的通信信息和
其他进程实现同步、通信等合作。几乎所有的操作都要依赖该结构,所以,task_struct 结
构是一个进程存在的唯一标志。

每日分享15分钟技术摘要选读,关注一波,一起保持学习动力!

深入分析Linux内核源代码4-进程描述(1)

相关文章: