第 5 章 进程管理及进程间通讯

131
5 5 第 第第第第第第第第第第 第 第第第第第第第第第第 第第第第第 第第第第第 Linux Linux 第第第第第 第第第第 第第第第第 第第第第 Linux Linux 第 第第第 第 第第第 第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第第第第第 ,。 第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第第第第第 ,。 Linux Linux 第第第 第第第第第第第第第 第 一, 第第第 第第第第第第第第第 第 一, Linux Linux 第第第第第第第第第 第第第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第 第第第第第第第第第第第第 第第第第第第第第第第第第第第第第第第第第第第第 第第第第第第第第第第第第 第第第第第第第第第第

Upload: dannon

Post on 13-Jan-2016

131 views

Category:

Documents


4 download

DESCRIPTION

第 5 章 进程管理及进程间通讯. 本章介绍了 Linux 进程的管理、调度以及 Linux 系统支持的进程间通讯机制,并对某些通信手段的内部实现机制进行了分析。本章还讨论了 Linux 核心的一些基本任务和机制,将 Linux 内核中为使内核其他部分能有效工作的用于同步的几种机制集中起来分析,强调了它们之间在实现和使用上的不同。. 5.1 Linux 进程和线程. - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: 第 5 章 进程管理及进程间通讯

第第 55 章 进程管理及进程间通讯章 进程管理及进程间通讯

本章介绍了本章介绍了 Linux Linux 进程的管理、调度以及 进程的管理、调度以及 Linux Linux 系统支持的进程间通讯机制,并对某些通信手段系统支持的进程间通讯机制,并对某些通信手段的内部实现机制进行了分析。本章还讨论了 的内部实现机制进行了分析。本章还讨论了 LinuLinux x 核心的一些基本任务和机制,将核心的一些基本任务和机制,将 LinuxLinux 内核中内核中为使内核其他部分能有效工作的用于同步的几种为使内核其他部分能有效工作的用于同步的几种机制集中起来分析,强调了它们之间在实现和使机制集中起来分析,强调了它们之间在实现和使用上的不同。用上的不同。

Page 2: 第 5 章 进程管理及进程间通讯

5.1 Linux 5.1 Linux 进程和线程进程和线程

一个大型的应用系统,往往需要众多进程协作。进程是一个大型的应用系统,往往需要众多进程协作。进程是操作系统理论的核心与基础,许多概念都和进程相关。进操作系统理论的核心与基础,许多概念都和进程相关。进程的标准定义是:进程是可并发执行的程序在一个数据集程的标准定义是:进程是可并发执行的程序在一个数据集合上的运行过程。换句话说,在自身的虚拟地址空间运行合上的运行过程。换句话说,在自身的虚拟地址空间运行的一个单独的程序称作一个进程。在的一个单独的程序称作一个进程。在 LinuxLinux 系统中,当一系统中,当一个程序开始执行后,在开始执行到执行完毕退出这段时间个程序开始执行后,在开始执行到执行完毕退出这段时间里,它在内存中的部分就被称作一个进程。进程与程序是里,它在内存中的部分就被称作一个进程。进程与程序是有区别的,程序只是一些预先设定好的代码和数据,进程有区别的,程序只是一些预先设定好的代码和数据,进程是一个随时都可能发生变化的、动态的、使用系统运行资是一个随时都可能发生变化的、动态的、使用系统运行资源的程序。程序是静态的,而进程是动态的。一个程序可源的程序。程序是静态的,而进程是动态的。一个程序可以启动多个进程。和进程联系在一起的不仅有进程的指令以启动多个进程。和进程联系在一起的不仅有进程的指令和数据,而且还有当前的指令指针、所有的 和数据,而且还有当前的指令指针、所有的 CPU CPU 寄存器寄存器以及用来保存临时数据的堆栈等,所有这些都随着程序指以及用来保存临时数据的堆栈等,所有这些都随着程序指令的执行在变化。 令的执行在变化。

Page 3: 第 5 章 进程管理及进程间通讯

LinuxLinux 操作系统包括三种不同类型的进程,每种类型的进程操作系统包括三种不同类型的进程,每种类型的进程都有自己的特点和属性。都有自己的特点和属性。

(1) (1) 交互进程——由交互进程——由 shellshell 启动的进程。交互进程既启动的进程。交互进程既可以在前台运行,也可以在后台运行。 可以在前台运行,也可以在后台运行。

(2) (2) 批处理进程——这种进程和终端没有联系,是批处理进程——这种进程和终端没有联系,是一个进程序列。 一个进程序列。

(3) (3) 监控进程(也称守护进程)——监控进程(也称守护进程)—— LinuxLinux 系统启系统启动时启动的进程,并在后台运行。 动时启动的进程,并在后台运行。

上述三种进程各有各的作用,使用场合也有所不同。上述三种进程各有各的作用,使用场合也有所不同。

Page 4: 第 5 章 进程管理及进程间通讯

5.1.1 Linux 5.1.1 Linux 进程管理的数据结构进程管理的数据结构

LinuxLinux 是一个多任务的操作系统,在同一个时是一个多任务的操作系统,在同一个时间内,可以有多个进程同时执行。由于单间内,可以有多个进程同时执行。由于单 CPUCPU 计计算机实际上在一个时间片断内只能执行一条指令,算机实际上在一个时间片断内只能执行一条指令,LinuxLinux 使用了一种称为“进程调度(使用了一种称为“进程调度( process schprocess schedulingeduling )”的机制。首先为每个进程指派一定)”的机制。首先为每个进程指派一定的运行时间,然后依照某种规则,从众多进程中的运行时间,然后依照某种规则,从众多进程中挑选一个投入运行,其他的进程暂时等待,当正挑选一个投入运行,其他的进程暂时等待,当正在运行的那个进程时间耗尽,或执行完毕退出,在运行的那个进程时间耗尽,或执行完毕退出,或因某种原因暂停,或因某种原因暂停, LinuxLinux 就会重新进行调度,就会重新进行调度,挑选下一个进程投入运行。因为每个进程占用的挑选下一个进程投入运行。因为每个进程占用的时间片都很短,在用户的角度看,就好像多个进时间片都很短,在用户的角度看,就好像多个进程同时运行一样。程同时运行一样。

Page 5: 第 5 章 进程管理及进程间通讯

进程在运行过程中,要使用许多计算机资源,例如 进程在运行过程中,要使用许多计算机资源,例如 CPCPUU 、内存、文件等。同时可能会有多个进程使用同一个资、内存、文件等。同时可能会有多个进程使用同一个资源,因此操作系统要跟踪所有的进程及其所使用的系统资源,因此操作系统要跟踪所有的进程及其所使用的系统资源,以便能够管理进程和资源。源,以便能够管理进程和资源。

在在 LinuxLinux 中,每个进程在创建时都会被分配一个数据结中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(构,称为进程控制块( Process Control BlockProcess Control Block,, PCBPCB)。)。PCBPCB中包含了很多重要的信息,供系统调度和进程本身执中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的是进程行使用,其中最重要的是进程 IDID(( process IDprocess ID,, PIDPID),),进程进程 IDID也被称作进程标识符,是一个非负的整数,在也被称作进程标识符,是一个非负的整数,在 LinLinuxux 操作系统中唯一地标志一个进程。在最常使用的操作系统中唯一地标志一个进程。在最常使用的 i386i386架构(即架构(即 PCPC 使用的架构)上,使用的架构)上, PIDPID的变化范围是一个非的变化范围是一个非负整数负整数 0-327670-32767,这也是所有可能取到的进程,这也是所有可能取到的进程 IDID。每个。每个进程的进程进程的进程 IDID各不相同。可使用各不相同。可使用 psps命令看看当前系统中命令看看当前系统中有多少进程在运行。除标题外,每一行都代表一个进程。有多少进程在运行。除标题外,每一行都代表一个进程。在各列中,在各列中, PIDPID一列代表了各进程的进程一列代表了各进程的进程 IDID,, commancommandd 一列代表了进程的名称或在一列代表了进程的名称或在 shellshell 中调用的命令行。 中调用的命令行。

Page 6: 第 5 章 进程管理及进程间通讯

Linux Linux 中的每个进程有自己的虚拟地址空间,操作系统中的每个进程有自己的虚拟地址空间,操作系统的一个最重要的基本管理目的,就是避免进程之间的互相的一个最重要的基本管理目的,就是避免进程之间的互相影响。但有时用户也希望能够利用两个或多个进程的功能影响。但有时用户也希望能够利用两个或多个进程的功能完成同一任务,为此,完成同一任务,为此, Linux Linux 提供许多机制,利用这些机提供许多机制,利用这些机制,进程之间可以进行通讯并共同完成某项任务,这种机制,进程之间可以进行通讯并共同完成某项任务,这种机制称为“进程间通讯(制称为“进程间通讯( Interprocess Communication ,IPInterprocess Communication ,IPCC )”。信号和管道是常见的两种 )”。信号和管道是常见的两种 IPC IPC 机制,但 机制,但 Linux Linux 也提供其他 也提供其他 IPC IPC 机制。机制。

一般来说,一般来说, LinuxLinux下的进程包含以下几个关键要素:有下的进程包含以下几个关键要素:有一段可执行程序;有专用的系统堆栈空间;内核中有它的一段可执行程序;有专用的系统堆栈空间;内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;具有独立的存储空间。进程才能接受内核的调度;具有独立的存储空间。

Page 7: 第 5 章 进程管理及进程间通讯

Linux Linux 内核利用一个数据结构内核利用一个数据结构 task_structtask_struct来来代表一个进程,代表进程的数据结构指针形成了代表一个进程,代表进程的数据结构指针形成了一个 一个 task task 数组(在数组(在 Linux Linux 中,任务和进程是两中,任务和进程是两个相同的术语),这种指针数组有时也成为指针个相同的术语),这种指针数组有时也成为指针向量。这个数组的大小默认为 向量。这个数组的大小默认为 512512 ,表明在 ,表明在 LinLinux ux 系统中能够同时运行的进程最多可有 系统中能够同时运行的进程最多可有 512512 。。当建立新进程的时候,当建立新进程的时候, Linux Linux 为新的进程分配一为新的进程分配一个 个 task_struct task_struct 结构,然后将指针保存在 结构,然后将指针保存在 task task 数组中。数组中。 task_struct task_struct 结构中包含了许多字段,结构中包含了许多字段,按照字段功能,可分成如下几类:按照字段功能,可分成如下几类:

Page 8: 第 5 章 进程管理及进程间通讯

(1) (1) 标识号。系统通过进程标识号唯一识别一个进程,但标识号。系统通过进程标识号唯一识别一个进程,但进程标识号并不是进程对应的 进程标识号并不是进程对应的 task_struct task_struct 结构指针在 结构指针在 ttask ask 数组中的索引号。另外,一个进程还有自己的用户和数组中的索引号。另外,一个进程还有自己的用户和组标识号,系统通过这两个标识号判断进程对文件或设备组标识号,系统通过这两个标识号判断进程对文件或设备的访问权。的访问权。

(2) (2) 状态信息。一个 状态信息。一个 Linux Linux 进程可有如下几种状态:运行、进程可有如下几种状态:运行、等待、停止和僵死。等待、停止和僵死。

(3) (3) 调度信息。调度程序利用该信息完成进程之间的切换。调度信息。调度程序利用该信息完成进程之间的切换。 (4) (4) 有关进程间通讯的信息。系统利用这一信息实现进程有关进程间通讯的信息。系统利用这一信息实现进程

间的通讯。间的通讯。 (5) (5) 进程链信息。在 进程链信息。在 Linux Linux 系统中,除初始化进程之外,系统中,除初始化进程之外,

任何一个进程都具有父进程。每个进程都是从父进程中任何一个进程都具有父进程。每个进程都是从父进程中“克隆”出来的。进程链则包含进程的父进程指针、和该“克隆”出来的。进程链则包含进程的父进程指针、和该进程具有相同父进程的兄弟进程指针以及进程的子进程指进程具有相同父进程的兄弟进程指针以及进程的子进程指针。另外,针。另外, Linux Linux 利用一个双向链表记录系统中所有的进利用一个双向链表记录系统中所有的进程,这个双向链表的根就是 程,这个双向链表的根就是 init init 进程。利用这个链表中进程。利用这个链表中的信息,内核可以很容易地找到某个进程。的信息,内核可以很容易地找到某个进程。

Page 9: 第 5 章 进程管理及进程间通讯

(6) (6) 时间和定时器。系统在这些字段中保存进程的建立时时间和定时器。系统在这些字段中保存进程的建立时间,以及在其生命周期中所花费的 间,以及在其生命周期中所花费的 CPU CPU 时间,这两个时时间,这两个时间均以 间均以 jiffies jiffies 为单位。该时间由两部分组成,一是进程为单位。该时间由两部分组成,一是进程在用户模式下花费的时间,二是进程在系统模式下花的时在用户模式下花费的时间,二是进程在系统模式下花的时间。间。 Linux Linux 也支持和进程相关的定时器,应用程序可通过也支持和进程相关的定时器,应用程序可通过系统调用建立定时器,当定时器到期,操作系统会向该进系统调用建立定时器,当定时器到期,操作系统会向该进程发送 程发送 sigalrmsigalrm信号。信号。

(7) (7) 文件系统信息。进程可以打开文件系统中的文件,系文件系统信息。进程可以打开文件系统中的文件,系统需要对这些文件进行跟踪。系统使用这类字段记录进程统需要对这些文件进行跟踪。系统使用这类字段记录进程所打开的文件描述符信息。另外,还包含指向虚拟文件系所打开的文件描述符信息。另外,还包含指向虚拟文件系统(统( Virtual File SystemsVirtual File Systems ,, VFSVFS)两个索引节点的指针,)两个索引节点的指针,这两个索引节点分别是进程的主目录以及进程的当前目录。这两个索引节点分别是进程的主目录以及进程的当前目录。索引节点中有一个引用计数器,当有新的进程指向某个索索引节点中有一个引用计数器,当有新的进程指向某个索引节点时,该索引节点的引用计数器会增加计数。未被引引节点时,该索引节点的引用计数器会增加计数。未被引用的索引节点的引用计数为 用的索引节点的引用计数为 00,因此,当包含在某个目录,因此,当包含在某个目录中的文件正在运行时,就无法删除这一目录,因为这一目中的文件正在运行时,就无法删除这一目录,因为这一目录的引用计数大于录的引用计数大于 00。。

Page 10: 第 5 章 进程管理及进程间通讯

(8) (8) 和进程相关的上下文信息。如前所述,进程可被看成和进程相关的上下文信息。如前所述,进程可被看成是系统状态的集合,随着进程的运行,这一集合发生变化。是系统状态的集合,随着进程的运行,这一集合发生变化。进程上下文就是用来保存系统状态的 进程上下文就是用来保存系统状态的 task_struct task_struct 字段。字段。当调度程序将某个进程从运行状态切换到暂停状态时,会当调度程序将某个进程从运行状态切换到暂停状态时,会在上下文中保存当前的进程运行环境,包括 在上下文中保存当前的进程运行环境,包括 CPU CPU 寄存器寄存器的值以及堆栈信息;当调度程序再次选择该进程运行时,的值以及堆栈信息;当调度程序再次选择该进程运行时,则会从进程上下文信息中恢复进程的运行环境。则会从进程上下文信息中恢复进程的运行环境。

Page 11: 第 5 章 进程管理及进程间通讯

5.1.2 5.1.2 标识符信息标识符信息

和所有的 和所有的 Unix Unix 系统一样,系统一样, Linux Linux 使用用户标使用用户标识符和组标识符判断用户对文件和目录的访问许识符和组标识符判断用户对文件和目录的访问许可。可。 Linux Linux 系统中的所有文件或目录均具有所有系统中的所有文件或目录均具有所有者和许可属性,者和许可属性, Linux Linux 据此判断某个用户对文件据此判断某个用户对文件的访问权限。的访问权限。

Page 12: 第 5 章 进程管理及进程间通讯

对一个进程而言,系统在 对一个进程而言,系统在 task_struct task_struct 结构中记录结构中记录如表如表 5.1 5.1 所示的所示的 44对标识符。对标识符。

uid 和 gid 运行进程所代表的用户之用户标识号和组标识号,通常就是执行该进程的用户。

有效 uid 和gid

某些程序可以将 uid 和 gid 改变为自己私有 的 uid 和 gid。系统在运行这样的程序时,会根

据修改后的 uid 及 gid 判断程序的特权,例如, 是否能够直接进行 I/O 输出等。通过 setuid 系统调

用,可将程序的有效 uid 和 gid 设置为其他用户。 在该程序映像文件的 VFS 索引节点中,有效 uid

和 gid 由索引节点的属性描述。

文件系统 uid 和 gid

这两个标识符和上述标识符类似,但用于检查 对文件系统的访问许可时。处于用户模式的 NFS 服

务器作为特殊进程访问文件时使用这两个标识符。保存 uid 和

gid 如果进程通过系统调用修改了进程的 uid 和 g

id ,这两个标识符则保存实际的 uid 和 gid。

Page 13: 第 5 章 进程管理及进程间通讯

5.1.3 5.1.3 进程状态信息进程状态信息Linux Linux 中的进程有中的进程有 44种状态,如表种状态,如表 5.2 5.2 所示。所示。

运行状态 该进程是当前正在运行的进程;或者,该进程是可 以运行的进程,即正在等待调度程序将 CPU 分配给它。

等待状态 进程正在等待某个事件或某个资源。这种进程又分为可中断的进程和不可中断的进程两种。可中断的等待进程可被信号中断,而不可中断的等待进程是正在直接等待硬件状态条件的进程,在任何情况下都不能被中断。

停止状态 进程处于停止状态,通常由于接收到信号而停止,例如,进程在接收到调试信号时处于停止状态。

僵死状态 进程已终止,但在 task 数组中仍占据着一个 task_struct 结构。顾名思义,处于这种状态的进程实际是死进程。

Page 14: 第 5 章 进程管理及进程间通讯

5.1.4 5.1.4 文件信息文件信息

如图如图 5.1 5.1 所示,系统中的每个进程有两个数据所示,系统中的每个进程有两个数据结构用于描述进程与文件相关的信息。其中,结构用于描述进程与文件相关的信息。其中, fs_fs_struct struct 描述了上面提到的指向描述了上面提到的指向 VFS VFS 两个索引节点两个索引节点的指针,即 的指针,即 root root 和 和 pwdpwd 。另外,这个结构还包。另外,这个结构还包含一个 含一个 umask umask 字段,它是进程创建文件时使用字段,它是进程创建文件时使用的默认模式,可通过系统调用修改这一默认模式。的默认模式,可通过系统调用修改这一默认模式。另一个结构为另一个结构为 files_structfiles_struct,它描述了当前进程所,它描述了当前进程所使用的所有文件信息。从图中可以看出,每个进使用的所有文件信息。从图中可以看出,每个进程能够同时拥有 程能够同时拥有 256 256 个打开的文件,个打开的文件, fs[0] fs[0] 到 到 fsfs[255] [255] 就是指向这些 就是指向这些 file file 结构的指针。文件的描结构的指针。文件的描述符实际就是 述符实际就是 fs fs 指针数组的索引号。指针数组的索引号。

Page 15: 第 5 章 进程管理及进程间通讯

task_struct

……

fs

files

……

fs_struct

count

umask

*root

*pwd

files_struct

count

close_on_exec

open_fs

fd[0]

fd[1]

……

fd[255]

inode

inode

f_mode

file

f_pos

f_flags

f_count

f_owner

f_inode

f_op

f_version

inode

文件操作例程集

图5.1 进程的文件信息

Page 16: 第 5 章 进程管理及进程间通讯

在 在 file file 结构中,结构中, f_mode f_mode 是文件的打开模式,只读、是文件的打开模式,只读、只写或读写;只写或读写; f_pos f_pos 是文件的当前位置;是文件的当前位置; f_inode f_inode 指向 指向 VFS VFS 中该文件的索引节点;中该文件的索引节点; f_op f_op 包含了对该文件的操作包含了对该文件的操作例程集。利用 例程集。利用 f_opf_op ,可以针对不同的文件定义不同的操,可以针对不同的文件定义不同的操作函数,例如一个用来向文件中写数据的函数。作函数,例如一个用来向文件中写数据的函数。 Linux Linux 利利用这一抽象机制,实现了管道这一进程间通讯机制。这种用这一抽象机制,实现了管道这一进程间通讯机制。这种抽象方法在 抽象方法在 Linux Linux 内核中很常见,通过这种方法,可使内核中很常见,通过这种方法,可使特定的内核对象具有类似 特定的内核对象具有类似 C++ C++ 对象的多态性。对象的多态性。

Linux Linux 进程启动时,有三个文件描述符被打开,它们是进程启动时,有三个文件描述符被打开,它们是标准输入、标准输出和错误输出,分别对应 标准输入、标准输出和错误输出,分别对应 fs fs 数组的三数组的三个索引,即 个索引,即 00、、 11 和和 22 。如果启动时进行输入输出重定。如果启动时进行输入输出重定向,则这些文件描述符指向指定的文件而不是标准的终端向,则这些文件描述符指向指定的文件而不是标准的终端输入输入 // 输出。每当进程打开一个文件时,就会利用输出。每当进程打开一个文件时,就会利用 files_sfiles_struct truct 的一个空闲 的一个空闲 file file 指针指向打开的文件描述结构 指针指向打开的文件描述结构 filefile 。。对文件的访问通过 对文件的访问通过 file file 结构中定义的文件操作例程和虚结构中定义的文件操作例程和虚拟文件系统(拟文件系统( Virtual File SystemVirtual File System,, VFSVFS)的索引节点)的索引节点信息来完成。 信息来完成。

Page 17: 第 5 章 进程管理及进程间通讯

5.1.5 5.1.5 虚拟内存虚拟内存

进程的虚拟内存包含了进程所有的可执行代码和数据。进程的虚拟内存包含了进程所有的可执行代码和数据。运行某个程序时,系统要根据可执行映像中的信息,为进运行某个程序时,系统要根据可执行映像中的信息,为进程代码和数据分配虚拟内存;进程在运行过程中,可能会程代码和数据分配虚拟内存;进程在运行过程中,可能会通过系统调用动态申请虚拟内存或释放已分配的内存,新通过系统调用动态申请虚拟内存或释放已分配的内存,新分配的虚拟内存必须和进程已有的虚拟地址链接起来才能分配的虚拟内存必须和进程已有的虚拟地址链接起来才能使用。使用。 Linux Linux 进程可以使用共享的程序库代码或数据,因进程可以使用共享的程序库代码或数据,因此,共享库的代码和数据也需要链接到进程已有的虚拟地此,共享库的代码和数据也需要链接到进程已有的虚拟地址中。址中。 LinuxLinux 系统利用了需求分页机制来避免对物理内存系统利用了需求分页机制来避免对物理内存的过分使用。因为进程可能会访问当前不在物理内存中的的过分使用。因为进程可能会访问当前不在物理内存中的虚拟内存,这时操作系统将通过对处理器的页故障处理装虚拟内存,这时操作系统将通过对处理器的页故障处理装入内存页。系统为此需要修改进程的页表,以便标志虚拟入内存页。系统为此需要修改进程的页表,以便标志虚拟页是否在物理内存中,同时,页是否在物理内存中,同时, Linux Linux 还需要知道进程地址还需要知道进程地址空间中任何一个虚拟地址区域的来源和当前所在位置,以空间中任何一个虚拟地址区域的来源和当前所在位置,以便能够装入物理内存。便能够装入物理内存。

Page 18: 第 5 章 进程管理及进程间通讯

……

task_struct

mm

……

count

mm_struct

pgd

mmap_sem

……

mmap

mmap_avl

count

vm_flags

vm_end

vm_area_struct

vm_start

vm_inode

vm_ops

count

vm_flags

vm_end

vm_area_struct

vm_start

vm_inode

vm_ops

vm_next

进程虚拟内存

数据

代码

0x000000

0

0x104800

0

0x8059BB

8

vm_next

图 5.2 进程的虚拟内存示意

Page 19: 第 5 章 进程管理及进程间通讯

Linux Linux 采用了比较复杂的数据结构跟踪进程的虚拟地址。采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的 在进程的 task_struct task_struct 结构中包含一个指向 结构中包含一个指向 mm_struct mm_struct 结构的指针。进程的 结构的指针。进程的 mm_struct mm_struct 则包含装入的可执行映则包含装入的可执行映像信息以及进程的页表指针。该结构还包含有指向 像信息以及进程的页表指针。该结构还包含有指向 vm_avm_area_struct rea_struct 结构的几个指针,每个 结构的几个指针,每个 vm_area_struct vm_area_struct 代代表进程的一个虚拟地址区域。表进程的一个虚拟地址区域。

图图 5.2 5.2 是某个进程的虚拟内存简化布局以及相应的进程是某个进程的虚拟内存简化布局以及相应的进程数据结构。从图中可以看出,系统以虚拟内存地址降序排数据结构。从图中可以看出,系统以虚拟内存地址降序排列 列 vm_area_structvm_area_struct。每个虚拟内存区域可能来源不同,。每个虚拟内存区域可能来源不同,有的可能来自映像,有的可能来自共享库,而有的则可能有的可能来自映像,有的可能来自共享库,而有的则可能是动态分配的内存区。因此,是动态分配的内存区。因此, Linux Linux 利用了虚拟内存处理利用了虚拟内存处理例程(例程( vm_opsvm_ops )来抽象对不同来源虚拟内存的处理方法。)来抽象对不同来源虚拟内存的处理方法。

Page 20: 第 5 章 进程管理及进程间通讯

在进程的运行过程中,在进程的运行过程中, Linux Linux 要经常为进程分配虚拟要经常为进程分配虚拟地址区域,或者因为从交换文件中装入内存而修改虚拟地地址区域,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,址信息,因此, vm_area_struct vm_area_struct 结构的访问时间就成了结构的访问时间就成了性能的关键因素。除链表结构外,性能的关键因素。除链表结构外, Linux Linux 还利用 还利用 AVLAVL(( Adelson-Velskii and LandisAdelson-Velskii and Landis )树组织 )树组织 vm_area_struvm_area_structct。通过这种树结构,。通过这种树结构, Linux Linux 可以快速定位某个虚拟内可以快速定位某个虚拟内存地址,但在该树中插入或删除节点需要花费较多的时间。存地址,但在该树中插入或删除节点需要花费较多的时间。

当进程利用系统调用动态分配内存时,当进程利用系统调用动态分配内存时, Linux Linux 首先分首先分配一个 配一个 vm_area_struct vm_area_struct 结构,并链接到进程的虚拟内结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区域时,因为 存链表中,当后续的指令访问这一内存区域时,因为 LinLinux ux 尚未分配相应的物理内存,因此处理器在进行虚拟地尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生页故障,当 址到物理地址的映射时会产生页故障,当 Linux Linux 处理这处理这一页故障时,就可以为新的虚拟内存区分配实际的物理内一页故障时,就可以为新的虚拟内存区分配实际的物理内存。 存。

Page 21: 第 5 章 进程管理及进程间通讯

5.1.6 5.1.6 时间和定时器 时间和定时器 Linux Linux 保存一个指向当前正在运行的进保存一个指向当前正在运行的进

程的程的 task_struct task_struct 结构的指针,即 结构的指针,即 currentcurrent 。。每当产生一次实时时钟中断(又称时钟周每当产生一次实时时钟中断(又称时钟周期),期), Linux Linux 就会更新 就会更新 current current 所指向的所指向的进程的时间信息,如果内核当前代表该进进程的时间信息,如果内核当前代表该进程执行任务(例如进程调用系统调用时),程执行任务(例如进程调用系统调用时),那么系统就把进程在系统模式下花费的时那么系统就把进程在系统模式下花费的时间作为时间记录,否则将进程在用户模式间作为时间记录,否则将进程在用户模式下花费的时间作为时间记录。 下花费的时间作为时间记录。

Page 22: 第 5 章 进程管理及进程间通讯

除了为进程记录其消耗的 除了为进程记录其消耗的 CPU CPU 时间外,时间外, Linux Linux 还支持还支持和进程相关的间隔定时器。当定时器到期时,会向定时器和进程相关的间隔定时器。当定时器到期时,会向定时器的所属进程发送信号。进程可使用三种不同类型的定时器的所属进程发送信号。进程可使用三种不同类型的定时器来给自己发送相应的信号,如表来给自己发送相应的信号,如表 5.3 5.3 所示。所示。

Real 该定时器实时更新,到期时发送 SIGALRM 信号。

Virtual

该定时器只在进程运行时更新,到期时发送 SIGVTALRM 信号。

Profile

该定时器在进程运行时,以及内核代表进程运行 时更新,到期时发送 SIGPROF 信号。

Linux 对 Virtual 和 Profile 定时器的处理是相同的,在每个时钟中断,定时器的计数值减 1 ,直到计数值为 0 时发送信号。 Real 定时器的处理比较特殊。

Page 23: 第 5 章 进程管理及进程间通讯

5.1.7 5.1.7 关于线程关于线程

和进程概念紧密相关的概念是线程。线程可看和进程概念紧密相关的概念是线程。线程可看成是进程中指令的不同执行路线。例如,常见的成是进程中指令的不同执行路线。例如,常见的字处理程序中,主线程处理用户输入,而其他并字处理程序中,主线程处理用户输入,而其他并行运行的线程在必要时可在后台保存用户的文档。行运行的线程在必要时可在后台保存用户的文档。与进程相关的基本要素有:代码、数据、堆栈、与进程相关的基本要素有:代码、数据、堆栈、文件 文件 I/OI/O和虚拟内存信息等,因此,系统对进程和虚拟内存信息等,因此,系统对进程的处理要花费更多的开支,尤其在进行进程调度的处理要花费更多的开支,尤其在进行进程调度时。利用线程则可以通过共享这些基本要素而减时。利用线程则可以通过共享这些基本要素而减轻系统开支,因此,线程也被称为“轻量级进轻系统开支,因此,线程也被称为“轻量级进程”。许多流行的多任务操作系统均支持线程。 程”。许多流行的多任务操作系统均支持线程。

Page 24: 第 5 章 进程管理及进程间通讯

线程有“用户线程”和“内核线程”之分。所线程有“用户线程”和“内核线程”之分。所谓用户线程是指不需要内核支持而在用户程序中谓用户线程是指不需要内核支持而在用户程序中实现的线程,这种线程甚至在象 实现的线程,这种线程甚至在象 DOS DOS 这样的操这样的操作系统中也可实现,但线程的调度需要用户程序作系统中也可实现,但线程的调度需要用户程序完成,类似于 完成,类似于 Windows 3.x Windows 3.x 的协作式多任务。的协作式多任务。另外一种则需要内核的参与,由内核完成线程的另外一种则需要内核的参与,由内核完成线程的调度。这两种模型各有其优缺点:用户线程不需调度。这两种模型各有其优缺点:用户线程不需要额外的内核开支,但是当一个线程因 要额外的内核开支,但是当一个线程因 I/O I/O 而处而处于等待状态时,整个进程就会被调度程序切换为于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核等待状态,其他线程得不到运行的机会;而内核线程则没有这个限制,但却占用了更多的系统开线程则没有这个限制,但却占用了更多的系统开支。支。

Linux Linux 支持内核空间的多线程,读者也可以从 支持内核空间的多线程,读者也可以从 Internet Internet 上下载一些用户级的线程库。上下载一些用户级的线程库。

Page 25: 第 5 章 进程管理及进程间通讯

Linux Linux 的内核线程和其他操作系统的内核实现的内核线程和其他操作系统的内核实现不同。大多数操作系统单独定义线程,从而增加不同。大多数操作系统单独定义线程,从而增加了内核和调度程序的复杂性;而 了内核和调度程序的复杂性;而 Linux Linux 则将线程则将线程定义为“执行上下文”,实际只是进程的另外一定义为“执行上下文”,实际只是进程的另外一个执行上下文而已。这样,个执行上下文而已。这样, Linux Linux 内核只需区分内核只需区分进程,只需要一个进程进程,只需要一个进程 //线程数组,而调度程序线程数组,而调度程序仍然是进程的调度程序。仍然是进程的调度程序。 Linux Linux 的克隆(的克隆( clonclonee ))系统调用可用来建立新的线程。系统调用可用来建立新的线程。

Page 26: 第 5 章 进程管理及进程间通讯

5.1.8 5.1.8 会话和进程组 会话和进程组 在在 UnixUnix 系统中,父进程创建子进程,子进程可以再创系统中,父进程创建子进程,子进程可以再创建新进程,形成一定的层次,称为“进程组”。一个或多建新进程,形成一定的层次,称为“进程组”。一个或多个进程可以合起来构成一个进程组(个进程可以合起来构成一个进程组( process groupprocess group ),),一个或多个进程组可以合起来构成一个会话(一个或多个进程组可以合起来构成一个会话( sessionsession )。)。这样,就有了对进程进行批量操作的能力,例如通过向某这样,就有了对进程进行批量操作的能力,例如通过向某个进程组发送信号以实现向该组中的每个进程发送信号。个进程组发送信号以实现向该组中的每个进程发送信号。

Linux Linux 内核通过维护会话和进程组而管理多用户进程。内核通过维护会话和进程组而管理多用户进程。如图如图 5.3 5.3 所示,每个进程是一个进程组的成员,而每个进所示,每个进程是一个进程组的成员,而每个进程组又是某个会话的成员。一般而言,当用户在某个终端程组又是某个会话的成员。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。 类似地,每个会话也对应有一个领头进程。

Page 27: 第 5 章 进程管理及进程间通讯

同一会话中的进程通过该同一会话中的进程通过该会话的领头进程和一个终端会话的领头进程和一个终端相连,该终端作为这个会话相连,该终端作为这个会话的控制终端。一个会话只能的控制终端。一个会话只能有一个控制终端,而一个控有一个控制终端,而一个控制终端只能控制一个会话。制终端只能控制一个会话。用户通过控制终端,可以向用户通过控制终端,可以向该控制终端所控制的会话中该控制终端所控制的会话中的进程发送键盘信号。的进程发送键盘信号。

同一会话中只能有一个前同一会话中只能有一个前台进程组,属于前台进程组台进程组,属于前台进程组的进程可从控制终端获得输的进程可从控制终端获得输入,而其他进程均是后台进入,而其他进程均是后台进程,可能分属于不同的后台程,可能分属于不同的后台进程组。进程组。

会话

前台进程组

后台进程组

后台进程组

领头

进程

领头

进程

领头

进程

图 5.3 会话和进程、进程组

Page 28: 第 5 章 进程管理及进程间通讯

5.2 5.2 进程的创建和进程调度进程的创建和进程调度5.2.1 5.2.1 进程的创建进程的创建

第一个进程在系统启动时创建,当系统启动的时候它运第一个进程在系统启动时创建,当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的信息存在初始进程的 task_structtask_struct数据结构中。在系统初数据结构中。在系统初始化结束的时候,系统初始化结束时,初始进程启动一个始化结束的时候,系统初始化结束时,初始进程启动一个内核线程内核线程 initinit ,而自己则处于空循环状态。当系统中没有,而自己则处于空循环状态。当系统中没有可运行的进程时,调度程序会运行这个空闲的进程。这个可运行的进程时,调度程序会运行这个空闲的进程。这个空闲进程的空闲进程的 task_structtask_struct是唯一的不是动态分配而是在核是唯一的不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做心连接的时候静态定义的,为了不至于混淆,叫做 init_tainit_tasksk。。

Page 29: 第 5 章 进程管理及进程间通讯

initinit 内核线程内核线程 //进程的标识号为 进程的标识号为 11 ,它是系统的第一个真正,它是系统的第一个真正进程。它负责初始的系统设置工作,例如打开控制台,挂进程。它负责初始的系统设置工作,例如打开控制台,挂装文件系统等。然后,装文件系统等。然后, initinit 进程执行系统的初始化程序,进程执行系统的初始化程序,这一程序可能是 这一程序可能是 /etc/init/etc/init、、 /bin/init /bin/init 或 或 /sbin/init/sbin/init。。 ininitit 程序将 程序将 /etc/inittab /etc/inittab 当作脚本文件建立系统中新的进当作脚本文件建立系统中新的进程,这些新的进程又可以建立新进程。例如,程,这些新的进程又可以建立新进程。例如, gettygetty 进进程可建立 程可建立 loginlogin 进程来接受用户的登录请求。 进程来接受用户的登录请求。

父进程的

文件描述符

子进程的

文件描述符

file 数据

结构图 5.4 父进程和子进程共享打开的文件

Page 30: 第 5 章 进程管理及进程间通讯

新的进程通过克隆旧的程序(当前程序)而建立。新的进程通过克隆旧的程序(当前程序)而建立。 forforkk 和 和 cloneclone 系统调用可用来建立新的进程。这两个系统系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新调用结束时,内核在系统的物理内存中为新的进程分配新的 的 task_struct task_struct 结构,同时为新进程要使用的堆栈分配物结构,同时为新进程要使用的堆栈分配物理页。理页。 Linux Linux 还会为新的进程分配新的进程标识符。然后,还会为新的进程分配新的进程标识符。然后,新 新 task_struct task_struct 结构的地址保存在 结构的地址保存在 task task 数组中,而旧进数组中,而旧进程的 程的 task_struct task_struct 结构内容被复制到新进程的 结构内容被复制到新进程的 task_strutask_struct ct 结构中。结构中。

在克隆进程时,在克隆进程时, Linux Linux 允许两个进程共享相同的资源。允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等。当可共享的资源包括文件、信号处理程序和虚拟内存等。当某个资源被共享时,该资源的引用计数值会增加 某个资源被共享时,该资源的引用计数值会增加 11 ,从而,从而只有两个进程均终止时,内核才会释放这些资源。图只有两个进程均终止时,内核才会释放这些资源。图 5.4 5.4 说明了父进程和子进程共享打开的文件。说明了父进程和子进程共享打开的文件。

Page 31: 第 5 章 进程管理及进程间通讯

系统对进程虚拟内存的克隆过程则更加巧妙。新的 系统对进程虚拟内存的克隆过程则更加巧妙。新的 vmvm_area_struct _area_struct 结构、新进程自己的 结构、新进程自己的 mm_struct mm_struct 结构以结构以及新进程的页表必须在一开始就准备好,但这时并不复制及新进程的页表必须在一开始就准备好,但这时并不复制任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,而有些在交换文件中,那么虚拟内存的复制将会非常困难而有些在交换文件中,那么虚拟内存的复制将会非常困难和费时。和费时。 Linux Linux 采用了称为“写时复制”的技术,只有当采用了称为“写时复制”的技术,只有当两个进程中的任意一个向虚拟内存中写入数据时才复制相两个进程中的任意一个向虚拟内存中写入数据时才复制相应的虚拟内存;而没有写入的任何内存页均可以在两个进应的虚拟内存;而没有写入的任何内存页均可以在两个进程之间共享。代码页实际总是可以共享的。程之间共享。代码页实际总是可以共享的。

为实现“写时复制”技术,为实现“写时复制”技术, Linux Linux 将可写虚拟内存页将可写虚拟内存页的页表项标志为只读。当进程要向这种内存页写入数据时,的页表项标志为只读。当进程要向这种内存页写入数据时,处理器会发现内存访问控制上的问题(向只读页中写入),处理器会发现内存访问控制上的问题(向只读页中写入),从而导致页故障。于是,操作系统可捕获这一被处理器认从而导致页故障。于是,操作系统可捕获这一被处理器认为是“非法的”写操作而完成内存页的复制。最后,为是“非法的”写操作而完成内存页的复制。最后, LinuLinux x 还要修改两个进程的页表以及虚拟内存数据结构。还要修改两个进程的页表以及虚拟内存数据结构。

Page 32: 第 5 章 进程管理及进程间通讯

进程终止条件有如下几种:进程终止条件有如下几种:(( 11 ) 进程运行结束,正常退出(主动终止);) 进程运行结束,正常退出(主动终止);(( 22 ) 发生可预料的错误,报错退出(主动终止);) 发生可预料的错误,报错退出(主动终止);(( 33 ) 发生严重错误,进程异常终止(被动终止);) 发生严重错误,进程异常终止(被动终止);

(( 44)被其他进程终止(被动终止)。)被其他进程终止(被动终止)。

Page 33: 第 5 章 进程管理及进程间通讯

5.2.2 5.2.2 进程的管理和调度进程的管理和调度

Linux Linux 是一个多任务操作系统,它要保证 是一个多任务操作系统,它要保证 CPU CPU 时刻保时刻保持在使用状态,如果某个正在运行的进程等待外部设备完持在使用状态,如果某个正在运行的进程等待外部设备完成工作(例如等待打印机完成打印任务),这时,操作系成工作(例如等待打印机完成打印任务),这时,操作系统就可以选择其他进程运行,从而保持 统就可以选择其他进程运行,从而保持 CPU CPU 的最大利用的最大利用率。这就是多任务的基本思想,进程之间的切换由调度程率。这就是多任务的基本思想,进程之间的切换由调度程序完成。序完成。

不同用途的系统其调度算法的目标有共性,也有各自独不同用途的系统其调度算法的目标有共性,也有各自独有的倾向。例如批处理系统的目标主要是增大每小时作业有的倾向。例如批处理系统的目标主要是增大每小时作业量(吞吐量),降低作业提交和终止之间的时间(周转时量(吞吐量),降低作业提交和终止之间的时间(周转时间 ),间 ), CPUCPU利用率(保持利用率(保持 CPUCPU总在工作);而交互式系总在工作);而交互式系统要求对用户要求做出快速反应(响应时间)和满足用户统要求对用户要求做出快速反应(响应时间)和满足用户期望(均衡);实时系统则要求不丢失数据(工作低限),期望(均衡);实时系统则要求不丢失数据(工作低限), 在多媒体系统中避免降低媒体质量(可预见性)等。 在多媒体系统中避免降低媒体质量(可预见性)等。

Page 34: 第 5 章 进程管理及进程间通讯

无论是什么系统,系统共同的目标都是:无论是什么系统,系统共同的目标都是: (( 11 )公平--给每个进程分配相同的)公平--给每个进程分配相同的 CPUCPU 时间;时间; (( 22 )坚持--保证制定的策略完满执行;)坚持--保证制定的策略完满执行; (( 33 )平衡--保证系统的各个部分都在工作。)平衡--保证系统的各个部分都在工作。 1. i3861. i386体系的进程管理和调度体系的进程管理和调度 Intel Intel 在在 i386i386体系的设计中考虑到了进程的管理和调度,体系的设计中考虑到了进程的管理和调度,

并从硬件上支持任务间的切换。为此目的,并从硬件上支持任务间的切换。为此目的, IntelIntel 在在 i386i386系统结构中增设了一种新段“任务状态段”系统结构中增设了一种新段“任务状态段” TSSTSS(( Task Task Status SegmentStatus Segment)。一个)。一个 TSSTSS虽然说像代码段、数据段虽然说像代码段、数据段等一样也是一个段,实际上却是一个等一样也是一个段,实际上却是一个 104104 字节的数据结构,字节的数据结构,用以记录一个任务的关键性的状态信息。像其他段一样,用以记录一个任务的关键性的状态信息。像其他段一样,TSSTSS也要在段描述表中有个表项。也要在段描述表中有个表项。

不过不过 TSSTSS只能在全局描述符表只能在全局描述符表 GDTGDT(( Global DescribtGlobal Describtor Tableor Table )中,而不能放在任何一个局部描述符表)中,而不能放在任何一个局部描述符表 LDTLDT(( Local Describtor TableLocal Describtor Table )中或中断描述表)中或中断描述表 IDTIDT(( InteInterrupt Describer Tablerrupt Describer Table )中。若通过一个段选择项访问一)中。若通过一个段选择项访问一个个 TSSTSS,而选择项中的,而选择项中的 TITI 位为位为 11 ,就会产生一次,就会产生一次 GPGP异异常。 常。

Page 35: 第 5 章 进程管理及进程间通讯

另外,另外, CPUCPU 中还增设一个任务寄存器中还增设一个任务寄存器 TR,TR,指向当前任指向当前任务的务的 TSSTSS。相应地,还增加了一条指令。相应地,还增加了一条指令 LTRLTR对对 TRTR寄存器寄存器进行装入操作。像进行装入操作。像 CSCS和和 DSDS寄存器一样,寄存器一样, TRTR也有一个程也有一个程序不可见部分,每当将一个段选择码装入到序不可见部分,每当将一个段选择码装入到 TRTR中时,中时, CPCPUU 就会自动找到所选择的就会自动找到所选择的 TSSTSS 描述项并将其装入到描述项并将其装入到 TRTR的的程序不可见部分,以加速以后对该程序不可见部分,以加速以后对该 TSSTSS段的访问。段的访问。

在在 IDTIDT表中,除了中断门、陷阱门和调用门以外,还定表中,除了中断门、陷阱门和调用门以外,还定义了一种任务门。任务门中包含一个义了一种任务门。任务门中包含一个 TSSTSS段选择码。当段选择码。当 CCPUPU因中断而穿过一个任务门时,就会将任务门中的选择因中断而穿过一个任务门时,就会将任务门中的选择码自动装入码自动装入 TRTR,使,使 TRTR指向新的指向新的 TSSTSS,并完成任务的切,并完成任务的切换。换。 CPUCPU 还可以通过还可以通过 JMPJMP 和和 CALLCALL 指令实现任务切换,指令实现任务切换,当跳转或调用的目标段实际上指向当跳转或调用的目标段实际上指向 GDTGDT表中的一个表中的一个 TSSTSS描述项时,就会引起一次任务切换。描述项时,就会引起一次任务切换。

Page 36: 第 5 章 进程管理及进程间通讯

2. Linux2. Linux 系统对进程状态管理的实现机制 系统对进程状态管理的实现机制 从系统内核的角度来看,一个进程仅仅是进程控制表从系统内核的角度来看,一个进程仅仅是进程控制表

(( process tableprocess table )中的一项。在)中的一项。在 LinuxLinux 中,每个进程用中,每个进程用一个一个 task_structtask_struct的数据结构来表示,进程控制表中的每的数据结构来表示,进程控制表中的每一项都是一个一项都是一个 task_struct task_struct 结构,用来管理系统中的进程。结构,用来管理系统中的进程。在在 include/Linux/sched.hinclude/Linux/sched.h 中定义的中定义的 task_structtask_struct结构中结构中存储各种低级和高级的信息,包括从一些硬件设备的寄存存储各种低级和高级的信息,包括从一些硬件设备的寄存器拷贝到进程的工作目录的链接点。器拷贝到进程的工作目录的链接点。 TaskTask 向量表是指向向量表是指向系统中每一个系统中每一个 task_structtask_struct数据结构的指针的数组。这意数据结构的指针的数组。这意味着系统中的最大进程数受到味着系统中的最大进程数受到 TaskTask 向量表的限制,默认向量表的限制,默认值是值是 512512 。。 LinuxLinux 可以在这个表中查到系统中的所有的进可以在这个表中查到系统中的所有的进程。操作系统初始化后,建立了第一个程。操作系统初始化后,建立了第一个 task_structtask_struct数据数据结构结构 INIT_TASKINIT_TASK。当新的进程创建时,从系统内存中分。当新的进程创建时,从系统内存中分配一个新的配一个新的 task_structtask_struct,并增加到,并增加到 TaskTask 向量表中。为向量表中。为了更容易查找,用了更容易查找,用 currentcurrent指针指向当前运行的进程。 指针指向当前运行的进程。

Page 37: 第 5 章 进程管理及进程间通讯

每个在每个在 task_structtask_struct结构中登记的进程都有相应的进程结构中登记的进程都有相应的进程状态和进程标志,是进行进程调度的进程调度的两个重要状态和进程标志,是进行进程调度的进程调度的两个重要的数据项。进程在执行了相应的进程调度操作后,会由于的数据项。进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也就是改变某些原因改变自身的状态和标志,也就是改变 statestate 和和 flaflagsgs 这两个数据项。进程的状态不同、标志位不同对应了进这两个数据项。进程的状态不同、标志位不同对应了进程可以执行不同操作。程可以执行不同操作。

struct task_struct {struct task_struct {………….………….volatile long state; // -1 unrunnable , 0 runnvolatile long state; // -1 unrunnable , 0 runnable , >0 stopped able , >0 stopped unsigned long flags; // per process flags, defiunsigned long flags; // per process flags, defined below ned below ………….…………. } }; ;

Page 38: 第 5 章 进程管理及进程间通讯

在在 Linux2.2.0Linux2.2.0及以后版本的及以后版本的 sched.hsched.h 中定义了进程的中定义了进程的六种状态,十三种标志。各个标志位的代表着不同含义,六种状态,十三种标志。各个标志位的代表着不同含义,对应着不同调用。对应着不同调用。

//// 进程状态进程状态#define TASK_RUNNING 0#define TASK_RUNNING 0#define TASK_INTERRUPTIBLE 1#define TASK_INTERRUPTIBLE 1#define TASK_UNINTERRUPTIBLE 2#define TASK_UNINTERRUPTIBLE 2#define TASK_ZOMBIE 4#define TASK_ZOMBIE 4#define TASK_STOPPED 8#define TASK_STOPPED 8#define TASK_SWAPPING 16 #define TASK_SWAPPING 16

Page 39: 第 5 章 进程管理及进程间通讯

进程控制表既是一个数组,又是一个双向链表,同时又进程控制表既是一个数组,又是一个双向链表,同时又是一个树。其物理实现是一个包括多个指针的静态数组。是一个树。其物理实现是一个包括多个指针的静态数组。此数组的长度保存在此数组的长度保存在 include/Linux/tasks.h include/Linux/tasks.h 定义的常量定义的常量NR_TASKSNR_TASKS中,其默认值为中,其默认值为 128128,数组中的结构则保存,数组中的结构则保存在系统预留的内存页中。链表是由在系统预留的内存页中。链表是由 next_task next_task 和和 prev_taprev_tasksk 两个指针实现的,而树的实现则比较复杂。 两个指针实现的,而树的实现则比较复杂。

系统启动后,内核通常作为某一个进程的代表。一个指系统启动后,内核通常作为某一个进程的代表。一个指向向 task_structtask_struct的全局指针变量的全局指针变量 currentcurrent用来记录正在运用来记录正在运行的进程。变量行的进程。变量 currentcurrent只能由只能由 kernel/sched.ckernel/sched.c 中的进程中的进程调度改变。当系统需要查看所有的进程时,则调用调度改变。当系统需要查看所有的进程时,则调用 for_eafor_each_taskch_task,这将比系统搜索数组的速度要快得多。,这将比系统搜索数组的速度要快得多。

Page 40: 第 5 章 进程管理及进程间通讯

3. 3. 竞争条件,竞争条件, Racing ConditionsRacing Conditions 在图在图 5.55.5中,一个中,一个 SpoolerSpooler目录下有许多槽,编号为目录下有许多槽,编号为 00、、

11 、、 22 、、 3…3…,槽中存放要打印文件的文件名。设置了两个,槽中存放要打印文件的文件名。设置了两个共享变量:共享变量: outout指明下一个被打印的文件,指明下一个被打印的文件, inin 指向目录中指向目录中下一个空闲槽,这两个变量保存于所有进程都可以访问的下一个空闲槽,这两个变量保存于所有进程都可以访问的文件中。正常的进程访问过程是:读取文件中。正常的进程访问过程是:读取 inin 的值,将文件名的值,将文件名存于相应槽中,将存于相应槽中,将 inin 的值加的值加 11 。。

图 5.5 竞争条件( Racing Conditions )

Page 41: 第 5 章 进程管理及进程间通讯

在某一时刻,在某一时刻, 0-30-3号槽为空(其中的文件打印完毕),号槽为空(其中的文件打印完毕),4-64-6 号槽被占用(其中文件等待打印)。此时,有两个进号槽被占用(其中文件等待打印)。此时,有两个进程(进程程(进程 AA和进程和进程 BB)决定要打印文件()决定要打印文件( A.txtA.txt和和 B.txB.txtt),这时就可能发生以下的情况。),这时就可能发生以下的情况。

进程进程 AA处于运行态,读到处于运行态,读到 inin 的值为的值为 77,正当进程,正当进程 AA准准备将备将 A.txtA.txt的文件名放到的文件名放到 77 号槽时发生了进程切换,进程号槽时发生了进程切换,进程BB开始运行;进程开始运行;进程 BB运行正常,读取运行正常,读取 inin 的值为的值为 77(尚未(尚未被进程被进程 AA更改),将文件名更改),将文件名 B.txtB.txt存入存入 77 号槽,将号槽,将 inin 的值的值改为改为 88 ;当进程;当进程 AA 再次运行时,再次运行时, inin 的值已经改为的值已经改为 88,但,但AA 会从上次中止的地方继续运行,这意味着从会从上次中止的地方继续运行,这意味着从 AA的角度看的角度看inin 的值仍为的值仍为 77,于是,于是 AA将将 A.txtA.txt存入存入 77 号槽(覆盖了号槽(覆盖了 B.txB.txtt),然后把),然后把 inin 的值改为的值改为 88。这样,。这样, B.txtB.txt将永远不会被将永远不会被打印。打印。

两个或者多个进程读两个或者多个进程读 // 写共享数据,而最后的运行结果写共享数据,而最后的运行结果取决于进程运行的精确时序,这样的情况称为竞争条件。取决于进程运行的精确时序,这样的情况称为竞争条件。在存在竞争条件时,就可能产生无法预知的错误(如上例在存在竞争条件时,就可能产生无法预知的错误(如上例中,中, A.txtA.txt和和 B.txtB.txt就没有正确的打印)。 就没有正确的打印)。

Page 42: 第 5 章 进程管理及进程间通讯

4. 4. 临界区临界区 上例的问题在于,当进程上例的问题在于,当进程 AA 访问共享数据的过程尚未结访问共享数据的过程尚未结束时,进程束时,进程 BB 访问了数据。显然,如果在进程访问了数据。显然,如果在进程 AA 访问时阻访问时阻塞进程塞进程 BB,在进程,在进程 AA完成了对共享数据的访问后才允许进完成了对共享数据的访问后才允许进程程 BB 访问,就不会发生错误。访问,就不会发生错误。

凡是涉及到共享资源的情况,无论共享内存、共享文件凡是涉及到共享资源的情况,无论共享内存、共享文件或是其他资源,都可能引发上述错误。要避免这种错误,或是其他资源,都可能引发上述错误。要避免这种错误,就必须寻找某些途径来阻止多于一个的进程同时读写共享就必须寻找某些途径来阻止多于一个的进程同时读写共享数据。换句话说,读写必须按顺序进行。所谓互斥就是数据。换句话说,读写必须按顺序进行。所谓互斥就是 oone at a timene at a time ,当一个进程在访问共享数据时,其他进程,当一个进程在访问共享数据时,其他进程无法对该数据进行任何操作。无法对该数据进行任何操作。

通常,把进程中访问共享数据的程序片段称为临界区。通常,把进程中访问共享数据的程序片段称为临界区。要避免竞争条件,就必须避免多个进程同时处于临界区。 要避免竞争条件,就必须避免多个进程同时处于临界区。 图图 5.6 5.6 是临界区的一个图示说明。是临界区的一个图示说明。

Page 43: 第 5 章 进程管理及进程间通讯

好的解决方案,需要以下好的解决方案,需要以下 44个条件:个条件:(( 11 ) 任何两个进程不能同时处于临界区;) 任何两个进程不能同时处于临界区;

(( 22 ) 不应对) 不应对 CPUCPU 的速度和数目作任何假设;的速度和数目作任何假设;(( 33 ) 临界区外的进程不得阻塞其他进程;) 临界区外的进程不得阻塞其他进程;

(( 44) 不得使进程在临界区外无休止的等待。) 不得使进程在临界区外无休止的等待。

图 5.6 临界区

Page 44: 第 5 章 进程管理及进程间通讯

5.5.用户进程和内核线程 用户进程和内核线程 一个进程只能运行在用户方式(一个进程只能运行在用户方式( user modeuser mode )或内核)或内核方式(方式( kernel modekernel mode )下。用户程序运行在用户方式下,)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)。 用的是固定大小的堆栈(一般为一个内存页的大小)。

尽管尽管 LinuxLinux 是一个宏内核系统,内核线程依然存在,以是一个宏内核系统,内核线程依然存在,以便并行地处理一些内核的日常工作。这些任务不占用用户便并行地处理一些内核的日常工作。这些任务不占用用户空间(空间( user memoryuser memory),而仅仅使用内核空间(),而仅仅使用内核空间( kernel kernel memorymemory)。和其他内核模块一样,它们也在高级权限)。和其他内核模块一样,它们也在高级权限(( i386i386系统中的系统中的 RING 0RING 0)下工作。内核线程是被核心线)下工作。内核线程是被核心线程(程( kernel_thread kernel_thread )创建的。通过调用著名的)创建的。通过调用著名的 cloneclone系统调用,例如系统调用,例如 forkfork系统调用的所有功能都是由它最终系统调用的所有功能都是由它最终实现 实现 ((参看参看 arch/i386/kernel/process.c)arch/i386/kernel/process.c) 。 。

Page 45: 第 5 章 进程管理及进程间通讯

5.2.3 5.2.3 进程的切换进程的切换

虽然虽然 IntelIntel提供了十分简洁的任务切换机制。但实际上,提供了十分简洁的任务切换机制。但实际上,i386i386中通过中通过 JMPJMP 指令或指令或 CALLCALL 指令自动完成的任务切换指令自动完成的任务切换的过程是一个相当复杂的过程,其执行过程长达的过程是一个相当复杂的过程,其执行过程长达 300300多个多个CPUCPU 时钟周期。在时钟周期。在 CPUCPU 实际上的执行过程中,有的工作实际上的执行过程中,有的工作在一定条件下是可以简化的;在某些条件下,一些工作则在一定条件下是可以简化的;在某些条件下,一些工作则可能应按不同的方式组合。此外,任务的切换往往不是孤可能应按不同的方式组合。此外,任务的切换往往不是孤立的,常常与其他操作有紧密的联系。为了达到更高的效立的,常常与其他操作有紧密的联系。为了达到更高的效率和更大的灵活性,率和更大的灵活性, LinuxLinux 并不直接采用并不直接采用 i386i386硬件提供硬件提供的任务切换机制。的任务切换机制。

Page 46: 第 5 章 进程管理及进程间通讯

LinuxLinux 内核为了满足内核为了满足 i386CPUi386CPU 的要求,只是在初始化的要求,只是在初始化的时候设置装载任务寄存器的时候设置装载任务寄存器 TRTR,使之指向一个,使之指向一个 TSSTSS,从,从此以后就不再修改此以后就不再修改 TRTR的值。也就是说,每个的值。也就是说,每个 CPUCPU 在初始在初始化以后就永远使用同一个化以后就永远使用同一个 TSSTSS。同时,内核也不依靠。同时,内核也不依靠 TSTSSS保存每个进程切换时的寄存器副本,而是将这些寄存器保存每个进程切换时的寄存器副本,而是将这些寄存器的副本保存在各自进程的系统空间堆栈中。的副本保存在各自进程的系统空间堆栈中。

Linux Linux 中的进程有一些部分运行在用户模式,而另一些中的进程有一些部分运行在用户模式,而另一些部分运行在内核模式,或称系统模式。运行模式的变化是部分运行在内核模式,或称系统模式。运行模式的变化是通过系统调用完成的。通过系统调用完成的。 LinuxLinux 从用户态转移到内核态有三从用户态转移到内核态有三个途径:系统调用、中断和异常。对应的代码在个途径:系统调用、中断和异常。对应的代码在 entry.sentry.s中。中。

Page 47: 第 5 章 进程管理及进程间通讯

系统模式具有更加高级的 系统模式具有更加高级的 CPU CPU 特权级,例如可以直接特权级,例如可以直接读取或写入任意的 读取或写入任意的 I/O I/O 端口,设置 端口,设置 CPU CPU 关键寄存器等。关键寄存器等。Linux Linux 中的进程无法停止当前正在运行的进程,它只能被中的进程无法停止当前正在运行的进程,它只能被动地等待调度程序选择它为运行进程,进程的切换操作需动地等待调度程序选择它为运行进程,进程的切换操作需要高特权级的 要高特权级的 CPU CPU 指令,因此,只能在系统模式中进行,指令,因此,只能在系统模式中进行,这样,当进行系统调用时,调度程序就有了机会进行进程这样,当进行系统调用时,调度程序就有了机会进行进程切换。例如,当某个进程因为系统调用而不得不处于暂停切换。例如,当某个进程因为系统调用而不得不处于暂停状态时(例如等待用户键入字符),调度程序就可以选择状态时(例如等待用户键入字符),调度程序就可以选择其他的进程运行。其他的进程运行。 Linux Linux 采用抢先式的调度方法,每个进采用抢先式的调度方法,每个进程每次最多只能运行给定的时间段,在 程每次最多只能运行给定的时间段,在 Linux Linux 中为 中为 200200msms 。当一个进程运行超过 。当一个进程运行超过 200ms 200ms 时,系统选择其他的时,系统选择其他的进程运行,而原有进程则等待下次运行机会。这一时间在进程运行,而原有进程则等待下次运行机会。这一时间在抢先式调度中称为“时间片”。抢先式调度中称为“时间片”。

为了能够为所有的进程平等分配 为了能够为所有的进程平等分配 CPU CPU 资源,内核在 资源,内核在 ttask_struct ask_struct 结构中记录如表结构中记录如表 5.4 5.4 所示的信息。所示的信息。

Page 48: 第 5 章 进程管理及进程间通讯

字段 描述policy(策略) 系统对该进程实施的调度策略。 Linux

进程有两种类型的进程:一般进程和实时进程。实时进程比所有一般进程的优先级高,只有一个实时进程可以运行,调度程序就会选择该进程运行。对实时进程而言,

“ 有两种调度策略,一种称为 循环赛( round robin ” “ ) ,另一种称为 先进先出( first in first out ”) 。

priority(优先级) 这是系统为进程给定的优先级,可通 过系统调用或 renice 命令修改该进程的优

先级。优先级实际是从进程开始运行算起 的、允许进程运行的时间值(以 jiffies

为单位)。

rt_priority(实时优先级) 这是系统为实时进程给定的相对优先级。counter(计数器) 这是进程运行的时间值(以 jiffies

为单位)。开始运行时设置为 priority, 每次时钟中断该值减 1。

表 5.4 和进程调度相关的 task_struct 信息

Page 49: 第 5 章 进程管理及进程间通讯

当需要选择下一个运行进程时,由调度程序选择最值得当需要选择下一个运行进程时,由调度程序选择最值得运行的进程。运行的进程。 Linux Linux 使用了比较简单的基于优先级的进程使用了比较简单的基于优先级的进程调度算法选择新的进程。进程的切换时需要作三个层次的调度算法选择新的进程。进程的切换时需要作三个层次的工作:工作:

(( 11 ) 用户数据的保存:包括正文段) 用户数据的保存:包括正文段 (TEXT)(TEXT) 、数据段、数据段 (D(DATAATA,, BSS)BSS) 、栈段、栈段 (STACK)(STACK) 、共享内存段、共享内存段 (SHARED ME(SHARED MEMORY)MORY) 的保存。的保存。

(( 22 ) 寄存器数据的保存:包括) 寄存器数据的保存:包括 PC(program counterPC(program counter ,,指向下一条要执行的指令的地址指向下一条要执行的指令的地址 )) 、、 PSW(processor statPSW(processor status wordus word ,处理机状态字,处理机状态字 )) 、、 SP(stack pointerSP(stack pointer ,栈指,栈指针针 )) 、、 PCBP(pointer of process control blockPCBP(pointer of process control block,进程控,进程控制块指针制块指针 )) 、、 FP(frame pointerFP(frame pointer ,指向栈中一个函数的,指向栈中一个函数的 llocal ocal 变量的首地址变量的首地址 )) 、 、 AP(augument pointerAP(augument pointer ,指向栈,指向栈中函数调用的实参位置中函数调用的实参位置 )) 、、 ISP(interrupt stack pointerISP(interrupt stack pointer ,,中断栈指针中断栈指针 ) ) 以及其他的通用寄存器等。以及其他的通用寄存器等。

(( 33 ) 系统层次的保存:包括) 系统层次的保存:包括 procproc 、、 ,,虚拟存储空间管虚拟存储空间管理表格和中断处理栈,以便于该进程再一次得到理表格和中断处理栈,以便于该进程再一次得到 CPUCPU 时时间片时能正常运行下去。 间片时能正常运行下去。

Page 50: 第 5 章 进程管理及进程间通讯

当调度程序选择了新的进程之后,它必须在当前进程的 当调度程序选择了新的进程之后,它必须在当前进程的 task_struct task_struct 结构中保存和该进程相关的 结构中保存和该进程相关的 CPU CPU 寄存器和寄存器和其他有关指令执行的上下文信息,然后从选定进程的 其他有关指令执行的上下文信息,然后从选定进程的 tastask_struct k_struct 结构中恢复 结构中恢复 CPU CPU 寄存器以及上下文信息,新的寄存器以及上下文信息,新的进程就可以继续在 进程就可以继续在 CPU CPU 中执行了。中执行了。

对于新建的进程,其 对于新建的进程,其 task_struct task_struct 结构被置为初始的结构被置为初始的执行上下文,当调度进程选择这一新建进程时,首先从 执行上下文,当调度进程选择这一新建进程时,首先从 ttask_struct ask_struct 结构中恢复 结构中恢复 CPU CPU 寄存器,寄存器, CPU CPU 的指令计数的指令计数寄存器(寄存器( PCPC )恰好是该进程的初始执行指令地址,这样,)恰好是该进程的初始执行指令地址,这样,新建的进程就可以从头开始运行了。新建的进程就可以从头开始运行了。

调度程序在如下几种情况下运行:当前进程处于等待状调度程序在如下几种情况下运行:当前进程处于等待状态而放入等待队列时;某个系统调用要返回到用户模式之态而放入等待队列时;某个系统调用要返回到用户模式之前,这是因为系统调用结束时,当前进程的 前,这是因为系统调用结束时,当前进程的 counter counter 值值可能刚好为 可能刚好为 00。下面是调度程序每次运行时要完成的任务:。下面是调度程序每次运行时要完成的任务:

Page 51: 第 5 章 进程管理及进程间通讯

(( 11 ) 调度程序运行底层半处理程序() 调度程序运行底层半处理程序( bottom half hanbottom half handlerdler )处理调度程序的任务队列。这些处理程序实际是一)处理调度程序的任务队列。这些处理程序实际是一些内核线程。些内核线程。

(( 22 ) 在选择其他进程之前,必须处理当前进程。如果当) 在选择其他进程之前,必须处理当前进程。如果当前进程的调度策略为循环赛,则将当前进程放到运行队列前进程的调度策略为循环赛,则将当前进程放到运行队列的尾部;如果该进程是可中断的,并且自上次调度以来接的尾部;如果该进程是可中断的,并且自上次调度以来接收到信号,则任务状态设置为运行;如果当前进程的 收到信号,则任务状态设置为运行;如果当前进程的 coucounter nter 值为 值为 00,则任务状态也变为运行;如果当前进程状,则任务状态也变为运行;如果当前进程状态为运行,则继续保持此状态;如果进程既不处于运行状态为运行,则继续保持此状态;如果进程既不处于运行状态,也不是可中断的,则从运行队列中移去该进程,这表态,也不是可中断的,则从运行队列中移去该进程,这表明调度程序在选择最值得运行的进程时,该进程不被考虑。明调度程序在选择最值得运行的进程时,该进程不被考虑。

Page 52: 第 5 章 进程管理及进程间通讯

(( 33 ) 调度程序在运行队列中搜索最值得运行的程序。调) 调度程序在运行队列中搜索最值得运行的程序。调度程序通过比较权重来选择进程。对实时进程而言,它的度程序通过比较权重来选择进程。对实时进程而言,它的权重为 权重为 counter counter 加 加 10001000 ;对一般进程而言,权重为 ;对一般进程而言,权重为 cocounterunter 。因此,实时进程总是会被认为是最值得运行的进。因此,实时进程总是会被认为是最值得运行的进程。如果当前进程的优先级和其他可运行进程一致,则因程。如果当前进程的优先级和其他可运行进程一致,则因为当前进程至少已花费了一个时间片,因此,总处于劣势。为当前进程至少已花费了一个时间片,因此,总处于劣势。如果许多进程的优先级一样,则调度程序选择运行队列中如果许多进程的优先级一样,则调度程序选择运行队列中最靠前的进程,这实际就是“循环赛”调度。最靠前的进程,这实际就是“循环赛”调度。

(( 44) 如果最值得运行的进程不是当前进程,就需要交换) 如果最值得运行的进程不是当前进程,就需要交换进程(或切换进程)。进程交换的作用是保存当前进程的进程(或切换进程)。进程交换的作用是保存当前进程的运行上下文,同时恢复新进程的运行上下文。交换的具体运行上下文,同时恢复新进程的运行上下文。交换的具体细节和细节和 CPU CPU 类型有关,但需要注意的是,交换进程时调类型有关,但需要注意的是,交换进程时调度程序运行在当前进程的上下文中,另外,调度程序还需度程序运行在当前进程的上下文中,另外,调度程序还需要设置某些关键的 要设置某些关键的 CPU CPU 寄存器并刷新硬件高速缓存。 寄存器并刷新硬件高速缓存。

Page 53: 第 5 章 进程管理及进程间通讯

Linux Linux 内核已具备在对称多处理系统内核已具备在对称多处理系统 SMPSMP (( symmetrisymmetric multiprocessingc multiprocessing )上运行的能力。在多处理器系统中,)上运行的能力。在多处理器系统中,每个处理器都在运行着进程。当运行在某个处理器上的进每个处理器都在运行着进程。当运行在某个处理器上的进程耗尽其时间片,或者该进程处于等待状态时,该处理器程耗尽其时间片,或者该进程处于等待状态时,该处理器将单独运行调度程序来选择新的进程。每个处理器有一个将单独运行调度程序来选择新的进程。每个处理器有一个自己的空闲进程,而每个处理器也有自己的当前进程。为自己的空闲进程,而每个处理器也有自己的当前进程。为了跟踪每个处理器的空闲进程和当前进程,进程的 了跟踪每个处理器的空闲进程和当前进程,进程的 task_task_struct struct 中包含了正在运行该进程的处理器编号(中包含了正在运行该进程的处理器编号( processprocessor or 字段),以及上次运行该进程的处理器编号(字段),以及上次运行该进程的处理器编号( last_prlast_processor ocessor 字段)。显然,当一个进程再次运行时,可由不字段)。显然,当一个进程再次运行时,可由不同的处理器运行,但在不同处理器上的进程交换所需开支同的处理器运行,但在不同处理器上的进程交换所需开支稍微大一些。为此,每个进程有一个 稍微大一些。为此,每个进程有一个 processor_musk processor_musk 字段,如果该字段的第 字段,如果该字段的第 N N 位为 位为 11 ,则该进程可以运行在,则该进程可以运行在第 第 N N 个进程上,利用这一字段,就可以将某个进程限制个进程上,利用这一字段,就可以将某个进程限制在单个处理器上运行。在单个处理器上运行。

Page 54: 第 5 章 进程管理及进程间通讯

5.3 5.3 可执行程序可执行程序

和 和 Unix Unix 类似,类似, Linux Linux 中的程序和命令通常由命令解中的程序和命令通常由命令解释器执行,这一命令解释器称为 释器执行,这一命令解释器称为 shellshell 。用户输入命令之。用户输入命令之后,后, shell shell 会在搜索路径(会在搜索路径( shell shell 变量变量 PATHPATH中包含搜索中包含搜索路径)指定的目录中搜索和输入命令匹配的映像名称。如路径)指定的目录中搜索和输入命令匹配的映像名称。如果发现匹配的映像,果发现匹配的映像, shell shell 负责装载并执行该命令。负责装载并执行该命令。 shelshell l 首先利用 首先利用 forkfork 系统调用建立子进程,然后用找到的可系统调用建立子进程,然后用找到的可执行映像文件覆盖子进程正在执行的 执行映像文件覆盖子进程正在执行的 shell shell 二进制映像。二进制映像。

可执行文件可以是具有不同格式的二进制文件,也可以可执行文件可以是具有不同格式的二进制文件,也可以是一个文本的脚本文件。可执行映像文件中包含了可执行是一个文本的脚本文件。可执行映像文件中包含了可执行代码及数据,同时也包含操作系统用来将映像正确装入内代码及数据,同时也包含操作系统用来将映像正确装入内存并执行的信息。存并执行的信息。 Linux Linux 使用的最常见的可执行文件格式使用的最常见的可执行文件格式是可执行可连接格式(是可执行可连接格式( Executable and Linking FormatExecutable and Linking Format,,ELFELF)和 )和 a.outa.out。理论上,。理论上, Linux Linux 有足够的灵活性可以有足够的灵活性可以装入任何格式的可执行文件。 装入任何格式的可执行文件。

Page 55: 第 5 章 进程管理及进程间通讯

5.3.1 5.3.1 可执行可连接格式 可执行可连接格式

可执行可连接格式(可执行可连接格式( Executable and Linking FormExecutable and Linking Formatat,, ELFELF)由 )由 Unix Unix 系统实验室制定。它是 系统实验室制定。它是 Linux Linux 中最中最经常使用的格式。和其他格式(例如 经常使用的格式。和其他格式(例如 a.out a.out 或 或 ECOFF ECOFF 格式)比较,格式)比较, ELF ELF 在装入内存时多一些系统开支,但是在装入内存时多一些系统开支,但是更为灵活。更为灵活。 ELF ELF 可执行文件包含了可执行代码和数据,可执行文件包含了可执行代码和数据,通常也称为正文和数据。这种文件中包含了几个表,根据通常也称为正文和数据。这种文件中包含了几个表,根据这些表中的信息,内核可组织进程的虚拟内存。另外,文这些表中的信息,内核可组织进程的虚拟内存。另外,文件中还包含有对内存布局的定义以及起始执行的指令位置。件中还包含有对内存布局的定义以及起始执行的指令位置。

Page 56: 第 5 章 进程管理及进程间通讯

分析如下简单程序分析如下简单程序在利用编译器编译并连在利用编译器编译并连接之后的 接之后的 ELFELF 文件:文件:

#include <stdio.h>#include <stdio.h>main ()main (){{ printf printf(“(“ Hello worHello world!\n”ld!\n” )) ;;}}

ELF 可执行映象e_idente_entrye_phoffe_phentsize

e_phnum

代码

数据

p_typep_offsetp_vaddrp_fileszp_memsz

p_flags

p_typep_offsetp_vaddrp_fileszp_memsz

p_flags

‘E’ ‘L’ ‘F’0x804809052322

PT_LOAD00x80480006853268532PF_R, PF_X

PT_LOAD685360x8059BB822004248PF_R, PF_W

物理头

物理头

图 5.7 一个简单的 ELF 可执行文件的布局

Page 57: 第 5 章 进程管理及进程间通讯

图图 5.75.7是上述源代码在编译连接后的 是上述源代码在编译连接后的 ELF ELF 可执行文件可执行文件的格式。可以看出,的格式。可以看出, ELF ELF 可执行映像文件的开头是三个可执行映像文件的开头是三个字符 ‘字符 ‘ E’E’、‘、‘ L’ L’ 和 ‘和 ‘ F’F’,作为这类文件的标识符。,作为这类文件的标识符。e_entry e_entry 定义了程序装入之后起始执行指令的虚拟地址。定义了程序装入之后起始执行指令的虚拟地址。这个简单的 这个简单的 ELF ELF 映像利用两个“物理头”结构分别定义映像利用两个“物理头”结构分别定义代码和数据,代码和数据, e_phnum e_phnum 是该文件中所包含的物理头信息是该文件中所包含的物理头信息个数,本例为 个数,本例为 22 。。 e_phyoff e_phyoff 是第一个物理头结构在文件是第一个物理头结构在文件中的偏移量,而中的偏移量,而 e_phentsize e_phentsize 则是物理头结构的大小,则是物理头结构的大小,这两个偏移量均从文件头开始算起。根据上述两个信息,这两个偏移量均从文件头开始算起。根据上述两个信息,内核可正确读取两个物理头结构中的信息。内核可正确读取两个物理头结构中的信息。

Page 58: 第 5 章 进程管理及进程间通讯

物理头结构的 物理头结构的 p_flags p_flags 字段定义了对应代码或数据的字段定义了对应代码或数据的访问属性。图中第一个 访问属性。图中第一个 p_flags p_flags 字段的值为 字段的值为 FP_X FP_X 和 和 FPFP_R_R,表明该结构定义的是程序的代码;类似地,第二个,表明该结构定义的是程序的代码;类似地,第二个物理头定义程序数据,并且是可读可写的。物理头定义程序数据,并且是可读可写的。 p_offset p_offset 定定义对应的代码或数据在物理头之后的偏移量。义对应的代码或数据在物理头之后的偏移量。 p_vaddr p_vaddr 定义代码或数据的起始虚拟地址。定义代码或数据的起始虚拟地址。 p_filesz p_filesz 和 和 p_memsz p_memsz 分别定义代码或数据在文件中的大小以及在内存中的大小。分别定义代码或数据在文件中的大小以及在内存中的大小。对的简单例子,程序代码开始于两个物理头之后,而程序对的简单例子,程序代码开始于两个物理头之后,而程序数据则开始于物理头之后的第 数据则开始于物理头之后的第 0x68533 0x68533 字节处,显然,字节处,显然,程序数据紧跟在程序代码之后。程序的代码大小为 程序数据紧跟在程序代码之后。程序的代码大小为 0x6850x6853232 ,显得比较大,这是因为连接程序将 ,显得比较大,这是因为连接程序将 C C 函数 函数 printfprintf 的代码连接到了 的代码连接到了 ELF ELF 文件的原因。程序代码的文件大小文件的原因。程序代码的文件大小和内存大小是一样的,而程序数据的文件大小和内存大小和内存大小是一样的,而程序数据的文件大小和内存大小不一样,这是因为内存数据中,起始的 不一样,这是因为内存数据中,起始的 2200 2200 字节是预先字节是预先初始化的数据,初始化值来自 初始化的数据,初始化值来自 ELF ELF 映像,而其后的 映像,而其后的 2042048 8 字节则由执行代码初始化。字节则由执行代码初始化。

Page 59: 第 5 章 进程管理及进程间通讯

Linux Linux 利用需求分页技术装入程序映像。当 利用需求分页技术装入程序映像。当 shell shell 进程进程利用 利用 forkfork 系统调用建立了子进程之后,子进程会调用 系统调用建立了子进程之后,子进程会调用 eexecxec 系统调用(实际有多种 系统调用(实际有多种 execexec 调用),调用), execexec 系统调系统调用将利用 用将利用 ELF ELF 二进制格式装载器装载 二进制格式装载器装载 ELF ELF 映像,当装载映像,当装载器检验映像是有效的 器检验映像是有效的 ELF ELF 文件之后,就会将当前进程文件之后,就会将当前进程(实际就是父进程或旧进程)的可执行映像从虚拟内存中(实际就是父进程或旧进程)的可执行映像从虚拟内存中清除,同时清除任何信号处理程序并关闭所有打开的文件清除,同时清除任何信号处理程序并关闭所有打开的文件(把相应 (把相应 file file 结构中的 结构中的 f_count f_count 引用计数减 引用计数减 11 ,如果这,如果这一计数为 一计数为 00,内核负责释放这一文件对象),然后重置进,内核负责释放这一文件对象),然后重置进程页表。完成上述过程之后,只需根据 程页表。完成上述过程之后,只需根据 ELF ELF 文件中的信文件中的信息将映像代码和数据的起始和终止地址分配并设置相应的息将映像代码和数据的起始和终止地址分配并设置相应的虚拟地址区域,修改进程页表。这时,当前进程就可以开虚拟地址区域,修改进程页表。这时,当前进程就可以开始执行对应的 始执行对应的 ELF ELF 映像中的指令了。映像中的指令了。

Page 60: 第 5 章 进程管理及进程间通讯

和静态连接库不同,动态连接库可在运行时连接到进和静态连接库不同,动态连接库可在运行时连接到进程虚拟地址中。对于使用同一动态连接库的多个进程,只程虚拟地址中。对于使用同一动态连接库的多个进程,只需在内存中保留一份共享库信息即可,这样就节省了内存需在内存中保留一份共享库信息即可,这样就节省了内存空间。当共享库需要在运行时连接到进程虚拟地址时,空间。当共享库需要在运行时连接到进程虚拟地址时, LiLinux nux 的动态连接器利用 的动态连接器利用 ELF ELF 共享库中的符号表完成连接共享库中的符号表完成连接工作,符号表中定义了 工作,符号表中定义了 ELF ELF 映像引用的全部动态库例程。映像引用的全部动态库例程。Linux Linux 的动态连接器一般包含在 的动态连接器一般包含在 /lib /lib 目录中,通常为 目录中,通常为 ld.ld.so.1so.1 、、 llibc.so.1 llibc.so.1 和和 ld-Linux.so.1ld-Linux.so.1 。 。

Page 61: 第 5 章 进程管理及进程间通讯

5.3.2 5.3.2 脚本文件脚本文件 脚本文件实际是一些可执行的命令,这些命令一般由指脚本文件实际是一些可执行的命令,这些命令一般由指

定的解释器解释并执行。定的解释器解释并执行。 Linux Linux 中常见的解释器有 中常见的解释器有 wishwish 、、perlperl 以及命令 以及命令 shellshell ,如 ,如 bash bash 等等。。

一般来说,脚本文件的第一行用来指定脚本的解释程序,一般来说,脚本文件的第一行用来指定脚本的解释程序,例如:例如:

#!/usr/bin/wish#!/usr/bin/wish 这行内容指定由 这行内容指定由 wishwish 作为该脚本的命令解释器。脚作为该脚本的命令解释器。脚

本的二进制装载器利用这一信息搜索解释器,如果能够找本的二进制装载器利用这一信息搜索解释器,如果能够找到指定的解释器,该装载器就和上述执行 到指定的解释器,该装载器就和上述执行 ELF ELF 程序的装程序的装载过程一样装载并执行解释器。脚本文件名成为传递给解载过程一样装载并执行解释器。脚本文件名成为传递给解释器的第一个命令参数,而最初的第一个参数则成为现在释器的第一个命令参数,而最初的第一个参数则成为现在的第二个参数,依此类推。为解释器传递了正确的命令参的第二个参数,依此类推。为解释器传递了正确的命令参数后,就可由脚本解释器执行脚本。数后,就可由脚本解释器执行脚本。

Page 62: 第 5 章 进程管理及进程间通讯

5.4 Linux5.4 Linux 下进程间通信的主要手段 下进程间通信的主要手段 LinuxLinux下的进程通信手段基本上是从下的进程通信手段基本上是从 UnixUnix平台上的进程平台上的进程

通信手段继承而来的。而通信手段继承而来的。而 AT&TAT&T的贝尔实验室及的贝尔实验室及 BSDBSD(加州大学伯克利分校的伯克利软件发布中心)在进程间(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对通信方面的侧重点有所不同。前者对 UnixUnix早期的进程间早期的进程间通信手段进行了系统的改进和扩充,形成了“通信手段进行了系统的改进和扩充,形成了“ system V Isystem V IPC”PC” ,通信进程局限在单个计算机内;后者则跳过了该,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接字(限制,形成了基于套接字( socketsocket)的进程间通信机制。)的进程间通信机制。LinuxLinux 则把两者都继承了下来。则把两者都继承了下来。

LinuxLinux下进程间通信的几种主要手段包括:下进程间通信的几种主要手段包括: (( 11 )管道()管道( pipepipe )及有名管道()及有名管道( named pipenamed pipe ):管):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信; 它还允许无亲缘关系进程间的通信;

Page 63: 第 5 章 进程管理及进程间通讯

(( 22 )信号()信号( signalsignal ):信号是比较复杂的通信方式,):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;外,进程还可以发送信号给进程本身; LinuxLinux除了支持除了支持 UUnixnix 早期信号语义函数早期信号语义函数 sigalsigal外,还支持语义符合外,还支持语义符合 Posix.1Posix.1标准的信号函数标准的信号函数 sigactionsigaction (实际上,该函数是基于(实际上,该函数是基于 BSDBSD的,的, BSDBSD为了实现可靠信号机制,又能够统一对外接口,为了实现可靠信号机制,又能够统一对外接口,用用 sigactionsigaction函数重新实现了函数重新实现了 signalsignal函数); 函数);

(( 33 )报文()报文( messagemessage )队列(消息队列):消息队列)队列(消息队列):消息队列是消息的链接表,包括是消息的链接表,包括 PosixPosix消息队列消息队列 system Vsystem V消息队消息队列。有足够权限的进程可以向队列中添加消息,被赋予读列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 大小受限等缺点。

(( 44)共享内存:使得多个进程可以访问同一块内存空间,)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用是最快的可用 IPCIPC形式。是针对其他通信机制运行效率较形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。 来达到进程间的同步及互斥。

Page 64: 第 5 章 进程管理及进程间通讯

(( 55)信号量()信号量( semaphoresemaphore ):主要作为进程间以及同):主要作为进程间以及同一进程不同线程之间的同步手段。 一进程不同线程之间的同步手段。

(( 66)套接字()套接字( socketsocket):更为一般的进程间通信机制,):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由可用于不同机器之间的进程间通信。起初是由 UnixUnix 系统系统的的 BSDBSD分支开发出来的,但现在一般可以移植到其它类分支开发出来的,但现在一般可以移植到其它类UnixUnix 系统上:系统上: LinuxLinux 和和 System VSystem V的变种都支持套接字。 的变种都支持套接字。

Page 65: 第 5 章 进程管理及进程间通讯

5.4.1 5.4.1 信号信号

信号是信号是 Unix Unix 系统中最古老的进程间通讯机制系统中最古老的进程间通讯机制之一,它主要用来向进程发送异步的事件信号。之一,它主要用来向进程发送异步的事件信号。键盘中断可能产生信号,而浮点运算溢出或者内键盘中断可能产生信号,而浮点运算溢出或者内存访问错误等也可产生信号。存访问错误等也可产生信号。 shell shell 通常利用信通常利用信号向子进程发送作业控制命令。号向子进程发送作业控制命令。

在 在 Linux Linux 中,信号种类的数目和具体的平台有中,信号种类的数目和具体的平台有关,因为内核用一个字代表所有的信号,因此字关,因为内核用一个字代表所有的信号,因此字的位数就是信号种类的最多数目。对 的位数就是信号种类的最多数目。对 32 32 位的 位的 i3i386 86 平台而言,一个字为 平台而言,一个字为 32 32 位,因此信号有 位,因此信号有 32 32 种。种。 Linux Linux 内核定义的最常见的信号、内核定义的最常见的信号、 C C 语言宏语言宏名及其用途如表名及其用途如表 5.5 5.5 所示:所示:

Page 66: 第 5 章 进程管理及进程间通讯

进程可以选择对某种信号所采取的特定操作,这些操作包括:进程可以选择对某种信号所采取的特定操作,这些操作包括: (1) (1) 忽略信号。进程可忽略产生的信号,但 忽略信号。进程可忽略产生的信号,但 SIGKILL SIGKILL 和 和 SIGSTOP SIGSTOP

信号不能被忽略。信号不能被忽略。 (2) (2) 阻塞信号。进程可选择阻塞某些信号。阻塞信号。进程可选择阻塞某些信号。 (3) (3) 由进程处理该信号。进程本身可在系统中注册处理信号的处理由进程处理该信号。进程本身可在系统中注册处理信号的处理

程序地址,当发出该信号时,由注册的处理程序处理信号。程序地址,当发出该信号时,由注册的处理程序处理信号。

值 C 语言宏名 用途1 SIGHUP 从终端上发出的结束信号

2 SIGINT 来自键盘的中断信号( Ctrl-c)

3 SIGQUIT 来自键盘的退出信号( Ctrl-\)

8 SIGFPE 浮点异常信号(例如浮点运算溢出)

9 SIGKILL 该信号结束接收信号的进程

14 SIGALRM 进程的定时器到期时,发送该信号

15 SIGTERM kill 命令发出的信号

17 SIGCHLD 标识子进程停止或结束的信号

19 SIGSTOP 来自键盘( Ctrl-z)或调试程序的停止执行信号

Page 67: 第 5 章 进程管理及进程间通讯

(4) (4) 由内核进行默认处理。信号由内核的默认处理程序处由内核进行默认处理。信号由内核的默认处理程序处理。大多数情况下,信号由内核处理。理。大多数情况下,信号由内核处理。

需要注意的是,需要注意的是, Linux Linux 内核中不存在任何机制用来区分内核中不存在任何机制用来区分不同信号的优先级。也就是说,当同时有多个信号发出时,不同信号的优先级。也就是说,当同时有多个信号发出时,进程可能会以任意顺序接收到信号并进行处理。另外,如进程可能会以任意顺序接收到信号并进行处理。另外,如果进程在处理某个信号之前,又有相同的信号发出,则进果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。产生上述现象的原因与内核对信程只能接收到一个信号。产生上述现象的原因与内核对信号的实现有关,将在下面解释。号的实现有关,将在下面解释。

Page 68: 第 5 章 进程管理及进程间通讯

系统在 系统在 task_struct task_struct 结构中利用两个字分别记录当前结构中利用两个字分别记录当前挂起的信号(挂起的信号( signalsignal )以及当前阻塞的信号()以及当前阻塞的信号( blockeblockedd )。挂起的信号指尚未进行处理的信号。阻塞的信号指)。挂起的信号指尚未进行处理的信号。阻塞的信号指进程当前不处理的信号,如果产生了某个当前被阻塞的信进程当前不处理的信号,如果产生了某个当前被阻塞的信号,则该信号会一直保持挂起,直到该信号不再被阻塞为号,则该信号会一直保持挂起,直到该信号不再被阻塞为止。除了 止。除了 SIGKILL SIGKILL 和 和 SIGSTOP SIGSTOP 信号外,所有的信号均信号外,所有的信号均可以被阻塞,信号的阻塞可通过系统调用实现。每个进程可以被阻塞,信号的阻塞可通过系统调用实现。每个进程的 的 task_struct task_struct 结构中还包含了一个指向 结构中还包含了一个指向 sigaction sigaction 结结构数组的指针,该结构数组中的信息实际指定了进程处理构数组的指针,该结构数组中的信息实际指定了进程处理所有信号的方式。如果某个 所有信号的方式。如果某个 sigaction sigaction 结构中包含有处理结构中包含有处理信号的例程地址,则由该处理例程处理该信号;反之,则信号的例程地址,则由该处理例程处理该信号;反之,则根据结构中的一个标志或者由内核进行默认处理,或者只根据结构中的一个标志或者由内核进行默认处理,或者只是忽略该信号。通过系统调用,进程可以修改 是忽略该信号。通过系统调用,进程可以修改 sigaction sigaction 结构数组的信息,从而指定进程处理信号的方式。结构数组的信息,从而指定进程处理信号的方式。

Page 69: 第 5 章 进程管理及进程间通讯

进程不能向系统中所有的进程发送信号,一般而言,除进程不能向系统中所有的进程发送信号,一般而言,除系统和超级用户外,普通进程只能向具有相同 系统和超级用户外,普通进程只能向具有相同 uid uid (( ususer IDer ID)和 )和 gidgid (( group IDgroup ID)的进程,或者处于同一进程)的进程,或者处于同一进程组的进程发送信号。产生信号时,内核将进程 组的进程发送信号。产生信号时,内核将进程 task_strutask_struct ct 的 的 signal signal 字中的相应位设置为 字中的相应位设置为 11 ,从而表明产生了,从而表明产生了该信号。系统不对置位之前该位已经为 该信号。系统不对置位之前该位已经为 11 的情况进行处理,的情况进行处理,因而进程无法接收到前一次信号。如果进程当前没有阻塞因而进程无法接收到前一次信号。如果进程当前没有阻塞该信号,并且进程正处于可中断的等待状态,则内核将该该信号,并且进程正处于可中断的等待状态,则内核将该进程的状态改变为运行,并放置在运行队列中。这样,调进程的状态改变为运行,并放置在运行队列中。这样,调度程序在进行调度时,就有可能选择该进程运行,从而可度程序在进行调度时,就有可能选择该进程运行,从而可以让进程处理该信号。以让进程处理该信号。

发送给某个进程的信号并不会立即得到处理。而只有该发送给某个进程的信号并不会立即得到处理。而只有该进程再次运行时,才有机会处理该信号。每次进程从系统进程再次运行时,才有机会处理该信号。每次进程从系统调用中退出时,内核会检查它的 调用中退出时,内核会检查它的 signal signal 和 和 block block 字段,字段,如果有任何一个未被阻塞的信号发出,内核就根据 如果有任何一个未被阻塞的信号发出,内核就根据 sigactsigaction ion 结构数组中的信息进行处理。处理过程如下:结构数组中的信息进行处理。处理过程如下:

Page 70: 第 5 章 进程管理及进程间通讯

(1)(1) 检查对应的 检查对应的 sigaction sigaction 结构,如果该信号不是 结构,如果该信号不是 SIGSIGKILL KILL 或 或 SIGSTOP SIGSTOP 信号,且被忽略,则不处理该信号。信号,且被忽略,则不处理该信号。

(2)(2) 如果该信号利用默认的处理程序处理,则由内核处如果该信号利用默认的处理程序处理,则由内核处理该信号,否则转向第 理该信号,否则转向第 3 3 步。步。

(3)(3) 该信号由进程自己的处理程序处理,内核将修改当该信号由进程自己的处理程序处理,内核将修改当前进程的调用堆栈帧,并将进程的程序计数寄存器修改为前进程的调用堆栈帧,并将进程的程序计数寄存器修改为信号处理程序的入口地址。此后,指令将跳转到信号处理信号处理程序的入口地址。此后,指令将跳转到信号处理程序,当从信号处理程序中返回时,实际就返回了进程的程序,当从信号处理程序中返回时,实际就返回了进程的用户模式部分。用户模式部分。

Linux Linux 是与可移植操作系统接口(是与可移植操作系统接口( Portable Operating Portable Operating System InterfaceSystem Interface ,, POSIXPOSIX)国际标准兼容的。因此,)国际标准兼容的。因此,进程在处理某个信号时,还可以修改进程的进程在处理某个信号时,还可以修改进程的 blockedblocked掩码。掩码。但是,当信号处理程序返回时,但是,当信号处理程序返回时, blocked blocked 值必须恢复为值必须恢复为原有的掩码值,这一任务由内核完成。原有的掩码值,这一任务由内核完成。 Linux Linux 在进程的调在进程的调用堆栈帧中添加了对清理程序的调用,该清理程序可以恢用堆栈帧中添加了对清理程序的调用,该清理程序可以恢复原有的 复原有的 blocked blocked 掩码值。当内核在处理信号时,可能掩码值。当内核在处理信号时,可能同时有多个信号需要由用户处理程序处理,这时,同时有多个信号需要由用户处理程序处理,这时, Linux Linux 内核可以将所有的信号处理程序地址推入堆栈帧,而当所内核可以将所有的信号处理程序地址推入堆栈帧,而当所有的信号处理完毕后,调用清理程序恢复原先的 有的信号处理完毕后,调用清理程序恢复原先的 blocked blocked 值。值。

Page 71: 第 5 章 进程管理及进程间通讯

5.4.2 5.4.2 管道和套接字管道和套接字

管道是 管道是 Linux Linux 中最常用的 中最常用的 IPC IPC 机制。利用管道时,一机制。利用管道时,一个进程的输出可成为另外一个进程的输入。当输入输出的个进程的输出可成为另外一个进程的输入。当输入输出的数据量特别大时,这种 数据量特别大时,这种 IPC IPC 机制非常有用。可以想象,机制非常有用。可以想象,如果没有管道机制,而必须利用文件传递大量数据时,会如果没有管道机制,而必须利用文件传递大量数据时,会造成许多空间和时间上的浪费。造成许多空间和时间上的浪费。

在 在 Linux Linux 中,通过将两个 中,通过将两个 file file 结构指向同一个临时的 结构指向同一个临时的 VFS VFS 索引节点,而两个 索引节点,而两个 VFS VFS 索引节点又指向同一个物理索引节点又指向同一个物理页而实现管道。如图页而实现管道。如图 5.8 5.8 所示。所示。

Page 72: 第 5 章 进程管理及进程间通讯

进程 1

file 结构

f_mode

f_pos

f_flags

f_count

f_owner

f_inode

f_version

f_op

进程 2

file 结构

f_mode

f_pos

f_flags

f_count

f_owner

f_inode

f_version

f_op

inode

数据页

管道写操作集 管道读操作集

图 5.8 管道示意图

Page 73: 第 5 章 进程管理及进程间通讯

图图 5.8 5.8 中,每个 中,每个 file file 数据结构定义不同的文件操作例数据结构定义不同的文件操作例程地址,其中一个用来向管道中写入数据,而另外一个用程地址,其中一个用来向管道中写入数据,而另外一个用来从管道中读出数据。这样,用户程序的系统调用仍然是来从管道中读出数据。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。管道写函数通过将字节复制到 这一特殊操作。管道写函数通过将字节复制到 VFS VFS 索引索引节点指向的物理内存而写入数据,而管道读函数则通过复节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。队列和信号。

当写进程向管道中写入时,它利用标准的库函数,系统当写进程向管道中写入时,它利用标准的库函数,系统根据库函数传递的文件描述符,可找到该文件的 根据库函数传递的文件描述符,可找到该文件的 file file 结结构。构。 file file 结构中指定了用来进行写操作的函数(即写入函结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 在向内存中写入数据之前,必须首先检查 VFS VFS 索引节点索引节点中的信息,同时满足如下条件时,才能进行实际的内存复中的信息,同时满足如下条件时,才能进行实际的内存复制工作:制工作:

Page 74: 第 5 章 进程管理及进程间通讯

(( 11 ) 内存中有足够的空间可容纳所有要写入的数据;) 内存中有足够的空间可容纳所有要写入的数据; (( 22 ) 内存没有被读程序锁定。) 内存没有被读程序锁定。 如果同时满足上述条件,写入函数首先锁定内存,然后如果同时满足上述条件,写入函数首先锁定内存,然后

从写进程的地址空间中复制数据到内存。否则,写入进程从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 就休眠在 VFS VFS 索引节点的等待队列中,接下来,内核将索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。唤醒。

管道的读取过程和写入过程类似。但是,进程可以在没管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。而共享数据页也被释放。

Page 75: 第 5 章 进程管理及进程间通讯

Linux Linux 还支持另外一种管道形式,称为命名管道或 还支持另外一种管道形式,称为命名管道或 FIFFIFOO(( first-in first-out)first-in first-out) ,这是因为这种管道的操作方式基,这是因为这种管道的操作方式基于“先进先出”原理。上面讲述的管道类型也被称为“匿于“先进先出”原理。上面讲述的管道类型也被称为“匿名管道”。命名管道中,首先写入管道的数据是首先被读名管道”。命名管道中,首先写入管道的数据是首先被读出的数据。匿名管道是临时对象,而 出的数据。匿名管道是临时对象,而 FIFO FIFO 则是文件系统则是文件系统的真正实体,用 的真正实体,用 mkfifomkfifo 命令可建立管道。如果进程有足命令可建立管道。如果进程有足够的权限就可以使用 够的权限就可以使用 FIFOFIFO。。 FIFO FIFO 和匿名管道的数据结和匿名管道的数据结构以及操作极其类似,二者的主要区别在于,构以及操作极其类似,二者的主要区别在于, FIFO FIFO 在使在使用之前就已存在,用户可打开或关闭 用之前就已存在,用户可打开或关闭 FIFOFIFO;而匿名管道;而匿名管道在只在操作时存在,是临时对象。在只在操作时存在,是临时对象。

套接字可以说是网络编程中一个非常重要的概念,套接字可以说是网络编程中一个非常重要的概念, LinLinuxux 以文件的形式实现套接字,与套接字相应的文件是套以文件的形式实现套接字,与套接字相应的文件是套接字文件系统接字文件系统 sockfssockfs ,创建一个套接字就是在,创建一个套接字就是在 sockfssockfs 中中创建一个特殊文件,并建立起为实现套接字功能的相关数创建一个特殊文件,并建立起为实现套接字功能的相关数据结构。换句话说,对每一个新创建的据结构。换句话说,对每一个新创建的 BSDBSD套接字,套接字, LinLinuxux 内核都将在内核都将在 sockfssockfs 特殊文件系统中创建一个新的索引特殊文件系统中创建一个新的索引节点。描述套接字的数据结构是节点。描述套接字的数据结构是 socketsocket,将在第,将在第 88章讨章讨论。 论。

Page 76: 第 5 章 进程管理及进程间通讯

一个套接字可以看作是进程间通信的端点(一个套接字可以看作是进程间通信的端点( endpoinendpointt),每个套接字的名字都是唯一的,其他进程可以发现、),每个套接字的名字都是唯一的,其他进程可以发现、连接并且与之通信。通信域用来说明套接字通信的协议,连接并且与之通信。通信域用来说明套接字通信的协议,不同的通信域有不同的通信协议以及套接字的地址结构等不同的通信域有不同的通信协议以及套接字的地址结构等等,因此,创建一个套接字时,要指明它的通信域。比较等,因此,创建一个套接字时,要指明它的通信域。比较常见的是常见的是 UnixUnix域套接字(采用套接字机制实现单机内的域套接字(采用套接字机制实现单机内的进程间通信)及网际通信域套接字。进程间通信)及网际通信域套接字。

Page 77: 第 5 章 进程管理及进程间通讯

5.5 System V 5.5 System V 的 的 IPC IPC 机制机制

为了和其他系统保持兼容,为了和其他系统保持兼容, Linux Linux 也提供三种首先出也提供三种首先出现在 现在 Unix System V Unix System V 中的 中的 IPC IPC 机制。这三种机制分别是:机制。这三种机制分别是:消息队列、信号量以及共享内存。消息队列、信号量以及共享内存。 System V IPC System V IPC 机制主机制主要有如下特点:要有如下特点:

(1) (1) 如果进程要访问 如果进程要访问 System V IPC System V IPC 对象,则需要在系统对象,则需要在系统调用中传递唯一的引用标识符。调用中传递唯一的引用标识符。

(2) (2) 对 对 System V IPC System V IPC 对象的访问,必须经过类似文件访对象的访问,必须经过类似文件访问的许可检验。对这些对象访问权限的设置由对象的创建问的许可检验。对这些对象访问权限的设置由对象的创建者利用系统调用设置。者利用系统调用设置。

(3) (3) 对象的引用标识符由 对象的引用标识符由 IPC IPC 机制作为访问对象表的索引,机制作为访问对象表的索引,但需要一些操作来生成索引。但需要一些操作来生成索引。

Page 78: 第 5 章 进程管理及进程间通讯

在 在 Linux Linux 中,所有表示中,所有表示 System V IPC System V IPC 对象的对象的数据结构中都包含一个 数据结构中都包含一个 ipc_perm ipc_perm 结构,该结构结构,该结构中包含了作为对象所有者和创建者的进程之用户中包含了作为对象所有者和创建者的进程之用户标识符和组标识符,以及对象的访问模式和对象标识符和组标识符,以及对象的访问模式和对象的访问键。访问键用来定位的访问键。访问键用来定位 System V IPC System V IPC 对象对象的引用标识符。系统支持两种访问键:公有和私的引用标识符。系统支持两种访问键:公有和私有。如果键是公有的,则系统中所有的进程通过有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到权限检查后,均可以找到 System V IPC System V IPC 对象的对象的引用标识符。但是,只能通过引用标识符引用 引用标识符。但是,只能通过引用标识符引用 SySystem V IPC stem V IPC 对象。对象。

Linux Linux 对这些 对这些 IPC IPC 机制的实施大同小异,在这机制的实施大同小异,在这里只主要介绍其中两种:消息队列和信号量。里只主要介绍其中两种:消息队列和信号量。

Page 79: 第 5 章 进程管理及进程间通讯

5.5.1 5.5.1 消息队列消息队列

一个或多个进程可向消息队列写入消息,而一个或多个一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户使用在客户 //服务器模型中,客户向服务器发送请求消息,服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求。在许多微内核结构的操服务器读取消息并执行相应请求。在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队作系统中,内核和各组件之间的基本通讯方式就是消息队列。例如,在 列。例如,在 MINIX MINIX 操作系统中,内核、操作系统中,内核、 I/O I/O 任务、服任务、服务器进程和用户进程之间就是通过消息队列实现通讯的。务器进程和用户进程之间就是通过消息队列实现通讯的。

Linux Linux 为系统中所有的消息队列维护一个 为系统中所有的消息队列维护一个 msgque msgque 链链表,该链表中的每个指针指向一个 表,该链表中的每个指针指向一个 msgid_ds msgid_ds 结构,该结构,该结构完整描述一个消息队列。当建立一个消息队列时,系结构完整描述一个消息队列。当建立一个消息队列时,系统从内存中分配一个 统从内存中分配一个 msgid_ds msgid_ds 结构并将指针添加到 结构并将指针添加到 mmsgque sgque 链表。链表。

Page 80: 第 5 章 进程管理及进程间通讯

图图 5.9 5.9 是 是 msgid_ds msgid_ds 结构的示意图。从图中可以看出,结构的示意图。从图中可以看出,每个 每个 msgid_ds msgid_ds 结构都包含一个 结构都包含一个 ipc_perm ipc_perm 结构以及指结构以及指向该队列所包含的消息指针,显然,队列中的消息构成了向该队列所包含的消息指针,显然,队列中的消息构成了一个链表。另外,一个链表。另外, Linux Linux 还在 还在 msgid_ds msgid_ds 结构中包含一结构中包含一些有关修改时间之类的信息,同时包含两个等待队列,分些有关修改时间之类的信息,同时包含两个等待队列,分别用于队列的写入进程和队列的读取进程。别用于队列的写入进程和队列的读取进程。

msqid_ds

ipc

*msg_last

*msg_first

times

*wwait

*rwait

msg_qnum

msg

*msg_next

msg_type

*msg_spot

msg_stime

massage

msg_ts

msg_ts

msg

*msg_next

msg_type

*msg_spot

msg_stime

massage

msg_ts

msg_ts

msg_qnum

图 5.9 System V IPC 机制--消息队列

Page 81: 第 5 章 进程管理及进程间通讯

消息队列的写入操作和读取操作是类似的,以消息的写消息队列的写入操作和读取操作是类似的,以消息的写入为例,步骤如下:入为例,步骤如下:

(( 11 ) 当某个进程要写入消息时,该进程的有效 ) 当某个进程要写入消息时,该进程的有效 uid uid 和 和 gid gid 首先要和 首先要和 ipc_perm ipc_perm 中的访问模式进行比较。如果中的访问模式进行比较。如果进程不能写入,系统调用返回错误,写操作结束。进程不能写入,系统调用返回错误,写操作结束。

(( 22 ) 如果该进程可以向消息队列写入,则消息可以复制) 如果该进程可以向消息队列写入,则消息可以复制到消息队列的末尾。在进行复制之前,必须判断消息队列到消息队列的末尾。在进行复制之前,必须判断消息队列当前是否已满。消息的具体内容和应用程序有关,由参与当前是否已满。消息的具体内容和应用程序有关,由参与通讯的进程约定。通讯的进程约定。

(( 33 ) 如果消息队列中当前没有空间容纳消息,则写入进) 如果消息队列中当前没有空间容纳消息,则写入进程被添加到该消息队列的写等待队列,否则,内核分配一程被添加到该消息队列的写等待队列,否则,内核分配一个 个 msg msg 结构,将消息从进程的地址空间中复制到 结构,将消息从进程的地址空间中复制到 msg msg 结构,然后将 结构,然后将 msg msg 添加到队列末尾,这时,系统调用成添加到队列末尾,这时,系统调用成功返回,写操作结束。功返回,写操作结束。

(( 44) 调用调度程序,调度程序选择其他进程运行,写操) 调用调度程序,调度程序选择其他进程运行,写操作结束。作结束。

Page 82: 第 5 章 进程管理及进程间通讯

如果有某个进程从消息队列中读取了消息,则系统会唤如果有某个进程从消息队列中读取了消息,则系统会唤醒写等待队列中的进程。醒写等待队列中的进程。

读取操作和写入操作类似,但进程在没有消息或没有指读取操作和写入操作类似,但进程在没有消息或没有指定类型的消息时进入等待状态。定类型的消息时进入等待状态。

Page 83: 第 5 章 进程管理及进程间通讯

5.5.2 5.5.2 信号量信号量

信号量实际是一个整数。进程在信号量上的操作分两种,信号量实际是一个整数。进程在信号量上的操作分两种,一种称为 一种称为 downdown ,而另外一种称为 ,而另外一种称为 upup 。。 down down 操作的操作的结果是让信号量的值减 结果是让信号量的值减 11 ,, up up 操作的结果是让信号量的操作的结果是让信号量的值加 值加 11 。在进行实际的操作之前,进程首先检查信号量的。在进行实际的操作之前,进程首先检查信号量的当前值,如果当前值大于 当前值,如果当前值大于 00,则可以执行 ,则可以执行 down down 操作,操作,否则进程休眠,等待其他进程在该信号量上的 否则进程休眠,等待其他进程在该信号量上的 up up 操作,操作,因为其他进程的 因为其他进程的 up up 操作将让信号量的值增加,从而它的 操作将让信号量的值增加,从而它的 down down 操作可以成功完成。某信号灯在经过某个进程的成操作可以成功完成。某信号灯在经过某个进程的成功操作之后,其他休眠在该信号量上的进程就有可能成功功操作之后,其他休眠在该信号量上的进程就有可能成功完成自己的操作,这时,系统负责检查休眠进程是否可以完成自己的操作,这时,系统负责检查休眠进程是否可以完成自己的操作。完成自己的操作。

Page 84: 第 5 章 进程管理及进程间通讯

semid_ds

ipc

times

sem_base

sem_pending

sem_pending_last

sem_nsems

undo proc_next

id_next

semadj

semid

sem_undo

next

prev

undo

sleeper

sem_queue

pid

status

sma

sops

nsops

信号量数组

图 5.10 System V IPC 机制--信号量

Page 85: 第 5 章 进程管理及进程间通讯

在操作系统中,信号量的最简单形式是一个整数,多个在操作系统中,信号量的最简单形式是一个整数,多个进程可检查并设置信号量的值。这种检查和设置操作是不进程可检查并设置信号量的值。这种检查和设置操作是不可被中断的,也称为“原语”操作。检查并设置操作的结可被中断的,也称为“原语”操作。检查并设置操作的结果是信号量的当前值和设置值相加的结果,该设置值可以果是信号量的当前值和设置值相加的结果,该设置值可以是正值,也可以是负值。根据检查和设置操作的结果,进是正值,也可以是负值。根据检查和设置操作的结果,进行操作的进程可能会进入休眠状态,而当其他进程完成自行操作的进程可能会进入休眠状态,而当其他进程完成自己的检查并设置操作后,由系统检查前一个休眠进程是否己的检查并设置操作后,由系统检查前一个休眠进程是否可以在新信号量值的条件下完成相应的检查和设置操作。可以在新信号量值的条件下完成相应的检查和设置操作。这样,通过信号量,就可以协调多个进程的操作。这样,通过信号量,就可以协调多个进程的操作。

信号量可用来实现所谓的“关键段”。关键段指同一时信号量可用来实现所谓的“关键段”。关键段指同一时刻只能有一个进程执行其中代码的代码段。也可用信号量刻只能有一个进程执行其中代码的代码段。也可用信号量解决经典的“生产者——消费者”问题。这一问题可以描解决经典的“生产者——消费者”问题。这一问题可以描述如下:述如下:

两个进程共享一个公共的、固定大小的缓冲区。其中的两个进程共享一个公共的、固定大小的缓冲区。其中的一个进程,即生产者,向缓冲区放入信息,另外一个进程,一个进程,即生产者,向缓冲区放入信息,另外一个进程,即消费者,从缓冲区中取走信息(该问题也可以一般化为 即消费者,从缓冲区中取走信息(该问题也可以一般化为 m m 个生产者和 个生产者和 n n 个消费者)。当生产者向缓冲区放入信个消费者)。当生产者向缓冲区放入信息时,如果缓冲区是满的,则生产者进入休眠,而当消费息时,如果缓冲区是满的,则生产者进入休眠,而当消费者从缓冲区中拿走信息后,可唤醒生产者;当消费者从缓者从缓冲区中拿走信息后,可唤醒生产者;当消费者从缓冲区中取信息时,如果缓冲区为空,则消费者进入休眠,冲区中取信息时,如果缓冲区为空,则消费者进入休眠,而当生产者向缓冲区写入信息后,可唤醒消费者。而当生产者向缓冲区写入信息后,可唤醒消费者。

Page 86: 第 5 章 进程管理及进程间通讯

Linux Linux 利用 利用 semid_ds semid_ds 结构来表示 结构来表示 System V IPC System V IPC 信号信号量,如图量,如图 5.105.10所示。和消息队列类似,系统中所有的信所示。和消息队列类似,系统中所有的信号量组成了一个 号量组成了一个 semary semary 链表,该链表的每个节点指向链表,该链表的每个节点指向一个 一个 semid_ds semid_ds 结构。从图结构。从图 5.10 5.10 可以看出,可以看出, semid_ds semid_ds 结构的 结构的 sem_base sem_base 指向一个信号量数组,允许操作这些指向一个信号量数组,允许操作这些信号量数组的进程可以利用系统调用执行操作。系统调用信号量数组的进程可以利用系统调用执行操作。系统调用可指定多个操作,每个操作由三个参数指定:信号量索引、可指定多个操作,每个操作由三个参数指定:信号量索引、操作值和操作标志。信号量索引用来定位信号量数组中的操作值和操作标志。信号量索引用来定位信号量数组中的信号量;操作值是要和信号量的当前值相加的数值。首先,信号量;操作值是要和信号量的当前值相加的数值。首先,Linux Linux 按如下的规则判断是否所有的操作都可以成功:操按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于 作值和信号量的当前值相加大于 00,或操作值和当前值均,或操作值和当前值均为 为 00,则操作成功。如果系统调用中指定的所有操作中有,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则 一个操作不能成功时,则 Linux Linux 会挂起这一进程。但是,会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,执行。如果进程被挂起, Linux Linux 必须保存信号量的操作状必须保存信号量的操作状态并将当前进程放入等待队列。为此,态并将当前进程放入等待队列。为此, Linux Linux 在堆栈中建在堆栈中建立一个 立一个 sem_queue sem_queue 结构并填充该结构。新的 结构并填充该结构。新的 sem_quesem_queue ue 结构添加到信号量对象的等待队列中(利用 结构添加到信号量对象的等待队列中(利用 sem_pesem_pending nding 和 和 sem_pending_last sem_pending_last 指针)。当前进程放入 指针)。当前进程放入 sesem_queue m_queue 结构的等待队列中(结构的等待队列中( sleepersleeper )后调用调度程)后调用调度程序选择其他的进程运行。序选择其他的进程运行。

Page 87: 第 5 章 进程管理及进程间通讯

如果所有的信号量操作都成功了,当前进程可继续运行。如果所有的信号量操作都成功了,当前进程可继续运行。在此之前,在此之前, Linux Linux 负责将操作实际应用于信号量队列的相负责将操作实际应用于信号量队列的相应元素。这时,应元素。这时, Linux Linux 检查任何等待的或挂起的进程,看检查任何等待的或挂起的进程,看它们的信号量操作是否可以成功。如果这些进程的信号量它们的信号量操作是否可以成功。如果这些进程的信号量操作可以成功,操作可以成功, Linux Linux 就会将它们从挂起队列中移去,并就会将它们从挂起队列中移去,并将它们的操作实际应用于信号量队列。同时,将它们的操作实际应用于信号量队列。同时, Linux Linux 会唤会唤醒休眠进程,以便可在下次调度程序运行时可以运行这些醒休眠进程,以便可在下次调度程序运行时可以运行这些进程。当新的信号量操作应用于信号量队列之后,进程。当新的信号量操作应用于信号量队列之后, Linux Linux 会接着检查挂起队列,直到没有操作可成功,或没有挂起会接着检查挂起队列,直到没有操作可成功,或没有挂起进程为止。进程为止。

和信号量操作相关的概念还有“死锁”。当某个进程修和信号量操作相关的概念还有“死锁”。当某个进程修改了信号量而进入关键段之后,却因为崩溃而没有退出关改了信号量而进入关键段之后,却因为崩溃而没有退出关键段,这时,其他被挂起在信号量上的进程永远得不到运键段,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。行机会,这就是所谓的死锁。 Linux Linux 通过维护一个信号量通过维护一个信号量数组的调整链表来避免这一问题。 数组的调整链表来避免这一问题。

Page 88: 第 5 章 进程管理及进程间通讯

5.5.3 5.5.3 共享内存共享内存

由于进程的虚拟地址可以映射到任意一处物理地址,这由于进程的虚拟地址可以映射到任意一处物理地址,这样,如果两个进程的虚拟地址映射到同一物理地址,这两样,如果两个进程的虚拟地址映射到同一物理地址,这两个进程就可以利用这一虚拟地址进行通讯。但是,一旦内个进程就可以利用这一虚拟地址进行通讯。但是,一旦内存被共享之后,对共享内存的访问同步需要由其他 存被共享之后,对共享内存的访问同步需要由其他 IPC IPC 机制,例如信号量来实现。机制,例如信号量来实现。 Linux Linux 中的共享内存通过访问中的共享内存通过访问键来访问,并进行访问权限的检查。共享内存对象的创建键来访问,并进行访问权限的检查。共享内存对象的创建者负责控制访问权限以及访问键的公有或私有特性。如果者负责控制访问权限以及访问键的公有或私有特性。如果具有足够的权限,也可以将共享内存锁定到物理内存中。具有足够的权限,也可以将共享内存锁定到物理内存中。

图图 5.11 5.11 是 是 Linux Linux 中共享内存对象的结构。和消息队列中共享内存对象的结构。和消息队列及信号量类似,及信号量类似, Linux Linux 中也有一个链表维护着所有的共享中也有一个链表维护着所有的共享内存对象。图内存对象。图 5.115.11 中的共享内存对象的结构元素可说明中的共享内存对象的结构元素可说明如下:如下:

Page 89: 第 5 章 进程管理及进程间通讯

(1) shm_segsz(1) shm_segsz:共享内存的大小;:共享内存的大小;(2) times(2) times :使用共享内存的进程数目;:使用共享内存的进程数目;(3) attaches(3) attaches :描述被共享的物理内存映射到各进程的虚拟:描述被共享的物理内存映射到各进程的虚拟

内存区域。内存区域。(4) shm_npages(4) shm_npages :共享虚拟内存页的数目;:共享虚拟内存页的数目;

shmid_ds

ipc

shm_segsz

times

shm_npages

shm_pagesattaches

pte

pte

ptevm_area_struct

vm_next_shared

vm_area_struct

vm_next_shared

图 5.11 System V IPC 机制--共享内存

Page 90: 第 5 章 进程管理及进程间通讯

(5) shm_pages(5) shm_pages :指向共享虚拟内存页的页表项表。:指向共享虚拟内存页的页表项表。 在利用共享内存时,参与通讯的进程通过系统调用将自在利用共享内存时,参与通讯的进程通过系统调用将自

己要共享的虚拟地址区域附加到 己要共享的虚拟地址区域附加到 attaches attaches 指向的链表中。指向的链表中。 某个进程第一次访问共享虚拟内存时将产生页故障。这某个进程第一次访问共享虚拟内存时将产生页故障。这

时,时, Linux Linux 找出描述该内存的 找出描述该内存的 vm_area_struct vm_area_struct 结构,结构,该结构中包含用来处理这种共享虚拟内存的处理函数地址。该结构中包含用来处理这种共享虚拟内存的处理函数地址。共享内存页故障处理代码在 共享内存页故障处理代码在 shmid_ds shmid_ds 的页表项链表中的页表项链表中查找,以便查看是否存在该共享虚拟内存的页表项。如果查找,以便查看是否存在该共享虚拟内存的页表项。如果没有,系统将分配一个物理页并建立页表项。该页表项加没有,系统将分配一个物理页并建立页表项。该页表项加入 入 shmid_ds shmid_ds 结构的同时也添加到进程的页表中。此后,结构的同时也添加到进程的页表中。此后,当另一个进程访问该共享内存时,共享内存页故障处理代当另一个进程访问该共享内存时,共享内存页故障处理代码将使用同一物理页,而只是将页表项添加到这一进程的码将使用同一物理页,而只是将页表项添加到这一进程的页表中。这样,前后两个进程就可以通过同一物理页进行页表中。这样,前后两个进程就可以通过同一物理页进行通讯。通讯。

Page 91: 第 5 章 进程管理及进程间通讯

当某个进程不再共享其虚拟内存时,利用系统调用将自当某个进程不再共享其虚拟内存时,利用系统调用将自己的虚拟地址区域从该链表中移去,并更新进程页表。当己的虚拟地址区域从该链表中移去,并更新进程页表。当最后一个进程释放了自己的虚拟地址空间之后,系统释放最后一个进程释放了自己的虚拟地址空间之后,系统释放所分配的物理页。当共享的虚存没有被锁定到物理内存时,所分配的物理页。当共享的虚存没有被锁定到物理内存时,共享内存也可能会被交换到交换空间中。共享内存也可能会被交换到交换空间中。

套接字和上述的 套接字和上述的 IPC IPC 机制有所不同,它能够实现不同机制有所不同,它能够实现不同计算机之间的进程间通讯。 计算机之间的进程间通讯。

Page 92: 第 5 章 进程管理及进程间通讯

5.5.4 5.5.4 相关系统工具相关系统工具

Linux Linux 中主要有三个命令可以查看当前系统中运行的进中主要有三个命令可以查看当前系统中运行的进程。程。 psps 命令可报告进程状态;命令可报告进程状态; pstreepstree 可打印进程之间可打印进程之间的父子关系;的父子关系; toptop 则可用来监视系统中 则可用来监视系统中 CPU CPU 利用率最利用率最高的进程,也可以交互式地操作进程。高的进程,也可以交互式地操作进程。

killkill 命令则用来向指定进程发送信号,如果没有指定要命令则用来向指定进程发送信号,如果没有指定要发送的信号,则发送 发送的信号,则发送 SIGTERM SIGTERM 信号,该信号的默认处理信号,该信号的默认处理是终止进程的运行。如果查看系统所支持的所有信号编号,是终止进程的运行。如果查看系统所支持的所有信号编号,可用 可用 kill –lkill –l 命令获取信号清单。可使用命令获取信号清单。可使用 mkfifomkfifo 来建立命来建立命名管道(名管道( FIFOFIFO)。)。

Page 93: 第 5 章 进程管理及进程间通讯

5.65.6 内核同步机制 内核同步机制 同步通常是为了达到多线程协同的目的而设计的一种机同步通常是为了达到多线程协同的目的而设计的一种机

制,通常包含异步信号机制和互斥机制作为其实现的底层。制,通常包含异步信号机制和互斥机制作为其实现的底层。在在 Linux 2.4Linux 2.4内核中也有相应的技术实现,包括信号量、内核中也有相应的技术实现,包括信号量、自旋锁、原子操作和等待队列,其中原子操作和等待队列自旋锁、原子操作和等待队列,其中原子操作和等待队列又是实现信号量的底层。又是实现信号量的底层。

Page 94: 第 5 章 进程管理及进程间通讯

5.6.1 5.6.1 原子操作和信号量原子操作和信号量

在在 POSIXPOSIX、、 SysV IPCSysV IPC 和核内中信号量的和核内中信号量的 down()down() 和和 upup()() ,, downdown意味着信号量减意味着信号量减 11 ,, upup意味着信号量加意味着信号量加 11 ,,分别对应分别对应 PP 操作和操作和 VV操作。由于操作。由于 down()down() 调用可能引起线调用可能引起线程挂起,和程挂起,和 sleep_onsleep_on 类似,也有不可中断(类似,也有不可中断( interruptibinterruptiblele )系列接口。)系列接口。 down()down() 和和 up()up() 操作是互斥的。在操作是互斥的。在 Linux Linux 2.42.4中,这个互斥不是用锁,而是使用原子操作来实现。中,这个互斥不是用锁,而是使用原子操作来实现。

在在 include/asm/atomic.hinclude/asm/atomic.h 中定义了一系列原子操作,中定义了一系列原子操作,包括原子读、原子写、原子加等等,大多是直接用汇编语包括原子读、原子写、原子加等等,大多是直接用汇编语句来实现的,这里不详细解释。句来实现的,这里不详细解释。

Page 95: 第 5 章 进程管理及进程间通讯

信号量数据结构定义在信号量数据结构定义在 include/asm/semaphore.hinclude/asm/semaphore.h 中:中:struct semaphore {struct semaphore { atomic_t count;atomic_t count;int sleepers;int sleepers;wait_queue_head_t wait;wait_queue_head_t wait;} } down()down() 操作可以理解为申请资源,操作可以理解为申请资源, up()up() 操作可以理解操作可以理解

为释放资源,因此,信号量实际表示的是资源的数量以及为释放资源,因此,信号量实际表示的是资源的数量以及是否有进程正在等待。在是否有进程正在等待。在 semaphoresemaphore 结构中,结构中, countcount相相当于资源计数,为正数或当于资源计数,为正数或 00时表示可用资源数,时表示可用资源数, -1-1 则表则表示没有空闲资源且有等待进程。而等待进程的数量并不关示没有空闲资源且有等待进程。而等待进程的数量并不关心。这种设计主要是考虑与信号量的原语相一致,当某个心。这种设计主要是考虑与信号量的原语相一致,当某个进程执行进程执行 up()up()函数释放资源,点亮信号灯时,如果函数释放资源,点亮信号灯时,如果 councountt 恢复到恢复到 00,则表示尚有进程在等待该资源,因此执行唤,则表示尚有进程在等待该资源,因此执行唤醒操作。一个典型的醒操作。一个典型的 down()-up()down()-up()流程是这样的:流程是这样的:

Page 96: 第 5 章 进程管理及进程间通讯

down()-->countdown()-->count做原子减做原子减 11 操作,如果结果不小于操作,如果结果不小于 00则则表示成功申请,从表示成功申请,从 down()down() 中返回;中返回;-->-->如果结果为负(实际上只可能是如果结果为负(实际上只可能是 -1-1 ),则表示需要等),则表示需要等待,则调用待,则调用 _down_fail()_down_fail() ;;_down_fail()_down_fail() 调用调用 _down()_down() ,, _down()_down() 用用 CC 代码实现,代码实现,要求已不如要求已不如 down()down() 和和 _down_fail()_down_fail() 严格,在此作实际的严格,在此作实际的等待(等待( arch/i386/kernel/semaphore.carch/i386/kernel/semaphore.c ):):

void _down(struct semaphore * sem)void _down(struct semaphore * sem){{ struct task_struct *tsk = current;struct task_struct *tsk = current; DECLARE_WAITQUEUE(wait, tsk);DECLARE_WAITQUEUE(wait, tsk); tsk->state = TASK_UNINTERRUPTIBLE;tsk->state = TASK_UNINTERRUPTIBLE; add_wait_queue_exclusive(&sem->wait, &wait);add_wait_queue_exclusive(&sem->wait, &wait); spin_lock_irq(&semaphore_lock);spin_lock_irq(&semaphore_lock); sem->sleepers++;sem->sleepers++; for (;;) {for (;;) {

Page 97: 第 5 章 进程管理及进程间通讯

int sleepers = sem->sleepers;int sleepers = sem->sleepers; //// * Add "everybody else" into it. They aren't* Add "everybody else" into it. They aren't * playing, because we own the spinlock.* playing, because we own the spinlock.if (!atomic_add_negative(sleepers - 1, &sem->count)) { if (!atomic_add_negative(sleepers - 1, &sem->count)) {

sem->sleepers = 0;sem->sleepers = 0; break; }break; }sem->sleepers = 1; // us - see -1 above sem->sleepers = 1; // us - see -1 above spin_unlock_irq(&semaphore_lock);spin_unlock_irq(&semaphore_lock);schedule();schedule();tsk->state = TASK_UNINTERRUPTIBLE;tsk->state = TASK_UNINTERRUPTIBLE;spin_lock_irq(&semaphore_lock);}spin_lock_irq(&semaphore_lock);}spin_unlock_irq(&semaphore_lock);spin_unlock_irq(&semaphore_lock);remove_wait_queue(&sem->wait, &wait);remove_wait_queue(&sem->wait, &wait);tsk->state = TASK_RUNNING;tsk->state = TASK_RUNNING;wake_up(&sem->wait);} wake_up(&sem->wait);}

Page 98: 第 5 章 进程管理及进程间通讯

__down()down() 表示当前进程进入表示当前进程进入 waitwait等待队列,状态为不可等待队列,状态为不可中断的挂起(中断的挂起( sleepers++sleepers++)如果这是第一次申请失败,)如果这是第一次申请失败,则则 sleeperssleepers 值为值为 11 ,否则为,否则为 22 。这个设置纯粹是为了下。这个设置纯粹是为了下面这句原子加而安排的。面这句原子加而安排的。

在真正进入休眠以前,在真正进入休眠以前, _down()_down() 还是需要判断一下是还是需要判断一下是不是确实没有资源可用,因为在不是确实没有资源可用,因为在 spin_lockspin_lock之前什么都可之前什么都可能发生。能发生。 atomic_add_negative()atomic_add_negative() 将将 sleepers-1sleepers-1 (只可(只可能是能是 00 或者或者 11 ,分别表示仅有一个等待进程或是多个)加,分别表示仅有一个等待进程或是多个)加到到 countcount(如果有多个进程申请资源失败进入(如果有多个进程申请资源失败进入 _down()_down() ,,countcount可能为可能为 -2-2 、、 -3-3 等)之上,这个加法完成后,结果等)之上,这个加法完成后,结果为为 00只可能是在只可能是在 sleeperssleepers 等于等于 11 的时候发生(因为如果的时候发生(因为如果 ssleepersleepers 等于等于 22 ,表示有多个进程执行了,表示有多个进程执行了 down()down() ,则,则 cocountunt 必然小于必然小于 -1-1 ,因此,因此 sleepers-1+countsleepers-1+count 必然小于必然小于 00),),表示表示 countcount在此之前已经变为在此之前已经变为 00了,也就是说已经有进程了,也就是说已经有进程释放了资源,因此本进程不用休眠而是获得资源退出释放了资源,因此本进程不用休眠而是获得资源退出 _do_down()wn() ,从而也从,从而也从 down()down() 中返回。如果没有进程释放资源,中返回。如果没有进程释放资源,那么在所有等待进程的这一加法完成后,那么在所有等待进程的这一加法完成后, countcount将等于将等于 --11 。。

Page 99: 第 5 章 进程管理及进程间通讯

因此,从因此,从 down()down() 调用外面看(无论是在调用外面看(无论是在 down()down() 中休眠中休眠还是获得资源离开还是获得资源离开 down()down() ),), countcount为负时只可能为为负时只可能为 -1-1(此时(此时 sleeperssleepers 等于等于 11 ),这么设计使得),这么设计使得 up()up() 操作只需操作只需要对要对 countcount 加加 11 ,判断是否为,判断是否为 00就可以知道是否有必要执就可以知道是否有必要执行唤醒操作行唤醒操作 _up_wakeup()_up_wakeup() 了。了。

获得了资源的进程将把获得了资源的进程将把 sleeperssleepers 设为设为 00,并唤醒所有,并唤醒所有其他等待进程,这个操作实际上只是起到恢复其他等待进程,这个操作实际上只是起到恢复 countcount为为 --11 ,并使它们再次进入休眠的作用,因为第一个被唤醒的,并使它们再次进入休眠的作用,因为第一个被唤醒的等待进程执行等待进程执行 atomic_add_negative()atomic_add_negative() 操作后会将操作后会将 councountt 恢复为恢复为 -1-1 ,然后将,然后将 sleeperssleepers 置为置为 11;以后的等待进程;以后的等待进程则会像往常一样重新休眠。则会像往常一样重新休眠。

Page 100: 第 5 章 进程管理及进程间通讯

将将 down()down() 操作设计得如此复杂的结果是操作设计得如此复杂的结果是 upup 操作相当操作相当简单。简单。 up()up()利用汇编原子加将利用汇编原子加将 countcount 加加 11 ,如果小于等,如果小于等于于 00 表示有等待进程,则调用表示有等待进程,则调用 _up_wakeup()_up_wakeup() 调用调用 _up()_up()唤醒唤醒 waitwait ;否则直接返回。;否则直接返回。

在在 down()down() 中竞争获得资源的进程并不是按照优先级排中竞争获得资源的进程并不是按照优先级排序的,只是在序的,只是在 up()up() 操作完成后第一个被唤醒或者正在操作完成后第一个被唤醒或者正在 _d_down()own() 中运行而暂未进入休眠的进程成功的可能性稍高一中运行而暂未进入休眠的进程成功的可能性稍高一些。些。

尽管可以将信号量的尽管可以将信号量的 countcount 初始化为初始化为 11 从而实现一种互从而实现一种互斥锁(斥锁( mutexmutex ),但),但 LinuxLinux 并不保证这个并不保证这个 countcount不会超不会超过过 11 ,因为,因为 upup 操作并不考虑操作并不考虑 countcount的初值,所以只能依的初值,所以只能依靠程序员自己来控制。相关的初始化接口定义在靠程序员自己来控制。相关的初始化接口定义在 include/include/asm/semaphore.hasm/semaphore.h 中,但一般程序员可以通过中,但一般程序员可以通过 sema_inisema_init()t() 接口来初始化信号量:接口来初始化信号量:

Page 101: 第 5 章 进程管理及进程间通讯

#define DECLARE_MUTEX(name) _DECLARE_SEMAPHOR#define DECLARE_MUTEX(name) _DECLARE_SEMAPHORE_GENERIC(name,1)E_GENERIC(name,1)

#define DECLARE_MUTEX_LOCKED(name) _DECLARE_S#define DECLARE_MUTEX_LOCKED(name) _DECLARE_SEMAPHORE_GENERIC(name,0)EMAPHORE_GENERIC(name,0)

static inline void sema_init (struct semaphore *sem, int static inline void sema_init (struct semaphore *sem, int val)val)

static inline void init_MUTEX (struct semaphore *sem)static inline void init_MUTEX (struct semaphore *sem)static inline void init_MUTEX_LOCKED (struct semaphorstatic inline void init_MUTEX_LOCKED (struct semaphor

e *sem) e *sem) 除了除了 down()down() 以外,以外, LinuxLinux 还提供了一个还提供了一个 down_interrdown_interr

uptible()uptible() ,操作序列与,操作序列与 down()down() 基本相同,仅在休眠状态基本相同,仅在休眠状态为可中断和信号处理上有所不同。在标准的信号量以外,为可中断和信号处理上有所不同。在标准的信号量以外,还有一套读写信号量,用于将资源的读写区分开来进行同还有一套读写信号量,用于将资源的读写区分开来进行同步以提高效率,采用读写锁来实现,有兴趣的读者可以参步以提高效率,采用读写锁来实现,有兴趣的读者可以参阅有关的参考资料。阅有关的参考资料。

Page 102: 第 5 章 进程管理及进程间通讯

5.6.2 5.6.2 任务队列任务队列 任务队列是内核将任务延迟到以后处理的主要手段。任务队列是内核将任务延迟到以后处理的主要手段。 LL

inux inux 提供了对队列上任务排队以及处理它们的通用机制。提供了对队列上任务排队以及处理它们的通用机制。任务队列通常和底层处理过程一起使用,例如,定时器任务队列通常和底层处理过程一起使用,例如,定时器任务队列在定时器底层处理过程中进行处理。任务队列任务队列在定时器底层处理过程中进行处理。任务队列的数据结构很简单,实际就是普通的单向链表结构,见的数据结构很简单,实际就是普通的单向链表结构,见图图 5.125.12 。 。

next

sync

*routine()

*data

next

sync

*routine()

*data

任务队列tq_struct tq_struct

图 5.12 任务队列数据结构

Page 103: 第 5 章 进程管理及进程间通讯

图图 5.125.12 由一个由一个 tq_structtq_struct结构链表构成,每个 结构链表构成,每个 tq_struct tq_struct 数据结构作为任务队列的节点,每个节点中包含处理过程数据结构作为任务队列的节点,每个节点中包含处理过程的地址以及指向数据的指针。处理任务队列上元素时将用的地址以及指向数据的指针。处理任务队列上元素时将用到这些过程并传递数据指针。内核中的任何部分(例如驱到这些过程并传递数据指针。内核中的任何部分(例如驱动程序)都可以建立并使用任务队列,但是内核自己创建动程序)都可以建立并使用任务队列,但是内核自己创建与管理的任务队列只有在表与管理的任务队列只有在表 5.6 5.6 中列出的中列出的 33 个。个。 TIMER(定时器)

该队列用于对需要在系统时钟周期之后尽可能快地完成的任务进行排队。每次时钟周期中,都要检查队列是否为空,如果不为空则定时器队列底层处理过程将激活此任务。在随后运行的调度程序中,定时器队列底层处理例程被调用,从而定时器队列中排队的任务也被处理。

IMMEDIATE(立即)

该队列在调度程序处理活动的底层处理程序时 处理。因为 IMMEDIATE 底层处理过程的优先级较

低,因此比起定时器底层处理过程,对这些任务的处理要稍微拖后一些。

SCHEDULER(调度程序)

该任务队列由调度程序直接处理。该队列用来支持系统中的其他任务队列,这种情况下,要运行的任务实际是处理某个任务队列的例程。

Page 104: 第 5 章 进程管理及进程间通讯

在处理任务队列时,队列中第一个元素从队列中移出,在处理任务队列时,队列中第一个元素从队列中移出,并用空指针代替。移出操作必须是一个原语操作,也就是并用空指针代替。移出操作必须是一个原语操作,也就是说,是不能被中断的操作。队列中每个处理例程依次调用。说,是不能被中断的操作。队列中每个处理例程依次调用。队列中的元素通常是静态分配的数据,因为没有内建用来队列中的元素通常是静态分配的数据,因为没有内建用来丢弃已分配内存的机制,因此,任务队列的处理过程简单丢弃已分配内存的机制,因此,任务队列的处理过程简单移向后续的链表元素。对已分配内核内存的清除工作由任移向后续的链表元素。对已分配内核内存的清除工作由任务本身完成。务本身完成。

Page 105: 第 5 章 进程管理及进程间通讯

5.6.3 5.6.3 等待队列和异步信号等待队列和异步信号

在进程的执行过程中,有时要等待某些系统资源。例如,在进程的执行过程中,有时要等待某些系统资源。例如,如果某个进程要读取一个描述目录的 如果某个进程要读取一个描述目录的 VFS VFS 索引节点,而索引节点,而该节点当前不在缓冲区高速缓存中,这时,该进程就必须该节点当前不在缓冲区高速缓存中,这时,该进程就必须等待系统从包含文件系统的物理介质中获取索引节点,然等待系统从包含文件系统的物理介质中获取索引节点,然后才能继续运行。后才能继续运行。

Linux Linux 利用一个简单的数据结构,即等待队列利用一个简单的数据结构,即等待队列 (wait qu(wait queue)eue) 来处理这种情况。等待队列以队列为基础数据结构,来处理这种情况。等待队列以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。通知机制。

Page 106: 第 5 章 进程管理及进程间通讯

图 图 5.13 5.13 是 是 Linux Linux 中的等待队列中的等待队列 wait queuewait queue ,该队列,该队列中的元素包含一个指向进程 中的元素包含一个指向进程 task_struct task_struct 结构的指针,以结构的指针,以及一个指向等待队列中下一个元素的指针。下面从及一个指向等待队列中下一个元素的指针。下面从 wait qwait queueueue 的使用范例着手,看看的使用范例着手,看看 Linux Linux 中的等待队列是如何中的等待队列是如何实现异步信号功能的。实现异步信号功能的。

next

task

next

task

wait_queue wait_queue

task_struct

...

task_struct

...

图 5.13 Linux 中的等待队列

Page 107: 第 5 章 进程管理及进程间通讯

加入到等待队列中的进程既可以是可中断的也可以是不加入到等待队列中的进程既可以是可中断的也可以是不可中断的进程。可中断进程能够被诸如定时器到期或者信可中断的进程。可中断进程能够被诸如定时器到期或者信号发送等事件中断。该种等待进程的状态必须说明成是号发送等事件中断。该种等待进程的状态必须说明成是 InInterruptibleterruptible 还是 还是 UninterruptibleUninterruptible 。如果等待队列中的进。如果等待队列中的进程是可中断的,则进程状态为 程是可中断的,则进程状态为 InterruptibleInterruptible;如果等待;如果等待进程是不可中断的,则进程状态为进程是不可中断的,则进程状态为 UninterruptibleUninterruptible 。由。由于进程此时不能继续运行,则调度管理器将接过系统控制于进程此时不能继续运行,则调度管理器将接过系统控制权并选择一个新进程运行;而等待进程将被挂起。处理等权并选择一个新进程运行;而等待进程将被挂起。处理等待进程时,每个处于等待队列中的进程都被置为待进程时,每个处于等待队列中的进程都被置为 RunningRunning状态。如果此进程已经从运行队列中删除则它将被重新放状态。如果此进程已经从运行队列中删除则它将被重新放入运行队列。下次调度管理器运行时入运行队列。下次调度管理器运行时 , , 由于这些进程不再由于这些进程不再等待,所以它们都将是运行候选者。 等待队列可以用来同等待,所以它们都将是运行候选者。 等待队列可以用来同步对系统资源的访问步对系统资源的访问 , , 同时它们还被同时它们还被 LinuxLinux 用于信号灯的用于信号灯的实现中。实现中。

在核心运行过程中,经常会因为某些条件不满足而需要在核心运行过程中,经常会因为某些条件不满足而需要挂起当前线程,直至条件满足了才继续执行。在挂起当前线程,直至条件满足了才继续执行。在 2.42.4内核内核中提供了一组新接口来实现这样的功能,下面是节选自中提供了一组新接口来实现这样的功能,下面是节选自 kkernel/printk.cernel/printk.c 的代码:的代码:

Page 108: 第 5 章 进程管理及进程间通讯

unsigned long log_size;unsigned long log_size;1: DECLARE_WAIT_QUEUE_HEAD(log_wait);...1: DECLARE_WAIT_QUEUE_HEAD(log_wait);...4: spinlock_t console_lock = SPIN_LOCK_UNLOCKED;...4: spinlock_t console_lock = SPIN_LOCK_UNLOCKED;... int do_syslog(int type,char *buf,int len){int do_syslog(int type,char *buf,int len){ ......2: error=wait_event_interruptible(log_wait,log_size);2: error=wait_event_interruptible(log_wait,log_size); if(error)if(error) goto out;goto out; ......5: spin_lock_irq(&console_lock);5: spin_lock_irq(&console_lock);......

log_size--;log_size--; ......

Page 109: 第 5 章 进程管理及进程间通讯

6:6: spin_unlock_irq(&console_lock);spin_unlock_irq(&console_lock); ...... }} asmlinkage int printk(const char *fmt,...){asmlinkage int printk(const char *fmt,...){......

7:7: spin_lock_irqsave(&map;console_lock,flags);spin_lock_irqsave(&map;console_lock,flags); ...... log_size++;...log_size++;...8:8: spin_unlock_irqrestore(&console_lock,flags);spin_unlock_irqrestore(&console_lock,flags);3: wake_up_interruptible(&map;log_wait);3: wake_up_interruptible(&map;log_wait); ...... }}

Page 110: 第 5 章 进程管理及进程间通讯

这段代码实现了这段代码实现了 printkprintk调用和调用和 syslogsyslog 之间的同步,之间的同步, sysyslogslog 需要等待需要等待 printkprintk 送数据到缓冲区,因此,在送数据到缓冲区,因此,在 2:2:处等处等待待 log_sizelog_size非非 00 ;而;而 printkprintk一边传送数据,一边增加一边传送数据,一边增加 lolog_sizeg_size 的值,完成后唤醒在的值,完成后唤醒在 log_waitlog_wait上等待的所有线程上等待的所有线程(这个线程不是用户空间的线程概念,而是核内的一个执(这个线程不是用户空间的线程概念,而是核内的一个执行序列)。执行了行序列)。执行了 3:3:的的 wake_up_interruptible()wake_up_interruptible() 后,后, 2:2:处的处的 wait_event_interruptible()wait_event_interruptible() 返回返回 00,从而进入,从而进入 syslsyslogog 的实际动作。的实际动作。

11 :定义:定义 log_waitlog_wait全局变量的宏调用。在实际操作全局变量的宏调用。在实际操作 loglog_size_size全局变量的时候,还使用了全局变量的时候,还使用了 spin_lockspin_lock自旋锁来实自旋锁来实现互斥,关于自旋锁,这里暂不作解释,但从这段代码中现互斥,关于自旋锁,这里暂不作解释,但从这段代码中已经可以清楚的知道它的使用方法了。已经可以清楚的知道它的使用方法了。

所有所有 wait queuewait queue 使用上的技巧体现在使用上的技巧体现在 wait_event_intwait_event_interruptible()erruptible() 的实现上,代码位于的实现上,代码位于 include/Linux/sched.hinclude/Linux/sched.h中:中:

Page 111: 第 5 章 进程管理及进程间通讯

#define _wait_event_interruptible(wq, condition, ret) #define _wait_event_interruptible(wq, condition, ret) do {wait_queue_t _wait; do {wait_queue_t _wait; init_waitqueue_entry(&_wait, current); init_waitqueue_entry(&_wait, current); add_wait_queue(&wq, &_wait); add_wait_queue(&wq, &_wait); for (;;) {set_current_state(TASK_INTERRUPTIBLE); for (;;) {set_current_state(TASK_INTERRUPTIBLE); if (condition) if (condition) break; break; if (!signal_pending(current)) { schedule(); if (!signal_pending(current)) { schedule(); continue; } continue; } ret = -ERESTARTSYS; ret = -ERESTARTSYS; break; } break; } current->state = TASK_RUNNING; current->state = TASK_RUNNING; remove_wait_queue(&wq, &_wait); } while (0)remove_wait_queue(&wq, &_wait); } while (0)#define wait_event_interruptible(wq, condition) #define wait_event_interruptible(wq, condition) ({ int _ret = 0; ({ int _ret = 0; if (!(condition)) if (!(condition)) _wait_event_interruptible(wq, condition, _ret); _wait_event_interruptible(wq, condition, _ret); _ret; })_ret; })

Page 112: 第 5 章 进程管理及进程间通讯

在在 wait_event_interruptible()wait_event_interruptible() 中首先判断中首先判断 conditioncondition是不是已经满足,如果是则直接返回是不是已经满足,如果是则直接返回 00,否则调用,否则调用 _wait_wait_event_interruptible()_event_interruptible() ,并用,并用 _ret_ret来存放返回值。来存放返回值。 _wa_wait_event_interruptible()it_event_interruptible() 首先定义并初始化一个首先定义并初始化一个 wait_quwait_queue_teue_t变量变量 _wait_wait,其中数据为当前进程结构,其中数据为当前进程结构 currentcurrent(( struct task_structstruct task_struct),并把),并把 _wait_wait 入队。在无限循环入队。在无限循环中,中, _wait_event_interruptible()_wait_event_interruptible() 将本进程置为可中断将本进程置为可中断的挂起状态,反复检查的挂起状态,反复检查 conditioncondition 是否成立,如果成立则是否成立,如果成立则退出,如果不成立则继续休眠。条件满足后,即把本进程退出,如果不成立则继续休眠。条件满足后,即把本进程运行状态置为运行态,并将运行状态置为运行态,并将 _wait_wait从等待队列中清除掉,从等待队列中清除掉,从而进程能够调度运行。如果进程当前有异步信号(从而进程能够调度运行。如果进程当前有异步信号( POSPOSIXIX的),则返回的),则返回 -ERESTARTSYS-ERESTARTSYS。。

挂起的进程不会自动转入运行,因此需要一个唤醒动作,挂起的进程不会自动转入运行,因此需要一个唤醒动作,该动作由该动作由 wake_up_interruptible()wake_up_interruptible() 完成,它将遍历作为完成,它将遍历作为参数传入的参数传入的 log_waitlog_wait等待队列,将其中所有的元素(通等待队列,将其中所有的元素(通常都是常都是 task_structtask_struct)置为运行态,从而可被调度到,执)置为运行态,从而可被调度到,执行行 _wait_event_interruptible()_wait_event_interruptible() 中的代码。中的代码。

DECLARE_WAIT_QUEUE_HEAD(log_wait)DECLARE_WAIT_QUEUE_HEAD(log_wait)经过宏展开经过宏展开后就是定义了一个后就是定义了一个 log_waitlog_wait等待队列头变量:等待队列头变量:

Page 113: 第 5 章 进程管理及进程间通讯

struct _wait_queue_head log_wait = {struct _wait_queue_head log_wait = {lock:lock: SPIN_LOCK_UNLOCKED,SPIN_LOCK_UNLOCKED,task_list:{ &map;log_wait.task_list, &log_wait.task_list }}task_list:{ &map;log_wait.task_list, &log_wait.task_list }} 其中其中 task_listtask_list是是 struct list_headstruct list_head 变量,包括两个变量,包括两个 listlist_head_head 指针,一个指针,一个 nextnext、一个、一个 prevprev,这里把它们初始化,这里把它们初始化为自身,属于队列实现上的技巧,其细节可以参阅关于内为自身,属于队列实现上的技巧,其细节可以参阅关于内核核 listlist数据结构的讨论,数据结构的讨论, add_wait_queue()add_wait_queue() 和和 remove_remove_wait_queue()wait_queue() 就等同于就等同于 list_add()list_add() 和和 list_del()list_del() 。。

wait_queue_twait_queue_t结构在结构在 include/Linux/wait.hinclude/Linux/wait.h 中定义,中定义,关键元素是一个表征当前进程的关键元素是一个表征当前进程的 struct task_structstruct task_struct变量。变量。

除了除了 wait_event_interruptible()/wake_up_interrupwait_event_interruptible()/wake_up_interruptible()tible() 以外,与此相对应的还有以外,与此相对应的还有 wait_event()wait_event() 和和 wake_uwake_up()p() 接口,接口, interruptibleinterruptible 是更安全、更常用的选择,因为是更安全、更常用的选择,因为可中断的等待可以接收信号,从而挂起的进程允许被外界可中断的等待可以接收信号,从而挂起的进程允许被外界killkill 。。

Page 114: 第 5 章 进程管理及进程间通讯

wait_event*()wait_event*() 接口是接口是 2.42.4内核引入并推荐使用的,在此之内核引入并推荐使用的,在此之前,最常用的等待操作是前,最常用的等待操作是 interruptible_sleep_on(wait_interruptible_sleep_on(wait_queue_head_t *wq)queue_head_t *wq) 。与此配套的还有不可中断版本。与此配套的还有不可中断版本 slesleep_on()ep_on() 以及带有超时控制的以及带有超时控制的 *sleep_on_timeout()*sleep_on_timeout() 。。 slsleep_oneep_on 系列函数的语义比系列函数的语义比 wait_eventwait_event 简单,没有条件简单,没有条件判断功能,其余动作与判断功能,其余动作与 wait_eventwait_event完全相同,也就是说,完全相同,也就是说,可以用可以用 interruptible_sleep_on()interruptible_sleep_on() 来实现来实现 wait_event_intwait_event_interruptible()erruptible() ::

do{interruptible_sleep_on(&log_wait);do{interruptible_sleep_on(&log_wait);if(condition)if(condition)break;}while(1); break;}while(1); 这种操作序列有反复的入队、出队动作,相对更加耗时,这种操作序列有反复的入队、出队动作,相对更加耗时,

而很大一部分等待操作是需要判断一个条件是否满足的,而很大一部分等待操作是需要判断一个条件是否满足的,因此因此 2.42.4 才推荐使用才推荐使用 wait_eventwait_event 接口。接口。

在在 wake_upwake_up 系列接口中,还有一类系列接口中,还有一类 wake_up_sync()wake_up_sync()和和 wake_up_interruptible_sync()wake_up_interruptible_sync() 接口,保证调度在接口,保证调度在 wawake_upke_up返回之后进行。返回之后进行。

Page 115: 第 5 章 进程管理及进程间通讯

5.6.4 Buzz 5.6.4 Buzz 锁 锁 1. 1. 自旋锁的概念自旋锁的概念 上面提到的上面提到的 mutexmutex 互斥锁仅仅是锁中的一种。互斥锁互斥锁仅仅是锁中的一种。互斥锁

被锁定时进入休眠,而系统还能正常运转,但有很多时候,被锁定时进入休眠,而系统还能正常运转,但有很多时候,锁应该不仅仅互斥访问,甚至应该让系统挂起,直至锁成锁应该不仅仅互斥访问,甚至应该让系统挂起,直至锁成功,也就是说在锁操作中功,也就是说在锁操作中 ""自旋自旋 "",这就是,这就是 LinuxLinux 中的中的 spspinlockinlock机制。机制。

Page 116: 第 5 章 进程管理及进程间通讯

Buzz Buzz 锁又称“自旋锁”,是用来保护数据和代码段的锁又称“自旋锁”,是用来保护数据和代码段的一种最原始方法。利用 一种最原始方法。利用 Buzz Buzz 锁,在某个时刻只允许一个锁,在某个时刻只允许一个进程访问临界区内的代码。进程访问临界区内的代码。 Linux Linux 使用一个整型域作为使用一个整型域作为 BBuzz uzz 锁,限制对某些数据结构域的访问。每个希望进入临锁,限制对某些数据结构域的访问。每个希望进入临界区的进程都试图将该整数的值从 界区的进程都试图将该整数的值从 0 0 修改为 修改为 11 ,如果当,如果当前值是前值是 11 则进程将再次尝试。此时进程好象在一段紧循环则进程将再次尝试。此时进程好象在一段紧循环代码中自旋。如果当前值为 代码中自旋。如果当前值为 00,则进程可以立即进入临界,则进程可以立即进入临界区,而整数值变为 区,而整数值变为 11;如果当前值为 ;如果当前值为 11 ,则说明其他进,则说明其他进程已进入该临界区,进程循环检查整数值,直到值变为 程已进入该临界区,进程循环检查整数值,直到值变为 00,,这时进程可修改值为 这时进程可修改值为 11 ,并进入临界区。进程在退出临界,并进入临界区。进程在退出临界区时它将递减此区时它将递减此 BuzzBuzz锁。任何处于自旋状态的进程都可锁。任何处于自旋状态的进程都可以读取它,它们中最快的那个将递增此值并进入临界区。以读取它,它们中最快的那个将递增此值并进入临界区。

对用来保存 对用来保存 Buzz Buzz 锁的内存的访问必须是原语操作,也锁的内存的访问必须是原语操作,也就是不能被中断的操作,即检验值是否为就是不能被中断的操作,即检验值是否为 00并将其改变成并将其改变成11 的过程不能被任何进程中断。大部分 的过程不能被任何进程中断。大部分 CPU CPU 提供特殊的提供特殊的指令支持 指令支持 Buzz Buzz 锁的原语操作,当然,也可以利用非缓冲锁的原语操作,当然,也可以利用非缓冲主存中实现 主存中实现 Buzz Buzz 锁。锁。

Page 117: 第 5 章 进程管理及进程间通讯

从实现上来说,自旋锁比较简单,主要分为两部分,一从实现上来说,自旋锁比较简单,主要分为两部分,一部分是中断处理,一部分是自旋处理,最基础的部分在部分是中断处理,一部分是自旋处理,最基础的部分在 sspin_lock_stringpin_lock_string 和和 spin_unlock_stringspin_unlock_string 这两段汇编代码这两段汇编代码中:中:

#define spin_lock_string \#define spin_lock_string \ "\n1:\t" \"\n1:\t" \"lock ; decb %0\n\t" \"lock ; decb %0\n\t" \"js 2f\n" \"js 2f\n" \ ".section .text.lock,\"ax\"\n" \".section .text.lock,\"ax\"\n" \"2:\t" \"2:\t" \"cmpb $0,%0\n\t" \"cmpb $0,%0\n\t" \"rep;nop\n\t" \"rep;nop\n\t" \"jle 2b\n\t" \"jle 2b\n\t" \"jmp 1b\n" \"jmp 1b\n" \ ".previous"".previous"#define spin_unlock_string \#define spin_unlock_string \"movb $1,%0" "movb $1,%0"

Page 118: 第 5 章 进程管理及进程间通讯

这段汇编代码的基本意义如下,这段汇编代码的基本意义如下, spin_lock_stringspin_lock_string 对对锁原子减锁原子减 11 ,循环检查锁值,直到锁值大于,循环检查锁值,直到锁值大于 00 ;而;而 spin_spin_unlock_stringunlock_string 则是对锁赋值则是对锁赋值 11 。。 spin_lock_stringspin_lock_string 用于用于构成构成 spin_lock()spin_lock() 函数,函数, spin_unlock_stringspin_unlock_string 用于构成用于构成 sspin_unlock()pin_unlock() 函数。函数。

spin_lock()/spin_unlock()spin_lock()/spin_unlock() 构成了自旋锁机制的基础,构成了自旋锁机制的基础,它们和关中断它们和关中断 local_irq_disable()/local_irq_disable()/开中断开中断 local_irq_enalocal_irq_enable()ble() 、关、关 bh local_bh_disable()/bh local_bh_disable()/开开 bh local_bh_enablbh local_bh_enable()e() 、关中断并保存状态字、关中断并保存状态字 local_irq_save()/local_irq_save()/开中断并恢开中断并恢复状态字复状态字 local_irq_restore()local_irq_restore() 结合就形成了整套自旋锁机结合就形成了整套自旋锁机制,接口定义在制,接口定义在 include/Linux/spinlock.hinclude/Linux/spinlock.h 中,这里就不中,这里就不列举了。 列举了。

Page 119: 第 5 章 进程管理及进程间通讯

实际上,以上的实际上,以上的 spin_lock()spin_lock() 都是在都是在 CONFIG_SMPCONFIG_SMP 的的前提下生成的。换句话说,在单处理机系统中,前提下生成的。换句话说,在单处理机系统中, spin_locspin_lock()k() 是一条空语句,因为在处理机执行它的语句时,不可是一条空语句,因为在处理机执行它的语句时,不可能受到打扰,语句肯定是串行的。在这种简单情况下,能受到打扰,语句肯定是串行的。在这种简单情况下, sspin_lock_irq()pin_lock_irq() 就只需要锁中断就可以完成任务了。在就只需要锁中断就可以完成任务了。在 ininclude/Linux/spinlock.hclude/Linux/spinlock.h 中就用中就用 #ifdef CONFIG_SMP#ifdef CONFIG_SMP 来来区分两种不同的情况。区分两种不同的情况。

自旋锁有很多种,信号量也可以用来构成互斥锁,原子自旋锁有很多种,信号量也可以用来构成互斥锁,原子操作也有锁功能,而且还有与标准锁机制类似的读写锁变操作也有锁功能,而且还有与标准锁机制类似的读写锁变种,在不同的应用场合应该选择不同的锁。种,在不同的应用场合应该选择不同的锁。

Page 120: 第 5 章 进程管理及进程间通讯

2. 2. 锁的使用锁的使用(1) (1) 用户上下文之间用户上下文之间 如果所访问的共享资源仅在用户上下文中使用,最高如果所访问的共享资源仅在用户上下文中使用,最高

效的办法就是使用信号量。在效的办法就是使用信号量。在 net/core/netfilter.cnet/core/netfilter.c 中有中有一处使用信号量的例子:一处使用信号量的例子:

static DECLARE_MUTEX(nf_sockopt_mutex);static DECLARE_MUTEX(nf_sockopt_mutex);int nf_register_sockopt(struct nf_sockopt_ops *reg)int nf_register_sockopt(struct nf_sockopt_ops *reg){...{... if (down_interruptible(&nf_sockopt_mutex) != 0)if (down_interruptible(&nf_sockopt_mutex) != 0) return -EINTR;...return -EINTR;...out:out:up(&nf_sockopt_mutex);up(&nf_sockopt_mutex);return ret;return ret;}}

Page 121: 第 5 章 进程管理及进程间通讯

(2) (2) 用户上下文与用户上下文与 bottom halfbottom half之间之间 此时有两种情况需要使用锁,一是用户上下文被此时有两种情况需要使用锁,一是用户上下文被 bottobottom halfm half中断,二是多个处理机同时进入一个临界段。一般中断,二是多个处理机同时进入一个临界段。一般使用使用 spin_lock_bh()/spin_unlock_bh()spin_lock_bh()/spin_unlock_bh() 就可满足要求,就可满足要求,它将关闭当前它将关闭当前 CPUCPU 的的 bottom halfbottom half,然后再获取锁,直,然后再获取锁,直至离开临界段再释放并对至离开临界段再释放并对 bottom halfbottom half 重新使能。重新使能。

(3) (3) 用户上下文和软中断(用户上下文和软中断( TaskletTasklet)之间)之间 tasklettasklet与与 bottom halfbottom half的实现机制是一样的,实际上的实现机制是一样的,实际上 ss

pin_lock_bh()pin_lock_bh() 也同时关闭了也同时关闭了 tasklettasklet的执行,因此,在的执行,因此,在这种情况下,用户上下文与这种情况下,用户上下文与 tasklettasklet之间的同步也使用之间的同步也使用 spispin_lock_bh()/spin_unlock_bh()n_lock_bh()/spin_unlock_bh() 。。

(4) bottom half(4) bottom half之间之间 bottom halfbottom half本身的机制就保证了不会有多于本身的机制就保证了不会有多于 11 个的个的 bobottom halfttom half同时处于运行态,即使对于同时处于运行态,即使对于 SMPSMP 系统也是如此,系统也是如此,因此,在设计共享数据的因此,在设计共享数据的 bottom halfbottom half时无需考虑互斥。时无需考虑互斥。

Page 122: 第 5 章 进程管理及进程间通讯

(5) tasklet(5) tasklet之间之间 tasklettasklet和和 bottom halfbottom half类似,也是受到类似,也是受到 local_bh_disalocal_bh_disable()ble() 保护的,因此,同一个保护的,因此,同一个 tasklettasklet不会同时在两个不会同时在两个 CPUCPU上运行;但不同的上运行;但不同的 tasklettasklet却有可能,因此,如果需要同却有可能,因此,如果需要同步不同的步不同的 tasklettasklet 访问共享数据的话,就应该使用访问共享数据的话,就应该使用 spin_lspin_lock()/spin_unlock()ock()/spin_unlock() 。正如上面提到的,这种保护仅对。正如上面提到的,这种保护仅对 SSMPMP 系统有意义,系统有意义, UPUP 系统中系统中 tasklettasklet的运行不会受到另一的运行不会受到另一个个 tasklettasklet(不论它是否与之相同)的打扰,因此也就没(不论它是否与之相同)的打扰,因此也就没有必要上锁。有必要上锁。

(6) softirq(6) softirq之间之间 softirqsoftirq是实现是实现 tasklettasklet和和 bottom halfbottom half的基础,限制较的基础,限制较

后二者都少,允许两个后二者都少,允许两个 softirqsoftirq同时运行于不同的同时运行于不同的 CPUCPU 之之上,而不论它们是否来自同一个上,而不论它们是否来自同一个 softirqsoftirq代码,因此,在代码,因此,在这种情况下,都需要用这种情况下,都需要用 spin_lock()/spin_unlock()spin_lock()/spin_unlock() 来同来同步。步。

Page 123: 第 5 章 进程管理及进程间通讯

(7) (7) 硬中断和软中断之间硬中断和软中断之间 硬中断是指硬件中断的处理程序上下文,软中断包括硬中断是指硬件中断的处理程序上下文,软中断包括 ss

oftirqoftirq和在它基础上实现的和在它基础上实现的 tasklettasklet和和 bottom halfbottom half等,等,此时,为了防止硬件中断软中断的运行,同步措施必须包此时,为了防止硬件中断软中断的运行,同步措施必须包括关闭硬件中断,括关闭硬件中断, spin_lock_irq()/spin_unlock_irq()spin_lock_irq()/spin_unlock_irq() 就就包括这个动作。还有一对包括这个动作。还有一对 APIAPI,, spin_lock_irqsave()/spispin_lock_irqsave()/spin_unlock_irqrestore()n_unlock_irqrestore() ,不仅关闭中断,还保存机器状,不仅关闭中断,还保存机器状态字,并在打开中断时恢复。 态字,并在打开中断时恢复。

(8) (8) 其他注意事项其他注意事项 首先需要提醒的是“死锁”,这在操作系统原理的课本首先需要提醒的是“死锁”,这在操作系统原理的课本

上都做过介绍,无论是使用信号量还是使用自旋锁,都有上都做过介绍,无论是使用信号量还是使用自旋锁,都有可能产生死锁,特别是自旋锁,如果死锁在可能产生死锁,特别是自旋锁,如果死锁在 spin_lockspin_lock上,上,整个系统就会挂起。如何避免死锁是理论课的问题,这里整个系统就会挂起。如何避免死锁是理论课的问题,这里就不多说了。就不多说了。

另外一点就是尽可能短时间的锁定,因此,“对数据上另外一点就是尽可能短时间的锁定,因此,“对数据上锁,而不是对代码上锁”就成了一个简单的原则;在有可锁,而不是对代码上锁”就成了一个简单的原则;在有可能的情况下,使用读写锁,而不要总是使用互斥锁;对读能的情况下,使用读写锁,而不要总是使用互斥锁;对读写排序,使用原子操作,从而完全避免使用锁,也是一个写排序,使用原子操作,从而完全避免使用锁,也是一个不错的设计思想。不错的设计思想。

Page 124: 第 5 章 进程管理及进程间通讯

不要在锁定状态下调用可能引起休眠的操作,例如对用不要在锁定状态下调用可能引起休眠的操作,例如对用户内存的访问等。以下操作是可能引起休眠的函数:户内存的访问等。以下操作是可能引起休眠的函数:

copy_from_user()copy_from_user() 、、 copy_to_user()copy_to_user() 、、 get_user()get_user() 、、 pput_user()ut_user() 、 、 kmalloc(GFP_KERNEL)kmalloc(GFP_KERNEL) 、、 down_interrupdown_interruptible()tible() 和和 down()down()

如果需要在如果需要在 spinlockspinlock中使用信号量,可以选择中使用信号量,可以选择 down_down_trylock()trylock() ,它不会引起挂起。,它不会引起挂起。 printk()printk() 的灵巧设计使得它的灵巧设计使得它不会挂起,因此可以在任何上下文中使用。不会挂起,因此可以在任何上下文中使用。

Page 125: 第 5 章 进程管理及进程间通讯

5.6.5 5.6.5 信号灯信号灯

信号灯被用来保护临界区中的代码和数据。每信号灯被用来保护临界区中的代码和数据。每次对临界区数据次对临界区数据 ((如描述某个目录如描述某个目录 VFS inode)VFS inode) 的的访问访问 , , 是通过代表进程的内核代码来进行的。如是通过代表进程的内核代码来进行的。如果允许某个进程修改当前由其他进程使用的临界果允许某个进程修改当前由其他进程使用的临界区数据是非常危险的。防止此问题发生的一种方区数据是非常危险的。防止此问题发生的一种方法是在被存取临界区周围使用法是在被存取临界区周围使用 buzzbuzz锁,实现对临锁,实现对临界区和数据的互斥访问,但这是一种很原始的方界区和数据的互斥访问,但这是一种很原始的方法,因为对 法,因为对 Buzz Buzz 锁值的循环重复测试,将降低锁值的循环重复测试,将降低系统性能。系统性能。 Linux Linux 利用信号灯实现对关键代码和利用信号灯实现对关键代码和数据的互斥访问,在某个时刻强迫只有唯一进程数据的互斥访问,在某个时刻强迫只有唯一进程访问临界区代码和数据,其他进程都必须等待资访问临界区代码和数据,其他进程都必须等待资源被释放才可使用。这些等待进程将被挂起而系源被释放才可使用。这些等待进程将被挂起而系统中其他进程可以继续运行。 统中其他进程可以继续运行。

Page 126: 第 5 章 进程管理及进程间通讯

假定该信号灯的初始计数为 假定该信号灯的初始计数为 11 ,第一个要求访问资源,第一个要求访问资源的进程可对计数减 的进程可对计数减 11 ,并可成功访问资源。现在,该进,并可成功访问资源。现在,该进程是“拥有”由信号灯所包含的资源或临界区的进程。当程是“拥有”由信号灯所包含的资源或临界区的进程。当该进程结束对资源的访问时,对计数加 该进程结束对资源的访问时,对计数加 11 。最优的情况。最优的情况是没有其他进程和该进程一起竞争资源所有权。是没有其他进程和该进程一起竞争资源所有权。 Linux Linux 针对这种最常见的情况对信号灯进行了优化,从而可以让针对这种最常见的情况对信号灯进行了优化,从而可以让信号灯高效工作。信号灯高效工作。

count(计数) 该域用来跟踪希望访问该资源的进程个数。正值表示资源是可用的,而负值或零表示有进

程正在等待该资源。该计数的初始值为 1,表明同一时刻有且只能有一个进程可访问该资

源。进程要访问该资源时,对该计数减 1, 结束对该资源的访问时,对该计数加 1。

waking(等待唤醒计数)

等待该资源的进程个数,也是当该资源空闲时等待唤醒的进程个数。

等待队列 某个进程等待该资源时被添加到该等待队列中。lock(锁) 用来实现对 waking 域的互斥访问的 Buzz 锁。

Linux 的信号灯数据结构 semaphore 中包含如表 5.7 所示的信息。

Page 127: 第 5 章 进程管理及进程间通讯

当某个进程当前拥有资源时,如果其他进程要访问该资当某个进程当前拥有资源时,如果其他进程要访问该资源,它首先将信号灯计数减 源,它首先将信号灯计数减 11 。因为现在计数值是负值 。因为现在计数值是负值 (( -1-1 ),因此该进程不能进入关键段,相反,它必须等),因此该进程不能进入关键段,相反,它必须等待资源当前的拥有者释放所有权。待资源当前的拥有者释放所有权。 Linux Linux 将等待进程置入将等待进程置入休眠状态,直到所有者退出关键段时唤醒。等待进程将自休眠状态,直到所有者退出关键段时唤醒。等待进程将自己添加到信号灯的等待队列中,然后循环检测信号灯 己添加到信号灯的等待队列中,然后循环检测信号灯 wawaking king 域的值,当 域的值,当 waking waking 非零时调用进程调度程序。非零时调用进程调度程序。

临界区的所有者将信号灯计数值加临界区的所有者将信号灯计数值加 11 ,如果计数大于或,如果计数大于或等于 等于 00,则表示还有等待此资源的进程在休眠。在理想情,则表示还有等待此资源的进程在休眠。在理想情况下此信号灯的计数将返回到初始值况下此信号灯的计数将返回到初始值 11 而无需做其他工作。而无需做其他工作。所有者进程将递增所有者进程将递增 wakingwaking 计数并唤醒在此 信号灯等待队计数并唤醒在此 信号灯等待队列上睡眠的进程。当等待进程醒来时,它发现列上睡眠的进程。当等待进程醒来时,它发现 wakingwaking 计计数值已为数值已为 11 ,那么它知道现在可以进入临界区了。然后它,那么它知道现在可以进入临界区了。然后它将递减将递减 wakingwaking 计数,将其变成计数,将其变成 00并继续。所有对信号灯并继续。所有对信号灯wakingwaking域的访问都将受到使用信号灯 的域的访问都将受到使用信号灯 的 buzzbuzz锁的保护。锁的保护。

Page 128: 第 5 章 进程管理及进程间通讯

关键段的所有者增加信号灯的计数,表明其他进程正在关键段的所有者增加信号灯的计数,表明其他进程正在处于休眠状态而等待该资源。在最优情况下,信号灯的计处于休眠状态而等待该资源。在最优情况下,信号灯的计数将返回到初值 数将返回到初值 11 ,因此没有必要进行额外的工作。在其,因此没有必要进行额外的工作。在其他情况下,资源的拥有者要增加 他情况下,资源的拥有者要增加 waking waking 计数,并唤醒计数,并唤醒处于信号灯等待队列中的休眠进程。当休眠进程被唤醒之处于信号灯等待队列中的休眠进程。当休眠进程被唤醒之后,后, waking waking 计数的当前值为 计数的当前值为 11 ,因此可以进入临界区,,因此可以进入临界区,这时,它减小 这时,它减小 waking waking 计数,将 计数,将 waking waking 计数的值还原计数的值还原为 为 00。对信号灯 。对信号灯 waking waking 域的互斥访问利用信号灯的 域的互斥访问利用信号灯的 lolock ck 域作为 域作为 Buzz Buzz 锁而实现。锁而实现。

Page 129: 第 5 章 进程管理及进程间通讯

思考与练习 思考与练习 1. 1. 进程的基本特征是什么?它与程序有什么不同?进程的基本特征是什么?它与程序有什么不同? 2. 2. 进程控制块进程控制块 PCBPCB有什么作用?它通常包括哪些内容?有什么作用?它通常包括哪些内容? 3. 3. 试列出进程状态变迁的典型原因。试列出进程状态变迁的典型原因。 4. 4. 什么叫原语、内核、微核?微核有什么特点?什么叫原语、内核、微核?微核有什么特点? 5. Unix5. Unix 系统系统 VV中进程的上下文的含义是什么?包括哪些中进程的上下文的含义是什么?包括哪些

内容?内容? 6. 6. 在进程环境中为什么容易发生与时间有关的错误?如在进程环境中为什么容易发生与时间有关的错误?如何防止这种错误?何防止这种错误?

7. 7. 什么叫临界资源?什么叫临界段?什么叫临界资源?什么叫临界段? 8. 8. 有哪些用于进程互斥的工具,如何使用?有哪些用于进程互斥的工具,如何使用? 9. 9. 有哪些用于进程同步的工具?它们各适用哪些场合?有哪些用于进程同步的工具?它们各适用哪些场合? 10. 10. 用用 PVPV操作描述同步互斥的程序其典型结构如何?操作描述同步互斥的程序其典型结构如何?

Page 130: 第 5 章 进程管理及进程间通讯

11. 11. 解释进程、管程、线程、过程、例程、类程。解释进程、管程、线程、过程、例程、类程。 12.12.在使用线程的系统中,是每个线程有一个堆栈还是每在使用线程的系统中,是每个线程有一个堆栈还是每

个进程有一个堆栈,说明原因。个进程有一个堆栈,说明原因。 13.13.什么是竞争条件?什么是竞争条件? 14.n14.n 个进程分别有标识号个进程分别有标识号 ((整数整数 )1,2,....)1,2,.... ,, NN。它们可同。它们可同

时存取文件时存取文件 filefile ,但要满足条件:参与同时存取文件的进,但要满足条件:参与同时存取文件的进程的标识号之和小于程的标识号之和小于 NN。写一个管程,协调多个进程对该。写一个管程,协调多个进程对该文件的存取。文件的存取。

15.15.缓冲区缓冲区 buffer1buffer1 的容量和缓冲区的容量和缓冲区 buffer2buffer2 的容量为无的容量为无穷。进程穷。进程 p1p1向向 buffer1buffer1输入产品,进程输入产品,进程 p2p2向向 bufferbuffer 输输入产品。但入产品。但 p1p1 和和 p2p2必须保证必须保证 buffer1buffer1 中的产品数量与中的产品数量与 bbuffer2uffer2 中产品数量之差在指定的范围中产品数量之差在指定的范围 [m,n][m,n]之间。试用之间。试用 PP 、、VV操作描述两个输入进程之间的协同关系。操作描述两个输入进程之间的协同关系。

16.16.在按优先级算法的进程调度中,如果不算闲逛进程,在按优先级算法的进程调度中,如果不算闲逛进程,是否可能没有现行进程或没有就绪进程或两者都没有?各是否可能没有现行进程或没有就绪进程或两者都没有?各是什么情况?运行进程是否一定是就绪队列中优先级最高是什么情况?运行进程是否一定是就绪队列中优先级最高的?的?

17.17.证明证明 SBF(SBF(最短周期优先最短周期优先 )) 对周转时间来说是最优的。对周转时间来说是最优的。

Page 131: 第 5 章 进程管理及进程间通讯

18.18.说明剥夺调度和非剥夺式调度的适用场合。说明剥夺调度和非剥夺式调度的适用场合。 19.19.一个一个 CPUCPU 分配给分配给 nn 个进程有多少不相同的分配方式使个进程有多少不相同的分配方式使

进程的执行顺序都不一样?如果进程的执行顺序都不一样?如果 nn 个进程在个进程在 22 个个 CPUCPU 上上运行,有多少种不同的执行顺序?运行,有多少种不同的执行顺序? ((提示:从排列组合上提示:从排列组合上考虑考虑 ))

20.20. 简述简述 LinuxUnixLinuxUnix 系统系统 VV的进程调度算法的特点。的进程调度算法的特点。 21.21.在集中式系统中,进程间的通信有哪些方式?这些方在集中式系统中,进程间的通信有哪些方式?这些方式各适用于什么场合?式各适用于什么场合?

22.22.设系统中仅有一类资源,数量为设系统中仅有一类资源,数量为 mm,由,由 nn 个进程竞争个进程竞争mm个资源。当个资源。当 Needi>=0Needi>=0对对 i=1,2,...,ni=1,2,...,n 且所有进程对该类且所有进程对该类资源的需求总和∑资源的需求总和∑ <m+n<m+n 。证明该系统是无死锁的。如果。证明该系统是无死锁的。如果不限定∑小于不限定∑小于 m+nm+n ,则该系统死锁的必要条件是什么?,则该系统死锁的必要条件是什么?

23.23.系统处于不安全状态,但有可能不导致死锁。请举例系统处于不安全状态,但有可能不导致死锁。请举例验证之。验证之。

24.24.考虑十字路口的交通死锁问题:把通过十字路口的汽考虑十字路口的交通死锁问题:把通过十字路口的汽车看作进程,十字路作为资源,画出进程资源图,说明产车看作进程,十字路作为资源,画出进程资源图,说明产生死锁问题的生死锁问题的 44个必要条件在此例中均成立。请用信号量个必要条件在此例中均成立。请用信号量方法建立避免死锁的交通规则。方法建立避免死锁的交通规则。