1. 程式人生 > >程序間通訊(二)共享記憶體、訊號量

程序間通訊(二)共享記憶體、訊號量

本片部落格會貼上部分程式碼,想要了解更多程式碼資訊,可訪問小編的GitHub關於本篇的程式碼

  • 共享記憶體

這裡有涉及的mmap的知識
下圖為共享記憶體原理圖
在這裡插入圖片描述

因為共享記憶體是直接將申請來的一塊實體記憶體對映到虛擬地址空間中,允許兩個或多個程序共享,因此進行資料傳輸的時候相較於其它程序間通訊方式,少了兩步使用者態與核心態資料拷貝的過程,因此共享記憶體是最快的程序間通訊方式。
  • 建立共享記憶體
int shmget(key_t key, size_t size, int shmflg);

key: 作業系統上ipc標識
size: 要建立的共享記憶體大小
shmflg:
IPC_CREAT|IPC_EXCL|0664
返回值:操作控制代碼 失敗:-1

  • 記憶體對映
void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid: 操作控制代碼
shmaddr: 對映起始地址,NULL(作業系統分配)
shmflg: SHM_RDONLY–只讀 否則讀寫
成功返回:對映的虛擬地址空間首地址
失敗返回:(void*)-1

  • 解除對映
int shmdt(const void *shmaddr);

shmaddr 共享記憶體的對映首地址
返回值:成功:0 失敗:-1

  • 刪除共享記憶體
int
shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid: 控制代碼
cmd: IPC_RMID 刪除
buf: 用於接收共享記憶體描述資訊,不關心可以置空

作業系統:如果有程序依然與共享記憶體保持對映連線關係,那麼共享記憶體將不會被立即刪除,而是等最後一個對映斷開後刪除 ,在這期間,將拒絕其他程序對映。

服務端
/*這是共享記憶體的服務端,目的是:往共享記憶體寫入資料後*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h> #include<string.h> #include<sys/ipc.h> #include<sys/shm.h> //定義一個ipc識別符號 #define KEY 0x520 int main() { umask(0); //建立共享記憶體 //int shmget(key_t key, size_t size, int shmflg); int shmid=-1; shmid=shmget(KEY,520,IPC_CREAT|0664); if(shmid<0){ perror("shmget error"); return -1; }else { //對映到虛擬地址空間 //void *shmat(int shmid, const void *shmaddr, int shmflg); void*shmhead=shmat(shmid,NULL,0);//shmaddr是要對映的起始地址,NULL表示由系統分配, //shmflg如果是SHM_RDONLY則只讀許可權,否則讀寫 if(shmhead==(void*)-1) { perror("shmat error"); return -1; } while(1) { //對映成功,寫操作 memset(shmhead,0x00,520); scanf("%s",(char*)shmhead); sleep(1); } //解除對映int shmdt(const void *shmaddr); shmdt(shmhead); //刪除共享記憶體int shmctl(int shmid, int cmd, struct shmid_ds *buf); shmctl(shmid,IPC_RMID,NULL); } return 0; }
客戶端
/*這是共享記憶體的客戶端,目的:隔一秒鐘,去共享記憶體取一次資料*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/shm.h>
//定義一個ipc識別符號
#define KEY 0x520

int main()
{
    umask(0);
    //建立共享記憶體
    //int shmget(key_t key, size_t size, int shmflg);
    int shmid=shmget(KEY,520,IPC_CREAT|0664);
    if(shmid<0){
        perror("shmget error");
        return -1;
    }else
    {
        //對映到虛擬地址空間
        //void *shmat(int shmid, const void *shmaddr, int shmflg);
        void*shmhead=shmat(shmid,NULL,SHM_RDONLY);//shmaddr是要對映的起始地址,NULL表示由系統分配,
        //shmflg如果是SHM_RDONLY則只讀許可權,否則讀寫
        if(shmhead==(void*)-1)
        {
            perror("shmat error");
            return -1;
        }
        while(1)
        {
            //對映成功,讀取資料
            printf("memshare:%s\n",(char*)shmhead);
            sleep(1);
        }
        //解除對映int shmdt(const void *shmaddr);
        shmdt(shmhead);
        //刪除共享記憶體int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        shmctl(shmid,IPC_RMID,NULL);
    }
    return 0;
}

在這裡插入圖片描述

  • 訊號量

用於實現程序間的同步與互斥(程序/執行緒安全概念),保證程序間對臨界資源的安全有序訪問。
多個程序同時操作一個臨界資源的時候就需要通過同步與互斥機制來實現臨界資源的安全訪問

同步:保證對臨界資源訪問的時序的可控性
互斥:對臨界資源同一時間的唯一訪問性

本質:具有一個等待佇列的計數器(代表現在還有沒有資源使用),當訊號量沒有資源可用時,這時候需要阻塞等待。
同步:只有訊號量資源計數從0轉變為1的時候,才會通知別人,打斷阻塞等待,去操作臨界資源。
互斥:同一時間,A獲取了訊號量的資源,其它程序就沒有辦法獲取資源了。
二元訊號量:訊號量如果想要實現互斥,那麼它的計數器只能是0或1

*P操作: 獲取訊號量資源說的是對計數器進行-1操作
*V操作:釋放訊號量資源說的是對計數器進行+1操作
程序在操作臨界資源之前先獲取訊號量資源,判斷是否可以對臨界資源進行操作,如果訊號量沒有資源了(計數器為0),則需要等待,當別人釋放訊號量計數器變為1,才會喚醒等待的程序去重新獲取訊號量資源。

  • 訊號量資料大於0,代表訊號量有資源,可以操作,
    訊號量資源等於0,代表訊號量沒有資源,需要等待。

  • 訊號量作為程序間通訊方式,意味著大家都能訪問到訊號量,實際上也是一個臨界資源,但是訊號量的這個臨界資源的操作是不會出問題的,因為訊號量的操作是一個原子操作。

  • 建立訊號量

int semget(key_t key, int nsems, int semflg);

key: IPC_KEY標識
nsems:指定這次要建立的訊號量個數
semflg:IPC_CREAT|IPC_EXCL|0664
返回值:操作訊號量控制代碼
失敗:-1

  • 設定訊號量初值
int semctl(int semid, int semnum, int cmd, ...);

semid:訊號量控制代碼
semnum:指定操作的是第幾個訊號量
cmd:SETVAL設定單個訊號量的初值,SETALL設定所有訊號量的初值,semnum將被忽略,IPC_RMID刪除訊號量,…將被NULL
成功返回值:0
失敗返回值:-1

... 是一個不定引數:

對於不同的操作 函式會對對應的操作的聯合結構中的物件操作
設定單個訊號量的初值 int val; /* Value for SETVAL */
設定所有訊號量的初值 unsigned short *array; /* Array for GETALL, SETALL */
union semun 
{
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO
						 (Linux-specific) */
};

  • 獲取/釋放訊號量資源
int semop(int semid, struct sembuf *sops, unsigned nsops);

semid:訊號量控制代碼
nsops操作的訊號量個數

這個sembuf在庫裡是這樣的

struct sembuf 
{
	unsigned short sem_num;  /* semaphore number */
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
}
PV操作:為了方便操作獲取、釋放資源,我們封裝了兩個函式

獲取資源:

void sem_P(int id)
{
    struct sembuf buf;
    buf.sem_num = 0;		//訊號量編號,因為下面的操作值建立了一個訊號量,編號為0
    buf.sem_op = -1;		//訊號量操作,獲取資源,訊號量資源-1
    buf.sem_flg = SEM_UNDO;	//SEM_UNDO,如果該程序意外退出,則會自動釋放該資源

    semop(id, &buf, 1);
}

釋放資源:

void sem_V(int id) 
{
    struct sembuf buf;
    buf.sem_num = 0;		//訊號量編號,因為下面的操作值建立了一個訊號量,編號為0
    buf.sem_op = 1;			//訊號量操作,釋放資源,訊號量資源+1
    buf.sem_flg = SEM_UNDO;	//SEM_UNDO,如果該程序意外退出,則會自動釋放該資源

    semop(id, &buf, 1); 
}
  • 刪除訊號量
semctl(semid, 0,IPC_RMID,NULL);
訊號量實現同步
//訊號量實現同步
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ipc.h>
#include<errno.h>
#include<sys/sem.h>

#define IPC_KEY 0x999
union semun {

    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                (Linux-specific) */
};
void sem_P(id)
{
    struct sembuf buff;
    buff.sem_num=0;
    buff.sem_op=-1;
    buff.sem_flg=SEM_UNDO;

    semop(id,&buff,1);
}
void sem_V(id)
{
    struct sembuf buff;
    buff.sem_num=0;
    buff.sem_op=1;
    buff.sem_flg=SEM_UNDO;

    semop(id,&buff,1);
}
int main()
{
    umask(0);
    int semid = semget(IPC_KEY,1,IPC_CREAT|0644);
    if(semid<0){
        perror("semget error");
        return -1;
    }
    union semun val; 
    //設定訊號量初值int semctl(int semid, int semnum, int cmd, ...);
    val.val=0;
    semctl(semid,0,SETVAL,val);

    int pid = -1;
    pid=fork();
    if(pid<0){
        perror("pid error");
        return -1;
    }else if(pid==0)
    {
        //子程序去獲取資源,吃方便麵
        while(1)
        {
            sem_P(semid);
            printf("我吃了一包方便麵\n");
            sleep(1);
        }
    }
    else{
        while(1){
            sem_V(semid);
            printf("我製造了一包方便麵\n");
            sleep(1);
        }
    }
    semctl(semid,0,IPC_RMID,NULL);
}

在這裡插入圖片描述

訊號量實現互斥

這是一個基於訊號量的互斥操作, 讓子程序列印A睡1000us然後再列印一個A 讓父程序列印B睡1000us然後再列印一個B
檢查結果是否是連續的?
如何讓列印結果是我們預期的AA BB這種形式
關鍵點就在於兩個程序的列印操作都不能被打斷,這時候就需要使用一個一元訊號量來完成互斥操作

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include <sys/sem.h>
//定義IPC標識
#define IPC_KEY 0x666
union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                (Linux-specific) */
};
void sem_P(int id)
{
    struct sembuf buf;
    buf.sem_num=0;
    buf.sem_op=-1;
    buf.sem_flg=SEM_UNDO;

    semop(id,&buf,1);
}
void sem_V(int id)
{
    struct sembuf buf;
    buf.sem_num=0;
    buf.sem_op=1;
    buf.sem_flg=SEM_UNDO;

    semop(id,&buf,1);
}

int main()
{
    umask(0);
    //1、建立訊號量int semget(key_t key, int nsems, int semflg);
    int semid=-1;
    semid=semget(IPC_KEY,1,IPC_CREAT|0664);
    if(semid<0){
        perror("semget error");
        return -1;
    }
    union semun godo;
    godo.val=1;
    //2、設定訊號量初值int semctl(int semid, int semnum, int cmd, ...);
    semctl(semid,0,SETVAL,godo);
    //建立子程序
    int pid=-1;
    pid=fork();
    if(pid<0){
        perror("fork error");
        return -1;
    }else if(pid==0){
        //子程序獲取訊號量,列印A,睡1000us再列印A,再釋放資源,輪迴
        while(1)
        {
            //獲取資源
            sem_P(semid);
            printf("A");
            fflush(stdout);
            usleep(1000);
            printf("A");
            fflush(stdout);
            //釋放資源
            sem_V(semid);
        }
    }else{
        //父程序
        while(1)
        {
            sem_P(semid);
            printf("B");
            fflush(stdout);
            usleep(1000);
            printf("B ");
            fflush(stdout);
            //釋放資源
            sem_V(semid);
        }
    }
    //刪除訊號量
    semctl(semid,0,IPC_RMID,NULL);
    return 0;
}

在這裡插入圖片描述

  • 將二元訊號量P/V操作,封裝成動態/靜態庫,並分別使用並測試
  • 生產者消費者原理
  • 同步:生產者消費者問題,主要是通過生產者生產出產品放入緩衝區,然後消費者從緩衝區中拿出產品消費。
  • 互斥:生產者和消費者不能同時在臨界區進行操作,同一時間只能有一個生產者或者一個消費者在臨界區進行操作。