您的位置:首页 > 运维架构 > Linux

通过Linux理解操作系统(二):进程管理(上)

2013-05-14 20:15 337 查看
在前文完成了概述之后,本文就要开始进入戏肉了,之前我们将操作系统的内核结构分成了三个模块,现在就先从进程管理模块来开始深入探讨一下。 1、进程间的关系 我们知道一个Linux系统里同时运行着大量的进程,当你在shell终端里输入ps -ef命令时,你会看到像下面这样一长串的东西,有很多我就截了一部分。

这些进程之间是什么关系呢? 首先,我们从最基本的父子关系说起,我们知道在一个程序里只要调用fork函数就可以创建一个新的进程,在这种情况下,调用fork函数的就是父进程,新创建的进程就是子进程,而子进程又可以创建子进程,这样层层往下延伸,就构成了一棵进程树。然后看上图,每个进程都对应了一个PID和一个PPID,在我们的程序里可以通过getpid和getppid函数获取,PID标识了进程本身,而PPID标识了父进程,所有进程的PPID一层层往上推最终都会汇集到0。然后这里又有一个概念叫做进程组,需要注意的是不能把进程组和前面提到的进程树等同,进程组只包括了一个进程的父进程(还有父进程往上的祖先进程),兄弟进程,以及子进程(还有子进程往下的子孙进程),它只是整个进程树的部分而已(可以自己画个图就明白了)。为什么要区分这个概念呢,是因为在进程通信中一个进程只能发送信号给同一个进程组的进程,如果混淆了的话就会以为能够发信号给所有进程了。进程组以外又有一个概念叫会话,一个会话由多个进程组组成,一个会话对应一个控制终端,也就是tty1-tty7。 关于父子进程又有另外两个概念,一个叫作僵尸进程,在父进程程序中,通常会调用waitpid函数等待子进程终止,而如果在父进程调用waitpid之前,子进程就先终止了,那么子进程就会成为僵尸进程(因为还是要等到父进程调用waitpid它才能真正终止,要死不死的所以叫僵尸),而相应的如果父进程在子进程终止之前自己先终止了,那么子进程就变成了孤儿进程,这样该进程以下的进程组就和整个系统的进程树隔离开了,这时候有一种机制叫做“收养”,就是把这个孤儿进程的父进程设为init进程(就是PID为1的那个)。 2、进程间通信 进程间通信是一个很常见的问题,有很多种方式,网上资料也是大把,所以我也不讲怎么实现,只是提一下方式还有稍微解释一下。 首先,关于进程通信不能只是狭隘地理解为交换数据,虽然基本上是这样,但是其实发送信号通知另外一个进程要干什么事,这也是一种通信。一个进程可以在程序里使用函数kill(pid,sig)发送信号给另一个进程(kill不只是杀死,也能发信号的),然后在接收信号的进程程序里可以定义处理相应的信号函数,这实际上是通过软中断实现的,指一个进程在运行的过程中,收到信号后,停止当前运行的指令,并保存进程状态,然后跳转到信号处理过程,处理完成后又回到原先停止的位置继续执行。 进程通信的另一种方式是管道,它通过阻塞的方式实现了同步,管道分为匿名管道命名管道两种,匿名管道存在与内存中,只能用于父子进程间,熟悉shell的应该对这样的命令不陌生: sort <f | head,它实际上创建了一个进程sort从文件f中都入数据进行排序,而sort进程又创建了一个子进程head,并构建了一个匿名管道,把sort的结果传到head,然后head输出前n行数据,当管道满了的时候,sort会停止输出等到head将管道的数据取出后再继续输出。 命名管道则存在与文件系统中,可以用于任意进程之间,因为这只是相当于一个进程创建一个文件并写如数据,然后另一个进程只要知道那个文件的路径,然后打开它读取就行了。 进程间通信还有其他的方式如使用socket监听本机的端口,或者使用IPC对象如共享内存,信号量(信号量不同于信号),消息队列等,这些就等到之后谈到I/O,和内存管理的时候再详细介绍。 3、Linux用于进程管理的系统调用 在了解Linux系统内核是怎么实现进程管理之前,我们先来了解我们的程序如何通过系统调用进行进程管理。 (1)前面已经提到了一个函数fork用于创建一个子进程,它实际上是创建了一份父进程的拷贝,他们的内存空间里包含了完全相同的内容,包括当前打开的资源,数据,当然也包含了程序运行到的位置,也就是说fork后子进程也是从fork函数的位置开始往下执行的,而不是从头开始。而为了判别当前正在运行的是哪个进程,fork函数返回了一个pid,在父进程里标识了子进程的id,在子进程里其值为0,在我们的程序里就根据这个值来分开父进程的代码和子进程的代码。 (2)通常情况下,在fork之后,子进程需要执行一个和父进程不同的程序,而父进程则阻塞等待子进程终止,这也是shell程序的执行方式,shell本身是一个进程,当我们输入一个命令时,shell本身停止,该命令对应的程序作为子进程执行(有时直接返回结果,有时进入程序执行界面),当程序返回后,shell又继续运行等待下一个指令。这个过程中涉及到两个函数,一个是前面提到的waitpid,还有一个是execve函数,它们的函数原型分别为: waitpid(pid, &staloc, opts); execve(name,argv,envp); waitpid的第一个参数用于指定等待终止的子进程id,也可以设为-1,表示任意一个子进程, 第二个参数用于返回子进程的终止状态,比如我们经常调用的exit(0),exit(1),所传的参数就在这里返回,表示是否正常终止,第三个参数用于指定父进程是否阻塞等待子进程终止。 看到execve的参数列表,很容易想到我们平时写程序时的main函数, int main(int argc, char **argv); (这里其实也可以有第三个参数envp) 这里其实就是对应的,execve的第一个参数表示可执行程序的名字,第二个是一个参数数组指针,第三个是环境变量数组的指针,在调用execve时,它实际上就是使用这些参数又调用了我们程序的main函数。例如:在shell中输入 cp file1 file2时,shell进程fork出一个子进程后,子进程再调用execve,然后把参数传给cp程序的main函数,这时cp的main里得到的 argc 就是3, argv[0] 是cp, argv[1] 是file1,argv[2]是file2。另外在库函数中有一组函数叫做exec族函数,包括execl,execv, execle这些,它们内部最终调用的都是execve函数,只是参数和处理方式不同而已(库函数有很多是这样的关系,比如fread内部调用的又是read)。 (3)还有一个经常使用的系统调用就是信号处理,我们最经常用来终止一个程序的操作就是直接按Crtl+C了,其实这就是向一个进程发出了终止的信号,但这不是唯一的方式,还可以在程序里使用前面提到的kill函数来给进程发信号,那么一个进程收到信号又是怎么处理的呢,可以调用sigaction来声明该进程会接收什么信号,并如何进行处理,它有三个参数,第一个是要捕捉的信号ID,第二个是一个结构指针,指向的结构体里保存了自定义的信号处理函数的函数指针和相关信息,第三个也是一个结构指针。我们在自己写的函数里定义对接到信号的处理过程然后将它传给sigaction即可。 (4) 有些时候我们又希望进程能够在没事情干的时候或者把要cpu让给其他进程时自己暂停下来,这时候可以使用sleep函数和pause函数,注意两者的区别,sleep可以传进去一个时间参数表示暂停多长时间后回复,而pause则没有参数,它要等到接收到另外一个信号时才会恢复。 有了这些系统调用之后,虽然还是受限于系统,但是我们已经能通过我们写的程序来实现我们想要的进程管理方式了,更详细的系统调用介绍还请参阅相关手册。OK~今天就到这,下次我们再往下深入看看系统又是怎么样实现进程管理的吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: