標籤:des style blog color 使用 strong
9.1 引言
本章將更詳盡地說明進程組以及POSIX.1引入的會話的概念。還將介紹登陸shell(登入時所調用的)和所有從登陸shell啟動的進程之間的關係。
9.1 終端登陸
系統管理員建立通常名為/etc/ttys的檔案,其中每個終端裝置都有一行,每一行說明裝置名稱傳遞給getty程式的參數。當系統自舉時,核心建立進程ID為1的進程,依舊是init進程。init進程使系統進入多使用者狀態。init進程讀檔案/etc/ttys,對每一個允許登陸的終端裝置,init調用一次fork,所產生的子進程則執行(exec)getty程式。(init以空環境執行getty程式)
getty為終端裝置調用open函數,以讀、寫方式將終端開啟。如果裝置是數據機,則open可能會在裝置驅動程式中滯留,知道使用者撥號數據機,並且呼叫被應答。一旦裝置被開啟,則檔案描述符0、1、2就被設定到該裝置。然後getty輸出“login:”之類的資訊,並等待使用者鍵入使用者名稱。如果終端支援多種速度,則getty可以測試特殊字元以便適當地更改終端速度(傳輸速率)
當使用者鍵入了使用者名稱後,getty的工作就完成了。然後調用login程式。init以一個空環境調用getty。getty以終端名和在gettytab中說明的環境字串為login建立一個環境。
login能執行多項工作。因為它得到了使用者名稱,所以能調用getpwnam取得相應使用者的口令檔案登陸項。然後調用getpass以顯示提示“password:”,接著讀使用者鍵入的口令。它調用crypt將使用者鍵入的口令加密,並與該使用者在陰影口令檔案中登陸想的pw_passwd欄位相比較。如果使用者幾次鍵入的口令都無效,則login以參數1調用exit表示登陸過程失敗。父進程瞭解到子進程終止情況後,將在此調用fork,其後接著執行getty,對此終端重複上述過程。
如果使用者正確登陸,login就將執行如下工作:
-將當前工作目錄更改為該使用者的起始目錄
-調用chown改變該終端的所有權,是登陸使用者成為它的所有者
-將對該終端裝置的存取權限改變成使用者讀和寫
-調用setgid和initgroups設定進程的組ID
-用login所得到的所有資訊初始化環境:起始目錄、shell、使用者名稱,以及一個系統預設路徑
-login進程改變為登陸使用者的使用者ID並調用該使用者的登陸shell
到此為止,登入使用者的登陸shell開始運行。
9.3 網路登陸
通過網路登陸時,終端和電腦之間的串連不是點對點連結。在這種情況下,login只是一種可用的服務,這與其他網路服務的性質相同。
在網路登陸情況下,所有登陸都經由核心的網路介面驅動程式,實現並不知道將會有多少這樣的登陸。我們不是使進程等待每個可能的登陸,而是必須等待一個網路連接請求的到達。
為使同一個軟體既能處理終端login,又能處理網路login,系統使用了一種稱為偽終端的軟體驅動程式,它模擬串列終端的運行行為,並將終端操作映射為網路操作
作為系統啟動的一部分,init調用一個shell,使其執行shell指令碼/etc/rc.由此shell指令碼啟動一個守護進程inetd。一旦此shell指令碼終止,inetd的父進程就變成init。inetd等待TCP/IP串連請求到達主機,而當一個串連請求到達時,它執行一次fork,然後產生的子進程執行適當的程式。
我們假定到達了一個針對TELNET服務的TCP串連請求。TELNET是使用TCP協議的遠程登陸應用程式。在另一台主機(它通過某種形式的網路與服務進程的主機相串連)上的使用者,或者同一台主機上的使用者啟動TELNET客戶進程,由此啟動登陸過程:
telnet hostname
該客戶進程開啟一個到hostname主機的TCP串連,在hostname主機上啟動的程式被稱為TELNET服務進程。然後,客戶進程和服務進程之間使用TELNET應用協議通過TCP串連交換資料。所發生的是啟動客戶進程的使用者現在登入到了服務進程所在的主機。
然後,telnetd進程開啟一個偽終端裝置,並用fork分成兩個進程。父進程處理通過網路連接的通訊,子進程則執行login程式。父子進程通過偽終端相串連。在調用exec之前,子進程使其檔案描述符0,1,2與偽終端相連。如果登陸正確,login就執行:更改當前工作目錄為起始目錄,設定登陸的組ID和使用者ID,以及登陸使用者的初始環境。然後login調用exec將自身替換為登陸使用者的登陸shell
(當通過終端或網路登陸時,我們得到一個登陸shell,其標準輸入、輸出和標準出錯串連到一個終端裝置或者偽終端裝置上。
9.4 進程組
函數getpgrp返回調用進程的進程組ID
#include<unistd.h>pid_t getpgrp(void); //傳回值:調用進程的進程組ID
進程可以通過調用setpgid來加入一個現有的組或者建立一個新進程組
#include<unistd.h>int setpgid(pid_t pid,pid_t pgid); //傳回值:若成功則返回0,若出錯則返回-1
9.5 會話
會話是一個或多個進程組的集合。
通常是shell的管道線將幾個進程編程一組的。進程調用setsid函數建立一個新會話
#include<unistd.h>pid_t setsid(void); //傳回值:若成功則返回進程組ID,若出錯則返回-1
如果調用此函數的進程不是一個進程組的組長,則此函數就會建立一個新會話,結果將發生下面3件事:
(1)該進程編程新會話首進程(會話首進程是建立該會話的進程),此時該進程是新會話中的惟一進程
(2)該進程稱為一個新進程組的組長進程。新進程組ID是該調用進程的進程ID
(3)該進程沒有控制終端。如果在調用setsid之前該進程有一個控制終端,那麼這種聯絡也會被中斷
如果該調用進程已經是一個進程組的組長,則此函數返回出錯。為了保證不發生這種情況,通常先調用fork,然後使其父進程終止,而子進程繼續。因為子進程繼承了父進程的進程組ID,而其進程ID則是新分配的,兩者不可能相等,所以這就保證子進程不會是一個進程組的組長。
9.6 控制終端
會話和進程組有一些其他特性:
-一個會話可以有一個控制終端。這通常是登入到其上的終端裝置(在終端登入情況下)或偽終端裝置(在網路登陸情況下)
-建立與控制終端連結的會話首進程被稱為控制進程
-一個會話中的幾個進程組可被分成一個前台進程組以及一個或幾個後台進程組
-如果一個會話有一個控制終端,則它有一個前台進程組,會話中的其他進程組則為後台進程組
-無論何時鍵入終端的中斷鍵,就會將中斷訊號發送給前台進程組的所有進程
-無論何時鍵入終端的退出鍵,就會將退出訊號發送給前台進程組的所有進程
-如果終端介面檢測到數據機已經中斷連線,則將掛斷訊號發送給控制進程
9.7 tcgetpgrp、tcsetpgrp和tcgetsid函數
需要有一種方法來通知核心哪一個進程組是前台進程組,這樣,終端裝置驅動程式就能瞭解將終端輸入和終端產生的訊號送到何處
#include<unistd.h>pid_t tcgetpgrp(int filedes); //傳回值:若成功則返回前台進程組的進程ID,若出錯則返回-1int tcsetpgrp(int filedes,pid_t pgrpid); //傳回值:若成功則返回0,若出錯則返回-1
9.8 作業控制
作業控制要求下面三種形式的支援:
(1)支援作業控制的shell
(2)核心中的終端驅動程式必須支援作業控制
(3)核心必須提供對某些作業控制訊號的支援
9.9 shell執行程式
管道線中最後一個進程是shell的子進程,該管道線中的第一個進程則是最後一個進程的子進程。當最後一個進程終止時,shell得到通知
9.10 孤兒進程組
我們曾提及,一個其父進程已經終止的進程稱為孤兒進程,這種進程由init進程“收養”。現在我們要說明整個進程組也可稱為“孤兒”
執行個體: 9_1 建立一個孤兒進程組
1 #include"apue.h" 2 #include<errno.h> 3 4 static void sig_hup(int signo) 5 { 6 printf("SIGHUP received,pid=%d\n",getpid()); 7 } 8 static void pr_ids(char *name) 9 {10 printf("%s:pid=%d,ppid=%d,pgrp=%d,tpgrp=%d\n",name,getpid(),getppid(),11 getpgrp(),tcgetpgrp(STDIN_FILENO));12 fflush(stdout);13 }14 int main()15 {16 char c;17 pid_t pid;18 pr_ids("parent");19 if((pid=fork())<0){20 err_sys("fork error");21 }else if(pid>0){22 sleep(5);23 exit(0);24 }25 else{26 pr_ids("child");27 signal(SIGHUP,sig_hup);28 kill(getpid(),SIGTSTP);29 pr_ids("child");30 if(read(STDIN_FILENO,&c,1)!=1)31 printf("read error from controlling TTY,errno=%d\n",errno);32 exit(0);33 }}