一、控制終端
對話期和進程組有一些其他特性:
- 一個對話期可以有一個單獨的控制終端。通常是我們在其上登錄的終端設備或僞終端設備。
- 建立與控制終端連接的對話期首進程,被稱之爲控制進程
- 一個對話期中的幾個進程組可以被分成一個前臺進程組以及一個或幾個後臺進程組
- 如果一個對話期有一個控制終端,則它有一個前臺進程組,其他進程組則爲後臺進程組。
- 無論何時鍵入終端鍵(Ctrl-C)或退出鍵(Ctrl-\),就會造成中斷信號或退出信號送至前臺進程組的所有進程。
- 如果終端界面檢測到調制解調器已經脫開連接,則將掛斷信號送至控制進程。
這些特性見下圖
登錄時會自動建立控制終端
二、tcgetpgrp和tcsetpgrp函數
以下兩個函數用來通知內核哪一個進程組是前臺進程組,這樣,終端設備驅動程序就能瞭解將終端輸入和終端產生的信號送到何處。
#include <sys/types.h>
#include <unistd.h>
pid_t tcgetpgrp(int filedes);
返回值:成功返回前臺進程組ID,出錯-1
int tcsetpgrp(int filedes, pid_t pgrpid);
返回值:成功爲0,出錯爲-1
函數tcgetpgrp返回前臺進程組ID,它與在filedes上打開的終端相關。
如果進程有一個控制終端,則該進程可以調用tcsetpgrp將前臺進程組ID設置爲pgrpid。pgrpid值應當是在同一會話期中的一個進程組的ID。filedes必須引用該對話期的控制終端。
大多數程序並不直接調用這兩個函數。它們通常由作業控制shell調用。只有定義了—_POSIX_JOB_CONTROL,這兩個函數才被定義了。否則它們返回出錯。
三、作業控制
作業控制允許在一個控制終端上啓動多個作業(進程組),控制哪一個作業可以存取該終端,以及哪些作業在後臺運行。作業控制要求三種形式的支持:
- 支持作業控制shell。
- 內核中的終端驅動程序必須支持作業控制
- 必須提供對某些作業控制信號的支持
一個作業只是幾個進程的集合,通常是一個進程管道。例如:
vim abc.c
在前臺啓動了只有一個進程的一個作業。下面的命令:
pr *.c | lpr &
make all &
在後臺啓動了兩個作業。這兩個作業所調用的進程都在後臺運行。 當啓動一個後臺作業時,shell賦予它一個作業標識。並打印一個或幾個進程ID。下面的操作過程顯示了KornShell是如何處理這一點的。
$ make all > Make.out &
[1] 1475
$ pr *.c | lpr &
[2] 1490
$ 鍵入回車
[2] + Done pr *.c | lpr &
[1] + Done make all > Make.out &
make是作業號1,所啓動的進程ID是1475.下一個管道線是作業號2,其中第一個進程的進程ID是1490。當作業已完成並且鍵入回車時,shell通知我們作業已完成。鍵入回車是爲了讓shell打印其提示符。shell並不在任意時間打印後臺作業的狀態改變,它只在打印其提示符之前這樣做。
有三個特殊字符可使終端驅動程序產生信號,並將他們送至前臺進程組,後臺進程組作業不受影響。它們是:
- 終端字符(一般用DELETE或者Ctrl-C)產生SIGINT
- 退出字符(一般用Ctrl-\)產生SIGOUT
- 掛起字符(一般採用Ctrl-Z)產生SIGTSTP
如果後臺作業試圖讀終端,終端驅動程序會檢測這種情況,並且發送一個特定信號SIGTTIN給後臺作業。這通常會停止此後臺作業,而有關用戶則會得到這種情況的通知,然後就可以將此作業轉爲前臺作業運行,於是它就可以讀終端。下面操作過程展示了這種情況:
$ cat > temp.foo & 在後臺啓動,但將從標準輸入讀
[1] 1681
$ 鍵入回車
[1] + Stopped (tty input) cat > temp.out &
$ fg &1 使1號作業成爲前臺作業
cat > temp.foo shell告訴我們現在哪一個作業在前臺
hello, world 輸入1行
^D 鍵入文件描述符
$ cat temp.foo 檢查該行已送入文件
hello, world
shell在後臺啓動cat進程,但是當cat試圖讀其標準輸入(控制終端)時,終端驅動程序知道它是後臺作業,於是將SIGTTIN信號送至該後臺作業。shell檢測到其子進程的狀態變化,並通知我們該作業已被停止。然後,用shell的fg命令將次停止的作業送入前臺運行。這樣做使shell將此作業轉爲前臺進程組(tcsetpgrp),並將繼續信號(SIGCONT)送給該進程組。因爲該作業現在在前臺進程組中,所以它可以讀控制終端。
如果後臺進程輸出到控制終端會發生什麼呢?這是一個可以允許或禁止的選擇項。通常,可以用stty命令來改變這一選項
$ cat temp.foo & 在後臺運行
[1] 1719
$ hello, world 在提示符出現後臺作業的輸出
鍵入回車
[1] + Done cat temp.foo &
$ stty tostop 禁止後臺作業向控制終端輸出
$ cat temp.foo & 在後臺再次運行
[1] 1721
$ 鍵入回車,發現作業已停止
[1] + Stopped(tty output) cat temp.foo &
$ fg %1 將停止的作業恢復爲前臺作業
cat temp.foo shell告訴我們現在哪一個作業在前臺
hello, world 該作業的輸出
下圖摘錄了我們已說明的作業控制的某些功能。穿過終端驅動程序的實線表示:終端I/O和終端產生的信號總是從前臺進程組連接到實際終端。對應於SIGTTOU信號的虛線表示後臺進程組進程的輸出是否出現在終端是可選擇的。
四、shell執行程序
首先使用不支持作業控制的經典的Bourne shell。如果執行:
ps -xj
則其輸出爲: PPID PID PGID SID TPGID COMMAND 1 163 163 163 163 -sh 163 163 163 163 163 ps 結果略去了現在無關的列。shell和ps命令兩者位於同一對話期和前臺進程組(163)中。因爲163是在TGPID列中顯示的進程組,所以稱其爲前臺進程組。
說進程與終端進程組ID(TPGID列)相關聯並不當。進程並沒有終端進程控制組。進程屬於一個進程組,而進程組屬於一個對話期。對話期可能有,也可能沒有控制終端。如果它確有一個控制終端,則此終端設備知道其前臺進程的進程組ID。這一值可以用tcsetpgrp函數在終端驅動程序中設置。前臺進程組ID是終端的一個屬性,而不是進程的屬性。取自終端設備驅動程序的該值是ps在TPGID列中打印的值。如果ps發現此對話期沒有控制終端,則它在該列打印1。
如果在後臺執行命令:
ps -xj &
則唯一改變的值是命令的進程ID。
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 163 163 163 163 ps
因爲這種shell不知道作業控制,所以後臺作業沒有構成另一個進程組,也沒有從後臺作業處取走控制終端。
看一下Bourne shell如何處理管道線。執行下列命令:
ps -xj | cat1
其輸出是:
PPID PID PGID SID TPGID COMMAND
1 163 163 163 163 -sh
163 200 163 163 163 cat1
200 201 163 163 163 ps
(程序cat1只是標準cat程序的一個副本,但名字不同)管道中最後一個進程是shell的子進程,該管道中的第一個進程則是最後一個進程的子進程。從中可以看出,shell fork一個它的副本,然後此副本再爲管道線中的每條命令各fork一個進程。
&nsbp;如果在後臺執行此管道線:
ps -xj | cat1 &
則只有進程ID改變了。因爲shell並不處理作業控制,後臺進程的進程組ID仍是163,如果終端進程組ID一樣。
在沒有作業控制時如果後臺作業試圖讀控制終端,其處理方法是:如果該進程自己不重新定向標準輸入,則shell自動將後臺進程的標準輸入重新定向到/dev/null。讀/dev/null則產生一個文件結束。這就意味着後臺cat進程立即讀到文件尾,並正常結束。
在一條管道中執行三個進程:
ps -xj | cat1 | cat2
該管道中的最後一個進程是shell的子進程,而執行管道中其他命令的進程則是該最後進程的子進程。下圖展示了所發生的情況:
五、孤兒進程組
一個父進程已終止的子進程稱爲孤兒進程(orphan process),這種進程由init進程收養。整個進程組也可以成爲孤兒。 考慮一個進程,它fork了一個子進程然後終止。這在系統中是進場發生的,但是在父進程終止時,如果該子進程停止(用作業控制)該如何?下面的程序就是這種情況的一個例子。下圖顯示了程序已經啓動,父進程已經fork了子進程之後的情況。
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include "ourhdr.h"
static void sig_hup(int);
static void pr_ids(char *);
int main(void)
{
char c;
pid_t pid;
pr_ids("parent");
if ( (pid = fork()) < 0) {
fprint(stderr, "fork error\n");
exit(1);
} else if (pid > 0) {
sleep(5); // sleep 5等待子進程退出
exit(0); // 父進程退出
} else { // 子進程
pr_ids("child");
signal(SIGHUP, sig_hup); //
kill(getpid(), SIGTSTP);
pr_ids("child");
if (read(0, &c, 1) != 1) {
printf("read error from control terminal,errno = %d\n", errno);
}
exit(0);
}
}
static void sig_hup(int signo) {
printf("SIGHUP received, pid = %d\n, getpid()");
return ;
}
static void pr_ids(char *name) {
printf("%s: pid = %d, ppid = %d, pgrp = %d\n", name, getpid(), getppid(), getpgrp());
fflush(stdout);
}
這裏假定使用了一個作業控制shell。shell將前臺進程放在一個進程組中(本例是512),shell則留在自己的組內(442)。子進程繼承其父進程512進程組。在fork後:
- 父進程睡眠5秒,讓子進程在父進程終止之前運行
- 子進程爲掛斷信號(SIGHUP)建立信號處理程序。
- 子進程用kill函數向其自身發送停止信號SIGTSTP。這停止了子進程,類似於用終端掛起字符(Ctrl-Z)停止一個前臺作業。
- 當父進程終止時,該子進程成爲孤兒進程,其父進程ID成爲1,也就是init進程ID。
- 現在,子進程成爲一個孤兒進程組的成員。POSIX.1將孤兒進程組定義爲:該組中每一個成員的父進程或者是該組中的一個成員,或者不是該組所屬對話期的成員。
- 因爲在父進程終止後,進程組成爲孤兒進程組,POSIX.1要求向新孤兒進程組中處於停止狀態的每一個進程發送掛斷信號(SIGHUP),接着又向其發送繼續信號(SIGCONT)。
- 在處理了掛斷信號後,子進程繼續。對掛斷信號的系統默認動作是終止該進程,爲此必須提供一個信號處理函數來捕捉此信號。因此我們期望sig_hup函數中的printf會在pr_id函數中的printf之前執行。
下面是程序的輸出:
因爲兩個進程,登錄shell和子進程都寫向終端,所以shell提示符和子進程的輸出一起出現。
在子進程中調用pr_ids後程序企圖讀標準輸入。正如前述,當後臺進程組試圖讀控制終端時,則對該後臺進程組產生SIGTTIN。但在這裏這是一個孤兒進程組,如果內核用此信號終止它,則此進程組中的進程就再也不會繼續。POSIX.1規定,read返回出錯,其errno設置爲EIO。
在父進程終止時,子進程變成後臺進程組,因爲父進程是由shell作爲前臺作業執行的。