.html
.python
.程序員
.shell
.編程
目錄安全
(一) 一塊兒學 Unix 環境高級編程 (APUE) 之 標準IObash
(二) 一塊兒學 Unix 環境高級編程 (APUE) 之 文件 IO網絡
(三) 一塊兒學 Unix 環境高級編程 (APUE) 之 文件和目錄ide
(四) 一塊兒學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息函數
(五) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程環境
(六) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程控制
(七) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程關係 和 守護進程
(八) 一塊兒學 Unix 環境高級編程 (APUE) 之 信號
(九) 一塊兒學 Unix 環境高級編程 (APUE) 之 線程
(十) 一塊兒學 Unix 環境高級編程 (APUE) 之 線程控制
(十一) 一塊兒學 Unix 環境高級編程 (APUE) 之 高級 IO
(十二) 一塊兒學 Unix 環境高級編程 (APUE) 之 進程間通訊(IPC)
(十三) [終篇] 一塊兒學 Unix 環境高級編程 (APUE) 之 網絡 IPC:套接字
上一篇博文中咱們討論了進程環境,相信你們對進程已經有了初步的認識。
今天討論進程控制這一章,也是進程中最終要的一部分,其實主要就是圍繞着 fork(2)、exec(2)、wait(2) 這三個函數來討論 *nix 系統是如何管理進程的。
ps(1) 命令能夠幫助咱們分析本章中的一些示例,因此簡單介紹一些參數的組合方式,更詳細的信息請查閱 man 手冊。
ps axf 主要用於查看當前系統中進程的 PID 以及執行終端(tty)和狀態等信息,更重要的是它能顯示出進程的父子關係。
ps axj 主要用於查看當前系統中進程的 PPID、PID、PGID、SID、TTY 等信息。
ps axm 顯示進程的詳細信息,PID 列下面的減號(-)是這個進程中的線程。
ps ax -L 以 Linux 的形式顯示當前系統中的進程列表。
PID 是系統中進程的惟一標誌,在系統中使用 pid_t 類型表示,它是一個非負整型。
1號 init 進程是全部進程的祖先進程(但不必定是父進程),內核啓動後會啓動 init 進程,而後內核就會像一個庫同樣守在後臺等待出現異常等狀況的時候再出來處理一下,其它的事情都由 init 進程建立子進程來完成。
進程號是不斷向後使用的,當進程號達到最大值的時候,再回到最小一個可用的數值從新使用。
在講 fork(2) 函數以前先來認識兩個函數:
1 getpid, getppid - get process identification 2 3 #include <sys/types.h> 4 #include <unistd.h> 5 6 pid_t getpid(void); 7 pid_t getppid(void);
getpid(2) 得到當前進程 ID。
getppid(2) 得到父進程 ID。
如今輪到咱們今天的主角之一:frok(2) 函數上場了。
1 fork - create a child process 2 3 #include <unistd.h> 4 5 pid_t fork(void);
fork(2) 函數的做用就是建立子進程。
調用 fork(2) 建立子進程的時候,剛開始父子進程是如出一轍的,就連代碼執行到的位置都是如出一轍的。
fork(2) 執行一次,但返回兩次。它在父進程中的返回值是子進程的 PID,在子進程中的返回值是 0。子進程想要得到父進程的 PID 須要調用 getppid(2) 函數。
通常來講調用fork後會執行 if(依賴fork的返回值) 分支語句,用來區分下面的哪些代碼由父進程執行,哪些代碼由子進程執行。
咱們畫幅圖來輔助解釋上面說的一大坨是什麼意思。
圖1 fork(2) 與寫時拷貝
結合上圖,咱們來聊聊 fork(2) 的前世此生。
最初的 frok(2) 函數在建立子進程的時候會把父進程的數據空間、堆和棧的副本等數據通通給子進程拷貝一份,若是父進程攜帶的數據量特別大,那麼這種狀況建立子進程就會比較耗費資源。
這還不是最要命的,萬一費了這麼大勁建立了一個子進程出來,結果子進程沒有使用父進程給它的數據,而只是打印了一句 「Hello World!」 就結束退出了,豈不是白白的浪費了以前的資源開銷?
因而聰明的程序猿們想出了一個辦法來替代:讓父子進程共享同一塊數據空間,這樣建立子進程的時候就沒必要擔憂複製數據耗費的資源較高的問題了,這就是傳說中的 vfork(2) 函數實現的效果。
那麼問題來了,若是子進程修改了數據會發生什麼狀況呢?Sorry,這個標準裏沒說,天知道會發生什麼事情,因此 vfork(2) 一般被認爲是過期了的函數,已經不推薦你們使用了。
既然上面兩個辦法都不完美,程序猿們只好再次改良 fork(2) 函數,此次雖然效率稍微比 vfork(2) 稍稍低了那麼一點點,可是安全性是能夠保證的,這就是寫時拷貝技術。
寫時複製(Copy-On-Write,COW)就是 圖1 裏下面的部分,fork(2) 函數剛剛建立子進程的時候父子進程的數據指向同一塊物理內存,可是內核將這些內存的訪問變爲只讀的了,當父子進程中的任何一個想要修改數據的時候,內核會爲修改區域的那塊內存製做一個副本,並將本身的虛擬地址映射到物理地址的指向修改成副本的地址,今後父子進程本身玩本身的,誰也不影響誰,效率也提升了許多。新分配的副本大小一般是虛擬存儲系統中的一「頁」。
固然,寫是複製技術中所謂製做一個副本,這個是在物理地址中製做的,並不是是咱們在程序中拿到的那個指針所指向的地址,咱們的指針所指向的地址實際上是虛擬地址,因此這些動做對用戶態程序員是透明的,不須要咱們本身進行管理,內核會自動爲咱們打點好一切。
好了,羅嗦了這麼多都是說父進程經過複製一份本身建立了子進程,難道父子進程就是如出一轍的嗎?
固然其實父子進程之間是有五點不一樣的:
(1) fork(2) 的返回值不一樣;
(2) 父子進程的 PID 不相同;
(3) 父子進程的 PPID 不相同; // PPID 就是父進程 PID
(4) 在子進程中資源的利用量清零,不然若是父進程打開了不少資源,子進程能使用的資源量就不多了;
(5) 未決信號和文件鎖不繼承。
父進程與子進程誰先運行是不肯定的,這個執行順序是由進程調度器決定的,不過 vfork(2) 會保證子進程先運行。進程調度器不是一個工具,是在內核中的一塊代碼。
寫個簡單的小栗子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 #include <unistd.h> 6 7 int main (void) 8 { 9 pid_t pid; 10 11 printf("[%d] Start!\n", getpid()); 12 13 pid = fork(); 14 if (pid < 0) { 15 perror("fork()"); 16 exit(1); 17 } else if (0 == pid) { // child 18 printf("[%d] Child process.\n", getpid()); 19 } else { // parent 20 printf("[%d] Parent process.\n", getpid()); 21 } 22 23 sleep(1000); 24 25 puts("End!"); 26 27 return 0; 28 }
執行結果:
1 >$ make 1fork 2 cc 1fork.c -o 1fork 3 >$ ./1fork 4 [3713] Start! 5 [3714] Child process. 6 [3713] Parent process.
7 [3713] End!
8 [3714] End!
新打開一個終端,驗證它們的父子進程關係:
1 >$ ps axf 2 3565 pts/1 Ss 0:00 \_ bash 3 3713 pts/1 S+ 0:00 | \_ ./1fork 4 3714 pts/1 S+ 0:00 | \_ ./1fork
從 ps(1) 命令能夠看出來,3713 進程確實產生了一個子進程 3714。
可是這裏面有一個問題,咱們從新執行一遍這個程序,此次將輸出重定向到文件中。
1 >$ ./1fork > result.txt 2 >$ cat result.txt 3 [3807] Start! 4 [3807] Parent process. 5 End! 6 [3807] Start! 7 [3808] Child process. 8 End!
發現有什麼不一樣了嗎?父進程居然輸出了兩次 Start!,這是爲何呢?
其實第二次 Start! 並非父進程輸出的,而是子進程輸出的。可是爲何 PID 倒是父進程的呢?
其實這是由於行緩衝變成了全緩衝致使的,以前咱們講過,標準輸出是行緩衝模式,而系統默認的是全緩衝模式。因此當咱們將它輸出到控制檯的時候是能夠獲得預期結果的,可是一旦重定向到文件的時候就由行緩衝模式變成了全緩衝模式,而子進程產生的時候是會複製父進程的緩衝區的數據的,因此子進程刷新緩衝區的時候子進程也會將從父進程緩衝區中複製到的內容刷新出來。所以,在使用 fork(2) 產生子進程以前必定要使用 fflush(NULL) 刷新全部緩衝區!
那麼你們再考慮一個問題,當程序運行的時候,爲何子進程的輸出結果是在當前 shell 中,而沒有打開一個新的 shell 呢?
這是由於子進程被建立的時候會複製父進程全部打開的文件描述符,所謂的「複製」是指就像執行了 dup(2) 函數同樣,父子進程每一個相同的打開的文件描述符共享一個文件表項。
而父進程默認開啓了 0(stdin)、1(stdout)、2(stderr) 三個文件描述符,因此子進程中也一樣存在這三個文件描述符。
既然子進程會複製父進程的文件描述符,也就是說若是父進程在建立子進程以前關閉了三個標準的文件描述符,那麼子進程也就沒有這三個文件描述符可使用了。
從上面的 ps(1) 命令執行結果能夠看出來,咱們的父進程是 bash 的子進程,因此咱們父進程的三個標準文件描述符是從 bash 中複製過來的。
再看一個栗子:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 #include <sys/types.h> 6 7 int main (void) 8 { 9 pid_t pid; 10 int i = 0; 11 12 for (i = 0; i < 10; i++) { 13 fflush(NULL); 14 pid = fork(); 15 if (pid < 0) { 16 perror("fork()"); 17 exit(1); 18 } else if (0 == pid) { 19 printf("pid = %d\n", getpid()); 20 exit(0); 21 } 22 } 23 24 sleep(100); 25 26 return 0; 27 }
運行:
1 >$ make 2fork 2 cc 2fork.c -o 2fork 3 >$ ./2fork 4 pid = 5101 5 pid = 5103 6 pid = 5105 7 pid = 5107 8 pid = 5108 9 pid = 5106 10 pid = 5104 11 pid = 5102 12 pid = 5110 13 pid = 5109 14 # ... 這裏父進程處於 sleep 狀態,便於咱們新打開一個終端查看進程狀態 15 >$ ps axf 16 3565 pts/1 Ss 0:00 \_ bash 17 5100 pts/1 S+ 0:00 | \_ ./2fork 18 5101 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 19 5102 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 20 5103 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 21 5104 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 22 5105 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 23 5106 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 24 5107 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 25 5108 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 26 5109 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 27 5110 pts/1 Z+ 0:00 | \_ [2fork] <defunct> 28 >$
從執行結果來看,子進程的狀態已經變爲 Z+ 了,說明子進程執行完成以後變成了「殭屍進程」。
那麼爲何子進程會變爲殭屍進程呢?是由於子進程比父進程先結束了,它們必須得等待父進程爲其「收屍」才能完全釋放。
在現實世界中白髮人送黑髮人一般會被認爲是件不吉利的事情,可是在計算機的世界中,父進程是須要爲子進程收屍的。
若是父進程先結束了,那麼這些子進程的父進程會變成 1 號 init 進程,當這些子進程運行結束時會變成殭屍進程,而後 1 號 init 進程就會及時爲它們收屍。
咱們修改下上面的栗子,將 sleep(100) 這行代碼移動到子進程中,讓父進程建立完子進程後直接退出,使子進程變成孤兒進程。代碼很簡單我就不重複貼出來了,直接貼測試的結果。
1 >$ make 2fork 2 cc 2fork.c -o 2fork 3 >$ ./2fork 4 pid = 5245 5 pid = 5247 6 pid = 5251 7 pid = 5254 8 >$ pid = 5252 # 這裏會輸出一個提示符,是由於父進程退出了,shell 已經爲咱們的父進程收屍了,因此提示符被輸出了。而咱們的父進程沒有爲子進程收屍,因此子進程會繼續輸出。 9 pid = 5250 10 pid = 5253 11 pid = 5248 12 pid = 5249 13 pid = 5246 14 15 # 下面咱們打開一個新的 shell 查看進程狀態 16 >$ ps -axj 17 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 18 1 5296 5295 3565 pts/1 3565 S 501 0:00 ./2fork 19 1 5297 5295 3565 pts/1 3565 S 501 0:00 ./2fork 20 1 5298 5295 3565 pts/1 3565 S 501 0:00 ./2fork 21 1 5299 5295 3565 pts/1 3565 S 501 0:00 ./2fork 22 1 5300 5295 3565 pts/1 3565 S 501 0:00 ./2fork 23 1 5301 5295 3565 pts/1 3565 S 501 0:00 ./2fork 24 1 5302 5295 3565 pts/1 3565 S 501 0:00 ./2fork 25 1 5303 5295 3565 pts/1 3565 S 501 0:00 ./2fork 26 1 5304 5295 3565 pts/1 3565 S 501 0:00 ./2fork 27 1 5305 5295 3565 pts/1 3565 S 501 0:00 ./2fork 28 >$
從上面 ps(1) 命令的執行結果來看,全部子進程的父進程都變成了 1 號 init 進程。
不少人會認爲殭屍進程不該該出現,它們會佔用大量的資源。其實否則,它們在內核中僅僅保留一個結構體,也就是自身的狀態信息,其它的資源都釋放了。可是它佔用了一個重要的系統資源:PID,由於系統中 PID 的數量是有限的,因此及時釋放殭屍進程仍是頗有必要的。
咱們的父進程沒有對子進程進行收屍,因此纔會出現這樣的狀況。其實對於這種轉瞬即逝的程序而言不會有什麼危害,可是假設父進程是一個要不斷執行一個月的程序,而它卻又不爲子進程收屍,那麼子進程就會佔用這些 PID 一個月之久,那麼就可能出現問題了。
因此在一個完善的程序中,父進程是要爲子進程收屍的,至於如何爲子進程收屍,下面咱們會講,fork(2) 函數就先討論到這裏。
vfork(2)
1 vfork - create a child process and block parent 2 3 #include <sys/types.h> 4 #include <unistd.h> 5 6 pid_t vfork(void); 7 8 Feature Test Macro Requirements for glibc (see feature_test_macros(7)): 9 10 vfork(): 11 Since glibc 2.12: 12 _BSD_SOURCE || 13 (_XOPEN_SOURCE >= 500 || 14 _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) && 15 !(_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700) 16 Before glibc 2.12: 17 _BSD_SOURCE || _XOPEN_SOURCE >= 500 || 18 _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED
vfork(2) 函數在上面介紹寫時拷貝技術的時候咱們就提到了它的工做方式,而且也說了這是一個過期的函數,不推薦你們使用了,簡單瞭解一下就能夠了。
使用 vfork(2) 函數建立的子進程除了與父進程共享數據外,vfork(2) 還保證子進程先運行,在子進程調用 exec(3) 函數族 或 exit(3)(_exit(2)、_Exit(2)) 函數前父進程處於休眠狀態。
另外,使用 vfork(2) 建立的子進程是不容許使用 return 語句返回的,只能使用 exit(3) 函數族的函數結束,不然會被信號殺死,父進程則不受這個限制。
wait(2)
1 wait, waitpid, waitid - wait for process to change state 2 3 #include <sys/types.h> 4 #include <sys/wait.h> 5 6 pid_t wait(int *status); 7 8 pid_t waitpid(pid_t pid, int *status, int options);
wait(2) 阻塞的等待子進程資源的釋放,至關於上面提到的「收屍」。
每次調用 wait(2) 函數會爲一個子進程收屍,而 wait(2) 函數並無讓咱們指定是哪一個特定的子進程。若是想要爲特定的子進程收屍,須要調用 waitpid(2) 函數。
收屍這件事只能是父進程對子進程作,並且只能對本身的子進程作。子進程是不能爲父進程收屍的,父進程也不能爲別人的子進程收屍。
參數列表:
status:由函數回填,表示子進程的退出狀態。若是填 NULL,表示僅回收資源,並不關心子進程的退出狀態。
status 參數可使用如下的宏進行解析:
宏 | 描述 |
WIFEXITED(status) | 返回真表示子進程正常終止,返回假表示子進程異常終止。正常與異常終止的8種方式上面講過。 |
WEXITSTATUS(status) | 返回子進程的退出碼。只有上一個宏返回正常終止時才能使用,異常終止是不會有返回值的。 |
WTERMSIG(status) | 能夠得到子進程具體被哪一個信號殺死了。 |
WIFSTOPPED(status) | 子進程是否被信號 stop 了。stop 和殺死是不一樣的,stop 的進程能夠被恢復(resumed)。 |
WSTOPSIG(status) | 若是子進程是被信號 stop 了,能夠查看具體是被哪一個信號 stop 了。 |
WIFCONTINUED(status) | 若是子進程被 stop 了,能夠查看它是否被 resumed 了。 |
表1 解析 wait(2) 函數 status 參數的宏
pid:一共分爲四種狀況:
pid 參數 | 解釋 |
< -1 | 爲歸屬於進程組 ID 爲 pid 參數的絕對值的進程組中的任何一個子進程收屍 |
== -1 | 爲任意一個子進程收屍 |
== 0 | 爲與父進程同一個進程組中的任意一個子進程收屍 |
> 0 | 爲一個 PID 等於參數 pid 的子進程收屍 |
表2 wait(2) 函數 pid 參數的取值說明
options:爲特殊要求;這個參數是這個函數的設計精髓。能夠經過 WNOHANG 宏要求 waitpid(2) 函數以非阻塞的形式爲子進程收屍,這個也是最經常使用的特殊要求。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 #include <sys/types.h> 6 #include <sys/wait.h> 7 8 int main (void) 9 { 10 pid_t pid; 11 int i = 0; 12 13 for (i = 0; i < 10; i++) { 14 fflush(NULL); 15 pid = fork(); 16 if (pid < 0) { 17 perror("fork()"); 18 exit(1); 19 } else if (0 == pid) { 20 printf("pid = %d\n", getpid()); 21 exit(0); 22 } 23 } 24 25 // 爲全部的子進程收屍 26 for (i = 0; i < 10; i++) { 27 wait(NULL); 28 } 29 30 return 0; 31 }
你們有沒有想過爲何要由父進程爲子進程收屍呢,爲何不讓子進程結束後自動釋放全部資源?試想若是沒有收屍這步會發生什麼。
假設父進程須要建立一個子進程而且要讓它作 3 秒鐘的事情,很不巧子進程剛啓動就出現了一個異常而後就掛掉了,而且直接釋放了本身的資源。
而此時系統 PID 資源緊張,很快已死掉的子進程的 PID 被分配給了其它進程,而父進程此時並不知道手裏的子進程的 PID 已經不屬於它了。
若是這時候父進程後悔執行子進程了,它要 kill 掉這個子進程。。。。。後果就是系統大亂對吧。
而使用了收屍技術以後,子進程狀態改變時會給父進程發送一個 SIGCHLD 信號,wait(2) 函數其實就是阻塞等待被這個信號打斷,而後爲子進程收屍。
系統經過收屍這種機制來保證父進程未執行收屍動做以前,手裏拿到的子進程 PID 必定是有效的了(即便子進程已死掉,可是這個 PID 依然是屬於父進程的子進程的,而不會歸屬於別人)。
終於輪到咱們今天第三個主角:exec(3) 函數上場了。
1 execl, execlp, execle, execv, execvp, execvpe - execute a file 2 3 #include <unistd.h> 4 5 extern char **environ; 6 7 int execl(const char *path, const char *arg, ...); 8 int execlp(const char *file, const char *arg, ...); 9 int execle(const char *path, const char *arg, 10 ..., char * const envp[]); 11 int execv(const char *path, char *const argv[]); 12 int execvp(const char *file, char *const argv[]); 13 int execvpe(const char *file, char *const argv[], 14 char *const envp[]); 15 16 Feature Test Macro Requirements for glibc (see feature_test_macros(7)): 17 18 execvpe(): _GNU_SOURCE
咱們再來看上面第一個 fork(2) 代碼的栗子執行時使用 ps -axf 命令查看父子依賴關係的結果:
1 >$ ps axf 2 3565 pts/1 Ss 0:00 \_ bash 3 3713 pts/1 S+ 0:00 | \_ ./1fork 4 3714 pts/1 S+ 0:00 | \_ ./1fork 5 >$
咱們知道 fork(2) 建立出來的子進程是經過複製父進程的形式實現的,可是咱們的父進程又是 bash 的子進程,爲何 bash 沒有建立出來一個與本身如出一轍的子進程呢?
這就是 exec(3) 函數族的功勞了。
它可使調用的它進程「外殼」不變,「內容物」改變爲新的東西。「外殼」就是父子關係、PID 等東西,「內容物」實際上是指一個新的可執行程序。也就是說 exec(3) 函數會將調用它的進程徹底(整個4GB虛擬內存空間,即代碼段、數據段、堆棧等等)變成另外一個可執行程序,但父子關係、PID 等東西不會改變。
在執行了 exec(3) 函數族的函數以後,整個進程的地址空間會當即被替換,因此 exec(3) 下面的代碼所有都不會再執行了,替代的是新程序的代碼段。
緩衝區也會被新的程序所替換,因此在執行 exec(3) 以前要使用 fflush(NULL) 刷新全部的緩衝區。這樣父進程纔會讓它緩衝區中的數據到達它們該去的地方,而不是在數據到達目的地以前緩衝區就被覆蓋掉。
參數列表:
path:要執行的二進制程序路徑
arg:傳遞給 path 程序的 argv 參數,第一個是 argv[0],其它參數從第二個開始。
...:argv 的後續參數,最後一個參數是 NULL,表示變長參數列表的結束。
看上去 execl(3)、execlp(3) 像是變參函數,execle(3) 像是定參函數,其實正好是反過來的,execl(3) 和 execlp(3) 是定參的,而 execle(3) 函數是變參的。
下面咱們來看一個 fork(2) + exec(3) + wait(2) 最經典的用法:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 /** 6 * 建立子進程 date,參數是 +%s 7 * 至關於在 shell 中執行 date +%s 命令 8 */ 9 int main() 10 { 11 pid_t pid; 12 13 puts("Begin!"); 14 15 fflush(NULL); 16 17 pid = fork(); 18 if(pid < 0) 19 { 20 perror("fork()"); 21 exit(1); 22 } 23 24 if(pid == 0) // child 25 { 26 execl("/bin/date","date","+%s",NULL); 27 perror("execl()"); 28 exit(1); 29 } 30 31 wait(NULL); 32 33 puts("End!"); 34 35 exit(0); 36 }
fork(2)、exec(3)、wait(2) 函數可讓咱們建立任何進程來執行任何命令了,如此看來,整個 *nix 世界都是由 fork(2)、exec(3)、wait(2) 這三個函數搭建起來的,如今你們能夠嘗試用這三個函數來執行一些命令了。
shell 的內部命令與外部命令
像 cd(1)、exit(2)、|、> 牽涉到環境變量改變等動做這樣的命令叫作內部命令,而使用 which(1) 命令能查詢到的在磁盤上存在的命令就是外部命令。
學會了 fork(2)、exec(3)、wait(2) 函數的使用,你們已經能夠嘗試編寫一個 shell 程序了,基本能夠執行全部的外部命令了。
可是一個 shell 不只僅支持外部命令,還支持不少內部命令,對內部命令的支持纔是 shell 的難點。
關於內部命令的內容多數都在《APUE》第三版 的第九章中,感興趣的童鞋能夠自行查閱。
更改用戶 ID 和更改組 ID
在 *nux 系統中,特權和訪問控制是基於用戶 ID 和用戶組 ID 的,因此當咱們須要使用特權或訪問無權訪問的文件時須要切換用戶 ID 或用戶組 ID。
uid
r(real) 用於保存用戶權限
e(effective) 鑑定用戶權限時使用
s 與 real 相同,因此有些系統不支持
gid
r(real) 用於保存用戶組權限
e(effective) 鑑定用戶組權限時使用
s 與 real 相同,因此有些系統不支持
好比普通用戶沒有查看 /etc/shadow 文件,可是爲何有權限修改本身的密碼呢?
1 >$ which passwd 2 /usr/bin/passwd 3 >$ ls -l /usr/bin/passwd 4 -rwsr-xr-x 1 root root 47032 2月 17 2014 /usr/bin/passwd 5 $ ls -l /etc/shadow 6 ---------- 1 root root 1899 Apr 1 16:25 /etc/shadow
這是由於 passwd(1) 命令是具備 U+S 權限的,用戶在使用這個程序的時候身份會切換爲這個程序文件全部者的身份。
G+S 與 U+S 相似,只不過執行的瞬間身份會切換爲與程序歸屬用戶組相同的組權限。
改變用戶 ID 和組 ID 可使用 setuid(2) 和 setgid(2) 函數實現,這兩個函數使用起來都比較簡單,須要用的童鞋本身查閱 main 手冊吧。
解釋器文件
解釋器文件其實就是腳本。
作一個系統級開發工程師須要具有的素質至少精通2門語言,一門面向過程,一門面向對象,還要精通至少一門腳本語言,如 shell、python等,還要具有紮實的網絡知識和一點硬件知識。
解釋器是一個二進制的可執行文件。就是爲了用一個可執行的二進制文件解釋執行解釋器文件中的命令。
#! 用於裝載解釋器
例如:
#!/bin/shell 裝載了 /bin/shell 做爲解釋器
#!/bin/cat 裝載了 /bin/cat 做爲解釋器
那麼裝載解釋器以後爲何不會遞歸執行裝載解釋器這行代碼呢?由於根據約定,腳本中的 # 表示註釋,因此解釋器在解析這個腳本的時候不會看到這行裝載解釋器的命令。
裝載解釋器的步驟由內核 exec(3) 系統調用來完成,若是使用 exec(3) 函數來調用解釋器文件,實際上 exec(3) 函數並無執行解釋器文件,而是執行了解釋器文件中裝載的解釋器,由它來執行解釋器文件中的指令。
system(3)
1 system - execute a shell command 2 3 #include <stdlib.h> 4 5 int system(const char *command);
該函數能夠執行一條系統命令,是經過調用 /bin/sh -c command 實現的。
其實咱們能夠猜想一下 system(3) 命令是如何實現的,下面是僞代碼:
1 pid_t pid; 2 3 pid = fork(); 4 if(pid < 0) 5 { 6 perror("fork()"); 7 exit(1); 8 } 9 10 if(pid == 0) // child 11 { 12 // system("date +%s"); 13 execl("/bin/sh","sh","-c","date +%s",NULL); 14 perror("execl()"); 15 exit(1); 16 } 17 18 wait(NULL); 19 20 exit(0);
進程會計
連 POSIX 標準都不支持,是方言中的方言。
它是典型的事實標準,各個系統的實現都不統一,因此建議少用爲妙。
1 acct - switch process accounting on or off 2 3 #include <unistd.h> 4 5 int acct(const char *filename);
做用是將進程的相關信息寫入到 filename 所指定的文件中。
用戶標識
1 getlogin, getlogin_r, cuserid - get username 2 3 #include <unistd.h> 4 5 char *getlogin(void); 6 int getlogin_r(char *buf, size_t bufsize);
可以不受任何因素影響的獲取當前終端的用戶名。
不受任何因素影響是指,好比咱們用 su(1) 等命令切換了用戶,getlogin(3) 函數得到到的仍然是原始的用戶名。
進程調度
用於控制進程調度優先級,通常不會調整進程的優先級。
進程調度
1 times - get process and waited-for child process times 2 3 #include <sys/times.h> 4 5 clock_t times(struct tms *buffer);
該函數得到的是進程的執行時間。
clock_t 是滴答數。位於秒級如下,具體的與秒的換算值須要經過 sysconf(_SC_CLK_TCK) 宏得到。