天天看點

程序間通信-記憶體映射的原理與共享記憶體子程序與父程序的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程序尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

文章目錄

  • 子程序與父程序的繼承
  • 寫時複制,fork,vfork與線程在Linux下的實作
      • 寫時複制
      • fork
      • vfork
      • 線程在Linux下的實作
  • 存儲映射I/O
      • 執行個體:使用mmap函數實作cp
  • 共享記憶體
      • 概述與特點
  • 核心怎樣保證各個程序尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)
      • 頁緩存
          • 緩存的回收
      • struct address_space對象 :用于管理實體檔案(struct inode)映射到記憶體的頁面(struct page)的
      • 重要
      • 與普通IPC的對比
      • 注意事項
      • 執行個體

子程序與父程序的繼承

子程序繼承父程序的

使用者号UIDs和使用者組号GIDs

環境Environment

堆棧

共享記憶體

打開檔案的描述符

執行時關閉(Close-on-exec)标志

信号處理程式

存儲映射區

程序組号

目前工作目錄

根目錄

檔案方式建立屏蔽字

資源限制

控制終端

子程序獨有的

程序号PID

不同的父程序号

自己的檔案描述符和目錄流的拷貝

子程序不繼承父程序的程序正文(text),資料和其他鎖定記憶體(memory locks)

不繼承異步輸入和輸出

父程序和子程序擁有獨立的位址空間和PID參數

寫時複制,fork,vfork與線程在Linux下的實作

寫時複制

fork()->exec() 讀取可執行檔案并将其載入位址空間然後運作,這樣的話,頁根本就沒有被寫入,就沒必要進行實際上的複制了

fork 開銷

:複制頁表,建立PID.

fork

forkj,vfor,__clone

->

clone

->

do_fork

->

copy_process

copy_process

  1. dup_task_struct建立核心棧,此時父PID與子PID相同
  2. 父子分離,清零與初始化
  3. 保證不會投入運作
  4. allloc_pid 配置設定新的PID
  5. clone_flags

    繼承該繼承的,線程的話就是共享該共享的
  6. 回到

    do_fork

    函數,喚醒子程序并運作他,這是為了避免其寫入(見虛拟記憶體管理的文章)

注意幾乎是子程序先執行

vfork

不拷貝父程序的頁表項,其餘與fork相同

線程在Linux下的實作

就是一個程序,有自己普通的

task_struct

,隻不過共享了相同的資源而已

clone_flags

共享位址空間,檔案系統資源,fds,信号處理程式

存儲映射I/O

Linux通過将一個虛拟記憶體區域(也在磁盤)與一個硬碟上的檔案關聯起來,以初始化這個虛拟記憶體區域的内容.于是當從緩沖區讀取資料的時候,就相當于讀取檔案,向緩沖區寫入資料就會自動寫入檔案,這樣的話,就不用使用read/write執行I/O了!!!

主要使用函數:

#include <sys/mman.h>

       void *mmap(void *addr, size_t len, int prot, int flags,
           int fildes, off_t off);
           
           

具體見

man

文檔.

必須先打開該檔案.若檔案是隻讀打開的,就不能設定

PROT_WRITE

flags表示是共享對象還是匿名對象還是私有對象:

  • MAP_SHARED  共享對象,提供了POSIX共享記憶體,對其操作就相當于修改檔案
  • MAP_PRIVATE  私有對象。對該記憶體段的修改不會反映到映射檔案(寫時複制)
  • MAP_ANNO   匿名對象

檔案的長度盡量與頁的長度相同

程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

有關的信号是

SIGSEGV

SIGBUS

.前者一般是表示權限,後者表示通路的某個部分不存在.

更改權限:

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);//addr必須是系統頁長的整數倍
           

強制性寫回映射檔案:

解除映射:

在解除後,對

MAP_PRIVATE

的修改會被丢棄.

執行個體:使用mmap函數實作cp

#include <fcntl.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

using namespace std;

#define SS (1024 * 1024 * 1024)

void err(const char *str)
{
    perror(str);
    exit(-1);
}
int main(int argc, char *argv[])
{
    int fdin = open(argv[1], O_RDONLY);
    if (fdin < 0)
        err("open");
    int fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC,0666);
    if (fdout < 0)
        err("open");
    struct stat sbuf;
    if (fstat(fdin, &sbuf) < 0)
        err("fstat");
    //如果不設定,對虛拟存儲區的第一次通路就會産生 SIGBUS 信号
    if (ftruncate(fdout, sbuf.st_size) < 0)
        err("ftruncate");

    off_t fsz = 0;
    size_t copysz;

    while (fsz < sbuf.st_size)
    {
        if ((sbuf.st_size - fsz) > SS)
            copysz = SS;
        else
            copysz = sbuf.st_size - fsz;

        void *src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz);
        if (src == MAP_FAILED)
            err("11122");

        void *dst = mmap(0, copysz, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, fsz);
        if (dst == MAP_FAILED)
            err("11122");
        memcpy(dst, src, copysz);
        munmap(src, copysz);
        munmap(dst, copysz);
        fsz += copysz;
    }
    exit(0);
}
           

共享記憶體

概述與特點

共享記憶體是程序間通信中最快的方式

。共享記憶體允許兩個或更多程序通路同一塊記憶體,就如同 malloc() 函數向不同程序傳回了指向同一個實體記憶體區域的指針。當一個程序改變了這塊位址中的内容的時候,其它程序都會察覺到這個更改。

實際上,程序之間在共享記憶體時,并不總是讀寫少量資料後就解除映射,有新的通信時,再重建立立共享記憶體區域。而是

保持共享區域,直到通信完畢為止

,這樣,

資料内容一直儲存在共享記憶體中,并沒有寫回檔案。共享記憶體中的内容往往是在解除映射時才寫回檔案的

。是以,采用共享記憶體的通信方式效率是非常高的。

Linux的2.2.x核心支援多種共享記憶體方式,如mmap()系統調用,Posix共享記憶體,以及System V 共享記憶體。

程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

核心怎樣保證各個程序尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

頁緩存

緩存的是記憶體頁面,但是緩存中的頁來自對檔案的讀寫,是以還不如直接看作是對于最近通路過得檔案的資料塊的一種緩存!!!

,避免對磁盤開銷大的通路(局部性原理)

OS會根據記憶體使用情況,動态調整!!!

read()->頁緩存(實體塊的一部分(幾頁大小))->磁盤

write的操作:
  • 使得緩存失效,直接寫到磁盤
  • 更新緩存+更新磁盤
  • 寫回:見緩存的文章(目前使用的政策)
緩存的回收

Linux核心用的是改良版的

LRU

,兩個連結清單,一個活躍連結清單,一個非活躍連結清單,實作LRU的算法政策見另一篇文章

struct address_space對象 :用于管理實體檔案(struct inode)映射到記憶體的頁面(struct page)的

頁緩存中的頁可能包含了多個不連續的實體磁盤塊.現在讓我們來思考一個問題:就下面的這個結構,如何找到對應的實體塊磁盤塊???

(頁大小4k,塊大小512KB)

程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

OK,這個就是我們要講的這個結構的作用了.

假設有程序建立了多個 vm_area_struct 都指向同一個檔案,那麼這個 vm_area_struct 對應的頁高速緩存隻有一份。也就是磁盤上的檔案緩存到記憶體後,它的虛拟記憶體位址可以有多個,但是實體記憶體位址卻隻能有一個。

程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

i_map

幫助核心找到關聯的被緩存的檔案

find_get_page( address_space ,偏移量)

進而找到

重要

所有的I/O操作必然都是通過頁緩存來進行的.

 運作機制和緩存的運作機制如出一轍!!!

程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)

address_space_operations

就是用來操作該檔案映射到記憶體的頁面,比如把記憶體中的修改寫回檔案、從檔案中讀入資料到頁面緩沖等

  1. page cache及swap cache中頁面的區分:一個被通路檔案的實體頁面都駐留在page cache或swap cache中,一個頁面的所有資訊由struct page來描述。struct page中有一個域為指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。
  2. 檔案與address_space結構的對應:一個具體的檔案在打開後,核心會在記憶體中為之建立一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個檔案就對應一個address_space結構,一個address_space與一個偏移量能夠确定一個page cache 或swap cache中的一個頁面。是以,當要尋址某個資料時,很容易根據給定的檔案及資料在檔案内的偏移量而找到相應的頁面。
程式間通信-記憶體映射的原理與共享記憶體子程式與父程式的繼承寫時複制,fork,vfork與線程在Linux下的實作存儲映射I/O共享記憶體核心怎樣保證各個程式尋址到同一個共享記憶體區域的記憶體頁面(深入了解共享記憶體,重點)
  1. 程序調用mmap()時,隻是在程序空間内新增了一塊相應大小的緩沖區,并設定了相應的通路辨別,但并沒有建立程序空間到實體頁面的映射。是以,第一次通路該空間時,會引發一個缺頁異常。
  2. 對于共享記憶體映射(普通檔案,MAP_SHARED)情況,缺頁異常處理程式首先在swap cache中尋找目标頁(符合address_space以及偏移量的實體頁),如果找到,則直接傳回位址;如果沒有找到,則判斷該頁是否在交換區(swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程式将配置設定新的實體頁面,并把它插入到page cache中。程序最終将更新程序頁表。

共享對象(MAP_SHARED)->swap cache ->找到傳回,沒找到去swap 分區找->如果在,換入->如果不在,就配新的實體頁面,更頁表,查page cache

注:對于映射普通檔案情況(匿名對象),缺頁異常處理程式首先會在page cache中根據address_space以及資料偏移量尋找相應的頁面。如果沒有找到,則說明檔案資料還沒有讀入記憶體,處理程式會從磁盤讀入相應的頁面,并傳回相應位址,同時,程序頁表也會更新。

表示很大的質疑!!!!!

  1. 所有程序在映射同一個共享記憶體區域時,情況都一樣,在建立線性位址與實體位址之間的映射之後,不論程序各自的傳回位址如何,實際通路的必然是同一個共享記憶體區域對應的實體頁面。

    注:一個共享記憶體區域可以看作是特殊檔案系統shm中的一個檔案,shm的安裝點在交換區上。

與普通IPC的對比

以下面“程序A從檔案f中讀取資料,進行加工之後,将資料傳遞給程序B”這種場景為例,若使用其他的IPC形式,我們至少需要以下步驟:

1. 從檔案f中複制資料到程序A的記憶體中;
2. 加工資料;
3. 将加工好的資料通過系統調用拷貝到核心空間中;
4. 程序B得知有資料發來,從核心空間将加工好的資料拷貝到程序B的記憶體中;
5. 程序B使用資料
           

而我們若使用共享記憶體,則至少需要以下三個步驟:

1. 從檔案f中複制資料到共享記憶體區域中;
2. 加工資料;
3. 程序B使用資料
           

注意事項

  • 多個程序之間對一個給定存儲區通路的互斥

    。若一個程序正在向共享記憶體區寫資料,則在它做完這一步操作前,别的程序不應當去讀、寫這些資料。(一般使用

    信号量

    去進行同步)
  • 當再也沒有程序需要使用這個共享記憶體塊的時候,必須有一個(且隻能是一個)程序負責釋放這個被共享的記憶體頁面。
System V 的大緻實作

共享記憶體實際上就是程序通過調用

shmget(Shared Memory GET 擷取共享記憶體)來配置設定一個共享記憶體塊

,然後

每個程序通過shmat(Shared Memory Attach 綁定到共享記憶體塊),将程序的虛拟位址空間指向共享記憶體塊中

。 随後需要通路這個共享記憶體塊的程序都必須将這個共享記憶體綁定到自己的位址空間中去。當一個程序往一個共享記憶體快中寫入了資料,共享這個記憶體區域的所有程序就可用都看到其中的内容。

執行個體

還是建立兩個程序,在 A 程序中建立一個共享記憶體,并向其寫入資料,通過 B 程序從共享記憶體中讀取資料。

writer.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
using namespace std;

const int BUFSIZE = 512;

int main(void)
{
    //1. 建立 key
    key_t key = ftok("/dev/shm/myshm2", 2016);
    cout << key << endl;
    if (key == -1)
    {
        perror("ftok ");
    }
    //2.建立共享記憶體
    int shmid = shmget(key, BUFSIZE, IPC_CREAT | 0666);
    //3.映射到這塊共享記憶體
    void *shmaddr = shmat(shmid, NULL, 0);

    //4. 寫入資料到共享記憶體
    printf("start   writing !!!!!\n");
    bzero(shmaddr, BUFSIZE); //清空共享記憶體 
    strcpy((char *)shmaddr, "劉生玺最帥@@@@@@\n");

    return 0;
}
           

reader.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <iostream>
using namespace std;

const int BUFSIZE = 512;

int main(void)
{
    //1. 建立 key
    key_t key = ftok("/dev/shm/myshm2", 2016);
    cout << key << endl ;
    if (key == -1)
    {
        perror("ftok ");
    }

    system("ipcs -m"); //檢視共享記憶體

    //2.找到對應的共享記憶體
    int shmid = shmget(key, BUFSIZE, IPC_CREAT | 0666);

    //3.映射到這塊共享記憶體
    void *shmaddr = shmat(shmid, NULL, 0);

    //4. 讀共享記憶體區資料
    printf("data = [%s]\n", shmaddr);

    //5.分離共享記憶體和目前程序
    int ret = shmdt(shmaddr);
    if (ret < 0)
    {
        perror("shmdt");
        exit(1);
    }
    else
    {
        printf("deleted shared-memory\n");
    }

    //删除共享記憶體
    shmctl(shmid, IPC_RMID, NULL); //IPC_RMID:删除。(常用 )

    system("ipcs -m"); //檢視共享記憶體

    return 0;
}
           

繼續閱讀