Linux進程間通訊(上)之管道、消息隊列實踐

一、進程間通訊簡述

進程間通訊的幾種方式:無名管道、有名管道、消息隊列、共享內存、信號、信號量、套接字(socket)。
linux

進程間通訊是不一樣進程直接進行的一些接觸,這種接觸有簡單,有複雜。機制不一樣,複雜度也不一樣。通訊是一個廣義上的意 義,不只指大批量數據傳送,還包括控制信息的傳送,可是使用的方法都是大同小異的。ios

如圖所示進程不是孤立的,不一樣的進程須要進行信息的交互和狀態的傳遞等,所以須要進程間通訊。web

二、管道

管道分爲無名管道和有名管道兩種方式。管道是一種半雙工的通訊方式,數據只能單向流動,可是無名管道和有名管道的區別是無名管道只能在具備親緣關係的進程間通訊,有名管道則是在無親緣關係進程間通訊。進程的親緣關係一般是指父子進程關係。管道是Linux支持的最初Unix IPC形式之一,管道與管道之間通訊其實就是一個文件,但它不是一個普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統並且只存在內存中。當一個進程向管道中寫的內容被管道另外一端的進程讀出;寫入的內容每次都會被添加到管道緩衝區的末尾,而且每次都是從緩衝區的頭部讀出數據。以下圖所示。編程

那麼,如何建立一條管道呢?下面,咱們就來了解下FIFO函數。緩存

FIFO不一樣於pipe函數,由於它提供了一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中,這樣,即便與FIFO的建立進程不存在親緣關係的進程,只要能夠訪問該路徑就可以彼此經過FIFO互相通訊,所以,經過FIFO不相關的進程也能交換數據。值得注意的是,FIFO嚴格遵循先進後出,和棧的原則同樣,對管道以及FIFO的讀老是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操做。微信

須要包含的頭文件以下:數據結構

#include<sys/types.h>
#include<sys/stat.h>
#incldue<fcntl.h>
#include<unistd.h>

FIFO函數建立:app

函數原型:socket

int mkfifo(const char *pathname,mode_t mode);

函數返回值 :編輯器

成功0,失敗-1

參數含義:

pathname爲路徑名,建立管道的名字(該函數的第一個參數是一個普通的路徑名,也就是建立後FIFO的名字)。mode爲建立fifo的權限(第二個參數與打開普通文件的open()函數中的mode參數相同)。

注:若是mkfido的第一個參數已是一個已經存在的路徑名時,就會返回EEXIST錯誤,因此當咱們調用的時候首先會檢查是否返回該錯誤,若是返回該錯誤那咱們只須要直接調用打開FIFO的函數便可。

FIFO比pipe函數打開的時候多了一個打開操做open;若是當時打開操做時爲讀而打開FIFO時,若已經有相應進程爲寫而打開該FIFO,則當前打開操做將返回成功;不然,可能阻塞到有相應進程爲寫而打開該FIFO;或者,成功返回。另外一種狀況就是爲寫而打開FIFO時,若已經有相應進程爲讀而打開該FIFO,則當前打開操做將成功返回;不然可能會阻塞直到有相應進程爲讀而打開該FIFO;或者,返回ENIO錯誤。

下面咱們使用FIFO實現進程間的通訊。

(1)打開一個文件,管道的寫入端向文件寫入數據;管道的讀取端從文件中讀取出數據。

fifo_write.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int fd;
    //要寫入有名管道的數據
    char buf[20] = "hello write_fifo";
    int ret=0;
    //建立有名管道,並賦予訪問有名管道的權限
    ret = mkfifo(P_FIFO,0777);
    //建立失敗
    if(ret < 0)
    {
        printf("create named pipe failed.\n");
        return -1;
    }
    fd = open(P_FIFO,O_WRONLY);
    if(fd < 0)
    {
        printf("open failed.\n");
        return -2;
    }
    //寫入數據到有名管道
    //第一個參數爲有名管道文件描述符
    //第二個參數爲寫入有名管道的數據
    //第三個參數爲寫入有名管道的數據長度
    write(fd,buf,sizeof(buf));
    //關閉有名管道
    close(fd);
    return 0;
}

fifo_read.c

#include <stdio.h>
#include <string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define P_FIFO  "txt"

int main()
{
    int ret;
    int fd;
    char buf[20];
    //打開有名管道
    //第一個參數爲有名管道文件路徑
    //第二個參數代表是以讀取方式並以非阻塞方式打開有名管道
    //O_RDONLY讀取模式
    //O_NONBLOCK非阻塞方式
    fd = open(P_FIFO,O_RDONLY);
    if(fd<0)
    {
        printf("open fail\n");
        return -1 ;
    }
    //循環讀取有名管道
    while(1)
    {
        memset(buf,0,sizeof(buf));
        if(read(fd,buf,sizeof(buf)) == 0)
        {
            printf("nodata.\n");
        }
        else
        {
            printf("getdata:%s\n",buf);
            break;
        }
   }
   close(fd);
   return 0;
}

下面先將fifo_write.c和fifo_read.c分別編譯成fifo_write和fifo_read兩個可執行程序:

接下來,先運行fifo_write,而後打開另外一個終端,接着運行fifo_read,運行fifo_write的時候,能夠看到程序阻塞在終端:

下面打開另一個終端運行fifo_read

切換到另一個終端,在終端輸入ls –l能夠看到因爲fifo_write中建立了管道文件txt,從前面的字串prwxr-xr-x中的p能夠知道,這是一個管道文件,以下圖所示:

運行fifo_read,這時候,能夠看到從管道中獲取的字符串hello write_fifo,以下圖所示:

管道讀取結束後,fifo_write這個程序也就不會在阻塞在終端了,以下圖所示:

寫管道程序還要注意,一旦咱們建立了FIFO,就能夠用open去打開它,可使用open、read、close等去操做FIFO和pipe有相同之處,當打開FIFO時,非阻塞標誌(O_NONBLOCK)將會對讀寫產生以下影響:

  • 一、沒有使用O_NONBLOCK:訪問要求沒法知足時進程將阻塞。如試圖讀取空的FIFO,將致使進程阻塞;
  • 二、使用O_NONBLOCK:訪問要求沒法知足時不阻塞,當即出錯返回,errno是ENXIO。

三、消息隊列

消息隊列(也叫作報文隊列)提供了一個進程向另外一個進程發送一個數據塊的方法。每一個數據塊都被認爲含有一個類型,接收進程能夠獨立地接收含有不一樣類型的數據結構。咱們能夠經過發送消息來避免命名管道的同步和阻塞問題。可是消息隊列與命名管道同樣,每一個數據塊都有一個最大長度的限制。

打開或者建立消息隊列的內核持續性要求每一個消息隊列都在系統範圍內對應惟一的鍵值,因此,要得到一個消息隊列的描述字,只須要提供該消息隊列的鍵值便可。

消息讀寫操做很是簡單,對於開發人員來講,每一個消息都相似以下的數據結構:

struct msgbuf
{
 long mtype;
 char mtext[1];
};

3.一、msgget函數

該函數用來建立或者訪問一個消息隊列。

int msgget(key_t key, int msgflg);

與其餘的IPC機制同樣,程序必須提供一個鍵來命名某個特定的消息隊列。msgflg是一個權限標誌,表示消息隊列的訪問權限,它與文件的訪問權限同樣。msgflg能夠與IPC_CREAT作或操做,表示當key所命名的消息隊列不存在時建立一個消息隊列,若是key所命名的消息隊列存在時,IPC_CREAT標誌會被忽略,成功則返回一個以key命名的消息隊列的標識符(非零整數),失敗時返回-1。

3.二、msgsnd函數

該函數用來向消息隊列發送一個消息。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

將發送的消息存儲在msgp指向的msgbuf結構中,消息大小由msgsz指定。對發送的消息來講,有意義的msgflg標準爲IPC_NOWAIT,指明在消息隊列沒有足夠的空間容納要發送的消息時,msgsnd是否等待。形成msgsnd()等待的條件有兩種:當前消息的大小與當前消息隊列中的字節數之和超過了消息隊列的總容量;當前消息隊列的消息數不小於消息隊列的總容量,此時,雖然消息隊列中的消息數目並很少,但基本上都只有一個字節。調用成功的時候返回0,失敗返回-1.

3.三、msgrcv函數

該函數用來從一個消息隊列獲取消息。

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgrcv函數前面三個參數和msgsnd函數的三個參數同樣不作講解。msgtype能夠實現一種簡單的接收優先級。若是msgtype爲0,就獲取隊列中的第一個消息。若是它的值大於零,將獲取具備相同消息類型的第一個信息。若是它小於零,就獲取類型等於或小於msgtype的絕對值的第一個消息。msgflg用於控制當隊列中沒有相應類型的消息能夠接收時將發生的事情。當調用成功時,該函數返回放到接收緩存區中的字節數,消息被複制到由msg_ptr指向的用戶分配的緩存區中,而後刪除消息隊列中對應的消息;失敗則返回-1.

3.四、msgctl函數

該函數用來控制消息隊列。

int msgctl(int msgid, int command, struct msgid_ds *buf);

該系統調用對由msqid標識的消息隊列執行cmd操做,共有三種cmd操做:

  • IPC_STAT:把msgid_ds結構中的數據設置爲消息隊列的當前關聯值,即用消息隊列的當前關聯值覆蓋msgid_ds的值。
  • IPC_SET:若是進程有足夠的權限,就把消息列隊的當前關聯值設置爲msgid_ds結構中給出的值。
  • IPC_RMID:刪除消息隊列。buf是指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構。成功返回0,不然返回-1。

經過上面的函數咱們清楚如何去建立一個消息隊列那咱們簡單的來看一個案例。

(1)建立一條消息隊列msg_get.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
 int  msgid ; 
 //建立消息隊列,注意,建立後面要有IPC_CREAT標誌
 msgid = msgget(0x123456 , IPC_CREAT | 0777);
 if(msgid < 0)
 {
  perror("msgget fail");
  return -1 ; 
 }
 printf("success ... ! \n");
 return 0  ;
}

運行結果:

那消息隊列呢?怎麼查看?使用ipcs –q命令能夠查看到剛剛咱們建立的消息隊列0x123456。

(2)向消息隊列發送消息 msgsend.c

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char *p = "hello world" ;
   //發送hello world到消息隊列0x123456
   //在這裏能夠直接發送
    ret = msgsnd(msgid , p , strlen(p) , 0);
    if(ret == -1)
    {
        perror("send msgid fail");
        return -2 ;
    }
    return 0 ;
}

運行結果:

使用ipcs –p命令查看:

(3)獲取消息隊列中的信息 msgrecv.c 在上面msgsend.c的基礎上,這個例程將上面發送到消息隊列的信息讀取回來。

#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid == -1)
    {
        perror("create msg queue fail");
        return -1 ;
    }
    printf("open msg success ... \n");
    int ret ;
    char buffer[1024] = {0};
    //接收消息隊列中的信息
    ret = msgrcv(msgid , buffer , 11 , 0 , 0);
    if(ret == -1)
    {
        perror("recv msgid fail");
        return -2 ;
    }
    printf("ret: %d  buffer:%s \n" , ret , buffer);
    return 0 ;
}

運行結果,如圖所示:

那麼,如何刪除一個消息隊列呢?先用ipcs –q查看消息隊列,如圖所示:

有兩種方法:

  • 一、使用命令ipcrm –q msqid 刪除消息隊列,如圖所示

  • 二、使用msgctl函數,寫IPC_RMID標誌刪除消息隊列

(4)刪除消息隊列 msgrm.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(void)
{
    int  msgid ;
    msgid = msgget(0x123456 , 0);
    if(msgid < 0)
    {
        perror("msgget fail");
        return -1 ;
    }
    printf("success ... ! msgid:%d \n" , msgid);
    //寫IPC_RMID標誌
    if(msgctl(msgid , IPC_RMID , NULL) == 0)
    {
        printf("remove success ... \n");
    }
    return 0  ;
}

運行結果,如圖所示:

使用系統提供的API的方式,能夠將消息隊列刪除。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點,相對於管道通訊有很大的改觀,並且消息隊列對數據的順序處理也是很是有條理性的不會產生混雜性。

往期精彩

Linux 進程必知必會

【Linux系統編程】IO標準緩衝區

【Linux系統編程】可重入和不可重入函數

韋東山:6000字長文告訴你如何學習linux

會C/C++就能夠開發Linux/Android應用程序?Yoxios瞭解一下!

以爲本次分享的文章對您有幫助,隨手點[在看]並轉發分享,也是對個人支持。

本文分享自微信公衆號 - 嵌入式雲IOT技術圈(gh_d6ff851b4069)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索