詳細的講述進程間通訊在這裏絕對是不可能的事情,並且筆者很難有信心說本身對這一部份內容的認識達到了什麼樣的地步,因此在這一節的開頭首先向你們推薦著 名做者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()建立一個新的信號量集,或者存取一個已經存在的信號量集: linux
系統調用: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下的多線程編程》中講述。 數組