1. 概念

1.1 引入

在现实生活中, 许多大型的软件或服务器必须保证7*24小时(一周7天,一天24小时)无障碍的运行,例如淘宝网、百度搜索引擎、支付宝等等,那么像这样一种要一直运行的程序怎么实现呢?究其本质其实就是我们的守护进程。

1.2 定义

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond等。

1.3 特点

Linux系统启动时会启动很多系统服务进程,这些系统服 务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)

在Linux中,每个系统与用户进行交流的界面成为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,知道整个系统关闭才退出(当然可以认为的杀死相应的守护进程)。如果想让某个进程不因为用户或中断或其他变化而影响,那么就必须把这个进程变成一个守护进程。
用户的登录与注销与守护进程无关系,不受其影响,守护进程自成进程组,自成会话 ,即pid = gid = sid。

2. 创建

2.1 查看系统中的进程

[root@localhost home]# ps axj
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    0     1     1     1 ?           -1 Ss       0   0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
    0     2     0     0 ?           -1 S        0   0:00 [kthreadd]
    2     4     0     0 ?           -1 S<       0   0:00 [kworker/0:0H]
    2     6     0     0 ?           -1 S        0   0:00 [ksoftirqd/0]
    2     7     0     0 ?           -1 S        0   0:00 [migration/0]
    2     8     0     0 ?           -1 S        0   0:00 [rcu_bh]
    2     9     0     0 ?           -1 S        0   0:00 [rcu_sched]
    2    10     0     0 ?           -1 S<       0   0:00 [lru-add-drain]
    2    11     0     0 ?           -1 S        0   0:00 [watchdog/0]
    2    12     0     0 ?           -1 S        0   0:00 [watchdog/1]
    2    13     0     0 ?           -1 S        0   0:01 [migration/1]
    2    14     0     0 ?           -1 S        0   0:00 [ksoftirqd/1]
...
  • a表示不仅列当前用户的进程,也列出所有其他用户的进程
  • x表示不仅列有控制终端的进程,也列出所无控制终端的进程
  • j表示列出与作业控制相关的信息

凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。在COMMAND一列用[]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。init进程我们已经很熟悉了,udevd负责维护/dev目录下的设备⽂文件,acpid负责电源管理,syslogd负责维护/var/log下的日志文件,可以看出,守护进程通常采用以d结尾的名字,表示Daemon。

2.2 setsid函数

1)创建守护进程最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。

#include<unistd.h>
pid_t setsid(void);

返回值:该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。

2)需要注意的是,调用这个函数之前,当前进程不允许是进程组的Leader,否则该函数返回-1。

解决办法:先fork再调用setsid,fork创建的子进程和父进程在同一个进程组中,进程组的Leader必然是该组的第一个进程,所以子进程不可能是该组的第一个进程,在子进程中调用setsid就不会有问题了。

3)成功调用该函数的结果:

a)创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id。
b)创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id。
c)如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。所谓失去控制终端是指,原来的控制终端仍然是打开的,仍然可以读写,但只是一个普通的打开文件而不是控制终端了。

2.3 创建步骤

1)调用umask将文件模式创建屏蔽字设置为0

umask(0);//umask必须清0,否则创建文件受系统默认权限的影响

2)调用fork,父进程退出(exit)

原因:
a)如果该守护进程是作为一条简单的shell命令启动的,那么⽗父进程终止使得shell认为该命令已经执行完毕。
b)保证子进程不是一个进程组的组长进程。

3)调用setsid创建一个新会话

setsid会导致:
a)调用进程成为新会话的首进程。
b)调用进程成为一个进程组的组长进程 。
c)调用进程没有控制终端。(再次fork一次,保证daemon进程,之后不会打开tty设备)

调用setsid的原因:
由于创建守护进程的第一步是调用fork()函数来创建子进程,再将父进程退出。由于在调用了fork()函数的时候,子进程拷贝了父进程的会话期、进程组、控制终端等资源、虽然父进程退出了,但是会话期、进程组、控制终端等并没有改变,因此,需要用setsid()将该子进程完全独立出来,从而摆脱其他进程的控制。
4)将当前工作目录更改为根目录

防止当前目录有一个目录被删除,导致守护进程无效。
使用fork()创建的子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其他的别的目录来作为守护进程的工作目录。
5)关闭不再需要的文件描述符

同文件权限码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些文件被打开的文件可能永远不会被守护进程读写,如果不进行关闭的话将会浪费系统的资源,造成进程所在的文件系统无法卸下以及引起预料的错误。

如:关闭标准输入流、标准输出流、标准错误流:

close(0);
close(1);
close(2);

6)其他:忽略SIGCHLD信号

signal(SIGCHLD,SIG_IGN);

3. 示例

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

void mydaemon()
{
    umask(0);//将文件模式创建屏蔽字设为0 
    pid_t pid = fork();//创建子进程
    if(pid == -1)
        perror("fork error");
    else if(pid == 0)//child
    {
        setsid();//创建新会话
        if(chdir("/") < 0)//更改当前的工作目录
            perror("chdir error");
        close(0);//关闭标准输入流的文件描述符
        close(1);//关闭标准输出流的文件描述符
        close(2);//关闭标准错误流的文件描述符

        signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号
    }
    else//father
    {
        exit(1);//父进程退出 
    }
}

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

ps axj | grep mydaemon查看精灵进程:

root@localhost shouhujincheng]# ps axj | grep myDaemon
    1  1795  1795  1795 ?           -1 Rs       0   0:15 ./myDaemon
 1729  1797  1796  1729 pts/0     1796 S+       0   0:00 grep --color=auto myDaemon

4. 系统函数daemon

#include <unistd.h>
int daemon(int nochdir, int noclose);

1)daemon函数主要用于希望脱离控制台,以守护进程的形式在后台运行的程序。
2)当nochdir为0时,daemon将更改当前进程的目录为root(“/”)目录。
3)当noclose为0时,daemon将进程的STDIN,STDOUT,STDERR都重定向到/dev/null。
/dev/null:linux下的黑洞,写入的所有数据会直接丢弃。
用daemon函数创建守护进程:

#include <unistd.h>
#include <stdio.h>

int main()
{
    daemon(1, 1);
    while(1);
    return 0;
}
[root@localhost shouhujincheng]# ps axj | grep daemon
    1   870   870   870 ?           -1 Ss      81   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
    1   882   882   882 ?           -1 Ssl      0   0:00 /usr/sbin/NetworkManager --no-daemon
    1  1845  1845  1845 ?           -1 Rs       0   0:23 ./daemon
 1729  1847  1846  1729 pts/0     1846 S+       0   0:00 grep --color=auto daemon

5. 杀死守护进程

1)利用ps axj | grep 守护进程名找到相应的守护进程,然后用kill -9 进程号将对应进程杀死

[root@localhost shouhujincheng]# ps axj | grep myDaemon
    1  1795  1795  1795 ?           -1 Rs       0   9:30 ./myDaemon
 1729  1864  1863  1729 pts/0     1863 S+       0   0:00 grep --color=auto myDaemon
[root@localhost shouhujincheng]# kill -9 1795

2)利用ps -ef命令查找相应的守护进程,再用kill命令将其杀死

3)也可创建shell脚本对进程的启动、关闭、重启进行自动管理

6. 创建守护进程fork两次

一个daemon函数常见的实现:

int daemon(void)  
{  
    pid_t pid = fork();  //第一次fork

    if( pid != 0 ) 
        exit(0);//parent  

    //first children  
    if(setsid() == -1)  
    {  
       printf("setsid failed\n");  
       assert(0);  
       exit(-1);  
    }  

    umask(0);  

    pid = fork();  //第二次fork

    if( pid != 0) 
        exit(0);  

    //second children   
    chdir ("/");  

    for (int i = 0; i < 3; i++)  
    {  
        close (i);  
    }  

    int stdfd = open ("/dev/null", O_RDWR);  
    dup2(stdfd, STDOUT_FILENO);  
    dup2(stdfd, STDERR_FILENO);  

    return 0;  
}

可以看到上面的代码里我fork了两次,虽然说这并不是必须的,但是这的确是对守护进程做出了一些更优化的操作。

首先第一次fork:这里第一次fork的作用就是让shell认为这条命令已经终止,不用挂在终端输入上;再一个是为了后面的setsid服务,因为调用setsid函数的进程不能是进程组组长(会报错Operation not permitted),如果不fork子进程,那么此时的父进程是进程组组长,无法调用setsid。所以到这里子进程便成为了一个新会话组的组长。

第二次fork:第二次fork是为了避免后期进程误操作而再次打开终端。因为打开一个控制终端的前提条件是该进程必须为会话组组长,而我们通过第二次fork,确保了第二次fork出来的子进程不会是会话组组长。

下面罗列一下控制终端会产生哪些信号。程序中只要处理好这些信号,同样能达到上面函数实现的目的。

//后台进程读取/写入终端输入产生下面两个信号,或者控制终端不存在情况读取和写入会产生
 

signal(SIGTTOU, SIG_IGN);
  signal(SIGTTIN, SIG_IGN);

//按CTRL-C ,CTRL-\ CTRL-Z会向前台进程组发送下面这些信号

signal(SIGINT,  SIG_IGN );
   signal(SIGQUIT, SIG_IGN );
   signal(SIGTSTP, SIG_IGN );

//终端断开,会给会话组长或孤儿进程组所有成员发送下面信号

signal(SIGHUP,  SIG_IGN );

还有有些信号也可以由终端shell产生,需要关注

signal(SIGCONT, SIG_IGN );
signal(SIGSTOP, SIG_IGN );

上面这些信号,应该有些程序缺省处理(SIG_DFL)本身动作就是忽略(SIG_IGN),不是退出进程。