守护进程

GuangYing Lv2

编程规则

编写守护进程程序时需遵循的一些基本规则,以防止产生不必要的交互作用:

  • 调用umask将文件模式创建屏蔽字设置为一个已知值(通常为0)
    • 由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限
      • 若守护进程要创建文件,则可能要设置特定的权限
      • 继承的文件模式创建屏蔽字可能会屏蔽权限导致无法发挥作用
    • 若守护进程调用的库函数创建了文件
      • 则将文件模式创建屏蔽字设置为一个限制性更强的值可能会更明智
      • 因为库函数可能不允许调用者通过一个显式的函数参数来设置权限
  • 调用fork,然后使父进程exit
    • 若守护进程是作为一条简单的shell命令启动的
      • 则父进程终止会让shell认为这条命令已经执行完毕
    • 虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID
      • 这保证了子进程不是一个进程组的组长进程
      • 这是下面调用setsid的先决条件
  • 调用setsid创建一个新会话
    • 然后执行下列步骤,使调用进程:
    • 成为新会话的首进程
    • 成为一个新进程组的组长进程
    • 没有控制终端
  • 将当前工作目录更改为根目录
    • 从父进程处继承而来的当前工作目录可能在一个挂载的文件系统中
    • 因为守护进程通常在系统再引导之前是一直存在的
      • 所以如果守护进程的当前工作目录在一个挂载的文件系统,则该文件系统就不能被卸载
    • 或者,某些守护进程还可能会把当前工作目录改到某个指定位置,并在此位置进行它们的全部工作
  • 关闭不再需要的文件描述符
    • 使得守护进程不再持有从其父进程继承来的任何文件描述符(父进程可能是shell进程,或某个其他进程)
    • 可以使用open_max函数 或 getrlimit函数来判定最高文件描述符值,并关闭直到该值的所有描述符
  • 某些守护进程打开/dev/null使其具有文件描述符0、1、2
    • 这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果
      • 因为守护进程并不与终端设备相关联,所以输出无处显示,也无处接收输入
    • 即便守护进程是从交互式会话启动的,但守护进程运行在后台
      • 所以登录会话的终止并不影响守护进程

初始化为守护进程的程序调用:

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
#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void
daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;

//清除文件模式创建屏蔽字
umask(0);

//获取最高文件描述符值
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can`t get file limit", cmd);

//调用fork,然后使父进程exit
if ((pid = fork()) < 0)
err_quit("%s: can`t fork", cmd);
else if (pid != 0)
exit(0);
setsid();

//基于System V的系统中,再次调用fork终止父进程
//保证了该守护进程不是会话首进程
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can`t ignore SIGHUP", cmd);
if ((pid = fork()) < 0)
err_quit("%s: can`t fork", cmd);
else if (pid != 0)
exit(0);

//设置工作目录为根目录
if (chdir("/") < 0)
err_quit("%s: can`t change direstory to /", cmd);

//关闭所有的文件描述符
if (rl.rlim_max == RLTM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; i++)
close(i);

//设置文件描述符为/dev/null
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

//初始化日志文件
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
fd0, fd1, fd2);
exit(1);
}
}

出错记录

集中的守护进程出错记录设施:syslog

3种产生日志消息的方法:

  • 内核例程可以调用log函数
    • 任何一个用户进程都可以通过打开(open)并读取(read)/dev/log设备来读取这些消息
  • 大多数用户继承(守护进程)调用syslog(3)函数来产生日志消息
  • 无论用户进程是在此主机上,还是通过TCP/IP网络连接到此主机的其他设备上
    • 都可以把日志消息发向UDP端口514
    • 注:syslog函数从不产生这些UDP数据报
      • 它们要求产生此日志消息的进程进行显式的网络编程

syslogd守护进程启动时读取一个配置文件/etc/syslog.conf
该文件决定了不同种类的消息应送向何处

该设施的接口是syslog函数

1
2
3
4
5
6
7
8
9
10
11
#include <syslog.h>

void openlog(const char *ident, int option, int facility);

void syslog(int priority, const char *format, ...);

void closelog(void);

int setlogmask(int maskpri);

返回值:前日志记录优先级屏蔽字值

调用openlog是可选的
如果不调用,第一次调用syslog时自动调用openlog

closelog也是可选的,只是关闭曾被用于与syslogd守护进程通信的描述符

openlog的参数

  • ident参数:一般是进程的名称
    • 会被加至每则日志消息中
  • opention参数:指定各种选项的位屏蔽
  • facility参数:可以让配置文件说明
    • 来自不同设施的消息将以不同的方式进行处理

syslog产生一个日志消息
priority参数:是facility和level的组合
%m字符将被替换为与errno值对应的出错消息字符串

setlogmask用于设置进程的记录优先级屏蔽字
当设置了记录优先级屏蔽字时
各条消息除非已在记录优先级屏蔽字中进行了设置
否则不会被记录

logger命令是专门为以非交互方式运行的需要产生日志消息的shell脚本设计的

除了syslog,很多平台还提供了它的一种变体来处理可变参数列表

1
2
3
4
#include <syslog.h>
#include <stdarg.h>

void vsyslog(int priority, const char *format, va_list arg);

大多数syslog实现将使消息短时间处于队列中

  • 如果在此期间有重复消息到达,则不会被写到日志记录中
  • 而是输出一条类似:上条消息重复了N次 的消息

单实例守护进程

为了正常运作,某些守护进程会实现为:
在任一时刻只允许该守护进程的一个副本

文件和记录锁机制提供了一种方法

  • 保证了一个守护进程只有一个副本在运行
  • 如果每个守护进程创建一个有固定名字的文件,并在该文件的整体上加一把写锁
    • 则只允许创建一把这样的写锁
    • 在此之后创建写锁的尝试都会失败
  • 这向后续守护进程副本指明已有一个副本正在运行

文件和记录锁提供了一种方便的互斥机制:

  • 如果守护进程在一个文件的整体上得到一把写锁
    • 则该守护进程终止时,这把锁将被自动删除
      简化了复原所需的处理

守护进程惯例

UNIX系统中,守护进程遵循下列通用惯例:

  • 若守护进程使用锁文件,则该文件通常存储在/var/run目录中
    • 需要注意:守护进程可能需要有超级用户权限才能在此目录下创建文件
    • 锁文件的名字通常是name.pid
      • name为守护进程或服务的名字
  • 若守护进程支持配置选项,则配置文件通常存放在/etc目录中
    • 配置文件的名字通常是name.conf
      • name为该守护进程或服务的名字
  • 守护进程可用命令启动,但通常它们是由系统初始化脚本之一(/etc/rc*/etc/init.d/*)启动的
    • 如果在守护进程终止时,应当自动的重新启动它
      • 则可在/etc/inittab中为该守护进程包括respawn记录项
      • 这样init就将重新启动该守护进程
  • 若一个守护进程有一个配置文件,则当该守护进程启动时会读取该文件,但在此之后一般就不会再查看它
    • 若某个管理员更改了配置文件
      • 则该守护进程可能需要被停止,然后再重新启动
      • 以使配置文件的更改生效
    • 为避免此种麻烦
      • 某些守护进程将捕捉SIGHUP信号
      • 当它们接收到该信号时,重新读配置文件
    • 因为守护进程并不与终端相结合,所以没有理由期望接收SIGHUP
      • 所以守护进程可以安全的重复使用SIGHUP
  • 标题: 守护进程
  • 作者: GuangYing
  • 创建于 : 2025-03-08 22:22:24
  • 更新于 : 2025-03-08 22:25:44
  • 链接: http://quebo.cn/2025/03/08/守护进程/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。