進程詳解(1)——可能是最深入淺出的進程學習筆記


原文地址:http://www.cnblogs.com/jacklu/p/5317406.html
 

進程控制塊(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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM