Linux 守护进程

1. 守护进程概述

        守护进程, 也就是通常所说的 daemon 进程, 是 Linux 中的后台服务进程。 它是一个生存期较长的进程, 通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 守护进程常常在系统引导载入时启动, 在系统关闭时终止。 Linux 有很多系统服务, 大多数服务都是通过守护进程实现的。 同时, 守护进程还能完成许多系统任务, 例如, 作业规划进程 crond、 打印进程 lqd 等(这里的结尾字母 d 就是 daemon 的意思)。

      由于在 Linux 中, 每一个系统与用户进行交流的界面称为终端, 每一个从此终端开始运行的进程都会依附于这个终端, 这个终端称为这些进程的控制终端, 当控制终端被关闭时,相应的进程都会自动关闭。 但是守护进程却能够突破这种限制, 它从被执行开始运转, 直到接收到某种信号或者整个系统关闭时才会退出。 如果想让某个进程不因为用户、 终端或者其他的变化而受到影响, 那么就必须把这个进程变成一个守护进程。 可见, 守护进程是非常重要的。


2. 编写守护进程

     编写守护进程看似复杂, 但实际上也是遵循一个特定的流程, 只要将此流程掌握了, 就能很方便地编写出自己的守护进程。下面就分 4 个步骤来讲解怎样创建一个简单的守护进程。在讲解的同时, 会配合介绍与创建守护进程相关的几个系统函数, 希望读者能很好地掌握。
(1) 创建子进程, 父进程退出。

      这是编写守护进程的第一步。 由于守护进程是脱离控制终端的, 因此, 完成第一步后就会在 shell 终端造成一种程序已经运行完毕的假象, 之后的所有工作都在子进程中完成, 而用户在 shell 终端则可以执行其他的命令, 从而在形式上做到与控制终端的脱离。
      到这里, 有心的读者可能会问, 父进程创建了子进程后退出, 此时该子进程不就没有父进程了吗? 守护进程中确实会出现这么一个有趣的现象: 由于父进程已经先于子进程退出,就会造成子进程没有父进程, 从而变成一个孤儿进程。 在 Linux 中, 每当系统发现一个孤儿进程时, 就会自动由 1 号进程(也就是 init 进程) 收养它, 这样, 原先的子进程就会变成 init进程的子进程。

注意:在不同的Linux的发行版,出来孤儿进程给的进程号,不一样,也就是可能不是1号进程,大多是用户进程

其关键代码如下:

pid = fork();
if (pid > 0)
{
exit(0); /* 父进程退出 */
}

(2) 在子进程中创建新会话。

        这个步骤是创建守护进程最重要的一步, 虽然实现非常简单, 但意义却非常重大。 在这里使用的是系统函数 setsid(), 在具体介绍 setsid()之前, 读者首先要了解两个概念: 进程组和会话期。
     进程组。 进程组是一个或多个进程的集合。 进程组由进程组 ID 来唯一标识。 除了进程号(PID) 之外, 进程组 ID 也是一个进程的必备属性。每个进程组都有一个组长进程, 其组长进程的进程号等于进程组 ID, 且该进程 ID 不会因组长进程的退出而受到影响,组长结束会宣称新组长,进程组的结束是最后一个进程的结束。
     会话期。 会话组是一个或多个进程组的集合。 通常, 一个会话开始于用户登录, 终
止于用户退出, 在此期间该用户运行的所有进程都属于这个会话期。 进程组和会话
期之间的关系如图  所示。

接下来就可以具体介绍 setsid()的相关内容。
① setsid()函数的作用。 setsid()函数用于创建一个新的会话组, 并担任该会话组的组长。
调用 setsid()有以下 3 个作用:
 让进程摆脱原会话的控制。
 让进程摆脱原进程组的控制。
 让进程摆脱原控制终端的控制。

那么, 在创建守护进程时为什么要调用 setsid()函数呢? 读者可以回忆一下创建守护进
程的第一步, 在那里调用了 fork()函数来创建子进程再令父进程退出。 由于在调用 fork()函
数时, 子进程全盘复制了父进程的会话期、 进程组和控制终端等, 虽然父进程退出了, 但原
先的会话期、 进程组和控制终端等并没有改变, 因此, 还不是真正意义上的独立。 而 setsid()
函数能够使进程完全独立出来, 从而脱离所有其他进程的控制。


② setsid()函数格式。 表 列出了 setsid()函数的语法要点。

(3) 改变当前目录为根目录。

    这一步也是必要的步骤。 使用 fork()创建的子进程继承了父进程的当前工作目录。 由于在进程运行过程中, 当前目录所在的文件系统(如“/mnt/usb”等) 是不能卸载的, 这对以后的使用会造成诸多的麻烦(如系统由于某种原因要进入单用户模式)。 因此, 通常的做法是让“/” 作为守护进程的当前工作目录, 这样就可以避免上述问题。 当然, 如有特殊需要, 也可以把当前工作目录换成其他的路径,如/tmp。 改变工作目录的常见函数是 chdir()。


(4) 重设文件权限掩码。

      文件权限掩码是指屏蔽掉文件权限中的对应位。 例如, 有一个文件权限掩码是 050, 它就屏蔽了文件组拥有者的可读与可执行权限。 由于使用 fork()函数新建的子进程继承了父进程的文件权限掩码, 这就给该子进程使用文件带来了诸多的麻烦。因此, 把文件权限掩码设置为 0, 可以大大增强该守护进程的灵活性。 设置文件权限掩码的函数是 umask()。 在这里, 通常的使用方法为 umask(0)。


(5) 关闭文件描述符。

     同文件权限掩码一样, 用 fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。 这些被打开的文件可能永远不会被守护进程读或写, 但它们一样消耗系统资源, 而且可能导致所在的文件系统无法被卸载。在上面的第(2) 步之后, 守护进程已经与所属的控制终端失去了联系, 因此, 从终端输入的字符不可能达到守护进程, 守护进程中用常规方法(如 printf()) 输出的字符也不可能在终端上显示出来。

所以, 文件描述符为 0、 1 和 2 的 3 个文件(常说的输入、 输出和报错这 3 个文件) 已经失去了存在的价值, 也应被关闭。 通常按如下方式关闭文件描述符:
 

for(i = 0; i < MAXFILE; i++)
{
close(i);
}

这样, 一个简单的守护进程就建立起来了。 创建守护进程的流程图如图  所示。

下面是实现守护进程的一个完整实例,该实例首先按照以上的创建流程建立了一个守护进程, 然后让该守护进程每隔 10s 向日志文件/tmp/daemon.log 写入一句话。
 

/* daemon.c 创建守护进程实例 */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
	pid_t pid;
	int i, fd;
	char *buf = "This is a Daemon\n";
	pid = fork(); /* 第一步 */
	if (pid < 0)
	{
		printf("Error fork\n");
		exit(1);
	}else if (pid > 0)
	{
		exit(0); /* 父进程退出 */
	} 
	setsid(); /* 第二步 */
	chdir("/"); /* 第三步 */
	umask(0); /* 第四步 */
	for(i = 0; i < getdtablesize(); i++) /* 第五步 */
	{
		close(i);
	} /* 这时创建完守护进程, 以下开始正式进入守护进程工作 */
	while(1)
	{
		if ((fd = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0)
		{
			printf("Open file error\n");
			exit(1);
		}
		write(fd, buf, strlen(buf) + 1);
	close(fd);
	sleep(10);
	}
	exit(0);
}


该程序下载到开发板上, 可以看到该程序每隔 10s 就会在对应的文件中输入相关内容,
并且使用 ps 可以看到该进程在后台运行, 结果如下:
$ tail -f /tmp/daemon.log
This is a Daemon
This is a Daemon
This is a Daemon
This is a Daemon

$ps -ef|grep daemon
76 root 1272 S ./daemon
85 root 1520 S grep daemon


3. 守护进程的输出消息处理

      读者在前面编写守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此, 不能像其他普通进程一样将错误信息输出到控制终端来通知程序员, 即使使用 gdb也无法正常调试。 那么, 守护进程的进程要如何调试呢? 一种通用的办法是使用 syslog 服
务, 将程序中的出错信息输入到系统日志文件中(如“/var/log/messages”), 从而可以直观地看到程序的问题所在(“/var/log/message” 系统日志文件只能由拥有 root 权限的超级用户查看。 在不同 Linux 发行版本中, 系统日志文件路径全名可能有所不同, 例如, 可能是
“/var/log/syslog”)。syslog 是 Linux 中的系统日志管理服务, 通过守护进程 syslogd 来维护。 该守护进程在启动时会读一个配置文件“/etc/syslog.conf”, 该文件决定了不同种类的消息会发送到何处。


例如, 紧急消息可被送到系统管理员并在控制台上显示, 而警告消息则可被记录到一个文件中。
该机制提供了 3 个 syslog 相关函数, 分别为 openlog()、 syslog()和 closelog(), 下面就分别介绍这 3 个函数。
1) syslog 相关函数说明

openlog()函数用于打开系统日志服务的一个链接;

syslog()函数用于向日志文件中写入消息, 在这里可以规定消息的优先级、 消息输出格式等;

closelog()函数用于关闭系统日志服务的链接。

 (1)openlog()函数语法要点

(2)列出了 syslog()函数的语法要点   ...表示变量的数量可变

实例:    syslog(LOG_NOTICE,"I LOVE YOU %dyear\n",10000);

(3)列出了 closelog()函数的语法要点

3) 使用实例
这里将上一个示例程序用 syslog 服务进行重写, 其中有区别的地方用加粗的字体表示,
源代码如下:
 

/* syslog_daemon.c 利用 syslog 服务的守护进程实例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <syslog.h>
int main()
{
	pid_t pid, sid;
	int i, fd;
	char *buf = "This is a Daemon\n";
	pid = fork(); /* 第一步 */
	if (pid < 0)
	{
		printf("Error fork\n");
		exit(1);
	}else if (pid > 0)
	{
		exit(0); /* 父进程退出 */
	} /* 打开系统日志服务, openlog */
	openlog("daemon_syslog", LOG_PID, LOG_DAEMON);
	if ((sid = setsid()) < 0) /* 第二步 */
	{
		syslog(LOG_ERR, "%s\n", "setsid");
		exit(1);
	} 
	if ((sid = chdir("/")) < 0) /* 第三步 */
	{
		syslog(LOG_ERR, "%s\n", "chdir");
		exit(1);
	} 
	umask(0); /* 第四步 */
	for(i = 0; i < getdtablesize(); i++) /* 第五步 */
	{
	close(i);
	} /* 这时创建完守护进程, 以下开始正式进入守护进程工作 */
	while(1)
	{
		if ((fd = open("/tmp/daemon.log", O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0)
		{
			syslog(LOG_ERR, "open");
			exit(1);
		} 
		write(fd, buf, strlen(buf) + 1);
		close(fd);
		sleep(10);
	} 
	closelog();
	exit(0);
}

读者可以尝试用普通用户的身份执行此程序。

由于这里的 open()函数必须具有 root 权限,
因此, syslog 会将错误信息写入到系统日志文件(如“/var/log/messages”) 中,

但是对于ubuntu来说,日志文件在/var/log/syslog中

cat /var/log/syslog

结果如下:
Jan 30 18:20:08 localhost daemon_syslog[612]: open

 

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值