一文搞定:Linux 共享內存原理

來源:Linux 雲計算網絡

在 Linux 系統中,每個進程都有獨立的虛擬內存空間,也就是說不同的進程訪問同一段虛擬內存地址所得到的數據是不一樣的,這是因爲不同進程相同的虛擬內存地址會映射到不同的物理內存地址上。

但有時候爲了讓不同進程之間進行通信,需要讓不同進程共享相同的物理內存,Linux 通過 共享內存 來實現這個功能。下面先來介紹一下 Linux 系統的共享內存的使用。

共享內存使用

1. 獲取共享內存

要使用共享內存,首先需要使用 shmget() 函數獲取共享內存,shmget() 函數的原型如下:

int shmget(key_t key, size_t size, int shmflg);

函數調用成功時返回一個新建或已經存在的的共享內存標識符,取決於 shmflg 的參數。失敗返回 - 1,並設置錯誤碼。

2. 關聯共享內存

shmget() 函數返回的是一個標識符,而不是可用的內存地址,所以還需要調用 shmat() 函數把共享內存關聯到某個虛擬內存地址上。shmat() 函數的原型如下:

void *shmat(int shmid, const void *shmaddr, int shmflg);

函數調用成功返回一個可用的指針(虛擬內存地址),出錯返回 - 1。

3. 取消關聯共享內存

當一個進程不需要共享內存的時候,就需要取消共享內存與虛擬內存地址的關聯。取消關聯共享內存通過 shmdt() 函數實現,原型如下:

int shmdt(const void *shmaddr);

函數調用成功返回 0,出錯返回 - 1。

共享內存使用例子

下面通過一個例子來介紹一下共享內存的使用方法。在這個例子中,有兩個進程,分別爲 進程A 和 進程B進程A 創建一塊共享內存,然後寫入數據,進程B 獲取這塊共享內存並且讀取其內容。

進程 A

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_PATH "/tmp/shm"
#define SHM_SIZE 128

int main(int argc, char *argv[])
{
    int shmid;
    char *addr;
    key_t key = ftok(SHM_PATH, 0x6666);
    
    shmid = shmget(key, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);
    if (shmid < 0) {
        printf("failed to create share memory\n");
        return -1;
    }
    
    addr = shmat(shmid, NULL, 0);
    if (addr <= 0) {
        printf("failed to map share memory\n");
        return -1;
    }
    
    sprintf(addr, "%s""Hello World\n");
    
    return 0;
}

進程 B

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_PATH "/tmp/shm"
#define SHM_SIZE 128

int main(int argc, char *argv[])
{
    int shmid;
    char *addr;
    key_t key = ftok(SHM_PATH, 0x6666);
    
    char buf[128];
    
    shmid = shmget(key, SHM_SIZE, IPC_CREAT);
    if (shmid < 0) {
        printf("failed to get share memory\n");
        return -1;
    }
    
    addr = shmat(shmid, NULL, 0);
    if (addr <= 0) {
        printf("failed to map share memory\n");
        return -1;
    }
    
    strcpy(buf, addr, 128);
    printf("%s", buf);
    
    return 0;
}

測試時先運行進程 A,然後再運行進程 B,可以看到進程 B 會打印出 “Hello World”,說明共享內存已經創建成功並且讀取。

共享內存實現原理

我們先通過一幅圖來了解一下共享內存的大概原理,如下圖:

通過上圖可知,共享內存是通過將不同進程的虛擬內存地址映射到相同的物理內存地址來實現的,下面將會介紹 Linux 的實現方式。

在 Linux 內核中,每個共享內存都由一個名爲 struct shmid_kernel 的結構體來管理,而且 Linux 限制了系統最大能創建的共享內存爲 128 個。通過類型爲 struct shmid_kernel 結構的數組來管理,如下:

struct shmid_ds {
 struct ipc_perm  shm_perm; /* operation perms */
 int   shm_segsz; /* size of segment (bytes) */
 __kernel_time_t  shm_atime; /* last attach time */
 __kernel_time_t  shm_dtime; /* last detach time */
 __kernel_time_t  shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short  shm_nattch; /* no. of current attaches */
 unsigned short   shm_unused; /* compatibility */
 void    *shm_unused2; /* ditto - used by DIPC */
 void   *shm_unused3; /* unused */
};

struct shmid_kernel
{ 
 struct shmid_ds  u;
 /* the following are private */
 unsigned long  shm_npages; /* size of segment (pages) */
 pte_t   *shm_pages; /* array of ptrs to frames -> SHMMAX */ 
 struct vm_area_struct *attaches; /* descriptors for attaches */
};

static struct shmid_kernel *shm_segs[SHMMNI]; // SHMMNI等於128

從註釋可以知道 struct shmid_kernel 結構體各個字段的作用,比如 shm_npages 字段表示共享內存使用了多少個內存頁。而 shm_pages 字段指向了共享內存映射的虛擬內存頁表項數組等。

另外 struct shmid_ds 結構體用於管理共享內存的信息,而 shm_segs數組 用於管理系統中所有的共享內存。

shmget() 函數實現

通過前面的例子可知,要使用共享內存,首先需要調用 shmget() 函數來創建或者獲取一塊共享內存。shmget() 函數的實現如下:

asmlinkage long sys_shmget (key_t key, int size, int shmflg)
{
 struct shmid_kernel *shp;
 int err, id = 0;

 down(¤t->mm->mmap_sem);
 spin_lock(&shm_lock);
 if (size < 0 || size > shmmax) {
  err = -EINVAL;
 } else if (key == IPC_PRIVATE) {
  err = newseg(key, shmflg, size);
 } else if ((id = findkey (key)) == -1) {
  if (!(shmflg & IPC_CREAT))
   err = -ENOENT;
  else
   err = newseg(key, shmflg, size);
 } else if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) {
  err = -EEXIST;
 } else {
  shp = shm_segs[id];
  if (shp->u.shm_perm.mode & SHM_DEST)
   err = -EIDRM;
  else if (size > shp->u.shm_segsz)
   err = -EINVAL;
  else if (ipcperms (&shp->u.shm_perm, shmflg))
   err = -EACCES;
  else
   err = (int) shp->u.shm_perm.seq * SHMMNI + id;
 }
 spin_unlock(&shm_lock);
 up(¤t->mm->mmap_sem);
 return err;
}

shmget() 函數的實現比較簡單,首先調用 findkey() 函數查找值爲 key 的共享內存是否已經被創建,findkey() 函數返回共享內存在 shm_segs數組 的索引。如果找到,那麼直接返回共享內存的標識符即可。否則就調用 newseg() 函數創建新的共享內存。newseg() 函數的實現也比較簡單,就是創建一個新的 struct shmid_kernel 結構體,然後設置其各個字段的值,並且保存到 shm_segs數組 中。

shmat() 函數實現

shmat() 函數用於將共享內存映射到本地虛擬內存地址,由於 shmat() 函數的實現比較複雜,所以我們分段來分析這個函數:

asmlinkage long sys_shmat (int shmid, char *shmaddr, int shmflg, ulong *raddr)
{
 struct shmid_kernel *shp;
 struct vm_area_struct *shmd;
 int err = -EINVAL;
 unsigned int id;
 unsigned long addr;
 unsigned long len;

 down(¤t->mm->mmap_sem);
 spin_lock(&shm_lock);
 if (shmid < 0)
  goto out;

 shp = shm_segs[id = (unsigned int) shmid % SHMMNI];
 if (shp == IPC_UNUSED || shp == IPC_NOID)
  goto out;

上面這段代碼主要通過 shmid 標識符來找到共享內存描述符,上面說過系統中所有的共享內存到保存在 shm_segs 數組中。

 if (!(addr = (ulong) shmaddr)) {
  if (shmflg & SHM_REMAP)
   goto out;
  err = -ENOMEM;
  addr = 0;
 again:
  if (!(addr = get_unmapped_area(addr, shp->u.shm_segsz))) // 獲取一個空閒的虛擬內存空間
   goto out;
  if(addr & (SHMLBA - 1)) {
   addr = (addr + (SHMLBA - 1)) & ~(SHMLBA - 1);
   goto again;
  }
 } else if (addr & (SHMLBA-1)) {
  if (shmflg & SHM_RND)
   addr &= ~(SHMLBA-1);       /* round down */
  else
   goto out;
 }

上面的代碼主要找到一個可用的虛擬內存地址,如果在調用 shmat() 函數時沒有指定了虛擬內存地址,那麼就通過 get_unmapped_area() 函數來獲取一個可用的虛擬內存地址。

 spin_unlock(&shm_lock);
 err = -ENOMEM;
 shmd = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
 spin_lock(&shm_lock);
 if (!shmd)
  goto out;
 if ((shp != shm_segs[id]) || (shp->u.shm_perm.seq != (unsigned int) shmid / SHMMNI)) {
  kmem_cache_free(vm_area_cachep, shmd);
  err = -EIDRM;
  goto out;
 }

上面的代碼主要通過調用 kmem_cache_alloc() 函數創建一個 vm_area_struct 結構,在內存管理一章知道,vm_area_struct 結構用於管理進程的虛擬內存空間。

 shmd->vm_private_data = shm_segs + id;
 shmd->vm_start = addr;
 shmd->vm_end = addr + shp->shm_npages * PAGE_SIZE;
 shmd->vm_mm = current->mm;
 shmd->vm_page_prot = (shmflg & SHM_RDONLY) ? PAGE_READONLY : PAGE_SHARED;
 shmd->vm_flags = VM_SHM | VM_MAYSHARE | VM_SHARED
    | VM_MAYREAD | VM_MAYEXEC | VM_READ | VM_EXEC
    | ((shmflg & SHM_RDONLY) ? 0 : VM_MAYWRITE | VM_WRITE);
 shmd->vm_file = NULL;
 shmd->vm_offset = 0;
 shmd->vm_ops = &shm_vm_ops;

 shp->u.shm_nattch++;     /* prevent destruction */
 spin_unlock(&shm_lock);
 err = shm_map(shmd);
 spin_lock(&shm_lock);
 if (err)
  goto failed_shm_map;

 insert_attach(shp,shmd);  /* insert shmd into shp->attaches */

 shp->u.shm_lpid = current->pid;
 shp->u.shm_atime = CURRENT_TIME;

 *raddr = addr;
 err = 0;
out:
 spin_unlock(&shm_lock);
 up(¤t->mm->mmap_sem);
 return err;
 ...
}

上面的代碼主要是設置剛創建的 vm_area_struct 結構的各個字段,比較重要的是設置其 vm_ops 字段爲 shm_vm_opsshm_vm_ops 定義如下:

static struct vm_operations_struct shm_vm_ops = {
 shm_open,  /* open - callback for a new vm-area open */
 shm_close,  /* close - callback for when the vm-area is released */
 NULL,   /* no need to sync pages at unmap */
 NULL,   /* protect */
 NULL,   /* sync */
 NULL,   /* advise */
 shm_nopage,  /* nopage */
 NULL,   /* wppage */
 shm_swapout  /* swapout */
};

shm_vm_ops 的 nopage 回調爲 shm_nopage() 函數,也就是說,當發生頁缺失異常時將會調用此函數來恢復內存的映射。

從上面的代碼可看出,shmat() 函數只是申請了進程的虛擬內存空間,而共享內存的物理空間並沒有申請,那麼在什麼時候申請物理內存呢?答案就是當進程發生缺頁異常的時候會調用 shm_nopage() 函數來恢復進程的虛擬內存地址到物理內存地址的映射。

shm_nopage() 函數實現

shm_nopage() 函數是當發生內存缺頁異常時被調用的,代碼如下:

static struct page * shm_nopage(struct vm_area_struct * shmd, unsigned long address, int no_share)
{
 pte_t pte;
 struct shmid_kernel *shp;
 unsigned int idx;
 struct page * page;

 shp = *(struct shmid_kernel **) shmd->vm_private_data;
 idx = (address - shmd->vm_start + shmd->vm_offset) >> PAGE_SHIFT;

 spin_lock(&shm_lock);
again:
 pte = shp->shm_pages[idx]; // 共享內存的頁表項
 if (!pte_present(pte)) {   // 如果內存頁不存在
  if (pte_none(pte)) {
   spin_unlock(&shm_lock);
   page = get_free_highpage(GFP_HIGHUSER); // 申請一個新的物理內存頁
   if (!page)
    goto oom;
   clear_highpage(page);
   spin_lock(&shm_lock);
   if (pte_val(pte) != pte_val(shp->shm_pages[idx]))
    goto changed;
  } else {
   ...
  }
  shm_rss++;
  pte = pte_mkdirty(mk_pte(page, PAGE_SHARED));   // 創建頁表項
  shp->shm_pages[idx] = pte;                      // 保存共享內存的頁表項
 } else
  --current->maj_flt;  /* was incremented in do_no_page */

done:
 get_page(pte_page(pte));
 spin_unlock(&shm_lock);
 current->min_flt++;
 return pte_page(pte);
 ...
}

shm_nopage() 函數的主要功能是當發生內存缺頁時,申請新的物理內存頁,並映射到共享內存中。由於使用共享內存時會映射到相同的物理內存頁上,從而不同進程可以共用此塊內存。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xexEMudXg7tWw6bS2y2f3A