歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

《Linux系統編程》筆記 第四章(三)

 

4.3 存儲映射

POSIX提供了相關調用,能使我們將文件映射到內存中,借由此機制我們可以方便的從內存中讀取文件數據,也可以修改內存中的數據來改變文件內容,或實現父子進程間通信。

4.3.1 mmap()

#include 
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr-開發人員建議的映射文件的內存起始地址。一般來說傳NULL讓系統自行決議。
length-用來映射的內存大小,單位是字節。由於內存最小可尋址單位是頁,因此不足一頁的長度也會占用一頁。
prot-內存區域的訪問權限,有如下參數,可以或運算,注意要和打開文件的訪問權限一致:

參數 含義 PROT_EXEC 頁內容可以被執行 PROT_READ 頁內容可以被讀取 PROT_WRITE 頁可以被寫入 PROT_NONE 頁不可訪問,無法讀寫,沒有意義

注意如果文件的讀寫權限是只寫的,那麼無法保證共享內存的功能可用,因為PROT_WRITE隱含了PROT_READ。
flags-共享內存的行為,常見的有如下參數:

參數 含義 MAP_FIXED 使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。該參數可以用來將不同的文件映射到同一個連續空間內 MAP_SHARED 與其它所有映射這個對象的進程共享映射空間。對共享區的寫入,相當於輸出到文件。直到msync()被調用,文件不保證被更新。 MAP_PRIVATE 建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標志和MAP_SHARED標志是互斥的,只能使用其中一個。使用該標志時內存的訪問權限無需與文件訪問權限一致 MAP_ANONYMOUS/MAP_ANON 創建一個匿名映射 MAP_LOCKED 將映射分頁鎖定在內存,防止被交換到交換分區

fd-要映射到內存中的文件描述符。
offset-文件描述符的偏移量,必須是頁大小的整數倍。
在調用成功後返回指向映射區的指針,失敗時返回MAP_FAILED,errno也會被設置,此處不再列舉錯誤碼類型。當程序試圖訪問無效的內存映射區域時,會收到SIGBUS信號;程序試圖寫入不可寫的區域時,收到SIGSEGV信號。
下圖顯示了信號的可能情況:
這裡寫圖片描述
2200-4095的區間內寫入不能同步到文件,這段區域是為了保證頁對齊空余出來的范圍。4095-8191是映射的長度,訪問這段區域會收到SIGBUS信號,超過8192是未映射的區域,訪問這段區域會收到SIGSEGV信號。

4.3.1.1 頁大小

前面說到offset參數必須指定為頁大小的整數倍(addr參數也一般由系統保證分配,也是頁面對齊的,下面相關的調用都是如此,不再贅述)。對於length,不足一頁的長度會占用一頁,讀取多占用部分獲得0,向多占用部分寫入也不會影響文件。
POSIX規定了通過sysconf調用可以獲得頁大小:

#include 
long sysconf(int name);//返回name對應的值
long lPageSize = sysconf(_SC_PAGESIZE);//獲取頁大小,字節

Linux提供了系統調用,也能獲取頁大小:

#include 
int getpagesize (void);

此外我們還可以使用宏來在編譯期獲取頁大小:

#include 
int iPageSize = PAGE_SIZE;

為了二進制文件的移植性考慮,建議還是使用運行期獲取頁大小的方式,同時將offset設置為0,addr由系統分配。

4.3.2 munmap()

在mmap()打開內存映射後,可以使用munmap()來關閉。

#include 
int munmap (void *addr, size_t len);

關閉addr起始,長度是len個字節的映射區域。
當我們mmap()映射一個文件到內存時,該文件的引用計數會增加1,當我們的進程結束、執行exec()系列函數或者關閉內存映射時,文件的引用計數會減1。為了保證數據完整性,在munmap()之前需要調用msync()來寫入硬盤。
調用成功返回0,失敗返回-1。

4.3.4 mmap()的優點

對比read()/write()等系統調用,mmap()有以下優點:

read()/write()需要內核在用戶空間內存做讀寫操作,mmap()直接操作內核的頁緩沖,可以避免一次數據拷貝,對大文件操作來說優勢明顯 由於對映射的內存區域做操作,因此不像read()/write()那樣需要頻繁的系統調用 多個進程將同一個文件映射到內存可以實現進程間通信 在文件上移動讀寫位置只需要指針操作而不是lseek()

4.3.5 mmap()的缺陷

映射的區域必須是頁的整數倍大小,對於映射小文件,很容易造成內存浪費。
使用mmap()來寫入文件時,需要提前知道寫入文件的大小,不像write()等系統調用能動態擴展文件大小。

4.3.6 調整映射大小

#define _GNU_SOURCE
#include 
#include 
void * mremap (void *addr, size_t old_size, size_t new_size, unsigned long flags);

將addr對應的映射從old_size調整到new_size。flags的取值可以是0或者MREMAP_MAYMOVE,0代表不允許內核移動映射區域,MREMAP_MAYMOVE則表示內核可以根據實際情況移動映射區域以找到一個符合new_size大小要求的內存區域。len和prot參數都會被向上取整到頁大小的整數倍。調用成功返回映射的地址,失敗則返回MAP_FAILED。

4.3.7 改變映射區域的權限

#include 
int mprotect (const void *addr, size_t len, int prot);

將len長度,addr起始對應的映射的訪問權限設置為prot。該調用在Linux上可以修改任意的內存段,而不僅僅是mmap()創建的,但是要保證addr是頁對齊的。prot的取值和mmap()提供的參數一致。

4.3.8 使用映射機制同步文件

我們對可寫存儲映射的修改在顯式調用同步操作之前是不會保證立即同步到文件中的。

#include 
int msync (void *addr, size_t len, int flags);

將以addr起始,長度為len的映射立即同步到內存中。flags取值含義如下:

取值 含義 MS_ASYNC msync調用會立即返回,不等到更新的完成 MS_SYNC msync調用會等到更新完成之後返回 MS_INVALIDATE 在共享內容更改之後,使得文件的其他映射失效,從而使得共享該文件的其他進程去重新獲取最新值。

調用成功返回0,失敗返回-1。

下面代碼演示如何將數據通過存儲映射的方式寫入硬盤:

#include 
#include 
#include  
#include  
#include  
#define WIRTE_SIZE 50
int main () 
{ 
    int fd = open("test.txt", O_CREAT|O_RDWR);
    ftruncate (fd, WIRTE_SIZE);//新創建的文件必須要形成空洞文件,否則寫入的數據會被放棄
    //映射的長度必須大於等於後面訪問的范圍,否則會收到SIGBUS信號
    void * p =mmap(NULL, WIRTE_SIZE, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED)
    {
        perror("mmap");
        return -1;
    }       
    memcpy(p, "123", 3);
    msync(p, WIRTE_SIZE, MS_SYNC);
    return 0; 
}

下面是一道練習題:使用mmap()實現命令cp類似的功能:

//使用方式 a.out xxx xxx
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//使用存儲映射實現cp命令功能
int main(int argc,char *argv[])
{
    if(argc < 3)
    {
        printf("please start as 'a.out srcfile destfile'\n");
        return 0;
    }
    int fd = open(argv[1], O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return -1;
    }
    //文件存在則不覆蓋
    /*if(access(argv[2], F_OK) != -1)
    {
        printf("please change a destfile name\n");
        return 0;
    }*/
    struct stat buf;
    fstat(fd, &buf);
    printf("file size:%ld\n", buf.st_size);
    //不能用creat,因為creat打開文件的模式是只寫,無法與mmap配合。前面說了mmap只能和有讀權限的文件描述符配合
    //int fdDest = creat(argv[2], S_IRWXU);
    int fdDest = open(argv[2], O_RDWR|O_CREAT, S_IRWXU);
    if(fdDest < 0)
    {
        perror("creat");
    }
    if(ftruncate(fdDest, buf.st_size)<0)
    {
        perror("ftruncate");
        return 0;
    }
    char* s = (char*)mmap(NULL, buf.st_size, PROT_WRITE|PROT_READ, MAP_PRIVATE, fd, 0);
    if(s == MAP_FAILED)
    {
        perror("mmap1");
        return 0;
    }
    char* d = (char*)mmap(NULL, buf.st_size, PROT_WRITE, MAP_SHARED, fdDest, 0);
    if(d == MAP_FAILED)
    {
        perror("mmap2");
        return 0;
    }
    memcpy(d, s, buf.st_size);
    if(msync(d, buf.st_size, MS_ASYNC)<0)
    {
        perror("mmap");
        return 0;
    }
    return 0;
}
Copyright © Linux教程網 All Rights Reserved