守护进程:(daemon进程)
也称为精灵进程,常常在系统启动自启,仅在系统关闭时才终止,生存周期比较长。是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理发生的事件。 Linux下的大多数服务器都是利用守护进程实现的。
可通过ps -axj命令查看常用系统的守护进程,其中最常见的是init进程,负责各运行层次间的系统服务。但凡事无绝对:daemon进程其实也是可以停止的,如很多daemon提供了stop命令,执行stop命令就可以终止jdaemon,或者通过发送信号将其杀死,有或者daemon进程因为代码存在bug而异常退出!
看看ps命令:
1.ps:表示对进程监测和控制。
2.参数a:表示不仅列出当前用户的进程,也列出所有其他用户的进程。
3.参数x:表示不仅列出控制终端的进程,也列出所有无控制终端的进程。
4.参数j:表示列出与作业控制相关的信息。
习惯上daemon进程名字通常以 d 结尾,那么接下来看看守护进程的编程规则,在这之前,先看看几个名词:
进程ID---
Linux下每个进程都会有一个非负整数表示的唯一进程ID,简称pid。Linux提供了getpid函数来获取进程的pid,同时还
提供了getppid函数来获取父进程的pid,相关接口:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
操作系统必须保证在任意时刻都不能出现两个进程有相同pid的情况。虽然进程ID是唯一的,但是进程ID可以重用。进程退出
以后,其进程ID还可以再次分配给其他的进程使用。那么问题就来了,内核是如何分配进程ID的?
Linux分配进程ID的算法不同于给进程分配文件描述符的最小可用算法,它采用了延迟重用的算法,即分配给新创建进程的ID
尽量不与最近终止进程的ID重复,这样就可以防止将新创建的进程误判为使用相同进程ID的已经退出的进程。
进程组---
修改进程组ID的接口如下:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
这个函数的含义是,找到进程ID为pid的进程,将其进程组ID修改为pgid,如果pid的值为0,则表示要修改调用进程的进程组ID。
该接口一般用来创建一个新的进程组。
进程组的概念并不难理解,可以将人与人之间的关系做类比。一起工作的同事,自然比毫不相干的路人更加亲近。shell中协同
工作的进程属于同一个进程组,就如同协同工作的人属于同一个部门一样。
会话---
系统提供setsid函数来创建会话,其接口定义如下:
#include <unistd.h>
pid_t setsid(void);
如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
- 创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
- 创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
- 该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。
注意:调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话
的严格的层次关系了。
好了~接下来看看daemon进程编写规则:
1、执行fork()函数,父进程退出,子进程继续
执行这一步,原因有二:
- 父进程有可能是进程组的组长(在命令行启动的情况下),从而不能够执行后面要执行的setsid函数,子进程继承了父进程的进程组ID,并且拥有自己的进程ID,一定不会是进程组的组长,所以子进程一定可以执行后面要执行的setsid函数。
- 如果daemon是从终端命令行启动的,那么父进程退出会被shell检测到,shell会显示shell提示符,让子进程在后台执行。
2、调用setsid()创建一个新会话
这个目的是切断与控制终端的所有关系,这一步比较关键,因为这一步确保了子进程不再归属于控制终端所关联的会话。因此无论终端是
否发送SIGINT、SIGQUIT或SIGTSTP信号,也无论终端是否断开,都与要创的daemon进程无关,不会影响到daemon进程的继续执行。
3、再fork,退出父进程
第二次fork是为了避免后期进程误操作而再次打开终端。因为打开一个控制终端的前提条件是该进程必须为会话组组长,而我们通过第
二次fork,确保了第二次fork出来的子进程不会是会话组组长。
4、将当前目录修改为根目录( / )
因为daemon一直在运行,如果当前工作路径上包含有根文件系统以外的其他文件系统,那么这些文件系统将无法卸载。因此,常规是
将当前工作目录切换成根目录,当然也可以是其他目录,只要确保该目录所在的文件系统不会被卸载即可。
5、清除掩码 umask(0)
这一步的目的是让daemon进程创建文件的权限属性与shell脱离关系。因为默认情况下,进程的umask来源于父进程shell的umask。如
果不执行umask(0),那么父进程shell的umask就会影响到daemon进程的umask。如果用户改变了shell的umask,那么也就相当于改变
了daemon的umask,就会造成daemon进程每次执行的umask信息可能会不一致。
6、关闭不在需要的文件描述符
如文件描述符0、1、2,指向的是控制终端。daemon进程已经不再与任意控制终端相关联,因此这三者都没有意义。一般来讲,关闭了
之后,会打开/dev/null,并执行dup2函数,将0、1和2重定向到/dev/null。这个重定向是有意义的,防止了后面的程序在文件描述符0、1和
2上执行I/O库函数而导致报错。
对于C语言而言,glibc提供了daemon函数,从而帮我们将程序转化成daemon进程。
#include <unistd.h>
int daemon(int nochdir, int noclose);
该函数有两个入参,分别控制一种行为,具体如下:
其中的nochdir,用来控制是否将当前工作目录切换到根目录。
- 0:将当前工作目录切换到/。
- 1:保持当前工作目录不变。
而noclose,用来控制是否将标准输入、标准输出和标准错误重定向到/dev/null。
- 0:将标准输入、标准输出和标准错误重定向到/dev/null。
- 1:保持标准输入、标准输出和标准错误不变。
一般情况下参数都为0 。成功时,daemon函数返回0;失败时,返回-1,并置errno。因为daemon函数内部会调用fork函数和setsid函数,
所以出错时errno可以查看fork函数和setsid函数的出错情形。
以下代码原博:https://blog.csdn.net/xiaodu655/article/details/80503712
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
void mydaemon(void)
{
pid_t id=fork();//调用fork,创建子进程
if(id<0){
perror("fork()");
}else if(id>0){//father
exit(0);
}
setsid();//set new session//调用setsid函数创建一个会话
signal(SIGCHLD,SIG_IGN);//忽略SIGCHLD信号,子进程退出时不再给父进程发信号。
pid_t id1=fork();
if(id1<0){
perror("fork()");
}else if(id1>0){//father
exit(0);
}
if(chdir("/")<0){//将当前工作目录更改为根目录:
printf("child dir error\n");
return;
}
umask(0);//调用umask将文件模式创建屏蔽字设置为0.
//关闭不需要的文件描述符,或者重定向到/dev/null中
close(0);
int fd0;
fd0=open("dev/null",O_RDWR);
dup2(fd0,1);
dup2(fd0,2);
}
int main()
{
mydaemon();
while(1){
sleep(1);
}
return 0;
}
fork()了两次!
在看看守护进程和用 & 结尾的后台运行程序有什么区别--
- 守护进程已经完全脱离终端控制台了,而后台程序并未完全脱离终端,在终端未关闭前还是会往终端输出结果
- 守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,需要在以nohup command & 格式运行才能避免影响
- 守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没改变。
如何使普通函数达到守护进程的效果--
nohup ./a.out &
over~(^-^)V