你在終端啟動的程式,最後都是什麼下場?(下)
在上期文章你在終端啟動的程式,最後都是什麼下場?(上)當中我們介紹了前臺程式最終結束的幾種情況,在本篇文章當中主要給大家介紹後臺程式號可能被殺死的幾種情況。
揭秘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、計算機系統基礎、演算法與資料結構)知識。