Linux下的多進程編程

(一) 理解Linux下進程的結構 

  Linux下一個進程在內存裏有三部份的數據,就是“數據段”,“堆棧段”和“代碼段”,其實學過彙編 語言的人一定知道,一般的CPU象I386,都有上述三種段寄存器,以方便操作系統的運行。“代碼段”,顧名思義,就是存放了程序代碼的數據,假如機器中有數個進程運行相同的一個程序,那麼它們就可以使用同一個代碼段。堆棧段存放的就是子程序的返回地址、子程序的參數以及程序的局部變量。而數據段則存放程序的全局變量,常數以及動態數據分配的數據空間(比如用malloc之類的函數取得的空間)。這其中有許多細節問題,這裏限於篇幅就不多介紹了。系統如果同時運行數個相同的程序,它們之間就不能使用同一個堆棧段和數據段。 

 

(二) 如何使用fork 

  在Linux下產生新的進程的系統調用就是fork函數,這個函數名是英文中“分叉”的意思。爲什麼取這個名字呢?因爲一個進程在運行中,如果使用了fork,就產生了另一個進程,於是進程就“分叉”了,所以這個名字取得很形象。下面就看看如何具體使用fork,這段程序演示了使用fork的基本框架:

 

void main()

  int i; 

  if ( fork() == 0 ) { 

  /* 子進程程序 */ 

  for ( i = 1; i < 1000; i ++ ) 

    printf("This is child process/n"); 

  } 

  else { 

  /* 父進程程序*/ 

  for ( i = 1; i < 1000; i ++ ) 

  printf("This is process process/n"); 

  } 

 

  程序運行後,你就能看到屏幕上交替出現子進程與父進程各打印出的一千條信息了。如果程序還在運行中,你用ps命令就能看到系統中有兩個它在運行了。 

  那麼調用這個fork函數時發生了什麼呢?一個程序一調用fork函數,系統就爲一個新的進程準備了前述三個段,首先,系統讓新的進程與舊的進程使用同一個代碼段,因爲它們的程序還是相同的,對於數據段和堆棧段,系統則複製一份給新的進程,這樣,父進程的所有數據都可以留給子進程,但是,子進程一旦開始運行,雖然它繼承了父進程的一切數據,但實際上數據卻已經分開,相互之間不再有影響了,也就是說,它們之間不再共享任何數據了。而如果兩個進程要共享什麼數據的話,就要使用另一套函數(shmget,shmat,shmdt等)來操作。現在,已經是兩個進程了,對於父進程,fork函數返回了子程序的進程號,而對於子程序,fork函數則返回零,這樣,對於程序,只要判斷fork函數的返回值,就知道自己是處於父進程還是子進程中。 

  讀者也許會問,如果一個大程序在運行中,它的數據段和堆棧都很大,一次fork就要複製一次,那麼fork 的系統開銷不是很大嗎?其實UNIX自有其解決的辦法,大家知道,一般CPU都是以“頁”爲單位分配空間的,象INTEL的CPU,其一頁在通常情況下是4K字節大小,而無論是數據段還是堆棧段都是由許多“頁”構成的, fork函數複製這兩個段,只是“邏輯”上的,並非“物理”上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都還是共享着的,當有一個進程寫了某個數據時,這時兩個進程之間的數據纔有了區別,系統就將有區別的“頁”從物理上也分開。系統在空間上的開銷就可以達到最小。 

  一個小幽默:下面演示一個足以"搞死"Linux的小程序,其源代碼非常簡單:  

 

void main() 

  for(;;) fork(); 

 

  這個程序什麼也不做,就是死循環地fork,其結果是程序不斷產生進程,而這些進程又不斷產生新的進程,很快,系統的進程就滿了,系統就被這麼多不斷產生的進程"撐死了"。用不着是root,任何人運行上述程序都足以讓系統死掉。哈哈,但這不是Linux不安全的理由,因爲只要系統管理員足夠聰明,他(或她)就可以預先給每個用戶設置可運行的最大進程數,這樣,只要不是root,任何能運行的進程數也許不足系統總的能運行和進程數的十分之一,這樣,系統管理員就能對付上述惡意的程序了。 

 

(三) 如何啓動另一程序的執行  

  下面我們來看看一個進程如何來啓動另一個程序的執行。在Linux中要使用exec類的函數,exec類的函數不止一個,但大致相同,在Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp爲例,其它函數究竟與execlp有何區別,請通過manexec命令來了解它們的具體情況。  

  一個進程一旦調用exec類函數,它本身就“死亡”了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,併爲新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。(不過exec類函數中有的還允許繼承環境變量之類的信息。) 

  那麼如果我的程序想啓動另一程序的執行但自己仍想繼續運行的話,怎麼辦呢?那就是結合fork與exec的使用。下面一段代碼顯示如何啓動運行其它程序: 

 

char command[256]; 

void main() 

  int rtn; /*子進程的返回數值*/ 

  while(1) { 

  /* 從終端讀取要執行的命令 */ 

  printf( ">" ); 

  fgets( command, 256, stdin ); 

  command[strlen(command)-1] = 0; 

  if ( fork() == 0 ) { 

    /* 子進程執行此命令 */ 

    execlp( command, command ); 

    /* 如果exec函數返回,表明沒有正常執行命令,打印錯誤信息*/ 

    perror( command ); 

    exit( errorno ); 

else { 

  /* 父進程, 等待子進程結束,並打印子進程的返回值 */ 

  wait ( &rtn ); 

  printf( " child process return %d/n",. rtn ); 

  } 

  } 

 

  此程序從終端讀入命令並執行之,執行完成後,父進程繼續等待從終端讀入命令。熟悉DOS和WINDOWS系統調用的朋友一定知道DOS/WINDOWS也有exec類函數,其使用方法是類似的,但DOS/WINDOWS還有spawn類函數,因爲DOS是單任務的系統,它只能將“父進程”駐留在機器內再執行“子進程”,這就是spawn類的函數。 

WIN32已經是多任務的系統了,但還保留了spawn類函數,WIN32中實現spawn函數的方法同前述UNIX中的方法差不多,開設子進程後父進程等待子進程結束後才繼續運行。UNIX在其一開始就是多任務的系統,所以從核心角度上講不需要spawn類函數。 

  另外,有一個更簡單的執行其它程序的函數system,它是一個較高層的函數,實際上相當於在SHELL環境下執行一條命令,而exec類函數則是低層的系統調用。 

 

(四) Linux的進程與Win32的進程/線程有何區別 

  熟悉WIN32編程的人一定知道,WIN32的進程管理方式與UNIX上有着很大區別,在UNIX裏,只有進程的概念,但在WIN32裏卻還有一個“線程”的概念,那麼UNIX和WIN32在這裏究竟有着什麼區別呢?

  UNIX裏的fork是七十年代UNIX早期的開發者經過長期在理論和實踐上的艱苦探索後取得的成果,一方面,它使操作系統在進程管理上付出了最小的代價,另一方面,又爲程序員提供了一個簡潔明瞭的多進程方法。 

  WIN32裏的進程/線程是繼承自OS/2的。在WIN32裏,“進程”是指一個程序,而“線程”是一個“進程”裏的一個執行“線索”。從核心上講,WIN32的多進程與UNIX並無多大的區別,在WIN32裏的線程才相當於UNIX的進程,是一個實際正在執行的代碼。但是,WIN32裏同一個進程裏各個線程之間是共享數據段的。這纔是與UNIX的進程最大的不同。 

  下面這段程序顯示了WIN32下一個進程如何啓動一個線程:(請注意,這是個終端方式程序,沒有圖形界面) 

 

int g; 

DWORD WINAPI ChildProcess( LPVOID lpParameter ){ 

  int i; 

  for ( i = 1; i < 1000; i ++) { 

    g ++; 

    printf( "This is Child Thread: %d/n", g ); 

  } 

  ExitThread( 0 ); 

}; 

 

void main() 

  int threadID; 

  int i; 

  g = 0; 

  CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID ); 

  for ( i = 1; i < 1000; i ++) { 

    g ++; 

    printf( "This is Parent Thread: %d/n", g ); 

  } 

}  

 

  在WIN32下,使用CreateThread函數創建線程,與UNIX不同,線程不是從創建處開始運行的,而是由 

CreateThread指定一個函數,線程就從那個函數處開始運行。此程序同前面的UNIX程序一樣,由兩個線程各打印1000條信息。threadID是子線程的線程號,另外,全局變量g是子線程與父線程共享的,這就是與UNIX最大的不同之處。大家可以看出,WIN32的進程/線程要比UNIX複雜,在UNIX裏要實現類似WIN32的線程並不難,只要fork以後,讓子進程調用ThreadProc函數,並且爲全局變量開設共享數據區就行了,但在WIN32下就無法實現類似fork的功能了。所以現在WIN32下的C語言編譯器所提供的庫函數雖然已經能兼容大多數UNIX的庫函數,但卻仍無法實現fork。 

  對於多任務系統,共享數據區是必要的,但也是一個容易引起混亂的問題,在WIN32下,一個程序員很容易忘記線程之間的數據是共享的這一情況,一個線程修改過一個變量後,另一個線程卻又修改了它,結果引起程序出問題。但在UNIX下,由於變量本來並不共享,而由程序員來顯式地指定要共享的數據,使程序變得更清晰與安全。 

  Linux還有自己的一個函數叫clone,這個函數是其它UNIX所沒有的,而且通常的Linux也並不提供此函數(要使用此函數需自己重新編譯內核,並設置CLONE_ACTUALLY_WORKS_OK選項),clone函數提供了更多的創建新進程的功能,包括象完全共享數據段這樣的功能。 

  至於WIN32的“進程”概念,其含義則是“應用程序”,也就是相當於UNIX下的exec了。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章