日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
聊聊跨進(jìn)程共享內(nèi)存的內(nèi)部工作原理

大家好,我是飛哥!

成都創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供通渭網(wǎng)站建設(shè)、通渭做網(wǎng)站、通渭網(wǎng)站設(shè)計(jì)、通渭網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、通渭企業(yè)網(wǎng)站模板建站服務(wù),十余年通渭做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。

在 Linux 系統(tǒng)的進(jìn)程虛擬內(nèi)存中,一個(gè)重要的特性就是不同進(jìn)程的地址空間是隔離的。A 進(jìn)程的地址 0x4000 和 B 進(jìn)程的 0x4000 之間沒(méi)有任何關(guān)系。這樣確確實(shí)實(shí)是讓各個(gè)進(jìn)程的運(yùn)行時(shí)互相之間的影響降到了最低。某個(gè)進(jìn)程有 bug 也只能自己崩潰,不會(huì)影響其它進(jìn)程的運(yùn)行。

但是有時(shí)候我們想要跨進(jìn)程傳遞一些數(shù)據(jù)。因?yàn)檫M(jìn)程虛擬內(nèi)存地址是隔離的。所以目前業(yè)界最常用的做法是讓進(jìn)程之間通過(guò) 127.0.0.1 或者是 Unix Domain Socket 等本機(jī)網(wǎng)絡(luò)手段進(jìn)行數(shù)據(jù)的傳輸。這個(gè)方案在傳輸?shù)臄?shù)據(jù)量較小的時(shí)候工作是很不錯(cuò)的。

但如果進(jìn)程間想共享的數(shù)據(jù)特別大,比如說(shuō)幾個(gè) GB,那如果使用網(wǎng)絡(luò) IO 方案的話,就會(huì)涉及到大量的內(nèi)存拷貝的開(kāi)銷,導(dǎo)致比較低的程序性能。這是可以采用進(jìn)程間共享內(nèi)存的方法來(lái)在通信時(shí)避免內(nèi)存拷貝。

那么問(wèn)題來(lái)了,不同進(jìn)程之間的虛擬地址是隔離的,共享內(nèi)存又是如何突破這個(gè)限制的呢?我們今天就來(lái)深入地了解下共享內(nèi)存的內(nèi)部工作原理。

一、共享內(nèi)存的使用方式

共享內(nèi)存發(fā)送方進(jìn)程的開(kāi)發(fā)基本過(guò)程是調(diào)用 memfd_create 創(chuàng)建一個(gè)內(nèi)存文件。然后通過(guò) mmap 系統(tǒng)調(diào)用為這個(gè)內(nèi)存文件申請(qǐng)一塊共享內(nèi)存。然后這個(gè)內(nèi)存文件就可以寫入數(shù)據(jù)了。最后把這個(gè)文件的句柄通過(guò) Unix Domain Socket 的方式給接收方進(jìn)程發(fā)送過(guò)去。

下面是發(fā)送方的核心代碼。

int main(int argc, char **argv) {
 // 創(chuàng)建內(nèi)存文件
 fd = memfd_create("Server memfd", ...);

 // 為內(nèi)存文件申請(qǐng) MAP_SHARED 類型的內(nèi)存
 shm = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

 // 向共享內(nèi)存中寫入數(shù)據(jù)
 sprintf(shm, "這段內(nèi)容是保存在共享內(nèi)存里的,接收方和發(fā)送方都能根據(jù)自己的fd訪問(wèn)到這塊內(nèi)容");

 // 把共享內(nèi)存文件的句柄給接收方進(jìn)程發(fā)送過(guò)去
 struct msghdr msgh;
 *((int *) CMSG_DATA(CMSG_FIRSTHDR(&msgh))) = fd;
 sendmsg(conn, &msgh, 0);
 ......
}

共享內(nèi)存接收方的工作過(guò)程是先用 Unix Domain Socket 連接上服務(wù)器,然后使用 recvmsg 就可以收到發(fā)送方發(fā)送過(guò)來(lái)的文件句柄。

int main(int argc, char **argv) {
 // 通過(guò) Unix Domain Socket 連接發(fā)送方
 connect(conn, (struct sockaddr *)&address, sizeof(struct sockaddr_un));

 // 通過(guò)連接取出發(fā)送方發(fā)送過(guò)來(lái)的內(nèi)存文件句柄
 int size = recvmsg(conn, &msgh, 0);
 fd = *((int *) CMSG_DATA(cmsgh));

 // 讀取共享文件中的內(nèi)容
 shm = mmap(NULL, shm_size, PROT_READ, MAP_PRIVATE, fd, 0);
 printf("共享內(nèi)存中的文件內(nèi)容是: %s\n", shm);
 ......
}

這樣這兩個(gè)進(jìn)程都各自有一個(gè)文件句柄,在底層上是指向同一個(gè)內(nèi)存文件的。這樣就實(shí)現(xiàn)了發(fā)送方和接收方之間的內(nèi)存文件共享了。

圖片

但我們上面介紹的是開(kāi)發(fā)基本過(guò)程。按照我們開(kāi)發(fā)內(nèi)功修煉公眾號(hào)的風(fēng)格,這還不算完,我們是要把它最底層的原理真正的弄通透才算的。所以接下來(lái)我們?cè)偕钊氲胤治?memfd_create、 mmap、以及 Unix Domain socket sendmsg 和 recvmsg 的底層工作原理,來(lái)看看它們是如何配合來(lái)實(shí)現(xiàn)跨進(jìn)程共享內(nèi)存的。

二、共享內(nèi)存文件原理

在發(fā)送方發(fā)送文件之前,需要先通過(guò) memfd_create 來(lái)創(chuàng)建一個(gè)內(nèi)存文件,然后再使用 mmap 為其分配內(nèi)存。

2.1 創(chuàng)建內(nèi)存文件

其中 memfd_create 函數(shù)是一個(gè)系統(tǒng)調(diào)用。內(nèi)核中它的主要邏輯有兩個(gè),一是調(diào)用 get_unused_fd_flags 申請(qǐng)一個(gè)沒(méi)使用過(guò)的文件句柄,二是調(diào)用 shmem_file_setup 創(chuàng)建一個(gè)共享內(nèi)存文件。

圖片

我們來(lái)看 memfd_create 的源碼。

// file:mm/memfd.c
SYSCALL_DEFINE2(memfd_create,
  const char __user *, uname,
  unsigned int, flags)
{
 ...
 // 申請(qǐng)一個(gè)未使用過(guò)的文件句柄
 fd = get_unused_fd_flags((flags & MFD_CLOEXEC) ? O_CLOEXEC : 0);

 // 創(chuàng)建一個(gè)共享內(nèi)存的文件
 file = shmem_file_setup(name, 0, VM_NORESERVE);

 fd_install(fd, file);
 return fd;
}

其中在 shmem_file_setup 函數(shù)中又調(diào)用了 __shmem_file_setup。

// file:mm/shmem.c
static struct file *__shmem_file_setup(struct vfsmount *mnt, const char *name, ...)
{
 ...
 // 申請(qǐng)一個(gè) inode
 inode = shmem_get_inode(mnt->mnt_sb, NULL, S_IFREG | S_IRWXUGO, 0,
    flags);
 inode->i_flags |= i_flags;
 inode->i_size = size;

 ...
 // 創(chuàng)建一個(gè)文件
 res = alloc_file_pseudo(inode, mnt, name, O_RDWR,
    &shmem_file_operations);
 return res;
}

我們都知道磁盤文件在內(nèi)核的實(shí)現(xiàn)中是由 inode 和 struct file 對(duì)象一起組成的。其實(shí)共享內(nèi)存文件也一樣,__shmem_file_setup 中就是先申請(qǐng)了一個(gè) inode,然后再調(diào)用 alloc_file_pseudo 創(chuàng)建一個(gè)文件。值得注意的是,這個(gè)文件并非是磁盤上的文件,而只是在內(nèi)存里的。

2.2 mmap申請(qǐng)內(nèi)存

mmap 也是一個(gè)系統(tǒng)調(diào)用,注意我們?cè)陂_(kāi)篇處調(diào)用它的時(shí)候傳入的第三個(gè) flag 參數(shù)是 MAP_SHARED。這表示的是要通過(guò) mmap 申請(qǐng)一塊跨進(jìn)程可共享的內(nèi)存出來(lái)。mmap 的實(shí)現(xiàn)入口在 arch/x86/kernel/sys_x86_64.c

//file:arch/x86/kernel/sys_x86_64.c
SYSCALL_DEFINE6(mmap, unsigned long, addr, ...)
{
 return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

接下來(lái)的這個(gè)函數(shù)的調(diào)用鏈路如下

SYSCALL_DEFINE6(mmap
-> ksys_mmap_pgoff
---> vm_mmap_pgoff
------> do_mmap_pgoff
--------> do_mmap

在 do_mmap 函數(shù)中,對(duì)輸入的 MAP_SHARED 進(jìn)行了處理。

//file:mm/mmap.c
unsigned long do_mmap(struct file *file, unsigned long addr,
   unsigned long len, unsigned long prot,
   unsigned long flags, vm_flags_t vm_flags,
   unsigned long pgoff, unsigned long *populate,
   struct list_head *uf)
{
 struct mm_struct * mm = current->mm;
 ...

 // 如果包含 MAP_SHARED,則對(duì)要申請(qǐng)的虛擬內(nèi)存設(shè)置一個(gè) VM_SHARED
 switch (flags & MAP_TYPE) {
  case MAP_SHARED:
  case MAP_SHARED_VALIDATE:
   vm_flags |= VM_SHARED | VM_MAYSHARE; 
   ... 
 } 
 ... 

 addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
 ......
}

如果 flag 包含了 MAP_SHARED,則對(duì)要申請(qǐng)的虛擬內(nèi)存設(shè)置一個(gè) VM_SHARED。該標(biāo)記指明的是要申請(qǐng)一個(gè)可以跨進(jìn)程共享的內(nèi)存塊。接下來(lái)進(jìn)入 mmap_region 中申請(qǐng)?zhí)摂M內(nèi)存。

//file:mm/mmap.c
unsigned long mmap_region(struct file *file, ...)
{
 struct mm_struct *mm = current->mm;
 ......

 // 申請(qǐng)?zhí)摂M內(nèi)存vma
 vma = vm_area_alloc(mm);

 // vma初始化
 vma->vm_start = addr;
 vma->vm_end = addr + len;
 vma->vm_flags = vm_flags;
 vma->vm_page_prot = vm_get_page_prot(vm_flags);
 vma->vm_pgoff = pgoff;
 ......

 // 加入到進(jìn)程的虛擬內(nèi)存 vma 鏈表中來(lái)
 vma_link(mm, vma, prev, rb_link, rb_parent);
}

進(jìn)程的虛擬內(nèi)存地址空間在內(nèi)核底層中就是由這樣一個(gè)個(gè)的 vma 來(lái)組成的。每一個(gè) vma 都聲明的是進(jìn)程虛擬地址中的某一段地址范圍已經(jīng)分配出去了。在 mmap_region 函數(shù)中申請(qǐng)了 vma,并在內(nèi)核中將其管理了起來(lái)。

這里注意我們?cè)谏暾?qǐng)共享內(nèi)存的時(shí)候,給 vma 是帶了 VM_SHARED 標(biāo)記的。帶了這個(gè)標(biāo)記的 vma和普通的虛擬內(nèi)存不一樣。后面在發(fā)生缺頁(yè)中斷申請(qǐng)物理內(nèi)存的時(shí)候,在不同的進(jìn)程間是可以對(duì)應(yīng)到同一塊物理內(nèi)存的。所以可以實(shí)現(xiàn)進(jìn)程間的共享。

圖片

所以真正讓進(jìn)程之間可以共享內(nèi)存的是這個(gè)帶 VM_SHARED 的 vma。

三、發(fā)送方發(fā)送文件句柄

發(fā)送方在使用 memfd_create 創(chuàng)建出來(lái)內(nèi)存文件,并用 mmap 為其申請(qǐng)可跨進(jìn)程共享的內(nèi)存后。接著就可以通過(guò) Unix Domain Socket 中對(duì)應(yīng)的 sendmsg 方法將這個(gè)共享內(nèi)存文件的句柄發(fā)送出來(lái)。如下是發(fā)送的代碼示例。

static void send_fd(int conn, int fd) {
    struct msghdr msgh;
    struct iovec iov;
    ...

    // 把文件句柄放到消息中來(lái)
    *((int *) CMSG_DATA(CMSG_FIRSTHDR(&msgh))) = fd;

    // 發(fā)送出去
    sendmsg(conn, &msgh, 0);
}

sendmsg 又是一個(gè)內(nèi)核提供的系統(tǒng)調(diào)用,它位于 net/socket.c 文件中。

//file:net/socket.c
SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
{
 return __sys_sendmsg(fd, msg, flags, true);
}

該函數(shù)的調(diào)用路徑如下

SYSCALL_DEFINE3(sendmsg, ...)
-> __sys_sendmsg
---> ___sys_sendmsg
-----> ____sys_sendmsg
-------> sock_sendmsg
---------> sock_sendmsg_nosec
-----------> unix_stream_sendmsg

在 unix_stream_sendmsg 中執(zhí)行了真正的發(fā)送。

//file:net/unix/af_unix.c 
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg, ...)
{
 // 把文件描述符指向的文件信息復(fù)制到 scm_cookie 中
 struct scm_cookie scm;
 scm_send(sock, msg, &scm, false);

 // 不斷構(gòu)建數(shù)據(jù)包發(fā)送,直到發(fā)送完畢
    while (sent < len) {
     // 申請(qǐng)一塊緩存區(qū)
     skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
        msg->msg_flags & MSG_DONTWAIT, &err,
        get_order(UNIX_SKB_FRAGS_SZ));

     // 拷貝數(shù)據(jù)到 skb
     err = unix_scm_to_skb(&scm, skb, !fds_sent);
     err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
     
     // 直接把 skb 放到對(duì)端的接收隊(duì)列中
     skb_queue_tail(&other->sk_receive_queue, skb);
  
  //發(fā)送完畢回調(diào)
  other->sk_data_ready(other);
  sent += size;
     ...
    }
}

在 unix_stream_sendmsg 中申請(qǐng)了個(gè) skb 緩存區(qū),然后把要發(fā)送的文件句柄等數(shù)據(jù)都塞到里面,最后調(diào)用 skb_queue_tail 直接把 skb 放到 Unix Domain Socket 連接另一端的接收隊(duì)列中了。

這里注意文件句柄只有在當(dāng)前進(jìn)程內(nèi)才是有意義的。如果直接發(fā)送 fd 出去,接收方是沒(méi)有辦法使用的。所以在 scm_send 函數(shù)中,重要的邏輯是把 fd 對(duì)應(yīng)的 struct file 的指針給找了出來(lái),放到待發(fā)送的數(shù)據(jù)里面了。只有 file 這種內(nèi)核級(jí)的對(duì)象接收方才能使用。

scm_send
-> __scm_send
---> scm_fp_copy

在 scm_fp_copy 中根據(jù) fd 把 file 給找了出來(lái)。它的指針會(huì)被放到發(fā)送數(shù)據(jù)中

//file:net/core/scm.c
static int scm_fp_copy(struct cmsghdr *cmsg, struct scm_fp_list **fplp)
{
 ...
 //把每一個(gè)要發(fā)送的 fd 對(duì)應(yīng)的 file 給找出來(lái)
 for (i=0; i< num; i++)
 {
  int fd = fdp[i];
  struct file *file;

  if (fd < 0 || !(file = fget_raw(fd)))
   return -EBADF;
  *fpp++ = file;
  fpl->count++;
 }
}

四、接收方接收文件

接下來(lái)接收方就可以通過(guò) recvmsg 來(lái)接收發(fā)送方發(fā)送過(guò)來(lái)的文件了。recvmsg 系統(tǒng)會(huì)調(diào)用到 unix_stream_read_generic 中,然后在這個(gè)函數(shù)中把 skb 給取出來(lái)。

圖片

下面是接收函數(shù)核心 unix_stream_read_generic 的源碼。

//file:net/unix/af_unix.c
static int unix_stream_read_generic(struct unix_stream_read_state *state,
        bool freezable)
{
 do {
  // 拿出一個(gè) skb
  last = skb = skb_peek(&sk->sk_receive_queue);
  ...
 }
 ...
 if (state->msg)
  scm_recv(sock, state->msg, &scm, flags);
 return copied ? : err;
}

在 skb 拿出來(lái)后,還需要調(diào)用 scm_recv 來(lái)把 skb 中包含的文件給找出來(lái)。在 scm_recv 中調(diào)用 scm_detach_fds。

//file:net/core/scm.c
void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
{

 for (i = 0; i < fdmax; i++) {
  err = receive_fd_user(scm->fp->fp[i], cmsg_data + i, o_flags);
  if (err < 0)
   break;
 }
 ...
}

在 scm->fp->fp[i] 中包含的是發(fā)送方發(fā)送過(guò)來(lái)的 struct file 指針。這樣文件就取出來(lái)了。當(dāng)然 struct file 是個(gè)內(nèi)核態(tài)的對(duì)象,用戶沒(méi)有辦法使用。所以還需要再為其在新的進(jìn)程中申請(qǐng)一個(gè)文件句柄,然后返回。本文來(lái)自公眾號(hào)「開(kāi)發(fā)內(nèi)功修煉」。

//file:fs/file.c
int __receive_fd(struct file *file, int __user *ufd, unsigned int o_flags)
{
 //申請(qǐng)一個(gè)新的文件描述符
 new_fd = get_unused_fd_flags(o_flags);
 ...

 //關(guān)聯(lián)文件
 fd_install(new_fd, get_file(file));
 return new_fd;
}

五、總結(jié)

共享內(nèi)存發(fā)送方進(jìn)程的開(kāi)發(fā)過(guò)程基本分 memfd_create 創(chuàng)建內(nèi)存文件、mmap 申請(qǐng)共享內(nèi)存、Unix Domain Socket 發(fā)送文件句柄三步。

  • 第一步,memfd_create 系統(tǒng)調(diào)用的主要邏輯有兩個(gè),一是調(diào)用 get_unused_fd_flags 申請(qǐng)一個(gè)沒(méi)使用過(guò)的文件句柄,二是調(diào)用 shmem_file_setup 創(chuàng)建一個(gè)共享內(nèi)存文件。
  • 第二步,mmap 系統(tǒng)調(diào)用在調(diào)用它的時(shí)候傳入的第三個(gè) flag 參數(shù)是 MAP_SHARED,該參數(shù)是申請(qǐng)一塊跨進(jìn)程可共享訪問(wèn)的物理內(nèi)存。
  • 第三步,接著通過(guò) Unix Domain Socket 中對(duì)應(yīng)的 sendmsg 方法將這個(gè)共享內(nèi)存文件的句柄發(fā)送出去。在發(fā)送時(shí),把文件句柄對(duì)應(yīng)的 struct file 指針找到并放到要封裝的 skb 數(shù)據(jù)包中了。

接收方進(jìn)程的主要實(shí)現(xiàn)原理是 recvmsg 系統(tǒng)調(diào)用。在這個(gè)系統(tǒng)調(diào)用中,內(nèi)核會(huì)把發(fā)送方發(fā)送過(guò)來(lái)的 struct file 指針取出來(lái),然后再在當(dāng)前進(jìn)程下為其申請(qǐng)一個(gè)新的文件句柄。這個(gè)文件句柄返回給用戶進(jìn)程后,用戶進(jìn)程就可以用它來(lái)和另外一個(gè)進(jìn)程共享地訪問(wèn)同一塊內(nèi)存了。

總體來(lái)看,共享內(nèi)存本質(zhì)上共享的是內(nèi)核對(duì)象 struct file,通過(guò)在不同的進(jìn)程之間使用同一個(gè) struct file 來(lái)實(shí)現(xiàn)的共享。當(dāng)然也得需要在虛擬內(nèi)存對(duì)象 vma 帶上 VM_SHARED 標(biāo)記來(lái)支持。


文章名稱:聊聊跨進(jìn)程共享內(nèi)存的內(nèi)部工作原理
本文地址:http://www.5511xx.com/article/ccshigh.html