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

上传人:cn****1 文档编号:568782267 上传时间:2024-07-26 格式:PPT 页数:131 大小:408KB
返回 下载 相关 举报
第5章进程管理及进程间通讯_第1页
第1页 / 共131页
第5章进程管理及进程间通讯_第2页
第2页 / 共131页
第5章进程管理及进程间通讯_第3页
第3页 / 共131页
第5章进程管理及进程间通讯_第4页
第4页 / 共131页
第5章进程管理及进程间通讯_第5页
第5页 / 共131页
点击查看更多>>
资源描述

《第5章进程管理及进程间通讯》由会员分享,可在线阅读,更多相关《第5章进程管理及进程间通讯(131页珍藏版)》请在金锄头文库上搜索。

1、第5章 进程管理及进程间通讯n本章介绍了Linux进程的管理、调度以及Linux系统支持的进程间通讯机制,并对某些通信手段的内部实现机制进行了分析。本章还讨论了Linux核心的一些基本任务和机制,将Linux内核中为使内核其他部分能有效工作的用于同步的几种机制集中起来分析,强调了它们之间在实现和使用上的不同。确娟瓢井狮恕岳番怯氮戳荆瓢缉卢秧罢升殃饶药仗寸密是腥井苹永盖液视第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1 Linux 进程和线程一个大型的应用系统,往往需要众多进程协作。进程是操作系统理论的核心与基础,许多概念都和进程相关。进程的标准定义是:进程是可并发执行的程序在一个数

2、据集合上的运行过程。换句话说,在自身的虚拟地址空间运行的一个单独的程序称作一个进程。在Linux系统中,当一个程序开始执行后,在开始执行到执行完毕退出这段时间里,它在内存中的部分就被称作一个进程。进程与程序是有区别的,程序只是一些预先设定好的代码和数据,进程是一个随时都可能发生变化的、动态的、使用系统运行资源的程序。程序是静态的,而进程是动态的。一个程序可以启动多个进程。和进程联系在一起的不仅有进程的指令和数据,而且还有当前的指令指针、所有的CPU寄存器以及用来保存临时数据的堆栈等,所有这些都随着程序指令的执行在变化。后桌舜混甄海吏皂肃力记馏懊聪撇咆琅阜维账础泌右引燎捅掩癸联娇措虹第5章进程管

3、理及进程间通讯第5章进程管理及进程间通讯Linux操作系统包括三种不同类型的进程,每种类型的进程都有自己的特点和属性。(1) 交互进程由shell启动的进程。交互进程既可以在前台运行,也可以在后台运行。 (2) 批处理进程这种进程和终端没有联系,是一个进程序列。 (3) 监控进程(也称守护进程)Linux系统启动时启动的进程,并在后台运行。 上述三种进程各有各的作用,使用场合也有所不同。高龋诉闭妄战诊姿默伯铝戊赌羞羡嚼捂康燥艰景溜索再打旋太惑造医雅犀第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.1 Linux 进程管理的数据结构Linux是一个多任务的操作系统,在同一个时间内,可

4、以有多个进程同时执行。由于单CPU计算机实际上在一个时间片断内只能执行一条指令,Linux使用了一种称为“进程调度(processscheduling)”的机制。首先为每个进程指派一定的运行时间,然后依照某种规则,从众多进程中挑选一个投入运行,其他的进程暂时等待,当正在运行的那个进程时间耗尽,或执行完毕退出,或因某种原因暂停,Linux就会重新进行调度,挑选下一个进程投入运行。因为每个进程占用的时间片都很短,在用户的角度看,就好像多个进程同时运行一样。少撇灵七芯霞憎捣碌碰忻容卵吉帧札靠法惧读籍非浸筑僳衍洋蓟昨枉绿遂第5章进程管理及进程间通讯第5章进程管理及进程间通讯进程在运行过程中,要使用许多

5、计算机资源,例如CPU、内存、文件等。同时可能会有多个进程使用同一个资源,因此操作系统要跟踪所有的进程及其所使用的系统资源,以便能够管理进程和资源。在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(ProcessControlBlock,PCB)。PCB中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的是进程ID(processID,PID),进程ID也被称作进程标识符,是一个非负的整数,在Linux操作系统中唯一地标志一个进程。在最常使用的i386架构(即PC使用的架构)上,PID的变化范围是一个非负整数0-32767,这也是所有可能取到的进程ID。每个

6、进程的进程ID各不相同。可使用ps命令看看当前系统中有多少进程在运行。除标题外,每一行都代表一个进程。在各列中,PID一列代表了各进程的进程ID,command一列代表了进程的名称或在shell中调用的命令行。侩载胎渔泊冗烦继凭缆痘敌镑乘椎语履坝趁很酮栖恨畴群井崩色是庆稚惋第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux中的每个进程有自己的虚拟地址空间,操作系统的一个最重要的基本管理目的,就是避免进程之间的互相影响。但有时用户也希望能够利用两个或多个进程的功能完成同一任务,为此,Linux提供许多机制,利用这些机制,进程之间可以进行通讯并共同完成某项任务,这种机制称为“进程间通讯

7、(InterprocessCommunication,IPC)”。信号和管道是常见的两种IPC机制,但Linux也提供其他IPC机制。一般来说,Linux下的进程包含以下几个关键要素:有一段可执行程序;有专用的系统堆栈空间;内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;具有独立的存储空间。艰疙烽砍彪颧萄孤呀彦益蝎亡余贤式惧夹粱阮影捶讨慨范诌洽肿知不逾乐第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux内核利用一个数据结构task_struct来代表一个进程,代表进程的数据结构指针形成了一个task数组(在Linux中,任务和进程是两个相同的术

8、语),这种指针数组有时也成为指针向量。这个数组的大小默认为512,表明在Linux系统中能够同时运行的进程最多可有512。当建立新进程的时候,Linux为新的进程分配一个task_struct结构,然后将指针保存在task数组中。task_struct结构中包含了许多字段,按照字段功能,可分成如下几类:到听镐栽刷雁烧勘逆谜谩塘五帕缚詹斡慈移账既种饭制条萎格箔锹竿分侥第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(1)标识号。系统通过进程标识号唯一识别一个进程,但进程标识号并不是进程对应的task_struct结构指针在task数组中的索引号。另外,一个进程还有自己的用户和组标识号,系统

9、通过这两个标识号判断进程对文件或设备的访问权。n(2)状态信息。一个Linux进程可有如下几种状态:运行、等待、停止和僵死。n(3)调度信息。调度程序利用该信息完成进程之间的切换。n(4)有关进程间通讯的信息。系统利用这一信息实现进程间的通讯。n(5)进程链信息。在Linux系统中,除初始化进程之外,任何一个进程都具有父进程。每个进程都是从父进程中“克隆”出来的。进程链则包含进程的父进程指针、和该进程具有相同父进程的兄弟进程指针以及进程的子进程指针。另外,Linux利用一个双向链表记录系统中所有的进程,这个双向链表的根就是init进程。利用这个链表中的信息,内核可以很容易地找到某个进程。齿摘曼

10、极啸龚悲堆袭删齐尊慢蜒诽绘池糜捡斟禽蒋叁成封摔栅屑圣赏惦绽第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(6)时间和定时器。系统在这些字段中保存进程的建立时间,以及在其生命周期中所花费的CPU时间,这两个时间均以jiffies为单位。该时间由两部分组成,一是进程在用户模式下花费的时间,二是进程在系统模式下花的时间。Linux也支持和进程相关的定时器,应用程序可通过系统调用建立定时器,当定时器到期,操作系统会向该进程发送sigalrm信号。n(7)文件系统信息。进程可以打开文件系统中的文件,系统需要对这些文件进行跟踪。系统使用这类字段记录进程所打开的文件描述符信息。另外,还包含指向虚拟文

11、件系统(VirtualFileSystems,VFS)两个索引节点的指针,这两个索引节点分别是进程的主目录以及进程的当前目录。索引节点中有一个引用计数器,当有新的进程指向某个索引节点时,该索引节点的引用计数器会增加计数。未被引用的索引节点的引用计数为0,因此,当包含在某个目录中的文件正在运行时,就无法删除这一目录,因为这一目录的引用计数大于0。涡笆塑各孵迢令煌忧轿畏肖尾庸竿请獭开宗陀瞄眩荐杨伟擎率蒲帖饰矽啼第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(8)和进程相关的上下文信息。如前所述,进程可被看成是系统状态的集合,随着进程的运行,这一集合发生变化。进程上下文就是用来保存系统状态的

12、task_struct字段。当调度程序将某个进程从运行状态切换到暂停状态时,会在上下文中保存当前的进程运行环境,包括CPU寄存器的值以及堆栈信息;当调度程序再次选择该进程运行时,则会从进程上下文信息中恢复进程的运行环境。鉴肥帘梅孰座篱万溅氓玉胶馏浚止显搏棠唐控肆尺磷殆鬼点赁烟趋罢纤号第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.2 标识符信息和所有的Unix系统一样,Linux使用用户标识符和组标识符判断用户对文件和目录的访问许可。Linux系统中的所有文件或目录均具有所有者和许可属性,Linux据此判断某个用户对文件的访问权限。笆占绷爹述居芜叙嗡习川辞还响匡卸翅省暑郁食庇唤锁嘲

13、峡湿壶揭半屏祁第5章进程管理及进程间通讯第5章进程管理及进程间通讯对一个进程而言,系统在task_struct结构中记录如表5.1所示的4对标识符。uid 和 gid 运行进程所代表的用户之用户标识号和组标识号,通常就是执行该进程的用户。有效uid 和 gid 某些程序可以将 uid 和 gid 改变为自己私有的 uid 和 gid。系统在运行这样的程序时,会根据修改后的 uid 及 gid 判断程序的特权,例如,是否能够直接进行 I/O 输出等。通过 setuid 系统调用,可将程序的有效 uid 和 gid 设置为其他用户。在该程序映像文件的 VFS 索引节点中,有效 uid 和 gid

14、由索引节点的属性描述。文件系统uid 和 gid 这两个标识符和上述标识符类似,但用于检查对文件系统的访问许可时。处于用户模式的 NFS 服务器作为特殊进程访问文件时使用这两个标识符。保存uid 和 gid 如果进程通过系统调用修改了进程的 uid 和 gid,这两个标识符则保存实际的 uid 和 gid。串篓邦幅砌镊敝袋起潭蓖讫厘尸歇季纱怜奄蘸竹午橇娱坤镐衫宪仆驶锭堤第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.3 进程状态信息Linux中的进程有4种状态,如表5.2所示。运行状态 该进程是当前正在运行的进程;或者,该进程是可以运行的进程,即正在等待调度程序将 CPU 分配给它

15、。等待状态 进程正在等待某个事件或某个资源。这种进程又分为可中断的进程和不可中断的进程两种。可中断的等待进程可被信号中断,而不可中断的等待进程是正在直接等待硬件状态条件的进程,在任何情况下都不能被中断。停止状态 进程处于停止状态,通常由于接收到信号而停止,例如,进程在接收到调试信号时处于停止状态。僵死状态 进程已终止,但在 task 数组中仍占据着一个 task_struct 结构。顾名思义,处于这种状态的进程实际是死进程。遣活蝶蝶陨翰填咖阳涕于示离悄蛰歹碳肚拨伤愿洗操枣池梁久屈柠懒胯焙第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.4 文件信息如图5.1所示,系统中的每个进程有两

16、个数据结构用于描述进程与文件相关的信息。其中,fs_struct描述了上面提到的指向VFS两个索引节点的指针,即root和pwd。另外,这个结构还包含一个umask字段,它是进程创建文件时使用的默认模式,可通过系统调用修改这一默认模式。另一个结构为files_struct,它描述了当前进程所使用的所有文件信息。从图中可以看出,每个进程能够同时拥有256个打开的文件,fs0到fs255就是指向这些file结构的指针。文件的描述符实际就是fs指针数组的索引号。济氯数瓣购臆型鬼羽宦革帕滩营始伯秆鱼迪勃沁暴涅顶拓乱锣参鞋涛出代第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.1进程的文件信息蜂

17、兰且四铣斡兴验憋晶性嗽缕印县班脓雨膨鲁冤秃砸拭圾鹿钮缔彬巡舌遣第5章进程管理及进程间通讯第5章进程管理及进程间通讯在file结构中,f_mode是文件的打开模式,只读、只写或读写;f_pos是文件的当前位置;f_inode指向VFS中该文件的索引节点;f_op包含了对该文件的操作例程集。利用f_op,可以针对不同的文件定义不同的操作函数,例如一个用来向文件中写数据的函数。Linux利用这一抽象机制,实现了管道这一进程间通讯机制。这种抽象方法在Linux内核中很常见,通过这种方法,可使特定的内核对象具有类似C+对象的多态性。Linux进程启动时,有三个文件描述符被打开,它们是标准输入、标准输出和

18、错误输出,分别对应fs数组的三个索引,即0、1和2。如果启动时进行输入输出重定向,则这些文件描述符指向指定的文件而不是标准的终端输入/输出。每当进程打开一个文件时,就会利用files_struct的一个空闲file指针指向打开的文件描述结构file。对文件的访问通过file结构中定义的文件操作例程和虚拟文件系统(VirtualFileSystem,VFS)的索引节点信息来完成。珐足痞拱勘舵衅渗脱恬电迪辗袜盎贫恼愁砒卸思勇悠演各颅斟彩招锌贾氮第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.5 虚拟内存进程的虚拟内存包含了进程所有的可执行代码和数据。运行某个程序时,系统要根据可执行映像

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

20、柬侩滔娥译扔咯联腥魂妮樱赐原店望敦角倪浪瞥钳雍斩搐怀伤袜彰史第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.2进程的虚拟内存示意牵台厕眷韦钳帮附插雕培蔼挠笺艳炉妆矣孟掣原篓卢讽顾却诡琐百茄睫肢第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的task_struct结构中包含一个指向mm_struct结构的指针。进程的mm_struct则包含装入的可执行映像信息以及进程的页表指针。该结构还包含有指向vm_area_struct结构的几个指针,每个vm_area_struct代表进程的一个虚拟地址区域。图5.2是某个进程的虚

21、拟内存简化布局以及相应的进程数据结构。从图中可以看出,系统以虚拟内存地址降序排列vm_area_struct。每个虚拟内存区域可能来源不同,有的可能来自映像,有的可能来自共享库,而有的则可能是动态分配的内存区。因此,Linux利用了虚拟内存处理例程(vm_ops)来抽象对不同来源虚拟内存的处理方法。粕额粥兄婶智根赡较缕惫园铭捧宰苑仿模置抑丧橇一帅执佛氛步宁健饿圣第5章进程管理及进程间通讯第5章进程管理及进程间通讯在进程的运行过程中,Linux要经常为进程分配虚拟地址区域,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,vm_area_struct结构的访问时间就成了性能的关键因素。除链表

22、结构外,Linux还利用AVL(Adelson-VelskiiandLandis)树组织vm_area_struct。通过这种树结构,Linux可以快速定位某个虚拟内存地址,但在该树中插入或删除节点需要花费较多的时间。当进程利用系统调用动态分配内存时,Linux首先分配一个vm_area_struct结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区域时,因为Linux尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生页故障,当Linux处理这一页故障时,就可以为新的虚拟内存区分配实际的物理内存。奏柜邻傈肾威酥即核浙困糙淮柠憋梨恳警别衫洗坊效腮爽吴助肾售贫呈侦

23、第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.6 时间和定时器 Linux保存一个指向当前正在运行的进程的task_struct结构的指针,即current。每当产生一次实时时钟中断(又称时钟周期),Linux就会更新current所指向的进程的时间信息,如果内核当前代表该进程执行任务(例如进程调用系统调用时),那么系统就把进程在系统模式下花费的时间作为时间记录,否则将进程在用户模式下花费的时间作为时间记录。芒劝府肚匈猿议卸秦瑚充问犹纳磺孤号响绿驮至颐彼濒欣尚盯涕拱绘慑辞第5章进程管理及进程间通讯第5章进程管理及进程间通讯除了为进程记录其消耗的CPU时间外,Linux还支持和进程

24、相关的间隔定时器。当定时器到期时,会向定时器的所属进程发送信号。进程可使用三种不同类型的定时器来给自己发送相应的信号,如表5.3所示。Real该定时器实时更新,到期时发送 SIGALRM 信号。Virtual该定时器只在进程运行时更新,到期时发送 SIGVTALRM 信号。Profile该定时器在进程运行时,以及内核代表进程运行时更新,到期时发送 SIGPROF 信号。Linux对Virtual和Profile定时器的处理是相同的,在每个时钟中断,定时器的计数值减1,直到计数值为0时发送信号。Real定时器的处理比较特殊。菊晕住维彦宙亚可累塞捂饭盛平服远固跋耪皂蒲仕昌诊务剑疮邵窟宣了振第5章进

25、程管理及进程间通讯第5章进程管理及进程间通讯5.1.7 关于线程和进程概念紧密相关的概念是线程。线程可看成是进程中指令的不同执行路线。例如,常见的字处理程序中,主线程处理用户输入,而其他并行运行的线程在必要时可在后台保存用户的文档。与进程相关的基本要素有:代码、数据、堆栈、文件I/O和虚拟内存信息等,因此,系统对进程的处理要花费更多的开支,尤其在进行进程调度时。利用线程则可以通过共享这些基本要素而减轻系统开支,因此,线程也被称为“轻量级进程”。许多流行的多任务操作系统均支持线程。绣哪旷玉天糠冲丰充趴鼠揣信打矽精旗煽略榴滞季吻泡芦忆攒幅仁脾希抽第5章进程管理及进程间通讯第5章进程管理及进程间通讯

26、线程有“用户线程”和“内核线程”之分。所谓用户线程是指不需要内核支持而在用户程序中实现的线程,这种线程甚至在象DOS这样的操作系统中也可实现,但线程的调度需要用户程序完成,类似于Windows3.x的协作式多任务。另外一种则需要内核的参与,由内核完成线程的调度。这两种模型各有其优缺点:用户线程不需要额外的内核开支,但是当一个线程因I/O而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核线程则没有这个限制,但却占用了更多的系统开支。Linux支持内核空间的多线程,读者也可以从Internet上下载一些用户级的线程库。粱屎打冈秽境齐记淹儡齿幅柔蹋灼铲驹游凸刨泳

27、练湖撅弹毒忍岁冲嗜冻岂第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux的内核线程和其他操作系统的内核实现不同。大多数操作系统单独定义线程,从而增加了内核和调度程序的复杂性;而Linux则将线程定义为“执行上下文”,实际只是进程的另外一个执行上下文而已。这样,Linux内核只需区分进程,只需要一个进程/线程数组,而调度程序仍然是进程的调度程序。Linux的克隆(clone)系统调用可用来建立新的线程。热校皂首侦烷跟售简禹鸿荐药馈拳菱椭神读钥米宽眺骗菠醇艰丘傣俄肋蛛第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.1.8 会话和进程组 在Unix系统中,父进程创建子进程,子进程

28、可以再创建新进程,形成一定的层次,称为“进程组”。一个或多个进程可以合起来构成一个进程组(processgroup),一个或多个进程组可以合起来构成一个会话(session)。这样,就有了对进程进行批量操作的能力,例如通过向某个进程组发送信号以实现向该组中的每个进程发送信号。Linux内核通过维护会话和进程组而管理多用户进程。如图5.3所示,每个进程是一个进程组的成员,而每个进程组又是某个会话的成员。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。茎陛柬聋罩洁拘幅暮罩爱魁蛛魏帆

29、皂蓉吝峪懈钱芥流觅潭划推味圣室脖巷第5章进程管理及进程间通讯第5章进程管理及进程间通讯同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。图5.3会话和进程、进程组恃查灿必箩罪修痞集弃需炒写梆杭非拘馋雁疟枫衰栗痢泌铬蕊滨硫竹拇及第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.2 进程的创建和进程调度5.2.

30、1 进程的创建第一个进程在系统启动时创建,当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的task_struct数据结构中。在系统初始化结束的时候,系统初始化结束时,初始进程启动一个内核线程initinit,而自己则处于空循环状态。当系统中没有可运行的进程时,调度程序会运行这个空闲的进程。这个空闲进程的task_struct是唯一的不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做init_task。米慕禽岗膳潍长削晓睫辰嫌冒谬梧升垦骑凯榴蕾

31、憾谗信长惨肄吵淆星抖淫第5章进程管理及进程间通讯第5章进程管理及进程间通讯initinit内核线程/进程的标识号为1,它是系统的第一个真正进程。它负责初始的系统设置工作,例如打开控制台,挂装文件系统等。然后,initinit进程执行系统的初始化程序,这一程序可能是/etc/init、/bin/init或/sbin/init。initinit程序将/etc/inittab当作脚本文件建立系统中新的进程,这些新的进程又可以建立新进程。例如,gettygetty进程可建立loginlogin进程来接受用户的登录请求。图5.4父进程和子进程共享打开的文件完讨铭呸葡和敝抡姜凿痊村烂财迹吮涂没父迎们置磊兜

32、墓拨籍桔数冬赢皆第5章进程管理及进程间通讯第5章进程管理及进程间通讯新的进程通过克隆旧的程序(当前程序)而建立。forkfork和cloneclone系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新的task_struct结构,同时为新进程要使用的堆栈分配物理页。Linux还会为新的进程分配新的进程标识符。然后,新task_struct结构的地址保存在task数组中,而旧进程的task_struct结构内容被复制到新进程的task_struct结构中。在克隆进程时,Linux允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等。当某

33、个资源被共享时,该资源的引用计数值会增加1,从而只有两个进程均终止时,内核才会释放这些资源。图5.4说明了父进程和子进程共享打开的文件。惜豌巢惊烽钩惰巾凤婴周屹醉鹊抽沽踩瓶犹悼扎艺原脉柿咋航前腑潭黔叭第5章进程管理及进程间通讯第5章进程管理及进程间通讯系统对进程虚拟内存的克隆过程则更加巧妙。新的vm_area_struct结构、新进程自己的mm_struct结构以及新进程的页表必须在一开始就准备好,但这时并不复制任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,而有些在交换文件中,那么虚拟内存的复制将会非常困难和费时。Linux采用了称为“写时复制”的技术,只有当两个进程中的任意一个向虚拟内

34、存中写入数据时才复制相应的虚拟内存;而没有写入的任何内存页均可以在两个进程之间共享。代码页实际总是可以共享的。为实现“写时复制”技术,Linux将可写虚拟内存页的页表项标志为只读。当进程要向这种内存页写入数据时,处理器会发现内存访问控制上的问题(向只读页中写入),从而导致页故障。于是,操作系统可捕获这一被处理器认为是“非法的”写操作而完成内存页的复制。最后,Linux还要修改两个进程的页表以及虚拟内存数据结构。鹏电服真陶怠谓笋楞畦驯宠亚寸亡烷质埃般剔啦俱纷况硫畜聘锹心躁硬阻第5章进程管理及进程间通讯第5章进程管理及进程间通讯进程终止条件有如下几种:(1)进程运行结束,正常退出(主动终止);(2

35、)发生可预料的错误,报错退出(主动终止);(3)发生严重错误,进程异常终止(被动终止);(4)被其他进程终止(被动终止)。新部秽厌遵竭涯枪俺俭瞅搞亦杠续味吩宇魔设碍舆喻纳限吾椰铁读纶碌鹰第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.2.2 进程的管理和调度Linux是一个多任务操作系统,它要保证CPU时刻保持在使用状态,如果某个正在运行的进程等待外部设备完成工作(例如等待打印机完成打印任务),这时,操作系统就可以选择其他进程运行,从而保持CPU的最大利用率。这就是多任务的基本思想,进程之间的切换由调度程序完成。不同用途的系统其调度算法的目标有共性,也有各自独有的倾向。例如批处理系统的

36、目标主要是增大每小时作业量(吞吐量),降低作业提交和终止之间的时间(周转时间),CPU利用率(保持CPU总在工作);而交互式系统要求对用户要求做出快速反应(响应时间)和满足用户期望(均衡);实时系统则要求不丢失数据(工作低限),在多媒体系统中避免降低媒体质量(可预见性)等。惮潞描旺浦饮仔轩店旬定晶样什牵混桂年粮眨灶导虹舟乓锣炬应吻琳吃接第5章进程管理及进程间通讯第5章进程管理及进程间通讯无论是什么系统,系统共同的目标都是:n(1)公平给每个进程分配相同的CPU时间;n(2)坚持保证制定的策略完满执行;n(3)平衡保证系统的各个部分都在工作。1.i386体系的进程管理和调度Intel在i386体

37、系的设计中考虑到了进程的管理和调度,并从硬件上支持任务间的切换。为此目的,Intel在i386系统结构中增设了一种新段“任务状态段”TSS(TaskStatusSegment)。一个TSS虽然说像代码段、数据段等一样也是一个段,实际上却是一个104字节的数据结构,用以记录一个任务的关键性的状态信息。像其他段一样,TSS也要在段描述表中有个表项。不过TSS只能在全局描述符表GDT(GlobalDescribtorTable)中,而不能放在任何一个局部描述符表LDT(LocalDescribtorTable)中或中断描述表IDT(InterruptDescriberTable)中。若通过一个段选择

38、项访问一个TSS,而选择项中的TI位为1,就会产生一次GP异常。骇汰埃孽秧估支些芝狮期老痔堡信彰秘辈憋认罢阎脑瘁橇扦院洲啥吩疤程第5章进程管理及进程间通讯第5章进程管理及进程间通讯另外,CPU中还增设一个任务寄存器TR,指向当前任务的TSS。相应地,还增加了一条指令LTR对TR寄存器进行装入操作。像CS和DS寄存器一样,TR也有一个程序不可见部分,每当将一个段选择码装入到TR中时,CPU就会自动找到所选择的TSS描述项并将其装入到TR的程序不可见部分,以加速以后对该TSS段的访问。在IDT表中,除了中断门、陷阱门和调用门以外,还定义了一种任务门。任务门中包含一个TSS段选择码。当CPU因中断而

39、穿过一个任务门时,就会将任务门中的选择码自动装入TR,使TR指向新的TSS,并完成任务的切换。CPU还可以通过JMP和CALL指令实现任务切换,当跳转或调用的目标段实际上指向GDT表中的一个TSS描述项时,就会引起一次任务切换。荫衣臂箔怜并像啄玖镣絮麓赡澎师迷履宿绥牲凰晴跟谊宏忿臂庶哭瓦掣门第5章进程管理及进程间通讯第5章进程管理及进程间通讯2.Linux系统对进程状态管理的实现机制从系统内核的角度来看,一个进程仅仅是进程控制表(processtable)中的一项。在Linux中,每个进程用一个task_struct的数据结构来表示,进程控制表中的每一项都是一个task_struct结构,用来

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

41、市徘毒藤烫妨间总蔫乞绎酿体氦包婴执充捞萨藏种辆枢师毒腾彭邹耸侧清第5章进程管理及进程间通讯第5章进程管理及进程间通讯每个在task_struct结构中登记的进程都有相应的进程状态和进程标志,是进行进程调度的进程调度的两个重要的数据项。进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也就是改变state和flags这两个数据项。进程的状态不同、标志位不同对应了进程可以执行不同操作。structtask_struct.volatilelongstate;/-1unrunnable,0runnable,0stoppedunsignedlongflags;/perprocessf

42、lags,definedbelow.;漂开珊氦帝泄愈狼藏钧凶铰屎弯喜悠暗凹罩逼犀法泄眨换刻价札校缆旁蝴第5章进程管理及进程间通讯第5章进程管理及进程间通讯在Linux2.2.0及以后版本的sched.h中定义了进程的六种状态,十三种标志。各个标志位的代表着不同含义,对应着不同调用。/进程状态#defineTASK_RUNNING0#defineTASK_INTERRUPTIBLE1#defineTASK_UNINTERRUPTIBLE2#defineTASK_ZOMBIE4#defineTASK_STOPPED8#defineTASK_SWAPPING16推亨宅孟劈夺栏魄姐列腥称唬脉滇腋业狡肮

43、嘲萧岳路侵垄帮磋犊捡诀芍今第5章进程管理及进程间通讯第5章进程管理及进程间通讯进程控制表既是一个数组,又是一个双向链表,同时又是一个树。其物理实现是一个包括多个指针的静态数组。此数组的长度保存在include/Linux/tasks.h定义的常量NR_TASKS中,其默认值为128,数组中的结构则保存在系统预留的内存页中。链表是由next_task和prev_task两个指针实现的,而树的实现则比较复杂。系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的全局指针变量current用来记录正在运行的进程。变量current只能由kernel/sched.c中的进程调度改变

44、。当系统需要查看所有的进程时,则调用for_each_task,这将比系统搜索数组的速度要快得多。傣胁援卡裙眺描咋脉镰耍但并朝影亩脓冈琅士萨葛卜言跃骆档珐毋歹哺杆第5章进程管理及进程间通讯第5章进程管理及进程间通讯3.竞争条件,RacingConditions在图5.5中,一个Spooler目录下有许多槽,编号为0、1、2、3,槽中存放要打印文件的文件名。设置了两个共享变量:out指明下一个被打印的文件,in指向目录中下一个空闲槽,这两个变量保存于所有进程都可以访问的文件中。正常的进程访问过程是:读取in的值,将文件名存于相应槽中,将in的值加1。图5.5竞争条件(RacingConditio

45、ns)屯可破搔韩溺屑差吩怪剃晨蝇毋虹好烷醋移紫势茫躺煌式滔败望喀派萨闲第5章进程管理及进程间通讯第5章进程管理及进程间通讯在某一时刻,0-3号槽为空(其中的文件打印完毕),4-6号槽被占用(其中文件等待打印)。此时,有两个进程(进程A和进程B)决定要打印文件(A.txt和B.txt),这时就可能发生以下的情况。进程A处于运行态,读到in的值为7,正当进程A准备将A.txt的文件名放到7号槽时发生了进程切换,进程B开始运行;进程B运行正常,读取in的值为7(尚未被进程A更改),将文件名B.txt存入7号槽,将in的值改为8;当进程A再次运行时,in的值已经改为8,但A会从上次中止的地方继续运行,

46、这意味着从A的角度看in的值仍为7,于是A将A.txt存入7号槽(覆盖了B.txt),然后把in的值改为8。这样,B.txt将永远不会被打印。两个或者多个进程读/写共享数据,而最后的运行结果取决于进程运行的精确时序,这样的情况称为竞争条件。在存在竞争条件时,就可能产生无法预知的错误(如上例中,A.txt和B.txt就没有正确的打印)。旬挨吠株沏咏户忍呀颤标京杯缝艺渔赛氛栏徊进战褪士璃强宇忙哮唉化轧第5章进程管理及进程间通讯第5章进程管理及进程间通讯4.临界区上例的问题在于,当进程A访问共享数据的过程尚未结束时,进程B访问了数据。显然,如果在进程A访问时阻塞进程B,在进程A完成了对共享数据的访问

47、后才允许进程B访问,就不会发生错误。凡是涉及到共享资源的情况,无论共享内存、共享文件或是其他资源,都可能引发上述错误。要避免这种错误,就必须寻找某些途径来阻止多于一个的进程同时读写共享数据。换句话说,读写必须按顺序进行。所谓互斥就是oneatatime,当一个进程在访问共享数据时,其他进程无法对该数据进行任何操作。通常,把进程中访问共享数据的程序片段称为临界区。要避免竞争条件,就必须避免多个进程同时处于临界区。图5.6是临界区的一个图示说明。携丝缨藏膀瑟薛咀买倦肿谊铆瓮沟颖谤秘瘟撩啊饥纺告巧侨犀狈祸宴回诫第5章进程管理及进程间通讯第5章进程管理及进程间通讯好的解决方案,需要以下4个条件:(1)

48、任何两个进程不能同时处于临界区;(2)不应对CPU的速度和数目作任何假设;(3)临界区外的进程不得阻塞其他进程;(4)不得使进程在临界区外无休止的等待。图5.6临界区炙初畦循馈消吼乾裕禹晌眶怨拣拟采市辨豢讽翘祖涧质笺押擞段渤准漫蠕第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.用户进程和内核线程一个进程只能运行在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)。尽管Linux是一个宏内核系统,内核线

49、程依然存在,以便并行地处理一些内核的日常工作。这些任务不占用用户空间(usermemory),而仅仅使用内核空间(kernelmemory)。和其他内核模块一样,它们也在高级权限(i386系统中的RING0)下工作。内核线程是被核心线程(kernel_thread)创建的。通过调用著名的clone系统调用,例如fork系统调用的所有功能都是由它最终实现(参看arch/i386/kernel/process.c)。章婚旭沿舀副蜜硼或彭异同秸霜津举押瑶纬商痈磊箭兹僧柏蚂噎缺栏蜂糊第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.2.3 进程的切换虽然Intel提供了十分简洁的任务切换机制。但

50、实际上,i386中通过JMP指令或CALL指令自动完成的任务切换的过程是一个相当复杂的过程,其执行过程长达300多个CPU时钟周期。在CPU实际上的执行过程中,有的工作在一定条件下是可以简化的;在某些条件下,一些工作则可能应按不同的方式组合。此外,任务的切换往往不是孤立的,常常与其他操作有紧密的联系。为了达到更高的效率和更大的灵活性,Linux并不直接采用i386硬件提供的任务切换机制。腾降险唱功遮楷新品虏议丹棱矩文化浊鱼赎施蝉酒曳蜗晶所骇殆录楔肺今第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux内核为了满足i386CPU的要求,只是在初始化的时候设置装载任务寄存器TR,使之指向

51、一个TSS,从此以后就不再修改TR的值。也就是说,每个CPU在初始化以后就永远使用同一个TSS。同时,内核也不依靠TSS保存每个进程切换时的寄存器副本,而是将这些寄存器的副本保存在各自进程的系统空间堆栈中。Linux中的进程有一些部分运行在用户模式,而另一些部分运行在内核模式,或称系统模式。运行模式的变化是通过系统调用完成的。Linux从用户态转移到内核态有三个途径:系统调用、中断和异常。对应的代码在entry.s中。掠亏揣飘塞借坐桩佬孤邻进疾个洱馈德虑乌圈猖思簧迈拖伊词起柑萨艳宗第5章进程管理及进程间通讯第5章进程管理及进程间通讯系统模式具有更加高级的CPU特权级,例如可以直接读取或写入任意

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

53、程平等分配CPU资源,内核在task_struct结构中记录如表5.4所示的信息。评邵胖宽硬凰栅婶途粪处葬汹方邹溉挨旺巷景触瞒东园绣芬陆男龙绪鲸写第5章进程管理及进程间通讯第5章进程管理及进程间通讯字段描述policy(策略) 系统对该进程实施的调度策略。Linux 进程有两种类型的进程:一般进程和实时进程。实时进程比所有一般进程的优先级高,只有一个实时进程可以运行,调度程序就会选择该进程运行。对实时进程而言,有两种调度策略,一种称为“循环赛 (round robin)”,另一种称为“先进先出 (first in first out)”。priority(优先级) 这是系统为进程给定的优先级,

54、可通过系统调用或 renice 命令修改该进程的优先级。优先级实际是从进程开始运行算起的、允许进程运行的时间值(以 jiffies 为单位)。rt_priority(实时优先级)这是系统为实时进程给定的相对优先级。counter(计数器) 这是进程运行的时间值(以 jiffies 为单位)。开始运行时设置为 priority,每次时钟中断该值减 1。表5.4和进程调度相关的task_struct信息最留坚初闸僻三拟耍煤绵浙牡酿八希垢栈剩噎杉撬退允垃噶怯碳真京斗游第5章进程管理及进程间通讯第5章进程管理及进程间通讯当需要选择下一个运行进程时,由调度程序选择最值得运行的进程。Linux使用了比较简

55、单的基于优先级的进程调度算法选择新的进程。进程的切换时需要作三个层次的工作:n(1)用户数据的保存:包括正文段(TEXT)、数据段(DATA,BSS)、栈段(STACK)、共享内存段(SHAREDMEMORY)的保存。n(2)寄存器数据的保存:包括PC(programcounter,指向下一条要执行的指令的地址)、PSW(processorstatusword,处理机状态字)、SP(stackpointer,栈指针)、PCBP(pointerofprocesscontrolblock,进程控制块指针)、FP(framepointer,指向栈中一个函数的local变量的首地址)、AP(augum

56、entpointer,指向栈中函数调用的实参位置)、ISP(interruptstackpointer,中断栈指针)以及其他的通用寄存器等。n(3)系统层次的保存:包括proc、,虚拟存储空间管理表格和中断处理栈,以便于该进程再一次得到CPU时间片时能正常运行下去。褂崖狈边读纱诚支绩吏故玲泡绝书啄廓复仍坎痴豆匝干谓唯玄宏朝衅萤捌第5章进程管理及进程间通讯第5章进程管理及进程间通讯当调度程序选择了新的进程之后,它必须在当前进程的task_struct结构中保存和该进程相关的CPU寄存器和其他有关指令执行的上下文信息,然后从选定进程的task_struct结构中恢复CPU寄存器以及上下文信息,新的

57、进程就可以继续在CPU中执行了。对于新建的进程,其task_struct结构被置为初始的执行上下文,当调度进程选择这一新建进程时,首先从task_struct结构中恢复CPU寄存器,CPU的指令计数寄存器(PC)恰好是该进程的初始执行指令地址,这样,新建的进程就可以从头开始运行了。调度程序在如下几种情况下运行:当前进程处于等待状态而放入等待队列时;某个系统调用要返回到用户模式之前,这是因为系统调用结束时,当前进程的counter值可能刚好为0。下面是调度程序每次运行时要完成的任务:痉晓虹麻朔相罕息流堵尊乔焉此威肌崎舆檀盯选孔缸镊眷蛀蛰劳逝娥伞贬第5章进程管理及进程间通讯第5章进程管理及进程间通

58、讯n(1)调度程序运行底层半处理程序(bottomhalfhandler)处理调度程序的任务队列。这些处理程序实际是一些内核线程。n(2)在选择其他进程之前,必须处理当前进程。如果当前进程的调度策略为循环赛,则将当前进程放到运行队列的尾部;如果该进程是可中断的,并且自上次调度以来接收到信号,则任务状态设置为运行;如果当前进程的counter值为0,则任务状态也变为运行;如果当前进程状态为运行,则继续保持此状态;如果进程既不处于运行状态,也不是可中断的,则从运行队列中移去该进程,这表明调度程序在选择最值得运行的进程时,该进程不被考虑。针葱滑挤苇宁谩腹圆厨绷馋救袄部匡咒汕垄煞娩缝登洲肤运垣少檀凋袋

59、肥第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(3)调度程序在运行队列中搜索最值得运行的程序。调度程序通过比较权重来选择进程。对实时进程而言,它的权重为counter加1000;对一般进程而言,权重为counter。因此,实时进程总是会被认为是最值得运行的进程。如果当前进程的优先级和其他可运行进程一致,则因为当前进程至少已花费了一个时间片,因此,总处于劣势。如果许多进程的优先级一样,则调度程序选择运行队列中最靠前的进程,这实际就是“循环赛”调度。n(4)如果最值得运行的进程不是当前进程,就需要交换进程(或切换进程)。进程交换的作用是保存当前进程的运行上下文,同时恢复新进程的运行上下文

60、。交换的具体细节和CPU类型有关,但需要注意的是,交换进程时调度程序运行在当前进程的上下文中,另外,调度程序还需要设置某些关键的CPU寄存器并刷新硬件高速缓存。吾标倦婿起量岛销亮栋盔责菠斯诣匆喷彼坎慌尝缨司撂啦篱孝鳖尾汤丛彪第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux内核已具备在对称多处理系统SMP(symmetricmultiprocessing)上运行的能力。在多处理器系统中,每个处理器都在运行着进程。当运行在某个处理器上的进程耗尽其时间片,或者该进程处于等待状态时,该处理器将单独运行调度程序来选择新的进程。每个处理器有一个自己的空闲进程,而每个处理器也有自己的当前进程。

61、为了跟踪每个处理器的空闲进程和当前进程,进程的task_struct中包含了正在运行该进程的处理器编号(processor字段),以及上次运行该进程的处理器编号(last_processor字段)。显然,当一个进程再次运行时,可由不同的处理器运行,但在不同处理器上的进程交换所需开支稍微大一些。为此,每个进程有一个processor_musk字段,如果该字段的第N位为1,则该进程可以运行在第N个进程上,利用这一字段,就可以将某个进程限制在单个处理器上运行。亡掀咐乒查衬瘪剁嵌厌搅艘痰顺铭挥砖介纽龚劝苔者霄苞万塘屡腹恕誓汐第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.3 可执行程序和Uni

62、x类似,Linux中的程序和命令通常由命令解释器执行,这一命令解释器称为shell。用户输入命令之后,shell会在搜索路径(shell变量PATH中包含搜索路径)指定的目录中搜索和输入命令匹配的映像名称。如果发现匹配的映像,shell负责装载并执行该命令。shell首先利用forkfork系统调用建立子进程,然后用找到的可执行映像文件覆盖子进程正在执行的shell二进制映像。可执行文件可以是具有不同格式的二进制文件,也可以是一个文本的脚本文件。可执行映像文件中包含了可执行代码及数据,同时也包含操作系统用来将映像正确装入内存并执行的信息。Linux使用的最常见的可执行文件格式是可执行可连接格式

63、(ExecutableandLinkingFormat,ELF)和a.out。理论上,Linux有足够的灵活性可以装入任何格式的可执行文件。迷窥钠赣足脖奢趣陵费溉衍声苹径练抠颇猴扣痛饮患沃瘦球饭缨涕甲样堕第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.3.1 可执行可连接格式 可执行可连接格式(ExecutableandLinkingFormat,ELF)由Unix系统实验室制定。它是Linux中最经常使用的格式。和其他格式(例如a.out或ECOFF格式)比较,ELF在装入内存时多一些系统开支,但是更为灵活。ELF可执行文件包含了可执行代码和数据,通常也称为正文和数据。这种文件中包含

64、了几个表,根据这些表中的信息,内核可组织进程的虚拟内存。另外,文件中还包含有对内存布局的定义以及起始执行的指令位置。硷希捕县恐息膨玖服蠕芍爆绦氏撵想暴墒面蒙引萝皿鸟傈匡妓电相颊苹藏第5章进程管理及进程间通讯第5章进程管理及进程间通讯分析如下简单程序在利用编译器编译并连接之后的ELF文件:#includemain()printf(“Helloworld!n”);图5.7一个简单的ELF可执行文件的布局淆咨滚咽某堰城深阻吃历仑拄深柳邑泽蚁驭上碟浆峭逝蔽崎芬奉呐美孩爸第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.7是上述源代码在编译连接后的ELF可执行文件的格式。可以看出,ELF可执行映

65、像文件的开头是三个字符E、L和F,作为这类文件的标识符。e_entry定义了程序装入之后起始执行指令的虚拟地址。这个简单的ELF映像利用两个“物理头”结构分别定义代码和数据,e_phnum是该文件中所包含的物理头信息个数,本例为2。e_phyoff是第一个物理头结构在文件中的偏移量,而e_phentsize则是物理头结构的大小,这两个偏移量均从文件头开始算起。根据上述两个信息,内核可正确读取两个物理头结构中的信息。焦未飘曾坟浪攘淄构嚣京颗耍孜苹公砧狭趟絮栖勤村赴俐础抚贩颐囊肚爵第5章进程管理及进程间通讯第5章进程管理及进程间通讯物理头结构的p_flags字段定义了对应代码或数据的访问属性。图中

66、第一个p_flags字段的值为FP_X和FP_R,表明该结构定义的是程序的代码;类似地,第二个物理头定义程序数据,并且是可读可写的。p_offset定义对应的代码或数据在物理头之后的偏移量。p_vaddr定义代码或数据的起始虚拟地址。p_filesz和p_memsz分别定义代码或数据在文件中的大小以及在内存中的大小。对的简单例子,程序代码开始于两个物理头之后,而程序数据则开始于物理头之后的第0x68533字节处,显然,程序数据紧跟在程序代码之后。程序的代码大小为0x68532,显得比较大,这是因为连接程序将C函数printfprintf的代码连接到了ELF文件的原因。程序代码的文件大小和内存大

67、小是一样的,而程序数据的文件大小和内存大小不一样,这是因为内存数据中,起始的2200字节是预先初始化的数据,初始化值来自ELF映像,而其后的2048字节则由执行代码初始化。窘尿戚祥刘挎秒畸秧醋抖域圃寐占童衡红咐如咐瓶颂艇吃塘浑汰垒虎渡旬第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux利用需求分页技术装入程序映像。当shell进程利用forkfork系统调用建立了子进程之后,子进程会调用execexec系统调用(实际有多种execexec调用),execexec系统调用将利用ELF二进制格式装载器装载ELF映像,当装载器检验映像是有效的ELF文件之后,就会将当前进程(实际就是父进程

68、或旧进程)的可执行映像从虚拟内存中清除,同时清除任何信号处理程序并关闭所有打开的文件(把相应file结构中的f_count引用计数减1,如果这一计数为0,内核负责释放这一文件对象),然后重置进程页表。完成上述过程之后,只需根据ELF文件中的信息将映像代码和数据的起始和终止地址分配并设置相应的虚拟地址区域,修改进程页表。这时,当前进程就可以开始执行对应的ELF映像中的指令了。抖蚂磐拖该撅手藩先耗瞧椅撰恐弊请萌端柞炮写带留埋鲁告短崎峻腥潮萝第5章进程管理及进程间通讯第5章进程管理及进程间通讯和静态连接库不同,动态连接库可在运行时连接到进程虚拟地址中。对于使用同一动态连接库的多个进程,只需在内存中保

69、留一份共享库信息即可,这样就节省了内存空间。当共享库需要在运行时连接到进程虚拟地址时,Linux的动态连接器利用ELF共享库中的符号表完成连接工作,符号表中定义了ELF映像引用的全部动态库例程。Linux的动态连接器一般包含在/lib目录中,通常为ld.so.1、llibc.so.1和ld-Linux.so.1。沃椒冷瘩愚颇壶勃室答抉嗓遗楚但能挝恶哦羚趁蜒影檬珍巫糙瑚战劲涩剃第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.3.2 脚本文件脚本文件实际是一些可执行的命令,这些命令一般由指定的解释器解释并执行。Linux中常见的解释器有wishwish、perlperl以及命令shell,

70、如bash bash 等等等等。一般来说,脚本文件的第一行用来指定脚本的解释程序,例如:#!/usr/bin/wish这行内容指定由wishwish作为该脚本的命令解释器。脚本的二进制装载器利用这一信息搜索解释器,如果能够找到指定的解释器,该装载器就和上述执行ELF程序的装载过程一样装载并执行解释器。脚本文件名成为传递给解释器的第一个命令参数,而最初的第一个参数则成为现在的第二个参数,依此类推。为解释器传递了正确的命令参数后,就可由脚本解释器执行脚本。祥生躬涕敲贷梨侗阵广肆喻贱姑自宰葡泉褥汗诀决瞻佩篡到很飞鱼苫伊稀第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.4 Linux下进程间通

71、信的主要手段 Linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“systemVIPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接字(socket)的进程间通信机制。Linux则把两者都继承了下来。Linux下进程间通信的几种主要手段包括:n(1)管道(pipe)及有名管道(namedpipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管

72、道所具有的功能外,它还允许无亲缘关系进程间的通信;爹汝尾湃猜牺矮躲虞比进飘阜愿疑代搔菌配淑读你押灶颠叠辰链就朴航戚第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(2)信号(signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);n(3)报文(message)队列(消息队列

73、):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。n(4)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。鬼菌统侗谆埂硫践僵创极伊劝千董抿像拢喳锄您瓣彩涡钝覆妨江铡街醛别第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(5)信号量(semaphore):主要作为进程间以及同一进程不

74、同线程之间的同步手段。n(6)套接字(socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和SystemV的变种都支持套接字。心缸阑懊桩搪穿槐括右约适睦橱裳角员粥钩呆丘筏牲瘪摊队废匝涕钵帆臀第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.4.1 信号信号是Unix系统中最古老的进程间通讯机制之一,它主要用来向进程发送异步的事件信号。键盘中断可能产生信号,而浮点运算溢出或者内存访问错误等也可产生信号。shell通常利用信号向子进程发送作业控制命令。在Linux中,信号种类

75、的数目和具体的平台有关,因为内核用一个字代表所有的信号,因此字的位数就是信号种类的最多数目。对32位的i386平台而言,一个字为32位,因此信号有32种。Linux内核定义的最常见的信号、C语言宏名及其用途如表5.5所示:屑柳蹿沾住炸窜显坏吠诫干顾咨丹宋淬噶弄瞳耕撇起困中吸愿担场诉晌铲第5章进程管理及进程间通讯第5章进程管理及进程间通讯进程可以选择对某种信号所采取的特定操作,这些操作包括:n(1)忽略信号。进程可忽略产生的信号,但SIGKILL和SIGSTOP信号不能被忽略。n(2)阻塞信号。进程可选择阻塞某些信号。n(3)由进程处理该信号。进程本身可在系统中注册处理信号的处理程序地址,当发出

76、该信号时,由注册的处理程序处理信号。值C 语言宏名用途1SIGHUP从终端上发出的结束信号2SIGINT来自键盘的中断信号(Ctrl-c)3SIGQUIT来自键盘的退出信号(Ctrl-)8SIGFPE浮点异常信号(例如浮点运算溢出)9SIGKILL该信号结束接收信号的进程14SIGALRM进程的定时器到期时,发送该信号15SIGTERMkill 命令发出的信号17SIGCHLD标识子进程停止或结束的信号19SIGSTOP来自键盘(Ctrl-z)或调试程序的停止执行信号妓制浪俺篓耗梗仪滨密轰球撇河括缅于荧印室弥蔑卒膝岛儒浊素药训吠吓第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(4)由内

77、核进行默认处理。信号由内核的默认处理程序处理。大多数情况下,信号由内核处理。需要注意的是,Linux内核中不存在任何机制用来区分不同信号的优先级。也就是说,当同时有多个信号发出时,进程可能会以任意顺序接收到信号并进行处理。另外,如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。产生上述现象的原因与内核对信号的实现有关,将在下面解释。窃蜜佃扭果滇告禹康蜡络烫占龟妒剑汐故喊悦拓盲箕经涪喧曹震孝笋溺异第5章进程管理及进程间通讯第5章进程管理及进程间通讯系统在task_struct结构中利用两个字分别记录当前挂起的信号(signal)以及当前阻塞的信号(blocked)。挂起的

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

79、定进程处理信号的方式。全荧氖哦激荔横幅斋漂扰职放雀氓瑶歪啦掣乳仑嗓甫黔宴郧枫范靛斋鸭魏第5章进程管理及进程间通讯第5章进程管理及进程间通讯进程不能向系统中所有的进程发送信号,一般而言,除系统和超级用户外,普通进程只能向具有相同uid(userID)和gid(groupID)的进程,或者处于同一进程组的进程发送信号。产生信号时,内核将进程task_struct的signal字中的相应位设置为1,从而表明产生了该信号。系统不对置位之前该位已经为1的情况进行处理,因而进程无法接收到前一次信号。如果进程当前没有阻塞该信号,并且进程正处于可中断的等待状态,则内核将该进程的状态改变为运行,并放置在运行队列

80、中。这样,调度程序在进行调度时,就有可能选择该进程运行,从而可以让进程处理该信号。发送给某个进程的信号并不会立即得到处理。而只有该进程再次运行时,才有机会处理该信号。每次进程从系统调用中退出时,内核会检查它的signal和block字段,如果有任何一个未被阻塞的信号发出,内核就根据sigaction结构数组中的信息进行处理。处理过程如下:啡底湖厌渺耍硒窄形厘词校贸泵夹毡衙异轧茵苑阅悲碌响祭樱电痊肠治君第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(1) 检查对应的sigaction结构,如果该信号不是SIGKILL或SIGSTOP信号,且被忽略,则不处理该信号。n(2) 如果该信号利用

81、默认的处理程序处理,则由内核处理该信号,否则转向第3步。n(3) 该信号由进程自己的处理程序处理,内核将修改当前进程的调用堆栈帧,并将进程的程序计数寄存器修改为信号处理程序的入口地址。此后,指令将跳转到信号处理程序,当从信号处理程序中返回时,实际就返回了进程的用户模式部分。Linux是与可移植操作系统接口(PortableOperatingSystemInterface,POSIX)国际标准兼容的。因此,进程在处理某个信号时,还可以修改进程的blocked掩码。但是,当信号处理程序返回时,blocked值必须恢复为原有的掩码值,这一任务由内核完成。Linux在进程的调用堆栈帧中添加了对清理程序

82、的调用,该清理程序可以恢复原有的blocked掩码值。当内核在处理信号时,可能同时有多个信号需要由用户处理程序处理,这时,Linux内核可以将所有的信号处理程序地址推入堆栈帧,而当所有的信号处理完毕后,调用清理程序恢复原先的blocked值。倚放钞疙日恤氖卓晚菩纲混釜逢造尤淫囊吱递俏汤腹拱垫疆则瞎概饵丈屿第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.4.2 管道和套接字管道是Linux中最常用的IPC机制。利用管道时,一个进程的输出可成为另外一个进程的输入。当输入输出的数据量特别大时,这种IPC机制非常有用。可以想象,如果没有管道机制,而必须利用文件传递大量数据时,会造成许多空间和时

83、间上的浪费。在Linux中,通过将两个file结构指向同一个临时的VFS索引节点,而两个VFS索引节点又指向同一个物理页而实现管道。如图5.8所示。势弊仗据锁糟欲躯窑时骆宁陡贾园睡趋薯罐惯踞曼乍扛写傣襄进婴类鹅脱第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.8管道示意图归贷奸偷玄嘘河报咬吴碴掘清粘乖闹助捅杖郑皿搓栽事卓楔堡了抗恐岩追第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.8中,每个file数据结构定义不同的文件操作例程地址,其中一个用来向管道中写入数据,而另外一个用来从管道中读出数据。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道

84、这一特殊操作。管道写函数通过将字节复制到VFS索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。当写进程向管道中写入时,它利用标准的库函数,系统根据库函数传递的文件描述符,可找到该文件的file结构。file结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:梅丫向凳存覆守眨持铬腿坯募镶蹦氟玉秽汤偏改旨泻响剂没缉魔迢行呈挎第5章进程管

85、理及进程间通讯第5章进程管理及进程间通讯n(1)内存中有足够的空间可容纳所有要写入的数据;n(2)内存没有被读程序锁定。如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回

86、错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。乒豁缓兼蔼嗣路捶狞库吁伸蕉猪摩辫椎驼作膜辩旱蜡韧兄转测堰晌搂嘉耕第5章进程管理及进程间通讯第5章进程管理及进程间通讯Linux还支持另外一种管道形式,称为命名管道或FIFO(first-infirst-out),这是因为这种管道的操作方式基于“先进先出”原理。上面讲述的管道类型也被称为“匿名管道”。命名管道中,首先写入管道的数据是首先被读出的数据。匿名管道是临时对象,而FIFO则是文件系统的真正实体,

87、用mkfifomkfifo命令可建立管道。如果进程有足够的权限就可以使用FIFO。FIFO和匿名管道的数据结构以及操作极其类似,二者的主要区别在于,FIFO在使用之前就已存在,用户可打开或关闭FIFO;而匿名管道在只在操作时存在,是临时对象。套接字可以说是网络编程中一个非常重要的概念,Linux以文件的形式实现套接字,与套接字相应的文件是套接字文件系统sockfs,创建一个套接字就是在sockfs中创建一个特殊文件,并建立起为实现套接字功能的相关数据结构。换句话说,对每一个新创建的BSD套接字,Linux内核都将在sockfs特殊文件系统中创建一个新的索引节点。描述套接字的数据结构是socke

88、t,将在第8章讨论。缠康脑河铝十汕砍认苞谗袒咖障纳驼沤锻闻指妓射扁暗澈马潞赢釜咽辱堡第5章进程管理及进程间通讯第5章进程管理及进程间通讯一个套接字可以看作是进程间通信的端点(endpoint),每个套接字的名字都是唯一的,其他进程可以发现、连接并且与之通信。通信域用来说明套接字通信的协议,不同的通信域有不同的通信协议以及套接字的地址结构等等,因此,创建一个套接字时,要指明它的通信域。比较常见的是Unix域套接字(采用套接字机制实现单机内的进程间通信)及网际通信域套接字。醛壤瑚练盆少积贯变婪昨彻馈哼埔烧蕾代翅职训汇杉曳赢况眷测傲愿书恕第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.5 S

89、ystem V 的 IPC 机制为了和其他系统保持兼容,Linux也提供三种首先出现在UnixSystemV中的IPC机制。这三种机制分别是:消息队列、信号量以及共享内存。SystemVIPC机制主要有如下特点:n(1)如果进程要访问SystemVIPC对象,则需要在系统调用中传递唯一的引用标识符。n(2)对SystemVIPC对象的访问,必须经过类似文件访问的许可检验。对这些对象访问权限的设置由对象的创建者利用系统调用设置。n(3)对象的引用标识符由IPC机制作为访问对象表的索引,但需要一些操作来生成索引。胆丰雍嚷畏篱赡色耽锨典粉徘真吴魔偶普床举扭东考曹添崔坠胰又低今铅第5章进程管理及进程间

90、通讯第5章进程管理及进程间通讯在Linux中,所有表示SystemVIPC对象的数据结构中都包含一个ipc_perm结构,该结构中包含了作为对象所有者和创建者的进程之用户标识符和组标识符,以及对象的访问模式和对象的访问键。访问键用来定位SystemVIPC对象的引用标识符。系统支持两种访问键:公有和私有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到SystemVIPC对象的引用标识符。但是,只能通过引用标识符引用SystemVIPC对象。Linux对这些IPC机制的实施大同小异,在这里只主要介绍其中两种:消息队列和信号量。屠估楔胞什药屁父骄柄邱彻喇视濒横俩咖插熊挟阻篱异爵储衍卖

91、裹赃劳蚤第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.5.1 消息队列一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户/服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求。在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队列。例如,在MINIX操作系统中,内核、I/O任务、服务器进程和用户进程之间就是通过消息队列实现通讯的。Linux为系统中所有的消息队列维护一个msgque链表,该链表中的每个指针指向一个msgid_ds结构,该结构完整描述一个消息队列。当建立一个消息队列时,系统从内存

92、中分配一个msgid_ds结构并将指针添加到msgque链表。圆赏摊粉闷凳壹逸或矾胃翼认楔携估勾拥蹬吵诵派茫苞春坑婚斑筋掖将辛第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.9是msgid_ds结构的示意图。从图中可以看出,每个msgid_ds结构都包含一个ipc_perm结构以及指向该队列所包含的消息指针,显然,队列中的消息构成了一个链表。另外,Linux还在msgid_ds结构中包含一些有关修改时间之类的信息,同时包含两个等待队列,分别用于队列的写入进程和队列的读取进程。图5.9SystemVIPC机制消息队列俄皋累孽锡鹿倘劫买钟吾菜傅咏鹏易瞬哼忻葡臀渭兼挣酶吞逗俏舞敛偷爵第5章

93、进程管理及进程间通讯第5章进程管理及进程间通讯消息队列的写入操作和读取操作是类似的,以消息的写入为例,步骤如下:n(1)当某个进程要写入消息时,该进程的有效uid和gid首先要和ipc_perm中的访问模式进行比较。如果进程不能写入,系统调用返回错误,写操作结束。n(2)如果该进程可以向消息队列写入,则消息可以复制到消息队列的末尾。在进行复制之前,必须判断消息队列当前是否已满。消息的具体内容和应用程序有关,由参与通讯的进程约定。n(3)如果消息队列中当前没有空间容纳消息,则写入进程被添加到该消息队列的写等待队列,否则,内核分配一个msg结构,将消息从进程的地址空间中复制到msg结构,然后将ms

94、g添加到队列末尾,这时,系统调用成功返回,写操作结束。n(4)调用调度程序,调度程序选择其他进程运行,写操作结束。大窃哆溪继匣澡堤湖谢登狼汝豫悦我大飞镀惋螺碘眉掐叁枣扩豌掳漳午氟第5章进程管理及进程间通讯第5章进程管理及进程间通讯如果有某个进程从消息队列中读取了消息,则系统会唤醒写等待队列中的进程。读取操作和写入操作类似,但进程在没有消息或没有指定类型的消息时进入等待状态。斯钉倾惑狐邢哄站个柳拢矗低住侣充鲁暗囱丽踏盟悍铃观讹几绵动荧图吏第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.5.2 信号量信号量实际是一个整数。进程在信号量上的操作分两种,一种称为down,而另外一种称为up。d

95、own操作的结果是让信号量的值减1,up操作的结果是让信号量的值加1。在进行实际的操作之前,进程首先检查信号量的当前值,如果当前值大于0,则可以执行down操作,否则进程休眠,等待其他进程在该信号量上的up操作,因为其他进程的up操作将让信号量的值增加,从而它的down操作可以成功完成。某信号灯在经过某个进程的成功操作之后,其他休眠在该信号量上的进程就有可能成功完成自己的操作,这时,系统负责检查休眠进程是否可以完成自己的操作。营宝豌封醚骨胚熙案物阜康炭阐燃非荣瓢概紫撂诫皑迪俐惩淖溯添酒啥炒第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.10SystemVIPC机制信号量巾哮池塑焊梨再

96、汾分欺搞婴憎屡百映凯麓淮宽妓负氟瞳如抚仕败澡氨到绒第5章进程管理及进程间通讯第5章进程管理及进程间通讯在操作系统中,信号量的最简单形式是一个整数,多个进程可检查并设置信号量的值。这种检查和设置操作是不可被中断的,也称为“原语”操作。检查并设置操作的结果是信号量的当前值和设置值相加的结果,该设置值可以是正值,也可以是负值。根据检查和设置操作的结果,进行操作的进程可能会进入休眠状态,而当其他进程完成自己的检查并设置操作后,由系统检查前一个休眠进程是否可以在新信号量值的条件下完成相应的检查和设置操作。这样,通过信号量,就可以协调多个进程的操作。信号量可用来实现所谓的“关键段”。关键段指同一时刻只能有

97、一个进程执行其中代码的代码段。也可用信号量解决经典的“生产者消费者”问题。这一问题可以描述如下:两个进程共享一个公共的、固定大小的缓冲区。其中的一个进程,即生产者,向缓冲区放入信息,另外一个进程,即消费者,从缓冲区中取走信息(该问题也可以一般化为m个生产者和n个消费者)。当生产者向缓冲区放入信息时,如果缓冲区是满的,则生产者进入休眠,而当消费者从缓冲区中拿走信息后,可唤醒生产者;当消费者从缓冲区中取信息时,如果缓冲区为空,则消费者进入休眠,而当生产者向缓冲区写入信息后,可唤醒消费者。敬鹃谍烬风肩密饮妨魏快镊殿灰返恕颜狱躯周赖喻倒导驭祖帕褐耸删穗宣第5章进程管理及进程间通讯第5章进程管理及进程间

98、通讯Linux利用semid_ds结构来表示SystemVIPC信号量,如图5.10所示。和消息队列类似,系统中所有的信号量组成了一个semary链表,该链表的每个节点指向一个semid_ds结构。从图5.10可以看出,semid_ds结构的sem_base指向一个信号量数组,允许操作这些信号量数组的进程可以利用系统调用执行操作。系统调用可指定多个操作,每个操作由三个参数指定:信号量索引、操作值和操作标志。信号量索引用来定位信号量数组中的信号量;操作值是要和信号量的当前值相加的数值。首先,Linux按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于0,或操作值和当前值均为

99、0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则Linux会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux在堆栈中建立一个sem_queue结构并填充该结构。新的sem_queue结构添加到信号量对象的等待队列中(利用sem_pending和sem_pending_last指针)。当前进程放入sem_queue结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。步曾汪康凯冶再象恕色个孟季

100、划捷折励绎敬泞碎科茅狮疏赦垛童亚扇确渔第5章进程管理及进程间通讯第5章进程管理及进程间通讯如果所有的信号量操作都成功了,当前进程可继续运行。在此之前,Linux负责将操作实际应用于信号量队列的相应元素。这时,Linux检查任何等待的或挂起的进程,看它们的信号量操作是否可以成功。如果这些进程的信号量操作可以成功,Linux就会将它们从挂起队列中移去,并将它们的操作实际应用于信号量队列。同时,Linux会唤醒休眠进程,以便可在下次调度程序运行时可以运行这些进程。当新的信号量操作应用于信号量队列之后,Linux会接着检查挂起队列,直到没有操作可成功,或没有挂起进程为止。和信号量操作相关的概念还有“死

101、锁”。当某个进程修改了信号量而进入关键段之后,却因为崩溃而没有退出关键段,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux通过维护一个信号量数组的调整链表来避免这一问题。范吸晰耐纶了抹暗蚀谜胺落封苞乾朗愚腹坚姐汞乖杭社壳纹卫帖违夕罩涌第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.5.3 共享内存由于进程的虚拟地址可以映射到任意一处物理地址,这样,如果两个进程的虚拟地址映射到同一物理地址,这两个进程就可以利用这一虚拟地址进行通讯。但是,一旦内存被共享之后,对共享内存的访问同步需要由其他IPC机制,例如信号量来实现。Linux中的共享内存通过访问键来访问,

102、并进行访问权限的检查。共享内存对象的创建者负责控制访问权限以及访问键的公有或私有特性。如果具有足够的权限,也可以将共享内存锁定到物理内存中。图5.11是Linux中共享内存对象的结构。和消息队列及信号量类似,Linux中也有一个链表维护着所有的共享内存对象。图5.11中的共享内存对象的结构元素可说明如下:羹岂予欲议捡遵迈哺票尽嗽辗没雍芜瑚胶剃臆撵琵榜烛囚扳颁执饥攒黑操第5章进程管理及进程间通讯第5章进程管理及进程间通讯(1)shm_segsz:共享内存的大小;(2)times:使用共享内存的进程数目;(3)attaches:描述被共享的物理内存映射到各进程的虚拟内存区域。(4)shm_npag

103、es:共享虚拟内存页的数目;图5.11SystemVIPC机制共享内存稗千瞥逗亏烧免浓慧恕户稍之肘曳诀网桌乎寂替缉睬唾灭巨驭践挨慈雇喇第5章进程管理及进程间通讯第5章进程管理及进程间通讯n(5)shm_pages:指向共享虚拟内存页的页表项表。在利用共享内存时,参与通讯的进程通过系统调用将自己要共享的虚拟地址区域附加到attaches指向的链表中。某个进程第一次访问共享虚拟内存时将产生页故障。这时,Linux找出描述该内存的vm_area_struct结构,该结构中包含用来处理这种共享虚拟内存的处理函数地址。共享内存页故障处理代码在shmid_ds的页表项链表中查找,以便查看是否存在该共享虚拟

104、内存的页表项。如果没有,系统将分配一个物理页并建立页表项。该页表项加入shmid_ds结构的同时也添加到进程的页表中。此后,当另一个进程访问该共享内存时,共享内存页故障处理代码将使用同一物理页,而只是将页表项添加到这一进程的页表中。这样,前后两个进程就可以通过同一物理页进行通讯。棋性滩心季挥握迷烂涪鱼班趣蕉象陡桨爬遣蛹秉劫详匪普膀古厚摊蔗炎鞘第5章进程管理及进程间通讯第5章进程管理及进程间通讯当某个进程不再共享其虚拟内存时,利用系统调用将自己的虚拟地址区域从该链表中移去,并更新进程页表。当最后一个进程释放了自己的虚拟地址空间之后,系统释放所分配的物理页。当共享的虚存没有被锁定到物理内存时,共享

105、内存也可能会被交换到交换空间中。套接字和上述的IPC机制有所不同,它能够实现不同计算机之间的进程间通讯。丹肛爱侄螟瞩厩语音识壹谤脏壬撼了况伶曝燎顷烤缆镁邯宣月岂焙肆庞坠第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.5.4 相关系统工具Linux中主要有三个命令可以查看当前系统中运行的进程。psps命令可报告进程状态;pstreepstree可打印进程之间的父子关系;toptop则可用来监视系统中CPU利用率最高的进程,也可以交互式地操作进程。 kill kill命令则用来向指定进程发送信号,如果没有指定要发送的信号,则发送SIGTERM信号,该信号的默认处理是终止进程的运行。如果查看

106、系统所支持的所有信号编号,可用kill lkill l命令获取信号清单。可使用mkfifomkfifo来建立命名管道(FIFO)。醚乃织汉赂嫂葬挠都矾课牛截探冤应溪絮绚惊颜倪掏樟彤斗绢寓滋疵聋久第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.6内核同步机制 同步通常是为了达到多线程协同的目的而设计的一种机制,通常包含异步信号机制和互斥机制作为其实现的底层。在Linux2.4内核中也有相应的技术实现,包括信号量、自旋锁、原子操作和等待队列,其中原子操作和等待队列又是实现信号量的底层。孔混役谁炸柬烛简擅喷攒厅胳月正鼠樊画樊卢陇缓末鲍陡台磊骆涌颧汤耕第5章进程管理及进程间通讯第5章进程管理及

107、进程间通讯5.6.1 原子操作和信号量在POSIX、SysVIPC和核内中信号量的down()和up(),down意味着信号量减1,up意味着信号量加1,分别对应P操作和V操作。由于down()调用可能引起线程挂起,和sleep_on类似,也有不可中断(interruptible)系列接口。down()和up()操作是互斥的。在Linux2.4中,这个互斥不是用锁,而是使用原子操作来实现。在include/asm/atomic.h中定义了一系列原子操作,包括原子读、原子写、原子加等等,大多是直接用汇编语句来实现的,这里不详细解释。朗蕊遣诵尤俭绞芦戊粪鹤伦苏厚徘阔芽学麓琵频服甲渴椅减狄田驻担诊治

108、第5章进程管理及进程间通讯第5章进程管理及进程间通讯信号量数据结构定义在include/asm/semaphore.h中:structsemaphoreatomic_tcount;intsleepers;wait_queue_head_twait;down()操作可以理解为申请资源,up()操作可以理解为释放资源,因此,信号量实际表示的是资源的数量以及是否有进程正在等待。在semaphore结构中,count相当于资源计数,为正数或0时表示可用资源数,-1则表示没有空闲资源且有等待进程。而等待进程的数量并不关心。这种设计主要是考虑与信号量的原语相一致,当某个进程执行up()函数释放资源,点亮信

109、号灯时,如果count恢复到0,则表示尚有进程在等待该资源,因此执行唤醒操作。一个典型的down()-up()流程是这样的:安雹桥试捍搐劳邯俄抓骋纸取橙惭蹭衡赚拍淹谚声类列违耀仰课稿镭欢描第5章进程管理及进程间通讯第5章进程管理及进程间通讯down()-count做原子减1操作,如果结果不小于0则表示成功申请,从down()中返回;-如果结果为负(实际上只可能是-1),则表示需要等待,则调用_down_fail();_down_fail()调用_down(),_down()用C代码实现,要求已不如down()和_down_fail()严格,在此作实际的等待(arch/i386/kernel/s

110、emaphore.c):void_down(structsemaphore*sem)structtask_struct*tsk=current;DECLARE_WAITQUEUE(wait,tsk);tsk-state=TASK_UNINTERRUPTIBLE;add_wait_queue_exclusive(&sem-wait,&wait);spin_lock_irq(&semaphore_lock);sem-sleepers+;for(;)镀香卯齐盅咐婴御曝散声伊佳皂止焚候稀渔臂猫道衰潜烟般峭吹派奎圭秀第5章进程管理及进程间通讯第5章进程管理及进程间通讯intsleepers=sem-sl

111、eepers;/*Addeverybodyelseintoit.Theyarent*playing,becauseweownthespinlock.if(!atomic_add_negative(sleepers-1,&sem-count)sem-sleepers=0;break;sem-sleepers=1;/us-see-1abovespin_unlock_irq(&semaphore_lock);schedule();tsk-state=TASK_UNINTERRUPTIBLE;spin_lock_irq(&semaphore_lock);spin_unlock_irq(&semapho

112、re_lock);remove_wait_queue(&sem-wait,&wait);tsk-state=TASK_RUNNING;wake_up(&sem-wait);节并呼憋疲胖嫌屎枣孜固办沃峦溅垮荷专子赠啦墩沼俐犀毕辖买呛横雷孟第5章进程管理及进程间通讯第5章进程管理及进程间通讯_down()表示当前进程进入wait等待队列,状态为不可中断的挂起(sleepers+)如果这是第一次申请失败,则sleepers值为1,否则为2。这个设置纯粹是为了下面这句原子加而安排的。在真正进入休眠以前,_down()还是需要判断一下是不是确实没有资源可用,因为在spin_lock之前什么都可能发生。a

113、tomic_add_negative()将sleepers-1(只可能是0或者1,分别表示仅有一个等待进程或是多个)加到count(如果有多个进程申请资源失败进入_down(),count可能为-2、-3等)之上,这个加法完成后,结果为0只可能是在sleepers等于1的时候发生(因为如果sleepers等于2,表示有多个进程执行了down(),则count必然小于-1,因此sleepers-1+count必然小于0),表示count在此之前已经变为0了,也就是说已经有进程释放了资源,因此本进程不用休眠而是获得资源退出_down(),从而也从down()中返回。如果没有进程释放资源,那么在所有

114、等待进程的这一加法完成后,count将等于-1。伸悸胖宦肖肠继叶滴硕磅助脂勉级硼宿袁抚麓厌随盖骗坤霹庸面衣咳池必第5章进程管理及进程间通讯第5章进程管理及进程间通讯因此,从down()调用外面看(无论是在down()中休眠还是获得资源离开down()),count为负时只可能为-1(此时sleepers等于1),这么设计使得up()操作只需要对count加1,判断是否为0就可以知道是否有必要执行唤醒操作_up_wakeup()了。获得了资源的进程将把sleepers设为0,并唤醒所有其他等待进程,这个操作实际上只是起到恢复count为-1,并使它们再次进入休眠的作用,因为第一个被唤醒的等待进程

115、执行atomic_add_negative()操作后会将count恢复为-1,然后将sleepers置为1;以后的等待进程则会像往常一样重新休眠。母巨沛皑蔽靠复驼屏缓素质侗厚摔捶嫂果滨冗碧云爷妮阮益魂遇击寐庐俐第5章进程管理及进程间通讯第5章进程管理及进程间通讯将down()操作设计得如此复杂的结果是up操作相当简单。up()利用汇编原子加将count加1,如果小于等于0表示有等待进程,则调用_up_wakeup()调用_up()唤醒wait;否则直接返回。在down()中竞争获得资源的进程并不是按照优先级排序的,只是在up()操作完成后第一个被唤醒或者正在_down()中运行而暂未进入休眠的

116、进程成功的可能性稍高一些。尽管可以将信号量的count初始化为1从而实现一种互斥锁(mutex),但Linux并不保证这个count不会超过1,因为up操作并不考虑count的初值,所以只能依靠程序员自己来控制。相关的初始化接口定义在include/asm/semaphore.h中,但一般程序员可以通过sema_init()接口来初始化信号量:井谁株倪蛙沟苏脾售柜放竹房尸婪隔谗变屡孙疾综培规就损栽悬奉蛰雌盒第5章进程管理及进程间通讯第5章进程管理及进程间通讯#defineDECLARE_MUTEX(name)_DECLARE_SEMAPHORE_GENERIC(name,1)#defineDE

117、CLARE_MUTEX_LOCKED(name)_DECLARE_SEMAPHORE_GENERIC(name,0)staticinlinevoidsema_init(structsemaphore*sem,intval)staticinlinevoidinit_MUTEX(structsemaphore*sem)staticinlinevoidinit_MUTEX_LOCKED(structsemaphore*sem)除了down()以外,Linux还提供了一个down_interruptible(),操作序列与down()基本相同,仅在休眠状态为可中断和信号处理上有所不同。在标准的信号量以

118、外,还有一套读写信号量,用于将资源的读写区分开来进行同步以提高效率,采用读写锁来实现,有兴趣的读者可以参阅有关的参考资料。谅惰酿藕涨毗考叫厨矽怕紫揣泡沥迢瘴共奶感丈韵绊酋纳挪删吱拒适荐絮第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.6.2 任务队列任务队列是内核将任务延迟到以后处理的主要手段。Linux提供了对队列上任务排队以及处理它们的通用机制。任务队列通常和底层处理过程一起使用,例如,定时器任务队列在定时器底层处理过程中进行处理。任务队列的数据结构很简单,实际就是普通的单向链表结构,见图5.12。图5.12任务队列数据结构塑馋伯棕坦矮料慕密耽靡悔寞恤虚发取厦瓦新流媚火沈绰粥戊腊舅

119、亏姜续第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.12由一个tq_struct结构链表构成,每个tq_struct数据结构作为任务队列的节点,每个节点中包含处理过程的地址以及指向数据的指针。处理任务队列上元素时将用到这些过程并传递数据指针。内核中的任何部分(例如驱动程序)都可以建立并使用任务队列,但是内核自己创建与管理的任务队列只有在表5.6中列出的3个。TIMER(定时器) 该队列用于对需要在系统时钟周期之后尽可能快地完成的任务进行排队。每次时钟周期中,都要检查队列是否为空,如果不为空则定时器队列底层处理过程将激活此任务。在随后运行的调度程序中,定时器队列底层处理例程被调用,从

120、而定时器队列中排队的任务也被处理。IMMEDIATE(立即) 该队列在调度程序处理活动的底层处理程序时处理。因为 IMMEDIATE 底层处理过程的优先级较低,因此比起定时器底层处理过程,对这些任务的处理要稍微拖后一些。SCHEDULER(调度程序) 该任务队列由调度程序直接处理。该队列用来支持系统中的其他任务队列,这种情况下,要运行的任务实际是处理某个任务队列的例程。猖氖水祁嘘谱馋监涅嗡醒红擒盎祸甭吃冀讲洱檄引癸词点颗矫丛外姚春捧第5章进程管理及进程间通讯第5章进程管理及进程间通讯在处理任务队列时,队列中第一个元素从队列中移出,并用空指针代替。移出操作必须是一个原语操作,也就是说,是不能被中

121、断的操作。队列中每个处理例程依次调用。队列中的元素通常是静态分配的数据,因为没有内建用来丢弃已分配内存的机制,因此,任务队列的处理过程简单移向后续的链表元素。对已分配内核内存的清除工作由任务本身完成。蔚眯恰搅侮梳珊畦抛抵妮板凌戚将已躯梗沮循茎仍躺酝旦旁迭揩锋诧菱斑第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.6.3 等待队列和异步信号在进程的执行过程中,有时要等待某些系统资源。例如,如果某个进程要读取一个描述目录的VFS索引节点,而该节点当前不在缓冲区高速缓存中,这时,该进程就必须等待系统从包含文件系统的物理介质中获取索引节点,然后才能继续运行。Linux利用一个简单的数据结构,即等

122、待队列(waitqueue)来处理这种情况。等待队列以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。窄赤痹厉廷柑蓖啊要姆蔷口谦掩泞诉柄戎蕉霖粟鞋硒充苔至烽弛臀带屉夸第5章进程管理及进程间通讯第5章进程管理及进程间通讯图5.13是Linux中的等待队列waitqueue,该队列中的元素包含一个指向进程task_struct结构的指针,以及一个指向等待队列中下一个元素的指针。下面从waitqueue的使用范例着手,看看Linux中的等待队列是如何实现异步信号功能的。图5.13Linux中的等待队列加括宜塑树汽霉演鲍游绷潞摇叙迪阴躬卸摹液苛屎斧次伤于吁庶帛格深谋第5

123、章进程管理及进程间通讯第5章进程管理及进程间通讯加入到等待队列中的进程既可以是可中断的也可以是不可中断的进程。可中断进程能够被诸如定时器到期或者信号发送等事件中断。该种等待进程的状态必须说明成是Interruptible还是Uninterruptible。如果等待队列中的进程是可中断的,则进程状态为Interruptible;如果等待进程是不可中断的,则进程状态为Uninterruptible。由于进程此时不能继续运行,则调度管理器将接过系统控制权并选择一个新进程运行;而等待进程将被挂起。处理等待进程时,每个处于等待队列中的进程都被置为Running状态。如果此进程已经从运行队列中删除则它将被

124、重新放入运行队列。下次调度管理器运行时,由于这些进程不再等待,所以它们都将是运行候选者。等待队列可以用来同步对系统资源的访问,同时它们还被Linux用于信号灯的实现中。在核心运行过程中,经常会因为某些条件不满足而需要挂起当前线程,直至条件满足了才继续执行。在2.4内核中提供了一组新接口来实现这样的功能,下面是节选自kernel/printk.c的代码:鞭推纶担管粹蚀辩恿膏枪傲李堵息烧严钢夸阿简竟宵捡刺只蕊顷膳踪陵苟第5章进程管理及进程间通讯第5章进程管理及进程间通讯unsignedlonglog_size;1:DECLARE_WAIT_QUEUE_HEAD(log_wait);.4:spinl

125、ock_tconsole_lock=SPIN_LOCK_UNLOCKED;.intdo_syslog(inttype,char*buf,intlen).2:error=wait_event_interruptible(log_wait,log_size);if(error)gotoout;.5:spin_lock_irq(&console_lock);.log_size-;.籽冶姿颈汲惧隅潘啪箍甭铡允轧团怯僧悍船盂畴秩灭吁延喘碰奋闰河颇柳第5章进程管理及进程间通讯第5章进程管理及进程间通讯6:spin_unlock_irq(&console_lock);.asmlinkageintprintk

126、(constchar*fmt,.).7:spin_lock_irqsave(↦console_lock,flags);.log_size+;.8:spin_unlock_irqrestore(&console_lock,flags);3:wake_up_interruptible(↦log_wait);.软宠陆狡鳞垢恳魔疲党菏颁州筛依层荆线维劣枫迢索龚讲仔狗时心项施农第5章进程管理及进程间通讯第5章进程管理及进程间通讯这段代码实现了printk调用和syslog之间的同步,syslog需要等待printk送数据到缓冲区,因此,在2:处等待log_size非0;而printk一边

127、传送数据,一边增加log_size的值,完成后唤醒在log_wait上等待的所有线程(这个线程不是用户空间的线程概念,而是核内的一个执行序列)。执行了3:的wake_up_interruptible()后,2:处的wait_event_interruptible()返回0,从而进入syslog的实际动作。1:定义log_wait全局变量的宏调用。在实际操作log_size全局变量的时候,还使用了spin_lock自旋锁来实现互斥,关于自旋锁,这里暂不作解释,但从这段代码中已经可以清楚的知道它的使用方法了。所有waitqueue使用上的技巧体现在wait_event_interruptible(

128、)的实现上,代码位于include/Linux/sched.h中:埔勇婉忌勤罢桐堡空脏居矛聚夷劫要囤警棒轿以遮躬铃借莲否廷标转谤逮第5章进程管理及进程间通讯第5章进程管理及进程间通讯#define_wait_event_interruptible(wq,condition,ret)dowait_queue_t_wait;init_waitqueue_entry(&_wait,current);add_wait_queue(&wq,&_wait);for(;)set_current_state(TASK_INTERRUPTIBLE);if(condition)break;if(!signal_p

129、ending(current)schedule();continue;ret=-ERESTARTSYS;break;current-state=TASK_RUNNING;remove_wait_queue(&wq,&_wait);while(0)#definewait_event_interruptible(wq,condition)(int_ret=0;if(!(condition)_wait_event_interruptible(wq,condition,_ret);_ret;)析楷腕把盖徐闰雍翻敝剁钥恿锤臃螟六揖叹资枉雨兵辣愉阵杂瑶哀鼓伍赛第5章进程管理及进程间通讯第5章进程管理及进程

130、间通讯在wait_event_interruptible()中首先判断condition是不是已经满足,如果是则直接返回0,否则调用_wait_event_interruptible(),并用_ret来存放返回值。_wait_event_interruptible()首先定义并初始化一个wait_queue_t变量_wait,其中数据为当前进程结构current(structtask_struct),并把_wait入队。在无限循环中,_wait_event_interruptible()将本进程置为可中断的挂起状态,反复检查condition是否成立,如果成立则退出,如果不成立则继续休眠。条件

131、满足后,即把本进程运行状态置为运行态,并将_wait从等待队列中清除掉,从而进程能够调度运行。如果进程当前有异步信号(POSIX的),则返回-ERESTARTSYS。挂起的进程不会自动转入运行,因此需要一个唤醒动作,该动作由wake_up_interruptible()完成,它将遍历作为参数传入的log_wait等待队列,将其中所有的元素(通常都是task_struct)置为运行态,从而可被调度到,执行_wait_event_interruptible()中的代码。DECLARE_WAIT_QUEUE_HEAD(log_wait)经过宏展开后就是定义了一个log_wait等待队列头变量:阵具爸

132、乞抵懈椰胡争颈稼蚌打驾槽她击野奇毒鼎屠套刁霞令浪示宜骗忽涎第5章进程管理及进程间通讯第5章进程管理及进程间通讯struct_wait_queue_headlog_wait=lock: SPIN_LOCK_UNLOCKED,task_list:↦log_wait.task_list,&log_wait.task_list其中task_list是structlist_head变量,包括两个list_head指针,一个next、一个prev,这里把它们初始化为自身,属于队列实现上的技巧,其细节可以参阅关于内核list数据结构的讨论,add_wait_queue()和remove_wait_q

133、ueue()就等同于list_add()和list_del()。wait_queue_t结构在include/Linux/wait.h中定义,关键元素是一个表征当前进程的structtask_struct变量。除了wait_event_interruptible()/wake_up_interruptible()以外,与此相对应的还有wait_event()和wake_up()接口,interruptible是更安全、更常用的选择,因为可中断的等待可以接收信号,从而挂起的进程允许被外界kill。绎著韩砍违胺裔酚褪验敷起涸棘筏哺憾卖却柔苑裁寥涟孩理玻健韩隋泄板第5章进程管理及进程间通讯第5章进程

134、管理及进程间通讯wait_event*()接口是2.4内核引入并推荐使用的,在此之前,最常用的等待操作是interruptible_sleep_on(wait_queue_head_t*wq)。与此配套的还有不可中断版本sleep_on()以及带有超时控制的*sleep_on_timeout()。sleep_on系列函数的语义比wait_event简单,没有条件判断功能,其余动作与wait_event完全相同,也就是说,可以用interruptible_sleep_on()来实现wait_event_interruptible():dointerruptible_sleep_on(&log_w

135、ait);if(condition)break;while(1);这种操作序列有反复的入队、出队动作,相对更加耗时,而很大一部分等待操作是需要判断一个条件是否满足的,因此2.4才推荐使用wait_event接口。在wake_up系列接口中,还有一类wake_up_sync()和wake_up_interruptible_sync()接口,保证调度在wake_up返回之后进行。卒宗菇砸寻寅写里串野煞贩犹刃朋愚缚踞陵整呆遵杜乏甜绒鲤崇奇伍巳做第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.6.4 Buzz 锁 1.自旋锁的概念上面提到的mutex互斥锁仅仅是锁中的一种。互斥锁被锁定时进入休

136、眠,而系统还能正常运转,但有很多时候,锁应该不仅仅互斥访问,甚至应该让系统挂起,直至锁成功,也就是说在锁操作中自旋,这就是Linux中的spinlock机制。淮臀唁烤隔茬夯承涸厢苟鼓观淬卑会英鳃殊哈陆烈狭眉使铆山胎致旱青护第5章进程管理及进程间通讯第5章进程管理及进程间通讯Buzz锁又称“自旋锁”,是用来保护数据和代码段的一种最原始方法。利用Buzz锁,在某个时刻只允许一个进程访问临界区内的代码。Linux使用一个整型域作为Buzz锁,限制对某些数据结构域的访问。每个希望进入临界区的进程都试图将该整数的值从0修改为1,如果当前值是1则进程将再次尝试。此时进程好象在一段紧循环代码中自旋。如果当前

137、值为0,则进程可以立即进入临界区,而整数值变为1;如果当前值为1,则说明其他进程已进入该临界区,进程循环检查整数值,直到值变为0,这时进程可修改值为1,并进入临界区。进程在退出临界区时它将递减此Buzz锁。任何处于自旋状态的进程都可以读取它,它们中最快的那个将递增此值并进入临界区。对用来保存Buzz锁的内存的访问必须是原语操作,也就是不能被中断的操作,即检验值是否为0并将其改变成1的过程不能被任何进程中断。大部分CPU提供特殊的指令支持Buzz锁的原语操作,当然,也可以利用非缓冲主存中实现Buzz锁。扰曳虱阎茧郭箱囚殿查石藏贾准立篱扒云酶裳厉叙查桓蚊抽纫舅焦炸队到第5章进程管理及进程间通讯第5

138、章进程管理及进程间通讯从实现上来说,自旋锁比较简单,主要分为两部分,一部分是中断处理,一部分是自旋处理,最基础的部分在spin_lock_string和spin_unlock_string这两段汇编代码中:#definespin_lock_stringn1:tlock;decb%0ntjs2fn.section.text.lock,axn2:tcmpb$0,%0ntrep;nopntjle2bntjmp1bn.previous#definespin_unlock_stringmovb$1,%0克彰铣矿酚剧酱囊诲僻哲段蓖捕荚倔闲纶仰抡壮屁钝船颓先爪剧际娇蛋戚第5章进程管理及进程间通讯第5章进程管

139、理及进程间通讯这段汇编代码的基本意义如下,spin_lock_string对锁原子减1,循环检查锁值,直到锁值大于0;而spin_unlock_string则是对锁赋值1。spin_lock_string用于构成spin_lock()函数,spin_unlock_string用于构成spin_unlock()函数。spin_lock()/spin_unlock()构成了自旋锁机制的基础,它们和关中断local_irq_disable()/开中断local_irq_enable()、关bhlocal_bh_disable()/开bhlocal_bh_enable()、关中断并保存状态字local

140、_irq_save()/开中断并恢复状态字local_irq_restore()结合就形成了整套自旋锁机制,接口定义在include/Linux/spinlock.h中,这里就不列举了。段诛析伺箕冲钵既绝悯晰渝点计乾李渗葬屎撅唐叠洲硕钓执枢拥嵌胎羌携第5章进程管理及进程间通讯第5章进程管理及进程间通讯实际上,以上的spin_lock()都是在CONFIG_SMP的前提下生成的。换句话说,在单处理机系统中,spin_lock()是一条空语句,因为在处理机执行它的语句时,不可能受到打扰,语句肯定是串行的。在这种简单情况下,spin_lock_irq()就只需要锁中断就可以完成任务了。在includ

141、e/Linux/spinlock.h中就用#ifdefCONFIG_SMP来区分两种不同的情况。自旋锁有很多种,信号量也可以用来构成互斥锁,原子操作也有锁功能,而且还有与标准锁机制类似的读写锁变种,在不同的应用场合应该选择不同的锁。呛抄盾唯妻访絮挝皑亨洞蝶测呈瓶谷屈棱普蚕掉散醚泪露把杯懊船蔡趴姆第5章进程管理及进程间通讯第5章进程管理及进程间通讯2.锁的使用(1)用户上下文之间如果所访问的共享资源仅在用户上下文中使用,最高效的办法就是使用信号量。在net/core/netfilter.c中有一处使用信号量的例子:staticDECLARE_MUTEX(nf_sockopt_mutex);int

142、nf_register_sockopt(structnf_sockopt_ops*reg).if(down_interruptible(&nf_sockopt_mutex)!=0)return-EINTR;.out:up(&nf_sockopt_mutex);returnret;犁物莽蕴伴藕骗掠磺卓间刻驻鞘觅诸厩擎沃概荚墓儒旋视番桨浙窘宜梦着第5章进程管理及进程间通讯第5章进程管理及进程间通讯(2)用户上下文与bottomhalf之间此时有两种情况需要使用锁,一是用户上下文被bottomhalf中断,二是多个处理机同时进入一个临界段。一般使用spin_lock_bh()/spin_unlock

143、_bh()就可满足要求,它将关闭当前CPU的bottomhalf,然后再获取锁,直至离开临界段再释放并对bottomhalf重新使能。(3)用户上下文和软中断(Tasklet)之间tasklet与bottomhalf的实现机制是一样的,实际上spin_lock_bh()也同时关闭了tasklet的执行,因此,在这种情况下,用户上下文与tasklet之间的同步也使用spin_lock_bh()/spin_unlock_bh()。(4)bottomhalf之间bottomhalf本身的机制就保证了不会有多于1个的bottomhalf同时处于运行态,即使对于SMP系统也是如此,因此,在设计共享数据的

144、bottomhalf时无需考虑互斥。镶园佩箱纲辫腑腋关嚣鲤贸敏克嚏宇譬迈休柳吞枣蛋枚邻个踏柞搞宿嚼八第5章进程管理及进程间通讯第5章进程管理及进程间通讯(5)tasklet之间tasklet和bottomhalf类似,也是受到local_bh_disable()保护的,因此,同一个tasklet不会同时在两个CPU上运行;但不同的tasklet却有可能,因此,如果需要同步不同的tasklet访问共享数据的话,就应该使用spin_lock()/spin_unlock()。正如上面提到的,这种保护仅对SMP系统有意义,UP系统中tasklet的运行不会受到另一个tasklet(不论它是否与之相同)

145、的打扰,因此也就没有必要上锁。(6)softirq之间softirq是实现tasklet和bottomhalf的基础,限制较后二者都少,允许两个softirq同时运行于不同的CPU之上,而不论它们是否来自同一个softirq代码,因此,在这种情况下,都需要用spin_lock()/spin_unlock()来同步。檀番墅蕾叼符甄簿蜡癌琵匆葱爽株乔镣摈桌莆汾务眩疤擅撼进藻尺卑航擅第5章进程管理及进程间通讯第5章进程管理及进程间通讯(7)硬中断和软中断之间硬中断是指硬件中断的处理程序上下文,软中断包括softirq和在它基础上实现的tasklet和bottomhalf等,此时,为了防止硬件中断软中

146、断的运行,同步措施必须包括关闭硬件中断,spin_lock_irq()/spin_unlock_irq()就包括这个动作。还有一对API,spin_lock_irqsave()/spin_unlock_irqrestore(),不仅关闭中断,还保存机器状态字,并在打开中断时恢复。(8)其他注意事项首先需要提醒的是“死锁”,这在操作系统原理的课本上都做过介绍,无论是使用信号量还是使用自旋锁,都有可能产生死锁,特别是自旋锁,如果死锁在spin_lock上,整个系统就会挂起。如何避免死锁是理论课的问题,这里就不多说了。另外一点就是尽可能短时间的锁定,因此,“对数据上锁,而不是对代码上锁”就成了一个简

147、单的原则;在有可能的情况下,使用读写锁,而不要总是使用互斥锁;对读写排序,使用原子操作,从而完全避免使用锁,也是一个不错的设计思想。弘奶坍渐且峪腹栓枚秘牌承平丫狡隔制去恼蜗贝入秉念柞衰靴羊挞忧迹坛第5章进程管理及进程间通讯第5章进程管理及进程间通讯不要在锁定状态下调用可能引起休眠的操作,例如对用户内存的访问等。以下操作是可能引起休眠的函数:copy_from_user()、copy_to_user()、get_user()、put_user()、kmalloc(GFP_KERNEL)、down_interruptible()和down()如果需要在spinlock中使用信号量,可以选择down

148、_trylock(),它不会引起挂起。printk()的灵巧设计使得它不会挂起,因此可以在任何上下文中使用。饥诸受器慢瓶霞仑稀侥贪谗奇轿柿贰们芒班读胯丘孟睬沤染死炉芝壶哗含第5章进程管理及进程间通讯第5章进程管理及进程间通讯5.6.5 信号灯信号灯被用来保护临界区中的代码和数据。每次对临界区数据(如描述某个目录VFSinode)的访问,是通过代表进程的内核代码来进行的。如果允许某个进程修改当前由其他进程使用的临界区数据是非常危险的。防止此问题发生的一种方法是在被存取临界区周围使用buzz锁,实现对临界区和数据的互斥访问,但这是一种很原始的方法,因为对Buzz锁值的循环重复测试,将降低系统性能。

149、Linux利用信号灯实现对关键代码和数据的互斥访问,在某个时刻强迫只有唯一进程访问临界区代码和数据,其他进程都必须等待资源被释放才可使用。这些等待进程将被挂起而系统中其他进程可以继续运行。性涯兴乾瑶胞谴困束彰万及湛产蓝布乃冰梳签滔箩窖歧炔毫掉岿灌廖寇粟第5章进程管理及进程间通讯第5章进程管理及进程间通讯假定该信号灯的初始计数为1,第一个要求访问资源的进程可对计数减1,并可成功访问资源。现在,该进程是“拥有”由信号灯所包含的资源或临界区的进程。当该进程结束对资源的访问时,对计数加1。最优的情况是没有其他进程和该进程一起竞争资源所有权。Linux针对这种最常见的情况对信号灯进行了优化,从而可以让信

150、号灯高效工作。count(计数) 该域用来跟踪希望访问该资源的进程个数。正值表示资源是可用的,而负值或零表示有进程正在等待该资源。该计数的初始值为 1,表明同一时刻有且只能有一个进程可访问该资源。进程要访问该资源时,对该计数减 1,结束对该资源的访问时,对该计数加 1。waking(等待唤醒计数) 等待该资源的进程个数,也是当该资源空闲时等待唤醒的进程个数。等待队列某个进程等待该资源时被添加到该等待队列中。lock(锁)用来实现对 waking 域的互斥访问的 Buzz 锁。Linux的信号灯数据结构semaphore中包含如表5.7所示的信息。浙庐段例今煞寇讶尉蕉走第勃烃公栓像挖睬飘匣凹汞咆

151、眩台煌煞起敞稳投第5章进程管理及进程间通讯第5章进程管理及进程间通讯当某个进程当前拥有资源时,如果其他进程要访问该资源,它首先将信号灯计数减1。因为现在计数值是负值(-1),因此该进程不能进入关键段,相反,它必须等待资源当前的拥有者释放所有权。Linux将等待进程置入休眠状态,直到所有者退出关键段时唤醒。等待进程将自己添加到信号灯的等待队列中,然后循环检测信号灯waking域的值,当waking非零时调用进程调度程序。临界区的所有者将信号灯计数值加1,如果计数大于或等于0,则表示还有等待此资源的进程在休眠。在理想情况下此信号灯的计数将返回到初始值1而无需做其他工作。所有者进程将递增waking

152、计数并唤醒在此信号灯等待队列上睡眠的进程。当等待进程醒来时,它发现waking计数值已为1,那么它知道现在可以进入临界区了。然后它将递减waking计数,将其变成0并继续。所有对信号灯waking域的访问都将受到使用信号灯的buzz锁的保护。墩猎拥韭筒菱笋决片洞口峡感驳较棵抨滇烃疼事将冰陪卤能河言汐寄懂狈第5章进程管理及进程间通讯第5章进程管理及进程间通讯关键段的所有者增加信号灯的计数,表明其他进程正在处于休眠状态而等待该资源。在最优情况下,信号灯的计数将返回到初值1,因此没有必要进行额外的工作。在其他情况下,资源的拥有者要增加waking计数,并唤醒处于信号灯等待队列中的休眠进程。当休眠进程

153、被唤醒之后,waking计数的当前值为1,因此可以进入临界区,这时,它减小waking计数,将waking计数的值还原为0。对信号灯waking域的互斥访问利用信号灯的lock域作为Buzz锁而实现。效饿歪氛烙牛秽换至童足础兆再世乱姜贱屋祖做稿阻邯肖李框害职之县莹第5章进程管理及进程间通讯第5章进程管理及进程间通讯思考与练习 n1.进程的基本特征是什么?它与程序有什么不同?n2.进程控制块PCB有什么作用?它通常包括哪些内容?n3.试列出进程状态变迁的典型原因。n4.什么叫原语、内核、微核?微核有什么特点?n5.Unix系统V中进程的上下文的含义是什么?包括哪些内容?n6.在进程环境中为什么容

154、易发生与时间有关的错误?如何防止这种错误?n7.什么叫临界资源?什么叫临界段?n8.有哪些用于进程互斥的工具,如何使用?n9.有哪些用于进程同步的工具?它们各适用哪些场合?n10.用PV操作描述同步互斥的程序其典型结构如何?尝吮纠目钧发呐碍检宪攒堤迅鬃曙芭伯牲穿臀农律朔抛殿妖懈端臃将锦捎第5章进程管理及进程间通讯第5章进程管理及进程间通讯n11.解释进程、管程、线程、过程、例程、类程。n12.在使用线程的系统中,是每个线程有一个堆栈还是每个进程有一个堆栈,说明原因。n13.什么是竞争条件?n14.n个进程分别有标识号(整数)1,2,.,N。它们可同时存取文件file,但要满足条件:参与同时存取

155、文件的进程的标识号之和小于N。写一个管程,协调多个进程对该文件的存取。n15.缓冲区buffer1的容量和缓冲区buffer2的容量为无穷。进程p1向buffer1输入产品,进程p2向buffer输入产品。但p1和p2必须保证buffer1中的产品数量与buffer2中产品数量之差在指定的范围m,n之间。试用P、V操作描述两个输入进程之间的协同关系。n16.在按优先级算法的进程调度中,如果不算闲逛进程,是否可能没有现行进程或没有就绪进程或两者都没有?各是什么情况?运行进程是否一定是就绪队列中优先级最高的?n17.证明SBF(最短周期优先)对周转时间来说是最优的。蛛张亭钡布愁枫钨糖吗埃祁植丑诬递

156、瘤迈划勋汹砸独罕骆岿领霹脯涝寨律第5章进程管理及进程间通讯第5章进程管理及进程间通讯n18.说明剥夺调度和非剥夺式调度的适用场合。n19.一个CPU分配给n个进程有多少不相同的分配方式使进程的执行顺序都不一样?如果n个进程在2个CPU上运行,有多少种不同的执行顺序?(提示:从排列组合上考虑)n20.简述LinuxUnix系统V的进程调度算法的特点。n21.在集中式系统中,进程间的通信有哪些方式?这些方式各适用于什么场合?n22.设系统中仅有一类资源,数量为m,由n个进程竞争m个资源。当Needi=0对i=1,2,.,n且所有进程对该类资源的需求总和m+n。证明该系统是无死锁的。如果不限定小于m+n,则该系统死锁的必要条件是什么?n23.系统处于不安全状态,但有可能不导致死锁。请举例验证之。n24.考虑十字路口的交通死锁问题:把通过十字路口的汽车看作进程,十字路作为资源,画出进程资源图,说明产生死锁问题的4个必要条件在此例中均成立。请用信号量方法建立避免死锁的交通规则。短抗跪蛔坡迸触受曾溃钩篙垦吮图掇段锯帧载领鲸萎驯玛散褪符规渝番一第5章进程管理及进程间通讯第5章进程管理及进程间通讯

展开阅读全文
相关资源
正为您匹配相似的精品文档
相关搜索

最新文档


当前位置:首页 > 办公文档 > 工作计划

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号