**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,另一终端发送信号, 获取输出]