守护进程也称为精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linuxd的大多数服务器就是用守护进程实现的。比如:Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务,比如,作业规划进程crond等。
我们想要了解守护进程,首先要了解一些基础知识。
进程组,作业,会话 概念
进程组
进程组(Process Group)是一个或多个进程的集合。每个进程除了有一个进程ID之外,还属于一个进程组。每个进程组有一个唯一的进程ID,每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。
注意:组长进程可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中一个进程存在,则该进程组就存在,与其组长是否终止无关。
作业
Shell分前后台控制的不是进程而是作业(Job)或进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成。
作业控制:Shell可以运行一个前台作业和任意多个后台作业。
作业与进程组的区别:如果作业中某个进程有创建了子进程,则子进程不属于作业,它属于进程组。
会话
会话(Session):是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。
⼀个会话中的⼏个进程组可被分为⼀个前台进程组以及⼀个或多个后台进程组。所以⼀个
会话中,应该包括控制进程(会话⾸进程),⼀个前台进程组和任意后台进程组。
$proc1 | proc2 &
$proc3 | proc4 |proc5
其中proc1与proc2属于同一个后台作业,proc3,proc4和proc5属于同一个前台作业,
bash本身属于一个单独的作业。这些作业的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C,产生SIGINT,Ctrk+,产生IGQUIT,Ctrl+Z,产生SIGTSTP),内核发送相应的信号给前台作业中的所有进程。
终端
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端 (Controlling Terminal),控制终端是保存在PCB中的信息,而我们知 道fork会复制PCB中的息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况 下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准 输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。此外 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表 示SIGINT,Ctrl-\表示SIGQUIT。
每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终 端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程 要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文件来访 问。ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不 能是任意文件。
下面我们通过实验看一下各种不同的终端所对应的设备文件名。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("fd: %d -> %s\n",0,ttyname(0));
printf("fd: %d -> %s\n",1,ttyname(1));
printf("fd: %d -> %s\n",2,ttyname(2));
return 0;
}
守护进程
概念
守护进程也称为精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。Linuxd的大多数服务器就是用守护进程实现的。比如:Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务,比如,作业规划进程crond等。
linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其它进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着。这种进程有一个名称叫守护进程(Daemon)。
守护进程的特点
- 所有的守护进程都没有控制终端,其终端名(TTY)设置为问号(?)。
- 自成会话,自成进程组。不与其他会话或进程组相互关联,干扰。所以一般一个守护进程的进程ID,组ID,会话ID都相同。(自成进程组这点说的也不太严谨,若父进程是守护进程,父进程fork的子进程也是守护进程。这时父子进程属于同一进程组)
- 命令以‘d’结尾。
- 守护进程不受用户登录注销的影响,当你注销或者重登后,守护进程一直在运行。
- 生存期长,在系统引导装入时启动,仅在系统关闭时终止。
- 在后台运行(原因可归结于没有控制终端)。
- 大多数的守护进程都以root特权运行。
系统中的进程
下面呢我们通过ps axj这个命令来查看一下系统中的进程:参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
我们可以看到,凡是TGPID一栏写着-1的都是没有控制终端的进程,也就是守护进程。在COMMAND一列用[]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行, 通常采用以k开头的名字,表示Kernel。 init进程我们已经很熟悉了,udevd负责维护/dev目录下的设备文件,acpid负责电源管理,syslogd负责维护/var/log下的日志文件,可以看出,守护进程通常采用以d结尾的名字,表示Daemon。
创建守护进程
函数setsid,创建守护进程很关键的一步就是调用setsid函数创建一个新的会话(Session),并让当前的进程称为这个会话的Leader,即会话首进程。
#include<stdio.h>
pid_t setsid(void)
返回值:成功返回进程组ID,失败返回-1。
函数的结果为:
- 创建一个新会话,使该进程变成新会话的会话首进程(会话首进程也可理解为创建会话的进程),此时,该进程是当前会话的唯一进程。
- 使该进程称为一个新进程组的组长进程,进程组ID就是该进程的进程ID。
- 使该进程没有控制终端,如果在调用setsid之前有,那么就切断控制终端与当前进程的联系。
当前进程的进程ID,进程组ID,会话ID都相等。
注意:
setsid函数调用之前有一个特殊要求:该进程不能是一个进程组的组长进程。如果是,函数调用将返回出错。所以我们通常为了防止出现这种情况会用以下做法:让一个进程fork出一个子进程,然后立即将父进程终止,而子进程继续。子进程的进程ID是新分配的,子进程的PCB集成了父进程的进程组ID,所以两者不可能相等。这样就保证了子进程不是一个进程组的组长。
创建守护进程的具体步骤
用umask将文件屏蔽字设置为0
文件权限掩码是屏蔽掉文件权限中的对应位。由于使用fork()函数新创建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带了很多的麻烦(比如父进程中的文件没有执行文件的权限,然而在子进程中希望执行相应的文件这个时候就会出问题)。因此在子进程中要把文件的权限掩码设置成为0,即在此时有最大的权限,这样可以大大增强该守护进程的灵活性。调用fork,父进程进行退出
原因:
(1)如果该守护进程是作为一条简单的shell命令启动的,那么⽗父进程终止使得shell认为该命令已经执行完毕。
(2)并且该操作保证子进程不是一个进程组的组长进程。- 用setsid创建一个新的会话session
- 将当前工作目录更改为根目录
防止当前目录有一个目录被删除,导致守护进程无效。 这是因为使用fork()创建的子进程是继承了父进程的当前工作目录。然而在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的目录,当然也可以指定其他的别的目录来作为守护进程的工作目录。 - 关闭不需要的文件描述符
同文件权限码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些文件被打开的文件可能永远不会被守护进程读写,如果不进行关闭的话将会浪费系统的资源,造成进程所在的文件系统无法卸下以及引起预料的错误。 - 忽略SIG_CHLD信号
代码实现
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
void create_daemon()
{
umask(0); //1.设置umask为0
pid_t pid = fork(); //2.fork出子进程,终止父进程后,子进程调用setsid创建新的会话
if(pid < 0)
{
perror("pid");
return ;
}
else if(pid > 0)
{
exit(0);//终止父进程
}
setsid();
//3.设置当前的工作目录为根目录
if(chdir("/") < 0)
{
perror("chdir error");
return;
}
//4.关闭文件描述表
close(0);
close(1);
close(2);
//5.忽略SIG_CHLD信号
signal(SIGCHLD,SIG_IGN);
}
int main()
{
create_daemon();
while(1)
sleep(1);
return 0;
}
运行结果:
daemon()函数创建守护进程
上面为我们创建守护进程的一种方式,其实Linux为我们提供了专门的函数接口来创建守护进程。
#include <unistd.h>
int daemon(int nochdir,int noclose);
参数:
第一个参数nochdir如果设置为0的话表示将工作目录改为根目录。
二个参数noclose如果设置为0的话就将文件描述符重定向到/dev/null文件。
代码实现:
#include <stdio.h>
#include <unistd.h>
int main()
{
daemon(0,0);
while(1);
return 0;
}
创建守护进程fork两次??
(1)调用一次fork的作用:
第一次fork的作用是让shell认为这条命令已经终止,不用挂在终端输入上,还有就是为了后面的setsid服务,因为调用setsid函数的进程不能是进程组组长,如果不fork出子进程,则此时的父进程是进程组组长,就无法调用setsid。当子进程调用完setsid函数之后,子进程是会话组长也是进程组组长,并且脱离了控制终端,此时,不管控制终端如何操作,新的进程都不会收到一些信号使得进程退出。
(2)第二次fork的作用:
虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。
只有会话首进程能打开终端设备,也就是再fork一次,再把父进程退出,再次fork的子进程作为守护进程继续运行,保证了该精灵进程不是对话期的首进程,
第二次不是必须的,是可选的,市面上有些开源项目也是fork一次