9、進程間通信(IPC)之消息隊列、共享內存、信號量

9、進程間通信(IPC)

1、進程間通信概述

進程間通信有如下一些目的:

  • 數據傳輸:一個進程需要將它的數據發送給另一個進程,發送的數據量在一個字節到幾兆字節之間。

  • 共享數據:多個進程想要操作共享數據,一個進程對共享數據的修改,別的進程應該立刻看到。

  • 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。

  • 資源共享:多個進程之間共享同樣的資源。爲了作到這一點,需要內核提供鎖和同步機制。

  • 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,並能夠及時知道它的狀態改變。

2、現在linux使用的進程間通信方式

  • 管道(pipe)和命名管道(FIFO)
  • 信號(signal)
  • 消息隊列
  • 共享內存
  • 信號量
  • 套接字(socket)

3、消息隊列

  • 消息隊列提供了一個從一個進程向另外一個進程發送一塊數據的方法
  • 每個數據塊都被認爲是有一個類型,接收者進程接收的數據塊可以有不同的類型值
  • 消息隊列也有管道一樣的不足,就是每個數據塊的最大長度是有上限的,系統上全體隊列的最大總長度也有一個上限

與管道的區別

  • 避免命名管道的同步和阻塞問題
  • 接收程序可以通過消息類型有選擇地接收數據,而不是像命名管道中那樣,只能順序地接收。

3-1、msgget函數

  • 作用:用來創建和訪問一個消息隊列,返回消息隊列的標識碼
int msgget(key_t key, int msgflg);
  • 參數:
    • key: 某個消息隊列的名字
    • msgflg:由九個權限標誌構成,它們的用法和創建文件時使用的mode模式標誌是一樣的
  • 返回值:
    • 成功將返回一個非負整數,即該消息隊列的標識碼;
    • 失敗,則返回-1

3-2、msgsnd函數

  • 作用:把一條消息添加到消息隊列裏去
int msgsnd(int msgid,const void *msgp,size_t msgsz,int msgflg);
  • 參數:

    • msgid:由msgget函數返回的消息隊列標識碼
    • msgp:是一個指針,指針指向準備發送的消息
    • msgsz:是msgp指向的消息長度,這個長度不能保存消息類型的那個“long int”長整型計算在內
    • msgflg:控制着當前消息隊列滿或到達系統上限時將要發生的事情
  • 返回值:

    • 成功:返回0
    • 失敗 :返回-1
  • msgflg=IPC_NOWAIT表示隊列滿不等待,返回EAGAIN錯誤。

  • 消息結構在兩方面受到制約。首先,它必須小於系統規定的上限值;其次,它必須以一個“long int”長整數開始,接收者函數將利用這個長整數確定消息的類型

  • 最好把消息結構定義爲下面這個樣子:

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

3-3、msgrcv函數

  • 作用:是從一個消息隊列裏檢索消息
int msgrcv(int msgid,void *msgp,size_t msgsz,long msgtype,int msgflg);
  • 參數:

    • msgid:由msgget函數返回的消息隊列標識碼

    • msgp:是一個指針,指針指向準備接收的消息

    • msgsz:是msgp指向的消息長度,這個長度不能保存消息類型的那個“long int”長整型計算在內

    • msgtype:它可以實現接收優先級的簡單形式

    • msgflg:控制着隊列中沒有相應類型的消息可供接收時將要發生的事

  • 返回值:

    • 成功:返回實際放到接收緩衝區裏去的字符個數
    • 失敗:返回 -1
參數 描述
msgtype=0 返回隊列第一條信息
msgtype>0 返回隊列第一條類型等於msgtype的消息
msgtype<0 返回隊列第一條類型小於等於msgtype絕對值的消息
msgflg=IPC_NOWAIT 隊列沒有可讀消息不等待,返回ENOMSG錯誤。
msgflg=MSG_NOERROR 消息大小超過msgsz時被截斷
msgtype>0且msgflg=MSC_EXCEPT 接收類型不等於msgtype的第一條消息。

3-4、msgctl函數

  • 作用:消息隊列的控制函數,與共享內存的控制函數很相似
int msgctl(int msqid,int cmd,strcut msqid_ds *buf);
  • 參數:
    • msqid:由msgget函數返回的消息隊列標識碼
    • cmd:是將要採取的動作,(有三個可取值)
命令 說明
IPC_STAT 把msqid_ds結構中的數據設置爲消息隊列的當前關聯值
IPC_SET 在進程有足夠權限的前提下,把消息隊列的當前關聯值設置爲msqid_ds數據結構中給出的值
IPC_RMID 刪除消息隊列
  • 返回值:
    • 成功:返回0
    • 失敗:返回-1

3-5、消息隊列代碼示例

//recv.cpp

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct my_msg_st{
    long my_msg_type;
    char some_text[BUFSIZ];
};

int main(){
    struct my_msg_st some_data;
    int msgid = msgget((key_t)1234,0666 | IPC_CREAT);
    if(msgid == -1){
        fprintf(stderr,"msgget failed with error:%d\n",errno);
        exit(1);
    }
    while(1){
		if(msgrcv(msgid,(void *)&some_data,BUFSIZ,0,0) == -1){
            fprintf(stderr,"msgrecv faild,error:%d\n",errno);
            exit(1);
        }
        printf("recv:%ld %s\n",some_data.my_msg_type,some_data.some_text);
        if(strncmp(some_data.some_text,"end",3) == 0){
            break;
        }
    }
    if(msgctl(msgid,IPC_RMID,0) == -1){
            fprintf(stderr,"msgctl failed\n");
            exit(1);
    }
    exit(0);
    return 0;
}
//send.cpp

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <iostream>
struct my_msg_st{
    long my_msg_type;
    char some_text[BUFSIZ];
};
int main(){
    struct my_msg_st some_data;
    int msgid = msgget((key_t)1234,0666 | IPC_CREAT);
    if(msgid == -1){
        fprintf(stderr,"msgget failed with error:%d\n",errno);
        exit(1);
    }
	some_data.my_msg_type = 1;//必須要賦值並且>0
    while(1){
        printf("please input some text:\n");
        scanf("%s",some_data.some_text);
		if(msgsnd(msgid,(void *)&some_data,BUFSIZ,0) == -1){
            fprintf(stderr,"msgsnd faild,error:%d\n",errno);
            exit(1);
        }
        if(strncmp(some_data.some_text,"end",3) == 0){
            break;
        }
    }
    if(msgctl(msgid,IPC_RMID,0) == -1){
            fprintf(stderr,"msgctl failed\n");
            exit(1);
    }
    exit(0);
    return 0;
}

4、共享內存

  • 共享內存允許兩個不相關的進程去訪問同一部分邏輯內存
  • 如果需要在兩個運行中的進程之間傳輸數據,共享內存將是一種效率極高的解決方案

共享內存概述

  • 共享內存是由IPC爲一個進程創建的一個特殊的地址範圍,它將出現在進程的地址空間中。
  • 其他進程可以把同一段共享內存段“連接到”它們自己的地址空間裏去。
  • 所有進程都可以訪問共享內存地址,就好像它們是有malloc分配的一樣
  • 如果一個進程向這段共享內存寫了數據,所做的改動會立刻被有權訪問同一段共享內存的其他進程看到

4-1、shmget函數

  • 作用:用來創建共享內存
int shmget(key_t key,size_t size,int shmflg);
  • 參數:
    • key:共享內存段的名字
    • size:需要共享的內存量
    • shmflg:由9個權限標誌構成,它們的用法和創建文件時使用的mode模式標誌是一樣的。
  • 返回值:
    • 成功:返回一個非負整數,即該段共享內存的標識碼
    • 失敗:返回 -1

4-2、shmat函數

  • 作用:共享內存段剛被創建的時候,任何進程還都不能訪問它,爲了建立對這個共享內存段的訪問渠道,必須由我們來把它連接到某個進程的地址空間,shmat函數就是用來完成這項工作的。
void *shmat(int shm_id,const void *shm_addr,int shmflg);
  • 參數:

    • shm_id:shmget返回的共享內存標識
    • shm_addr:把共享內存連接到當前進程去的時候準備放置它的那個地址
    • shmflg:是一組按位OR(或)在一起的標誌。它的兩個可能取值是SHM_RND和SHM_RDONLY
  • 返回值:

    • 成功:返回一個指針,指針指向共享內存的第一個字節
    • 失敗:返回 -1
參數 描述
shmaddr爲0 內核自動選擇一個地址
shmaddr不爲0且shmflg無SHM_RND標記 以shmaddr爲連接地址。
shmaddr不爲0且shmflg設置了SHM_RND標記 連接的地址會自動向下調整爲SHMLBA的整數倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY 表示連接操作用來只讀共享內存
  • 在fork() 後,子進程繼承已連接的共享內存
  • 在exec後,已連接的共享內存會自動脫離(detach)
  • 在結束進程後,已連接的共享內存會自動脫離(detach)

4-3、shmdt函數

  • 作用:把共享內存與當前進程脫離開
int shmdt(const void *shm_addr);
  • 參數:
    • shm_addr:由shmat返回的地址指針
  • 返回值:
    • 成功:返回 0
    • 失敗:返回 -1

脫離共享內存並不等於刪除它,只是當前進程不能再繼續訪問它而已

4-4、shmctl函數

  • 作用:共享內存的控制函數
int shmctl(int shm_id,int conmand,struct shmid_ds *buf);
  • 參數:

    • shm_id:由shmget返回的共享內存標識碼
    • command:將要採取的動作(有三個可取值)
    • buf:指向一個保存着共享內存的模式狀態和訪問權限的數據結構
  • 返回值:

    • 成功:返回 0
    • 失敗:返回 -1

command:將要採取的動作(有三個可取值),分別如下:

命令 說明
IPC_STAT 把shmid_ds結構中的數據設置爲共享內存的當前關聯值
IPC_SET 在進程有足夠權限的前提下,把共享內存的當前關聯值設置爲shmid_ds數據結構中給出的值
IPC_RMID 刪除共享內存段

4-5、共享內存示例

//shminfo.h

typedef struct shmbuf_st{
    int bexit;
    int rdwr;
    char name[64];
    int id;
}shmbuf_t;
//shmread.cpp

#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shminfo.h"

int main(){
    //1.創建或獲取
    //int shmget(key_t key,size_t size,int shmflg);
    int shmid = shmget((key_t)1000,0,0);
    if(shmid == -1){
        perror("shmget");
        exit(1);
    }
    
    //2.連接
    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    void *shmaddr = shmat(shmid,NULL,0);
    if(shmaddr == (void *) -1){
        perror("shmat");
        exit(1);
    }
    
    //3.正常對共享內存操作
    shmbuf_t *pbuf = (shmbuf_t *)shmaddr;
    while(1){
        if(pbuf->rdwr == 0){//判斷可讀
            printf("id = %d,name = %s \n",pbuf->id,pbuf->name);
            if(pbuf->bexit){
                break;
            }
            pbuf->rdwr = 1;//重新設置爲可寫
		}
    }
    
    //4.斷開連接
    //int shmdt(const void *shm_addr);
    if(shmdt(shmaddr) == -1){
        perror("shmdt");
    }
    
    //5.刪除共享內存
    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    //shmctl(shmid, IPC_RMID, NULL);
    
    return 0;
}
//shmwrite.cpp

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shminfo.h"

int main(){
    //1.創建或獲取共享內存
    //int shmget(key_t key,size_t size,int shmflg);
    int shmid = shmget((key_t)1000,0,0);
    if(shmid != -1){
        printf("has exit shared memory!\n");
        shmctl(shmid,IPC_RMID,NULL);
    }
    shmid = shmget((key_t)1000,sizeof(shmbuf_t),IPC_CREAT|0666);{
        if(shmid == -1){
            perror("shmget");
            exit(1);
        }
    }
    
    //2.連接到共享內存
    //void *shmat(int shmid,const void *shmaddr,int shmflg);
    void *shmaddr = shmat(shmid,NULL,0);
    if(shmaddr == (void *)-1){
        perror("shmat");
        exit(1);
    }
    
    //3.正常對共享內存操作
    shmbuf_t *pbuf = (shmbuf_t *)shmaddr;
    pbuf->rdwr = 1;
    pbuf->bexit = 0;
    while(1){
        if(pbuf->rdwr == 1){
            printf("Please input id:");
            scanf("%d",&pbuf->id);
            printf("Please input name:");
            scanf("%s",pbuf->name);
            printf("exit?");
            scanf("%d",&pbuf->bexit);
            pbuf->rdwr = 0;//設置爲可讀
            if(pbuf->bexit){
                sleep(1);
                break;
            }
        }
    }
    
    //4.斷開連接
    shmdt(shmaddr);
    
    //5.刪除共享內存
    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}

5、信號量(PV操作)

  • Dijkstra提出的“信號量”概念是共發程序設計領域的一項重大進步
  • 信號量是一種變量,它只能取正整數值,對這些正整數只能進行兩種操作:等待和信號
  • 用兩種記號來表示信號量的這兩種操作:
    • P(semaphore variable) 代表等待
    • V(semaphore variable) 代表信號

信號量的分類

  • 最簡單的信號量是一個只能取“0”和“1”值的變量,也就是人們常說的“二進制信號量”
  • 可以取多種正整數值的信號量叫做“通用信號量”

PV操作的定義

假設我們有一個信號量變量sv,則pv操作的定義如下:

  • P(sv):如果sv的值大於零,就給它減去1;如果sv的值等於零,就掛起該進程的執行
  • V(sv):如果有其他進程因等待sv變量而被掛起,就讓它恢復執行;如果沒有進程因等待sv變量而被掛起,就給它加1

PV操作工作情況

  • 兩個進程共享着sv信號量變量。如果其中之一執行了P(sv)操作,就等於它得到了信號量,也就能夠進入關鍵代碼部分了。
  • 第二個進程將無法進入關鍵代碼,因爲當它嘗試執行P(sv)操作的時候,它會被掛起等待一個進程離開關鍵代碼並執行V(sv)操作釋放這個信號量

5-1、semget函數

  • 作用:創建一個新的信號量或者取得一個現有信號量的鍵字
int semget(key_t key,int num_sems,int sem_flag);
  • 參數:
    • key:是一個整數值,不相關的進程將通過這個值去訪問同一個信號量
    • num_sems:需要使用的信號量個數,它幾乎總是取值爲1
    • sem_flags:是一組標誌,其作用與open函數的各種標誌很相似,它低端的九個位是該信號量的權限,其作用相當於文件的訪問權限,可以與鍵值IPC_CREATE做按位的OR操作以創建一個新的信號量
  • 返回值:
    • 成功:返回一個正數值,它就是其他信號量函數要用到的那個標識碼
    • 失敗:返回 -1

5-2、semop函數

  • 作用:改變信號量的鍵值
int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops);

struct sembuf{
	short sem_num;	//信號量的編號,如果你的工作不需要使用一組信號量,這個值一般就取爲0。
    short sem_op;	//信號量一次PV操作時加減的數值,一般-1(P)和+1(V)
    short sem_flg;	//通常被設置爲SEM_UNDO.她將使操作系統跟蹤當前進程對該信號量的修改情況
};
  • 參數:
    • sem_id:是該信號量的標識碼,也就是semget函數的返回值
    • sem_ops:是個指向一個結構數值的指針
    • num_sem_ops:指定要執行的操作個數,即sem_ops數組中元素的個數。

5-3、semctl函數

  • 作用:允許我們直接控制信號量的信息
int semctl(int sem_id,int sem_num,int command,...);
  • 參數:
    • sem_id:由semget函數返回的一個信號量標識碼
    • sem_num:信號量的編號,如果在工作中需要使用到成組的信號量,就要用到這個編號;它一般取值爲0,表示這是第一個也是唯一的信號量
    • command:將要採取的操作動作
      • 如果還有第四個參數,那它將是一個“union semun”複合結構
        | command | 描述 |
        | -------- | -------- |
        | SETVAL | 用來把信號量初始化爲一個已知的值,這個值在semun結構裏是以val成員的面目傳遞的。 |
        | IPC_RMID | 刪除一個已經沒有人繼續使用的信號量標識碼|

5-4、信號量示例

semget的調用者可以給其key參數傳遞一個特殊的鍵值IPC_PRICATE(其值爲0),這樣無理該信號量是否已經存在,semget都將創建一個新的信號量。使用該鍵值創建的信號量並非像它的名字聲稱的那樣是進程私有的。其他進程,尤其是子進程,也有方法來訪問這個信號量。所以semget的man手冊的BUGS部分上說,使用名字IPC_PRIVATE有些誤導(歷史原因),應該稱爲IPC_NEW。比如下面的代碼清單就在父、子進程間使用一個IPC_PRIVATE信號量來同步。

#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short int *array;
    struct seminfo *_buf;
};

//op爲-1時執行P操作,op爲1時執行V操作
void pv(int sem_id,int op)
{
	struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = op;
    sem_b.sem_flg = SEM_UNDO;
    //int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops);
    semop(sem_id,&sem_b,1);
}

int main(int argc,char *argv[])
{
    //int semget(key_t key,int num_sems,int sem_flag);
    int sem_id = semget(IPC_PRIVATE,1,0666);
    
    union semun sem_un;
    sem_un.val = 1;
    //int semctl(int sem_id,int sem_num,int command,...);
    semctl(sem_id,0,SETVAL,sem_un);
    
    pid_t id = fork();
    if(id < 0){
        return 1;
    }else if(id == 0){
        printf("child try to get binary sem\n");
        //在父、子進程間共享IPC_PRIVATE信號量的關鍵就在於二者都可以操作該信號量的標識符sem_id
        pv(sem_id,-1);
        printf("child get the sem and would release it after 5 second\n");
        sleep(5);
        pv(sem_id,1);
        exit(0);
    }else{
        printf("parent try to get binary sem\n");
        pv(sem_id,-1);
        printf("parent get the sem and would release it after 5 second\n");
        sleep(5);
        pv(sem_id,1);
    }
    waitpid(id,NULL,0);
    semctl(sem_id,0,IPC_RMID,sem_un);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章