進程是程序在內存中運行的一個實例,這裡說出了進程與程序的一個區別就是進程是在內存空間裡的,程序實際上是存儲在你的硬盤上的那個文件,又可以叫做可執行文件。
1. 如何標識一個進程
1)當然是用一個ID來標識一個進程啦,在Linux系統上貌似任何東西的標識都是使用一個整數也就是ID,進程當然也不例外,那麼一個進程除了PID 以外還有哪些標識呢?
-->PID : 最常用的就是 process ID
-->PPID : 所有的進程都有一個父進程 Parent Process ID
-->UID : 用戶ID,誰運行的這個進程
-->EUID : 有效用戶ID,雖然一個進程是由用戶A執行起來的,但是進程擁有的權限卻可以與A不同, 這個EUID是跟可執行文件的S權限息息相關的
-->GID : 組ID
-->EGID :有效組ID
PID與PPID很直白很容易理解, UID/GID也還可以,畢竟進程都是由用戶執行起來的嘛, 但是EUID?EGID是什麼鬼?關於這兩個ID,大家可以使用系統調用getUID() getEUID(),來觀察一下,EUID與UID一般情況下是一樣的,那麼什麼情況下會不一樣呢 ?那就是當設置可執行文件的SUID這個權限的時候,也是使用了chmod u+s XXX。
SUID又是什麼鬼? OK,看下面的實驗吧
首先寫一個測試程序
#include#include int main() { uid_t uid = getuid(); uid_t euid = geteuid(); printf("uid = %d, euid = %d\n", uid, euid); return 0; }
ls -l -rwxrwxr-x 1 gengj gengj 13451 8月 22 10:25 test chown root test chmod u+x test ls -l -rwsrwxr-x 1 root gengj 13451 8月 22 10:25 test看到沒有, 剛開始 test 文件的 用戶和組都是gengj, 這個時候執行test,得到 uid == euid,但是當執行了後邊的操作也就是設置了S權限之後(這個時候實際上test是以root權限來運行的,也就是它的有效用戶是 root),再執行,可以看到euid變成了 0, UID沒有改變。關於GID/EGID與上面的描述是一樣的。
2)現在我們知道了如何獲得關於進程的各種ID,那麼怎麼對這些ID進程設置呢?
實際上PPID是沒有辦法改變的,PID是內核分配的,這兩個ID是不能改變的,也沒有必要改變。可以改變的是UID EUID GID EGID, 這裡我們不討論組只討論用戶 ID, 因為他們的操作是一樣的。
設置UID我們可以使用系統調用 setuid(uid_t uid); 關於這個函數需要說明一下:
-->如果進程擁有root權限的話,該進程調用setuid(uid) 可以把 本進程的UID, EUID, saved set-uid全部設置成參數 uid;
-->如果進程不是root權限,參數uid==UID 或者 uid == SUID的話, 本函數將會把EUID設置成uid,其他的ID都不變。
各種ID搞得人比較暈,我也不是特別的清楚, 不過只要記得不同的UID會使得進程擁有不同的權限,如果設置了EUID, 則表示了進程的實際可以擁有的權限。
2. 創建新的進程 fork
1)系統調用 pid_t fork(void) 是用來創建一個新的子進程的, 這個函數比較特殊,一次調用會返回兩次:
#include#include int main() { pid_t pid; int i = 0; if ((pid = fork()) < 0) { printf("fork failed\n"); return -1; } else if (pid == 0) { // this is child process printf("this is in child process\n"); i++; } else { //this is in parent process sleep(2); printf("this is in parent process\n"); } //this is in bothprocess printf("i = %d\n", i); return 0; }
this is in child process i = 1 this is in parent process i = 0
this is in child process i = 1 this is in parent process i = 0
輸出:
-------------------------------------------------------------------------------------------
this is in child process i = 1 this is in parent process i = 0
this is in child process i = 1 this is in parent process i = 0
this is in child process i = 1 this is in parent process i = 0this is in child process
i = 1
...(sleep 2)
this is in parent process
i = 0
-------------------------------------------------------------------------------------------
如上顯示的, fork返回的pid如果為0 則表示這是從子進程返回的值,如果大於 0則表示是從父進程返回的值,返回值就是子進程的PID。
如上顯示的,fork之後,父子進程都會繼續執行接下來的代碼,也就是說父子進程共享一個代碼段 (text segment),但是數據是不一樣的,因為在子進程對 i 賦值並不會影響父進程中的 i,說明,父子進程的數據段是不一樣的,實際上子進程復制了父進程的數據段,所以導致父進程與子進程都有一個數據 i 的copy,各自的改變並不會影響另一個進程。
2) fork中的文件描述符
父進程中打開的文件描述符將在子進程中同樣有效,也就是說父子進程共享文件描述符。這種共享會導致父子進程產生競爭現象,需要在編程中避免這種競爭。
3)子進程繼承的東東與沒有繼承的東東
先看看有哪些東西沒有被子進程繼承吧:
-- PID & PPID
-- time (tms_utime, tms_stime) 在子進程中被置 0
-- 文件鎖 在子進程中沒有
-- 定時器
-- 信號集
被繼承下來的東西:
-- UID , GID
-- 進程組
-- 會話(session) ID
-- 控制終端
-- set-user-id/ set-group-id
-- root directory
-- current work directory
-- 文件權限掩碼 mask
-- 信號掩碼
-- close-on-exec 標志
-- 環境變量
-- 共享內存
-- 內存映射
4) fork為什麼會失敗
fork失敗意味著系統不能生成新的進程啦,這有可能是由於系統資源不足造成的, 也有可能是一個real user id 的進程數超出了限制造成的。
5)fork 與 exec函數
fork會產生一個新的進程,子進程會有父進程地址空間的一份copy, 但是如果在fork之後的子進程中調用exec函數,那麼子進程將會被新的程序替代,子進程將會從新程序的main函數開始執行。
這裡替換的意思不是創建一個新的進程,而是繼續在原來的地址空間裡面繼續執行,只不過代碼段,數據段,堆棧全部被替換啦,而且當新程序結束後也不再返回到子進程中啦。既然是在原子進程空間裡面運行新的程序,PID當然還是不會改變的啦。
#include#include int main() { pid_t pid; if ((pid = fork()) < 0) { printf("fork failed\n"); return -1; } else if (pid == 0) { // this is child process printf("this is in child process\n"); printf("exec a new program\n");
execlp("ls", "ls", "-1", NULL); // run another program printf(" I am back into child process\n"); // never come back to here } else { sleep(2); printf("this is in parent process\n"); global_num++; } //this is in bothprocess printf("i = %d, global_num = %d\n", i, global_num); return 0; }
parent-process --> fork --> exec
|-----------------------------waitpid()
3 進程結束
結束一個進程有很多種方法:
正常的退出
-- main函數中調用 return 或者 exit(),這兩者是等價的
-- 調用 _exit 或者 _Exit
不正常退出
-- 調用abort, 將會產生SIGABRT信號
-- 接收到特定的信號, 比如運行中我們按下ctl-c也就是發送了TERM 信號給進程
不正常的退出現在先不講,只討論一下正常的退出中的exit _exit _Exit
1) exit 與return一樣是一種比較安全溫和的退出方式,將會flush 所有IO然後關閉所有的文件, 依次調用注冊的at_exit 函數, 最後退出
2)_exit 與_Exit這是一種簡單粗暴的退出方式,不會調用at_exit注冊的函數,也可能不會flush IO (it depends on OS implementation)。
當子進程正常退出時,父進程可以獲得子進程的退出狀態值,退出狀態值是exit函數的參數,當子進程非正常退出時,內核來把退出狀態值送給父進程,總之,父進程總是可以獲得子進程的退出狀態值。 這裡我們考慮以下兩種情況
1)父進程先於子進程退出
如果父進程比子進程先退出,那麼init將會成為子進程的父進程,這是因為Linux要確保每一個進程都有一個父進程
2)子進程先於父進程退出
如果子進程先退出啦,那麼內核會在內存中維護這個子進程的一些信息,以便父進程在調用wait或者waitpid時能夠獲得子進程的退出狀態,一般來講內核保存的信息必然會包括PID和退出狀態值, 因為這些都是wait函數需要的。
子進程退出後,如果父進程調用了wait或者waitpid,那麼內核就不需要在保存這些殘留信息了,那麼這個子進程就完全退出了。如果父進程不調用wait&waitpid,那麼這個子進程就變成了僵屍進程(zombie)。
是時候說說wait & waitpid函數了,這兩個函數都會使得內核對子進程進行清理,也就是消除zombie。 我們先來講講這兩個函數的特點吧
1)wait: 阻塞的,直到有一個子進程退出他才返回,實際上任何一個子進程的退出都會導致wait返回,也就是說,如果有很多子進程就需要調用很多次wait;如果沒有子進程的話,wait也會立即返回,只不過是返回error。
2) waitpid: 相對於wait,waitpid比較人性化,他會等待特定的有其參數指定的進程退出。他可以是阻塞的也可以通過其參數配置成非阻塞的。haha, 可以配置的這個特性不錯,大家可以試試看,這裡不多說。
總結
這篇文章講述了進程的創建fork exec, 進程的退出 exit, 如何避免僵屍進程waitpid,並給了簡單的demo來說明fork等的應用。本文並沒有講述在什麼情形中使用子進程這個特性,算是不足之處,以後盡量補上。
歡迎閱讀提問,如果有不對的地方,請指正。