Linux下的多進程編程


1、進程

1.1進程的定義

《計算機操作系統》這門課對進程有這樣的描述:進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。

1.2進程的概念

進程的概念主要有兩點:

第一,進程是一個實體。每一個進程都有它自己的地址空間,一般情況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲處理器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲着活動過程調用的指令和本地變量。

第二,進程是一個“執行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成為一個活動的實體,我們稱其為進程。

進程是操作系統中最基本、重要的概念。是多道程序系統出現后,為了刻畫系統內部出現的動態情況,描述系統內部各道程序的活動規律引進的一個概念,所有多道程序設計操作系統都建立在進程的基礎上。

操作系統引入進程的概念的原因:

從理論角度看,是對正在運行的程序過程的抽象;

從實現角度看,是一種數據結構,目的在於清晰地刻划動態系統的內在規律,有效管理和調度進入計算機系統主存儲器運行的程序。

1.3進程的特征

主要特征:

  1. 動態性:進程的實質是程序在多道程序系統中的一次執行過程,進程是動態產生,動態消亡的。
  2. 並發性:任何進程都可以同其他進程一起並發執行
  3. 獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位;
  4. 異步性:由於進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可預知的速度向前推進
  5. 結構特征:進程由程序、數據和進程控制塊三部分組成。

多個不同的進程可以包含相同的程序:一個程序在不同的數據集里就構成不同的進程,能得到不同的結果;但是執行過程中,程序不能發生改變。

1.4進程切換

進行進程切換就是從正在運行的進程中收回處理器,然后再使待運行進程來占用處理器。

這里所說的從某個進程收回處理器,實質上就是把進程存放在處理器的寄存器中的中間數據找個地方存起來,從而把處理器的寄存器騰出來讓其他進程使用。那么被中止運行進程的中間數據存在何處好呢?當然這個地方應該是進程的私有堆棧。

讓進程來占用處理器,實質上是把某個進程存放在私有堆棧中寄存器的數據(前一次本進程被中止時的中間數據)再恢復到處理器的寄存器中去,並把待運行進程的斷點送入處理器的程序指針PC,於是待運行進程就開始被處理器運行了,也就是這個進程已經占有處理器的使用權了。

在切換時,一個進程存儲在處理器各寄存器中的中間數據叫做進程的上下文,所以進程的切換實質上就是被中止運行進程與待運行進程上下文的切換。在進程未占用處理器時,進程的上下文是存儲在進程的私有堆棧中的。

1.5Linux下進程的結構

在Linux操作系統中,進程在內存里有三部分的數據,就是“數據段”、“堆棧段”和“代碼段”。簡單的說“代碼段”,顧名思義,就是存放了程序代碼的數據,假如機器中有數個進程運行相同的一個程序,那么它們就可以使用同一個代碼段。  
堆棧段存放的就是子程序的返回地址、子程序的參數以及程序的局部變量。而數據段則存放程序的全局變量,常數以及動態數據分配的數據空間(比如用malloc之類的函數取得的空間)。

2、fork()函數

在Linux下產生新的進程的系統調用就是fork函數。fork是最難理解的概念之一:它執行一次卻返回兩個值。fork函數是Unix系統最傑出的成就之一,它是七十年代UNIX早期的開發者經過長期在理論和實踐上的艱苦探索后取得的成果,一方面,它使操作系統在進程管理上付出了最小的代價,另一方面,又為程序員提供了一個簡潔明了的多進程方法。與DOS和早期的Windows不同,Unix/Linux系統是真正實現多任務操作的系統,可以說,不使用多進程編程,就不能算是真正的Linux環境下編程。

示例:

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 parent process\n");
    }
}

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

有個疑問:如果一個大程序在運行中,它的數據段和堆棧都很大,一次fork就要復制一次,那么fork的系統開銷不是很大嗎?

其實Linux自有其解決的辦法,大家知道,一般CPU都是以“頁”為單位分配空間的,象INTEL的CPU,其一頁在通常情況下是4K字節大小,而無論是數據段還是堆棧段都是由許多“頁”構成的,fork函數復制這兩個段,只是“邏輯”上的,並非“物理”上的,也就是說,實際執行fork時,物理空間上兩個進程的數據段和堆棧段都還是共享着的,當有一個進程寫了某個數據時,這時兩個進程之間的數據才有了區別,系統就將有區別的“頁”從物理上也分開。系統在空間上的開銷就可以達到最小。  

如何搞死Linux系統,看下面的小程序:

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

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

2.1如何啟動另一程序的執行

下面我們來看看一個進程如何來啟動另一個程序的執行。在Linux中要使用exec類的函數,exec類的函數不止一個,但大致相同,在Linux中,它們分別是:execl,execlp,execle,execv,execve和execvp,下面以execlp為例,其它函數究竟與execlp有何區別,請通過man exec命令來了解它們的具體情況。  一個進程一旦調用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函數的方法同前述Linux中的方法差不多,開設子進程后父進程等待子進程結束后才繼續運行。Linux在其一開始就是多任務的系統,所以從核心角度上講不需要spawn類函數。  
另外,有一個更簡單的執行其它程序的函數system,它是一個較高層的函數,實際上相當於在SHELL環境下執行一條命令,而exec類函數則是低層的系統調用。

3、Linux下的進程間通信 

首先,進程間通信至少可以通過傳送打開文件來實現,不同的進程通過一個或多個文件來傳遞信息,事實上,在很多應用系統里,都使用了這種方法。但一般說來,進程間通信(IPC:Inter Process Communication)不包括這種似乎比較低級的通信方法。Unix系統中實現進程間通信的方法很多,而且不幸的是,極少方法能在所有的Unix系統中進行移植(唯一一種是半雙工的管道,這也是最原始的一種通信方式)。而Linux作為一種新興的操作系統,幾乎支持所有的Unix下常用的進程間通信方法:管道、消息隊列、共享內存、信號量、套接口等等。

3.1管道 

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

無名管道由pipe()函數創建: 
#include <unistd.h> 
int pipe(int filedis[2]);

參數filedis返回兩個文件描述符:filedes[0]為讀而打開,filedes[1]為寫而打開。filedes[1]的輸出是filedes[0]的輸入。

下面的例子示范了如何在父進程和子進程間實現通信。

#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函數如open、close、read、write等來對它進行操作。

下面即是一個簡單的例子,假設我們已經創建了一個名為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); 

shmid為shmget函數返回的共享存儲標識符,addr和flag參數決定了以什么方式來確定連接的地址,函數的返回值即是該進程數據段所連接的實際地址,進程可以對此進程進行讀寫操作。
使用共享存儲來實現進程間通信的注意點是對數據存取的同步,必須確保當一個進程去讀取數據時,它所想要的數據已經寫好了。通常,信號量被要來實現對共享存儲數據存取的同步,另外,可以通過使用shmctl函數設置共享存儲內存的某些標志位如SHM_LOCK、SHM_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服務、TELNET服務等都是基於套接口編程來實現的。除了在異地的計算機進程間以外,套接口同樣適用於本地同一台計算機內部的進程間通信。

文章摘選:http://blog.csdn.net/wallwind/article/details/6899493


免責聲明!

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



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