Linux 进程

概述

进程可以理解为应用程序执行的一个实例,它包括可执行程序及其相关系统资源。
简单地来说,应用程序运行起来就是个进程。
在进程方面,Linux 和 Windows 是有区别的,Linux 的进程变现地更像 Windows 的线程。
每一个进程都有一个唯一的 ID 号。操作系统有一个初始进程 init,它的 ID 是1。
每个进程都可以通过 fork 产生子进程,每个进程都是 init 进程的子进程。

每个进程的 task_struct 结构体中有一个 state 字段(可以通过 ps aux 命令看到),用来表示当前进程的状态。
进程一共有以下五种状态:

  • R:running~运行
  • S:sleeping~休眠
  • D:uninterruptible sleep~不可中断
  • Z:zombie~僵尸进程
  • T:traced~停止

如何创建进程

依靠 fork() 函数可以随时产生新的进程,产生新的进程以后,当前进程被分为两个进程,父进程和子进程,两者拥有相同的代码段,不相同的数据段。通过 fork() 函数的返回值来区分是父进程还是子进程。返回小于0说明创建失败,等于0代表子进程在执行,大于0是子进程的 ID 号,可以通过分支来控制父子进程分别走哪个代码段。

看下面这个程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>

int main()
{
pid_t pid;
printf("before\n");
int num = 0;
pid = fork();

if(pid < 0) {
printf("create failed: %s\n",strerror(errno));
}

if(pid == 0) {
printf("child process is running\n");
}

if(pid > 0) {
printf("parent process is running \n");
}

num--;
printf("%d\n",num);

return 0;
}

看完它想必就会理解上面的话。这个程序的输出顺序不会是固定的(因为父子进程拥有相同的优先级),子进程和父进程哪一个先开始哪个先结束,都是不一定的。num 最后输出的值都是-1,也就是父子进程虽然共享代码段,但是数据是不共享的。

是不是比 Windows 简单多了?

关于 fork() 还有个有意思的点,比如在程序一开始有如下代码段,一共会有几个进程?
fork(); fork();

答案是四个。 父进程在第一次 fork 产生了一个子进程;第二次 fork,子进程和父进程又分别产生了一个进程,所以就会像满二叉树一样, fork() n 次,就会出现 2^n 个进程。


两种特殊进程

僵尸进程

什么是僵尸进程?通常子进程在结束(死掉)以后,需要父进程负责回收操作系统没有回收掉的小部分资源和 ID 等。但是父进程被困住了(比如在死循环里),子进程死掉之后,父亲没有机会为他收尸(比如调用 wait() 或者 waitpid() )。于是这个子进程就变成了僵尸进程。

进程虽死,但是还占着 ID 和小部分资源,是不是跟僵尸很像?
看一段代码:

int main()
{
pid_t pid;

if( (pid = fork() ) < 0) {
printf("create failed %s\n",strerror(errno));
}

if(pid == 0) {
printf("child process is running\n");
exit(0);
}

if(pid > 0) {
printf("parent process is running \n");
while(1) {
}
wait(NULL);
}

return 0;
}

可以看到父进程深陷死循环,根本没有机会为死掉的子进程收尸,子进程就变成僵尸咯(注意,这个时候子进程是有父亲的)。
少量的僵尸进程还 OK,但是系统的进程 ID 是有限的,大量的僵尸进程只会拖累系统,导致无法产生新的进程。
感兴趣的可以看一下fork 炸弹,十几个字符就可以一摧毁一台计算机。

服务器通常是7*24小时开启的,如果程序编写不当,是一定会产生僵尸进程,那么该如何避免僵尸进程呢?

有两种方法,第一种,通过信号的方法:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>

void handler()
{
wait(NULL);
}

int main()
{
pid_t pid;
signal(SIGCHLD,handler);

if( (pid = fork()) < 0) {
printf("create failed %s\n",strerror(errno));
}

if(pid == 0) {
printf("child process is running\n");
exit(0);
}

if(pid > 0) {
printf("parent process is running \n");
while(1) {
}
wait(NULL);
}

return 0;
}

signal() 用来监视信号,两个参数分别是信号和信号处理函数。子进程退出会发出 SIGCHLD 信号,处理函数调用 wait() 把子进程的尸体回收。

第二种方法涉及孤儿进程的概念,随后道来。

孤儿进程

孤儿进程,顾名思义就是那些父进程已经退出了,但它还没有退出。操作系统怎么会让孤儿进程
没了父亲呢?于是 init 进程会把该进程自动接收为子进程(收养),由 init 负责回收。
是不是很有爱?
孤儿进程的存在有什么意义呢?还记得防止僵尸进程的第二个方法么?
先看代码:

int main()
{
pid_t pid;
pid = fork();
if(pid < 0) {
printf("create failed %s\n",strerror(errno));
}
if(pid == 0) {
printf("child process is running\n");
pid = fork();
if(pid < 0) {
printf("create failed %s\n",strerror(errno));
}
if(pid > 0) {
exit(0);
}
exit(0);
}
if(pid > 0) {
printf("parent process is running \n");
while(1) {
}
wait(NULL);
}

return 0;
}

在父进程无脑循环的时候,子进程又创建了一个进程,并且自己先退出了,我们把子进程创建的进程称为孙子进程,他的父亲退出了,便成为了孤儿进程,这时 init 负责收养,并在孙子进程退出的时候为他收尸。
说实话,不知是我代码的问题,还是怎样,这种方法并不如信号那样奏效。而且还难于理解,所以我并不推荐。

对了,关于僵尸进程的测试方法我一般是这样做的:
执行程序的时候,放到后台:./a.out &
查看进程,看有没有僵尸进程: ps aux | grep Z
有的话说明错误了,这时候把后台程序拉回来:fg
结束程序:CTRL + C