Linux環境下,進程地址空間相互獨立,每一個進程各自有不一樣的用戶地址空間。任何一個進程的全局變量在另外一個進程中都看不到,因此進程和進程之間不能相互訪問,要交換數據必須經過內核,在內核中開闢一塊緩衝區,進程1把數據從用戶空間拷到內核緩衝區,進程2再從內核緩衝區把數據讀走,內核提供的這種機制稱爲進程間通訊(IPC,InterProcess Communication)。shell
在進程間完成數據傳遞須要藉助操做系統提供特殊的方法,如:管道、命名管道、信號、消息隊列、共享內存、信號量、套接字等。隨着計算機的蓬勃發展,一些方法因爲自身設計缺陷被淘汰或者棄用。現今經常使用的進程間通訊方式有:服務器
管道的通訊方式爲:寫端每次都將數據寫入管道緩衝區的 末尾 ,而讀端每次都從管道緩衝區的 頭部 讀出數據。數據結構
管道的實質是內核利用 環形隊列 的數據結構在 內核緩衝區 中的一個實現,默認設置大小爲4K,能夠經過ulimit -a
命令查看。因爲利用 環形隊列 進行實現,讀和寫的位置都是自動增加的,不能隨意改變,一個數據只能被讀取一次,讀取後數據就會從緩衝區中移除。當緩衝區讀空或者寫滿時,有必定的規則控制相應的讀進程或者寫進程進入等待隊列,當空的緩衝區有新數據寫入或者滿的緩衝區有數據讀出來時,就喚醒等待隊列中的進程繼續讀寫。異步
沒有名稱函數
建立管道操作系統
int pipe(int pipefd[2]); //成功:0;失敗:-1,設置errno
設計
函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] → r; fd[1] → w,就像0對應標準輸入,1對應標準輸出同樣。向管道文件讀寫數據實際上是在讀寫內核緩衝區指針
管道建立成功之後,建立該管道的進程(父進程)同時掌握着管道的讀端和寫端。如何實現父子進程間通訊呢?一般能夠採用以下步驟:code
eg.server
#include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char *argv[]) { int fd[2]; int res = pipe(fd); if(res == -1) { perror("pipe error"); return -1; } pid_t pid = fork(); if(pid == 0){ // child close(fd[1]);// close write char buf[100]; int count = 0; while(count < 5) { memset(buf,0,sizeof(buf)); int res = read(fd[0], buf, sizeof(buf)); if(res > 0) { buf[res - 1] = '\0'; } printf("I am child! Recv msg:%s from father\n", buf); ++count; } close(fd[0]); }else if(pid > 0){ close(fd[0]);//close read int count = 0; char buf[100]; while(count < 5) { memset(buf,0,sizeof(buf)); sprintf(buf,"count = %d", ++count); int res = write(fd[1],buf,strlen(buf) + 1);// 多一個字節存放\0 printf("I am father! Send msg:%s to child!\n",buf); sleep(2); } close(fd[1]); if(waitpid(pid, NULL, 0) < 0) { return -1; } }else{ perror("fork error!"); return -1; } return 0; }
運行結果:
使用管道須要注意如下4種特殊狀況(假設都是阻塞I/O操做,沒有設置O_NONBLOCK標誌):
總結:
FIFO常被稱爲命名管道,以區分管道(pipe)。管道(pipe)只能用於「有血緣關係」的進程間。但經過FIFO,不相關的進程也能交換數據。
命名管道不一樣於匿名管道之處在於它提供了一個路徑名與之關聯,以命名管道的文件形式存在於文件系統中,這樣,即便與命名管道的建立進程不存在親緣關係的進程,只要能夠訪問該路徑,就可以彼此經過命名管道相互通訊,所以,經過命名管道不相關的進程也能交換數據。值的注意的是,命名管道嚴格遵循先進先出(first in first out),對匿名管道及有名管道的讀老是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操做。命名管道的名字存在於文件系統中,內容存放在內存中。
命名管道阻塞問題:命名管道在打開時須要確實對方的存在,不然將阻塞。即以讀方式打開某管道,在此以前必須一個進程以寫方式打開管道,不然阻塞。此外,能夠以讀寫(O_RDWR)模式打開命名管道,即當前進程讀,當前進程寫,不會阻塞。
庫函數:
int mkfifo(const char *pathname, mode_t mode); //成功:0; 失敗:-1
一旦使用mkfifo建立了一個FIFO,就可使用open打開它,常見的文件I/O函數均可用於fifo。如:close、read、write、unlink等。
eg.
fifo_w.c // 向FIFO中寫入數據
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <fcntl.h> #define MAX_BUF_SIZE 655360 int main(int argc, char* argv[]) { if(argc < 2) { fprintf(stderr,"usage: %s argv[1].\n",argv[0]); return -1; } if(mkfifo(argv[1], 0666) < 0 && errno != EEXIST) { fprintf(stderr,"Fail to mkfifo %s : %s.",argv[1],strerror(errno)); return -1; } int fd; if((fd = open(argv[1],O_WRONLY)) < 0) { fprintf(stderr,"Fail to open mkfifo %s : %s.",argv[1],strerror(errno)); return -1; } printf("open for write success\n"); int n; char buf[MAX_BUF_SIZE]; while(1) { memset(buf, 0, sizeof(buf)); printf(">"); scanf("%s",buf); n = write(fd, buf, strlen(buf) + 1);// 將\0也寫入 printf("Write %d bytes.\nSend MSG:%s\n",n, buf); } return 0; }
fifo_r.c // 從FIFO中讀取數據
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h> #define MAX_BUF_SIZE 655360 int main(int argc, char *argv[]) { if(argc < 2) { fprintf(stderr,"Usage: %s argv[1]\n",argv[0]); return -1; } if(mkfifo(argv[1],0666) < 0 && errno != EEXIST) { fprintf(stderr,"Fail to mkfifo %s : %s",argv[1],strerror(errno)); return -1; } int fd; if((fd = open(argv[1],O_RDONLY)) < 0) { fprintf(stderr,"Fail to open mkfifo %s : %s",argv[1],strerror(errno)); return -1; } int n; char buf[MAX_BUF_SIZE]; while(1) { memset(buf, 0, sizeof(buf)); n = read(fd, buf, sizeof(buf)); printf("Read %d bytes\nRECV MSG:%s\n",n, buf); } return 0; }
運行結果:
寫入:
讀取:
Linux系統中經常使用信號:
- SIGHUP:用戶從終端註銷,全部已啓動進程都將收到該進程。系統缺省狀態下對該信號的處理是終止進程。
- SIGINT:程序終止信號。程序運行過程當中,按
Ctrl+C
鍵將產生該信號。- SIGQUIT:程序退出信號。程序運行過程當中,按
Ctrl+\
鍵將產生該信號。- SIGBUS和SIGSEGV:進程訪問非法地址。
- SIGFPE:運算中出現致命錯誤,如除零操做、數據溢出等。
- SIGKILL:用戶終止進程執行信號。shell下執行
kill -9
發送該信號。- SIGTERM:結束進程信號。shell下執行
kill 進程pid
發送該信號。- SIGALRM:定時器信號。
- SIGCLD:子進程退出信號。若是其父進程沒有忽略該信號也沒有處理該信號,則子進程退出後將造成殭屍進程。
可使用kill -l
查看當前系統可用信號有哪些
信號是軟件層次上對中斷機制的一種模擬,是一種異步通訊方式,,信號能夠在用戶空間進程和內核之間直接交互,內核能夠利用信號來通知用戶空間的進程發生了哪些系統事件,信號事件主要有兩個來源:
Ctrl+C
退出、硬件異常如無效的存儲訪問等。kill
函數、軟件異常產生信號。eg.
藉助SIGCHLD信號回收子進程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> void sys_err(char *str) { perror(str); exit(1); } void do_sig_child(int signo) { int status; pid_t pid; while ((pid = waitpid(0, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) printf("child %d exit %d\n", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("child %d cancel signal %d\n", pid, WTERMSIG(status)); } } int main(void) { pid_t pid; int i; for (i = 0; i < 10; i++) { if ((pid = fork()) == 0) break; else if (pid < 0) sys_err("fork"); } if (pid == 0) { int n = 1; while (n--) { printf("child ID %d\n", getpid()); sleep(1); } return i+1; } else if (pid > 0) { struct sigaction act; act.sa_handler = do_sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); while (1) { printf("Parent ID %d\n", getpid()); sleep(1); } } return 0; }
運行結果:
另外與管道不一樣的是,消息隊列在某個進程往一個隊列寫入消息以前,並不須要另外某個進程在該隊列上等待消息的到達
消息隊列特色總結:
- 消息隊列是消息的鏈表,具備特定的格式,存放在內存中並由消息隊列標識符標識.
- 消息隊列容許一個或多個進程向它寫入與讀取消息
- 管道和消息隊列的通訊數據都是先進先出的原則。
- 消息隊列能夠實現消息的隨機查詢,消息不必定要以先進先出的次序讀取,也能夠按消息的類型讀取.比FIFO更有優點。
- 消息隊列克服了信號承載信息量少,管道只能承載無格式字 節流以及緩衝區大小受限等缺。
- 目前主要有兩種類型的消息隊列:POSIX消息隊列以及System V消息隊列,System V消息隊列目前被大量使用。System V消息隊列是隨內核持續的,只有在內核重起或者人工刪除時,該消息隊列纔會被刪除。
對於系統中的每一個消息隊列,內核維護一個定義在<sys/msg.h>
頭文件中的信息結構。
struct msqid_ds { struct ipc_perm msg_perm ; struct msg* msg_first ; //指向隊列中的第一個消息 struct msg* msg_last ; //指向隊列中的最後一個消息 …… } ;
調用的第一個函數一般是msgget
,其功能是打開一個現存隊列或建立一個新隊列。
#include <sys/msg.h> int msgget (key_t key, int oflag) ;
返回值是一個整數標識符,其餘三個msg函數就用它來指代該隊列。它是基於指定的key產生的,而key既能夠是ftok的返回值,也能夠是常值IPC_PRIVATE。
oflag是讀寫權限的組合(用於打開時)。它還能夠是IPC_CREATE或IPC_CREATE | IPC_EXCL(用於建立時)。
使用msgsnd
打開一個消息隊列後,咱們使用msgsnd
往其上放置一個消息。
#include <sys/msg.h> int msgsnd (int msqid, const void *ptr, size_t length, int flag) ;
其中msqid是由msgget返回的標識符。ptr是一個結構指針,該結構具備以下模板(咱們須要按這個模板本身定義結構體)
struct mymesg { long mtype ; //消息類型(大於0) char mtext[512] ; //消息數據 } ; //結構體的名字和其中變量名都由咱們本身肯定,咱們只要按照這個模板定義便可。
消息數據mtext中,任何形式的數據都是容許的,不管是二進制數據仍是文本,內核根本不解釋消息數據的內容。(咱們能夠在消息的數據部分 再分割一部分 根據須要定義本身的通訊協議)
參數length指定了待發送消息數據部分的長度。
參數flag的值能夠指定爲IPC_NOWAIT。這相似於文件IO的非阻塞IO標誌。若消息隊列已滿,則指定IPC_NOWAIT使得msgsnd當即出錯返回EAGAIN。
若是沒有指定IPC_NOWAIT,則進程阻塞直到下述狀況出現爲止:①有空間能夠容納要發送的消息 ②從系統中刪除了此隊列(返回EIDRM「標識符被刪除」)③捕捉到一個信號,並從信號處理程序返回(返回EINTR)
使用msgrcv函數從某個消息隊列中讀出一個消息。
#include <sys/msg.h> ssize_t msgrcv (int msqid, void* ptr, size_t length, long type, int flag) ;
參數ptr指定所接收消息的存放位置。參數length指定了數據部分大小(只想要多長的數據)
參數type指定但願從隊列中讀出什麼樣的消息。
type == 0 返回隊列中的第一個消息
type > 0 返回隊列中消息類型爲type的第一個消息
type < 0 返回隊列中消息類型值小於或等於type絕對值的消息,若是這種消息有若干個。則取類型值最小的消息。
(若是一個消息隊列由多個客戶進程和一個服務器進程使用,那麼type字段能夠用來包含客戶進程的進程ID)
參數flag能夠指定爲IPC_NOWAIT,使操做不阻塞。
msgctl函數提供在一個消息隊列上的各類控制操做。
#include <sys/msg.h> int msgctl (int msqid, in cmd, struct msqid_ds * buff) ;
參數cmd說明對由msqid指定的隊列要執行的命令:
IPC_STAT :取此隊列的msqid_ds結構,並將它存放在buf指向的結構中。
IPC_SET :按由buf指向結構中的值,設置與此隊列相關結構中的字段。
IPC_RMID:從系統中刪除該消息隊列以及仍在該隊列中的全部數據。
(這三條命令也可用於信號量和共享存儲)
eg.
一個寫進程,多個讀進程
//-------------------頭文件msgqueue.h ------------------ #ifndef _MAGQUEUE_H_ #define _MAGQUEUE_H_ #include <sys/ipc.h> //包含ftok #include <sys/msg.h> #include <sys/types.h> //消息隊列的讀 寫模式掩碼 #define MSG_W 0200 #define MSG_R 0400 //定義衆所周知的消息隊列鍵 #define MQ_KEY1 128L #define DATA_SIZE 512 typedef struct msgbuf { long mtype ; char mdata[DATA_SIZE] ; } mymsg_t ; #endif //-----------------客戶端進程----------------- #include "msgqueue.h" #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void client(int, int) ; int main(int argc, char** argv) { int msgqid ; //打開消息隊列 msgqid = msgget(MQ_KEY1, 0) ; if (msgqid < 0) { puts("Open msg queue error!\n") ; exit(0) ; } client(msgqid, msgqid) ; exit(0) ; } void client(int readfd, int writefd) { mymsg_t msgToServer ; mymsg_t msgFromServer ; char* writePtr ; ssize_t pidLen ; ssize_t dataLen ; ssize_t recvBytes ; int pid ; //-------構造一條消息----- //消息類型爲1 msgToServer.mtype = 1 ; //在消息頭部放本進程ID和空格 pid = getpid() ; snprintf(msgToServer.mdata, DATA_SIZE, "%ld ", pid) ; pidLen = strlen(msgToServer.mdata) ; writePtr = msgToServer.mdata + pidLen ; //從標準輸入讀入文件路徑 fgets(writePtr, DATA_SIZE - pidLen, stdin) ; dataLen = strlen(msgToServer.mdata) ; if (msgToServer.mdata[dataLen-1] == '\n') //刪除換行符 { msgToServer.mdata[dataLen-1] = '\0' ; } //發送消息 if (msgsnd(writefd, &msgToServer, strlen(msgToServer.mdata), 0) == -1) { puts("Send Error!"); exit(0) ; } //-----接收來自服務器的消息 while ((recvBytes = msgrcv(readfd, &msgFromServer, DATA_SIZE, pid, 0)) > 0) { write(STDOUT_FILENO, msgFromServer.mdata, recvBytes) ; } } //---------------服務器端進程--------------- //消息隊列是雙向通訊的,故用單個隊列就夠用。 //咱們用每一個消息的類型來標識該消息是從客戶到服務器,仍是從服務器到客戶。 //客戶向隊列發類型爲一、PID和路徑名。 //服務器向隊列發類型爲客戶進程ID的文件內容。 // //當心死鎖隱患: //客戶們能夠填滿消息隊列,妨礙服務器發送應答,因而客戶被阻塞在發送中,服務器也被阻塞。 //避免的方法是:約定服務器對消息隊列老是使用非阻塞寫。 #include "msgqueue.h" #include <stdio.h> #include <stdlib.h> #include <string.h> void server(int, int) ; int main(int argc, char** argv) { int msgqid; //建立消息隊列 msgqid = msgget(MQ_KEY1, IPC_CREAT) ; if (msgqid < 0) { puts("Create msg queue error!\n") ; exit(0) ; } server(msgqid, msgqid) ; exit(0) ; } void server(int readfd, int writefd) { FILE* fp ; pid_t clientPid ; mymsg_t* msgPtr ; ssize_t recvBytes ; char* pathStr ; while(1) { //從消息隊列中讀取來自客戶的請求文件路徑 msgPtr = malloc(DATA_SIZE + sizeof(long)) ; recvBytes = msgrcv(readfd, msgPtr, DATA_SIZE, 1, 0) ; //阻塞讀 if (recvBytes <= 0) { puts("pathname missing") ; continue ; } msgPtr->mdata[recvBytes] = '\0' ; //分析消息,提取客戶PID,文件路徑 if ((pathStr = strchr(msgPtr->mdata, ' ')) == NULL) { puts("bogus request!") ; continue ; } *pathStr++ = 0 ; clientPid = atol(msgPtr->mdata) ; //讀取文件內容 返回給客戶 msgPtr->mtype = clientPid ; //msgPtr既做爲接收消息 又用做發送消息 if ((fp = fopen(pathStr, "r")) == NULL) { //讀取文件失敗,返回給客戶失敗信息(在原消息內容後 添加錯誤信息) snprintf(msgPtr->mdata + recvBytes, sizeof(msgPtr->mdata) -recvBytes, ": can't open!") ; if (msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) == -1) { puts("Send Error!"); exit(0); } } else { //copy文件內容 發給客戶 while (fgets(msgPtr->mdata, DATA_SIZE, fp) != NULL) { msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) ; //非阻塞寫 } } }//while() }