Linux多進程編程講解

1 Linux下進程的結構

   Linux下一個進程在內存裏有三部分的數據,就是"代碼段""堆棧段""數據段"。一般的CPU都有上述三種段寄存器,以方便操作系統的運行。這三個部分也是構成一個完整的執行序列的必要的部分。

   "代碼段",顧名思義,就是存放了程序代碼的數據,假如機器中有數個進程運行相同的一個程序,那麼它們就可以使用相同的代碼段。"堆棧段"存放的就是子程序的返回地址、子程序的參數以及程序的局部變量。而數據段則存放程序的全局變量,常數以及動態數據分配的數據空間(比如用malloc之類的函數取得的空間)。系統如果同時運行數個相同的程序,它們之間就不能使用同一個堆棧段和數據段。

2 Linux進程控制

   在傳統的Unix環境下,有兩個基本的操作用於創建和修改進程:函數fork( )用來創建一個新的進程,該進程幾乎是當前進程的一個完全拷貝;函數族exec( )用來啓動另外的進程以取代當前運行的進程。

2.1 使用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函數返回子程序的進程號,而對於子程序,fork函數則返回零。在操作系統中,我們用ps函數就可以看到不同的進程號,對父進程而言,它的進程號是由比它更低層的系統調用賦予的,而對於子進程而言,它的進程號即是fork函數對父進程的返回值。在程序設計中,父進程和子進程都要調用函數fork()下面的代碼,而我們就是利用fork()函數對父子進程的不同返回值用if...else...語句來實現讓父子進程完成不同的功能,正如我們上面舉的例子一樣。我們看到,上面例子執行時兩條信息是交互無規則的打印出來的,這是父子進程獨立執行的結果,雖然我們的代碼似乎和串行的代碼沒有什麼區別。

 

   一般CPU都是以""爲單位來分配內存空間的,每一個頁都是實際物理內存的一個映像,象INTELCPU,其一頁在通常情況下是4086字節大小,而無論是數據段還是堆棧段都是由許多""構成的,fork函數複製這兩個段,只是"邏輯"上的,並非"物理"上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都還是共享着的,當有一個進程寫了某個數據時,這時兩個進程之間的數據纔有了區別,系統就將有區別的""從物理上也分開。系統在空間上的開銷就可以達到最小。

 

2.2 exec( )函數族

   Linux中一個進程啓動另一個程序的執行使用exec函數族。一個進程一旦調用exec類函數,它本身就"死亡"了,系統把代碼段替換成新的程序的代碼,廢棄原有的數據段和堆棧段,併爲新程序分配新的數據段與堆棧段,唯一留下的,就是進程號,也就是說,對系統而言,還是同一個進程,不過已經是另一個程序了。(不過exec類函數中有的還允許繼承環境變量之類的信息。)那麼如果我的程序想啓動另一程序的執行但自己仍想繼續運行的話,可以結合forkexec的使用。下面一段代碼顯示如何啓動運行其它程序:

 

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 );

}

}

}

   此程序從終端讀入命令並執行之,執行完成後,父進程繼續等待從終端讀入命令。

3 Linux進程間通信

   首先,進程間通信至少可以通過傳送打開文件來實現,不同的進程通過一個或多個文件來傳遞信息,事實上,在很多應用系統裏,都使用了這種方法。但一般說來,進程間通信(IPCInterProcess Communication)不包括這種似乎比較低級的通信方法。Linux作爲一種新興的操作系統,幾乎支持所有的Unix下常用的進程間通信方法:管道、消息隊列、共享內存、信號量、套接口等等。

 

3.1 管道

   管道是進程間通信中最古老的方式,它包括無名管道和有名管道兩種,前者用於父進程和子進程間的通信,後者用於運行於同一臺機器上的任意兩個進程間的通信。

   無名管道由pipe()函數創建,下面的例子示範瞭如何在父進程和子進程間實現通信:

#define INPUT 0

#define OUTPUT 1

void main() {

int file_descriptors[2];

pid_t pid;

char buf[256];

int returned_count;

pipe(file_descriptors);  /*創建無名管道*/

if((pid = fork()) == -1) { /*創建子進程*/

printf("Error in fork"n");

exit(1);

}

if(pid == 0) {

printf("in the spawned (child) process..."n");

/*子進程向父進程寫數據,關閉管道的讀端*/

close(file_descriptors[INPUT]);

write(file_descriptors[OUTPUT], "test data", strlen("test data"));

exit(0);

} else {

printf("in the spawning (parent) process..."n");

/*父進程從管道讀取子進程寫的數據,關閉管道的寫端*/

close(file_descriptors[OUTPUT]);

returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));

printf("%d bytes of data received from spawned process: %s"n",

returned_count, buf);

}

}

   Linux系統下,有名管道可由兩種方式創建:命令行方式mknod系統調用和函數mkfifo。下面的兩種途徑都在當前目錄下生成了一個名爲myfifo的有名管道:

     方式一:mkfifo("myfifo","rw");

     方式二:mknod myfifo p

   生成了有名管道後,就可以使用一般的文件I/O函數如openclosereadwrite等來對它進行操作。下面即是一個簡單的例子,假設我們已經創建了一個名爲myfifo的有名管道。

/* 進程一:讀有名管道*/

#include <stdio.h>

#include <unistd.h>

void main() {

FILE * in_file;

int count = 1;

char buf[80];

in_file = fopen("mypipe", "r");

if (in_file == NULL) {

printf("Error in fdopen."n");

exit(1);

}

while ((count = fread(buf, 1, 80, in_file)) > 0)

printf("received from pipe: %s"n", buf);

fclose(in_file);

}

 

/* 進程二:寫有名管道*/

#include <stdio.h>

#include <unistd.h>

void main() {

FILE * out_file;

int count = 1;

char buf[80];

out_file = fopen("mypipe", "w");

if (out_file == NULL) {

printf("Error opening pipe.");

exit(1);

}

sprintf(buf,"this is test data for the named pipe example"n");

fwrite(buf, 1, 80, out_file);

fclose(out_file);

}

 

3.2 消息隊列

   消息隊列用於運行於同一臺機器上的進程間通信,它和管道很相似,事實上,它是一種正逐漸被淘汰的通信方式,我們可以用流管道或者套接口的方式來取代它,所以,我們對此方式也不再解釋,也建議讀者忽略這種方式。

3.3 共享內存

   共享內存是運行在同一臺機器上的進程間通信最快的方式,因爲數據不需要在不同的進程間複製。通常由一個進程創建一塊共享內存區,其餘進程對這塊內存區進行讀寫。得到共享內存有兩種方式:映射/dev/mem設備和內存映像文件。

前一種方式不給系統帶來額外的開銷,但在現實中並不常用,因爲它控制存取的將是實際的物理內存,在Linux系統下,這隻有通過限制Linux系統存取的內存纔可以做到,這當然不太實際。常用的方式是通過shmXXX函數族來實現利用共享內存進行存儲的。

   首先要用的函數是shmget,它獲得一個共享存儲標識符。

     #include <sys/types.h>

     #include <sys/ipc.h>

     #include <sys/shm.h>

    int shmget(key_t key, int size, int flag);

   這個函數有點類似大家熟悉的malloc函數,系統按照請求分配size大小的內存用作共享內存。Linux系統內核中每個IPC結構都有的一個非負整數的標識符,這樣對一個消息隊列發送消息時只要引用標識符就可以了。這個標識符是內核由IPC結構的關鍵字得到的,這個關鍵字,就是上面第一個函數的key。數據類型key_t是在頭文件sys/types.h中定義的,它是一個長整形的數據。在我們後面的章節中,還會碰到這個關鍵字。

  共享內存創建後,其餘進程可以調用shmat()將其連接到自身的地址空間中。

   void *shmat(int shmid, void *addr, int flag);

   shmidshmget函數返回的共享存儲標識符,addrflag參數決定了以什麼方式來確定連接的地址,函數的返回值即是該進程數據段所連接的實際地址,進程可以對此進程進行讀寫操作。

   使用共享存儲來實現進程間通信的注意點是對數據存取的同步,必須確保當一個進程去讀取數據時,它所想要的數據已經寫好了。通常,信號量被要來實現對共享存儲數據存取的同步,另外,可以通過使用shmctl函數設置共享存儲內存的某些標誌位如SHM_LOCKSHM_UNLOCK等來實現。

3.4 信號量

   信號量又稱爲信號燈,它是用來協調不同進程間的數據對象的,而最主要的應用是前一節的共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。一般說來,爲了獲得共享資源,進程需要執行下列操作:

   1 測試控制該資源的信號量。

   2 若此信號量的值爲正,則允許進行使用該資源。進程將進號量減1

   3 若此信號量爲0,則該資源目前不可用,進程進入睡眠狀態,直至信號量值大於0,進程被喚醒,轉入步驟(1)。

   4 當進程不再使用一個信號量控制的資源時,信號量值加1。如果此時有進程正在睡眠等待此信號量,則喚醒此進程。

   維護信號量狀態的是Linux內核操作系統而不是用戶進程。我們可以從頭文件/usr/src/linux/include /linux/sem.h中看到內核用來維護信號量狀態的各個結構的定義。信號量是一個數據集合,用戶可以單獨使用這一集合的每個元素。要調用的第一個函數是semget,用以獲得一個信號量ID

   #include <sys/types.h>

   #include <sys/ipc.h>

   #include <sys/sem.h>

   int semget(key_t key, int nsems, int flag);

   key是前面講過的IPC結構的關鍵字,它將來決定是創建新的信號量集合,還是引用一個現有的信號量集合。nsems是該集合中的信號量數。如果是創建新集合(一般在服務器中),則必須指定nsems;如果是引用一個現有的信號量集合(一般在客戶機中)則將nsems指定爲0

   semctl函數用來對信號量進行操作。

   int semctl(int semid, int semnum, int cmd, union semun arg);

   不同的操作是通過cmd參數來實現的,在頭文件sem.h中定義了7種不同的操作,實際編程時可以參照使用。

   semop函數自動執行信號量集合上的操作數組。

   int semop(int semid, struct sembuf semoparray[], size_t nops);

   semoparray是一個指針,它指向一個信號量操作數組。nops規定該數組中操作的數量。

   下面,我們看一個具體的例子,它創建一個特定的IPC結構的關鍵字和一個信號量,建立此信號量的索引,修改索引指向的信號量的值,最後我們清除信號量。在下面的代碼中,函數ftok生成我們上文所說的唯一的IPC關鍵字。

 

#include <stdio.h>

#include <sys/types.h>

#include <sys/sem.h>

#include <sys/ipc.h>

void main() {

key_t unique_key; /* 定義一個IPC關鍵字*/

int id;

struct sembuf lock_it;

union semun options;

int i;

unique_key = ftok(".", 'a'); /* 生成關鍵字,字符'a'是一個隨機種子*/

/* 創建一個新的信號量集合*/

id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);

printf("semaphore id=%d"n", id);

options.val = 1; /*設置變量值*/

semctl(id, 0, SETVAL, options); /*設置索引0的信號量*/

/*打印出信號量的值*/

i = semctl(id, 0, GETVAL, 0);

printf("value of semaphore at index 0 is %d"n", i);

/*下面重新設置信號量*/

lock_it.sem_num = 0; /*設置哪個信號量*/

lock_it.sem_op = -1; /*定義操作*/

lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/

if (semop(id, &lock_it, 1) == -1) {

printf("can not lock semaphore."n");

exit(1);

}

i = semctl(id, 0, GETVAL, 0);

printf("value of semaphore at index 0 is %d"n", i);

/*清除信號量*/

semctl(id, 0, IPC_RMID, 0);

}

3.5 套接口

   套接口(socket)編程是實現Linux系統和其他大多數操作系統中進程間通信的主要方式之一。我們熟知的WWW服務、FTP服務等都是基於套接口編程來實現的。除了在異地的計算機進程間以外,套接口同樣適用於本地同一臺計算機內部的進程間通信。

具體略。

4 Linux的進程和Win32的進程/線程比較

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

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

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

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


原文地址:http://www.cnblogs.com/Myhsg/archive/2009/07/26/1531502.html

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