十七、進程間通訊--共享內存

一、共享內存基本概念

(一)定義

在物理內存上申請一塊地址,多個進程可以將其映射到自己的虛擬地址空間中,所有進程都可以訪問這塊內存的地址。這塊內存就稱爲被共享的內存
規範化來說就是:共享內存允許兩個或多個進程共享同一塊存儲區,通過地址映射將這塊物理內存映射到不同進程的地址空間中,多個進程可以通過這塊物理空間進行數據的交互,達到進程間通訊的目的。如下圖所示:
在這裏插入圖片描述
如果某個進程向共享內存寫入數據,所做的改動將立刻被可以訪問同一段共享內存的其他進程看到,也會影響其他進程讀取到的數據。

因爲共享內存並沒有提供同步機制,所以會出現多進程的讀取不可控,所以我們需要利用其他機制來同步對共享內存的訪問,如信號量。

(二)原理

在Linux中,每個進程都有屬於自己的進程控制塊PCB,地址空間,頁表。內存管理單元MMU負責將進程的虛擬地址與物理地址進行映射。 兩個不同的虛擬地址空間通過頁表映射到物理內存的同一區域,它們所指向的這塊區域即共享內存

在這裏插入圖片描述
進程通過頁表將虛擬地址映射到物理地址時,在物理地址中有一塊共同的內存區,即共享內存。這塊內存可以被兩個進程看到,那麼當一個進程修改共享內存的數據時,另一個進程訪問共享內存時就會得到新數據。

如果我們用函數指定獲取128字節大小的共享內存時,還是會映射4K大小的共享內存,因爲虛擬地址到物理地址的映射是通過頁表,以頁爲單位進行映射,故一個頁表大小4K的共享內存空間被進程共享,但只能使用128字節。

(三)特點


1.共享內存是最快的IPC(進程間通訊)方式,它只需要兩次數據拷貝,而管道和消息隊列需要四次數據拷貝:因爲管道和消息隊列進行共享的空間都是由內核對象提供管理,所執行的操作也都是系統調用,而這些數據最終還是要在內存中存儲的。管道使用數組來保存數據,利用write,read系統調用將數據寫入/讀取;消息隊列使用自定義消息結構體保存數據,利用msgsnd,msgrcv將數據保存/讀取到內核中。我們畫出管道的拷貝過程:

在這裏插入圖片描述
所以有四次拷貝,分別是:

  • 從內存空間緩衝區將數據拷貝到內核空間緩衝區。
  • 從內核緩衝區將數據拷貝到內存
  • 從內存將數據拷貝到內核空間緩衝區
  • 從內核空間緩衝區將數據拷貝到用戶空間緩衝區。

而共享內存使用相關函數,在內存中開闢一塊空間,映射到不同進程的虛擬地址空間,並且向用戶返回指向該塊內存的指針,因此對該內存可通過指針直接訪問,只需要兩次:

在這裏插入圖片描述

  • 從用戶空間緩衝區拷貝數據到內存。
  • 從內存拷貝數據到用戶空間緩衝區。

2.可以實現任意兩個進程之間的通訊。
3.父進程forl子進程或者exec執行一個新的程序,在子進程和新程序裏面不會繼承父進程之間使用的共享內存。
4.共享內存沒有提供同步機制,需要配合其他機制實現進程間同步和通訊。


二、共享內存相關函數

內核爲每個共享內存維護着一個結構,該結構包含以下成員

struct shmid_ds {
     struct ipc_perm shm_perm;   //用戶權限
     size_t          shm_segsz;  //共享內存大小
     time_t          shm_atime;  //最後一次連接時間
     time_t          shm_dtime;  //最後斷開連接時間
     time_t          shm_ctime;  //最後改變事件
     pid_t           shm_cpid;   //PID
     pid_t           shm_lpid;   //最後一個PID
     shmatt_t        shm_nattch; //多少個進程正在使用這個共享存儲
};

(一)共享內存的創建引用shmget函數

用來創建或獲得一個共享內存,使用shmget函數,函數原型爲:

# include<sys/shm.h>
int shmget(key_t key,size_t size,int flag);
            成功返回共享存儲ID,失敗返回-1

每一個共享存儲對應一個ID,唯一標識符,這個函數就返回一個ID。
參數:

  • key,flag參數和消息隊列的使用一樣,不再闡述。
  • size:指定創建一個共享內存的長度,單位爲字節。如果獲取共享內存,那麼指定爲0。

(二)共享內存的操作shmctl函數

用來控制共享內存shmctl函數,函數原型爲:

# include<sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds* buf);
                    成功返回0,失敗返回-1。

可以對指定的共享內存進行刪除,獲取信息的操作。
參數:

  • shmid:共享內存標識符

  • cmd:要採取的操作,取值如下:
    (1)IPC_STAT:把shmid_ds結構中的數據設置爲共享內存當前的關聯值,即共用內存的當前關聯值覆蓋shmid_ds的值。
    (2)IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置爲shmid_ds結構中給出的值。
    (3)IPC_RMID:刪除共享內存段。

  • buf:內核爲每個共享內存創建的結構體shmid_ds。

(三)連接共享內存shmat函數

一旦創建/獲得一個共享內存ID,那麼進程就可以調用shmat函數將共享內存連接到它的地址空間中。函數原型爲:

# include<sys/shm.h>
void* shmat(int shmid,const void* addr,int flag);
            成功返回指向共享存儲段的指針,出錯返回-1

參數:

  • shmid:共享內存標識符
  • addr:指定共享內存連接到當前地址中的地址位置。一般爲NULL空,表示讓系統來選擇共享內存的地址。
  • flag:標誌位,通常爲0。

(四)斷開共享內存shmdt函數

對共享內存的操作完成時,將共享內存從當前進程分離,函數原型爲:

# include<sys/shm.h>
int shmdt(const void* shmaddr);
               成功返回0,出錯返回-1

注意,分離不代表刪除共享內存,只是使該共享內存對當前進程不再可用,直到某個進程帶IPC_RMID命令的調用shmctl刪除共享內存爲止,共享內存才消失。

參數:

  • shmaddr:shmat函數返回的地址指針。

執行成功,內核將使與該共享內存相關的shmid_dds結構中的shm_nattch計數器減一。

(五)命令

  1. 查看系統中的共享存儲段
ipcs -m
  1. 刪除系統中的共享存儲段
ipcrm -m [shmid]

三、實例

題目: 兩個進程,A進程將獲取到的用戶數據寫入內存,B進程打印共享內存中的數據。

如果我們不對這塊共享內存進行同步控制,就會出現讀取混亂的問題,會出現A寫入的數據還沒有被B讀,A又重新寫入了等問題,所以我們必須要對這塊內存進行一個同步控制,就採用信號量來控制,我們分析一下,如何控制A,B纔可以實現:
A寫數據時,B不能讀數據;B讀數據時,A不能寫數據,B進程只有在A寫入數據後纔可讀取數據,A只有在B取出數據後纔可以繼續發數據。

可以看到A對B有影響,B對A有影響。故需要兩個信號量控制,sem1控制B,sem2控制A。信號量初始值的考慮:運行程序,A進程先寫入數據,B進程處於阻塞狀態,故sem1=0,sem2=1,那麼我們可以畫出A.c,B.c如何實現對進程的同步,達到要求:
在這裏插入圖片描述
那我們就可以按照這個控制方法寫出代碼:

  • 先進行shmget函數對內存空間的創建和初始化,再連接共享內存,用ptr保存shmat連接到的內存地址。
  • 信號量的創建,初始化。
  • 進行循環讀取數據,對sem2信號量P操作,進行數據的寫入,寫完之後對sem1信號量V操作。B進程這一塊相反,
  • shmdt斷開共享內存連接。
  • shcmtl刪除共享內存。

A.C

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<sys/shm.h>
# include<string.h>
# include "./sem.h"
int main()
{
    int shmid=shmget((key_t)1234,128,IPC_CREAT|0664);//A先運行,創建共享內存
    assert(shmid!=-1);

    char* ptr=(char*)shmat(shmid,NULL,0);//連接共享內存,得到指向共享內存塊的地址
    assert(ptr!=(char*)-1);

    //創建兩個信號量sem1控制B,sem2控制A
    int init_val[2]={0,1};
    int semid=CreateSem(1234,init_val,2);
    assert(semid!=-1);

    while(1)
    {
        SemP(semid,1);//對sem2進行P操作
        printf("input:");
        fgets(ptr,127,stdin);
        SemV(semid,0);//對sem1進行V操作
        if(strncmp(ptr,"end",3)==0)
        {
            break;
        }
    }
    shmdt(ptr);//斷開連接
    shmctl(shmid,IPC_RMID,NULL);//刪除共享內存塊
}


B.C

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<assert.h>
# include<sys/sem.h>
# include<sys/shm.h>
# include<string.h>
# include "./sem.h"
int main()
{
    int shmid=shmget((key_t)1234,128,IPC_CREAT|0664);//獲得共享內存
    assert(shmid!=-1);

    char* ptr=(char*)shmat(shmid,NULL,0);
    assert(ptr!=(char*)-1);

    //創建兩個信號量sem1控制B,sem2控制A
    int init_val[2]={0,1};
    int semid=CreateSem(1234,init_val,2);
    assert(semid!=-1);

    while(1)
    {
        SemP(semid,0);//對sem1進行P操作
        if(strncmp(ptr,"end",3)==0)
        {
            break;
        }
        printf("B process:%s\n",ptr);
        sleep(3);
        printf("B over\n");
        memset(ptr,0,128);
        SemV(semid,1);//對sem2進行V操作
    }
    shmdt(ptr);
    shmctl(shmid,IPC_RMID,NULL);
}


可以看到代碼和我們控制的一樣。

在這裏插入圖片描述

加油哦!💪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章