进程间通信IPC(一)(半双工管道和消息队列)
引言:
之前學習的進程之間交換信息的方法只能由fork或exec傳送打開文件,或者文件系統。但是這種通訊方式有局限性,接下來將說明進程之間相互通信的其他技術——IPC(InterProcessCommunication),過去UNIX系統IPC是各種進程間通信方式的統稱,但是,其中極少能在所有UNIX系統中實現移植。隨著POSIX和Open Group(以前是X/Open)標準化的推進和影響擴大,情況隨已得到改善,但是差別仍然存在。以下將介紹幾種實現所支持的不同形式的IPC。
單機版進程間通信方式:
-
半雙工管道(包括無名管道和命名管道)
-
消息隊列
-
信號量
-
共享內存
多機版進程間通訊方式:
- 套接字(socket)
- streams
管道(父進程創建):
概念:
管道,通常指無名管道,是一種最基本的IPC機制,作用于有血緣關系的進程之間,完成數據傳遞。我們把從一個進程連接到另一個進程的一個數據流稱為一個“管道”,本質:內核緩沖區。
特點:
- 它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端,當數據從管道中讀取后管道中就沒有了。
- 它只能用于具有親緣關系的進程之間的通信(也是父子進程或者兄弟進程之間)。
- 它可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,并不屬于其他任何文件系統,并且只存在于內存中。本質:內核緩沖區.
如何建立半雙工管道:
管道是由調用pipe函數來創建
#include <unistd.h> int pipe(int pipefd[2]);函數的參數是:含有兩個元素的整型數組 返回值:返回:成功返回0,出錯返回-1 fd參數返回兩個文件描述符,fd[0]指向管道的讀端read(fd[0],-,-) 其中read()函數如果讀不到東西,會阻塞。 fd[1]指向管道的寫端,write(fd[1],-,-)。向管道文件讀寫數據其實 是在讀寫內核緩沖區。要關閉管道只需要將讀端和寫端close掉即可。fd[1]的寫出是fd[0]的讀入。0對應標準輸入,1對應標準輸出一樣。 默認標準輸入對應的設備是鍵盤,標準輸出和標準錯誤對應的是顯示器 linux下一起皆文件,設備一定是文件,文件不一定是設備,標準輸入 和標準輸出就不是文件,他們是鏈接文件,什么是鏈接文件?文件內容 是另一個文件的地址的文件稱為鏈接文件。標準輸入、標準輸出解釋
父進程給子進程信息:
#include <unistd.h> #include<stdio.h> #include <sys/types.h> #include <sys/wait.h> #include<string.h> #include<stdlib.h> int main() {int fd[2];int status;pid_t fpid;char* writebuf=NULL;writebuf=(char*)malloc(1024);printf("請輸入父進程要傳遞給子進程的消息:\n");scanf("%[^\n]",writebuf);if(pipe(fd)==-1){printf("creat file fail\n");}fpid=fork();if(fpid<0){printf("創建子進程失敗\n");perror("fork");}else if(fpid>0){close(fd[0]);write(fd[1],writebuf,strlen(writebuf));waitpid(fpid,&status,0);if(WIFEXITED(status)){printf("子進程正常結束,狀態值是:%d\n",WEXITSTATUS(status));}close(fd[1]);}else{close(fd[1]);char* readbuf=NULL;int n_read;readbuf=(char*)malloc(strlen(writebuf));n_read=read(fd[0],readbuf,strlen(writebuf));printf("子進程得到數據:%s,字節數是:%d\n",readbuf,n_read);close(fd[0]);}return 0; }這個程序:起初當輸入字符串中間有空格時,子進程只會收到空格之前的字符串 問題診斷:scanf()遇到空格、回車、Tab則認為輸入結束,后面的就不會當做輸入了 解決方法:%s換為%[^\n](^有非的意思,即不遇到\n不停止)或者用gets()scanf輸入字符串的時候不會接收Space空格,回車Enter,Tab鍵,則認為輸入結束。 scanf函數直接從輸入緩沖區中取數據,而并非從鍵盤(也就是終端)緩沖區讀取。1、空格:空格鍵產生的字符,ascii碼十進制:32 2、空字符:字符串結束標志‘\0’,為被動添加,ascii碼十進制:0 3、字符0:ascii碼十進制:48子進程發送消息給父進程:
#include <unistd.h> #include<stdio.h> #include <sys/types.h> #include <sys/wait.h> #include<string.h> #include<stdlib.h> int main() {int fd[2];int status;pid_t fpid;char* writebuf=NULL;writebuf=(char*)malloc(1024);printf("請輸入父進程要傳遞給子進程的消息:\n");scanf("%[^\n]",writebuf);if(pipe(fd)==-1){printf("creat file fail\n");}fpid=fork();if(fpid<0){printf("創建子進程失敗\n");perror("fork");}else if(fpid>0){sleep(1);close(fd[1]);char* readbuf=NULL;int n_read;readbuf=(char*)malloc(strlen(writebuf));n_read=read(fd[0],readbuf,strlen(writebuf));waitpid(fpid,&status,0);printf("父進程得到數據:%s,字節數是:%d\n",readbuf,n_read);if(WIFEXITED(status)){printf("子進程正常結束,狀態值是:%d\n",WEXITSTATUS(status));}}else{close(fd[0]);write(fd[1],writebuf,strlen(writebuf));}return 0; }memcpy()函數介紹、scanf輸入字符串遇到空格?
FIEO(有名管道,半雙工):
無名管道,由于沒有名字,只能用于親緣關系的進程間通信。為了克服這個缺點,提出了命名管道(FIFO),也叫有名管道、FIFO 文件。命名管道(FIFO)不同于無名管道之處在于它提供了一個路徑名與之關聯,以 FIFO 的文件形式存在于文件系統中,這樣,即使與 FIFO 的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過 FIFO 相互通信,因此,通過 FIFO 不相關的進程也能交換數據。
命名管道(FIFO)和無名管道(pipe)有一些特點是相同的,不一樣的地方在于:
- FIFO 在文件系統中作為一個特殊的文件而存在,但 FIFO 中的內容卻存放在內存中,FIFO文件在磁盤上沒有數據塊,僅僅用來標識內核中一條通道。各進程可以打開這個文件進行read/write,實際上是在讀寫內核通道,這樣就實現了進程間通信。
- 當使用 FIFO 的進程退出后,FIFO 文件將繼續保存在文件系統中以便以后使用。
- FIFO 有名字,不相關的進程可以通過打開命名管道進行通信。
- 另外,使用統一fifo文件,可以有多個讀端和多個寫端。
創建有名管道:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);pathname: 普通的路徑名(就是創建的管道的位置),也就是創建后 FIFO 的名字。 mode: 文件的權限,與打開普通文件的 open() 函數中的 mode 參數相同返回值:成功:0失敗:如果文件已經存在,則會出錯且返回 -1。如果因為文件存在引起的錯誤,則會返回-1并將errno的值賦EEXIST可以用下面的代碼找出其他的出錯原因 if(mkfifo("./fifodir",0666)==-1&&errno!=EEXIST){printf("管道創建失敗\n");perror("because"); }注意:
當進程對命名管道的使用結束后,命名管道依然存在于文件系統中,除非對其進行刪除操作。命名管道的數據讀取后也會消失(不能反復讀取),即且嚴格遵循先進先出的規則。因此,每次命名管道文件使用完后,其大小為0字節,不會產生中間臨時文件。
命名管道的默認操作:
- 后期的操作,把這個命名管道當做普通文件一樣進行操作:open()、write()、read()、close()。但是,和無名管道一樣,操作命名管道肯定要考慮默認情況下其阻塞特性。
- 下面驗證的是默認情況下的特點,即 open() 的時候沒有指定非阻塞標志( O_NONBLOCK )。open() 以只讀方式打開FIFO 時,要阻塞到某個進程為寫而打開此 FIFO。open() 以只寫方式打開 FIFO 時,要阻塞到某個進程為讀而打開此 FIFO。
- 簡單一句話,只讀等著只寫,只寫等著只讀,只有兩個都執行到,才會往下執行。
- 在這里我們需要注意一點,就是不能以 O_RDWR 方式打開管道文件,這種行為是未定義的。倘若有一個進程以讀寫方式打開了某個管道,那么該進程寫入的數據又會被該進程本身讀取,而管道一般只用于進程間的單向數據通信。
管道打開方式:
- (1)只讀且阻塞方式
open(const char *pathname, O_RDONLY); - (2)只讀且非阻塞方式
open(const char *pathname, O_RDONLY | O_NONBLOCK); - (3)只寫且阻塞方式
open(const char *pathname, O_WRONLY); - (4)只寫且非阻塞方式
open(const char *pathname, O_WRONLY | O_NONBLOCK);
讀端代碼:
#include<stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include<errno.h> #include<string.h> int main() {char readbuf[30];int n_read;int fd;memset(readbuf,'\0',30);if(mkfifo("./fifodir",0600)==-1&&errno!=EEXIST){printf("mkfifo fail\n");perror("mkfifo");}fd=open("./fifodir",O_RDONLY);while(1){n_read=read(fd,readbuf,30);printf("讀取到%d個字節,內容是%s\n",(int)strlen(readbuf),readbuf);}close(fd);return 0; }寫端代碼:
include<stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include<errno.h>int main() {int fd;fd=open("./fifodir",O_WRONLY);while(1){write(fd,"hello reader",20);sleep(1);}close(fd);return 0; }消息隊列、消息隊列參數詳解
什么是消息隊列?
- 消息隊列是消息的鏈表,存放在內核中并由消息隊列標識符表示。
- 消息隊列提供了一個從一個進程向另一個進程發送數據塊的方法,每個數據塊都可以被認為是有一個類型,接受者接受的數據塊可以有不同的類型。
- 但是同管道類似,它有一個不足就是每個消息的最大長度是有上限的(MSGMAX),每個消息隊列的總的字節數(MSGMNB),系統上消息隊列的總數上限(MSGMNI)??梢杂胏at /proc/sys/kernel/msgmax查看具體的數據。
特點:
- 消息隊列是面向記錄的,其中的消息具有特定的格式以及優先級
- 消息隊列獨立于發送與接收進程,進程終止時,消息隊列及其內容并不會被刪除(生命周期隨內核,消息隊列會一直存在,需要我們顯示的調用接口刪除或使用命令刪除)。
- 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
- 克服了管道只能承載無格式字節流的缺點
- 消息隊列可以雙向通信
對于消息隊列,要知道如何創建一個消息隊列,如何將消息添加到消息隊列,如何從消息隊列讀取信息
ftok()函數:
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id);fname就是你指定的文件名(已經存在的文件名),一般使用當前目錄,如: key_t key; key = ftok(".", 1); 這樣就是將fname設為當前目錄。 id是子序號。雖然是int類型,但是只使用8bits(1-255)。 在一般的UNIX實現中,是將文件的索引節點號取出,前面加上子序號得到key_t的返回值。 如指定文件的索引節點號為65538,換算成16進制為0x010002,而你指定的ID值為38,換算成16進制為0x26,則最后的key_t返回值為0x26010002。 當刪除重建文件后,索引節點號由操作系統根據當時文件系統的使用情況分配,因此與原來不同,所以得到的索引節點號也不同。調用成功返回一個key值,用于創建消息隊列,如果失敗,返回-1鍵值和消息隊列標識符的關系: 在創建一個消息隊列(其他ipc相同)時,需要先通過文件路徑名和項目ID獲取一個鍵值, 然后通過此 鍵值由內核生成標識符,在以后可通過此標識符來使用此消息隊列。查詢文件索引節點號的方法是:
ls -i ls -al//顯示所有文件的所以值,包括隱藏文件 執行結果: fhn@ubuntu:~/jincheng/communication$ ls -i 932272 fifo 932283 msgread 932262 msgsend.c 932274 read 932250 write.c 932256 fifo.c 932263 msgread.c 932265 nomamepipe 932279 read.c 932267 znomamepipe 932229 fifodir 932280 msgsend 932236 nomamepipe.c 932275 write 932248 znomamepipe.cmsgget函數:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflag);功能:創建和訪問一個消息隊列 key:某個消息隊列的名字,用ftok()產生 msgflag:有兩個選項IPC_CREAT和IPC_EXCL,單獨使用IPC_CREAT,如果消息隊列不存在則創建之, 如果存在則打開返回;單獨使用IPC_EXCL是沒有意義的;兩個同時使用,如果消息隊列不存在則創建之, 如果存在則出錯返回。如果將key值設為IPC_PRIVATE則創建私有的消息隊列,只能有一個進程訪問。 msgflag由九個權限標志構成,如0644,它們的用法和創建文件時使用的mode模式標志是一樣的(但是消息隊列沒有x(執行)權限)返回值:成功返回一個非負整數,即消息隊列的標識碼,失敗返回-1為什么要有鍵值和標識符兩個值呢?
標識符是對于用戶操作而言的,讓用戶感覺操作和對文件的操作相同,鍵是對于系統內部說的。
我們使用ftok來創建鍵值,具體你可以man一下fotk函數,大概是這樣的:按給定的路徑名取得其stat結構,從該結構中取出部分st_dev和st_ino字段,然后再與項目id組合起來,如果兩個路徑名引用兩個不同的文件,那么,對這兩個路徑名調用ftok通常返回不同的鍵,但是,因為i節點號和鍵通常都存放在長整型中,于是創建鍵時可能會丟失信息,這意味著,如果使用同一項目id,那么對于不同文件的兩個路徑名可能產生相同的鍵。而標識符是唯一確定的,可以用來區別于其他ipc的。
msgsnd()函數:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);功能:把一條消息添加到消息隊列中 參數: msgid:由msgget函數返回的消息隊列標識碼 msgp:指針指向準備發送的消息 msgze:msgp指向的消息的長度(不包括消息類型的long int長整型) msgflg:默認為0 返回值:成功返回0,失敗返回-1消息結構一方面必須小于系統規定的上限,另一方面必須以一個long int長整型開始,接受者以此來確定消息的類型struct msgbuf {long mtype; /* message type, must be > 0 */char mtext[1]; /* message data */};消息隊列內一個節點類型如下: struct msq_Node { Type msq_type; //類型 Length msg_len; //長度 Data msg_data; //數據 struct msg_Node *next; };msgrcv()函數:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); msgid:由msgget函數返回的消息隊列標識碼 msgp:指針指向準備接收的消息 msgze:msgp指向的要存消息內存的長度(不包括消息類型的long int長整型) msgtyp:msgtyp==0返回隊列中第一個消息msgtyp>0返回隊列中消息類型為msgtyp的第一個消息msgtyp<0返回隊列中消息類型小于或等于msgtyp絕對值的消息,如果有多個,則取類型值最小的消息。 可以看出msgtyp值非0時用以非先進先出次序讀消息,也可以把msgtyp看做優先級的權值。 msgflg:默認為0,阻塞式接收消息,沒有該類型的消息msgrcv函數一直阻塞等待 功能:是從一個消息隊列接受消息 返回值:成功返回實際放到接收緩沖區里去的字符個數,失敗返回-1msgctl()函數:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);功能:消息隊列的控制函數 第一個參數msgqid 是消息隊列對象的標識符。 第二個參數是函數要對消息隊列進行的操作,它可以是: IPC_STAT:取出系統保存的消息隊列的msqid_ds 數據,并將其存入參數buf 指向的msqid_ds 結構中。 IPC_SET:設定消息隊列的msqid_ds 數據中的msg_perm 成員。設定的值由buf 指向的msqid_ds結構給出。 IPC_RMID:將隊列從系統內核中刪除,此時第三個參數設為NULL。 這三個命令的功能都是明顯的,所以就不多解釋了。唯一需要強調的是在IPC_STAT命令中隊列的msqid_ds 數據中唯一能被設定的只msg_perm 成員,其是ipc_perm 類型的數據。而ipc_perm 中能被修改的只有mode,pid 和uid 成員。其他的都是只能由系統來設定的。成功返回0,失敗返回-1消息隊列需要手動刪除IPC資源
linux下消息隊列的查看與刪除(ipcs&ipcrm的使用)
消息隊列接收端:
#include<stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include<string.h> typedef struct msgbuf {long mtye;char mtext[128]; }MSG,*PMSG; int main() {key_t key;int msgid;MSG readbuf;MSG sendbuf;sendbuf.mtye=666;strcpy(sendbuf.mtext,"hello sender,i have receive your msg");key=ftok(".",30);msgid=msgget(key,IPC_CREAT|0600);//flage使用IPC_CREAT表示若消息隊列不存在則創建存在則打開后返回//0600是在沒有消息隊列時創建消息隊列的權限,和文件那里那個權限一樣if(msgid==-1){printf("make fail\n");perror("why");}memset(readbuf.mtext,'\0',128);msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),777,0);printf("msgrcv得到消息:%s\n",readbuf.mtext);msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);return 0; }消息隊列發送端:
#include<stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include<string.h> typedef struct msgbuf {long mtye;char mtext[128]; }MSG,*PMSG; int main() {key_t key;int msgid;MSG sendbuf;MSG readbuf;sendbuf.mtye=777;strcpy(sendbuf.mtext,"hello i am sender");key=ftok(".",30);msgid=msgget(key,IPC_CREAT|0600);//flage使用IPC_CREAT表示若消息隊列不存在則創建存在則打開后返回//0600是在沒有消息隊列時創建消息隊列的權限,和文件那里那個權限一樣if(msgid==-1){printf("make fail\n");perror("why");}msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);printf("msgsnd發送消息完成:%s\n",sendbuf.mtext);msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),666,0);printf("msgsnd讀到消息:%s\n",readbuf.mtext);msgctl(msgid,IPC_RMID,NULL);//等同于在命令行刪除return 0; }總結
以上是生活随笔為你收集整理的进程间通信IPC(一)(半双工管道和消息队列)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle数据库实例,数据库的理解
- 下一篇: vim 文本编辑器