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]$