守护进程的概念
守护进程也被称为精灵进程。它具有如下特点:
(1)它是运行在后台的一种特殊进程。
(2)它独立于控制终端,所以不能与用户进行直接交互。
(3)它周期性的执行某种任务或等待处理某些发生的事件。
(4)它不受用户登录注销的影响,一直在运行着。其它进程都是在用户登录或程序运行后才创建。
基于以上的这些特点,守护进程有如下用途:
(1)Linux中的大多数服务器都是通过守护进程来实现的。如ssh服务器用于远程连接,ftp服务器。
(2)守护进程可以完成很多系统任务,如作业规划进程crond用于设置定时任务等。
可以通过ps axj命令查看系统中的守护进程。守护进程通常是以d结尾的名字,表示Daemon。
创建守护进程
创建步骤如下:
1. 首先,守护进程运行在后台。而我们编写的程序在运行起来会在前台执行,所以,在程序中要用fork创建一个子进程,创建成功后,父进程退出,则子进程就被提到后台继续运行了。(这里也可以是程序直接加&以运行在后台,不过fork的目的主要是为了第二步)。
2. 要使进程转化为守护进程,最重要的一步是要使该进程脱离原来的控制终端。此时,需要调用如下函数:
#include<unistd.h> pid_t setsid(void);
注意:在调用该函数时有个前提条件。就是当前进程不允许是进程组的组长进程。否则函数调用失败。所以就需要fork一个子进程。因为程序运行起来后,父进程必然会成为进程组的而第一个进程,即成为进程组的组长进程,而子进程必然不是组长进程,所以,在子进程中调用setsid函数即可成功。而fork创建子进程在第一步中已经实现。
该函数的作用是:
(1)使调用该函数的进程脱离原会话的控制。再新创建一个会话,并使进程称为会话的Leader,当前进程的id就是该会话的id;
(2)使调用该函数的进程拜托原进程组的控制。再新创建一个进程组,并使该进程成为进程组的组长,该进程的id即为进程组的组id;
(3)摆脱控制终端。如果该进程原本有一个控制终端,则它将失去该控制终端,成为一个没有控制终端的进程。失去控制终端是指,原控制终端仍然是打开的,仍然可以读写,但只是一个打开的文件而不是控制终端了。
守护进程还具有如下特点:它没有控制终端,自成会话,自成进程组,是一个孤儿进程。
该函数调用成功返回新创建的会话id,即子进程的id,出错返回-1。
3. 守护进程创建好后,称为一个新会话的话首进程,而话首进程是可以重新申请连接控制终端的,所以,这里需要再次fork使父进程退出,子进程继承父进程的新会话,但又不是话首进程,从而实现不能再次申请控制终端的目的。注意,这一步不是必须的。
4. 守护进程创建好后,会继承父进程的文件屏蔽字。它可能会改变守护进程创建的文件的相关权限,所以要将掩码清零。
5. 守护进程创建好后,会继承父进程的当前工作目录。 而当守护进程用于特殊的任务时,会在某个特定的工作目录下执行,所以,这里需要修改守护进程的当前工作目录,一般修改为根目录/,也可以修改为其他目录。
6. 守护进程创建好后,会继承父进程的文件描述表。但是守护进程已经脱离了控制终端,所以也就不需要使用标准输入,标准输出,标准错误了。因此将其关闭,或重定向到/dev/null即可。其中,写入/dev/null的内容相当于直接丢弃了。
7. 守护进程创建好后,可能会在后序的操作中创建子进程。子进程退出如果不回收会成为僵尸进程,造成内存泄漏。如果回收,就会造成守护进程的负担。将子进程退出时发送的SIGCHLD信号设置为SIG_IGN,使子进程自动回收资源即可。
根据上述步骤,编写代码如下:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<signal.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #include<string.h> void mydaemon() { pid_t pid = fork(); if(pid < 0)//创建子进程失败 { perror("fork error"); exit(1); } else if(pid > 0) { exit(0);//使父进程退出 } //子进程调用setsid函数,创建新会话,新进程组,摆脱原控制终端 if(setsid() < 0) { perror("setsid error");//setsid函数调用失败 exit(2); } //为防止子进程再次申请连接控制终端,再次fork使父进程退出,子进程不再是话首进程 if(pid = fork() > 0) { exit(0); } else if(pid < 0) { printf("fork again error\n"); return; } //将子进程的掩码清空 umask(0); //改变从父进程处继承的当前工作目录为根目录 if(chdir("/") < 0) { printf("chdir error\n"); return; } //将从父进程处继承的不再需要的文件关闭或重定向至/etc/null close(0); int fd0; if(fd0 = open("/dev/null",O_RDWR) < 0) { printf("open error\n"); return; } dup2(fd0,1); dup2(fd0,2); //将子进程的SIGCHLD处理信号设置为SIG_IGN struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGCHLD,&sa,NULL) < 0) { printf("sigaction error\n"); return; } } int main() { mydaemon();//创建守护进程 //在守护进程打开一个文件 int fd = open("mytext.log",O_CREAT|O_RDWR,0644); if(fd < 0) { printf("open error\n"); exit(1); } //使守护进程1s中向上述打开的文件写一句hello while(1) { write(fd,"hello\n",strlen("hello\n")); sleep(1); } return 0; }
运行该程序,结果如下:
发现在根目录下并没有生成mytext.log文件。这是因为,在根目录下创建文件需要root权限,所以切换至root用户,再次运行,结果如下:
打开mytext.log文件便可发现1s向其中写一条hello。
不将标准输出重定向至dev/null时,错误信息会打印出来。再次切换回普通用户运行:
调用daemon创建守护进程
上述是由我们自己编写代码创建守护进程。而系统也提供了创建守护进程的接口函数:
#include<unistd.h> int daemon(int nochdir,int noclose);
函数功能:创建一个守护进程
参数:
nochdir:为0表示将当前工作目录改为根目录
noclose:为0表示将关闭标准输入,标准输出,标准错误。
返回值:成功返回0,失败返回-1
可以将上述的mydaemon函数替换为daemon函数进行测试。
上文中有关进程组,会话的相关知识见:操作系统------进程组/作业/会话/作业控制
有关信号函数sigaction函数的相关知识见:信号相关的知识