進程控制塊(PCB)
在Linux中task_struct結構體即是PCB。PCB是進程的唯一標識,PCB由鏈表實現(為了動態插入和刪除)。
進程創建時,為該進程生成一個PCB;進程終止時,回收PCB。
PCB包含信息:1、進程狀態(state);2、進程標識信息(uid、gid);3、定時器(time);4、用戶可見寄存器、控制狀態寄存器、棧指針等(tss)
每個進程都有一個
非負的
唯一進程ID(PID)。雖然是唯一的,但是PID可以重用,當一個進程終止后,其他進程就可以使用它的PID了。
PID為0的進程為調度進程,該進程是內核的一部分,也稱為系統進程;PID為1的進程為init進程,它是一個普通的用戶進程,但是以超級用戶特權運行;PID為2的進程是頁守護進程,負責支持虛擬存儲系統的分頁操作。
除了PID,每個進程還有一些其他的標識符:
五種進程狀態轉換如下圖所示:
每個進程的task_struct和系統空間堆棧存放位置如下:兩個連續的物理頁【《Linux內核源代碼情景分析》271頁】
系統堆棧空間不能動態擴展,在設計內核、驅動程序時要避免函數嵌套太深,同時不宜使用太大太多的局部變量,因為局部變量都是存在堆棧中的。
進程的創建
新進程的創建,首先在內存中為新進程創建一個task_struct結構,然后將父進程的task_struct內容復制其中,再修改部分數據。分配新的內核堆棧、新的PID、再將task_struct 這個node添加到鏈表中。所謂創建,實際上是“復制”。
子進程剛開始,內核並沒有為它分配物理內存,而是以只讀的方式共享父進程內存,只有當子進程寫時,才復制。即“copy-on-write”。
fork都是由do_fork實現的,do_fork的簡化流程如下圖:
fork函數
fork函數時調用一次,返回兩次。在父進程和子進程中各調用一次。
子進程中返回值為0,父進程中返回值為子進程的PID。程序員可以根據返回值的不同讓父進程和子進程執行不同的代碼。
一個形象的過程:
運行這樣一段演示程序:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 pid_t pid; 8 char *message; 9 int n = 0; 10 pid = fork(); 11 while(1){ 12 if(pid < 0){ 13 perror("fork failed\n"); 14 exit(1); 15 } 16 else if(pid == 0){ 17 n--; 18 printf("child's n is:%d\n",n); 19 } 20 else{ 21 n++; 22 printf("parent's n is:%d\n",n); 23 } 24 sleep(1); 25 } 26 exit(0); 27 }
運行結果:
可以發現子進程和父進程之間並沒有對各自的變量產生影響。
一般來說,fork之后父、子進程執行順序是不確定的,這取決於內核調度算法。進程之間實現同步需要進行進程通信。
什么時候使用fork呢?
一個父進程希望子進程同時執行不同的代碼段,這在網絡服務器中常見——父進程等待客戶端的服務請求,當請求到達時,父進程調用fork,使子進程處理此請求。
一個進程要執行一個不同的程序,一般fork之后立即調用exec
vfork函數
vfork與fork對比:
相同:
返回值相同
不同:
fork創建子進程,把父進程數據空間、堆和棧
復制一份;vfork創建子進程,與父進程內存數據
共享;
vfork先保證子進程先執行,當子進程調用exit()或者exec后,父進程才往下執行
為什么需要vfork?
因為用vfork時,一般都是緊接着調用exec,所以不會訪問父進程數據空間,也就不需要在把數據復制上花費時間了,因此vfork就是”為了exec而生“的。
運行這樣一段演示程序:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 pid_t pid; 8 char *message; 9 int n = 0; 10 int i; 11 pid = vfork(); 12 for(i = 0; i < 10; i++){ 13 if(pid < 0){ 14 perror("fork failed\n"); 15 exit(1); 16 } 17 else if(pid == 0){ 18 n--; 19 printf("child's n is:%d\n",n); 20 if(i == 1) 21 _exit(0); 22 //return 0; 23 //exit(0); 24 } 25 else{ 26 n++; 27 printf("parent's n is:%d\n",n); 28 } 29 sleep(1); 30 } 31 exit(0); 32 }
運行結果:
可以發現子進程先被執行,exit后,父進程才被執行,同時子進程改變了父進程中的數據
子進程return 0 會發生什么?
運行結果:
從上面我們知道,結束子進程的調用是exit()而不是return,如果你在vfork中return了,那么,這就意味main()函數return了,注意因為函數棧父子進程共享,所以整個程序的棧就跪了。 如果你在子進程中return,那么基本是下面的過程: 1)子進程的main() 函數 return了,於是程序的函數棧發生了變化。 2)而main()函數return后,通常會調用 exit()或相似的函數(如:_exit(),exitgroup()) 3)這時,父進程收到子進程exit(),開始從vfork返回,但是尼瑪,老子的棧都被你子進程給return干廢掉了,你讓我怎么執行?(注:棧會返回一個詭異一個棧地址,對於某些內核版本的實現,直接報“棧錯誤”就給跪了,然而,對於某些內核版本的實現,於是有可能會再次調用main(),於是進入了一個無限循環的結果,直到vfork 調用返回 error) 好了,現在再回到 return 和 exit,return會釋放局部變量,並彈棧,回到上級函數執行。exit直接退掉。如果你用c++ 你就知道,return會調用局部對象的析構函數,exit不會。(注:exit不是系統調用,是glibc對系統調用 _exit()或_exitgroup()的封裝) 可見,子進程調用exit() 沒有修改函數棧,所以,父進程得以順利執行
【《vfork掛掉的一個問題》
http://coolshell.cn/articles/12103.html#more-12103】
execve
可執行文件裝入內核的linux_binprm結構體。
進程調用exec時,該進程執行的程序完全被替換,新的程序從main函數開始執行。因為調用exec並不創建新進程,只是替換了當前進程的代碼區、數據區、堆和棧。
六種不同的exec函數:
當指定filename作為參數時:
如果filename中包含/,則將其視為路徑名。
否則,就按系統的PATH環境變量,在它所指定的各個目錄中搜索可執行文件。
*出於安全方面的考慮,有些人要求在搜索路徑中不要包括當前目錄。
在這6個函數中,只有execve是內核的系統調用。另外5個只是庫函數,他們最終都要調用該系統調用,如下圖所示:
execve的實現由do_execve完成,簡化的實現過程如下圖:
關於這些函數的區別,需要時可以查看《APUE》關於exec函數部分的內容。
運行這樣一段演示程序:
1 #include <errno.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 char command[256]; 6 void main() 7 { 8 int rtn; /*child process return value*/ 9 while(1) { 10 printf( ">" ); 11 fgets( command, 256, stdin ); 12 command[strlen(command)-1] = 0; 13 if ( fork() == 0 ) { 14 execlp( command, NULL ); 15 perror( command ); 16 exit( errno ); 17 } 18 else { 19 wait ( &rtn ); 20 printf( " child process return %d\n", rtn ); 21 } 22 } 23 }
a.out 是一個打印hello world的可執行文件。
運行結果:
進程終止
正常終止(5種)
從main返回,等效於調用exit
調用exit
exit 首先調用各終止處理程序,然后按需多次調用fclose,關閉所有的打開流。
調用_exit或者_Exit
最后一個線程從其啟動例程返回
最后一線程調用pthread_exit
異常終止(3種)
調用abort
接到一個信號並終止
最后一個線程對取消請求作出響應
wait和waitpid函數
wait用於使父進程阻塞,等待子進程退出;waitpid有若干選項,如可以提供一個非阻塞版本的wait,也能實現和wait相同的功能,實際上,linux中wait的實現也是通過調用waitpid實現的。
waitpid返回值:正常返回子進程號;使用WNOHANG且沒有子進程退出返回0;調用出錯返回-1;
運行如下演示程序
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/types.h> 4 #include <sys/wait.h> 5 6 int main() 7 { 8 pid_t pid0,pid1; 9 pid0 = fork(); 10 if(pid0 < 0){ 11 perror("fork"); 12 exit(1); 13 } 14 else if(pid0 == 0){ 15 sleep(5); 16 exit(0);//child 17 } 18 else{ 19 do{ 20 pid1 = waitpid(pid0,NULL,WNOHANG); 21 if(pid1 == 0){ 22 printf("the child process has not exited.\n"); 23 sleep(1); 24 } 25 }while(pid1 == 0); 26 if(pid1 == pid0){ 27 printf("get child pid:%d",pid1); 28 exit(0); 29 } 30 else{ 31 exit(1); 32 } 33 } 34 return 0; 35 } 36 37 38 39 當把第三個參數WNOHANG改為0時,就不會有上面五個顯示語句了,說明父進程阻塞了。 40 41 42 43 a.out 的代碼如下: 44 45 46 #include <stdio.h> 47 void main() 48 49 { 50 printf("hello WYJ\n"); 51 } 52 53 54 55 process.c的代碼如下: 56 57 #include <stdio.h> 58 #include <sys/types.h> 59 #include <unistd.h> 60 #include <stdlib.h> 61 #include <sys/times.h> 62 #include <sys/wait.h> 63 64 int main() 65 { 66 pid_t pid_1,pid_2,pid_wait; 67 pid_1 = fork(); 68 pid_2 = fork(); 69 if(pid_1 < 0){ 70 perror("fork1 failed\n"); 71 exit(1); 72 }else if(pid_1 == 0 && pid_2 != 0){//do not allow child 2 to excute this process. 73 if(execlp("./a.out", NULL) < 0){ 74 perror("exec failed\n"); 75 }//child; 76 exit(0); 77 } 78 if(pid_2 < 0){ 79 perror("fork2 failded\n"); 80 exit(1); 81 }else if(pid_2 == 0){ 82 sleep(10); 83 } 84 if(pid_2 > 0){//parent 85 do{ 86 pid_wait = waitpid(pid_2, NULL, WNOHANG);//no hang 87 sleep(2); 88 printf("child 2 has not exited\n"); 89 }while(pid_wait == 0); 90 if(pid_wait == pid_2){ 91 printf("child 2 has exited\n"); 92 exit(0); 93 }else{ 94 // printf("pid_2:%d\n",pid_2); 95 perror("waitpid error\n"); 96 exit(1); 97 98 } 99 } 100 exit(0); 101 }
運行結果:
編寫一個多進程程序:該實驗有 3 個進程,其中一個為父進程,其余兩個是該父進程創建的子進程,其中一個子進程運行“ls -l”指令,另一個子進程在暫停 5s 之后異常退出,父進程並不阻塞自己,並等待子進程的退出信息,待收集到該信息,父進程就返回。
1 #include<stdio.h> 2 #include<string.h> 3 #include<fcntl.h> 4 #include<unistd.h> 5 #include<sys/types.h> 6 #include<sys/wait.h> 7 int main() 8 { 9 pid_t child1,child2,child; 10 if((child1 = fork()) < 0){ 11 perror("failed in fork 1"); 12 exit(1); 13 } 14 if((child2 = fork()) < 0){ 15 perror("failed in fork 2"); 16 exit(1); 17 } 18 if(child1 == 0){ 19 //run ls -l 20 if(child2 == 0){ 21 printf("in grandson\n"); 22 } 23 else if(execlp("ls", "ls", "-l", NULL) < 0){ 24 perror("child1 execlp"); 25 } 26 } 27 else if(child2 == 0){ 28 sleep(5); 29 exit(0); 30 } 31 else{ 32 do{ 33 sleep(1); 34 printf("child2 not exits\n"); 35 child = waitpid(child2, NULL, WNOHANG); 36 }while(child == 0); 37 if(child == child2){ 38 printf("get child2\n"); 39 } 40 else{ 41 printf("Error occured\n"); 42 } 43 } 44 }
運行結果:
init進程成為所有僵屍進程(孤兒進程)的父進程
僵屍進程
在進程調用了exit之后,該進程並非馬上就消失掉,而是留下了一個成為僵屍進程的數據結構,記載該進程的退出狀態等信息供其他進程收集,除此之外,僵屍進程不再占有任何內存空間。
子進程結束之后為什么會進入僵屍狀態? 因為父進程可能會取得子進程的退出狀態信息。
如何查看僵屍進程?
linux中命令ps,標記為Z的進程就是僵屍進程。
執行下面一段程序:
1 #include <sys/types.h> 2 #include <unistd.h> 3 int main() 4 { 5 pid_t pid; 6 pid = fork(); 7 if(pid < 0){ 8 printf("error occurred\n"); 9 }else if(pid == 0){ 10 exit(0); 11 }else{ 12 sleep(60); 13 wait(NULL); 14 } 15 }
運行結果:
ps -ef|grep defunc可以找出僵屍進程
ps -l 可以得到更詳細的進程信息
運行結果顯示:

運行兩次之后發現有兩個Z進程,然后等待一分鍾后,Z進程被父進程回收。
其中S表示狀態:
O:進程正在處理器運行
S:休眠狀態
R:等待運行
I:空閑狀態
Z:僵屍狀態
T:跟蹤狀態
B:進程正在等待更多的內存分頁
C:cpu利用率的估算值
收集僵屍進程的信息,並終結這些僵屍進程,需要我們在父進程中使用waitpid和wait,這兩個函數能夠手機僵屍進程留下的信息並使進程徹底消失。
守護進程Daemon
是linux的后台服務進程。它是一個生存周期較長的進程,沒有控制終端,輸出無處顯示。用戶層守護進程的父進程是init進程。
守護進程創建步驟:
1、創建子進程,父進程退出,子進程被init自動收養;fork exit
2、調用setsid創建新會話,成為新會話的首進程,成為新進程組的組長進程,擺脫父進程繼承過來的會話、進程組等;setsid
3、改變當前目錄為根目錄,保證工作的文件目錄不被刪除;chdir(“/”)
4、重設文件權限掩碼,給子進程更大的權限;umask(0)
5、關閉不用的文件描述符,因為會消耗資源;close
一個守護進程的實例:每隔10s寫入一個“tick”
1 #include<stdio.h> 2 #include<string.h> 3 #include<fcntl.h> 4 #include<unistd.h> 5 #include<sys/types.h> 6 #define MAXFILE 65535 7 8 int main() 9 { 10 int fd,len,i; 11 pid_t pid; 12 char *buf = "tick\n"; 13 len = strlen(buf); 14 if((pid = fork()) < 0){ 15 perror("fork failed"); 16 exit(1); 17 } 18 else if(pid > 0){ 19 exit(0); 20 } 21 setsid(); 22 if(chdir("/") < 0){ 23 perror("chdir failed"); 24 exit(1); 25 } 26 umask(0); 27 for(i = 0; i < MAXFILE; i++){ 28 close(i); 29 } 30 while(1){ 31 if((fd = open("/tmp/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0){ 32 perror("open log failed"); 33 exit(1); 34 } 35 write(fd, buf, len+1); 36 close(fd); 37 sleep(10); 38 } 39 }
運行結果:
1 #include<stdio.h> 2 #include<string.h> 3 #include<fcntl.h> 4 #include<unistd.h> 5 #include<sys/types.h> 6 #include<syslog.h> 7 #define MAXFILE 65535 8 9 int main() 10 { 11 int fd,len,i; 12 pid_t pid,child; 13 char *buf = "tick\n"; 14 len = strlen(buf); 15 if((pid = fork()) < 0){ 16 perror("fork failed"); 17 exit(1); 18 } 19 else if(pid > 0){ 20 exit(0); 21 } 22 openlog("Jack", LOG_PID, LOG_DAEMON); 23 if(setsid() < 0){ 24 syslog(LOG_ERR, "%s\n", "setsid"); 25 exit(1); 26 } 27 28 if(chdir("/") < 0){ 29 syslog(LOG_ERR, "%s\n", "chdir"); 30 exit(1); 31 } 32 umask(0); 33 for(i = 0; i < MAXFILE; i++){ 34 close(i); 35 } 36 if((child = fork()) < 0){ 37 syslog(LOG_ERR, "%s\n", "fork"); 38 exit(1); 39 } 40 if(child == 0){ 41 //printf("in child\n");//can not use terminal from now on. 42 syslog(LOG_INFO, "in child"); 43 sleep(10); 44 exit(0); 45 } 46 else{ 47 waitpid(child, NULL, 0); 48 //printf("child exits\n");//can not use terminal from now on. 49 syslog(LOG_INFO, "child exits"); 50 closelog(); 51 while(1){ 52 sleep(10); 53 } 54 } 55 56 }
真正編寫調試的時候會發現需要殺死守護進程。
如何殺死守護進程?
ps -aux 找到對應PID
kill -9 PID
其他參考資料:
《APUE》
《操作系統》清華大學公開課 向勇、陳渝
《嵌入式Linux應用程序開發詳解》
《LInux 的僵屍(zombie)進程》
http://coolshell.cn/articles/656.html
