Linux进程学习 - 孤儿进程和守护进程

时间:2023-12-30 13:07:56

孤儿进程和守护进程

通过前面的学习我们了解了如何通过fork()函数和vfork()函数来创建一个进程。现在 我们继续深入来学习两个特殊的进程:孤儿进程和守护进程

一.孤儿进程

1.什么是 孤儿进程
如果一个子进程的父进程先于子进程 结束, 子进程就成为一个孤儿进程,它由 init 进程收养,成为 init 进程的子进程。
2.那么如何让一个进程变为一个孤儿进程呢?
我们可以先创建一个进程,然后杀死其父进程,则其就变成了孤儿进程。
pid =  fork();
if(pid > 0) {
                 exit(0);
}
3. 函数实例:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 pid_t pid;
9 pid = fork();
10 if(!pid){
11 while(1){
12 printf("A background process,PID:%d/n,ParentID:%d/n" ,getpid(),getppid());
13 sleep(3);
14
15 }
16 }
17 else if(pid > 0){
18 printf("I am parent process,my pid is %d/n",getpid() );
19 exit(0);
20 }
21 else {
22 printf("Process creation failed!/n");
23 }
24 return 0;
25
26 }
程序运行结果
I am parent process,my pid is 2026
A background process,PID:2027
,ParentID:2026
think@ubuntu:~/work/process_thread/fork2$ A background process,PID:2027
,ParentID:1
A background process,PID:2027
,ParentID:1
A background process,PID:2027
,ParentID:1
A background process,PID:2027
,ParentID:1
A background process,PID:2027
,ParentID:1
A background process,PID:2027
,ParentID:1
Tiger-John说明:
通过以上方法,就可以实现把一个进程变为孤儿进程 。 当要结束一个孤儿进程时只能在终端输入命令: kill  2027(kill 孤儿进程号)来结束其运行。

二守护进程

1 . 什么是守护进程呢?

( daemon) 是指在后台运行,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务 。
Tiger-John说明: 那么,守护进程为什么要脱离后台去运行呢?
守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断
2. 为什么要引入守护进程:
由于在
Linux
中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程
3 .守护进程的特性
1>守护进程最重要的特性是后台运行 。
2>其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是 shell )中继承下来的。
3>最后,守护进程的启动方式有其特殊之处。它可以在 Linux 系统启动时从启动脚本 /etc/rc.d 中启动,可以由作业规划进程 crond 启动,还可以由用户终端(通常是 shell )执行。
4. 守护进程的启动方式有多种:
a. 它可以在 Linux 系统启动时从启动脚本 /etc/rc.d 中启动
b. 可以由作业规划进程 crond 启动;
c. 还可以由用户终端(通常是 shell )执行。
Tiger-John 总结:
 守护进程是
Linux
中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。
Linux 系统有很多守护进程,大多数服务都是通过守护进程实现的,同时,守护进程还能完成许多系统任务,例如,作业规划进程 crond
、打印进程 lqd 等(这里的结尾字母 d 就是 Daemon 的意思)。
5. 如何编写守护进程呢

第一步:首先要做的是调用umask将文件模式创建屏蔽字设置为0。由继承得来的文件模式创建屏蔽字可能会拒绝设置某些权限。例如,若守护进程要创建一个组可读、写的文件,而继承的文件模式创建屏蔽字可能屏蔽了这两种权限,于是所要求的组可读、写就不能起作用。
第一步:创建子进程,父进程退出
1>. 由于守护进程是脱离控制终端的,因此,完成第一步后就会在 Shell 终端里造成一程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在 Shell 终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离。
2> 在 Linux 中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程时就会由 1 号进程( init) 收养它。
方法是调用 fork 产生一个子进程,然后使得父进程退出
pid = fork();
if( 0 == pid)
exit(0); // 如果是父进程,就结束父进程,子进程结束。

这样就实现了下面几点:第一,如果该守护进程是作为一个简单shell命令启动的,那么父进程终止使得shell认为这条命令已经执行完毕;第二,子进程继承了父进程的进程组ID,但具有一个新的进程ID,这就保证了子进程不是一个进程组的组长进程。这对于下面就要做的setsid调用是必要的前提条件。
第二步:在子进程中创建新会话:
这个步骤是创建守护进程中最重要的一步,使用系统函数 setsid。使得调用进程:(a)成为新会话的首进程,(b)成为一个新进程组的组长进程,(c)没有控制终端。

Tiger-John 补充: 几个相关概念
a.
进程组:是一个或多个进程的集合。进程组有进程组 ID 来唯一标识。除了进程号( PID )之外,进程组 ID ( GID)
也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组 ID 。且该进程组 ID 不会因组长进程的退出而受到影响。
b. 会话周期:会话期是一个或多个进程组的集合。通常,一个会话开始与用户登录,终止于用户退出,在此期间该用户运行的所用进程都属于这个会话期。
c. 登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
Tiger-John 说明: 为什么要涉及它们呢?
因为控制终端,登录会话和进程组通常是从父进程继承下来的。我们就是要摆脱它们,使之不受它们的影响。
那么如何去实现呢,此时我们在第一步的基础上可以调用 setsid ()函数。
1>setsid 函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid 有下面的 3 个作用:
让进程摆脱原会话的控制
让进程摆脱原进程组的控制
让进程摆脱原进程组的控制
让进程摆脱原控制终端的控制
2>. 在创建守护进程时为什么要调用 setsid 函数呢?
由于创建守护进程的第一步调用了
fork 函数来创建子进程,再将父进程退出。由于在调用了 fork
函数时,子进程全盘拷贝了父进程的会话期,进程组,控制终端等,虽然父进程退出了,但会话期,进程组,控制终端等并没有改变,因此,还不是真正意义上的独立开来,而
setsid 函数能够使进程完全独立出来,从而摆脱其他进程的控制。
Tiger-John 说明:
a. 当进程组是会话组长时 setsid() 调用失败。但是通过第一步已经保证了进程不是会话组长。
b.setsid( )调用成功后,进程成为新的会话组长和新的进程组长,并于原来的登录会话和进程组脱离由于会话过程对控制终端的独占性,进程同时与控制终端脱离
c. 此时我们还要禁止进程重新打开控制终端
进程虽然已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端
那么如何实现呢?
我们可以再次建立一个子进程,退出父进程,保证该进程不是进程组长,同时让该进程无法再打开一个新的终端
pid = fork() ;
exit(0) ;
第三步:改变当前目录为根目录
1>使用
fork
创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后的使用会造成很多的不便。因此,我们一般是让”
/” 作为守护进程的当前工作目录,这样就可以避免上述的问题。如果有特殊需要,也可以把当前工作目录换成其他的路径。
2>改变工作目录的常见函数是 chdir().
第四步:重设文件权限掩码
1>文件权限掩码是指屏蔽掉文件权限中的对应位。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了很多的麻烦。因此,把文件权限掩码设置为 0 ,可以很大程度上增强该守护进程的灵活性。
2>设置文件权限掩码的函数是 umask. 通常使用的方法是 umask(0).
第五步:关闭文件描述符
1> 因为用 fork 函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。
2>
在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能到达守护进程,守护进程中常规方法(如 printf
)输出的字符也不可能在终端上显示出来。所以,文件描述符为 0 , 1 和 2 的 3
个文件(常说的输入,输出和报错)已经失去了意义,也应该关掉。
3>函数实例:
for(i=0;i<MAXFILE;i++)
close(i);
第六步:处理 SIGCHLD 信号
1>处理
SIGCHLD
信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(
zombie) 从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务进程的并发性能。
2>函数实现:
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。

6具体函数实现:
编写一个守护进程要包括两部分:主程序 test.c 和初始化程序 init.c 。
初始化程序中的 init_daemon 函数负责生成守护进程。利用 init_daemon 函数可以生成自己的守护进程。

daemon.c

1 #include<stdio.h>
2 #include<signal.h>
3 #include<sys/param.h>
4 #include<sys/types.h>
5 #include<sys/stat.h>
6 #include<stdlib.h>
7
8 int init_daemon(void)
9 {

10         pid_t pid;
11         int i;

12
13         pid = fork();
14         if(pid > 0){          //第一步,结束父进程,使得子进程成为后台
15                 exit(0);
16         }
17         else if(pid < 0){
18                 return -1;
19         }
20 /*第二步建立一个新的进程组,在这个新的进程组中,子进程成为这个进程组的首进程,以使该进程脱离所用终端*/   
21         setsid();
22 /*再次新建一个子进程,退出父进程,保证该进程不是进程组长,同时让该进程无法再打开一个新的终端*/
 23         pid = fork();
 24         if(pid > 0){
 25                 exit(0);
 26         }
 27         else if(pid < 0){
 28                 return -1;
 29         }
 30 //第三步:关闭所用从父进程继承的不再需要的文件描述符
 31         for(i = 0;i < NOFILE;close(i++));
 32 //第四步:改变工作目录,使得进程不与任何文件系统联系
 33         chdir("/");
 34 //第五步:将文件屏蔽字设置为0
 35         umask(0);
 36 //第六步:忽略SIGCHLD信号
 37         signal(SIGCHLD,SIG_IGN);
 38         return 0;
 39 }

test.c
 1 #include<stdio.h>
 2 #include<time.h>
 3 #include<syslog.h>
 4 extern int init_daemon(void);
 5
 6 int main()
 7 {
 8         time_t now;
 9         init_daemon();//初始化Daemon
 10         syslog(LOG_USER | LOG_INFO,"测试守护进程!/n");
 11         while(1){
 12                 sleep(8);//睡眠一分钟
 13                 time(&now);
 14                 syslog(LOG_USER | LOG_INFO,"系统时间:/t%s/t/t/n",ctime(&now    ));
 15                 }
 16 }
 17

程序在 ubuntu 2 .6 版本上进过调试
think@ubuntu:/etc$ gcc -g -o test daemon.c test.c
think@ubuntu:/etc$ ./test
编译成功后可以用 ps -ef 查看进程状态,
think@ubuntu:/etc$ ps -ef
UID PID   PPID C   STIME   TTY TIME   CMD
think 2995 1    0   11:05    ? 00:00:00   ./test
从此处可以看出该进程具备守护进程的所用特征
查看系统日志
think@ubuntu:~$ cat /var/log/syslog
Nov 13 11:05:37 ubuntu test: 测试守护进程!
Nov 13 11:05:45 ubuntu test: 系统时间: #011Sat Nov 13 11:05:45 2010#012#011#011
Nov 13 11:05:53 ubuntu test: 系统时间: #011Sat Nov 13 11:05:53 2010#012#011#011
Nov 13 11:06:01 ubuntu test: 系统时间: #011Sat Nov 13 11:06:01 2010#012#011#011
Nov 13 11:06:09 ubuntu test: 系统时间: #011Sat Nov 13 11:06:09 2010#012#011#011
Nov 13 11:06:17 ubuntu test: 系统时间: #011Sat Nov 13 11:06:17 2010#012#011#011
Nov 13 11:06:25 ubuntu test: 系统时间: #011Sat Nov 13 11:06:25 2010#012#011