Linux进程简述

来源:互联网 发布:网络安全法展板 编辑:程序博客网 时间:2024/06/02 05:58

关于Linux进程相关的几个内容

  • Linux进程的创建 对应于函数:fork()
  • Linux进程的退出,结束 对应于函数:exit()
  • Linux进程的执行 对应于函数:execve()

对于进程的创建,系统调用fork()允许一个进程(父进程)创建一个新进程(子进程)。新的进程几乎就等于父进程的翻版,子进程获得了父进程的栈,数据段,堆和执行文本段的拷贝。

对于进程的终止,则是使用exit(status)函数,此函数会将进程所占用的所有资源(内存,文件描述符等)归还内核,交给内核进行再次分配。status这个参数则表明了进程退出的状态,父进程可以使用wait()来获取到该状态。

系统会调用wait(),其原因有二:

  • 如果子进程尚未调用exit()终止,那么wait()会挂起父进程直至子进程终止
  • 子进程的终止状态通过wait()的status参数来进程返回

最后,系统调用execve(pathname,argv,envp)来加载一个新的程序到当前进程的内存之中,注意,在子进程的内存中现在还存在着父进程留下的程序文本段,但是此函数执行后,会丢弃掉现存的程序文本段,并为新程序重新创建栈,数据段,以及堆,把这一系列的行为称之为:execing。值得注意的是exec开头的还有多个函数,但是无一例外,它们都是由execve函数为基础的。

对以上的内容稍微做个总结可以得出如下图的内容,下图以一个shell执行命令为例子:
这里写图片描述

进程创建

如上所述,创建新景程主要是使用了以下函数:

#include <unistd.h>pid_t fork(void)

理解这个函数需要知道如下的信息:

  • 在此函数调用之后就已经存在两个进程了
  • 这两个 进程拥有相同的程序文本段,也就是说,若果什么都不做,这两个进程会执行相同的指令
  • 然而,这两个进程却拥有不同的栈,数据段,以及堆拷贝,也就是说,两个进程各自修改栈数据与堆变量都不会受另一个进程的影响。

就代码层面上而言,是通过fork的返回值来区别两个进程的。对于父进程,直接返回的是子进程的进程ID,这很好理解,因为父进程通常都会fork出不止一个子进程,所以知道子进程的进程号是很有必要的,相对地,子进程的返回值则是0,因为它只需要自己就可以了,因为其返回值是0,所以若是有必要的话,子进程可以使用getpid()来得到自身的pid,也可以调用getppid来获取到父进程的pid。

进程创建之后的调用

关于fork函数调用完 之后的函数执行顺序(即谁先被cpu调度),需要知道的就是,这个是无法确定的,这个是很好理解的,因为你无法确认cpu的指令执行进度,在这种时候,很容易出现 ”竞争条件“的错误,这是由内核根据系统当时的负载而做出的调度决定,因而,很难进行调试 。若是在这个时候,需要对执行顺序进行控制,有以下的方法:

  • 可以在fork执行完成之后,让父进程执行sleep函数,以此来确保子进程可以获取到被先执行的机会,从而将执行顺序给固定下来,当然从理论上讲的话,这种方法不是百分之百有用的。

  • 利用同步来进行同步,其中包含信号量,文件锁,进程间pipe等。

关于fork之后,是子进程先执行,还是父进程先执行,其实各有各的理由,若是父进程先执行,那么父进程依旧会为子进程复制那些修改后的数据,然而子进程一旦获得调度,执行exec,之前的复制工作就纯粹是浪费。反过来,若是子进程先执行,在这时,父进程这是处于cpu活跃中的状态,让其挂起则是低效率的。

虽然这么说,但是在应用程序来说,无乱哪一个进程先执行,影响都不大。

进程的终止

进程的终止,主要是有以下两种方式:

  • 一种为异常,它是由接收某一信号而引起的,该信号的默认动作就是终止当前线程,同时可能产生core dump文件。
  • 当然,同样能够去正常终止线程,此时需要调用以下函数:
#include <unistd.h>void _exit(int status);

该函数中的status值定义了终止状态值,父进程可以调用wait()来获取这个值。按照惯例,当返回值为0时,就表示success,而非0,这是失败,具体的失败值需要具体定义。

  • 值得一提的是,对于进程的终止还有一种方式,那就是在main函数中进行返回,其实在 此处返回最终执行的依然是exit函数。

以上一直在说函数:_exit(),但是通常情况下,程序一般不会直接执行该函数,而是会执行库函数:exit()。

该函数主要是做了以下步骤的工作:

  • 调用退出程序(通过atexit(),on_exit()注册进去的),其执行顺序和其注册顺序相反,这很好理解,越早注册,那么需要进行的清理等操作就越基础一些。
  • 刷新stdio的缓冲区
  • 最后才是使用之前一直提到的函数_exit()

这里需要说明下,这个注册进去的退出程序是由程序员自己所定义的内容,执行程序员想要的回收相关工作。

还需要注意的是,这个退出程序只有在正常退出情况下才会执行,而在异常情况下,则不会执行,这个就很好理解了, 这个只调用库函数时才会调用。

监控子进程

有一种情况,那就是父进程需要知道子进程在何时改变了状态,在Linux主要靠以下技术来进行监控:

  • 系统调用:wait()
  • 信号 : SIGCHLD

系统调用的函数声明如下;

#include <sys/wait.h>pid_t wait(int *status);

这个函数的行为主要如下:

  • 若是之前并没有子进程被终止,那么调用将一直阻塞,直至某个子进程被终止了,如果调用时已经有子进程被终止了,wait()会立刻返回
  • 若是status不为空,那么子进程如何终止的信息通过status指向整型变量返回。
  • 内核会为父进程下所有的子进程的运行总量追加cpu时间以及资源使用数据

僵尸进程与孤儿进程

大家都知道,父进程与子进程的生命周期一般都是不同的,它们长短不一。这会有以下几个问题:
* 谁是孤儿进程的父进程?答案是所有进程之父,进程id为1的init进程会接管孤儿进程,换言之,某一进程的父进程终止之后,对其使用getppid会得到1.这也是一个判断某一进程直接父进程是否还存在的方法。
* 那么父进程在执行wait()之前,子进程就终止了,这是什么情况?这时候系统仍然允许父进程在之后某一时刻去执行wait,以此来确定子进程是如何结束的。在这种情况下,即父进程未执行wait,内核会将进程转化为僵尸进程,换句话说,系统会将子进程的大部分资源释放,仅仅在内核进程表中保存一条记录,里面包含了子进程id,终止状态,资源使用数等信息。僵尸进程之所以叫僵尸进程,原因在于它无法使用信号来杀死,这可以确保父进程总是可以执行wait。

但是有一个问题,若是父进程创建了子进程,然后并没有执行wait,那么内核进程表将会永远为该进程保留这么一条记录,若是大量的僵尸进程存在,势必会填满内核进程表,从来阻碍新进程的创建。这时候唯一的方法就是杀死它们的父进程,然后将子进程交接给init进程,init进程会自动调用wait来清理这些进程

SIGCHLD信号

当子进程终止时,其父进程会收到SIGCHLD的信号,可以利用该信号设置信号处理程序来进行处理。