1. 程式人生 > 實用技巧 >《Linux/UNIX系統程式設計手冊》第49章 記憶體對映【轉】

《Linux/UNIX系統程式設計手冊》第49章 記憶體對映【轉】

轉自:https://www.cnblogs.com/arnoldlu/p/12550993.html

關鍵詞:mmap()、munmap()、msync()、SIGSEGV、SIGBUS、MAP_NORESERVE、MAP_FIXED、mremap()、remap_file_pages()等等。

1. 概述

mmap()系統呼叫在呼叫程序的虛擬地址空間中建立一個新記憶體對映。對映分為兩種:

  • 檔案對映:將一個檔案的一部分直接對映到呼叫程序的虛擬記憶體中。一旦一個檔案被對映之後就可以通過在相應的記憶體區域中操作位元組來訪問檔案內容了。對映的分頁會在需要的時候從檔案中載入。
  • 匿名對映:一個匿名對映沒有對應的檔案,這種對映的分頁會被初始化為0。

一個程序的對映中的記憶體可以與其他程序中的對映共享:

  • 當兩個程序映射了一個檔案的同一個區域時他們會共享實體記憶體的相同分頁。
  • 通過fork()建立的子程序會繼承其父程序的對映的副本,並且這些對映所引用的實體記憶體分頁與父程序中相應對映所引用的分頁相同。

關於私有對映和共享對映:

  • 私有對映(MAP_PRIVATE):在對映內容上發生的變更對其他程序不可見,對於檔案對映來講,變更將不會在底層檔案上進行。初始時是共享的,但對影射內容所做出的變更對各個程序來講則是私有的。核心使用了寫時複製完成這個任務,當一個程序試圖修改一個分頁的內容是,核心首先會為該程序建立一個新分頁並將需修改的分頁中的內容複製到新分頁中。
  • 共享對映(MAP_SHARED):在對映內容上發生的變更對所有共享同一個對映的其他程序都可見,對檔案對映來講,變更將發生在底層的檔案上。

以上四種不同記憶體對映的建立和使用方式如下:

  • 私有檔案對映:對映的內容被初始化為一個檔案區域中的內容。多個對映同一個檔案的的程序初始時會共享同樣的記憶體物理分頁,單系統使用寫時複製使得一個程序對對映所做出的變更對其他程序不可見。主要用途是使用一個檔案的內容來初始化一塊記憶體區域。常見的例子包括根據二進位制可執行檔案或共享庫檔案的相應部分來初始化一個程序的文字和資料段。
  • 私有匿名對映:每次呼叫mmap()建立一個私有匿名對映時都會產生一個新對映,該對映與同一或不同程序建立的其他匿名對映是不同的,既不會共享實體記憶體分頁。私有匿名對映的主要用途是為一個程序分配新用零填充記憶體。
  • 共享檔案對映:所有對映一個檔案的同一區域的程序會共享同樣的記憶體物理分頁,這些分頁的內容會被初始化為該檔案區域。對對映內容的修改將直接在檔案中進行。
  • 共享匿名對映:每次呼叫mmap()建立一個共享匿名對映時都會產生一個新的、與任何其他對映不共享分頁的截然不同的對映。和私有匿名對映區別在於對映的分頁不會被寫時複製。for()的子程序繼承對映,父子程序共享同樣的RAM分頁,並且一個程序對應攝內容的變更會對其他程序可見。

一個程序在執行exec()時對映會丟失,但通過fork()的子程序會繼承對映,對映型別(MAP_PRIVATE或MAP_SHARED)也會被繼承。

2. 建立一個對映:mmap()

mmap()系統呼叫在程序的虛擬地址空間中建立一個新對映。

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    Returns starting address of mapping on success, or MAP_FAILED on error

addr:指定了對映被放置的虛擬地址。如果addr為NULL,那麼核心會為對映選擇一個合適的地址。

length:指定了對映的位元組數。核心會以分頁大小為單位來建立對映,實際上length會被向上提升為分頁大小的下一個倍數。

prot:是一個位掩碼,指定了施加於應設定上的保護資訊,其取值要麼是PROT_NONE,要麼是另三個標記的組合。

flags:是一個控制對映操作各個方面選項的位掩碼。

MAP_PRIVATE:建立一個私有對映。區域中內容上所發生的變更對使用同一對映的其他程序是不可見的,對於檔案對映來講,所發生的的變更將不會反應在底層檔案上。

MAP_SHARED:建立一個共享對映。區域中內容上所發生的變更對使用MAP_SHARED特性對映同一區域的程序是可見的,對於檔案對映來講,所發生的變更將直接反應在底層檔案上。

fd和offset:用於檔案對映,fd標識被對映檔案的檔案描述符;offset指定對映在檔案中的起點,它必須是系統分頁大小的倍數。

成功時mmap()會返回新對映的起始地址,錯誤是mmap()會返回MAP_FAILED。MAP_FAILED等同於((void *)-1)。

下面的示例通過mmap()將一個檔案對映,然後將其內容輸出到STDOUT。

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

int
main(int argc, char *argv[])
{
    char *addr;
    int fd;
    struct stat sb;

    if (argc != 2 || strcmp(argv[1], "--help") == 0)
        printf("%s file\n", argv[0]);

    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        printf("open failed");
        return -1;
    }

    if (fstat(fd, &sb) == -1) {
        printf("fstat failed");
        return -1;
    }

    if (sb.st_size == 0)
        exit(EXIT_SUCCESS);

    addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);-------將檔案控制代碼fd全部內容以Read、Private對映到addr起始的對映區域。
    if (addr == MAP_FAILED) {
        printf("mmap failed");
        return -1;
    }

    if (write(STDOUT_FILENO, addr, sb.st_size) != sb.st_size) {---------將對映區域全部內容輸出到STDOUT。
        printf("partial/failed write");
        return -1;
    }

    exit(EXIT_SUCCESS);
}

3. 解除對映區域:munmap()

munmap()系統調研執行與mmap()相反操作,即從呼叫程序的虛擬地址空間中刪除一個對映。

#include <sys/mman.h>
int munmap(void *addr, size_t length);
    Returns 0 on success, or –1 on error

addr:是待解除對映的地址範圍的起始地址。

length:是一個非負整數,指定了待解除對映區域的大小。範圍為系統分割槽頁大小的下一個倍數的地址空間將會被解除對映。

當一個程序終止或執行了一個exec()之後程序中所有的對映會自動被解除。

為確保一個共享檔案對映的內容會被寫入到底層檔案中,在使用munmap()解除一個對映之前需要呼叫msync()。

4. 檔案對映

建立一個檔案對映的步驟:

1. 獲取檔案的一個描述符,通常通過open()來完成。

2. 將檔案描述符作為fd引數傳入mmap()呼叫。

即可將開啟檔案的內容對映到呼叫程序的地址空間中,即使檔案被關閉,也不會對對映產生影響。

fd引用檔案時必須要具備與prot和flags引數值匹配的許可權。

offset引數指定了從檔案區域中的哪個位元組開始對映,他必須是系統分頁大小的倍數。offset指定為0則從檔案的起始位置開始對映。

length引數指定了對映的位元組數。

4.1 私有檔案對映

私有檔案對映用途:

  • 允許多個執行同一個程式或使用同一個共享庫的程序共享同樣的文字段,它是從底層可執行檔案或庫檔案的相應部分對映而來的。(儘管可執行檔案文字段只允許讀取和執行訪問,但在被對映時仍然使用了MAP_PRIVATE,這是因為偵錯程式或自修改的程式能夠修改程式文字,而這樣的變更不應該發生在底層檔案上或影響其他程序。)
  • 應設一個可執行檔案或共享庫的初始化資料段。這種對映會被處理成私有是的對對映資料段內容的變更不會發生在底層檔案上。

mmap()的這種用法通常對程式是不可見的,這些對映是由程式載入器和動態連結器建立的。

4.2 共享檔案對映

多個程序建立了同一個檔案區域的共享對映時,它們會共享同樣的實體記憶體分頁。

共享檔案對映存在兩個用途:記憶體對映I/O和IPC。

記憶體對映I/O

用於共享檔案對映中的內容是從檔案初始化而來的,並且對對映內容所做出的變更都會自動反映到檔案上,因此可以簡單地通過訪問記憶體中的位元組來執行檔案I/O,而依靠核心來確保對記憶體的變更會被傳遞到對映檔案中。

記憶體對映I/O相對於read()/write()優勢

記憶體對映I/O具備兩個潛在的優勢:使用記憶體訪問來取代read()和write()系統呼叫能夠簡化一些應用程式邏輯;在一些情況下,它能夠比使用傳統的I/O系統呼叫執行檔案I/O這種做法提供更好的效能。

記憶體對映I/O為什麼會具有優勢?

正常的read()或者write()需要兩次傳輸:一次是在檔案和核心高速緩衝區之間,另一次是在高速緩衝區和使用者空間緩衝區之間。使用mmap()則無需二次傳輸,對於輸入來講,一旦核心將相應檔案塊對映金記憶體之後,使用者即可使用這些資料;對於輸出來講,使用者程序僅需要修改記憶體中內容,然後可依靠核心記憶體管理器來自動更新底層檔案。

mmap()還能夠減少所需使用記憶體來提升效能。當使用read()或write()時,資料將被儲存在兩個緩衝區中:一個位於使用者空間,一個位於核心空間。當使用mmap()時,核心空間和使用者空間會共享一個緩衝區。多個程序正在同一個檔案上執行I/O,那麼他們通過使用mmap()就能夠共享通過一個核心緩衝區,從而又能夠節省記憶體消耗。

記憶體對映I/O有什麼劣勢?

對於小資料量I/O來講,記憶體對映I/O開銷,即對映、缺頁故障、講出對映以及更新硬體記憶體管理單元的超前轉換緩衝器,實際上比簡單的read()或write()大。

有時候核心難以高效地處理可寫入對映的回寫,需要藉助msync()或sync_file_range()有助於提高效率。

使用共享檔案對映的IPC

由於所有使用同樣檔案區域的共享對映的程序共享同樣的記憶體物理分頁,因此共享檔案對映第二個用途是作為一種IPC方法。

使用共享檔案對映IPC和System V共享記憶體物件之間區別在於區域中內容上的變更會反應到對映檔案上。

4.3 邊界情況

通常情況下,一個對映的大小是系統分頁大小整數倍,並且對映會完全落入對映檔案的範圍之內。

對映完全落入對映檔案範圍之內,但區域大小不是系統分頁大小整數倍

假設系統分頁大小為4096位元組,被對映檔案大小為9500位元組,將檔案首6000位元組對映。

  • 要求對映的6000位元組,會被對齊到8192位元組。
  • 記憶體實際可以訪問範圍為0~8091,並且對齊修改會落實到實際檔案中。
  • 檔案8192~9499區域沒有被對映,超出8191記憶體訪問會產生SIGSEGV異常。

擴充底層檔案結尾對映

假設系統分頁大小為4096位元組,被對映檔案大小為2200位元組,mmap()長度為8192。

  • 要求對映的8192位元組,被分為三部分:0~2199 - 對映到檔案的可訪問部分,2200~4095 - 沒有對映到檔案的可訪問部分,4096~8191 - 不可訪問部分。
  • 對映到檔案的可訪問部分,變更會更新到底層檔案;沒有對映到檔案的可訪問部分,被初始化為0,不會被對映到底層檔案上,也不會與對映同一個檔案的其他程序共享。
  • 4096~8191範圍的不可訪問部分,對齊訪問會產生SIGBUS異常。
  • 對8192~之後的地址訪問,同樣產生SIGSEGV異常。

4.4 記憶體保護和檔案訪問模式互動

一般來講,PROT_READ和PROT_EXEC保護要求被影射的檔案使用O_RDONLY或O_RDWR開啟,而PROT_WRITE保護要求被對映的檔案使用O_WRONLY或O_RDWR開啟。

5. 同步對映區域:msync()

核心會自動將發生在MAP_SHARED對映內容上的變更寫入到底層檔案中,但是不保證這種操作會在何時發生。

msync()系統呼叫讓應用程式能夠顯式控制何時完成共享對映和對映檔案之間的同步。

呼叫msync()還允許一個 應用程式確保在可寫入對映上發生的更新會對在該檔案上執行read()的其他程序可見。

#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
    Returns 0 on success, or –1 on error

addr指定的地址必須是分頁對齊的。

length會被向上攝入到系統分頁大小下一個整數倍。

flags指定msync()同步方式。

MS_SYNC執行一個同步檔案寫入。這個調動會阻塞知道記憶體區域中所有被修改過的分頁被寫入到檔案為止。MS_ASYNC執行一個非同步檔案寫入。變更會在後面某個時刻被寫入磁碟並立即對在相應檔案區域中執行read()的其他程序可見。

MS_SYNC操作之後,記憶體區域會與磁碟同步;MS_ASYNC操作之後,記憶體區域僅僅是與核心高速緩衝區同步。

MS_INVALIDATE使對映資料的快取副本失效。當記憶體區域中所有被修改過的分頁被同步到檔案中之後,記憶體區域中所有與底層檔案不一致的分頁會被標記為無效。

6. 其他mmap()標記

7. 匿名對映

MAP_ANONYMOUS和/dev/zero

在Linux上,使用mmap()建立匿名對映存在樑紅不同但等價方法:

  • flags指定MAP_ANONYMOUS並將fd指定為-1。
  • 開啟/dev/zero裝置檔案並將得到的檔案描述符傳遞給mmap()。

這兩種對映得到的位元組都會被初始化Wie0,並且offset都會被忽略。

MAP_PRIVATE匿名對映

MAP_SHARED匿名對映用來分配程序私有的記憶體塊並將其中的內容初始化為0.

MAP_SHARED匿名對映

MAP_SHARED匿名對映允許相關程序共享一塊記憶體區域而無需一個對應的對映檔案。

8. 重新對映一個對映區域:mremap()

mreamap()系統個呼叫用來執行對映區域大小變更。

#define _GNU_SOURCE
#include <sys/mman.h>
void *mremap(void *old_address, size_t old_size, size_t new_size, int flags, ...);
    Returns starting address of remapped region on success, or MAP_FAILED on error

old_address和old_size指定了需擴充套件或收縮的既有對映的位置和大小。old_address通常是一個由之前mmap()呼叫返回的值。

new_size是對映預期的新大小。

在執行重對映的過程中核心可能會為對映在程序的虛擬地址空間中重新指定一個位置,是否允許這種行為是由flags引數控制。flags要麼是0,要麼包含下列幾個值。

MREMAP_MAYMOVE:如果指定了標記,核心可能會為對映在程序的虛擬地址空間中重新指定一個位置。如果沒有指定,並且在當前位置處沒有足夠的空間來擴充套件這個對映,那麼就返回ENOMEM錯誤。

MREMAP_FIXED:只能和MAP_MAYMOVE一起使用。如果指定了標記,那麼mremap()會接收一個額外引數void *new_address,該引數指定了一個分頁對齊的地址,並且對映將會被遷移至該地址處。

mremap()在成功時會返回對映的起始地址。由於這個地址可能有變化,從而導致指向這個區域中的指標可能會變得無效,因此使用mremap()的應用程式在引用對映區域中的地址是應該只使用偏移量。

9. MAP_NORESERVE和過度利用交換空間

核心如何處理交換空間的預留是由呼叫mmap()時是否使用了MAP_NORESERVE標記以及影響系統層面的交換空間過度利用操作的/proc介面來控制的。

/proc/sys/vm/overcommit_memory包含一個整數值:0表示拒絕明顯的過度利用;1表示所有情況下都允許過度利用;2表示採用嚴格的過度利用。

overcommit_memory為2情況下,核心會在所有mmap()分配上執行嚴格的幾張並將系統中此類分配的總量控制在小於等於:

overcommit_ratio是一個整數-用百分比表示-它位於/proc/sys/vm/overcommit_ratio檔案中。這個檔案包含預設值是50,表示核心最多可分配的空間為系統RAM總量的50%。

過度利用監控只適用於下面對映:

私有可寫對映,這種對映的交換開銷等於所有使用該對映的程序為該對映所分配的空間總和。

共享匿名對映,這種對映的交換開銷等於對映的大小。

10. MAP_FIXED標記

mmap()的flags引數MAP_FIXED標記會強制核心原樣地解釋addr中的地址,addr必須是分頁對齊的。

如果在呼叫mmap()時指定了MAP_FIXED,並且記憶體區域的起始位置為addr,覆蓋的length位元組與之前的對映的分頁重疊了,那麼重疊的分頁會被新對映取代。使用這個特性可以可移植地將一個檔案的多個部分對映進一塊連續的記憶體區域。

  1. 使用mmap()建立一個匿名對映,在mmap()呼叫將addr指定為NULL並不指定MAP_FIXED標記。
  2. 使用一系列指定了MAP_FIXED標記的mmap()呼叫來將檔案區域對映進在上一步建立的對映的不同部分中。

11. 非線性對映:remap_file_pages()

使用mmap()建立的檔案對映是連續的,對映檔案的分頁與記憶體區域的分頁存在一個順序的、一對一的對應關係。

非線性對映檔案分頁的順序與它們在連續記憶體中出現的順序不同的對映。

remap_file_pages()系統呼叫在無需建立多個vma情況下建立非線性對映:

  • 使用mmap()建立一個對映。
  • 使用一個或多個remap_file_pages()呼叫來調整記憶體分頁和檔案分頁之間的對應關係。
#define _GNU_SOURCE
#include <sys/mman.h>
int remap_file_pages(void *addr, size_t size, int prot, size_t pgoff, int flags);
    Returns 0 on success, or –1 on error

pgoff指定了檔案區域的起始位置,其單位是系統分頁大小。

size引數指定了檔案區域的長度,其單位為位元組。

addr引數起兩個作用,它標識了分頁需要調整的既有對映。addr必須是一個位於之前通過mmap()對映的區域中的地址;指定了通過pgoff和size標識出的檔案分頁所處的記憶體地址。

addr和size都應該是系統分頁大小的整數倍。

prot引數會被忽略,其值必須是0.

flags引數當前未被使用。

remap_file_pages()僅適用於共享對映。

12. 總結

mmap()系統呼叫在呼叫程序的虛擬地址空間中建立一個新記憶體對映。munmap()系統呼叫執行你操作,僅從程序的地址空間中刪除一個對映。

對映可以分為兩種:基於檔案的對映和匿名對映。檔案對映將一個檔案區域中的內容對映到程序的虛擬地址空間中。匿名對映並沒有對應的穩健趨於,該對映中的位元組會被初始化為0.

對映既可以是私有的,也可以是共享的。對檔案對映來講,這種差別確定了核心是否會將對映內容上發生的變更傳遞到底層檔案上。使用MAP_PRIVATE,對映內容上發生的變更對其他程序是不可見的,也不會反應到對映檔案上。MAP_SHARED檔案對映的做法則相反,在對映上發生的變更對其他程序可見並且會反應到對映檔案上。

msync()系統呼叫顯式地控制一個對映的內容何時與對映檔案進行同步。

記憶體對映用途

  • 分配程序私有的記憶體(私有匿名記憶體)
  • 對一個程序的文字段和初始化資料段中的內容進行初始化(私有檔案對映)
  • 通過fork()關聯起來的程序之間的共享記憶體(共享匿名對映)
  • 執行記憶體對映I/O,還可以將其與無關程序之間的記憶體共享結合起來(共享檔案對映)

兩個訊號:SIGSEGV和SIGBUS

如果在對映時違反了應設定上的保護規則(或訪問一個當前未被對映的地址),那麼就會產生一個SIGSEGV訊號。

對於基於檔案的對映來講,如果訪問的對映部分在檔案中沒有相關區域與之對應(即對映大於底層檔案),那麼就會產生一個SIGBUS訊號。

使用MAP_NORESERVE標記可以控制每個mmap()呼叫的過度利用情況,而是用/proc檔案則可以控制整個系統的過度利用情況。

mremap()系統呼叫允許調整一個既有對映的大小。remap_file_pages()系統呼叫允許建立非線性檔案對映。