Home

cdeveloper

读书,写作

Linux 高级编程 - 守护进程


版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

什么是守护进程?

守护进程可以简单的理解为后台的服务进程,很多上层的服务器都是以守护进程为基础开发的。例如 Linux 上运行的 Apache 服务器,Android 系统的 Service 服务,它们的底层都由 Linux 的守护进程提供服务。

这篇文章介绍的是在 Linux 编写守护进程的方法。

编写守护进程的 6 个步骤

先来看看整体的编写步骤:

  1. 重新设置 umask(0)
  2. 执行 fork 并脱离父进程
  3. 重启 session 会话
  4. 改变当前工作目录
  5. 关闭文件描述符
  6. 固定文件描述符 0, 1, 2 到 /dev/null

下面我们来写一个守护进程的初始化程序 daemon_init.c 来学习这 6 个步骤。

1. 重新设置 umask

进程从创建他的父进程那里继承文件创建的掩码,它可能修改守护进程所创建的文件的权限,这里清除它:

void daemon_init(void) {
	// 1. 重新设置 umask
	umask(0);
}

2. 执行 fork 并脱离父进程

既然是服务进程,意味着是可以独立运行的,不会因为父进程退出而销毁,在 Linux 系统中有一个进程号为 1 的 init 进程,这个进程一直存在与系统中,当我们的子进程从父进程脱离后,子进程变为孤儿进程,随之系统的 init 进程会接管对这个进程的控制。我们看看代码:

void daemon_init(void) {
	// 1. 重新设置 umask
	// 2. 脱离父进程
	pid_t pid = fork()  ;

	if(pid < 0)
		exit(1);	// 进程创建失败
	else if(pid > 0)
		exit(0);	// 退出父进程

	return 0;
}

3. 重启 session 会话

使用 setsid 重新开启一个 session 会话,防止脱离父进程的子进程重新受控于字符终端进程,尽量加上这一步防备工作让 fork 的子进程直接受控于 init 进程,要知道编写守护进程的核心是让子进程直接受控于 init 进程

void daemon_init(void) {
	// 1. 重新设置 umask
	// 2. 脱离父进程
	// 3. 重启 session 会话
	setsid();
}

4. 改变工作目录

进程活动时,其工作目录所在的文件系统不能卸载,一般需要将守护进程工作目录改变到根目录 /

void daemon_init(void) {
	// 1. 重新设置 umask
	// 2. 脱离父进程
	// 3. 重启 session 会话
	// 4. 改变工作目录
	chdir("/");
}

5. 关闭文件描述符

进程从创建它的父进程哪里继承了打开的文件描述符,若不关闭将会造成资源浪费,造成进程所在的文件系统无法卸下以及引起无法预料的错误:

void daemon_init(void) {
	// 1. 重新设置 umask
	// 2. 脱离父进程
	// 3. 重启 session 会话
	// 4. 改变工作目录
	// 5. 得到并关闭文件描述符
	struct rlimit rl;
	getrlimit(RLIMIT_NOFILE, &rl);
	if (rl.rlim_max == RLIM_INFINITY)
		rl.rlim_max = 1024;
	for(int i = 0; i < rl.rlim_max; i++)
		close(i);
}

6. 固定文件描述符 0, 1, 2 到 /dev/null

守护进程在后台运行,不会与用户发生直接的交互,我们不希望在终端上看到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取,因此我们将文件描述符 0, 1, 2 定位到 /dev/null

void daemon_init(void) {
	// 1. 重新设置 umask
	// 2. 脱离父进程
	// 3. 重启 session 会话
	// 4. 改变工作目录
	// 5. 得到并关闭文件描述符
	// 6. 固定文件描述符 0, 1, 2 到 /dev/null
	int fd0 = open("/dev/null", O_RDWR);
	int fd1 = dup(0);
	int fd2 = dup(0);
}

这样,我们 daemon_init 就写好了,可以用这个函数来初始化一个守护进程了,我们来测试测试。

测试 daemon_init 初始化函数

我们编写一个 printlg.c 循环向文件中输出信息:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

void daemon_init(void);

int main(void) {
	// 初始化守护进程
	daemon_init();

	// 守护进程逻辑
	char *msg = "I'm printlg process...\n" ;
	int msg_len = strlen(msg);

	int fd = open("/tmp/test_printlg.log", O_RDWR | O_CREAT | O_APPEND, 0666);
	if(fd < 0) {
		printf("open /tmp/test_printlg.log fail.\n");
		exit(1);
	}

	while(1) {
		// 每隔 3s 输出 msg 到 /tmp/test_printlg.log 文件中
		write(fd, msg, msg_len);
		sleep(3);
	}

	close(fd);

	return 0;
}

完整的代码参考:printlg.c,下面我们来测试这个守护进程能够工作。

测试 printlg.c 守护进程

编译

gcc printlg.c -o printlg

运行

./printlg

结果最终写入到 /tmp/test_printlg.log 文件中,我们每隔 3 s 查看这个文件的内容,发现内容在不断增多的:

cat /tmp/test_printlg.log

# 结果
I'm printlg process...
I'm printlg process...
I'm printlg process...

到此为止,一个守护进程就写好了,但是很多的守护进程都可以开机自启动,我们的可不可以呢?

让守护进程开机自启动

很多的守护进程都设置了开机自启动,我们也来让 printlg 能够开机自启动,先来了解自启动的原理。

每个系统启动级别的守护进程分别在 /etc/rcN.d 下,比如我的图形界面的启动级别是 5,那么在这个启动级别下自动运行和禁止启动守护进程都在 /etc/rc5.d 下。这里我的 ubuntu 系统的守护进程目录是 /etc/rcN.d,如果你是其他的 Linux 可能会不太一样,你可以使用 whereis rc[N].d 来看看具体的目录位置,不要死记硬背。

这是我的 ubuntu 的 /etc/rc5.d 下的守护进程: daemon

知道了守护进程的位置,现在就可以把 printlg 放在 /etc/rc5.d/ 下,并且还要改名称,因为系统需要根据指定的名称来使用 for 循环来启动或者关闭每个程序,命名规则如下:

  1. S[num][name]:启动守护进程 name,例如:S01printlg
  2. K[num][name]:禁止启动守护进程 name,例如:K01printlg

我们这里肯定要启动了,所以将 printlg 命名为 S01printlg

mv printlg /etc/rc5.d/S01printlg

之后我们重启机器,再次查看 /tmp/test_printlg 文件,就可以看到服务已经启动了,这样守护进程就成功自启动啦。

结语

这次我们学习了如何在 Linux 编写一个守护进程,守护进程其实就是一个后台的服务程序,学习如何在 Linux 创建服务程序还是非常有必要的,因为我们无时无刻都在使用很多系统提供的服务,了解原理以后再使用 Linux 的服务会更加得心应手。

最后,感谢你的阅读,我们下次再见 :)

欢迎关注我的公众号:「cdeveloper」

cdeveloper at 08/04/17