Linux 之守护进程、僵死进程与孤儿进程

我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
由于孤儿进程会被init进程给收养,所以孤儿进程不会对系统造成危害

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

僵死进程的危害:

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

一个进程如果只复制fork子进程而不负责对子进程进行wait()或是waitpid()调用来释放其所占有资源的话,那么就会产生很多的僵死进程,如果要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会编程孤儿进程,从而被init所收养,这样init就会释放所有的僵死进程所占有的资源,从而结束僵死进程。

守护进程

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

创建守护进程步骤

首先我们要了解一些基本概念:

进程组 :
  1. 每个进程也属于一个进程组
  2. 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
  3. 一个进程只能为它自己或子进程设置进程组ID号

会话期:

会话期(session)是一个或多个进程组的集合。

setsid()函数可以建立一个对话期:

如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。

(1).此进程变成该对话期的首进程

(2)此进程变成一个新进程组的组长进程。

(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。

(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行

现在我们来给出创建守护进程所需步骤:

编写守护进程的一般步骤步骤:

(1)在父进程中执行fork并exit推出;

(2)在子进程中调用setsid函数创建新的会话;

(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;

(4)在子进程中调用umask函数,设置进程的umask为0;

(5)在子进程中关闭任何不需要的文件描述符

说明:

1: 在后台运行。

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

1
2
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续

2: 脱离控制终端,登录会话和进程组

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:

1
setsid();

说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3: 禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

1
2
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4: 关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

1
2
for (i=0; i < MAXFILE; i++)
close(i);

5: 改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir(“/“)

6: 重设文件创建掩模

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

7: 处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。

1
signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

守护进程实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 守护进程初始化函数
void init_daemon()
{

pid_t pid;
int i = 0;

if ((pid = fork()) == -1) {
printf("Fork error !\n");
exit(1);
}
if (pid != 0) {
exit(0); // 父进程退出
}

setsid(); // 子进程开启新会话,并成为会话首进程和组长进程
if ((pid = fork()) == -1) {
printf("Fork error !\n");
exit(-1);
}
if (pid != 0) {
exit(0); // 结束第一子进程,第二子进程不再是会话首进程
}
chdir("/tmp"); // 改变工作目录
umask(0); // 重设文件掩码
for (; i < getdtablesize(); ++i) {
close(i); // 关闭打开的文件描述符
}

return;
}

int main(int argc, char *argv[])
{

int fp;
time_t t;
char buf[] = {"This is a daemon: "};
char *datetime;
int len = 0;
//printf("The NOFILE is: %d\n", NOFILE);
//printf("The tablesize is: %d\n", getdtablesize());
//printf("The pid is: %d\n", getpid());

// 初始化 Daemon 进程
init_daemon();

// 每隔一分钟记录运行状态
while (1) {
if (-1 == (fp = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600))) {
printf("Open file error !\n");
exit(1);
}
len = strlen(buf);
write(fp, buf, len);
t = time(0);
datetime = asctime(localtime(&t));
len = strlen(datetime);
write(fp, datetime, len);
close(fp);
sleep(60);
}

return 0;
}

利用库函数daemon()创建守护进程

其实我们完全可以利用daemon()函数创建守护进程,其函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>

int daemon(int nochdir, int noclose);


DESCRIPTION
The daemon() function is for programs wishing to detach themselves from
the controlling terminal and run in the background as system daemons.

If nochdir is zero, daemon() changes the process’s current working
directory to the root directory ("/"); otherwise,

If noclose is zero, daemon() redirects standard input, standard output
and standard error to /dev/null; otherwise, no changes are made to
these file descriptors.

功能:创建一个守护进程

参数:

nochdir:=0将当前目录更改至“/”

noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”

返回值:

成功:0

失败:-1

现在我们利用daemon()改写刚才那个程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}\
while (0);\

void creat_daemon(void);
int main(void)
{

time_t t;
int fd;
if(daemon(0,0) == -1)
ERR_EXIT("daemon error");
while(1){
fd = open("daemon.log",O_WRONLY|O_CREAT|O_APPEND,0644);
if(fd == -1)
ERR_EXIT("open error");
t = time(0);
char *buf = asctime(localtime(&t));
write(fd,buf,strlen(buf));
close(fd);
sleep(60);

}
return 0;
}