记录某个程序无奈启动问题

2023.06.27

背景介绍

一个独立的程序,下文称作test-bin,在start.sh脚本中被调用,start.sh中大略的代码如下所示:

#!/bin/bashkillall test-apikiallall test-bintest-bin 1>/dev/nulltest-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)}

问题景象

  1. 在终端中手动执行start.sh脚本,所有的服务都能够失常启动,零碎能够失常运行;
  2. 当test-api收到申请后重启服务,test-bin却无奈失常启动,失常状况下应该会有两个test-bin过程,但发现只有一个。另外一个过程平白无故没有启动,也没有任何core文件信息;

问题定位及剖析

因为失常状况下,应该会启动两个test-bin过程,目前只启动了一个,查看以后已启动的test-bin的文件描述符,发现如下:

bash-5.0# ls -l /proc/29764/fdtotal 0lr-x------ 1 root root 64 Jun 16 10:00 0 -> /dev/nulll-wx------ 1 root root 64 Jun 16 10:00 1 -> /dev/nulll-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/fdtotal 0lr-x------ 1 root root 64 Jun 16 10:03 0 -> /dev/nulll-wx------ 1 root root 64 Jun 16 10:03 1 -> /dev/nulllrwx------ 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/fdtotal 0lr-x------ 1 root root 64 Jun 16 10:00 0 -> /dev/nulll-wx------ 1 root root 64 Jun 16 10:00 1 -> /dev/nulll-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/bashkillall test-apikiallall test-bintest-bin 1>/dev/nulltest-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博客