如何让程序真正地后台运行?

如何实现一个守护进程?如何让程序在后台运行?这是后台开发面试常问的一道题,那么守护进程到底是什么?又该如何实现?

守护进程

守护进程通常生存期长,很多是在系统启动时启动,系统退出时才关闭。它们的特点通常没有控制终端,后台运行。

有人可能会会心一笑,后台运行程序,我知道呀。还有两种方式呢

1
$ ./hello &

看,多么简单。但是运行之后,你试着关闭当前终端,你会发现程序会停止运行,因为一旦关闭终端,程序会收到一个信号SIGHUP,而收到该信号默认的动作就是程序退出。

没关系啊,我还有招:

1
2
3
$ nohup ./hello & #注意这里&是必要的,否则不会变成后台进程
$ jobs
[1]+ Running nohup ./hello &

我使用nohup命令总可以了吧?

挺好的,nohup会忽略SIGHUP命令,并有了&的加持,即便终端关了,也能继续执行。但它的终端输出还会记录默认还在nohup.out文件中,同时,如果将huponexit关闭,它同样难逃命运:

1
2
3
$ shopt -s huponexit  #shopt -u huponexit 设置为off
$ shopt |grep onexit
huponexit on

一旦终端退出(ctrl+D)后,nohup也救不了。

下面要介绍的守护进程一一种完全脱离终端,有着自己的会话。
如果你在你的Linux系统中执行下面的命令:

1
$ ps -elf

就会发现一些进程的tty列是?,当然这并不是说明它们是守护进程,而那些用[]括起来的,是内核守护进程

想象一下,如果没有任何人登录的服务器上面的运行程序,难道每次执行的时候都要使用nuhup+&?况且,一旦系统的huponexit选项是打开的,这种方式仍然无法避免终端关闭程序就退出的命运!

那么就需要实现用户守护进程了,或者说daemon化。

如何实现

其实现过程基本遵循以下原则:

  • 调用umask设置文件模式,通常设置为0。为了便于后续创建文件,不使用继承而来的父进程的设置,需要设置新的权限掩码。
  • 调用fork,创建子进程,并且父进程退出
  • 调用setdid创建新的会话(一个或多个进程组的集合),由于当前进程不是一个进程组的组长,因此会创建一个新的会话,却成为组长进程,同时没有控制终端。
  • 将当前工作目录切换为根目录。同样的,其工作目录可能是从父进程继承而来的,可以自己另立山头。
  • 关闭不需要的文件描述符。同样的,可能从父进程继承了一些打开的文件描述符,而这些描述符可能再也不需要,因此可以关闭。
  • 重定向标准输出,标准输入和标准错误到/dev/null(相关阅读:)

实际上,从上面的描述可以发现,这些规则都有几乎相同的目标,那就是不想成为富二代,摆脱父亲的控制。(在fork的介绍中,我们说到,儿子从父亲那里继承了很多东西)

  • 重新设置权限掩码,避免受父进程影响
  • 创建新的会话,脱离终端
  • 使用新的工作目录
  • 关闭不需要的文件描述符
  • 关闭标准输入,标准输出和标准错误

所以通过这些也可以明白,有些规则并不是完全强制的,可根据实际程序的情况进行设置,不过按照常规做法是一个比较好的选择。

具体实现

参考代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//来源:公众号【编程珠玑】
//https://www.yanbinghu.com
#include<stdio.h>
#include<sys/resource.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
/*实现仅供参考,可根据实际情况调整*/
int daemonize()
{
/*清除文件权限掩码*/
umask(0);

/*父进程退出*/
pid_t pid;
if((pid=fork()) < 0)
{
/*for 出错*/
perror("fork error");
return -1;
}
else if(0 != pid)/*父进程*/
{
printf("father exit\n");
exit(0);
}
/*子进程,成为组长进程,并且摆脱终端*/
setsid();

/*修改工作目录*/
if(chdir("/") < 0)
{
perror("change dir failed");
return -1;
}


struct rlimit rl;
/*先获取文件描述符最大值*/
if(getrlimit(RLIMIT_NOFILE,&rl) < 0)
{
perror("get file decription failed");
return -1;
}
/*如果无限制,则设置为1024*/
if(rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;

/*为了使得终端有输出,保留了文件描述符0,1,2;实际上父进程可能没有打开2以上的文件描述符*/
int i;
for(i = 3;i < rl.rlim_max;i++)
close(i);
return 0;
}
int main(void)
{
if(0 == daemonize())
{
printf("daemonize ok\n");
sleep(20);
}
else
{
printf("daemonize failed\n");
sleep(20);
}
return 0;
}

编译运行,你就会发现,它已经可以欢脱地运行啦。

代码中有几个点,需要关注一下。为了保留printf的输出,我在daemonize函数中,并没有关闭所有的文件描述符,0,1,2可以参考《如何理解Linux shell中“2>&1”》,当然了,如果想让printf的输出保存到文件,也有方法,可以参考《优雅地保存printf的打印》,这里就不再赘述了。

实际实现

实际上,已经有一个接口可以帮我们做这些事情:

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

即daemon函数,它有两个参数

  • nochdir 为0时,表示修改其根目录为/,否则不变
  • noclose,为0时,表示将标准输入,标准输出,标准错误重定向到/dev/null。

简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//来源:公众号【编程珠玑】
//https://www.yanbinghu.com
#include<stdio.h>
#include<unistd.h>
int main(void)
{
if(0 == daemon(1,1))
{
printf("daemon ok\n");
sleep(20);
}
else
{
printf("daemon failed\n");
sleep(20);
}
return 0;
}

如果你还要实现单例化,可以参考《如何实现单例运行》,使得同时只有一个该进程运行。

总结

以上就进程后台运行以及是守护进程实现的介绍,关键点有

  • 创建子进程,父进程退出
  • 创建新的会话,脱离终端

附上daemon的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int daemon(nochdir, noclose)
int nochdir, noclose;
{
int fd;

switch (fork())
{
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}

if (setsid() == -1)
return (-1);

if (!nochdir)
(void)chdir("/");

if (!noclose && (fd = open(_PATH_DEVNULL, O_RDWR, 0)) != -1)
{
(void)dup2(fd, STDIN_FILENO);
(void)dup2(fd, STDOUT_FILENO);
(void)dup2(fd, STDERR_FILENO);
if (fd > 2)
(void)close (fd);
}
return (0);
}

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!