使用nohup [command] &程序仍然会挂断的问题 nohup+&不起作用

原文地址,转载请注明出处: https://blog.csdn.net/qq_34021712/article/details/115587702  ©王赛超

背景介绍

使用Go语言写的一个推送程序,最终打包成可执行的二进制文件上传到服务器,使用以下命令启动服务。

nohup ./MISS.GO.PayNotifyService >/dev/null &

MISS.GO.PayNotifyService是打包之后的二进制名称,但是第二天来了发现服务已经停止了。看到这个结果的第一想法就是宕机了, 先查看磁盘容量,内存使用情况,发现服务器一切正常,并非资源问题。之前无论是java程序还是其他的go服务都是使用nohup启动的,完全没问题啊,为什么会宕机呢?

难道是触发了panic恐慌导致程序宕机?

我们知道,go语言如果触发了恐慌panic没有recover进程会直接挂掉,通过查看日志最后的几行内容:

get signal hangup, application will shutdown.
Graceful shutdown --- Destroy all registries. 
Exporter unexport.
Destroy invoker: dubbo://:28555/......
Exporter unexport.
Destroy invoker: dubbo://:28555/......
(ZkProviderRegistry)reconnectZkRegistry goroutine exit now...
zkClient Conn{name:zk registry, zk addr:102523761507391298} exit now.
get signal hangup
Selector.watch() = error{listener have been closed}
client.done(), listen(path{......}) goroutine exit now...

看上面的日志 get signal hangup, application will shutdowndubbo go源代码,接受 hangup 之后 各种销毁动作。get signal hangup是程序收到了hangup信号,我自己打印的日志,通过以上内容可以确认是程序收到了hangup信号终止了程序。

原因我们已经知道了,就是因为程序接收到了SIGHUP信号,然后经过一系列收尾动作,最终结束进程。

问题复现

第一种: 使用nohup启动,然后直接点击x号关闭SecureCRT。

第一种方式最简单,可以立刻看到结果, 直接点击x号之后,再重新开一个终端进入服务器,查看进程已经结束,并且打印以上日志。

第二种: 使用nohup启动,然后等SecureCRT终端超时。

我自己是因为这种情况,因为开发环境联调,同事需要调用我的服务,我就是使用SecureCRT登录,然后使用nohup启动服务就不管了,第二天过来发现服务已经停止。
这种情况复现比较麻烦,我直接告诉大家步骤和结果:

  1. 使用SecureCRT登录服务器,然后使用nohup启动, 在终端输入tty命令回车,查看当前伪终端为: pts/0。
  2. 等待SecureCRT超时,然后重新进入服务器,查看进程.进程存在。查看当前伪终端为: pts/1。
  3. 第二步再次进入的时候,伪终端为: pts/1 第一步使用的伪终端为: pts/0。
  4. 使用 w 命令查看当前活跃用户, 有 pts/0 和 pts/1 两个,但是 pts/0目前已经类似于游离状态,没人使用。
  5. 使用 w 命令查看当前活跃用户, 等 pts/0 从活跃用户列表消失,再次查看进程,进程结束.(等pts/0超时大概等了2个半小时)

结论: 使用nohup启动,等SecureCRT超时,但是启动的伪终端还在,并且处于在线状态,大概过2个半小时,伪终端进程被结束,然后启动的服务进程也结束。
因为测试的时候就测试那么几分钟,可能一天也就测试一次。等SecureCRT超时,再到伪终端最终被结束,有几个小时的时间,所以每次都是第二天再测试的时候发现进程结束了。

第三种: 使用nohup启动,然后在终端执行 kill -1 发送SIGHUP信号给服务。

服务还是会收到了SIGHUP信号,然后服务停止,并且打印了以上的日志。

通过以上3种方式,可以复现我们nohup启动之后,然后进程收到SIGHUP信号,最终进程停止的原因。那么问题来了。

产生的问题

1.信号是谁发送的?
2.nohup是忽略SIGHUP信号的,为什么还能收到?nohup为什么不起作用?

先来了解一下nohup

nohup介绍

nohup,顾名思义,就是使得运行的命令可以忽略SIGHUP信号。因此,即使突然断网或者关闭终端,该后台任务依然可以继续执行。这里需要指明的是,nohup并不会自动将任务运行在后台,我们需要在命令行末尾加上&来显示的指明。
一般我们的使用方法如下:

# 启动一个jar包
nohup java -jar XXX.jar &
# 启动一个shell脚本
nohup sh test.sh &
# 启动一个二进制文件
nohup ./MISS.GO.PayNotifyService &

生产环境的服务都是用nohup启动的服务,功能测试环境有的是脚本启动,有的是直接执行nohup命令来启动的, 都没有问题,只有这个服务有问题。

我启动的命令为: nohup ./MISS.GO.PayNotifyService >/dev/null & 难道是nohup后面跟的参数有问题?于是我把下面的这些命令都测试了一遍:

nohup ./MISS.GO.PayNotifyService &
nohup ./MISS.GO.PayNotifyService >/dev/null &
nohup ./MISS.GO.PayNotifyService >/dev/null 2>&1 &
nohup ./MISS.GO.PayNotifyService 1>/dev/null 2>&1 &
nohup ./MISS.GO.PayNotifyService </dev/null >/dev/null 2>&1 &

最终的结果跟问题复现的结果一样,进程都会被结束。nohup根本不起作用。

了解一下linux信号以及相关知识。

既然nohup不起作用,暂时也没有头绪,那就看一下信号是谁发出来的? 看能不能找到点线索,终端结束的时候,为什么之上运行的进程也会结束? 我们先来了解以下linux基础知识。

进程

linux是以进程为单位来执行程序, 当计算机开机的时候,内核(kernel)只建立了一个init进程(centos7之后是systemd)。剩下的所有进程都是init进程通过fork机制建立的。我们执行的每一条指令,像pssh等等都是一个进程。都是从父进程fock过来的。每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构。使用pstree命令可以查看进程树状结构。每一个进程都有一个唯一的PID来代表自己的身份。

进程组

每个进程都属于唯一的一个进程组(process group),每个进程组中可以包含多个进程。每个进程组都有一个领导进程(process group leader),领导进程的PID成为进程组的ID (process group ID, PGID),以识别进程组。发送给一个进程组的信号会发送给进程组中的每一个进程。

会话

会话(Session)是一个或者多个进程组的集合,通常一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话。登录后的第一个进程叫做会话领导进程,通常我们都是通过ssh连接,所以领导进程就是bash进程。会话领导进程PID 为会话的 SID。

  • 控制终端: 控制终端通常是登陆到其上的终端设备(终端登陆)或伪终端设备(网络登录),一个会话最多可以有一个控制终端。一般我们都是通过网络ssh连接服务器,所以控制终端就是一个伪终端。使用tty命令可以查看当前的终端名。
  • 控制进程:与控制终端连接的会话首进程被称为控制进程。
  • 前台进程组: 如果一个会话有一个控制终端,则它有一个前台进程组。一个会话最多只能有一个前台进程组。
  • 后台进程组: 除了一个前台进程组, 会话中的其他进程组则为后台进程组。
  • 作业(job): 会话中的每个进程组称为一个作业(job)。会话可以有一个进程组成为会话的前台作业(前台进程组),而其他的进程组是后台作业(后台进程组),可以通过jobs命令查看后台运行的进程组。也可以通过fg命令将后台进程组切换到前端,这样就可以继续接收用户的输入了。如果作业中的某个进程创建了子进程,那此子进程属于进程组,不属于作业。
  • 如何查看前台进程组和后台进程组:使用命令 ps -ajxf 查看 TPGID(终端前台进程组) 一栏,如果该值为 -1,表示该进程没有控制终端,是属于后台进程组,反之则为前台进程组。
守护进程

守护进程是运行在后台的一种特殊进程。它独立于控制终端,并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程和后台进程组的区别:
后台进程的文件描述符是继承于父进程,例如shell,所以它也可以在当前终端下显示输出数据。但是daemon进程自己变成了进程组长,其文件描述符号和控制终端没有关联,是控制台无关的。基本上任何一个程序都可以后台运行,但守护进程是具有特殊要求的进程,守护进程肯定是后台进程,但反之不成立。一个进程成为daemon进程,可以不随会话的退出而退出,但是进程的uid/gid并不会因此而改变。

如何判断一个进程是否为守护进程:
所有的守护进程都是以超级用户启动的(UID为0)、没有控制终端(TTY为?)、TPGID(终端前台进程组)为-1

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

孤儿进程和守护进程的区别:
孤儿进程和守护进程都脱离终端运行,在其运行的过程中在终端中使用jobs命令发现不了。从这点来看,孤儿进程与守护进程都可以脱离终端运行。那么他们的本质区别是什么?

  • 孤儿进程是因为父进程异常结束了,然后被1号进程init收养。
  • 守护进程是创建守护进程时有意把父进程结束,然后被1号进程init收养。
  • 虽然他们都会被init进程收养,但是他们是不一样的进程。
  • 守护进程会随着系统的启动默默地在后台运行,周期地完成某些任务或者等待某个事件的发生,直到系统关闭守护进程才会结束。
  • 孤儿进程则不是,孤儿进程会因为完成使命后结束运行。
僵尸进程

一个子进程退出,但是其父进程并没有调用子进程的wait()waitpid()的情况下。这个子进程就是僵尸进程。杀死父进程可以直接回收僵尸进程。
注意:僵尸进程将会导致资源浪费,而孤儿进程则不会

信号

信号由内核(kernel)管理的。它可以是内核自身产生的,也可以是其它进程产生的,发送给内核,再由内核传递给目标进程。

关于进程、进程组、会话等关系,网上找了一张图大家看一下:
在这里插入图片描述

SIGHUP信号介绍

kill -l 命令可以查看所有的信号,这里就不展示了,在对以上的概念有所了解之后,我们现在开始正式了解一下SIGHUP信号。

SIGHUP会在以下4种情况下被发送给相应的进程:

  1. kill -1 PID 这种就是a进程b进程发送了一个sighup信号。我们前面也说了,信号可以是其它进程产生的,发送给内核,再由内核传递给目标进程。
  2. 终端关闭时,该信号被发送到session首进程以及作为作业(job)提交的进程(即用 & 符号提交的进程)。
  3. session首进程退出时,该信号被发送到该session中的前台进程组和后台进程组中的每一个进程。
  4. 若进程的退出,导致一个进程组变成了孤儿进程组,且新出现的孤儿进程组中有进程处于停止状态,则SIGHUPSIGCONT信号会按顺序先后发送到新孤儿进程组中的每一个进程。

系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。

我们主要看一下session退出后,linux执行的大概步骤:

  1. 用户退出 session (正常退出、远程登录时的网络断开、sshd挂掉、手动叉掉 ssh 登陆窗口)
  2. 系统向该 session 发出SIGHUP信号
  3. session 将SIGHUP信号发给所有子进程(包括前台进程和后台进程)。
  4. 子进程收到SIGHUP信号后,自动退出。

第一个问题: sighup信号是谁发的?为什么会发送sighup信号?

到这里,我们上面的第一个问题已经解决了, 一般情况下sighup信号是在session退出后,内核向session的子进程发送的信号。

/proc/{pid}/status命令

这里又新学了一个命令,可以使用 cat /proc/{pid}/status 来查看进程忽略和接受的信号。命令查看的信息有很多,可以使用grep过滤一下,只查看信号相关的字段。命令为:grep Sig /proc/{pid}/status

# grep Sig /proc/890/status
SigQ:   0/15078
SigPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: fffffffe57f0d8fc
SigCgt: 00000000280b2603

解释:
SigQ: 待处理信号的个数
SigPnd:屏蔽位,存储了该线程的待处理信号,等同于线程的PENDING信号.
SigBlk:存放被阻塞的信号,等同于BLOCKED信号.
SigIgn: 存放被忽略的信号,等同于IGNORED信号.
SigCgt: 存放捕获的信号,等同于CAUGHT信号.

右边的数字是位掩码。如果将其从十六进制转换为二进制,则每个1位代表捕获的信号,从1开始从右到左计数。因此,通过解析SigCgt行,可以看到进程正在捕获的信号:

00000000280b2603 ==> 101000000010110010011000000011
                     | |       | ||  |  ||       |`->  1 = SIGHUP
                     | |       | ||  |  ||       `-->  2 = SIGINT
                     | |       | ||  |  |`----------> 10 = SIGUSR1
                     | |       | ||  |  `-----------> 11 = SIGSEGV
                     | |       | ||  `--------------> 14 = SIGALRM
                     | |       | |`-----------------> 17 = SIGCHLD
                     | |       | `------------------> 18 = SIGCONT
                     | |       `--------------------> 20 = SIGTSTP
                     | `----------------------------> 28 = SIGWINCH
                     `------------------------------> 30 = SIGPWR

每次这样算也挺麻烦的,下面是一个写好的脚本,使用命令:sh signals.sh 890

#read -p "PID=" pid
pid=$1
cat /proc/$pid/status|egrep '(Sig|Shd)(Pnd|Blk|Ign|Cgt)'|while read name mask;do
    bin=$(echo "ibase=16; obase=2; ${mask^^*}"|bc)
    echo -n "$name $mask $bin "
    i=1
    while [[ $bin -ne 0 ]];do
        if [[ ${bin:(-1)} -eq 1 ]];then
            kill -l $i | tr '\n' ' '
        fi
        bin=${bin::-1}
        set $((i++))
    done
    echo
done
# vim:et:sw=4:ts=4:sts=4:

第二个问题:nohup为什么没有忽略SIGHUP信号?

既然session在退出的时候会向子进程发送sighup信号,那么忽略这个信号不就得了嘛? nohup就是用来忽略SIGHUP信号的,但是为什么不起作用呢?

其他的java服务和go服务也是用nohup启动的,为什么它们没问题呢?于是我写了一个很简单的http服务,代码如下:

func main() {
   http.HandleFunc("/tree/", HelloServer)
    _ = http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
    all, _ := ioutil.ReadAll(r.Body)
    fmt.Println("===> "+string(all))
    all, _ = ioutil.ReadAll(r.Body)
    fmt.Println("===> "+string(all))
    _, err := w.Write([]byte(r.URL.Path))
    if err != nil {
        fmt.Println(err)
    }
}

将这个goweb服务打包成二进制,上传到服务器使用nohup启动,无论是网络超时断开,还是直接点x号关闭终端,或者使用exit退出,进程都在。为什么会这样?

1.查看goweb服务的进程信息
# 先查看进程号
[root@localhost goweb]# ps -ef|grep goweb
root       890   750  0 12:52 pts/0    00:00:00 ./goweb
root       898   750  0 12:52 pts/0    00:00:00 grep --color=auto goweb
# 查看进程的信号信息
[root@localhost goweb]# grep Sig /proc/890/status 
SigQ:   0/15078
SigPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000001
SigCgt: fffffffe7fc1fefe

解析SigIgn可以看到,该进程是忽略 1 SIGHUP 这个信号的。

2.查看有问题的 MISS.GO.PayNotifyService 进程
[root@localhost MISS.GO.PayNotifyService]# ps -ef|grep MISS.GO.PayNotifyService 
root      8049  6487  8 15:09 pts/0    00:00:00 ./MISS.GO.PayNotifyService
root      8061  6487  0 15:09 pts/0    00:00:00 grep --color=auto MISS.GO.PayNotifyService
[root@localhost MISS.GO.PayNotifyService]# grep Sig /proc/8049/status 
SigQ:   0/15078
SigPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: fffffffe7fc1feff

解析SigIgn可以看到,该值的所有位都是0 证明没有忽略任何信号,也就是说会接受到 SIGHUP 这个信号。

启动方式是nohup + &为什么MISS.GO.PayNotifyService进程没有忽略 SIGHUP 信号呢? 而goweb进程却可以呢?难道是代码里有对 SIGHUP信号做了处理?查看代码,如下:

func initSignal() {
   signals := make(chan os.Signal, 1)
   // It is not possible to block SIGKILL or syscall.SIGSTOP
   signal.Notify(signals, os.Interrupt, os.Kill, syscall.SIGHUP,
       syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
   for {
       sig := <-signals
       fmt.Println("get signal %s", sig.String())
       switch sig {
       case syscall.SIGHUP:
           fmt.Println("app exit now by SIGHUP...")
           os.Exit(1)
       default:
           time.AfterFunc(time.Duration(10e9), func() {
               fmt.Println("app exit now by force...")
               os.Exit(1)
           })
           // The program exits normally or timeout forcibly exits.
           fmt.Println("app exit now...")
           return
       }
   }
}

重点关注signal.Notify 这行代码,其实这个 signal.Notify 就是对信号做了处理,相当于修改了 SIGHUP信号的默认处理。

nohup原本是忽略SIGHUP信号的,但是程序启动后,又被signal.Notify重新修改了SIGHUP的处理方式。

nohup启动只是启动了一个后台作业。session退出之后,前台进程组和后台进程组都会收到SIGHUP信号。

所以这也是为什么 nohup 启动之后没有忽略 SIGHUP信号的原因。

3.那么goweb进程没有结束的原因?

goweb程序本身没有对SIGHUP信号做处理,因为用nohup启动,所以启动的进程是忽略SIGHUP信号的,session退出或者直接点击x号关闭终端,goweb进程是收不到SIGHUP信号的。
但是由于 父进程 已经退出,goweb进程还在运行,所以goweb进程会被init进程接管,变成一个孤儿进程。我们可以测试一下:

1.先使用nohup+&启动。
2.使用 命令查看
3.直接点击x号关闭终端。
4.重新登录终端
5.使用 命令查看

# ======> 1.第一次启动 ,然后使用 ps查看进程信息
[root@localhost goweb]# nohup ./goweb &
[1] 9435
[root@localhost goweb]# nohup: ignoring input and appending output to ‘nohup.out’

[root@localhost goweb]# ps ajxf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  3539  3539  3539 ?           -1 Ss       0   0:00 /usr/sbin/sshd -D
 3539  9141  9141  9141 ?           -1 Ss       0   0:00  \_ sshd: root@pts/1
 9141  9143  9143  9143 pts/1     9496 Ss       0   0:00      \_ -bash
 9143  9435  9435  9143 pts/1     9496 Sl       0   0:00          \_ ./goweb
 9143  9496  9496  9143 pts/1     9496 R+       0   0:00          \_ ps ajxf

# 可以看到 TTY为:伪终端 pts/1
#          PPID为:父进程为9143,就是bash进程。
#          SID为:bash 还有启动的 goweb进程 以及我们使用的命令  ps ajxf 产生的进程都属于 9143 这个会话(session)。


# ======> 2.然后我们点击 x号直接关闭终端。
# ======> 3.我们再重新登录终端。
# ======> 4.我们再重新登录终端。 
# ======> 5.再次使用ps命令查看进程信息

[root@iz2ze5uw2drhkk7vdxkeftz ~]# ps -ajxf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  9435  9435  9143 ?           -1 Sl       0   0:00 ./goweb

# 可以看到 TTY为:?号 代表没有终端
#          PPID为:1 直接被init进程接管。 
#          SID和PGID还是之前的值。这个孤儿进程也可以被称为守护进程。

为什么进程的SIDPGID还是之前的值?因为没有调用setsid将进程设置为领导进程,这就是为什么有sid != pid守护程序并不少见的原因。

解决方法

1.使用nohup启动之后,手动执行 exit 退出终端。

使用exit命令退出终端,我们的程序还是会继续运行,这是为什么呢?这是因为使用exit命令退出终端时不会向终端所属任务发SIGHUP信号,这是huponexit配置项控制的,默认是off,可以使用shopt命令查看。

[root@localhost ~]# shopt |grep huponexit  
huponexit off 

huponexit配置成on,再次使用exit命令退出,所属的任务就会跟随退出。可以再次使用 shopt -u huponexit设置为off

[root@localhost ~]# shopt -s huponexit
[root@localhost ~]# shopt |grep huponexit
huponexit on 

大多数Linux系统,这个参数默认关闭(off)。因此,使用exit退出session的时候,不会把SIGHUP信号发给"后台任务"

但是 nohup + & 显然不是够安全的。因为有的系统的huponexit参数可能是打开的(on)

2.使用 disown 命令

disown可以将指定任务从"后台任务"列表(jobs命令的返回结果)之中移除。一个"后台任务"只要不在这个列表之中,session 就不会向它发出SIGHUP信号。

nohup ./MISS.GO.PayNotifyService & disown 

执行上面的命令以后,MISS.GO.PayNotifyService进程从"后台任务"列表移除。再执行jobs命令验证,输出结果里面,不会有这个作业。

3.使用 setsid 命令

setsid可以新建一个会话,调用setsid是有条件的:即调用进程自己不能是进程组长,因此,调用setsid之前需要先fork,然后由产生的子进程调用setsid(这里我们不需要关心,我们使用ssh登录,在终端执行的每一条命令都是fock自bash进程)

为什么不允许进程组长调用setsid?

调用setsid的进程将成为一个新的进程组的组长,如果允许一个进程组长调用setsid的话,那这个人将成为两个组的组长,再说,进程组长的定义是"其进程ID=进程组ID",如果某个进程是两个组的组长,那么这两个组的进程组ID是相同的?肯定不允许。

当进程是进程组长时调用setsid失败,setsid调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。

setsid的使用也非常方便,只需要在要处理的命令之前添加setsid即可。

setsid ./MISS.GO.PayNotifyService
4.使用 screen 命令

screen命令用于管理多个终端,它可以创建终端,让程序在里面运行。screen使用之前需要先执行yum install screen

screen ./MISS.GO.PayNotifyService &

以上的解决方法还有很多其他的功能,大家可以选择一种方式,自行查阅一下相关文档。

  • 34
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值