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

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)
}

问题现象

  1. 在终端中手动执行start.sh脚本,所有的服务都可以正常启动,系统可以正常运行;
  2. 当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博客


viyon
6 声望0 粉丝