1. 程式人生 > >Linux下的程序間通訊

Linux下的程序間通訊

 
   詳細的講述程序間通訊在這裡絕對是不可能的事情,而且筆者很難有信心說自己對這一部分內容的認識達到了什麼樣的地步,所以在這一節的開頭首先向大家推薦著 名作者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結構的關鍵字,它將來決定是建立新的訊號量集合,還是引用一個現有的訊號量集合。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);
}

   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下的多執行緒程式設計》中講述。