nohup、setsid 与 disown 的不同之处【转】

发布时间 2023-07-03 23:28:51作者: 白日何短短

nohup、setsid 与 disown 都可以用来让需要长期运行的程序在退出终端后继续在后台运行。 然而它们实现这一目的的原理不同,因此使用起来也有一些不同。

 

 

退出终端时发生了什么

 

 

让我们先看看终端退出时发生什么:

 

 

当终端被挂断或伪终端程序被关掉,若终端的 CLOCAL 标志没有被设置,则 SIGHUP 信号会被发送到与该终端相关的控制进程(即会话首进程,通常为 shell)。 而 SIGHUP 的默认行为是终止程序的运行。 当会话首进程终止,也会将 SIGHUP 信号发送给前台进程组中的每一个进程(根据 shell 的具体实现,还可能会把 SIGNHUP 发送给后台进程组)

 

 

nohup、setsid 以及 disown

 

 

那么很自然就会发现,要实现终端退出后进程依然在后台运行,有两种途径:

 

 

  1. 进程收到 SIGHUP 后忽略该信号
  2. 进程根本没有收到 SIGHUP 信号

 

 

nohup

 

 

nohup 的使用方法很简单,只需要在要执行的命令前加上 nohup 就行了。

 

 

nohup commands

 

 

它的原理就是第一种途径。其核心代码为:

 

 

signal (SIGHUP, SIG_IGN);char **cmd = argv + optind;execvp (*cmd, cmd);

 

 

除此之外,它还做了以下动作:

 

 

  • 关闭进程的 stdin,当进程尝试读取输入时,只会得到EOF
  • 重定向进程的 stdout 和stderr 到 nohup.out 中

 

 

由于进程的 stdin,stdout 和 stderr 都脱离了终端,因此让它在前台运行似乎意义不大,一般我们会在后面加上 & 让它在后台运行。

 

 

setsid

 

 

nohup是通过忽略HUP信号来使我们的进程避免中途被中断,而 setsid 可以使我们的进程不属于接受 HUP 号的会话首进程的会话,从而避免受到HUP信号。

 

 

setsid 的使用方法跟 nohup 很类似,只需要在要执行的命令前加上 setsid 即可。

 

 

setsid command

 

 

它的核心代码是 setsid 函数

 

 

pid_t setsid(void);

 

 

调用 setsid 函数的进程若不是一个进程组的组长就会创建一个新会话。具体来说会发生下面3件事情

 

 

  • 该进程会变成新会话的会话首进程(会话首进程即创建该会话的进程),此时新会话中只有该进程这么一个进程
  • 该进程会变成一个新进程组的组长进程,新进程组 ID 就是该进程的 PID
  • 该进程与控制终端的联系被切断。

 

 

若调用 setsid 函数的进程就是一个进程组的组长,则该函数会返回出错。 为了解决这种情况,通常函数需要先fork,然后父进程退出,由子进程执行setsid。 由于子进程继承的是父进程的进程组 ID,而其PID是新分配的ID,因此这两者不可能相等,即子进程不可能是进程组的组长。 这种情况下,由于父进程先于子进程退出,因此子进程的父进程会有init进程接管。 而这就是sid命令的实现原理。

 

 

下面这个实验可以看书 setsid 所做的事情:

 

 

先编译一个测试程序(假设编译后的执行文件为 s.out),源代码如下:

 

 

#include <unistd.h>#include <stdio.h>int main(){  pid_t sid=getsid(0);          /* 会话 id */  pid_t pgrp=getpgrp();         /* 进程组id */  pid_t ppid=getppid();         /* 父进程id */  pid_t pid=getpid();           /* 进程ID */  printf("会话id:%d\n进程组id:%d\n父进程id:%d\n进程id:%d\n",sid,pgrp,ppid,pid);}

 

 

  会话id:17791  进程组id:17791  父进程id:5732  进程id:17791

 

 

在shell下直接执行的输出是:

 

 

[lujun9972@X61 ~]$ ./s.out 会话id:5235进程组id:17095父进程id:5235进程id:17095

 

 

其中5235就是 bash 的进程ID

 

 

而使用 setsid 的执行输出为:

 

 

[lujun9972@X61 ~]$ setsid ./s.out [lujun9972@X61 ~]$ 会话id:17146进程组id:17146父进程id:1进程id:17146

 

 

对比这两个输出,你会发现,setsid新建了一个全新的会话,而且其父进程变成了init京城。

 

 

由于会话和父进程都与shell无关了,因此无论如何shell都无法向该进程发送SIGHUP命令。

 

 

disown

 

 

前面提到的 nohup 和 setsid 都是外部命令,跟具体的shell无关。 即无论shell(同时也是终端相关的控制进程)的具体实现是怎样的都能保证执行的进程不会被SIGHUP挂断。 而且使用他们的一个限制就是必须在执行命令前,事先在命令前加上 nohup 或者 setsid

 

 

是如果我们未加任何处理就已经提交了命令,那就只能使用disown命令了。 然而disown是bash的内置命令,它只能在bash下使用。

 

 

比较常用的disown有以下三种方式

 

 

disown -h $jobspec               #使某个作业忽略HUP信号。disown -ah                       #使所有的作业都忽略HUP信号。disown -rh                       #使正在运行的作业忽略HUP信号。

 

 

disown 命令会将命令从 bash 的 job list 中删除。 这样,当 bash 收到 SIGHUP 信号后,并不会将 SIGHUP 信号发送给该命令。

 

 

然而,使用 disown 并不会切断命令与终端的关联关系,这样当终端被关闭后,若命令尝试从 stdin 中读取或输出到 stdout 中,可能会导致异常退出。

 

 

关于 &

 

 

事实上,在新版本的 bash 上,bash 并不会向后台程序发送 SIGHUP 命令,也就是说任何以 & 结尾运行在后台的进程都不会因为终端退出的SIGHUP信号而退出。

 

 

我们可以做个实验:先准备一个测试文件,代码如下:

 

 

#include <signal.h>#include <stdio.h>#include <unistd.h>void sig_handler(int signo){  if(signo == SIGHUP)    {      printf("RECV SIGHUP\n");    }  else    {      printf("RECV signal %d\n",signo);    }}int main(){  signal(SIGHUP,sig_handler);  sleep(180);}

 

 

编译后(假设编译出来的执行文件是 a.out)在终端中执行:

 

 

a.out >a.txt &

 

 

让程序在后台运行,然后关闭终端,再打开新终端,查看 a.txt 没有发现有内容,用 ps 也能查到 a.out 没有被杀掉,只是它的父进程变成了 init,但若在终端中执行

 

 

a.out >a.txt