Unix环境高级编程:守护进程
参考
- Unix环境高级编程,第9,13章
介绍
守护进程就是Linux中使用ps aux
那些一般以d结尾的程序,比如rsyslogd,sshd等,为daemon简称。他们是长期在后台执行的随终端关闭而关闭的程序。
一般情况下我们登陆终端,执行程序只要产生的不是守护进程,一般的fork得到的进程,它们在终端失去连接或者关闭后都会被相应的终止。平常使用中可以采用nohup
命令来执行一个不希望随终端而关闭的程序,实际上就是以守护进程的方式执行它。
原理
要使一个进程成为守护进程,那么就要将它和终端脱离关系,否则终端一旦关闭就会相应的关闭与它相关的进程。在Linux中每个终端会对应一个会话(Session)。如果我们手工新建一个会话那么就不会自动的和终端关联,也就是说脱离了原来的终端成为一个守护进程。
方法
命令行
可以使用setsid
命令将某个程序启动为守护进程。有如下程序
#include <stdio.h>
#include <unistd.h>
int main() {
for(;;) {
sleep(1);
printf(".\n");
}
return 0;
}
编译后设生成文件为a.out,则执行命令
setsid ./a.out
可以发现命令执行后并没有阻塞当前的终端,当然这种效果使用./a.out&
也可以达到,但两者是不同的。后者会在终端关闭后被关闭,而前者已经成为一个守护进程不受影响。此时可以退出当前执行setsid命令的终端,然后再次登录,查看a.out进程是否依然在运行。
ubuntu@dev00:~$ ps aux|grep a.out
ubuntu 20567 0.0 0.0 4192 356 ? Ss 13:02 0:00 ./a.out
ubuntu 20652 0.0 0.0 10460 948 pts/2 R+ 13:11 0:00 grep --color=auto a.out
可以看到a.out进程依然是在运行的。但是这里有个疑问,就是上述程序命的内容明明是每隔1秒输出一个.
并换行然而重新登录后,我们并没有看到有任何的输出。我们通过proc
文件系统来看一下究竟
ubuntu@dev00:~$ ll /proc/20567/fd
total 0
dr-x------ 2 ubuntu ubuntu 0 Jul 29 13:11 ./
dr-xr-xr-x 9 ubuntu ubuntu 0 Jul 29 13:02 ../
lrwx------ 1 ubuntu ubuntu 64 Jul 29 13:11 0 -> /dev/pts/1 (deleted)
lrwx------ 1 ubuntu ubuntu 64 Jul 29 13:11 1 -> /dev/pts/1 (deleted)
lrwx------ 1 ubuntu ubuntu 64 Jul 29 13:11 2 -> /dev/pts/1 (deleted)
可以看到进程打开文件描述符(0,1,2分别对应标准输入,标准输出,错误输出)实际指向已经被标识为已删除(deleted)。而他们的指向/dev/pts/1
其实是一个伪终端。当与主机断开连接后对应的伪终端自然就失效了。可以使用who
命令查看当前登陆用户和他们使用的终端
ubuntu@dev00:~/c$ who
ubuntu pts/0 2015-07-29 01:09 (10.214.224.50)
ubuntu pts/2 2015-07-29 13:06 (10.214.224.50)
由此我们可以知道一般的继承自环境的标准输入输出对守护进程来说是不必要的,因为这些标准输入输出一般都是与终端挂钩的,一旦终端关闭这些输入输出已经失效了,我们也就无法看到守护进程的输出了。这也是为什么所用的守护进程都采用日志形式进行日志信息输出的原因,当然他们的输入一般就是配置文件。
编程实现
与setsid
命令对应的有一个同名的setsid
系统调用,使得当前进程运行于一个全新的会话中。这个调用原型非常简单
pid_t setsid(void);
但是它有个限制就是进程主的组长是不能够调用这个的,而通过bash执行的命令或者启动的程序恰好会放到一个新进程组内并把运行的进程作为该组组长。这样我们在程序中就只能先fork一下(父进程主动退出),然后用子进程去调用setsid
。父进程主动退出那么在运行的命令行上看来命令似乎立即返回了。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t cid = fork();
if (cid > 0) {
// parent process(process group leader), exit immediately
exit(0);
}
// child process
pid_t sid = setsid();
for (;;) {
sleep(10);
printf(".\n");
}
return 0;
}
编译运行后,我们可以通过ps aux
命令查到该进程确实已经是一个守护进行了,即中断一列是一个?表示没有对应的关联终端。
ubuntu 20762 0.0 0.0 4188 88 ? Ss 13:43 0:00 ./a.out
ubuntu 20763 0.0 0.0 17164 1324 pts/2 R+ 13:43 0:00 ps aux
不过《Unix环境高级编程》中给出的建议是在上述子进程内在fork一次然后在调用setsid避免守护进程成为会话的leader这样在某些系统上是有限制的,不过linux似乎没有碰到。
概念
进程组
进程组表示一组进程,一般情况下运行的程序fork出来的子进程和父进程都是属于同一个进程组的。可以向一个进程组发送信号,然后进程组内的每个进程都会收到该信号。如果不进行额外的设置我们可以推断,系统中所有的进程都是同一个进程组的,不过显然这个假设是不成立的,虽然进程都是不断fork出来的。一个例外就是bash程序会把执行的命令程序放到一个单独的进程组中,而不是放到它自己所在的那个。进程组有一个leader,进程组ID即为该leader的pid。试着执行以下命令:
$ sleep 100 &
$ ps -o pid,pgid,ppid,sid,cmd -e
在ps输出的最后几行应该有类似如下几行:
27245 27245 27244 27245 -bash
28931 28931 27245 27245 sleep 100
28932 28932 27245 27245 ps -o pid,pgid,ppid,sid,cmd -e
其中输出的数字依次是进程ID,进程组ID,父进程ID,会话ID,命令参数。从中我们可以知道sleep命令和ps命令的父进程都是bash进程,sleep和ps命令在各自的进程组中是各自的leader。如果自己写一个如下的一个普通程序:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main() {
fork();
sleep(100);
return 0;
}
编译并运行:
$ ./a.out &
$ ps -o pid,pgid,ppid,sid,cmd -e
28986 28986 27245 27245 ./a.out
28987 28986 28986 27245 ./a.out
28988 28988 27245 27245 ps -o pid,pgid,ppid,sid,cmd -e
可以看到a.out产生的两个进程都在自己的进程组内,不是各自独立的。
设置进程组
可以通过int setpgrp(pid_t pid, pid_t gpid)
来将一个进程设置为指定的进程组,也可以创建一个新进程组然后pid
参数指定的进程成为该组leader。这个调用只能对自己或者子进程有效。我们可以修改原来的代码:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main() {
pid_t child = fork();
if (child > 0) {
setpgid(child, child);
}
sleep(100);
return 0;
}
此时再来通过ps命令进行检验可以得到如下类似输出
29011 29011 27245 27245 ./a.out
29012 29012 29011 27245 ./a.out
29013 29013 27245 27245 ps -o pid,pgid,ppid,sid,cmd -e
a.out程序产生的两个进程现在已经位于不同的进程组中了。在linux中可以通过一个()
让其中的命令在一个进程组中如下:
$ (sleep 10 | sleep 20)&
29020 29020 27245 27245 -bash
29021 29020 29020 27245 sleep 10
29022 29020 29020 27245 sleep 20
29023 29023 27245 27245 ps -o pid,pgid,ppid,sid,cmd -e
前台进程组
每个会话一般只有一个前台进程组,前台进程即可以在控制终端上进行交互的那些进程。可以通过tcsetpgrp
来将哪个进程组设定为前台进程组。我们在执行bg
&fg
命令时就用的了这个调用。
give_terminal_to (pgrp, force)
pid_t pgrp;
int force;
{
sigset_t set, oset;
int r, e;
r = 0;
if (job_control || force)
{
sigemptyset (&set);
sigaddset (&set, SIGTTOU);
sigaddset (&set, SIGTTIN);
sigaddset (&set, SIGTSTP);
sigaddset (&set, SIGCHLD);
sigemptyset (&oset);
sigprocmask (SIG_BLOCK, &set, &oset);
if (tcsetpgrp (shell_tty, pgrp) < 0)
{
/* Maybe we should print an error message? */
#if 0
sys_error ("tcsetpgrp(%d) failed: pid %ld to pgrp %ld",
shell_tty, (long)getpid(), (long)pgrp);
#endif
r = -1;
e = errno;
}
else
terminal_pgrp = pgrp;
sigprocmask (SIG_SETMASK, &oset, (sigset_t *)NULL);
}
if (r == -1)
errno = e;
return r;
}
后台进程组
一个会话可以有多个后台进程组,就如我们在命令行后跟一个&
就使得命令在后台执行一样。