共计 2883 个字符,预计需要花费 8 分钟才能阅读完成。
记录某个程序无奈启动问题
2023.06.27
背景介绍
一个独立的程序,下文称作 test-bin,在 start.sh 脚本中被调用,start.sh 中大略的代码如下所示:
#!/bin/bash
killall test-api
kiallall test-bin
test-bin 1>/dev/null
test-api 1>/dev/null
之所以用 start.sh 脚本启动 test-bin,是因为整个零碎波及到多个服务及一些其余操作,所以放到 start.sh 脚本中。
另外一个独立的程序,应用 go 编写,对外提供接口,能够重启服务,下文称作 test-api,如果 test-api 收到须要重启的申请,则调用 start.sh 脚本重新启动所有的程序。大略的代码如下所示:
func Restart(c *gin.Context) {go func() {out, err := exec.Command(utils.ShellToUse, "-c", "start.sh").Output()
if err != nil {model.Loger("Error", fmt.Sprintf("restart error: %s, out: %s", err.Error(), out))
return
}
}()
response.Ok(c)
}
问题景象
- 在终端中手动执行 start.sh 脚本,所有的服务都能够失常启动,零碎能够失常运行;
- 当 test-api 收到申请后重启服务,test-bin 却无奈失常启动,失常状况下应该会有两个 test-bin 过程,但发现只有一个。另外一个过程平白无故没有启动,也没有任何 core 文件信息;
问题定位及剖析
因为失常状况下,应该会启动两个 test-bin 过程,目前只启动了一个,查看以后已启动的 test-bin 的文件描述符,发现如下:
bash-5.0# ls -l /proc/29764/fd
total 0
lr-x------ 1 root root 64 Jun 16 10:00 0 -> /dev/null
l-wx------ 1 root root 64 Jun 16 10:00 1 -> /dev/null
l-wx------ 1 root root 64 Jun 16 10:00 2 -> 'pipe:[100456522]'
lrwx------ 1 root root 64 Jun 16 10:00 3 -> /dev/zero
而对于通过终端执行 start.sh 脚本,可能失常启动两个 test-bin 过程的时候,查看 test-bin 过程的文件描述符,是如下内容:
bash-5.0# ls -l /proc/34480/fd
total 0
lr-x------ 1 root root 64 Jun 16 10:03 0 -> /dev/null
l-wx------ 1 root root 64 Jun 16 10:03 1 -> /dev/null
lrwx------ 1 root root 64 Jun 16 10:03 10 -> 'anon_inode:[eventpoll]'
发现子过程的 stderr(fd 为 2)被批改了,是一个管道 (pipe:[100456522]),想用 lsof 看一下,但因为零碎上没有这个命令,遂作罢。
从新梳理了一遍流程,发现 test-api 程序在执行 start.sh 脚本的时候,应用的是如下语句:
out, err := exec.Command(utils.ShellToUse, "-c", "start.sh").Output()
这条语句会获取 start.sh 脚本的所有输入,其是如何做到的呢?就是利用管道,大略的原理是父过程建设一个管道,而后 fork 出子过程,父过程敞开管道的写入。子过程敞开管道的读取,将规范输入重定向到管道的写入端。
因为 test-api 程序获取子过程的所有 stdout、stderr 的所有输入,所以会将子过程的 stdout、stderr 都重定向到管道,这也是咱们下面看到的:
bash-5.0# ls -l /proc/29764/fd
total 0
lr-x------ 1 root root 64 Jun 16 10:00 0 -> /dev/null
l-wx------ 1 root root 64 Jun 16 10:00 1 -> /dev/null
l-wx------ 1 root root 64 Jun 16 10:00 2 -> 'pipe:[100456522]'
lrwx------ 1 root root 64 Jun 16 10:00 3 -> /dev/zero
能够看到文件描述符为 2 的被重定向到了管道。但文件描述符 1 为什么没被重定向到管道呢?
起因在于 start.sh 脚本中启动 test-bin 的形式,应用如下形式启动的:
test-bin 1>/dev/null
start.sh 过程的 stdout、stderr 被重定向到管道,但在 start.sh 脚本中启动 test-bin 过程的时候,stdout 被重定向到了 /dev/null,而 stderr 管道并未被重定向,所以 test-bin 过程的 stderr 还是继承自 start.sh 过程,也被重定向到了管道。
被重定向到管道实践上也不会有什么问题,那为什么 test-bin 过程只启动了一个,而没有都启动呢?依据代码剖析,发现目前启动的这个 test-bin 过程是子过程,目前正在期待主 test-bin 过程初始化,但 test-bin 的主过程退出了,平白无故隐没了(因为找不到任何 core 文件)。主过程为何会退出呢?查看主过程的日志文件,也失常,而且从终端手动启动也能失常启动。再次剖析代码,发现在 start.sh 脚本中,有以下内容:
#!/bin/bash
killall test-api
kiallall test-bin
test-bin 1>/dev/null
test-api 1>/dev/null
首先会把 test-api 杀掉,而后再启动 test-bin,依照方才的剖析,test-api 中会从管道中读取子过程的输入信息,而子过程(start.sh)过程中又把 test-api 过程给杀掉了,那么管道的读取端就会被敞开,而子过程中(test-bin 程序继承 start.sh 中的管道写端)会向管道中写入内容,如果在启动过程中,test-bin 程序向 stderr 写入信息,即是向管道中写入信息,那么就会收到 SIGPIPE 信号,该信号默认的动作是退出程序。
于是在 test-bin 程序中编写信号处理函数,解决 SIGPIPE 信号,果然收到了该信号。至此,该问题已被定位,解决起来就比拟容易了。
总结起来,起因就是:test-api 中执行 start.sh 脚本的时候,会将 stderr 重定向到管道中。而执行 start.sh 脚本的时候,会将 test-api 过程 kill 掉,管道的读取端会被敞开,在启动 test-bin 的时候,因为 stderr 没有重定向为其它文件,会向 stderr 中写入信息,但因为管道的读取端已被敞开,所以会触发 SIGPIPE 信号,造成过程退出。
扩大材料
重定向子过程控制台程序的输入输出 – 绿色的麦田 – 博客园
管道的创立与读写 pipe – 邶风 – 博客园管道的创立与读写 pipe – 邶风 – 博客园
SIGPIPE 信号_平平无奇的小垃圾的博客 -CSDN 博客