學習內容:在前面的學習中,咱們學習瞭如何建立進程以及如何對進程進行基本的控制,而這些都只是停留在父子進程之間的控制,本次將要學習不一樣的進程間進行通訊的方法。html
在以前的學習中咱們瞭解到,進程是一個程序的一次執行。這裏所說的進程通常是指運行在用戶態的進程,而因爲處於用戶態的不一樣進程之間是彼此隔離的,它們必須經過某種方式來進行通訊,就像現在不一樣地域的人們使用手機聯繫同樣。接下來咱們將學習如何爲不一樣的進程間創建通訊方式。node
Linux下的進程通訊手段基本上是從UNIX 平臺上的進程通訊手段繼承而來的。而對UNIX發展作出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟件發佈中心)在進程間的通訊方面的重點有所不一樣。前者是對UNIX早期的進程間通訊手段進行了系統的改進和擴充,造成了「system V IPC」,其通訊進程主要侷限在單個計算機內;後者則跳過了該限制,造成了基於套接口(socket)的進程間通訊機制。而Linux則把二者的優點都繼承了下來,以下圖所示:linux
如今Linux中經常使用的進程間通訊方式主要有如下幾種:程序員
接下來將詳細介紹前5種進程間通訊方式,套接字在「網絡編程」部分重點研究。編程
『1.管道概述』數組
管道是Linux中一種很重要的通訊方式,它能夠把一個程序的輸出直接鏈接到另外一個程序的輸入。還記得咱們以前使用man -k process | grep create
命令搜索與建立進程相關的函數:網絡
這就是管道的一種使用方式,即把man -k process
命令的輸出當作grep create
命令的輸入,進行二次檢索。數據結構
管道是Linux中進程間通訊的一種方式。這裏所說的管道主要指無名管道,它具備如下特色:異步
『2.管道系統調用』socket
(1)管道建立與關閉說明:
管道是基於文件述符的通訊方式,當一個管道創建時,它會建立兩個文件述符fds[0]和fds[1],其中fds[0]固定用於讀管道,而fd[1]固定用於寫管道,這樣就構成了一個半雙工的通道。
管道關閉時只需將這兩個文件描述符關閉便可,可以使用普通的close()函數逐個關閉各個文件描述符。
注意:當一個管道共享多對文件描述符時,若將其中一對讀寫文件描述符都刪除,則該管道就失效。
(2)管道建立函數:
建立管道能夠經過調用pipe()實現,如下列出了pipe()函數的語法要點:
(3)管道讀寫說明:
通常狀況下使用管道時,先建立一個管道,再經過fork()函數建立一子進程,該子進程會繼承父進程所建立的管道。爲了實現父子進程之間的讀寫,只需把無關的讀端或寫端的文件描述符關閉便可。例如在下圖中將父進程的寫端fd[1]和子進程的讀端fd[0]關閉。此時,父子進程之間就創建起了一條「子進程寫入父進程讀取」的通道。
一樣,也能夠關閉父進程的fd[0]和子進程的fd[1],這樣就能夠創建一條「父進程寫入子進程讀取」的通道。另外,父進程還能夠建立多個子進程,各個子進程都繼承了相應的fd[0]和fd[1],這時,只須要關閉相應端口就能夠創建其各子進程之間的通道。
(4)管道使用實例:
在下面的測試代碼中,首先建立管道,以後父進程使用fork()函數建立子進程,以後經過關閉父進程的讀描述符和子進程的寫描述符,創建起父子進程之間的管道通訊。
/* pipe.c */ #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #define MAX_DATA_LEN 256 #define DELAY_TIME 1 int main() { pid_t pid; int pipe_fd[2]; char buf[MAX_DATA_LEN]; const char data[] = "Pipe Test Program"; int real_read, real_write; memset((void*)buf, 0, sizeof(buf)); /* 建立管道 */ if (pipe(pipe_fd) < 0) { printf("pipe create error\n"); exit(1); } /* 建立一子進程 */ if ((pid = fork()) == 0) { /* 子進程關閉寫描述符,並經過使子進程暫停1s等待父進程關閉相應的讀描述符*/ close(pipe_fd[1]); sleep(DELAY_TIME * 3); /* 子進程讀取管道內容 */ if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0) { printf("%d bytes read from the pipe is '%s'\n", real_read, buf); } /* 關閉子進程讀描述符 */ close(pipe_fd[0]); exit(0); } else if (pid > 0) { /* 父進程關閉讀描述符,並經過使父進程暫停1s等待子進程關閉相應的寫描述符*/ close(pipe_fd[0]); sleep(DELAY_TIME); if((real_write = write(pipe_fd[1], data, strlen(data))) != -1) { printf("Parent wrote %d bytes : '%s'\n", real_write, data); } /* 關閉父進程寫描述符 */ close(pipe_fd[1]); /* 收集子進程退出信息 */ waitpid(pid, NULL, 0); exit(0); } }
運行結果以下圖所示:
『3.標準流管道』
(1)標準流管道函數說明:
與Linux的文件操做中有基於文件流的標準I/O操做同樣,管道的操做也支持基於文件流的模式。這種基於文件流的管道主要是用來建立一個鏈接到另外一個進程的管道,這裏的「另外一個進程」也就是一個能夠進行必定操做的可執行文件,例如,用戶執行「ls -l」或者本身編寫的程序「./pipe」等。因爲這一類操做很經常使用,所以標準流管道就將一系列的建立過程合併到一個函數popen()中完成。它所完成的工做有如下幾步。
這個函數的使用能夠大大減小代碼的編寫量,但同時也有一些不利之處,例如,它不如前面管道建立的函數那樣靈活多樣,而且用popen()建立的管道必須使用標準I/O函數進行操做,但不能使用前面的read()、write()一類不帶緩衝的I/O函數。
與之相對應,關閉用popen()建立的流管道必須使用函數pclose()來關閉該管道流。該函數關閉標準I/O流,並等待命令執行結束。
(2)函數格式:
(3)函數使用實例:
下面的程序使用popen()來執行「ps -ef」命令,能夠看出popen()函數使程序變得短小精悍:
/* standard_pipe.c */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #define BUFSIZE 1024 int main() { FILE *fp; char *cmd = "ps -ef"; char buf[BUFSIZE]; if ((fp = popen(cmd, "r")) == NULL) { printf("Popen error\n"); exit(1); } while ((fgets(buf, BUFSIZE, fp)) != NULL) { printf("%s",buf); } pclose(fp); exit(0); }
運行結果以下:
『4.FIFO』
(1)有名管道說明
前面介紹的管道是無名管道,它只能用於具備親緣關係的進程之間,這就大大地限制了管道的使用。有名管道的出現突破了這種限制,它可使互不相關的兩個進程實現彼此通訊。該管道能夠經過路徑名來指出,而且在文件系統中是可見的。在創建了管道以後,兩個進程就能夠把它看成普通文件同樣進行讀寫操做,使用很是方便。不過值得注意的是,FIFO是嚴格地遵循先進先出規則的,對管道及FIFO的讀老是從開始處返回數據,對它們的寫則把數據添加到末尾,它們不支持如lseek()等文件定位操做。
有名管道的建立可使用函數mkfifo(),該函數相似文件中的open()操做,能夠指定管道的路徑和打開的模式。
在建立管道成功以後,就可使用open()、read()和write()這些函數了。與普通文件的開發設置同樣,對於爲讀而打開的管道可在open()中設置O_RDONLY,對於爲寫而打開的管道可在open()中設置O_WRONLY,在這裏與普通文件不一樣的是阻塞問題。因爲普通文件的讀寫時不會出現阻塞問題,而在管道的讀寫中卻有阻塞的可能,這裏的非阻塞標誌能夠在open()函數中設定爲O_NONBLOCK。
(2)mkfifo()函數格式
(3)使用實例
下面的實例包含了兩個程序,一個用於讀管道,另外一個用於寫管道。其中在讀管道的程序裏建立管道,而且做爲main()函數裏的參數由用戶輸入要寫入的內容。讀管道的程序會讀出用戶寫入到管道的內容,這兩個程序採用的是阻塞式讀寫管道模式。
/* fifo_write.c 寫管道*/ #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MYFIFO "/tmp/myfifo" //有名管道文件名 #define MAX_BUFFER_SIZE PIPE_BUF int main(int argc, char * argv[]) //參數爲即將寫入的字符串 { int fd; char buff[MAX_BUFFER_SIZE]; int nwrite; if(argc <= 1) { printf("Usage: ./fifo_write string\n"); exit(1); } sscanf(argv[1], "%s", buff); /*以只寫阻塞方式打開FIFO管道*/ fd = open(MYFIFO, O_WRONLY); if (fd == -1) { printf("Open fifo file error\n"); exit(1); } /*向管道中寫入字符串*/ if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0) { printf("Write '%s' to FIFO\n", buff); } close(fd); exit(0); }
/*fifo_read.c 讀管道程序*/ #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MYFIFO "/tmp/myfifo" #define MAX_BUFFER_SIZE PIPE_BUF int main() { char buff[MAX_BUFFER_SIZE]; int fd; int nread; /*判斷有名管道是否已存在,若還沒有建立,則以相應的權限建立*/ if (access(MYFIFO, F_OK) == -1) { if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST)) { printf("Cannot create fifo file\n"); exit(1); } } /*以只讀阻塞方式打開有名管道*/ fd = open(MYFIFO, O_RDONLY); if (fd == -1) { printf("Open fifo file error\n"); exit(1); } while (1) { memset(buff, 0, sizeof(buff)); if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) { printf("Read '%s' from FIFO\n", buff); } } close(fd); exit(0); }
爲了可以較好地觀運行結果,須要把這兩個程序分別在兩個終端裏運行,在這裏首先啓動讀管道程序。讀管道進程在創建管道以後就開始循環地從管道里讀出內容,若是沒有數據可讀,則一直到寫管道進程向管道寫入數據。在啓動了寫管道程序後,讀進程可以從管道里讀出用戶的入內容,程序運行結果以下所示:
終端一:
終端二:
『1.信號概述』
信號是UNIX中所使用的進程通訊的一種最古老的方法。它是在軟件層次上對中斷機制的一種模擬,是一種異步通訊方式。
使用kill -l
命令能夠列出該系統所支持的全部信號的列表:
在上圖所示系統中,,信號值在32 以前的則有不一樣的名稱,而信號值在32 之後的都是用「SIGRTMIN」或「SIGRTMAX」開頭的,這就是兩類..型的信號。前者是從UNIX 系統中繼承下來的信號,爲不可靠信號(也稱爲非實時信號);後者是爲了解決前面「不可靠信號」的問題而進行了更改和擴充的信號,稱爲「可靠信號」(也稱爲實時信號)。
那麼,爲何以前的信號不可靠呢?這裏咱們首先介紹一下信號的生命週期。
一個完整的信號生命週期能夠分爲3個重要階段,這3個階段由4個重要事件來刻畫:信號產生、信號在進程中註冊、信號在進程中註銷、執行信號處理函數,相鄰兩個事件的時間間隔構成信號生命週期的一個階段,以下圖所示:
信號處理有多種方式,通常是由內核完成的,固然也能夠由用戶進程來完成,故在此沒有明確畫出。
用戶進程對信號的響應能夠有3種方式:
Linux中的大多數信號是提供給內核的,使用man 7 signal
能夠查看信號的定義及其默認操做。下圖展現了一部分:
『2.信號的發送與捕捉』
如今咱們想了解與信號有關的函數都有哪些,使用man -k signal
可查詢到全部與signal有關的函數,咱們重點研究如下幾個:kill()、raise()、alarm()以及pause()。
(1)kill()和raise()
kill()函數同讀者熟知的kill系統命令同樣,能夠發送信號給進程或進程組(實際上,kill系統命令只是kill()函數的一個用戶接口)。這裏須要注意的是,它不只能夠停止進程(實際上發出SIGKILL信號),也能夠向進程發其餘信號。
與kill()函數所不一樣的是,raise()函數容許進程向自身發送信號。
使用man 2 kill/man 3 raise
查詢函數相關信息,以下:
pid取值:①pid爲正數:要發送信號的進程號;②pid=0:信號被髮送到全部和當前進程在同一個進程組的進程;③pid=-1:,信號發給全部的進程表中的進程(除了進程號最大的進程外);④pid<-1時,信號發送給進程組號爲-pid的每個進程。
返回值:①成功:0,②失敗:-1。
返回值:①成功:0,②失敗:-1。
下面這個示例首先使用fork()建立了一個子進程,接着爲了保證子進程不在父進程調用kill()以前退出,在子進程中使用raise()函數向自身發送SIGSTOP 信號,使子進程暫停。接下來再在父進程中調用kill()向子進程發送信號,如本示例中使用的是SIGKILL。
/* kill_raise.c */ #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid; int ret; if ((pid = fork()) < 0) { printf("Fork error\n"); exit(1); } if (pid == 0) { printf("Child(pid : %d) is waiting for any signal\n", getpid()); raise(SIGSTOP); exit(0); } sleep(3); if ((waitpid(pid, NULL, WNOHANG)) == 0) { if ((ret = kill(pid, SIGKILL)) == 0) { printf("Parent kill %d\n",pid); } } waitpid(pid, NULL, 0); exit(0); }
運行結果以下圖所示:
(2)alarm()和pause()
alarm()也稱爲鬧鐘函數,它能夠在進程中設置一個定時器,當定時器指定的時間到時,它就向進程發送SIGALARM信號。要注意的是,一個進程只能有一個鬧鐘時間,若是在調用alarm()以前已設置過鬧鐘時間,則任何之前的鬧鐘時間都被新值所代替。
pause()函數是用於將調用進程掛起直至捕捉到信號爲止。這個函數很經常使用,一般能夠用於判斷信號是否已到。
該實例實際上已完成了一個簡單的sleep()函數的功能,因爲SIGALARM 默認的系統動做爲終止該進程,所以程序在打信息以前,就會被結束了。
/* alarm_pause.c */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { int ret = alarm(5); pause(); printf("I have been waken up.\n",ret);//此語句不會被執行 }
『3.信號的處理』
一個進程能夠決定在該進程中須要對哪些信號進行什麼樣的處理。例如,一個進程能夠選擇忽略某些信號而只處理其餘一些信號,另外,一個進程還能夠選擇如何處理信號。總之,這些都是與特定的進程相聯繫的。所以,首先就要創建進程與其信號之間的對應關係,這就是信號的處理。
信號處理的主要方法有兩種,一種是使用簡單的signal()函數,另外一種是使用信號集函數組。下面分別介紹這兩種處理方式。
(1)信號處理函數
使用signal()函數處理時,只須要指出要處理的信號和處理函數便可。它主要是用於前32種非實時信號的處理,不支持信號傳遞信息,可是因爲使用簡單、易於理解,所以也受到不少程序員的歡迎。
Linux還支持一個更健壯、更新的信號處理函數sigaction(),推薦使用該函數。
這裏對函數原型進行簡要說明:可先用以下的typedef進行替換說明:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
第一個參數signum:指明瞭所要處理的信號類型,它能夠取除了SIGKILL和SIGSTOP外的任何一種信號。
第二個參數handler:描述了與信號關聯的動做,它能夠取如下三種值:
①SIG_IGN:忽略該信號,②SIG_DFL:恢復對信號的系統默認處理,③sighandler_t類型的函數指針:用戶自定義的處理函數。
這裏着重講解sigaction()函數中第2個和第3個參數用到的sigaction結構。可以使用man -k sigaction
查看定義:
sa_handler 是一個函數指針,指定信號處理函數,取值與signal()函數相同;
sa_mask 是一個信號集,它能夠指定在信號處理程序執行過程當中哪些信號應當被屏蔽,在調用信號捕獲函數以前,該信號集要加入到信號的信號屏蔽字中;
sa_flags 中包含了許多標誌位,是對信號進行處理的各個選擇項。
第一個實例代表瞭如何使用signal()函數捕捉相應信號,並作出給定的處理。這裏,my_func就是信號處理的函數指針。第二個實例是用sigaction()函數
實現一樣的功能。如下是使用signal()函數的示例:
/* signal.c */ #include <signal.h> #include <stdio.h> #include <stdlib.h> //自定義信號處理函數 void my_func(int sign_no) { if (sign_no == SIGINT) { printf("I have get SIGINT\n"); } else if (sign_no == SIGQUIT) { printf("I have get SIGQUIT\n"); } } int main() { printf("Waiting for signal SIGINT or SIGQUIT...\n"); //發出相應的信號,並跳轉到信號處理函數處 signal(SIGINT, my_func); signal(SIGQUIT, my_func); pause(); exit(0); }
運行結果以下所示:
若使用sigaction()函數實現一樣功能,main()函數作以下修改:
//sigaction.c //前部分省略 int main() { struct sigaction action; printf("Waiting for signal SIGINT or SIGQUIT...\n"); /* sigaction結構初始化 */ action.sa_handler = my_func; sigemptyset(&action.sa_mask); action.sa_flags = 0; /* 發出相應的信號,並跳轉到信號處理函數處 */ sigaction(SIGINT, &action, 0); sigaction(SIGQUIT, &action, 0); pause(); exit(0); }
(2)信號集函數組
咱們已經知道,咱們能夠經過信號來終止進程,也能夠經過信號來在進程間進行通訊,程序也能夠經過指定信號的關聯處理函數來改變信號的默認處理方式,也能夠屏蔽某些信號,使其不能傳遞給進程。那麼咱們應該如何設定咱們須要處理的信號,咱們不須要處理哪些信號等問題呢?信號集函數就是幫助咱們解決這些問題的。
如下爲信號集函數:
int sigemptyset(sigset_t *set); //該函數的做用是將信號集初始化爲空。 int sigfillset(sigset_t *set); //該函數的做用是把信號集初始化包含全部已定義的信號。 int sigaddset(sigset_t *set, int signo); //該函數的做用是把信號signo添加到信號集set中,成功時返回0,失敗時返回-1。 int sigdelset(sigset_t *set, int signo); //該函數的做用是把信號signo從信號集set中刪除,成功時返回0,失敗時返回-1. int sigismember(sigset_t *set, int signo); //該函數的做用是判斷給定的信號signo是不是信號集中的一個成員,若是是返回1,若是不是,返回0,若是給定的信號無效,返回-1; int sigpromask(int how, const sigset_t *set, sigset_t *oset); //該函數能夠根據參數指定的方法修改進程的信號屏蔽字。新的信號屏蔽字由參數set(非空)指定,而原先的信號屏蔽字將保存在oset(非空)中。若是set爲空,則how沒有意義,但此時調用該函數,若是oset不爲空,則把當前信號屏蔽字保存到oset中。 int sigpending(sigset_t *set); //該函數的做用是將被阻塞的信號中停留在待處理狀態的一組信號寫到參數set指向的信號集中,成功調用返回0,不然返回-1,並設置errno代表錯誤緣由。 int sigsuspend(const sigset_t *sigmask); //該函數經過將進程的屏蔽字替換爲由參數sigmask給出的信號集,而後掛起進程的執行。注意操做的前後順序,是先替換再掛起程序的執行。程序將在信號處理函數執行完畢後繼續執行。若是接收到信號終止了程序,sigsuspend()就不會返回,若是接收到的信號沒有終止程序,sigsuspend()就返回-1,並將errno設置爲EINTR。
注意:若是一個信號被進程阻塞,它就不會傳遞給進程,但會停留在待處理狀態,當進程解除對待處理信號的阻塞時,待處理信號就會馬上被處理。
如下面的程序爲例,介紹上述函數的用法:
#include <stdio.h> #include <signal.h> #include <unistd.h> void handler(int sig) { printf("Handle the signal %d\n", sig); } int main(int argc, char **argv) { sigset_t sigset; // 用於記錄屏蔽字 sigset_t ign; // 用於記錄被阻塞(屏蔽)的信號集 struct sigaction act; // 清空信號集 sigemptyset(&sigset); sigemptyset(&ign); // 向信號集中添加 SIGINT sigaddset(&sigset, SIGINT); // 設置處理函數 和 信號集 act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, 0); printf("Wait the signal SIGNAL...\n"); pause(); // 設置進程屏蔽字, 在本例中爲屏蔽 SIGINT sigprocmask(SIG_SETMASK, &sigset, 0); printf("Please press Ctrl + C in 10 seconds...\n"); sleep(10); // 測試 SIGINT 是否被屏蔽 sigpending(&ign); if (sigismember(&ign, SIGINT)) { printf("The SIGINT signal has ignored\n"); } // 從信號集中刪除信號 SIGINT sigdelset(&sigset, SIGINT); printf("Wait the signal SIGINT...\n"); // 將進程的屏蔽字從新設置, 即取消對 SIGINT 的屏蔽, 並掛起進程 sigsuspend(&sigset); printf("The process will exit in 5 seconds!\n"); sleep(5); return 0; }
運行結果以下圖所示:
『1.信號量概述』
在多任務操做系統環境下,多個進程會同時運行,而且一些進程之間可能存在必定的關聯。多個進程可能爲了完成同一個任務會相互協做,這樣造成進程之間的同步關係。並且在不一樣進程之間,爲了爭奪有限的系統資源(硬件或軟件資源)會進入競爭狀態,這就是進程之間的互斥關係。
進程之間的互斥與同步關係存在的根源在於臨界資源。臨界資源是在同一個時刻只容許有限個(一般只有一個)進程能夠訪問(讀)或修改(寫)的資源,一般包括硬件資源(處理器、內存、存儲器以及其餘外圍設備等)和軟件資源(共享代碼段,共享結構和變量等)。訪問臨界資源的代碼叫作臨界區,臨界區自己也會成爲臨界資源。
信號量是用來解決進程之間的同步與互斥問題的一種進程之間通訊機制,包括一個稱爲信號量的變量和在該信號量下等待資源的進程等待隊列,以及對信號量進行的兩個原子操做(PV操做)。其中信號量對應於某一種資源,取一個非負的整型值。信號量值指的是當前可用的該資源的數量,若它等於0 則意味着目前沒有可用的資源。PV原子操做的具體定義以下:
最簡單的信號量是隻能取0和1兩種值,這種信號量被叫作二維信號量。在這裏,咱們主要討論二維信號量。
『2.信號量的應用』
(1)函數說明:
在Linux系統中,使用信號量一般分爲如下幾個步驟:
(2)函數格式:
(3)使用實例:
下面的程序展現了信號量的概念及其基本用法。在示例程序中,使用信號量來控制父子進程之間的執行順序。
方便起見,咱們將信號量相關的函數封裝成二維單個信號量的幾個基本函數。它們分別爲爲信號量初始化函數(或者信號量賦值函數)init_sem()、P 操做數sem_p()、V操做函數sem_v()以及刪除信號量的函數del_sem()等,具體實現以下所示:
/* sem_com.c */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> union semun { int val; struct semid_ds *buf; unsigned short *array; }; /*信號量初始化(賦值)函數*/ int init_sem(int sem_id, int init_value) { union semun sem_union; sem_union.val = init_value; //init_value爲初始值 if (semctl(sem_id, 0, SETVAL, sem_union) == -1) { perror("Initialize semaphore"); return -1; } return 0; } /*從系統中刪除信號量的函數*/ int del_sem(int sem_id) { union semun sem_union; if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) { perror("Delete semaphore"); return -1; } } /*P操做函數*/ int sem_p(int sem_id) { struct sembuf sem_b; /*單個信號量的編號應該爲0 */ sem_b.sem_num = 0; /*表示P操做*/ sem_b.sem_op = -1; /*系統自動釋放將會在系統中殘留的信號量*/ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { perror("P operation"); return -1; } return 0; } /*V操做函數*/ int sem_v(int sem_id) { struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1; sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { perror("V operation"); return -1; } return 0; }
下面編寫一個測試程序,調用這些簡單易用的接口,從而解決控制兩個進程之間的執行順序的同步問題。代碼以下:
/* fork.c */ #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define DELAY_TIME 3 /*爲了突出演示效果,等待幾秒鐘*/ int main(void) { pid_t result; int sem_id; sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /*建立一個信號量*/ init_sem(sem_id, 0); /*調用fork()函數*/ result = fork(); if(result == -1) { perror("Fork\n"); } else if (result == 0) //返回值爲0表明子進程 { printf("Child process will wait for some seconds...\n"); sleep(DELAY_TIME); printf("The returned value is %d in the child process(PID = %d)\n", result, getpid()); sem_v(sem_id); } else //返回值大於0表明父進程 { sem_p(sem_id); printf("The returned value is %d in the father process(PID = %d)\n", result, getpid()); sem_v(sem_id); del_sem(sem_id); } exit(0); }
能夠先從fork.c中刪去信號量相關的代碼部分查看運行結果:
再添加信號量的控制部分並運行結果:
『1.共享內存概述』
採用共享內存通訊的一個顯而易見的好處是效率高,由於進程能夠直接讀寫內存,而不須要任何數據的拷貝。對於像管道和消息隊列等通訊方式,則須要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據:一次從輸入文件到共享內存區,另外一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不老是讀寫少許數據後就解除映射,有新的通訊時,再從新創建共享內存區域。而是保持共享區域,直到通訊完畢爲止,這樣,數據內容一直保存在共享內存中,並無寫回文件。共享內存中的內容每每是在解除映射時才寫回文件的。所以,採用共享內存的通訊方式效率是很是高的。
Linux的2.2.x內核支持多種共享內存方式,如mmap()系統調用,Posix共享內存,以及系統V共享內存。linux發行版本如Redhat 8.0支持mmap()系統調用及系統V共享內存,但還沒實現Posix共享內存,接下來將主要介紹mmap()系統調用及系統V共享內存API的原理及應用。
『2.mmap()系統調用』
(1)函數說明:
mmap()系統調用使得進程之間經過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程能夠向訪問普通內存同樣對文件進行訪問,沒必要再調用read(),write()等操做。
注:實際上,mmap()系統調用並非徹底爲了用於共享內存而設計的。它自己提供了不一樣於通常對普通文件的訪問方式,進程能夠像讀寫內存同樣對普通文件的操做。而Posix或系統V的共享內存IPC則純粹用於共享目的,固然mmap()實現共享內存也是其主要應用之一。
(2)函數格式:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
參數含義:
返回值:若映射成功則返回映射區的內存起始地址,不然返回MAP_FAILED(-1),錯誤緣由存於errno 中。
munmap():
int munmap( void * addr, size_t len )
該調用在進程地址空間中解除一個映射關係,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關係解除後,對原來映射地址的訪問將致使段錯誤發生。
int msync ( void * addr , size_t len, int flags)
通常說來,進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,每每在調用munmap()後才執行該操做。能夠經過調用msync()實現磁盤上文件內容與共享內存區的內容一致。
(3)使用實例:
下面給出兩個進程經過映射普通文件實現共享內存通訊。
示例包含兩個子程序:map_normalfile1.c及map_normalfile2.c。編譯兩個程序,可執行文件分別爲map_normalfile1及map_normalfile2。兩個程序經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。
map_normalfile2試圖打開命令行參數指定的一個普通文件,把該文件映射到進程的地址空間,並對映射後的地址空間進行寫操做。
map_normalfile1把命令行參數指定的文件映射到進程地址空間,而後對映射後的地址空間執行讀操做。這樣,兩個進程經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。
/*-------------map_normalfile1.c-----------*/ #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; char temp; fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,"",1); p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); close( fd ); temp = 'a'; for(i=0; i<10; i++) { temp += 1; memcpy( ( *(p_map+i) ).name, &temp,2 ); ( *(p_map+i) ).age = 20+i; } printf(" initialize over \n "); sleep(10); munmap( p_map, sizeof(people)*10 ); printf( "umap ok \n" ); } /*-------------map_normalfile2.c-----------*/ #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; fd=open( argv[1],O_CREAT|O_RDWR,00777 ); p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); for(i = 0;i<10;i++) { printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age ); } munmap( p_map,sizeof(people)*10 ); }
map_normalfile1.c首先定義了一個people數據結構,(在這裏採用數據結構的方式是由於,共享內存區的數據每每是有固定格式的,這由通訊的各個進程決定,採用結構的方式有廣泛表明性)。map_normfile1首先打開或建立一個文件,並把文件的長度設置爲5個people結構大小。而後從mmap()的返回地址開始,設置了10個people結構。而後,進程睡眠10秒鐘,等待其餘進程映射同一個文件,最後解除映射。
map_normfile2.c只是簡單的映射一個文件,並以people數據結構的格式從mmap()返回的地址處讀取10個people結構,並輸出讀取的值,而後解除映射。
分別把兩個程序編譯成可執行文件map_normalfile1和map_normalfile2後,在一個終端上先運行./map_normalfile1 ./test,程序輸出結果以下:
在map_normalfile1輸出initialize over以後,輸出umap ok以前,在另外一個終端上運行map_normalfile2 /tmp/test_shm,將會產生以下輸出(爲了節省空間,輸出結果爲稍做整理後的結果):
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;
在map_normalfile1 輸出umap ok後,運行map_normalfile2則輸出以下結果:
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;
『3.System V共享內存』
(1)函數說明:
共享內存的實現分爲兩個步驟,第一步是建立共享內存,這裏用到的函數是shmget(),也就是從內存中得到一段共享內存區域,第二步映射共享內存,也就是把這段建立的共享內存映射到具體的進程空間中,這裏使用的函數是shmat()。到這裏,就可使用這段共享內存了,也就是可使用不帶緩衝的I/O 讀寫命令對其進行操做。除此以外,固然還有撤銷映射的操做,其函數爲shmdt()。這裏就主要介紹這3個函數。
(2)函數格式:
(3)使用實例:
下面的程序將給出System V共享內存API的使用方法,並對比分析System V共享機制與mmap()映射普通文件實現共享內存之間的差別。首先給出兩個進程經過System V共享內存通訊的範例:
/***** testwrite.c *******/ #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; main(int argc, char** argv) { int shm_id,i; key_t key; char temp; people *p_map; char* name = "./test"; key = ftok(name,0); if(key==-1) perror("ftok error"); shm_id=shmget(key,4096,IPC_CREAT); if(shm_id==-1) { perror("shmget error"); return; } p_map=(people*)shmat(shm_id,NULL,0); temp='a'; for(i = 0;i<10;i++) { temp+=1; memcpy((*(p_map+i)).name,&temp,1); (*(p_map+i)).age=20+i; } if(shmdt(p_map)==-1) perror(" detach error "); }
/********** testread.c ************/ #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; main(int argc, char** argv) { int shm_id,i; key_t key; people *p_map; char* name = "./test"; key = ftok(name,0); if(key == -1) perror("ftok error"); shm_id = shmget(key,4096,IPC_CREAT); if(shm_id == -1) { perror("shmget error"); return; } p_map = (people*)shmat(shm_id,NULL,0); for(i = 0;i<10;i++) { printf( "name:%s\n",(*(p_map+i)).name ); printf( "age %d\n",(*(p_map+i)).age ); } if(shmdt(p_map) == -1) perror(" detach error "); }
testwrite.c建立一個系統V共享內存區,並在其中寫入格式化數據;testread.c訪問同一個系統V共享內存區,讀出其中的格式化數據。分別把兩個程序編譯爲testwrite及testread,前後執行./testwrite及./testread則./testread運行結果以下:
經過對試驗結果分析,對比系統V與mmap()映射普通文件實現共享內存通訊,能夠得出以下結論:
『1.消息隊列概述』
消息隊列,即一些消息的列表。用戶能夠從消息隊列中添加消息和讀取消息等。從這點上看消息隊列具備必定的FIFO特性,可是它能夠實現消息的隨機查詢,比FIFO具備更大的優點。同時,消息又是存在於內核中的,由「隊列ID」來標識。
『2.消息隊列的應用』
(1)函數說明:
消息隊列的實現包括建立或打開消息隊列、添加消息、讀取消息和制消息隊列這4種操做。
(2)函數格式:
(3)使用實例:
這裏首先介紹一個函數ftok(),它能夠根據不一樣的路徑和關鍵字產生標準的key。共享內存,消息隊列,信號量它們三個都是找一箇中間介質,來進行通訊的,而使用ftok()產生一個號,就能夠惟一區分這個介質了。
ftok()函數的具體形式以下:
key_t ftok(const char *pathname, int proj_id);
其中參數fname是指定的文件名,這個文件必須是存在的並且能夠訪問的。id是子序號,它是一個8bit的整數。即範圍是0~255。當函數執行成功,則會返回key_t鍵值,不然返回-1。在通常的UNIX中,一般是將文件的索引節點取出,而後在前面加上子序號就獲得key_t的值。
對於該函數,還有如下幾點補充說明:
如下是一個簡單的驗證程序:
#include <stdio.h> #include <sys/sem.h> #include <stdlib.h> int main() { key_t semkey; if((semkey = ftok("./test", 1))<0) { printf("ftok failed\n"); exit(EXIT_FAILURE); } printf("ftok ok ,semkey = %d\n", semkey); return 0; }
運行結果以下:
言歸正傳。下面實例體現瞭如何使用消息隊列進行兩個進程(發送端和接收端)之間的通訊,包括消息隊列的建立、消息發送與讀取、消息隊列的撤銷和刪除等多種操做。
/* msgsnd.c 消息隊列發送端*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFFER_SIZE 512 struct message { long msg_type; char msg_text[BUFFER_SIZE]; }; int main() { int qid; key_t key; struct message msg; /*根據不一樣的路徑和關鍵字產生標準的key*/ if ((key = ftok(".", 'a')) == -1) { perror("ftok"); exit(1); } /*建立消息隊列*/ if ((qid = msgget(key, IPC_CREAT|0666)) == -1) { perror("msgget"); exit(1); } printf("Open queue %d\n",qid); while(1) { printf("Enter some message to the queue:"); if ((fgets(msg.msg_text, BUFFER_SIZE, stdin)) == NULL) { puts("no message"); exit(1); } msg.msg_type = getpid(); /*添加消息到消息隊列*/ if ((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0) { perror("message posted"); exit(1); } if (strncmp(msg.msg_text, "quit", 4) == 0) { break; } } exit(0); }
/* msgrcv.c 消息隊列接收端*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFFER_SIZE 512 struct message { long msg_type; char msg_text[BUFFER_SIZE]; }; int main() { int qid; key_t key; struct message msg; /*根據不一樣的路徑和關鍵字產生標準的key*/ if ((key = ftok(".", 'a')) == -1) { perror("ftok"); exit(1); } /*建立消息隊列*/ if ((qid = msgget(key, IPC_CREAT|0666)) == -1) { perror("msgget"); exit(1); } printf("Open queue %d\n", qid); do { /*讀取消息隊列*/ memset(msg.msg_text, 0, BUFFER_SIZE); if (msgrcv(qid, (void*)&msg, BUFFER_SIZE, 0, 0) < 0) { perror("msgrcv"); exit(1); } printf("The message from process %d : %s", msg.msg_type, msg.msg_text); } while(strncmp(msg.msg_text, "quit", 4)); /*從系統內核中移走消息隊列*/ if ((msgctl(qid, IPC_RMID, NULL)) < 0) { perror("msgctl"); exit(1); } exit(0); }
如下是程序的運行結果,輸入「quit」則兩個進程都結束。