Linux下的處理序間通訊-詳解

來源:互聯網
上載者:User
   詳細的講述處理序間通訊在這裡絕對是不可能的事情,而且筆者很難有信心說自己對這一部分內容的認識達到了什麼樣的地步,所以在這一節的開頭首先向大家推薦著 名作者Richard Stevens的著名作品:《Advanced Programming in the UNIX Environment》,它的中文譯本《UNIX環境進階編程》已有機械工業出版社出版,原文精彩,譯文同樣地道,如果你的確對在Linux下編程有濃 厚的興趣,那麼趕緊將這本書擺到你的書桌上或電腦旁邊來。說這麼多實在是難抑心中的景仰之情,言歸正傳,在這一節裡,我們將介紹處理序間通訊最最初步和最 最簡單的一些知識和概念。
   首先,處理序間通訊至少可以通過傳送開啟檔案來實現,不同的進程通過一個或多個檔案來傳遞資訊,事實上,在很多應用系統裡,都使用了這種方法。但一般說來, 處理序間通訊(IPC:InterProcess Communication)不包括這種似乎比較低級的通訊方法。Unix系統中實現處理序間通訊的方法很多,而且不幸的是,極少方法能在所有的Unix系 統中進行移植(唯一一種是半雙工的管道,這也是最原始的一種通訊方式)。而Linux作為一種新興的作業系統,幾乎支援所有的Unix下常用的處理序間通訊 方法:管道、訊息佇列、共用記憶體、訊號量、套介面等等。下面我們將逐一介紹。

   2.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);
}

   2.3.2 訊息佇列
   訊息佇列用於運行於同一台機器上的處理序間通訊,它和管道很相似,是一個在系統核心中用來儲存訊息的隊列,它在系統核心中是以訊息鏈表的形式出現。訊息鏈表中節點的結構用msg聲明。
事實上,它是一種正逐漸被淘汰的通訊方式,我們可以用流管道或者套介面的方式來取代它,所以,我們對此方式也不再解釋,也建議讀者忽略這種方式。

   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等來實現。

   2.3.4 訊號量
   訊號量又稱為號誌,它是用來協調不同進程間的資料對象的,而最主要的應用是前一節的共用記憶體方式的處理序間通訊。本質上,訊號量是一個計數器,它用來記錄對某個資源(如共用記憶體)的存取狀況。一般說來,為了獲得共用資源,進程需要執行下列操作:
   (1) 測試控制該資源的訊號量。
   (2) 若此訊號量的值為正,則允許進行使用該資源。進程將訊號量減1。
   (3) 若此訊號量為0,則該資源目前不可用,進程進入睡眠狀態,直至訊號量值大於0,進程被喚醒,轉入步驟(1)。
   (4) 當進程不再使用一個訊號量控制的資源時,訊號量值加1。如果此時有進程正在睡眠等待此訊號量,則喚醒此進程。
    維護訊號量狀態的是Linux核心作業系統而不是使用者進程。我們可以從標頭檔/usr/src/linux/include /linux /sem.h 中看到核心用來維護訊號量狀態的各個結構的定義。訊號量是一個資料集合,使用者可以單獨使用這一集合的每個元素。要調用的第一個函數是semget,用以獲 得一個訊號量ID。

struct sem {
  short sempid;/* pid of last operaton */
  ushort semval;/* current value */
  ushort semncnt;/* num procs awaiting increase in semval */
  ushort semzcnt;/* num procs awaiting semval = 0 */
}

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/sem.h>
   int semget(key_t key, int nsems, int flag);

   key是前面講過的IPC結構的關鍵字,flag將來決定是建立新的訊號量集合,還是引用一個現有的訊號量集合。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);
}

semget()

     可以使用系統調用semget()建立一個新的訊號量集,或者存取一個已經存在的訊號量集:

系統調用:semget();
原型:intsemget(key_t key,int nsems,int semflg);
傳回值:如果成功,則返回訊號量集的IPC標識符。如果失敗,則返回-1:errno=EACCESS(沒有許可權)
EEXIST(訊號量集已經存在,無法建立)
EIDRM(訊號量集已經刪除)
ENOENT(訊號量集不存在,同時沒有使用IPC_CREAT)
ENOMEM(沒有足夠的記憶體建立新的訊號量集)
ENOSPC(超出限制)    系統調用semget()的第一個參數是關鍵字值(一般是由系統調用ftok()返回的)。系統核心將此值和系統中存在的其他的訊號量集的關鍵字值進行比較。開啟和存取操作與參數semflg中的內容相關。IPC_CREAT如果訊號量集在系統核心中不存在,則建立訊號量集。IPC_EXCL當和 IPC_CREAT一同使用時,如果訊號量集已經存在,則調用失敗。如果單獨使用IPC_CREAT,則semget()要麼返回新建立的訊號量集的標識符,要麼返回系統中已經存在的同樣的關鍵字值的訊號量的標識符。如果IPC_EXCL和IPC_CREAT一同使用,則要麼返回新建立的訊號量集的標識符,要麼返回-1。IPC_EXCL單獨使用沒有意義。參數nsems指出了一個新的訊號量集中應該建立的訊號量的個數。訊號量集中最多的訊號量的個數是在linux/sem.h中定義的:#defineSEMMSL32/*<=512maxnumofsemaphoresperid*/
下面是一個開啟和建立訊號量集的程式:
intopen_semaphore_set(key_t keyval,int numsems)
{
intsid;
if(!numsems)
return(-1);
if((sid=semget(mykey,numsems,IPC_CREAT|0660))==-1)
{
return(-1);
}
return(sid);
}
};==============================================================

semop()

系統調用:semop();
調用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
傳回值:0,如果成功。-1,如果失敗:errno=E2BIG(nsops大於最大的ops數目)
EACCESS(許可權不夠)
EAGAIN(使用了IPC_NOWAIT,但操作不能繼續進行)
EFAULT(sops指向的地址無效)
EIDRM(訊號量集已經刪除)
EINTR(當睡眠時接收到其他訊號)
EINVAL(訊號量集不存在,或者semid無效)
ENOMEM(使用了SEM_UNDO,但無足夠的記憶體建立所需的資料結構)
ERANGE(訊號量值超出範圍)

    第一個參數是關鍵字值。第二個參數是指向將要操作的數組的指標。第三個參數是數組中的操作的個數。參數sops指向由sembuf組成的數組。此數組是在linux/sem.h中定義的:/*semop systemcall takes an array of these*/
structsembuf{
ushortsem_num;/*semaphore index in array*/
shortsem_op;/*semaphore operation*/
shortsem_flg;/*operation flags*/
sem_num將要處理的訊號量的個數。
sem_op要執行的操作。
sem_flg操作標誌。    如果sem_op是負數,那麼訊號量將減去它的值。這和訊號量控制的資源有關。如果沒有使用IPC_NOWAIT,那麼調用進程將進入睡眠狀態,直到訊號量控制的資源可以使用為止。如果sem_op是正數,則訊號量加上它的值。這也就是進程釋放訊號量控制的資源。最後,如果sem_op是0,那麼調用進程將調用sleep(),直到訊號量的值為0。這在一個進程等待完全閒置資源時使用。===============================================================

semctl()

系統調用:semctl();
原型:int semctl(int semid,int semnum,int cmd,union semunarg);
傳回值:如果成功,則為一個正數。
如果失敗,則為-1:errno=EACCESS(許可權不夠)
EFAULT(arg指向的地址無效)
EIDRM(訊號量集已經刪除)
EINVAL(訊號量集不存在,或者semid無效)
EPERM(EUID沒有cmd的權利)
ERANGE(訊號量值超出範圍)

    系統調用semctl用來執行在訊號量集上的控制操作。這和在訊息佇列中的系統調用msgctl是十分相似的。但這兩個系統調用的參數略有不同。因為訊號量一般是作為一個訊號量集使用的,而不是一個單獨的訊號量。所以在訊號量集的操作中,不但要知道IPC關鍵字值,也要知道訊號量集中的具體的訊號量。這兩個系統調用都使用了參數cmd,它用來指出要操作的具體命令。兩個系統調用中的最後一個參數也不一樣。在系統調用msgctl中,最後一個參數是指向核心中使用的資料結構的指標。我們使用此資料結構來取得有關訊息佇列的一些資訊,以及設定或者改變隊列的存取許可權和使用者。但在訊號量中支援額外的可選的命令,這樣就要求有一個更為複雜的資料結構。
系統調用semctl()的第一個參數是關鍵字值。第二個參數是訊號量數目。    參數cmd中可以使用的命令如下:
    ·IPC_STAT讀取一個訊號量集的資料結構semid_ds,並將其儲存在semun中的buf參數中。
    ·IPC_SET設定訊號量集的資料結構semid_ds中的元素ipc_perm,其值取自semun中的buf參數。
    ·IPC_RMID將訊號量集從記憶體中刪除。
    ·GETALL用於讀取訊號量集中的所有訊號量的值。
    ·GETNCNT返回正在等待資源的進程數目。
    ·GETPID返回最後一個執行semop操作的進程的PID。
    ·GETVAL返回訊號量集中的一個單個的訊號量的值。
    ·GETZCNT返回這在等待完全閒置資源的進程數目。
    ·SETALL設定訊號量集中的所有的訊號量的值。
    ·SETVAL設定訊號量集中的一個單獨的訊號量的值。    參數arg代表一個semun的執行個體。semun是在linux/sem.h中定義的:
/*arg for semctl systemcalls.*/
unionsemun{
intval;/*value for SETVAL*/
structsemid_ds*buf;/*buffer for IPC_STAT&IPC_SET*/
ushort*array;/*array for GETALL&SETALL*/
structseminfo*__buf;/*buffer for IPC_INFO*/
void*__pad;    val當執行SETVAL命令時使用。buf在IPC_STAT/IPC_SET命令中使用。代表了核心中使用的訊號量的資料結構。array在使用GETALL/SETALL命令時使用的指標。
    下面的程式返回訊號量的值。當使用GETVAL命令時,調用中的最後一個參數被忽略:intget_sem_val(intsid,intsemnum)
{
return(semctl(sid,semnum,GETVAL,0));
}    下面是一個實際應用的例子:#defineMAX_PRINTERS5
printer_usage()
{
int x;
for(x=0;x<MAX_PRINTERS;x++)
printf("Printer%d:%d/n/r",x,get_sem_val(sid,x));
}    下面的程式可以用來初始化一個新的訊號量值:void init_semaphore(int sid,int semnum,int initval)
{
union semunsemopts;
semopts.val=initval;
semctl(sid,semnum,SETVAL,semopts);
}    注意系統調用semctl中的最後一個參數是一個等位型別的副本,而不是一個指向等位型別的指標。

 

   2.3.5 套介面
    套介面(socket)編程是實現Linux系統和其他大多數作業系統中處理序間通訊的主要方式之一。我們熟知的WWW服務、FTP服務、TELNET服務 等都是基於套介面編程來實現的。除了在異地的電腦進程間以外,套介面同樣適用於本地同一台電腦內部的處理序間通訊。關於套介面的經典教材同樣是 Richard Stevens編著的《Unix網路編程:連網的API和通訊端》,清華大學出版社出版了該書的影印版。它同樣是Linux程式員的必備書籍之一。
    關於這一部分的內容,可以參照本文作者的另一篇文章《設計自己的網路螞蟻》,那裡由常用的幾個套介面函數的介紹和樣本程式。這一部分或許是Linux進程 間通訊編程中最須關注和最迷人的一部分,畢竟,Internet 正在我們身邊以不可思議的速度發展著,如果一個程式員在設計編寫他下一個程式的時候,根本沒有考慮到網路,考慮到Internet,那麼,可以說,他的設 計很難成功。

3 Linux的進程和Win32的進程/線程比較
   熟悉WIN32編程的人一定知道,WIN32的進程管理方式與Linux上有著很大區別,在UNIX裡,只有進程的概念,但在WIN32裡卻還有一個"線程"的概念,那麼Linux和WIN32在這裡究竟有著什麼區別呢?
    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了。
   Linux也有自己的多線程函數pthread,它既不同於Linux的進程,也不同於WIN32下的進程,關於pthread的介紹和如何在Linux環境下編寫多線程程式我們將在另一篇文章《Linux下的多線程編程》中講述。  

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.