程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> (六) 一起學 Unix 環境高級編程(APUE) 之 進程控制,高級編程apue

(六) 一起學 Unix 環境高級編程(APUE) 之 進程控制,高級編程apue

編輯:關於C語言

(六) 一起學 Unix 環境高級編程(APUE) 之 進程控制,高級編程apue


.

.

.

.

.

目錄

(一) 一起學 Unix 環境高級編程(APUE) 之 標准IO

(二) 一起學 Unix 環境高級編程(APUE) 之 文件 IO

(三) 一起學 Unix 環境高級編程(APUE) 之 文件和目錄

(四) 一起學 Unix 環境高級編程(APUE) 之 系統數據文件和信息

(五) 一起學 Unix 環境高級編程(APUE) 之 進程環境

(六) 一起學 Unix 環境高級編程(APUE) 之 進程控制

 

上一篇博文中我們討論了進程環境,相信大家對進程已經有了初步的認識。

今天討論進程控制這一章,也是進程中最終要的一部分,其實主要就是圍繞著 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)) 函數前父進程處於休眠狀態。

 

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) 宏獲得。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved