Linux系统编程---精灵进程与守护进程
1、前言
精灵进程又称守护进程、后台进程,在英文中称为 daemon 进程。精灵进程是运行在一个相对干净的环境、不受终端影响、常驻内存的进程,和神话中的精灵一样,拥有不死不灭的特性,长期稳定提供某种功能或服务。
在Linux系统中,通过ps命令,可以看到许多以 -d 结尾的进程,它们大多都是守护进程,例如:
Snail@ubuntu:~/Desktop$ ps -ef | grep '.*d$'
root 376 1 0 19:45 ? 00:00:01 /lib/systemd/systemd-journald
root 394 1 0 19:45 ? 00:00:00 vmware-vmblock-fuse /run/vmblock-fuse -o rw,subtype=vmware-vmblock,default_permissions,allow_other,dev,suid
root 440 1 0 19:45 ? 00:00:01 /lib/systemd/systemd-udevd
systemd+ 687 1 0 19:45 ? 00:00:00 /lib/systemd/systemd-oomd
systemd+ 688 1 0 19:45 ? 00:00:00 /lib/systemd/systemd-resolved
systemd+ 701 1 0 19:45 ? 00:00:00 /lib/systemd/systemd-timesyncd
root 725 1 0 19:45 ? 00:00:00 /usr/sbin/blkmapd
root 727 1 0 19:45 ? 00:00:00 /usr/sbin/nfsdcld
root 728 1 0 19:45 ? 00:00:00 /usr/sbin/rpc.idmapd
root 738 1 0 19:45 ? 00:00:01 /usr/bin/vmtoolsd
root 788 1 0 19:45 ? 00:00:00 /usr/sbin/acpid
root 803 1 0 19:45 ? 00:00:00 /usr/sbin/irqbalance --foreground
root 834 1 2 19:45 ? 00:00:08 /usr/lib/snapd/snapd
root 844 1 0 19:45 ? 00:00:00 /lib/systemd/systemd-logind
root 847 1 0 19:45 ? 00:00:00 /usr/libexec/udisks2/udisksd
colord 955 1 0 19:45 ? 00:00:00 /usr/libexec/colord
root 1097 1 0 19:45 ? 00:00:00 /usr/sbin/cups-browsed
statd 1109 1 0 19:45 ? 00:00:00 /sbin/rpc.statd
root 1112 1 0 19:45 ? 00:00:00 /usr/sbin/rpc.mountd
root 1312 1 0 19:45 ? 00:00:00 /usr/libexec/upowerd
root 1318 1 10 19:45 ? 00:00:34 /usr/libexec/packagekitd
Snail 1635 1522 0 19:48 ? 00:00:00 /usr/libexec/gvfsd
Snail 1848 1522 0 19:49 ? 00:00:00 /usr/libexec/gsd-keyboard
Snail 1875 1522 0 19:49 ? 00:00:00 /usr/libexec/gsd-smartcard
Snail 1880 1522 0 19:49 ? 00:00:00 /usr/libexec/gsd-sound
root 2407 1 4 19:49 ? 00:00:05 /usr/libexec/fwupd/fwupd
有许多程序或服务理应成为这种“不死”的守护进程,比如提供系统网络服务的核心程序systemd-networkd,只要系统需要基于TCP/IP协议栈进行网络通信,它就应该一直常驻内存,永不退出。
成为守护进程的关键在于使进程运行在一个相对独立、干净的环境,尽量不受各种事件的影响(除非断电、关机),接下来讲着重讲解守护进程相关的基础知识及守护进程创建的流程。
其中,章节2为守护进程的基础铺垫,注意讲解在创建守护进程时,可能遇到的一些知识储备,在第三章主要讲解守护进程的创建流程及参考代码。
2、守护进程的基础铺垫
2.1、基本概念
在创建守护进程的过程中,会跟如下诸多概念打交道,如进程组、前台进程组、后台进程组、会话、控制终端等,下图简略地反应了它们之间的关系:
系统调度的最小单位是进程,若干进程可组成一个进程组,若干进程组可组成一个会话,可见这几个概念都只是进程的组织方式,之所以要构造进程组和会话,其根本目的是为了便于发送信号:通过给进程组或会话发送信号,便可使得其内所有的进程均可收到信号。
控制终端指的是跟会话相关联的登录窗口程序,或伪终端程序(比如gnome-terminal),这些程序一般都有关联的输入设备,一般是键盘。可以使用控制终端设备来产生输入数据或信号,进而影响会话中的前台进程组,也可通过挂断操作来影响会话中的后台进程组。
2.2、进程组
①、什么是进程组
进程是系统中的活跃个体,是系统的调度单位,进程就像现实世界中的活动的人,社会为了便于管理,一般都会将人归总到某一个集体中,比如公司、学校、组织等,系统中的进程也一样,他们可以按照实际所需进入某个进程组。进程组的好处在于,可以给组内的所有进程统一发送信号。
进程组API接口:
#include <sys/types.h>
#include <unistd.h>// 功能: 将进程pid的所在进程组设置为pgid
// 如果pid == 0,则设置本进程
// 如果pgid == 0,等价于pgid == pid
// 注意:若进程pid与进程组pgid不在同一会话内,会设置失败
int setpgid(pid_t pid, pid_t pgid); // 把进程 pid 设置到 进程组ID为pgid 的进程组中// 功能: 获取进程pid所在进程组ID
pid_t getpgid(pid_t pid); // 获取进程 pid 所属的进程组//函数setpgid( )使用细节:
// 将指定进程123加入进程组7799中
// 注意,进程组7799必须存在且与进程123同处相同的会话
setpgid(123, 7799);// 将本进程加入进程组7799中
// 注意,进程组7799必须存在且与本进程同处相同的会话
setpgid(0, 7799);// 创建一个ID等于本进程PID的进程组
// 并将本进程置入其中,成为进程组组长
setpgid(0, 0); // 自己创建一个进程组并把自己设置组长
②、前台进程组
一般而言,进程从终端启动时,会自动创建一个新进程组,并且该进程组内只包含这个创始进程,而其后代进程默认都将会被装载在该进程组内部,这个进程组被称为前台进程组。
前台进程组最大的特点是:可以接收控制终端发来的信号,所谓控制终端,一般就是指标准输入设备键盘。
以下代码显示了一个子进程脱离父进程的前台进程组,从而脱离控制终端的过程:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>void display_process_id()
{printf("本进程ID:%d\t", getpid());printf("父进程ID:%d\t", getppid());printf("进程组ID:%d\t", getpgid(0));printf("所在会话ID:%d\n", getsid(0));
}int main()
{if(fork() == 0){display_process_id();// 将本进程的所属进程组,设置为等于本进程ID// 即:创建一个仅包含自身的进程组并将自身置入其中if(setpgid(0, 0) < 0)perror("setpgid() failed");elseshowid();}pause();
}
通过上面的代码,使得子进程“自立门户”,不再与其父进程同处一个进程组,因此如果从控制终端按下 ctrl+c 时,那么这个快捷键触发的信号 SIGINT 只会发送给前台进程组,子进程将不受影响。
注意:在一个会话中,前台进程组只有一个,而后台进组可以有多个!!!
③、后台进程组
后台进程组,顾名思义,就是在后台运行的进程组。
在终端运行程序时,可以加个 & 来使得进程进入后台进程组:
# 使 a.out 在后台进程组中运行
Snail@ubuntu:~/Desktop$ ./a.out &
让一个进程在后台进程组中运行,通常是为了使其让出控制终端,不接受控制终端的输入和信号,但这并不意味着它不受控制终端控制(Cntl + C ),控制终端依然可向其发送挂断信号(kill -9 xxxx)。
上述后台进组进程 a.out 与即将要介绍的后台进程(或称精灵进程、守护进程)无关。
2.3、会话
会话(session)原本指的是一个登录过程中所产生的的所有进程组的总和,可以理解为一个登录窗口就是一个会话。但在伪终端中,每打开一个窗口实际上也是创建了一个会话,例如:
如上所述,会话可以理解为就是进程组的进程组,会话的作用可总结为:
- 可关联一个控制终端(比如键盘)设备,并从控制终端获得输入信息(键盘的输入、键盘快捷方式产生的信号)
- 可对会话内的所有进程组统一发送信号(关闭终端--> 相当于给该会话中的所有进程组发送挂断的信号)
- 可将控制终端的关闭动作,转换为触发挂断信号并发送给所有进程
当打开一个伪终端,或者打开一个远程登录工具输入账户密码的过程中,默认都调用了如下函数接口去创建一个新的会话。
#include <sys/types.h>
#include <unistd.h>pid_t setsid(void);
注意:
- 进程组组长不能调用该函数(组长没有权限去打开一个新的终端)。
- 新创建的会话没有关联控制终端,因此其内进程不受控制终端影响。
- 创建会话的进程,称为该会话的创始进程,创始进程有权捕获一个控制终端(在编写守护进程时通常需要避免),会话的其余成员进程无权获得控制终端。
2.4、控制终端
控制终端通常会关联一个输入设备,给前台进程组发送数据或信号,平常使用的许多信号快捷键,就是通过控制终端发送给前台进程组内的进程的:
快捷键 | 对应信号 |
ctrl + c | SIGINT |
ctrl + \ | SIGQUIT |
ctrl + z | SIGSTOP |
控制终端不仅可以向前台进程组发送数据,也能向整个会话发送挂断信号(即SIGHUP),默认情况下收到SIGHUP的进程会被终止,因此,为了避免被控制终端“误杀”,常驻内存的守护进程的必备步骤须包括忽略掉SIGHUP、脱离控制终端、避免再次获取控制终端等操作。以下是示例代码:
// 忽略信号SIGHUP
signal(SIGHUP, SIG_IGN);// 脱离控制终端(新建一个会话)
setsid();// 避免会话再次关联控制终端(退出会话创始进程)
if(fork() > 0)exit(0);
3、守护进程的创建流程
3.1、忽略SIGHUP
由于终端的关闭会触发SIGHUP并发送给终端所关联的会话的所有进程,而一开始进程尚未脱离原会话,因此应尽早忽略该信号,避免被挂断信号误杀。
实现代码如下:
// 1,忽略挂断信号SIGHUP,防止被终端误杀
signal(SIGHUP, SIG_IGN);
3.2、产生子进程
从终端(不管是远程登录窗口还是本地伪终端)启动的进程所在的会话都关联了控制终端,而控制终端会有各种数据或信号的输入干扰,为了避开这些干扰,需要脱离控制终端,而脱离控制终端的简单做法就是新建一个新的、没有控制终端的会话,但创建一个新会话的进程必须是非进程组组长,但Linux系统中,从终端启动的进程默认就是其所在进程组的组长,因此摆脱这一困境的简单做法就是让其产生一个子进程,退出原进程(即父进程)并让子进程继续下面的步骤即可。
实现代码如下:
// 2,退出父进程(原进程组组长),为能成功创建新会话做准备
if(fork() > 0)exit(0);
3.3、创建新会话
创建新会话,脱离原会话,脱离控制终端。
实现代码如下:
// 3,创建新会话,脱离原会话,脱离控制终端。
setsid();
3.4、产生孙子进程
此时的进程是其所在的会话的创始进程,而创始进程拥有可以再次关联的控制终端的权限,为避免此种情况的发生,最简单的做法就是退出当前创始进程,改由其子进程(非创始进程)继续完成成为守护进程的使命。
实现代码如下:
// 4,断绝重新关联控制终端的可能性
if(fork() > 0)exit(0);
3.5、进入新进程组
虽然此时进程的父进程、祖父进程已经退出,但进程组是一直都在的,且处于新会话中的孙子进程一直都在其祖父进程的进程组之中,而进程组是可以传递信号的,因此为了与任何方面脱离关系,应“自立门户”创建新进程组,并将自身置入其中。
实现代码如下:
// 5,脱离原进程组,创建并进入只包含自身的进程组
setgpid(0, 0);
3.6、关闭文件资源
文件资源是可以在父子进程之间代际相传的,这其中也包括了标准输入输出文件,而作为守护进程,是一种在后台运行的程序,运行过程中一般无需交互,若有消息需要输出一般会以系统日志的方式输出到指定日志文件中。因此,为了节约系统资源,也为了避免不必要的逻辑谬误,守护进程一般都需要将所有从父辈进程继承下来的文件全部关闭。
实现代码如下:
// 6,关闭父辈继承下来的所有文件
for(int i=0; i<sysconf(_SC_OPEN_MAX); i++) // 通过遍历把已经打开所有描述符进行逐个关闭close(i);
3.7、关闭文件权限掩码
在Linux系统中创建一个新文件时,可以通过相关的函数参数指定文件的权限,比如:
// 试图在file.txt不存在的情况下,创建一个权限为0777的文件
int fd = open("file.txt", O_CREAT|O_RDWR, 0777); // 实际则是 0777 - umask = 0775
但其实被创建出来的文件的权限并非代码中指定的权限,该权限与系统当前的文件权限掩码做位与操作之后的值才是文件真正的权限,我们可以通过命令 umask 来查看当前系统默认的文件权限掩码的值:
Snail@ubuntu:~/Desktop/process$ umask
0002
Snail@ubuntu:~/Desktop/process$
因此,上述创建的文件的权限最后不是0777,而是0775:
Snail@ubuntu:~/Desktop/process$ ls -l
-rwxrwxr-x 1 Snail Snail 0 Apr 23 20:57 file.txt
Snail@ubuntu:~/Desktop/process$
为了让守护进程在后续工作过程创建文件时指定的权限不受系统文件权限掩码干扰,可以将umask设置为0。
实现代码如下:
// 7,避开系统文件权限掩码的干扰
umask(0);
3.8、切换进程工作路径
任何一个进程都有一个当前工作路径,从终端启动的进程的工作路径就是启动时终端所在的系统路径。以下代码可以输出进程当前所在路径:
#include <stdio.h>
#include <unistd.h>int main(void)
{printf("%s\n", getcwd(NULL, 0));
}
提醒:当一个进程的工作路径被卸载时,进程也会随时消亡。守护进程为了避免此种情况发生,最简单的做法就是将自身的工作路径切换到一个无法被卸载的路径下,比如根目录。
实现代码如下:
// 8,避免所在路径被卸载
chdir("/");
守护进程创建的最后一步是修改进程运行的工作路径,因此在守护进程中所有访问文件的路径建议都使用绝对路径,避免打开、创建文件失败。
3.9、守护进程代码实例
#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>int main(void)
{pid_t a;int max_fd, i;/*********************************************
1. ignore the signal SIGHUP, prevent the
process from being killed by the shutdown
of the present controlling termination
**********************************************/signal(SIGHUP, SIG_IGN);/***************************************
2. generate a child process, to ensure
successfully calling setsid()
****************************************/a = fork();if(a > 0)exit(0);/******************************************************
3. call setsid(), let the first child process running
in a new session without a controlling termination
*******************************************************/setsid();/*************************************************
4. generate the second child process, to ensure
that the daemon cannot open a terminal file
to become its controlling termination
**************************************************/a = fork();if(a > 0)exit(0);/*********************************************************
5. detach the daemon from its original process group, to
prevent any signal sent to it from being delivered
**********************************************************/setpgrp();/*************************************************
6. close any file descriptor to release resource
**************************************************/max_fd = sysconf(_SC_OPEN_MAX);for(i=0; i<max_fd; i++)close(i);/******************************************
7. clear the file permission mask to zero
*******************************************/umask(0);/****************************************
8. change the process's work directory,
to ensure it won't be uninstalled
*****************************************/chdir("/");// Congratulations! Now, this process is a DAEMON!pause();return 0;
}