你在终端启动的进程,最后都是什么下场?(下)
在上期文章你在终端启动的进程,最后都是什么下场?(上)当中我们介绍了前台进程最终结束的几种情况,在本篇文章当中主要给大家介绍后台进程号可能被杀死的几种情况。
揭秘nohup——后台进程的死亡
如果大家有过让程序在后台持续的运行,当你退出终端之后想让你的程序继续在后台运行,我们通常会使用命令 nohup。那么现在问题来了,为什么我们让程序在后台运行需要 nohup 命令,nohup 命令又做了什么?
在前面的文章你在终端启动的进程,最后都是什么下场?(上)当中我们已经谈到了,当你退出终端之后 shell 会发送 SIGHUP 信号给前台进程组的所有进程,然后这些进程在收到这个信号之后如果没有重写 SIGHUP 信号的 handler 或者也没有忽略这个信号,那么就会执行这个信号的默认行为,也就是退出程序的执行。
事实上当你退出终端之后 shell 不仅给前台进程组的所有进程发送 SIGHUP 信号,而且也会给所有的后台进程组发送 SIGHUP 信号,因此当你退出终端之后你启动的所有后台进程都会收到一个 SIGHUP 信号,注意 shell 是给所有的后台进程组发送的信号,因此如果你的后台进程是一个多进程的程序的话,那么你这个多进程程序的每一个进程都会收到这个信号。
根据上面的分析我们就可以知道了当我们退出终端之后,shell 会给后台进程发送一个 SIGHUP 信号。在我们了解了 shell 的行为之后我们应该可以理解为什么我么需要 nohup 命令,因为我们正常的程序是没有处理这个 SIGHUP 信号的,因此当我们退出终端之后所有的后台进程都会收到这个信号,然后终止执行。
看到这里你应该能够理解 nohup 命令的原理和作用了,这个命令的作用就是让程序忽略 SIGHUP 这个信号,我们可以通过 nohup 的源代码看出这一点。
nohup 的核心代码如下所示:
int
main(int argc, char *argv[])
{
int exit_status;
while (getopt(argc, argv, "") != -1)
usage();
argc -= optind;
argv += optind;
if (argc < 1)
usage();
if (isatty(STDOUT_FILENO))
dofile();
if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
/* may have just closed stderr */
err(EXIT_MISC, "%s", argv[0]);
(void)signal(SIGHUP, SIG_IGN); // 在这里忽略 SIGHUP 这个信号
execvp(*argv, argv); // 执行我们在命令行当中指定的程序
exit_status = (errno == ENOENT) ? EXIT_NOTFOUND : EXIT_NOEXEC;
err(exit_status, "%s", argv[0]);
}
在上面的程序当中我们可以看到,在 main 函数当中,nohup 首先创建使用 signal 忽略了 SIGHUP 信号,SIG_IGN 就是忽略这个信号,然后使用 execvp 执行我们在命令行当中指定的程序。
这里需要注意一点的是关于 execvp 函数,也就是 execve 这一类系统调用,只有当我们使用 SIG_IGN 忽略信号的时候,才会在 execvp 系列函数当中起作用,如果是我们自己定义的信号处理器 (handler),那么在我们执行完 execvp 这个系统调用之后,所有的我们自己定义的信号处理器的行为都将失效,所有被重新用新的函数定义的信号都会恢复成信号的默认行为。
比如说下面这个程序:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void sig(int no)
{
char* s = "Hello World\n";
write(STDOUT_FILENO, s, strlen(s));
sync();
}
int main(int argc, char* argv[], char* argvp[])
{
signal(SIGINT, sig);
execvp(argv[1], argv);
}
在上面的程序当中我们定义了一个信号处理器 sig 函数,如果接受到 SIGINT 信号那么就会执行 sig 函数,但是我们前面说了,因为只有 SIG_IGN 才能在 execvp 函数执行之后保持,如果是自定函数的话,那么这个信号的行为就会被重置成默认行为,SIGINT 的默认行为是退出程序,现在我们使用上面的程序去加载执行一个死循环的程序,执行结果如下:
从上面的程序的输出结果我们就可以知道,在我们按下 ctrl + c 之后进程会收到一个来自内核的 SIGINT 信号,但是并没有执行我们设置的函数 sig ,因此验证了我们在上文当中谈到的结论!
有心的同学可能会发现当我们在终端使用 nohup 命令的时候会生成一个 “nohup.out” 文件,记录我们的程序的输出内容,我们可以在 nohup 的源代码当中发现一点蛛丝马迹,我们可以看一下 nohup 命令的完整源代码:
#if 0
#ifndef lint
static const char copyright[] =
"@(#) Copyright (c) 1989, 1993\n\
The Regents of the University of California. All rights reserved.\n";
#endif /* not lint */
#ifndef lint
static char sccsid[] = "@(#)nohup.c 8.1 (Berkeley) 6/6/93";
#endif /* not lint */
#endif
#include <sys/cdefs.h>
__FBSDID("FreeBSD");
#include <sys/param.h>
#include <sys/stat.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void dofile(void);
static void usage(void);
#define FILENAME "nohup.out" // 定义输出文件的文件名
/*
* POSIX mandates that we exit with:
* 126 - If the utility was found, but failed to execute.
* 127 - If any other error occurred.
*/
#define EXIT_NOEXEC 126
#define EXIT_NOTFOUND 127
#define EXIT_MISC 127
int
main(int argc, char *argv[])
{
int exit_status;
while (getopt(argc, argv, "") != -1)
usage();
argc -= optind;
argv += optind;
if (argc < 1)
usage();
if (isatty(STDOUT_FILENO))
dofile();
if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) == -1)
/* may have just closed stderr */
err(EXIT_MISC, "%s", argv[0]);
(void)signal(SIGHUP, SIG_IGN);
execvp(*argv, argv);
exit_status = (errno == ENOENT) ? EXIT_NOTFOUND : EXIT_NOEXEC;
err(exit_status, "%s", argv[0]);
}
static void
dofile(void)
{
int fd;
char path[MAXPATHLEN];
const char *p;
/*
* POSIX mandates if the standard output is a terminal, the standard
* output is appended to nohup.out in the working directory. Failing
* that, it will be appended to nohup.out in the directory obtained
* from the HOME environment variable. If file creation is required,
* the mode_t is set to S_IRUSR | S_IWUSR.
*/
p = FILENAME;
// 在这里打开 nohup.out 文件
fd = open(p, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
if (fd != -1)
// 如果文件打开成功直接进行文件描述符的替代,将标准输出重定向到文件 nohup.out
goto dupit;
if ((p = getenv("HOME")) != NULL && *p != '\0' &&
(size_t)snprintf(path, sizeof(path), "%s/%s", p, FILENAME) <
sizeof(path)) {
fd = open(p = path, O_RDWR | O_CREAT | O_APPEND,
S_IRUSR | S_IWUSR);
if (fd != -1)
goto dupit;
}
errx(EXIT_MISC, "can't open a nohup.out file");
dupit:
if (dup2(fd, STDOUT_FILENO) == -1)
err(EXIT_MISC, NULL);
(void)fprintf(stderr, "appending output to %s\n", p);
}
static void
usage(void)
{
(void)fprintf(stderr, "usage: nohup [--] utility [arguments]\n");
exit(EXIT_MISC);
}
在源代码当中的宏 FILENAME 定义的文件名就是 nohup.out,在上面的代码当中,如果判断当前进程的标准输出是一个终端设备就会打开文件 nohup.out 然后将进程的标准输出重定向到文件 nohup.out ,因此我们在程序当中使用 printf 的输出就都会被重定向到文件 nohup.out 当中,看到这里就破案了,原来如此。
后台进程和终端的纠缠
后台进程是不能够从终端读取内容的,当我们从终端当中读的时候内核就会给这个后台进程发送一个 SIGTTIN 信号,这个条件主要是避免多个不同的进程都读终端。如果后台进程从终端当中进行读,那么这个进程就会收到一个 SIGTTIN 信号,这个信号的默认行为就是退出程序。
我们可以使用下面的程序进程测试:
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void sig(int no, siginfo_t* si, void* ucontext)
{
char s[1024];
sprintf(s, "signal number = %d sending pid = %d\n", no, si->si_pid);
write(STDOUT_FILENO, s, strlen(s));
sync();
_exit(0);
}
int main()
{
struct sigaction action;
action.sa_flags |= SA_SIGINFO;
action.sa_sigaction = sig;
action.sa_flags &= ~(SA_RESETHAND);
sigaction(SIGTTIN, &action, NULL);
while(1)
{
char c = getchar();
}
return 0;
}
然后我们在终端输入命令,并且对应的输出如下:
➜ daemon git:(master) ✗ ./job11.out&
[1] 47688
signal number = 21 sending pid = 0
[1] + 47688 done ./job11.out
从上面程序的输出结果我们可以知道,当我们在程序当中使用函数 getchar 读入字符的时候,程序就会收到来自内核的信号 SIGTTIN,根据下面的信号名和编号表可以知道,内核发送的信号位 SIGTTIN。
当我们在终端当中进行写操作的时候会收到信号 SIGTTOU,但是默认后台进程是可以往终端当中写的,如果我们想要进程不能够往终端当中写,当进程往终端当中写数据的时候就收到信号 SIGTTOU,我们可以使用命令 stty 进行设置。我们使用一个例子看看具体的情况:
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
void sig(int no, siginfo_t* si, void* ucontext)
{
char s[1024];
sprintf(s, "signal number = %d sending pid = %d\n", no, si->si_pid);
write(STDOUT_FILENO, s, strlen(s));
sync();
_exit(0);
}
int main()
{
struct sigaction action;
action.sa_flags |= SA_SIGINFO;
action.sa_sigaction = sig;
action.sa_flags &= ~(SA_RESETHAND);
sigaction(SIGTTOU, &action, NULL);
while(1)
{
sleep(1);
printf("c");
fflush(stdout);
}
return 0;
}
上面是一个比较简单的信号程序,不断的往终端当中输出字符 c
,我们可以看一下程序的执行情况(job12 就是上面的代码):
➜ daemon git:(master) ✗ stty tostop
➜ daemon git:(master) ✗ ./job12.out&
[1] 48467
➜ daemon git:(master) ✗ signal number = 22 sending pid = 0
[1] + 48467 done ./job12.out
在上面的输出结果当中我们使用命令 stty tostop
主要是用于启动当有后台进程往终端当中写内容的时候,向这个进程发送 SIGTTOU 信号,这个信号的默认行为也是终止进程的执行。
首先看一下当我们没有使用 stty tostop
命令的时候程序的行为。
现在我们使用 stty tostop
命令重新设置一下终端的属性,然后重新进程测试:
从上面的输出结果我们可以看到当我们在终端当中,默认是允许进程往终端当中进行输出的,但是当我们使用命令 stty tostop
之后,如果还有后台进程往终端当中进行输出,那么这个进程就会收到一个 SIGTTOU 信号。
后台进程和终端的命令交互
在前文当中我们谈到了当我们在一条命令后面加上 & 的话,那么这个程序将会变成后台进程。那么有没有办法将一个后台进程变成前台进程呢?
当然有办法,我们可以使用 fg ——一个 shell 的内置命令,将一个后台进程变成前台进程。在正式进行验证之前我们需要来了解三个命令:
- jobs 这条命令主要是用于查看当前所有的后台进程组,也就是所有的后台作业,。
- fg 这条命令主要是将一个后台进程放到前台来运行。
- bg 这条命令主要是让一个终端的后台程序继续执行。
具体的例子如下所示:
➜ daemon git:(master) ✗ sleep 110 & # 创建一个后台进程 每当创建一个后台作业 shell 都会给这个作业分配一个作业号 就是 [] 当中的数字,从 1 开始
[1] 7467
➜ daemon git:(master) ✗ sleep 111 & # 创建一个后台进程
[2] 7485
➜ daemon git:(master) ✗ sleep 112 & # 创建一个后台即成
[3] 7503
➜ daemon git:(master) ✗ jobs # 查看所有的后台进程 其中 + 表示当前作业 可以认为是最近一次使用 & 生成的作业 - 表示上一个作业 可以认为是倒数第二个使用 & 生成的作业
[1] running sleep 110
[2] - running sleep 111
[3] + running sleep 112
➜ daemon git:(master) ✗ fg # fg 的使用方式为 fg %num 如果不指定 %num 的话,默认就是将当前作业放到前台 饿我们在上面已经谈到了 当前作业为 sleep 112 因此将这个进程恢复到前台
[3] - 7503 running sleep 112
^C # 终止这个作业
➜ daemon git:(master) ✗ jobs # 因为终止了作业 sleep 112 因此后台进程组只剩下两个了
[1] running sleep 110
[2] + running sleep 111
➜ daemon git:(master) ✗ fg # 在将最近一次提交的作业放到前台
[2] - 7485 running sleep 111
^C # 终止这个任务的执行
➜ daemon git:(master) ✗ sleep 112 &
[2] 7760
➜ daemon git:(master) ✗ jobs
[1] - running sleep 110
[2] + running sleep 112
➜ daemon git:(master) ✗ sleep 112 &
[3] 7870
➜ daemon git:(master) ✗ sleep 112 &
[4] 7888
➜ daemon git:(master) ✗ jobs
[1] running sleep 110
[2] running sleep 112
[3] - running sleep 112
[4] + running sleep 112
➜ daemon git:(master) ✗ fg %1
[1] 7467 running sleep 110
^C
➜ daemon git:(master) ✗
接下来我们使用下面的程序进行验证,下面的程序的主要目的就是判断当前进程是否是前台进程,如果是则打印消息,如果不是那么就一直进行死循环:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
sleep(1);
// tcgetpgrp 返回前台进程组的进程组号
// getpgid(0) 得到当前进程组的进程组号
// 如果两个结果相等则说明当前进程组是前台进程组
// 反之则是后台进程组
if(getpgid(0) == tcgetpgrp(STDOUT_FILENO))
{
printf("I am a process of foregroup process\n");
}
}
return 0;
}
然后我们在终端当中执行这个程序,对应的几个结果如下所示:
➜ daemon git:(master) ✗ ./job13.out& # 先将这个程序放到后台运行,因为不是前台程序因此不会打印消息
[1] 5832
➜ daemon git:(master) ✗ fg # 将这个程序放到前台执行,因为到了前台因此上面的程序会输出消息
[1] + 5832 running ./job13.out
I am a process of foregroup process
I am a process of foregroup process
I am a process of foregroup process
^Z
[1] + 5832 suspended ./job13.out # 在这里我们按下 ctrl + z 给进程发送 SIGTSTP 信号 让进程暂停执行
➜ daemon git:(master) ✗ bg %1 # bg 命令默认是给进程发送一个 SIGCONT 因为在上一行当中信号 SIGTSTP 让进程暂停执行了 因此进程在收到信号 SIGCONT 之后会继续执行(SIGCONT 的作用就是让一个暂停的进程继续执行)
[1] + 5832 continued ./job13.out # 因为进程还是在后台当中,因此进程继续执行还是在后台执行,所以依然没有输出
➜ daemon git:(master) ✗ fg %1 # 这条命令是让后台进程组当中的第一个作业到前台执行,因此进程开始打印输出
[1] + 5832 running ./job13.out
I am a process of foregroup process
I am a process of foregroup process
^C # 在这里输入 ctrl + c 命令,让前台进程组当中所有进程停止执行
➜ daemon git:(master) ✗
在上面的输出结果当中,我们首先在后台启动一个进程,因为是在后台所以当前进程组不是前台进程组,因此不会在终端当中打印输出,而当我们使用 fg 命令将后台当中的最近生成的一个作业(当我们输入命令之后,终端打印的[]当中的数字就是表示作业号,默认是从 1 开始的,因为我们只启动一个后台进程(执行一条命令就是开启一个作业),因此作业号等于 1)放到前台来执行,在上面的例子当中,命令 fg 和 fg %1 的效果是一样的。
总结
在本篇文章当中主要给大家介绍了后台进程的一些生与死的情况,总体来说有以下内容:
- 当我们退出终端的时候,shell 会给所有前台和后台进程组发送一个 SIGHUP 信号,nohup 命令的原理就是让程序忽略这个 SIGHUP 信号。
- 当后台进程从终端当中读的时候内核会给这个进程发送一个 SIGTTIN 信号。
- 当我们设置了 ssty tostop 之后,如果我们往终端当中进行写操作的话,那么内核会给这个进程发送一个 SIGTTOU 信号,这两个信号的默认行为都是终止这个进程的执行。
- 我们可以使用 jobs fg bg 命令让终端和后台进程进行交互操作,fg 将一个后台进程放到前台执行,如果这个进行暂停执行的话, shell 还会给这个进程发送一个 SIGCONT 信号让这个进程继续执行,bg 可以让一个后台暂停执行的进程恢复执行,本质也是给这个后台进程发送一个 SIGCONT 信号。
- 当你在终端当中输入 ctrl + c 的时候,内核会给所有的前台进程组当中所有的进程发送 SIGINT 信号,当你在终端输入 ctrl + z 时,内核会给前台进程组当中的所有进程发送 SIGTSTP 信号,当你在终端输入 ctrl + \ 内核会给所有的前台进程组发送 SIGQUIT 信号。
- 综合上面的分析,上面的结果可以使用下面的图进行表示分析。
以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。