Linux深入理解内存管理34(基于Linux6.6)---文件映射介绍
一、概述
当没有找到对应的PTE并且没有设置vma->vm_ops时候,就会触发匿名缺页异常,那么如果vm_ops操作函数存在的情况下,该如何处理呢?本章主要是学习文件映射存在的情况下,会产生文件映射缺页异常的处理流程。
文件映射的关键在于将文件的内容与进程的虚拟内存空间之间建立一种关联。具体来说,当进程需要访问某个文件时,操作系统会将该文件的部分或全部内容映射到进程的地址空间,并通过内存的方式提供文件数据访问。文件映射通常用来优化文件I/O,避免频繁的磁盘读写操作。
在文件映射机制下,文件并不是直接读写,而是通过内存访问,操作系统会在后台管理文件与内存之间的同步。通常,这种方式会带来性能的提升,尤其是在多次访问同一文件内容时。
1. 文件映射的内存管理
文件映射的内存管理在 Linux 内核中有着重要的角色。文件映射的内存通常是通过 页表 来管理的,每个文件页(文件块)与物理内存中的页面进行映射。当进程访问文件映射的内存区域时,Linux 会检查该页面是否在物理内存中,如果不在,则发生缺页异常(page fault),操作系统会从磁盘加载数据。
内存映射文件的内容与磁盘上的文件是同步的。操作系统会在以下情况下进行同步:
-
内存到文件的同步:当进程修改映射区域时,操作系统会在合适的时机将数据从内存写回到文件。这通常发生在进程退出时,或者调用了
msync()
函数进行显式同步时。 -
文件到内存的同步:当文件内容发生变化时,操作系统可以通过内存映射的方式将文件更新加载到内存中。
2. 文件映射的同步操作
在使用文件映射时,操作系统需要保证文件内容与内存映射之间的一致性。可以通过以下几种方法来控制和保证文件映射的同步:
-
msync()
:手动同步内存中的更改到文件。msync()
函数会将指定区域的内存内容写回到磁盘。 -
munmap()
:解除文件映射。通过munmap()
函数,进程可以解除之前通过mmap()
创建的文件映射,并且操作系统会确保映射区域中的内容被写回磁盘。
3. 文件映射的性能优势
文件映射相对于传统的文件读取和写入操作具有几个明显的性能优势:
-
减少 I/O 操作的开销:通过文件映射,操作系统能够直接在内存中访问文件内容,避免了文件 I/O 操作的重复,显著提高了文件访问效率。
-
支持按需加载:文件映射支持按需加载,即只有在访问文件的某个区域时,操作系统才会加载该部分数据到内存中。这可以大大节省内存资源,特别是在处理大文件时。
-
内存共享:通过文件映射,不同的进程可以共享内存区域,从而实现高效的进程间通信,避免了数据复制的开销。
二、代码流程
对于文件映射缺页异常,内核会调用do_fault()函数进行处理,do_fault()函数处理大致分为以下三种情况:
mm/memory.c
static vm_fault_t do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct mm_struct *vm_mm = vma->vm_mm;
vm_fault_t ret;
/*
* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND
*/
if (!vma->vm_ops->fault) {
/*
* If we find a migration pmd entry or a none pmd entry, which
* should never happen, return SIGBUS
*/
if (unlikely(!pmd_present(*vmf->pmd)))
ret = VM_FAULT_SIGBUS;
else {
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
vmf->pmd,
vmf->address,
&vmf->ptl);
/*
* Make sure this is not a temporary clearing of pte
* by holding ptl and checking again. A R/M/W update
* of pte involves: take ptl, clearing the pte so that
* we don't have concurrent modification by hardware
* followed by an update.
*/
if (unlikely(pte_none(*vmf->pte)))
ret = VM_FAULT_SIGBUS;
else
ret = VM_FAULT_NOPAGE;
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
} else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf);
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf);
else
ret = do_shared_fault(vmf);
/* preallocated pagetable is unused: free it */
if (vmf->prealloc_pte) {
pte_free(vm_mm, vmf->prealloc_pte);
vmf->prealloc_pte = NULL;
}
return ret;
}
- 首先,检查VMA的vm_ops方法中是否提供fault(),如果没有,就直接返回VM_FAULT_SIGBUS。
- 如果需要获取的页面不具备可写属性,就执行do_read_fault,一般这次缺页是由于读内存导致的缺页异常。
- 如果需要获取的页面具有可写属性,但时VMA的属性中没有设置VM_SHARED,即这个VMA是属于私有映射,则执行do_cow_fault。
- 其他情况就属于可写的共享页面,即属于共享映射并且这次异常是可写内存导致的缺页异常,则执行do_shared_fault。
2.1、do_read_fault
do_read_fault处理因为读内存导致的缺页中断。
mm/memory.c
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
vm_fault_t ret = 0;
/*
* Let's call ->map_pages() first and use ->fault() as fallback
* if page by the offset is not ready to be mapped (cold cache or
* something).
*/
if (should_fault_around(vmf)) {---解析1
ret = do_fault_around(vmf);
if (ret)
return ret;
}
ret = __do_fault(vmf);---解析2
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
ret |= finish_fault(vmf);
unlock_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
put_page(vmf->page);
return ret;
}
- 如果定义了map_pages函数,那么可以在缺页异常地址附件提前映射尽可能多的页面。提前建立进程地址空间与页面高速缓存的映射关系有利于减小因为发生缺页的次数,提高了效率。do_fault_around函数只是建立映射关系,而不会创建页面高速缓存,需要在__do_fault函数中创建新的页面高速缓存。
- __do_fault是真正为异常地址分配物理页面。
下面来看看核心函数__do_fault
mm/memory.c
static vm_fault_t __do_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret;
/*
* Preallocate pte before we take page_lock because this might lead to
* deadlocks for memcg reclaim which waits for pages under writeback:
* lock_page(A)
* SetPageWriteback(A)
* unlock_page(A)
* lock_page(B)
* lock_page(B)
* pte_alloc_one
* shrink_page_list
* wait_on_page_writeback(A)
* SetPageWriteback(B)
* unlock_page(B)
* # flush A, B to clear the writeback
*/
if (pmd_none(*vmf->pmd) && !vmf->prealloc_pte) {
vmf->prealloc_pte = pte_alloc_one(vma->vm_mm);
if (!vmf->prealloc_pte)
return VM_FAULT_OOM;
}
ret = vma->vm_ops->fault(vmf);---解析1
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY |
VM_FAULT_DONE_COW)))
return ret;
if (unlikely(PageHWPoison(vmf->page))) {
struct page *page = vmf->page;
vm_fault_t poisonret = VM_FAULT_HWPOISON;
if (ret & VM_FAULT_LOCKED) {
if (page_mapped(page))
unmap_mapping_pages(page_mapping(page),
page->index, 1, false);
/* Retry if a clean page was removed from the cache. */
if (invalidate_inode_page(page))
poisonret = VM_FAULT_NOPAGE;
unlock_page(page);
}
put_page(page);
vmf->page = NULL;
return poisonret;
}
if (unlikely(!(ret & VM_FAULT_LOCKED)))---解析2
lock_page(vmf->page);
else
VM_BUG_ON_PAGE(!PageLocked(vmf->page), vmf->page);
return ret;
}
- 这里调用了struct vm_operations_struct vm_ops的fault函数,还记得咱们上面用mmap映射文件的时候,对于ext4文件系统,vm_ops指向了ext4_file_vm_ops也就是调用了函数ext4_filemap_fault。
static const struct vm_operations_struct ext4_file_vm_ops = {
.fault = ext4_filemap_fault,
.map_pages = filemap_map_pages,
.page_mkwrite = ext4_page_mkwrite,
};
- 此处如果没有返回VM_FAULT_LOCKED,则调用lock_page()设置PG_locked标志位,而此处的lock_page在拿不到PG_locked flag时会导致系统睡眠,将进程设置为UNINTERRUPTABLE sleep。
- ext4_filemap_fault里面的逻辑我们很容易就能读懂,vm_file就是咱们当时mmap的时候映射的那个文件,然后需要调用filemap_fault。
对于文件映射来说,一般这个文件会在物理内存里面有页面作为它的缓存,find_get_page就是找那个页,如果找到了,就调用,预读一些数据到内存里面;如果没有,就跳到no_cached_page,就调用page_cache_read,在这里显示分配一个缓存页。
页面缓存基于内存管理系统,同时又和文件系统打交道,是两者之间的一个重要的纽带,应用层读取文件的方法有mmap和read/write。linux一般会利用空闲的内存进file cache,只有接收到内存申请时,才会清理页面缓存。
2.2、do_cow_fault
mm/memory.c
static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret;
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
if (!vmf->cow_page)
return VM_FAULT_OOM;
if (mem_cgroup_charge(page_folio(vmf->cow_page), vma->vm_mm,
GFP_KERNEL)) {
put_page(vmf->cow_page);
return VM_FAULT_OOM;
}
cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);
ret = __do_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (ret & VM_FAULT_DONE_COW)
return ret;
copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
__SetPageUptodate(vmf->cow_page);
ret |= finish_fault(vmf);
unlock_page(vmf->page);
put_page(vmf->page);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
return ret;
uncharge_out:
put_page(vmf->cow_page);
return ret;
}
do_cow_fault函数主要处理由写内存导致的缺页异常,而且VMA的属性是具有所有映射的,页就是处理私有文件映射的VMA中发生了写时复制。
其流程与上基本类似,主要是包括以下几个方面
- 申请一个新的页面,然后通过__do_fault将文件内容读取到fault page页面
- 如果fault page页面存在,则将fault page的内容复制到new page中,并将new page对应的PTE entry设置到硬件页表中
2.3、do_shared_fault
do_shared_fault函数处理共享文件映射时发生的缺页异常的情况,其代码实现在mm/memory.c中
static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
vm_fault_t ret, tmp;
ret = __do_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
return ret;
/*
* Check if the backing address space wants to know that the page is
* about to become writable
*/
if (vma->vm_ops->page_mkwrite) {
unlock_page(vmf->page);
tmp = do_page_mkwrite(vmf);
if (unlikely(!tmp ||
(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
put_page(vmf->page);
return tmp;
}
}
ret |= finish_fault(vmf);
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
VM_FAULT_RETRY))) {
unlock_page(vmf->page);
put_page(vmf->page);
return ret;
}
ret |= fault_dirty_shared_page(vmf);
return ret;
}
- 调用__do_fault函数,并通过vma->vm_ops->fault回调函数来读取文件内容到vmf->page里
- 如果VMA的操作函数中定义了page_mkwrite方法,那么调用page_mkwrite来通知进程地址空间,页面将变成可写的。如果一个页面变成可写的,那么进程可能需要等待这个页面的内容回写成功
- 将新生成的PTE entry设置到硬件页表中
- 将page标记为dirty,然后将这个fault_page添加到文件页面的RMAP机制中
- 通过balance_dirty_pages_ratelimited()来平衡并回写一部分脏页
三、总结
do_fault函数用于处理文件页异常,包括以下三种情况:
- 读文件页错误:
- 写私有文件页错误:
- 写共享文件页错误
1. 读文件页错误(Read File Page Fault)
当进程试图读取一个尚未加载到内存中的文件映射页面时,会发生缺页异常。在这种情况下,操作系统需要将文件的对应数据从磁盘加载到内存中。此时,o_fault()
会进行以下操作:
-
检查文件映射状态:确认该页面是否已映射到文件。如果映射是合法的且该页面不在内存中,内核将会读取文件的相应部分。
-
从磁盘加载数据:内核会发出 I/O 请求,从磁盘读取文件的相关块,并将其加载到内存中。该数据加载完成后,进程会继续执行并能够访问该页面。
-
更新页表:一旦数据加载到内存,操作系统会更新进程的页表,以便进程可以正确地访问该内存区域。
对于只读映射的文件,这种错误通常不会导致页面的修改,但仍然需要将文件的内容加载到内存中供进程读取。
2. 写私有文件页错误(Write Private File Page Fault)
对于进程使用 MAP_PRIVATE
标志进行文件映射的情况,进程访问文件映射区域时对文件的修改是私有的,不会反映到文件系统中。当进程试图修改一个尚未加载到内存的文件页时,会触发写私有文件页错误。
-
拷贝写(Copy-on-Write, COW):Linux 内核采用 拷贝写机制来处理这种情况。当进程尝试写入一个文件映射的页面时,操作系统首先会将该页面从只读模式切换为可写模式,并为该页面分配一个新的副本(复制一份页面)。然后进程继续写入新的页面副本,而不是修改原始文件的映射区域。
-
从文件加载数据:内核会将文件的相关数据加载到内存中,生成一个可修改的副本。如果进程尝试修改该文件的内容,这些修改不会反映到原文件中,而是存储在进程的内存副本中。
-
更新页表:修改后的页面会更新进程的页表,指向新的内存副本。这些修改对于进程是可见的,但不会影响文件的实际内容。
这种机制保证了修改文件内容的进程不会影响其他使用同一文件映射的进程,且这些修改不会被写回到原文件中。
3. 写共享文件页错误(Write Shared File Page Fault)
在 MAP_SHARED
映射模式下,进程共享映射的文件页面,任何修改都会反映到文件本身。如果进程尝试修改一个尚未加载到内存中的共享文件页,就会触发写共享文件页错误。
-
从文件加载数据:在这种情况下,内核会将文件的相关部分从磁盘加载到内存中,并使得该页面对所有映射到该文件的进程可用。
-
修改映射区域:由于使用的是共享映射模式,写操作将直接影响原始文件内容。内核会确保修改操作会同步到文件中,确保共享内存的状态与文件内容一致。
-
更新页表:一旦页面被加载到内存并允许写入,进程的页表会更新,以便进程可以直接访问文件映射区域。
与 MAP_PRIVATE
不同,在 MAP_SHARED
模式下,对文件的修改是同步到文件系统的,即进程对文件的更改会直接写回磁盘。