1. 程式人生 > >Linux 下的多程序程式設計

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時,物理空間上兩個程序的資料段和堆疊段 都還是共享著的,當有一個程序寫了某個資料時,這時兩個程序之間的資料才有了區別──即為COW:核心建立程序時,為其分配的虛擬記憶體並沒有對映到實體記憶體上,對子程序,被對映到的是父程序的實體記憶體上,只有在寫東西時才產生一個頁面異常,在異常處理函式中開始將虛擬地址對映到實體記憶體上。系統就將有區別的“頁”從物理上也分開。系統在空間上的開銷就可以達到 最小。
   一個小幽默:下面演示一個足以"搞死"Linux的小程式,其原始碼非常簡單:

void main()
{
for(;;) fork();
}

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


    v f o r k用於建立一個新程序,而該新程序的目的是e x e c一個新程式它並不將父程序的地址空間完全複製到子程序中,因為子程序會立即呼叫e x e c (或e x i t ),於是也就不會存訪該地址空間。不過在子程序呼叫e x e c或e x i t之前,它在父程序的空間中執行v f o r k保證子程序先執行,在它呼叫e x e c或e x i t之後父程序才可能被排程執行(如果在呼叫這兩個函式之前子程序依賴於父程序的進一步動作,則會導致死鎖。)

(三) 如何啟動另一程式的執行
   下面我們來看看一個程序如何來啟動另一個程式的執行。在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類函式則是低層的系統呼叫

    在這一節裡,我們還要講講system()和popen()函式。system()函式先呼叫fork(),然後再呼叫exec()來執行使用者的登入 shell,通過它來查詢可執行檔案的命令並分析引數,最後它麼使用wait()函式族之一來等待子程序的結束。函式popen()和函式system ()相似,不同的是它呼叫pipe()函式建立一個管道,通過它來完成程式的標準輸入和標準輸出。這兩個函式是為那些不太勤快的程式設計師設計的,在效率和安 全方面都有相當的缺陷,在可能的情況下,應該儘量避免。

(四) 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了


//////////////////補充兩個fork的例子///////////////////////////////////////
1
#include <unistd.h>;
#include <sys/types.h>;

main ()
{       int i=5;
        pid_t pid;
        pid=fork();
        for(;i>;0;i--){
        if (pid < 0)
                printf("error in fork!");
        else if (pid == 0)
                printf("i am the child process, my process id is %d and i=%d/n",getpid(),i);
        else
                printf("i am the parent process, my process id is %d and i=%d/n",getpid(),i);
          }
}

i am the child process, my process id is 11879 and i=5
i am the child process, my process id is 11879 and i=4
i am the child process, my process id is 11879 and i=3
i am the child process, my process id is 11879 and i=2
i am the child process, my process id is 11879 and i=1
i am the parent process, my process id is 11878 and i=5
i am the parent process, my process id is 11878 and i=4
i am the parent process, my process id is 11878 and i=3
i am the parent process, my process id is 11878 and i=2
i am the parent process, my process id is 11878 and i=1


子程序的PID不是0
根據fork的實現,fork的返回值可能為>0 =0 <0,3種情況
如果是>0說明是父程序,返回值是子程序的PID;如果是=0說明是子程序,子程序可以通過getpid()得到自己的PID,通過getppid得到父程序的PID;<0就說明fork失敗
先列印父程序,還是子程序由CPU決定

2

main()
{
int a;
int pid;
printf("AAAAAAAA");//
pid=fork();
if(pid==0){//在這裡定義的變數父程序是不會有的:int b;
printf("ok");}
else if(pid>;0){
printf("is ok/n");//if you want print b;error!but you can print a;
}
printf("BBBBBBB");//父子程序都會列印;
}

如果你將 printf("AAAAAA") 換成 printf("AAAAAA/n")   那麼就是隻列印一次了.
主要的區別是因為有了一個 /n  回車符號

當沒有/n時候,printf函式並沒有把字元輸出到終端,而是儲存在緩衝區中直到程式退出的時候才printf出來,此時呼叫fork後,顯然子程序把父程序的空間都copy過來,包括緩衝區中的內容,所以就出現了兩次