Linux内核分析之进程总结

张明奇(卡哥) 

本文学习目标

深入了解进程的原理

无论是系统管理员还是普通用户,监视系统进程的运行情况,并适时终止一些失控的进程是每天的例行任务(读者或许对windows的任务管理器非常熟悉),系统管理员可能还要兼顾到任务的重要程度,并相应调整进程的优先级策略。

1.1  Linux进程简介

Linux系统的核心部分从整体上说可以分为两部分,即“静”的文件系统和“动”的进程控制系统。进程控制系统则负责为将要执行的程序和数据文件分配内存空间,并负责进程调度、控制并发进程的执行速度和分配必要的资源,以及负责通信和内存管理等。

1.1.1  进程的概念

Linux系统中,进程被赋予了下述特性的含义和特性:

1. 一个进程是对一个程序的执行。

2.一个进程的存在意味着存在一个task_struct结构,它包含着相应的进程控制信息。

3.一个进程可以生成或消灭其子进程。

4.一个进程是获得和释放各种系统资源的基本单位。

上述1反映进程动态特性的,而2又反映了进程的静态特性。34反映了Linux系统的进程之间的关系以及Linux没有作业概念的特性。事实上,一个进程的静态描述是由三部分组成的,即进程状态控制块PCB(栈段),进程的程序文本(正文)段以及进程的数据段。把这三部分统称为进程上下文,而进程的动态特性则定义为在进程上下文中的执行。

1.2  进程的虚拟地址结构

由于Linux进程的虚拟地址结构是依赖于硬件,因此,如果不作特别说明,文本默认那些与硬件有关的部门都是依赖于Intel 80x86的,80x86平台中,每个进程拥有一个4GB的虚拟空间。其中0~3GB的地址空间由用户进程使用,用户进程可以对其直接访问。3~4GB的地址空间称为核心地址空间,在所有的进程中共享,存放核心的正文和数据,只被核心使用,用户进程不能直接访问。

Linux将用户进程的所有地址空间有关的信息保存在一个名为mm_struct的数据结构中,该数据结构自身则保存在进程描述符中,这个在前面介绍Linux的进程描述符的时候提到过。

Linux的进程由逻辑段组成的,例如有存放状态控制块的栈段、存放CPU执行指令集合的正文段以及被执行指令所访问的数据段。相应的Linux中,一个进程的虚拟地址空间被分成若干个虚拟区来存放上述的逻辑段。区是进程虚拟地址空间上的一段连续区域,它是被共享、保护以及进行内存分配和地址交换的独立实体。正文、数据和栈分别存放于各自的区中。在Linux中虚拟区域被命名为vm area, 在核心代码中通常简写为VMA

1.2.1  管理每个进程中的区

系统设立了称为vm_area_struct的数据类型,进程的每个区都对应一个vm_area_struct结构,它主要包括下列内容:

1.区的标志位,指明该区的类型以及是否被锁住,是否可共享等属性。缺页处理程序会根据地址所在区的标志位查找缺页原因,并做相应处理。

2.区的起始地址,结束地址。

3.共享区域指针,给出共享区 vm_area_struct链表。

4.文件系统指针,指向外存中与该区对应的数据文件。

5.此区域的操作函数指针。

在系统创建新进程时,核心将从父进程复制相应的表项给所创建的进程。

这里,要强调的一点是对于一个进程,它所有的区的地址范围绝对不会重叠,两个区的虚拟地址不一定连续,而进程的虚拟地址在各区之间是连续的。

为加快对区域的查找和插入删除操作,Linux使用AVL平衡二叉树来组织和管理区域。

对于用户进程,它可以通过系统调用mmap()请求创建一个虚拟区域,并通过munmap()系统调用加以释放。

在虚拟区域的讨论中,大家也可能主要到一点,即Linux中的区和段页式管理中的段非常相像。所不同的是,段页式管理中的虚拟地址空间是二维的,而Linux的各个进程的分区地址仍然是一维的。

1.3进程的状态和状态转换

一个进程的生命周期是由一组状态来刻画的。这些状态是进程task_struct结构的一部分。

1.3.1  Linux中的五种状态。

1.TASK_RUNNING 进程处在执行或就绪状态,表示在占有CPU,或者在就绪队列中等待调度,只要调度到它,就可以投入执行。

2.TASK_INTERRUPTIBLE 进程正在睡眠,但是可以被软中断信号唤醒。

3.TASK_UNINTERRUPTIBLE 进程正在睡眠,且不可以被软中断信号唤醒。

4.TASK_STOPPED 表示进程的执行被暂停,当一个进程受到SIGSTIOP、SIGTSTP、SIGTTIN、SIGTTOU软中断信号后进入这个状态。

4TASK_ZOMBIE 进程执行了系统调用exit后,进入僵死状态。         

 1.4  进程控制

用户的创建、执行和自我终止的问题,与此相对应,Linux系统提供有相应的系统调用fork(),exec()exit(),以便在用户级上实现上述功能。 

fork()的功能是创建一个子进程。调用fork的进程称为父进程。

系统调用fork的语法格式是:

 pid=fork();

从系统调用fork返回时,父进程和子进程除了返回值pidtask_struct结构中某些特性参数不同之处,其他完全相同。CPU 在父进程中时,pid值为所创建子进程的进程号,若在子进程中时,pid的值为零。

1.4.1  理解Linux系统进程的并发性

下面介绍一下fork的功能与实现过程。

系统调用fork通过执行核心程序fork过程完成的功能是:

1.为子进程分配一个进程描述符task_struct结构,将父进程的进程描述符的内容复制到新创建的结构中,并重新设置那些与父进程不同的数据成员。

2.为子进程分配一个唯一的进程标识符号pid

3.将父进程的地址空间的逻辑副本复制到子进程。

4.复制父进程相联的有关文件系统的数据结构和用户文件描述符表,这样子进程就继承 了父进程的文件系统相关的信息。

5.复制软中断信号有关的数据结构。

6.设备子进程的状态为TASK_RUNNING,把它加入到就绪队列,并启动调度程序。

7.对父进程返回子进程的进程标识号,对子进程返回零。

1.5  进程管理

Linux是一个多用户多工的操作系统。多用户是指多个用户可以在同一时间使用电脑系统;多工是指Linux可以同时执行多个任务,它可以在还未执行完一个任务时又执行另一项任务。

Linux系统上所有运行的任务都可以称之为一个进程,每个用户任务、每个系统管理守护进程,也都可以称之为进程。Linux用分时管理方法使所有的任务共同分享系统资源。我们所关心的是如何去控制这些进程,让它们能够很好地为用户服务。

Linux系统中所有进程都是相互联系的。除了初始化进程外,所有进程都有一个父进程。新进程不是被创建,而是被复制,或者从以前的进程复制而来。Linux系统中所有的进程都是由一个进程号为1init进程衍生而来的。而我们在Shell下执行程序启动的进程则是Shell进程的子进程,当然我们启动的进程可以再启动自己的子进程。这样形成了一棵进程树,每个进程都是树中的一个节点,其中树的根是init

1.6  进程调度

Linux系统的进程调度有核心的调度过程schedule()实现。Linux系统中没有三级调度中的高级调度也没有中级调度。

Linux系统的进程调度对实时进程和普通进程采用不同的调度算法。对于普通进程采用的基本时间片的动态优先数调度法。

1.6.1  进程调度涉及的主要问题

1.调度的时机。

2.调度标志设置。

3.调度策略与优先数的计算。

4.调度的实现。

1.7  进程通信

Linux中的进程通信分为三个部分:低级通信、管理通信和进程通信IPCinter-process communication)。Linux同时支持计算机间通信(网络通信)用TCP/TP协议并提供了相应的系统调用接口。

现在最常用的进程间通信的方式有信号、信号量、消息队列、共享内存。所谓进程通信,就是不同进程之间进行一些“接触”。这种接触有简单,也有复杂。机制不同,复杂度也不一样。通信是一个广义上的意义,不仅仅指传递一些message。它们的使用方法是基本相同的,所以只要掌握了一种使用方法,然后记住其他的使用方法就可以了。信号和信号量是不同的,它们虽然都可以用来实现同步和互斥,但前者是使用信号处理器来进行的,后者是使用PV操作来实现的,消息队列是比较高级的一种进程间通信方法,因为它真的可以在进程间传送message,连传一个“I  seek  you”都可以。

一个消息队列可以被多个进程所共享(IPC就是在这个基础上进行的);如果一个进程的消息太多,一个消息队列放不下,也可以用多于一个的消息队列(不过可能管理会比较复杂)。共享消息队列的进程所发送的消息中除了message本身外还有一个标志,这个标志可以指明该消息将由哪个进程或者是哪个进程接受。每一个共享消息队列的进程针对这个队列也有自己的标志,可以用来申明自己的身份。

1.8  死锁

所谓死锁(deadlocks)是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待得现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

1.8.1  产生死锁的条件

1.互斥条件:一个资源每次只能被一个进程使用。

2.请求与保持条件:一个进程因请求资源而阻塞时,对已经获得的资源保持不放。

3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必须成了,而只要上述条件之一不满足,就不会发生死锁。

1.8.2  死锁的解决和预防方法

理解了死锁的原因,尤其是产生死锁的4个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这4个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要放在进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。

根据产生死锁的4个必要条件,只要是其中之一不能成立,死锁就不会出现。为此,可以采取下列3中预防措施。

 1.采用资源静态分配策略,破坏“部分分配”条件。

 2.允许进程剥夺使用其他进程占有的资源,从而破坏“不可剥夺”条件。

 3.采用资源有序分配法,破坏“环路”条件。

1.9  Linux下的孤儿进程和僵尸进程

1.9.1  孤儿进程

父进程已经退出,子进程还存在,那么子进程将会变成孤儿进程,孤儿进程将会被init进程收养和清理,孤儿进程不会对系统造成危害。

1.9.2  僵尸进程

僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。在forkexecve过程中,如果子进程退出时父进程依然存在,而父进程在退出时没有安装SIGCHLD信号处理函数并调用waitwaipid函数等待子进程结束,又没有显式忽略该信号,那么子进程就会成为僵尸进程,无法正常结束,root用户kill -9 pid也不能结束,补救之法是结束掉僵尸进程的父进程,让僵尸进程变成孤儿进程,孤儿进程会自动被init进程收养和清理。

fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用 waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵尸进程,无法正常结束,此时即使是root身份kill -9pid也不能杀死僵尸进程。补救办法是杀死僵尸进程的父进程(僵尸进程的父进程必然存在),僵尸进程成为"孤儿进程",过继给1号进程initinit始终会负责清理僵尸进程。

僵尸进程是指的父进程已经退出,而该进程dead之后没有进程接受,就成为僵尸进程.(zombie)进程 

1.9.3  怎样产生僵尸进程的 

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用waitwaitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。 

1.9.4  怎么查看僵尸进程

利用命令ps,可以看到有标记为Z的进程就是僵尸进程。 

1.9.5  怎样来清除僵尸进程 

1.改写父进程

在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。 

2.把父进程杀掉

父进程死后,僵尸进程成为"孤儿进程",过继给1号进程initinit始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。

1.10  Linux进程的层次关系

当打开Linux系统,LILOLinux LOader)找到Linux内核把它加载到内存。它初始化各种硬件,包括磁盘控制器。然后转到保护模式,加载操作系统,执行初始化各种内核数据结构的代码,例如inode和文件表。此进程的PID0。它启动初试进程(init进程,PID1)完成引导过程的其余工作。init进程启动守护进程kflushdkupdatekpiodkswapd,其PID分别为2345Init进程然后初始化文件系统,安装根文件系统。接下来试着执行/sbin/init程序,在每一个激活的终端上执行minegetty进程(经常被称为getty进程)。getty进程设置终端属性,如波特率,这些属性在/etc/termcap文件中都有定义。它显示login:提示符,等待用户登录。

login:提示符下,输入登录名并按回车键,getty进程产生一个子进程。它转变为以登录名为参数的登录进程。登录进程提示输入密码,并检查输入名和密码的有效性。如果两者均正确,登录进程产生一个子进程,它将转变为登录shell。如果登录进程没有在/etc/passwd文件中找到登录名或者输入的密码与/etc/passwd文件中(或者/etc/shadow文件)存放的密码不匹配,他将显示错误提示信息然后终止。控制权又回到getty进程,重新显示login:提示符。一旦进入登录shell,就可以完成自己的工作,还可以按键终止当前shell。如果这样做了,shell进程会终止,控制权又回到getty进程,再次显示login:提示符,又开始循环。

就是说,当登录到Linux系统,系统产生第一个进程,称为登录进程,它又创建登录shell。登录shell为所输入的命令创建进程,用以解释/执行命令。

两个Linux进程贯穿系统生命周期:swapperinit进程。监视终端行的getty进程,只要终端与系统关联上就会一直存在。登录进程和登录shell进程只有在登录时才存在。所有其它进程生存期较短,只在命令或者程序执行时短暂存在。

ps -ef 命令或者pstree命令可以用图的形式显示当前系统中执行进程的进程树,勾勒出进程间的父子关系。pstree命令显示的图比ps -ef命令更简洁。pstree显示的结果,前有"+"的是当前的后台进程,而前面的有"-"的是后续后台进程。pstree命令使用-h参数,输出用粗体(加亮)显示当前进程。使用"-a"选项,pstree显示带参数的命令。如"pstree 402 -a"可以显示PID402的进程的那个的层次关系。

Bash shell可以使用ulimit显示用户可以同时执行的最大进程个数。TC shell下为limit。两个命令都可以用来显示硬件和操作系统资源的使用限制。

1.11  Linux的一生

对于Linux进程的一生,有人做过一个比较形象的比喻:

随着一句fork,一个新的进程呱呱坠地,但他此时是父进程的一个克隆,随着exec,新进程脱胎换骨,离家独立,开始了为人民服务的职业生涯。

人总有生老病死,进程也一样,他可以自然死亡,即运行到main函数的最后一个“}”,从容地离我们而去。

当然,他也可以自杀,自杀有2种方式:一种是调用exit函数,一种是在main函数内使用return,无论采用哪一种方式,他都可以留下遗书,放在返回值里面保留下来。他甚至可以被谋杀,被其他进程通过另外一些手段结束他的生。进程死掉后,会留下一具僵尸; waitwaitpid充当了殓尸工,把僵尸推出去火化,使其最终归于无形。

子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常地终止或父进程调用wait才宣告结束。因此,进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来以备父进程今后的wait调用使用。

父进程异常终止,子进程自动将PID的进程(即init)作为自己的父进程,子进程现在就是一个不再运行的僵尸进程,由init接管。僵尸进程将一直保留在进程表中直到被init进程所发现并释放。进程表越大,这一过程就越慢。应该尽量避免僵尸进程,因为在init清理它们之前,它们将一直消耗着系统的资源。

参考文献

[1] 张尧学,史美林,张高: 计算机操作系统教程,清华大学出版社,2006年。

[2] 欧立奇,刘洋,段韬 : 程序员便是宝典,电子工业出版,2010年。

[3] 刘忆智:Linux从入门到精通,清华大学出版社,2010年。

[4] 王继刚,顾国昌,徐立峰,李翌:强实现Linux内核的研究和设计,系统工程与电子,2006年。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值