1.0 简介
** 守护进程**(daemon):daemon原指是希腊神话中的守护神,被用于计算机术语指守护进程。
1.1 特点:
1:生存期长
2:常在系统引导装入时启动,在系统关闭时终止
3:守护进程没有控制终端,是在后台运行的。
1.2 显示unix进程状态: 可以用ps -ajx命令打印系统中各进程的状态。
-a: 显示其他用户所拥有的进程状态
-x:显示没有控制终端的进程状态
-j: 显示与作业相关的信息,会话ID,进程组id,控制终端,以及终端进程组id
1.3.系统进程
系统进程依赖于操作系统实现,父进程id为0的各进程通常是内核进程,它们作为系统引导装入过程的一部分而启动。(init除外,init是引导装入时启动的用户层次的命令)
内核进程通常存在于系统的整个生命周期中,它们以root权限运行,无控制终端,无命令行。
ps输出的值中,方括号包起来的是内核守护进程的名字。
1.4 守护进程
对于需要在进程上下文执行工作但却不被用户层进程上下文调用的每一个内核组件,通常有它自己的内核守护进程。 例如在linux中
kswapd :进程也被称为内存换页守护进程。它支持虚拟内存子系统在经过一段时间后将脏页面写回磁盘来回收这些页面。
flush :进程在可用内存达到设置的最小阈值时,将脏页面冲洗至磁盘。它也定期地将脏页面冲洗回磁盘来减少在系统出现故障时发生的数据丢失。 多个冲洗守护进程可以同时存在,每个写回设备都有一个冲洗守护进程。
sync_supers:进程定期将文件系统元数据冲洗至磁盘
jbd:进程帮助实现了ext4文件系统中的日志功能。
init:进程1通常是init, 它是一个系统守护进程,主要负责秋冬各运行层次特定的系统服务。这个服务通常是在它们自己拥有的守护进程的帮助下实现的。
rpcbind: 守护进程提供将远程过程调用(remote procedure call, rpc)程序号映射为网络端口号的服务。
rsyslogd: 进程可以被由管理员启用的将系统消息记入日志的任何程序使用。可以在一台实际的控制台上打印这些消息,也可以将他们写入一个文件中。
inetd: 守护进程监听系统网络接口,以便获得各种对网络服务进程的请求。
nfsd, nfsiod, lockd,rpciod,rpc.idmapd,rpc.statd和rpc.mountd: 守护进程提供对网络文件系统(NFS)的支持。
nfsd, nfsiod, lockd,rpciod属于内核守护进程
rpc.idmapd,rpc.statd和rpc.mountd: 属于用户级守护进程
cron: 定时任务守护进程 atd:也是定时任务守护进程,但每个任务只执行一次,而不会反复执行。
cupsd 打印假脱机进程,它处理对系统提出的各个打印请求。 sshd 守护进程提供了安全的远程登录和执行设施
所有的守护进程都没有控制终端,其终端名设置为问号。
内核守护进程以无控制终端方式启动,用户层守护进程缺少控制终端,可能是守护进程调用了setsid的结果,大多数用户层守护进程都是进程组的组长进程,以及会话的首进程,而且是这些进程组和会话中的唯一进程(rsyslogd是例外)。用户层守护进程的父进程是init进程。
2. 编程规则
编写守护进程程序需要遵循一些基本规则,以防止产生不必要的交互作用。按照编写规则编写守护进程函数daemonize。
2.1
调用umask设置文件模式创建屏蔽字为一个已知值(通常为0),屏蔽字是用来禁用某个权限的。如果是4禁止读,如果是2禁止写,如果是1禁止执行。
1.继承了文件模式创建屏蔽字的守护进程,就会失去创建拥有上述能力的文件的权限。
2.如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值,可能会更明智,因为库函数可能不允许调用者通过一个显示的函数参数来设置权限。
2.2
调用fork,然后使父进程exit。
这样做实现了几点:
1.如果守护进程是shell命令启动的,则父进程终止会让shell命令认为已经执行完毕。
2.虽然子进程继承父进程的进程组id,但是获得一个新的进程id,这保证了子进程不是进程组的组长进程。这是setsid调用的先决条件。
2.3
调用setsid创建一个新会话,执行三个步骤
a. 成为新会话的首进程
b.成为一个新进程组的组长进程
c. 没有控制终端
这三个步骤主要是为了脱离可能的终端会话,成为无终端进程。
为了避免取得控制终端的另一个办法是,无论何时打开一个终端设备都一定要指定O_NOCTTY。
2.4
1: 将当前工作目录更改为根目录。从父进程继承来的工作目录可能在一个挂载的文件系统中。守护进程通常在系统再引导之前就存在,如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
2: 或者守护进程还可能会把当前工作目录更改到某个指定位置,并且在此位置进行它们的全部工作。(例如行式打印机假脱机守护进程就可能将其工作目录更改到它们的spool目录上)。
2.5
关闭不再需要的文件描述符。 这使得守护进程不再持有父进程继承而来的任何文件描述符(父进程可能是shell进程,或某个其他进程)。可以使用open_max和getrlimit函数获取最高文件描述符值,并关闭直到该值的所有描述符。
2.6
某些守护进程通常会重定向其标准输入(文件描述符0)、标准输出(文件描述符1)和标准错误(文件描述符2)到/dev/null
这样做有以下几个目的:
-
避免阻塞等待输入:标准输入(stdin, 文件描述符0)通常用于接收用户或程序输入。守护进程一般不需要从标准输入读取数据,因此将其重定向到
/dev/null可以防止进程因为等待输入而阻塞,从而保证了守护进程能够持续运行而不受外部输入的影响。 -
抑制输出:标准输出(stdout, 文件描述符1)和标准错误(stderr, 文件描述符2)是程序输出信息的两个通道。守护进程通常处理后台服务任务,不希望其输出干扰用户界面或日志系统。重定向这两个流到
/dev/null可以丢弃所有输出,使得守护进程在执行时不产生任何可见的输出,保持系统日志的清洁和减少不必要的磁盘空间占用。 -
避免资源占用和潜在错误:如果守护进程意外地尝试向标准输出或标准错误写入信息,而这些输出没有被妥善处理(比如没有被重定向或记录),可能会导致输出丢失,或者在某些情况下,如果输出被定向到了一个满的文件系统或不可写的设备,还可能导致进程收到错误信号甚至崩溃。重定向到
/dev/null确保了即使有这样的意外写入操作,也不会引发问题,因为/dev/null会默默地丢弃所有写入它的数据,而不会报错。
这样任何一个试图读标准输入,写标准输出或写标准错误的库例程都不会对守护进程产生任何影响。守护进程既不向终端输出也不接收终端的输入。就算守护进程是从交互式会话启动的,但是守护进程是在后台运行,登录会话的终止也不影响守护进程继续执行。如果有多用户在同一终端设备上登录,那我们肯定不想突然看到deamons的输出,也不想让守护进程默默读取我们的输入。
以下是初始化守护进程的代码
//初始化一个守护进程
#include "../../apue.3e/apue.h"
#include <pthread.h>
#include <fcntl.h>
#include<sys/resource.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <command_name>\n", argv[0]);
exit(EXIT_FAILURE);
}
daemonize(argv[1]); // 调用daemonize函数,传入命令名作为参数
// 注意:此处假设daemonize函数执行后,守护进程的具体任务逻辑需要你自己添加
// 例如,你可以添加一个while循环来持续执行某些操作
while(1) {
sleep(1); // 示例:简单休眠,代替实际的任务逻辑
}
return 0;
}
void
daemonize(const char *cmd)
{
int i,fd0,fd1,fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
//clear file creation mask
umask(0);
//获取最大的文件描述符
if(getrlimit(RLIMIT_NOFILE,&rl)<0)
err_quit("%s: can't get file limit",cmd);
//创建新进程,成为新会话的新进程组的组长进程,脱离控制终端(TTY)
if((pid = fork())<0)
err_quit("%s: can't fork",cmd);
else if(pid !=0)
exit(0);
setsid();
sa.sa_handler = SIG_IGN; //表示忽略sa_handler信号
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 directory to /",cmd);
//关闭所有文件描述符
if(rl.rlim_max == RLIM_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);
if(fd0 !=0 || fd1 != 1 || fd2 !=2){
printf("unexpected file descriptors %d %d %d",fd0,fd1,fd2);
exit(1);
}
}
编译命令:
gcc -Wall -o mydaemon mydaemon.c
由于守护进程运行在后台,不会有内容输出,使用ps -efj查看进程,没有pid的那个孤儿进程就是守护进程。