第四章 进程管理
本章重点讨论Linux内核如何在系统中创建、管理以及删除进程。
进程在操作系统中执行特定的任务。而程序是存储在磁盘上包含可执行
机器指令和数据的静态实体。进程或者任务是处于活动状态的计算机程序。
进程是一个随执行过程不断变化的实体。和程序要包含指令和数据一样,
进程也包含程序计数器和所有CPU寄存器的值,同时它的堆栈中存储着如
子程序参数、返回地址以及变量之类的临时数据。当前的执行程序,或者
说进程,包含着当前处理器中的活动状态。Linux是一个多处理操作系统。
进程具有独立的权限与职责。如果系统中某个进程崩溃,它不会影响到其
余的进程。每个进程运行在其各自的虚拟地址空间中,通过核心控制下可
靠的通讯机制,它们之间才能发生联系。
进程在生命期内将使用系统中的资源。它利用系统中的CPU来执行指令,在
物理内存来放置指令和数据。使用文件系统提供的功能打开并使用文件,
同时直接或者间接的使用物理设备。Linux必须跟踪系统中每个进程以及资
源,以便在进程间实现资源的公平分配。如果系统有一个进程独占了大部
分物理内存或者CPU的使用时间,这种情况对系统中的其它进程是不公平的。
系统中最宝贵的资源是CPU,通常系统中只有一个CPU。Linux是一个多处理
操作系统,它最终的目的是:任何时刻系统中的每个CPU上都有任务执行,
从而提高CPU的利用率。如果进程个数多于CPU的个数,则有些进程必须等
待到CPU空闲时才可以运行。多处理是的思路很简单;当进程需要某个系统
资源时它将停止执行并等待到资源可用时才继续运行。单处理系统中,如
DOS,此时CPU将处于空等状态,这个时间将被浪费掉。在多处理系统中,
因为可以同时存在多个进程,所以当某个进程开始等待时,操作系统将把
CPU控制权拿过来并交给其它可以运行的进程。调度器负责选择适当的进程
来运行,Linux使用一些调度策略以保证CPU分配的公平性。
Linux支持多种类型的可执行文件格式,如ELF,JAVA等。由于这些进程必
须使用系统共享库,所以对它们的管理要具有透明性。
4.1 Linux进程
为了让Linux来管理系统中的进程,每个进程用一个task_struct数据结构
来表示(任务与进程在Linux中可以混用)。数组task包含指向系统中所有
task_struct结构的指针。
这意味着系统中的最大进程数目受task数组大小的限制,缺省值一般为512。
创建新进程时,Linux将从系统内存中分配一个task_struct结构并将其加入
task数组。当前运行进程的结构用current指针来指示。
Linux还支持实时进程。这些进程必须对外部时间作出快速反应(这就是
“实时”的意思),系统将区分对待这些进程和其他进程。虽然task_struct
数据结构庞大而复杂,但它可以分成一些功能组成部分:
State 进程在执行过程中会根据环境来改变state。Linux进程有以下状态:
Running
进程处于运行(它是系统的当前进程)或者准备运行状态(它在
等待系统将CPU分配给它)。
Waiting
进程在等待一个事件或者资源。Linux将等待进程分成两类;可
中断与不可中断。可中断等待进程可以被信号中断;不可中断等
待进程直接在硬件条件等待,并且任何情况下都不可中断。
Stopped
进程被停止,通常是通过接收一个信号。正在被调试的进程可能
处于停止状态。
Zombie
这是由于某些原因被终止的进程,但是在task数据中仍然保留
task_struct结构。
它象一个已经死亡的进程。
Scheduling Information
调度器需要这些信息以便判定系统中哪个进程最迫切需要运行。
Identifiers
系统中每个进程都有进程标志。进程标志并不是task数组的索引,它仅
仅是个数字。每个进程还有一个用户与组标志,它们用来控制进程对系
统中文件和设备的存取权限。
Inter-Process Communication
Linux支持经典的Unix
IPC机制,如信号、管道和信号灯以及系统V中IPC机制,包括共享内存、
信号灯和消息队列。我们将在IPC一章中详细讨论Linux中IPC机制。
Links
Linux系统中所有进程都是相互联系的。除了初始化进程外,所有进程
都有一个父进程。新进程不是被创建,而是被复制,或者从以前的进
程克隆而来。每个进程对应的task_struct结构中包含有指向其父进程
和兄弟进程(具有相同父进程的进程)以及子进程的指针。我们可以使
用pstree命令来观察Linux系统中运行进程间的关系:
init(1)-+-crond(98)
|-emacs(387)
|-gpm(146)
|-inetd(110)
|-kerneld(18)
|-kflushd(2)
|-klogd(87)
|-kswapd(3)
|-login(160)---bash(192)---emacs(225)
|-lpd(121)
|-mingetty(161)
|-mingetty(162)
|-mingetty(163)
|-mingetty(164)
|-login(403)---bash(404)---pstree(594)
|-sendmail(134)
|-syslogd(78)
`-update(166)
另外,系统中所有进程都用一个双向链表连接起来,而它们的根是
init进程的task_struct数据结构。这个链表被Linux核心用来寻找
系统中所有进程,它对ps或者kill命令提供了支持。
Times and Timers
核心需要记录进程的创建时间以及在其生命期中消耗的CPU时间。
时钟每跳动一次,核心就要更新保存在jiffies变量中,记录进程
在系统和用户模式下消耗的时间量。Linux支持与进程相关的interval
定时器,进程可以通过系统调用来设定定时器以便在定时器到时后向
它发送信号。这些定时器可以是一次性的或者周期性的。
File system
进程可以自由地打开或关闭文件,进程的task_struct结构中包含一个
指向每个打开文件描叙符的指针以及指向两个VFS inode的指针。每个
VFS inode唯一地标记文件中的一个目录或者文件,同时还对底层文
件系统提供统一的接口。Linux对文件系统的支持将在filesystem一章
中详细描叙。这两个指针,一个指向进程的根目录,另一个指向其当
前或者pwd目录。pwd从Unix命令pwd中派生出来,用来显示当前工作目录.
这两个VFS inode包含一个count域,当多个进程引用它们时,它的值
将增加。这就是为什么你不能删除进程当前目录,或者其子目录的原因。
Virtual memory
多数进程都有一些虚拟内存(核心线程和后台进程没有),Linux核心
必须跟踪虚拟内存与系统物理内存的映射关系。
Processor Specific Context
进程可以认为是系统当前状态的总和。进程运行时,它将使用处理器
的寄存器以及堆栈等等。进程被挂起时,进程的上下文-所有的CPU相
关的状态必须保存在它的task_struct结构中。当调度器重新调度该进
程时,所有上下文被重新设定。
4.2 Identifiers
和其他Unix一样,Linux使用用户和组标志符来检查对系统中文件和可执行
映象的访问权限。Linux系统中所有的文件都有所有者和允许的权限,这些
权限描叙了系统使用者对文件或者目录的使用权。基本的权限是读、写和可
执行,这些权限被分配给三类用户:文件的所有者,属于相同组的进程以及
系统中所有进程。每类用户具有不同的权限,例如一个文件允许其拥有者读
写,但是同组的只能读而其他进程不允许访问。
Linux使用组将文件和目录的访问特权授予一组用户,而不是单个用户或者
系统中所有进程。如可以为某个软件项目中的所有用户创建一个组,并将其
权限设置成只有他们才允许读写项目中的源代码。一个进程可以同时属于多
个组(最多为32个),这些组都被放在进程的task_struct中的group数组中。
只要某组进程可以存取某个文件,则由此组派生出的进程对这个文件有相应
的组访问权限。
task_struct结构中有四对进程和组标志符:
uid, gid
表示运行进程的用户标志符和组标志符。
effective uid and gid
有些程序可以在执行过程中将执行进程的uid和gid改成其程序自身的
uid和gid(保存在描叙可执行映象的VFS inode属性中)。这些程序被
称为setuid程序,常在严格控制对某些服务的访问时使用,特别是那些
为别的进程而运行的进程,例如网络后台进程。有效uid和gid是那些
setuid执行过程在执行时变化出的uid和gid。当进程试图访问特权数
据或代码时,核心将检查进程的有效gid和uid。
file system uid and gid
它们和有效uid和gid相似但用来检验进程的文件系统访问权限。如运行
在用户模式下的NFS服务器存取文件时,NFS文件系统将使用这些标志符。
此例中只有文件系统uid和gid发生了改变(而非有效uid和gid)。这样
可以避免恶意用户向NFS服务器发送KILL信号。
saved uid and gid
POSIX标准中要求实现这两个标志符,它们被那些通过系统调用改变进
程uid和gid的程序使用。当进程的原始uid和gid变化时,它们被用来保
存真正的uid和gid。
4.3 调度
所有进程部分时间运行于用户模式,部分时间运行于系统模式。如何支持这些
模式,底层硬件的实现各不相同,但是存在一种安全机制可以使它们在用户模
式和系统模式之间来回切换。用户模式的权限比系统模式下的小得多。进程通
过系统调用切换到系统模式继续执行。此时核心为进程而执行。在Linux中,
进程不能被抢占。只要能够运行它们就不能被停止。当进程必须等待某个系统
事件时,它才决定释放出CPU。例如进程可能需要从文件中读出字符。一般等待
发生在系统调用过程中,此时进程处于系统模式;处于等待状态的进程将被挂
起而其他的进程被调度管理器选出来执行。
进程常因为执行系统调用而需要等待。由于处于等待状态的进程还可能占用CPU
时间,所以Linux采用了预加载调度策略。在此策略中,每个进程只允许运行很
短的时间:200毫秒,当这个时间用完之后,系统将选择另一个进程来运行,原
来的进程必须等待一段时间以继续运行。这段时间称为时间片。
调度器必须选择最迫切需要运行而且可以执行的进程来执行。
可运行进程是一个只等待CPU资源的进程。Linux使用基于优先级的简单调度算
法来选择下一个运行进程。当选定新进程后,系统必须将当前进程的状态,处
理器中的寄存器以及上下文状态保存到task_struct结构中。同时它将重新设置
新进程的状态并将系统控制权交给此进程。为了将CPU时间合理的分配给系统中
每个可执行进程,调度管理器必须将这些时间信息也保存在task_struct中。
policy
应用到进程上的调度策略。系统中存在两类Linux进程:普通与实时进程。
实时进程的优先级要高于其它进程。如果一个实时进程处于可执行状态,
它将先得到执行。实时进程又有两种策略:时间片轮转和先进先出。在时
间片轮转策略中,每个可执行实时进程轮流执行一个时间片,而先进先出
策略每个可执行进程按各自在运行队列中的顺序执行并且顺序不能变化。
priority
调度管理器分配给进程的优先级。同时也是进程允许运行的时间(jiffies)。
系统调用renice可以改变进程的优先级。
rt_priority
Linux支持实时进程,且它们的优先级要高于非实时进程。调度器使用这
个域给每个实时进程一个相对优先级。同样可以通过系统调用来改变实时
进程的优先级。
counter
进程允许运行的时间(保存在jiffies中)。进程首次运行时为进程优先级的
数值,它随时间变化递减。
核心在几个位置调用调度管理器。如当前进程被放入等待队列后运行或者系统
调用结束时,以及从系统模式返回用户模式时。此时系统时钟将当前进程的counter
值设为0以驱动调度管理器。每次调度管理器运行时将进行下列操作:
kernel work
调度管理器运行底层处理程序并处理调度任务队列。
Current process
当选定其他进程运行之前必须对当前进程进行一些处理。
如果当前进程的调度策略是时间片轮转,则它被放回到运行队列。
如果任务可中断且从上次被调度后接收到了一个信号,则它的状
态变为Running。
如果当前进程超时,则它的状态变为Running。
如果当前进程的状态是Running,则状态保持不变。
那些既不处于Running状态又不是可中断的进程将会从运行队列中
删除。这意味着调度管理器选择运行进程时不会将这些进程考虑在内。
Process selection
调度器在运行队列中选择一个最迫切需要运行的进程。如果运行队
列中存在实时进程(那些具有实时调度策略的进程),则它们比普
通进程更多的优先级权值。普通进程的权值是它的counter值,而实时
进程则是counter加上1000。这表明如果系统中存在可运行的实时进程,
它们将总是在任何普通进程之前运行。如果系统中存在和当前进程相同
优先级的其它进程,这时当前运行进程已经用掉了一些时间片,所以它
将处在不利形势(其counter已经变小);而原来优先级与它相同的进程
的counter值显然比它大,这样位于运行队列中最前面的进程将开始执
行而当前进程被放回到运行队列中。在存在多个相同优先级进程的平衡
系统中,每个进程被依次执行,这就是Round Robin策略。然而由于进
程经常需要等待某些资源,所以它们的运行顺序也常发变化。
Swap processes
如果系统选择其他进程运行,则必须被挂起当前进程且开始执行新进
程。进程执行时将使用寄存器、物理内存以及CPU。每次调用子程序时,
它将参数放在寄存器中并把返回地址放置在堆栈中,所以调度管理器总
是运行在当前进程的上下文。虽然可能在特权模式或者核心模式中,但
是仍然处于当前运行进程中。当挂起进程的执行时,系统的机器状态,
包括程序计数器(PC)和全部的处理器寄存器,必须存储在进程的task_struct
数据结构中。同时加载新进程的机器状态。这个过程与系统类型相关,
不同的CPU使用不同的方法完成这个工作,通常这个操作需要硬件辅助完成。
进程的切换发生在调度管理器运行之后。以前进程保存的上下文与当
前进程加载时的上下文相同,包括进程程序计数器和寄存器内容。
如果以前或者当前进程使用了虚拟内存,则系统必须更新其页表入口,
这与具体体系结构有关。如果处理器使用了转换旁视缓冲或者缓冲了
页表入口(如Alpha AXP),那么必须冲刷以前运行进程的页表入口。
4.3.1 多处理器系统中的调度
在Linux世界中,多CPU系统非常少见。但是Linux上已经做了很多工作来
保证它能运行在SMP(对称多处理)机器上。Linux能够在系统中的CPU间
进行合理的负载平衡调度。这里的负载平衡工作比调度管理器所做的更加
明显。
在多处理器系统中,人们希望每个处理器总处与工作状态。当处理器上的
当前进程用完它的时间片或者等待系统资源时,各个处理器将独立运行调
度管理器。SMP系统中一个值得注意的问题是系统中不止一个idle进程。在
单处理器系统中,idle进程是task数组中的第一个任务,在SMP系统中每个
CPU有一个idle进程,同时每个CPU都有一个当前进程,SMP系统必须跟踪每
个处理器中的idle进程和当前进程。
在SMP系统中,每个进程的task_struct结构中包含着当前运行它的处理器
的编号以及上次运行时处理器的编号。
把进程每次都调度到不同CPU上执行显然毫无意义,Linux可以使用
processor_mask来使得某个进程只在一个或者几个处理器上运行:如果N位
置位,则进程可在处理器N上运行。当调度管理器选择新进程运行时,它
不会考虑一个在其processor_mask中在当前处理器位没有置位的进程。同
时调度管理器将给予上次在此处理器中运行的进程一些优先权,因为将进
程迁移到另外处理器上运行将带来性能的损失。
4.4 文件
给出了两个描叙系统中每个进程所使用的文件系统相关信息。第一个
fs_struct包含了指向进程的VFSinode和其屏蔽码。这个屏蔽码值是创建
新文件时所使用的缺省值,可以通过系统调用来改变。
第二个数据结构files_struct包含了进程当前所使用的所有文件的信息。
程序从标准输入中读取并写入到标准输出中去。任何错误信息将输出到
标准错误输出。这些文件有些可能是真正的文件,有的则是输出/输入终
端或者物理设备,但程序都将它们视为文件。每个文件有一个描叙符,
files_struct最多可以包含256个文件数据结构,它们分别描叙一个被当
前进程使用的文件。f_mode域表示文件将以何种模式创建:只读
、读写还是只写。f_pos中包含下一次文件读写操作开始位置。f_inode指
向描叙此文件的VFS inode,f_ops指向一组可以对此文件进行操作的函数
入口地址指针数组。这些抽象接口十分强大,它们使得Linux能够支持多
种文件类型。在Linux中,管道是用我们下面要讨论的机制实现的。
每当打开一个文件时,位于files_struct中的一个空闲文件指针将被用来
指向这个新的文件结构。Linux进程希望在进程启动时至少有三个文件描
叙符被打开,它们是标准输入,标准输出和标准错误输出,一般进程
会从父进程中继承它们。这些描叙符用来索引进程的fd数组,所以标准输
入,标准输出和标准错误输出分别对应文件描叙符0,1和2.
4.5 虚拟内存
进程的虚拟内存包括可执行代码和多个资源数据。首先加载的是程序映象,
例如ls。ls和所有可执行映象一样,是由可执行代码和数据组成的。此映
象文件包含所有加载可执行代码所需的信息,同时还将程序数据连接进入
进程的虚拟内存空间。然后在执行过程中,进程定位可以使用的虚拟内存,
以包含正在读取的文件内容。新分配的虚拟内存必须连接到进程已存在的
虚拟内存中才能够使用。
最后Linux进程调用通用库过程,比如文件处理子程序。如果每个进程都有
库过程的拷贝,那么共享就变得没有意义。而Linux可以使多个进程同时使
用共享库。来自共享库的代码和数据必须连接进入进程的虚拟地址空间以及
共享此库的其它进程的虚拟地址空间。
任何时候进程都不同时使用包含在其虚拟内存中的所有代码和数据。虽然
它可以加载在特定情况下使用的那些代码,如初始化或者处理特殊事件时,
另外它也使用了共享库的部分子程序。但如果将这些没有或很少使用的代
码和数据全部加载到物理内存中引起极大的浪费。如果系统中多个进程都
浪费这么多资源,则会大大降低的系统效率。Linux使用请求调页技术来把
那些进程需要访问的虚拟内存带入物理内存中。核心将进程页表中这些虚
拟地址标记成存在但不在内存中的状 而无需将所有代码和数据直接调入物
理内存。当进程试图访问这些代码和数据时,系统硬件将产生页面错误并将
控制转移到Linux核心来处理之。这样对于处理器地址空间中的每个虚拟内存
区域,Linux都必须知道这些虚拟内存从何处而来以及如何将其载入内存以
处理页面错误。
Linux核心需要管理所有的虚拟内存地址,每个进程虚拟内存中的内容在
其task_struct结构中指向的vm_area_struct结构中描叙。进程的mm_struct
数据结构也包含了已加载可执行映象的信息和指向进程页表的指针。它还包
含了一个指向vm_area_struct链表的指针,每个指针代表进程内的一个虚拟
内存区域。
此链表按虚拟内存位置来排列由于那些虚拟内存区域来源各不相同,Linux
使用vm_area_struct中指向一组虚拟内存处理过程的指针来抽象此接口。通
过使用这个策略,所有的