守护进程

什么是守护进程

  • 守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进 程。 Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。 比如,作业规划进程crond,打印进程lpd等。
  • 守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环 境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下 来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还 可以由用户终端(通常是 shell)执行。
  • 一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
    实现守护进程要注意的地方

  • 在后台运行
    为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
    if(pid=fork())exit(0); //是父进程,结束父进程,子进程继续

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

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

setsid();

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

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

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

  • 关闭打开的文件描述符

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

for(i=0;i 关闭打开的文件描述符close(i);

  • 改变当前工作目录
    进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir(“/“)
  • 重设文件创建掩模
    进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
  • 处理SIGCHLD信号
    处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结 束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下 可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int Create_Daemon()
{
int pid;
//屏蔽一些控制终端信号
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP ,SIG_IGN);
//设置文件掩码
umask(0);
//调用fork函数,父进程退出
pid = fork();
if(pid < 0 )
{
printf("error fork");
return -1;
}
else if(pid > 0)
{//father
exit(0);
}
//设置新会话
setsid();
//处理SIGCHlD信号
signal(SIGCHLD,SIG_IGN);

//禁止进程重新打开控制终端
if(pid = fork())
{//father
exit(0);
}
else if(pid <0)
{
perror("fork");
exit(-1);
}

//关闭打开的文件描述符
close(0);close(1);close(2);

//改变当前的工作目录
chdir("/");

return 0;
}

int main()
{
Create_Daemon();
while(1);
return 0;
}

总结

创建过程:

  1. 创建子进程,父进程退出。
  • 即创建子进程后,显示退出父进程,造成在终端这一进程已运行完毕的假象。之后的操作都由子进程完成。形式上做到与控制终端脱离。
  • 孤儿进程:父进程先于子进程退出,则称为孤儿进程。系统发现一个孤儿进程后,就自动有1号进程(init进程)收养,即该子进程称为init进程的子进程。
  1. 在子进程中创建新会话。
  • 继承:调用fork()函数,子进程会拷贝父进程的所有会话期、进程组、控制终端等。需要重设这些,才使子进程真正的与控制终端脱离。
  • 进程组:一个或多个进程集合。每个进程有进程pid,进程组有组ID。pid和进程组ID都是一个进程的必备属性。每个进程组都有组长进程,其进程号等于进程组ID。当进程组ID不受组长进程退出的影响。
  • 会话期:一个或多个进程组集合。通常,一个会话始于用户登录,终于用户退出,这之间的所有进程都属于该会话期。
  • setsid:创建新会话,并担任该会话组组长。有3个作用:
    让进程摆脱原会话的控制
    让进程摆脱原会话组的控制
    让进程摆脱原控制终端的控制
  1. 改变当前目录为根目录
  • 继承:fork()创建的子进程还拷贝了父进程的当前工作目录。需要重设。
  • 进程运行中,当前目录所在文件系统是不能卸载的,即原工作目录无法卸载。可能造成很多麻烦,如需要进入单用户模式。所以必须重设当前目录。
  • chdir(“/“):重设为根目录
  1. 重设文件权限掩码
  • 文件权限掩码:屏蔽掉文件权限中的对应位。有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。
  • 继承:fork()创建的子进程还继承了父进程的文件权限掩码。
  • umask(0):重设为0,灵活性更强。
  1. 关闭文件描述符
  • 继承:fork()创建的子进程从父进程继承了一些已经打开了的文件。被打开的进程可能永远不会被守护进程使用,却消耗资源。所以必须手动关闭文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)。
  1. 守护进程退出处理
  • 可能需要支持用户在外部手动停止守护进程运行,通常使用kill命令。编码实现kill发出的signal信号处理,达到线程正常退出。

创建进程

Linux上创建子进程的方式有三种(极其重要的概念):一种是fork出来的进程,一种是exec出来的进程,一种是clone出来的进程。

  • fork是复制进程,它会复制当前进程的副本(不考虑写时复制的模式),以适当的方式将这些资源交给子进程。所以子进程掌握的资源和父进程是一样的,包括内存中的内容,所以也包括环境变量和变量。但父子进程是完全独立的,它们是一个程序的两个实例。

  • exec是加载另一个应用程序,替代当前运行的进程,也就是说在不创建新进程的情况下加载一个新程序。exec还有一个动作,在进程执行完毕后,退出exec所在环境(实际上是进程直接跳转到exec上,执行完exec就直接退出。而非exec加载程序的方式是:父进程睡眠,然后执行子进程,执行完后回到父进程,所以不会立即退出当前环境)。所以为了保证进程安全,若要形成新的且独立的子进程,都会先fork一份当前进程,然后在fork出来的子进程上调用exec来加载新程序替代该子进程。例如在bash下执行cp命令,会先fork出一个bash,然后再exec加载cp程序覆盖子bash进程变成cp进程。但要注意,fork进程时会复制所有内存页,但使用exec加载新程序时会初始化地址空间,意味着复制动作完全是多余的操作,当然,有了写时复制技术不用过多考虑这个问题。

  • clone用于实现线程。clone的工作原理和fork相同,但clone出来的新进程不独立于父进程,它只会和父进程共享某些资源,在clone进程的时候,可以指定要共享的是哪些资源。

如何创建一个子进程?

每次fork一个进程的时候,虽然调用一次fork(),会分别为两个进程返回两个值:对子进程的返回值为0,对父进程的返回值是子进程的pid。所以,可以使用下面的shell伪代码来描述运行一个ls命令时的过程:

1
2
3
4
5
6
fpid=`fork()`
if [ $fpid = 0 ]{
exec(ls) || echo "Can't exec ls"
exit
}
wait($fpid)

假设上面是在shell脚本中执行ls命令,那么fork的是shell脚本进程。fork后,父进程将继续执行,且if语句判断失败,于是执行wait;而子进程执行时将检测到fpid=0,于是执行exec(ls),当ls执行结束,子进程因为exec的原因将退出。于是父进程的wait等待完成,继续执行后面的代码。

如果在这个shell脚本中某个位置,执行exec命令(exec命令调用的其实就是exec家族函数),shell脚本进程直接切换到exec命令上,执行完exec命令,就表示进程终止,于是exec命令后面的所有命令都不会再执行。

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

+--------+
| pid=7 |
| ppid=4 |
| bash |
+--------+
|
| calls fork
V
+--------+ +--------+
| pid=7 | forks | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash | | bash |
+--------+ +--------+
| |
| waits for pid 22 | calls exec to run ls
| V
| +--------+
| | pid=22 |
| | ppid=7 |
| | ls |
V +--------+
+--------+ |
| pid=7 | | exits
| ppid=4 | <---------------+
| bash |
+--------+
|
| continues
V
一般情况下,兄弟进程之间是相互独立、互不可见的,但有时候通过特殊手段,它们会实现进程间通信。例如管道协调了两边的进程,两边的进程属于同一个进程组,它们的PPID是一样的,管道使得它们可以以"管道"的方式传递数据。

进程是有所有者的,也就是它的发起者,某个用户如果它非进程发起者、非父进程发起者、非root用户,那么它无法杀死进程。且杀死父进程(非终端进程),会导致子进程变成孤儿进程,孤儿进程的父进程总是init/systemd。

进程信号

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
- SIGHUP   数值1  终端退出时,此终端内的进程都将被终止

- SIGQUIT 数值2 从键盘输入 Ctrl+'\'可以产生此信号

- SIGILL 数值4 非法指令

- SIGABRT 数值6 abort调用

- SIGSEGV 数值11 非法内存访问

- SIGTRAP 数值5 调试程序时使用的断点
- SIGINT 2 中断进程,可被捕捉和忽略,几乎等同于sigterm,所以也会尽可能的释放执行clean-up,释放资源,保存状态等(CTRL+C)
- SIGQUIT 3 从键盘发出杀死(终止)进程的信号

- SIGKILL 9 强制杀死进程,该信号不可被捕捉和忽略,进程收到该信号后不会执行任何clean-up行为,所以资源不会释放,状态不会保存
- SIGTERM 15 杀死(终止)进程,可被捕捉和忽略,几乎等同于sigint信号,会尽可能的释放执行clean-up,释放资源,保存状态等
- SIGCHLD 17 当子进程中断或退出时,发送该信号告知父进程自己已完成,父进程收到信号将告知内核清理进程列表。所以该信号可以解除僵尸进程,也可以让非正常退出的进程工作得以正常的clean-up,释放资源,保存状态等。

- SIGSTOP 19 该信号是不可被捕捉和忽略的进程停止信息,收到信号后会进入stopped状态
- SIGTSTP 20 该信号是可被忽略的进程停止信号(CTRL+Z)
- SIGCONT 18 发送此信号使得stopped进程进入running,该信号主要用于jobs,例如bg & fg 都会发送该信号。可以直接发送此信号给stopped进程使其运行起来

- SIGUSR1 10 用户自定义信号1
- SIGUSR2 12 用户自定义信号2

其中SIGSEGV应该是我们最常接触的,尤其是使用C/C++程序的同学更是常见。

main.c
#include <stdio.h>int main(){ int *p = NULL;
printf("hello world! \n");
printf("this will cause core dump p %d", *p);
}
使用下面命名编译即可产生运行时触发非法内存访问SIGSEGV信号,其中-g选项是编译添加调试信息,对于gdb调试非常有用。

# gcc -g main.c -o test
除了上述产生信号的方法外,使用我们经常使用的kill命令可以非常方便的产生这些信号,另外还有gcore命令可以在不终止进程的前提下产生core文件。

fork进程时资源的深拷贝和浅拷贝

linux(和 unix)将进程的概念说的很大,而且很细,进程不再仅仅拥有一个执行流,而是有了一个容器,其实某种意义上它本身就是一个容器。unix传统将进程想成 了一个执行绪,概念真的就是如此简单,简单的东西往往是好的东西,复杂的反而会更加糟糕。进程概念的简单性使得fork可以如此美妙如此简单的实现,使得 创建一个可执行映像可以分为fork和exec,正如我的前文所述。更加重要的是,传统unix将执行绪和执行过程中需要的资源分开了,如此就可以将资源 作为一件物品在进程之间传递,这丝毫没有问题。

linux继承了unix一切好的东西,它的进程由task_struct表示,内部有很多表示资源的字段,比如files_struct表示打开的文 件,unix中的fork的意义就是进程复制,复制是如此的简单且直观,使得你只需要复制当前的一切就可以了,复制往往比创造要简单得多,就像我们小的时 候总喜欢抄作业一样,后来又有了写时复制,更加节约了时间增加了效率,于是fork的意义就是复制一切资源,而对于执行绪内部的资源比如地址空间采取写时 复制的策略。因此,文件描述符作为打开文件的索引其实也是一种资源,这样的话在fork的时候就可以传递给子进程了。linux就是这样像叉子一样不断的 fork最终形成一片天地。

linux一向以地址空间的隔离作为其安全的根本,但是为何却可以拿资源传来传去呢?地址空间不也是一种资源吗?互相传递资源不会引起不安全吗?当然不会 不安全,linux的进程结构设计的非常好,资源的共享完全在可控范围内,也就是说你可以选择传递或者不传递,一切由你决定,如果出了问题就是你自己造成 的而不是操作系统造成的。内核当然知道什么东西是可以安全传递的,比如文件描述符,该描述符对应的文件可以被子进程随意读写,而且文件描述符可以不变,对 于地址空间,它是进程的根本,在fork的时候是完全复制的(现在是写时复制),其实地址空间和文件描述符都是资源,那为什么内核对待它们的态度却不同 呢?对于文件描述符的复制是浅拷贝而对于地址空间的拷贝却是深拷贝,why?这就涉及到一个资源性质的问题。我们看一下文件和地址空间作为资源有何不同, 其实很容易就可以看出它们的不同,对于文件是共享资源,它并不是进程的内禀属性,进程可拥有可不拥有,因此它当然可以被共享,而对于地址空间,它是进程的 内禀属性,进程的定义中明确规定地址空间不能和别的进程共享,因此它就必须被进程独享,因此在fork的时候要深拷贝。举个例子,丈夫这个定义,它表示此 人拥有一个妻子,妻子就是他的一个资源,而且他也可以拥有一支钢笔,但是对于丈夫而言妻子是他的内禀属性,因此妻子是他独有的,但是钢笔和丈夫并没有必然 关系,因此钢笔是可以让别人用的。

fork出的子进程和父进程谁先运行??

linux中的进程是个很重要的概念,这个就不必多说了,linux中进程创建的fork机制继承了unix的基因,是操作系统中最重要的东西,fork中的写时复制机制是fork的精髓,是进程机制的精髓,它不仅仅代表了这些,它的实现还帮了另一个忙,这就是一般说来,linux在fork之后一般让新进程先运行,这是为了避免不必要的写时复制操作,因为新进程往往不再操作父进程的地址空间而是马上进行新的逻辑或者进行exec调用,但是却复制了父进程的地址空间,如果父进程优先运行,那么父进程的每一步运行只要是写操作都会导致写时复制,这是个根本没有必要的操作,系统的机制虽然要求写时复制,但是策略上却是很少会有子进程操作父进程地址空间的情况,父进程操作其地址空间却是一定的,因为它们共享一个地址空间,所以会导致没有用的写时复制,所以解决的办法就是让子进程先运行,最起码一旦子进程进行了exec,写时复制就再也么有必要了

参考

https://www.cnblogs.com/f-ck-need-u/p/7058920.html#auto_id_0
<https://blog.csdn.net/dog250/article/details/5302859 >

-------------本文结束感谢您的阅读-------------