mmap文件修改內容的寫回


一、問題

在Linux下,使用mmap是操作文件內容的一個非常方便的方法,它可以將相對受限的文件操作接口轉換為大家喜聞樂見的內存操作。這個本身可以引申出很多方便的操作,比如,我們可以將這個內存地址(也就是對應的文件的某個部分)轉換為一個特定的數據結構指針,從而可以方便的進行結構的讀取和修改。

大部分情況下,應用都是將文件mmap之后將文件進行讀取操作,當然最為典型的就是操作系統給我們代勞的可執行文件的映射。但是對於文件的操作,如果我們不再小心翼翼,就可能會嘗試來修改mmap映射之后的文件,但是現在的問題是這個修改是否會寫回文件,它在什么情況下或者是何時寫回文件系統中?

二、映射的系統調用mmap

在mmap系統調用的linux man手冊說明

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

The  flags argument determines whether updates to the mapping are visible to other processes mapping the same region, and whether updates are  carried through to the underlying file.  This behavior is determined by including exactly one of the following values in flags:

       MAP_SHARED Share this mapping.  Updates to the mapping are  visible  to other  processes that map this file, and are carried through
                  to the underlying  file.   The  file  may  not  actually  be  updated until msync(2) or munmap() is called.

       MAP_PRIVATE
                  Create a private copy-on-write mapping.  Updates to the mapping are not visible to other  processes  mapping  the  same
                  file,  and  are  not carried through to the underlying file. It is unspecified whether changes made to the file after the
                  mmap() call are visible in the mapped region.
這里說的很清楚,這里的MAP_SHARED和MAP_PRIVATE兩個必須有且只能有一個,如果沒有設置其中的任何一個(普通程序員)或者兩個都設置(2B程序員),則這個mmap會返回失敗。后面將會看到,這兩個標志位對文件是否寫回將會有決定性的影響,雖然只是兩個bit。

三、mmap的內核簡單流程

sys_mmap--->>>>do_mmap2----->>>>do_mmap_pgoff

在這個函數中,其中對flags中的MAP_PRIVATE和MAP_SHARED的相關性檢測,但是這個並不是重點和亮點,重點是在於這兩個標志到底起到了什么決定性的影響,或者說這兩個bit的作用是如何被放大到影響文件寫盤的。其中最為重要的操作就是對於設置了MAP_SHARED屬性的映射會在新的即將創建的VMA的屬性中設置了下面兩個屬性,也就是這個共享屬性。

   vm_flags |= VM_SHARED | VM_MAYSHARE;
這里順便說一個基本的概念,這里的vm_flags是內核管理的VMA結構即struct vm_area_struct中設置的標志,表示這個VMA是否允許寫、讀、執行、共享等屬性,這個屬性和CPU識別的頁表項pte中的標志可能不一致,例如在寫時復制(Copy On Write)時,這個vma的屬性是可寫的,但是pte中的該項卻是只讀的,這樣在寫入時出發CPU的異常,從而執行寫時復制。

然后對於基於真正文件的映射(相對於匿名映射)執行

error = file->f_op->mmap(file, vma);

按照慣例,使用Linux的直系文件系統ext2文件系統的這個操作。linux-2.6.21\fs\ext2\file.c

const struct file_operations ext2_file_operations = {
……

 .mmap  = generic_file_mmap,
……}

這個函數非常簡單,所以我們就摘抄一下

int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
{
 struct address_space *mapping = file->f_mapping;

 if (!mapping->a_ops->readpage)
  return -ENOEXEC;
 file_accessed(file);
 vma->vm_ops = &generic_file_vm_ops;
 return 0;
}
這個太簡陋了,主要就是安裝了一個通用的內存區操作,當然是由於這個函數本身就是一個通用的映射,所以這么做也無可厚非。但是這里可能大家沒有注意到,其中並沒有對文件大小進行判斷,也就是說假設我有一個文件只有100B,然后mmap的時候卻拿雞毛當令箭,要把這個文件映射到100000B的地址空間,在mmap的時候是可以的,也就是這個系統調用可以正確返回。

四、寫入時觸發異常

由於這個mmap是如此的簡陋,所以真正的訪問的時候就會出現問題,因為文件中的內容還安安穩穩的呆在硬盤上呢,內存里毛都沒有一個。此時就觸發了CPU的保護異常,典型的第一次訪問引發的異常就是頁面不存在的異常。

do_page_fault----->>>>handle_mm_fault--->>>__handle_mm_fault--->>>handle_pte_fault

if (!pte_present(entry)) {
  if (pte_none(entry)) {
   if (vma->vm_ops) {
    if (vma->vm_ops->nopage)
     return do_no_page(mm, vma, address,對於第一訪問,滿足的是這條路徑,從而進入do_no_page函數
         pte, pmd,
         write_access);
    if (unlikely(vma->vm_ops->nopfn))
     return do_no_pfn(mm, vma, address, pte,
        pmd, write_access);
   }
   return do_anonymous_page(mm, vma, address,
       pte, pmd, write_access);
  }

…………

然后進入do_no_page函數,其中關鍵代碼

 new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, &ret);這里的這個vm_ops就是在前面的generic_file_mmap函數中安裝的generic_file_vm_ops函數,其中的nopage函數filemap_nopage
……

 */
 if (write_access) {
  if (!(vma->vm_flags & VM_SHARED)) {這里就是非常重要的那個VM_SHARED的標志獨當一面的時候了,這里是否設置了共享在這里分道揚鑣,而這個是之后兩者差別的根本來源,可以認為共享在這里得到第一次放大
   struct page *page;

   if (unlikely(anon_vma_prepare(vma)))
    goto oom;
   page = alloc_page_vma(GFP_HIGHUSER, vma, address);這里不管三七二十一,強制分配了一個頁面,這個頁面就體現了“私有”的概念
   if (!page)
    goto oom;
   copy_user_highpage(page, new_page, address, vma);
   page_cache_release(new_page);
   new_page = page;
   anon = 1;

  } else { 對應的,對於共享頁面,它將會使用上面vma_vm_ops->nopage中返回的頁面,那么這個頁面是從哪里來的呢?具體的說是從這個文件的address_space中搜索的,如果這個頁面尚未在內存中,那么它就負責這個時候把這個頁面讀入內存,如果已經被其它mmap觸發了讀入,那么就不用讀入了,直接返回這個已經在內存中的頁面,從而所有的mmap的同一個shared文件都可以看到其它文件mmap的修改
   /* if the page will be shareable, see if the backing
    * address space wants to know that the page is about
    * to become writable */
   if (vma->vm_ops->page_mkwrite &&
       vma->vm_ops->page_mkwrite(vma, new_page) < 0
       ) {
    page_cache_release(new_page);
    return VM_FAULT_SIGBUS;
   }
  }
 }

五、寫回判斷

從前面可以看到,對於private的mmap,它的頁面是新分配的,並且是從內存中分配的匿名頁面,這樣在

do_munmap--->>> unmap_region --->>> unmap_vmas --->>> unmap_page_range --->>>  zap_pud_range --->>>  zap_pmd_range  --->>> zap_pte_range

if (PageAnon(page))
    anon_rss--; 這種匿名映射,此時只是更新了統計信息。同樣,內存分配的頁面,它的page結構的mapping成員為空,而對於這些頁面,內核的定時寫回線程pdflush同樣會忽略這個頁面,它的內容將會在頁面不使用之后丟失。
   else {
    if (pte_dirty(ptent))
     set_page_dirty(page);
    if (pte_young(ptent))
     SetPageReferenced(page);
    file_rss--;
   }

作為比較,我們看一下文件映射page->mapping的設置路徑

filemap_nopage--->>>page_cache_read --->>>add_to_page_cache_lru--->>> add_to_page_cache

  if (!error) {
   page_cache_get(page);
   SetPageLocked(page);
   page->mapping = mapping; 此處安裝了這個mapping。
   page->index = offset;
   mapping->nrpages++;
   __inc_zone_page_state(page, NR_FILE_PAGES);

六、一個小問題

那么mmap沒有對頁表項做任何處理,當頁面真正訪問的時候會不會出現頁表項是隨機值呢?事實上不會,因為即使386的3級分層,它的任何一層的管理結構的分配都是至少以頁面為單位分配的,這樣,從最上級看來,如果某一級沒有映射,那么它所在的頁面必定有一級為0,也就是pte_none。也就是未映射的頁面的pte是一個確定值而不是隨機值。關於這一點,可以參考pte_alloc_one函數的實現。

七、測試代碼

[tsecer@Harry PrivateMap]$ cat Privatmap.c 
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main(int argc , char* argv[])
{
 int filedes = open (argv[1],O_RDWR);
 char * addr = mmap(0,MAP_LEN,PROT_READ|PROT_WRITE,
#if SHARED
 MAP_SHARED
#else
 MAP_PRIVATE
#endif
 ,filedes,0);
 memset(addr,'Z',MAP_LEN);
 munmap(addr,MAP_LEN);
 close(filedes);
 return 0;
}
[tsecer@Harry PrivateMap]$ cat Makefile  makefile文件內容
MAP_LEN = 100
MAP_FILE = test.txt
default:
         truncate $(MAP_FILE) -s $(MAP_LEN)
          rm -f $(MAP_FILE)
         for i in `seq 1 $(MAP_LEN)` ; do echo -n A >> $(MAP_FILE) ; done
         cat $(MAP_FILE)
         gcc -DMAP_LEN=$(MAP_LEN) -DNOUNMAP=$(NOUNMAP) -DSHARED=$(SHARED) -static Privatmap.c -o PrivateMap.exe 
         ./PrivateMap.exe $(MAP_FILE)
 cat $(MAP_FILE)
[tsecer@Harry PrivateMap]$ make SHARED=1 使用共享映射,修改寫回文件。
truncate test.txt -s 100
rm -f test.txt
for i in `seq 1 100` ; do echo -n A >> test.txt ; done
cat test.txt
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcc -DMAP_LEN=100 -DNOUNMAP= -DSHARED=1 -static Privatmap.c -o PrivateMap.exe 
./PrivateMap.exe test.txt
cat test.txt
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ[tsecer@Harry PrivateMap]$ make SHARED=0      這里使用私有映射,可以看到修改沒有寫回到文件中
truncate test.txt -s 100
rm -f test.txt
for i in `seq 1 100` ; do echo -n A >> test.txt ; done
cat test.txt
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgcc -DMAP_LEN=100 -DNOUNMAP= -DSHARED=0 -static Privatmap.c -o PrivateMap.exe 
./PrivateMap.exe test.txt
cat test.txt
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[tsecer@Harry PrivateMap]$ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM