**Linux 知识补充 -- 守护进程的编写及使用方法 - 02 守护进程的作用、用途、父进程标识的特点**
1652270 **冯舜**
## 进入子目录并编辑
如//2dir//, 进入 `02` 目录, 拷贝 `01` 目录的若干文件, 以此为基础编辑.
!![2dir,进入 02 目录并拷贝]
用两种方法实现 `test2`. 编辑 `test2.c`、`test2-1.c` 和 `makefile`, 如//2edit//.
!![2edit,编辑文件]
- 第一种实现方式是实现 SystemV 风格守护进程的推荐方法, 对应文件 `test2.c`:
+ 用 `fork` 创建一个子进程
+ 父进程退出
+ 子进程设置 SID, 成为 session leader
+ 子进程忽略或捕获 `SIGCHILD` 和 `SIGHUP` 信号
+ 子进程用 `fork` 创建孙进程, 之后子进程退出
+ 按需设置文件权限、工作目录
+ 按需重置输入输出的文件描述符(本例中未实现)
!!!
(https://stackoverflow.com/questions/31485204/why-fork-twice-while-daemonizing)
使用两次 `fork` 创建守护进程是一个最佳实践. 称两次 `fork()` 中的三层进程分别为父进程、子进程、孙进程, 则可以这样解释:
- 守护进程不能有控制终端. 否则控制终端可以用 `^C` 等控制字符控制守护进程的状态, 这是我们不希望发生的. 同一 session 的进程有相同的控制终端, 所以守护进程不能处于与开启它的进程 (有可能有一个控制终端) 同一 session 内, 必须开辟不同的 session.
- 守护进程不能是一个 session leader. 若 session leader 后续有打开 TTY 进行读写的操作, 将有可能让该 TTY 成为自己的控制终端.
- 所以, 子进程必须先执行 `setsid()` 使自己成为 session leader, 再 `fork` 出孙进程作为守护进程. 这样可保证前两条要求成立.
- session leader 不能执行 `setsid()`. 考虑到不是从终端中启动的情况, 父进程有可能是一个 session leader. 故父进程不直接 `setsid()` 后 `fork()` 出守护进程, 需要先 `fork()` 出非 session leader 的子进程, 再由子进程当 session leader, `fork()` 出孙进程.
- 父进程大多数情况由 bash 作为终端命令直接运行, 不宜开启新 session 脱离 bash 和终端的控制. 所以父进程不直接 `setsid()` 后 `fork()` 出守护进程.
- 第二种方法是直接执行 `daemon(1,1)` 系统函数. 该函数未覆盖上述的部分步骤, 故不推荐用这种方法. 对应文件 `test2-1.c`.
各文件源码如下:
~~~~~~~~~~~~~~~~C linenumbers
~~~~~~~~~~~~~~~~
[Listing [test2]: `test2.c` 的内容]
~~~~~~~~~~~~~~~~C linenumbers
~~~~~~~~~~~~~~~~
[Listing [test2-1]: `test2-1.c` 的内容]
~~~~~~~~~~~~~~~~makefile linenumbers
BIN = test2 test2-1 test2-redirect test2-sendsig
OBJ = $(addsuffix .o,$(BIN))
all : $(BIN)
.PHONY : clean
clean :
$(RM) $(BIN) $(OBJ) output.log
~~~~~~~~~~~~~~~~
[Listing [makefile]: `makefile` 的内容]
## 运行 `test2`
运行 `make` 进行构建后, 两个可执行文件生成成功, 代表两种实现方式.
执行 `./test2`, 如//2run1//:
!![2run1,执行 `test2`]
执行 `./test2-1`, 如//2run2//:
!![2run2,执行 `test2-1`]
可以发现, 直接执行程序, 得到正确输出, 但并没有阻塞 bash. 说明程序已经作为守护进程运行.
## 查看 `test2` 进程标识和父进程标识
尝试用 `jobs` 查看后台作业列表, 但没有得到输出, 说明两个进程已经不是受本终端管理的作业, 如//2jobs//.
!![2jobs,`jobs` 没有得到期望结果]
使用 `ps l -C 可执行文件名` 来查询两个进程, 得到结果. 如//2ps//, 进程 PID = 6550, PPID = 1, 说明在创建守护进程的过程中, 父进程退出, 子进程成为孤儿故被 PID = 1 的 `init` 进程 (CentOS 中为 `systemd`) 接管为子进程.
!![2ps,查询进程信息]
## 另一个控制台中查看 `test2` 相关信息
进程是全局的, 与控制台无关. 故另一个控制台同样可以用 `ps l -C test2` 查看进程信息, 如//2ps2//.
!![2ps2,另一个控制台查看进程信息]
## 控制台登出对于守护进程的影响
使用 `Ctrl-D` 登出启动 `test2` 的控制台, 如//2logout//.
!![2logout,登出控制台]
在另一控制台上使用 `ps l -C test2`, 仍可以查看到进程信息, 如//2psafterlogout//.
!![2psafterlogout,另一控制台查询进程信息]
要在另一控制台查看进程的标准输出, 使用 [reredirect](https://github.com/jerome-pouiller/reredirect/) 可以进行重定向. 但不像 `reptyr`, 它不修改进程的控制终端.
~~~~~~~~~~~~~~~~~~~~~~~~bash
[root@vm-linux ~]# git clone https://github.com/jerome-pouiller/reredirect/
正克隆到 'reredirect'...
remote: Enumerating objects: 530, done.
remote: Total 530 (delta 0), reused 0 (delta 0), pack-reused 530
接收对象中: 100% (530/530), 117.26 KiB | 44.00 KiB/s, done.
处理 delta 中: 100% (312/312), done.
[root@vm-linux ~]# cd reredirect/
[root@vm-linux reredirect]# make
cc -Wall -g -c -o reredirect.o reredirect.c
cc -Wall -g -c -o ptrace.o ptrace.c
cc -Wall -g -c -o attach.o attach.c
cc reredirect.o ptrace.o attach.o -o reredirect
[root@vm-linux reredirect]# make install
install -d -m 755 /usr/local/bin/
install -m 755 reredirect /usr/local/bin/reredirect
install -m 755 relink /usr/local/bin/relink
install -d -m 755 /usr/local/share/man/man1
install -m 644 reredirect.1 /usr/local/share/man/man1/reredirect.1
[root@vm-linux reredirect]#
~~~~~~~~~~~~~~~~~~~~~~~~
!![reredirect,安装 reredirect]
使用 `reredirect -m TTY设备路径 test2的PID号` 可以将进程的输出定向到新终端上, 但不改变其控制终端. 如//beforereredirect// 和 //afterreredirect//.
!![beforereredirect,在虚拟终端 3 运行守护进程]
!![afterreredirect,在虚拟终端 4 重定向守护进程输出]
## 保证进程中的打印信息能始终被看到
下面介绍四种方法使得进程中的打印信息能始终被看到.
### 使用外部工具重定向进程输出到当前终端
用户可以自由退出终端, 只需要保证守护进程在后台运行. 下次需要读进程的输出时, 只需用外部工具如 `reredirect` 将进程的输出重定向到当前终端.
### 使用日志文件
运行 `./test2 > output.log 2>&1` 将输出重定向至日志文件 `output.log`. 之后利用 `tail -f output.log`, 可以实时查看 `test2` 的标准输出和标准错误. 在另一个终端, 只要指定了正确的 `output.log` 路径, 可保证始终能看到 `test2` 的输出. 如//2tail// 和 //2tail2//.
!![2tail,记录 `test2` 的输出]
!![2tail2,另一个终端追踪 `test2` 的输出]
### 使用 screen 工具
GNU screen 是一个屏幕管理器. 使用 `screen` 生成一个虚拟终端, 在其中运行 `test2` 程序. 若运行 `screen` 的父进程 `bash` 遭到关闭, 只需在另一个终端中运行 `screen -r` 即可恢复之前运行 `test2` 的屏幕, 继续追踪其输出.
### 使用信号动态改变程序进行输出的 `pts` 设备
`sigqueue` 函数可以发送带 `int` 参数的信号至某个进程. 只需要知道目标进程 PID 以及所需重定向的 `pts` 终端号, 可以利用向目标进程发送特定带参信号的方式切换终端. 当然, 主程序应含有相应的信号注册代码和终端切换代码.
创建新的程序供测试:
~~~~~~~~~~~~~~~~C linenumbers
~~~~~~~~~~~~~~~~
[Listing [test2-redirect]: 信号接收端, 主程序 `test2-redirect.c` 的内容]
~~~~~~~~~~~~~~~~C linenumbers
~~~~~~~~~~~~~~~~
[Listing [test2-sendsig]: 信号发送端 `test2-sendsig.c` 的内容]
首先在某终端下运行 `./test2-redirect` 主程序, 使其后台运行. 之后按 `Ctrl-D` 登出, 如//2redirect//.
!![2redirect,运行主程序并登出]
接着, 开启另一个终端(仅限 `pts` 设备, 如 SSH 后开启的终端), 运行:
~~~~~~~~bash
./test2-sendsig `pgrep test2-redirect` `tty|cut -d/ -f4`
~~~~~~~~
此句利用 `test2-sendsig` 程序, 将本终端的 `pts` 号作为参数, 送 `SIGUSR1` 信号至 `test2-redirect` 程序的 PID. `test2-redirect` 收到信号及其参数, 切换标准输出和标准错误利用的设备文件, 在本终端开始输出, 如//2sendsig//.
!![2sendsig,另一终端发送信号, 获取输出]