**Linux 知识补充 -- 守护进程的编写(进阶)以及 systemctl 的使用、rpm 安装包的生成** 1652270 **冯舜** 目录结构 =============== 用 `mkdir 1652270-000107` 创建本次作业的文件夹. 创建之后目录结构如//mkdir//. !![mkdir,创建后的目录结构] 编写守护进程程序 ==================== ## 形成不脱离控制台的守护进程 编写 `test.c` 如下: !![cont1,`test.c` 内容] !![cont2,`test.c` 内容] 编写后, 将其编译, 再运行, 发现并没有阻塞 bash. 使用 `ps -l` 查看同终端同用户进程, 发现 `test` 仍在运行, 且 PPID 为 1, 说明成为了守护进程. `kill PID号` 结束它后, 再查看进程, 发现其正常退出. 如//daemon1//. !![daemon1,第一个 test 程序的运行情况] ## 形成不脱离控制台的守护进程, 并能够派发子进程、修改进程的 ucmd (可执行文件名) ps 中所示的进程的 ucmd (表格表头中用 CMD 表示, 可以从 `/proc/{PID号}/comm` 查询、设置) 可以使用 `prctl(PR_SET_NAME, {名字字符串})` 来修改. 修改 `test.c` 如下: !![cont3,`test.c` 内容] !![cont4,`test.c` 内容] !![cont5,`test.c` 内容] 编写后编译运行. 使用 `ps` 持续查看, 发现 `test` 的 ucmd 值 (CMD) 成功修改为 `./test [main]`, 并能够每 1 秒派发一个子进程, ucmd 值为 `./test [sub]`. !![subs,成功修改 ucmd 和派发子进程] ## 改变进程的 cmdline (命令行, 包括所有的参数) 使主子进程显示运行时间 ### 显示运行时间 模仿 `ps` 源码中获取 `etime` (Elapsed Time, 进程从开始运行到现在的时间) 的方法, 先获取 `/proc/uptime` 中的启动时间 `up` (自系统启动后到现在的秒数), 再获取 `/proc/self/stat` 中的 `start_time` (以时钟数计数, 记录进程启动相对于系统启动的时间), 将其除以 `sysconf(_SC_CLK_TCK)` 获取到的每秒 CPU 时钟数进而得到自系统启动到进程启动的秒数 (表示为 `(unsigned long)(P_start_time / Hertz)`). 对两者作差即可获得运行时间: ~~~~~~~~~~~~~ unsigned long t = up - (unsigned long)(P_start_time / Hertz); ~~~~~~~~~~~~~ 当然, 可以采用更为简单的方法如调用 `gettimeofday()` 获取当前时刻, 对当前值与进程刚开始运行时的值作差; 等等. ### 改变 cmdline `prctl` 函数不能改变 `cmdline`, 因为 `/proc/self/cmdline` 是只读的, 它由 `main` 函数的 `argv[]` 参数指向的内存区域进行合并得到. 所以, 要修改 cmdline 为想要的值, 可以改变 `argv[0]` 的值. 此处只是粗糙地执行 `strcpy()`, 没有顾及字符串变长对 `argv[0]` 对应内存区域后侧数据的影响. 要做到万无一失, 应参考 http://www.cnblogs.com/doop-ymc/p/3432184.html 等方法, 申请内存区域对其他 `argv` 值乃至 `environ` 的数据进行搬迁, 再改动 `argv[0]` 的值. 代码如下: !![cont6,`test.c` 内容] !![cont7,`test.c` 内容] !![cont8,`test.c` 内容] !![cont9,`test.c` 内容] !![cont10,`test.c` 内容] 编译后运行: !![testwithtime,编译后运行带计时的 `test`] 可以看到, 十个 `test` 进程均已修改了 cmdline 指示运行时间, 且它们的时间差恒定, 与运行它们时间隔 1 秒的情况符合. ## 杀子进程后, 父进程重新分裂 用一个 n 个 `pid_t` 的动态空间 (首地址为 `subproc`) 记录 n 个子进程的 PID, 如 1 号子进程的 PID 为 `subproc[1]` (若某号子进程还未创建/已被杀死, 则 `subproc[i] == 0`). 每次创建子进程时, 查询 subproc 中 PID 为 0 的最小编号, 并将其分给子进程; 每次收到 SIGCHLD 时, 根据获取的已杀死子进程 PID, 将相应号数的 PID 置空, 并重新启动一个子进程. `test.c` 的代码省略 (可直接参考源文件). 运行情况如下: !![restart,可自动重启的子进程] ## 杀父进程后, 子进程自动结束 已在最开始通过 `prctl(PR_SET_PDEATHSIG, SIGTERM);` 实现了这一点. !![killall,杀父进程自动结束子进程] ## 从配置文件读取进程数 `readconfig.c` 文件内容: ~~~~~~~~~~~~~~~~~C ~~~~~~~~~~~~~~~~~ 则引用了 `lib1652270.so` 的主程序通过 `readN()` 可从返回值获取进程数. ## makefile makefile 如下所示: ~~~~~~~~~~~~~~~~~~~~~~~Makefile OBJ = test.o readconfig.o BIN = test lib1652270.so ifeq ($(PREFIX),) PREFIX := /usr endif all : $(BIN) #all : test_nolib CFLAGS_LIB := -fPIC -c CFLAGS_LD_LIB := -shared -Wl,-soname,lib1652270.so RMDIR = -rmdir --ignore-fail-on-non-empty -v lib1652270.so : readconfig.o $(CC) $(CFLAGS_LD_LIB) -o $@ $^ readconfig.o : readconfig.c $(CC) $(CFLAGS_LIB) -o $@ $^ test : test.o lib1652270.so $(CC) -L. -l1652270 -o $@ $< .PHONY : test_nolib test_nolib : test.o $(CC) -o test $< .PHONY : clean clean : $(RM) $(OBJ) $(BIN) $(RM) ./test-1652270.rpm .PHONY : install install : install -d $(DESTDIR)$(PREFIX)/lib64 install -m 0744 ./lib1652270.so $(DESTDIR)$(PREFIX)/lib64/ install -d $(DESTDIR)$(PREFIX)/sbin install -m 0744 ./test $(DESTDIR)$(PREFIX)/sbin/test-1652270 install -d $(DESTDIR)$(PREFIX)/1652270 install -m 0644 ./1652270.dat $(DESTDIR)$(PREFIX)/1652270/ install -d $(DESTDIR)/etc install -m 0644 ./1652270.conf $(DESTDIR)/etc/ install -d $(DESTDIR)$(PREFIX)/lib/systemd/system install -m 0644 ./test-1652270.service $(DESTDIR)$(PREFIX)/lib/systemd/system/ .PHONY : uninstall uninstall : $(RM) $(DESTDIR)$(PREFIX)/lib64/lib1652270.so $(RM) $(DESTDIR)$(PREFIX)/sbin/test-1652270 $(RM) $(DESTDIR)/etc/1652270.conf $(RM) $(DESTDIR)$(PREFIX)/1652270/1652270.dat $(RM) $(DESTDIR)$(PREFIX)/lib/systemd/system/test-1652270.service $(RMDIR) $(DESTDIR)$(PREFIX)/1652270/ $(RM) -r $(DESTDIR)$(HOME)/rpmbuild/BUILD/* $(RM) $(DESTDIR)$(HOME)/rpmbuild/RPMS/x86_64/test-1652270-1.0.0-1.x86_64.rpm $(RM) $(DESTDIR)$(HOME)/rpmbuild/SRPMS/test-1652270-1.0.0-1.src.rpm .PHONY : rpm rpm : clean mkdir -p $(HOME)/rpmbuild/BUILD/ (yes | cp -rf ./* $(HOME)/rpmbuild/BUILD/) rpmbuild -ba ./test-1652270-1.0.0-1.spec (yes | cp -f $(HOME)/rpmbuild/RPMS/x86_64/test-1652270-1.0.0-1.x86_64.rpm ./test-1652270.rpm) ~~~~~~~~~~~~~~~~~~~~~~~ 注意在 `install` 目标中, `install` 应将 `test-1652270` 和 `lib1652270.so` 以可执行权限复制到系统目录, 否则: - `test-1652270` 安装后无法直接运行 - 在后续 RPM 包的安装中会导致缺少 `lib1652270` 依赖的问题. `test-1652270.service` 的编写 ================== 写一个 `test-1652270.service` 文件: ~~~~~~~~~~~~~~~~~~~~ [Unit] Description=test-1652270 ShenJian Homework [Service] Type=forking WorkingDirectory=/root ExecStart=/usr/sbin/test-1652270 Restart=on-failure PIDFile=/var/run/test-1652270.pid [Install] WantedBy=multi-user.target ~~~~~~~~~~~~~~~~~~~~ 这个 service 指定了可执行文件为 `/usr/sbin/test-1652270`, 工作目录为 `/root`, 在运行失败时会自动重启. 将其放置到 `/usr/lib/systemd/system/` 下, 运行 `systemctl daemon-reload`, 新服务就载入完毕. 接下来对其进行一系列 `systemctl` 操作: !![systemctl,一系列 `systemctl` 操作] 可以看到, 服务能够正常启动并派生出 7 个 (配置文件中默认为 7 个) 子进程, 符合预期; 停止服务后, 相关进程消失. !![systemctlrestart,`systemctl restart` 操作] 可以看到, 服务正常重启, 重新 `ps` 后发现新进程还未派生完毕, 并且计时器重新从零开始. 制作 RPM 安装包 ==================== 在同目录下创建 RPM 包规格文件 `test-1652270-1.0.0-1.spec` 如下: ~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~ 其中要注意 `%file` 中可以直接定义目录, RPM 包卸载时会使用 `rmdir`, 在有其他文件时不删除目录. 构造 RPM 包的方法: - 将整个工程目录结构复制到 `~/rpmbuild/BUILD`, 执行 `rpmbuild -ba SPEC文件`. - 编写完善 Makefile, 添加 `rpm` 目标后, 直接执行 `make rpm` . !![rpmbuild1,构造 RPM 包-1] !![rpmbuild2,构造 RPM 包-2] 构造好后, `~/rpmbuild/RPMS/x86_64` 会出现构造好的 RPM 文件: (若使用 `make`, RPM 文件被拷贝到工程目录) !![RPMS,构造好的 RPM 包] 构建完后, 尝试安装如下: !![rpminstall,RPM 包的安装] 发现安装正常, 运行正常. 尝试在 `/usr/1652270/` 内有或没有其他文件两种情况下进行卸载: !![rpmuninstall,RPM 包的卸载] 在有其他文件时, 卸载不会删除 `/usr/1652270` 目录; 无其他文件时, 目录正常删除.