深入理解Linux虚拟内存管理(一)2:https://developer.aliyun.com/article/1597682
4.4 内存区域
linux内核编程之二:vm_area_struct / vm_struct 结构体
进程的地址空间很少能全部用满,一般都只是用到了其中一些分离的区域。区域由 vm_area_struct 来表示。区域之间不会交叉,它们各自代表一个有着相同属性和用途的地址集
合。如一个被装载到进程堆空间的只读共享库就包含在这样的一个区域。一个进程所有已被映射的区域都可以在 /proc/PID/maps 里看到,其中 PID 是该进程的进程号。
一个区域可能有许多与它相关的结构,如图 4.2 所示。在图的顶端,有一个 vm_area_struct 结构,它足以用来表示一个匿名区域。
如果一个文件被映射到内存,则可以通过 vm_file 字段得到 struct file。 vm_file 字段有一个指针指向 struct inode,索引节点用于找到 struct address_space,而 struct address_space 中包含与文件有关的所有信息,包括一系列指向与文件系统相关操作函数的指针,如读磁盘页面和写磁盘页面的操作。
vm_area_struct 结构在 中声明如下:
/* * This struct defines a memory VMM memory area. There is one of these * per VM-area/task. A VM area is any part of the process virtual memory * space that has a special rule for the page-fault handlers (ie a shared * library, the executable area etc). */ struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; /* Access permissions of this VMA. */ unsigned long vm_flags; /* Flags, listed below. */ rb_node_t vm_rb; /* * For areas with an address space and backing store, * one of the address_space->i_mmap{,shared} lists, * for shm areas, the list of attaches, otherwise unused. */ struct vm_area_struct *vm_next_share; struct vm_area_struct **vm_pprev_share; /* Function pointers to deal with this struct. */ struct vm_operations_struct * vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */ unsigned long vm_raend; /* XXX: put full readahead info here. */ void * vm_private_data; /* was vm_pte (shared mem) */ };
下面是对该结构中字段的简要解释。
vm_mm:这个 VMA 所属的 mm_struct。
vm_start:这个区域的起始地址。
vm_end:这个区间的结束地址。
vm_next:在一个地址空间中的所有 VMA 都按地址空间次序通过该字段简单地链接在一起。可以很有趣地发现该 VMA 链表在内核中所使用的单个链表中是非常少见的。
vm_page_prot:这个 VMA 对应的每个 PTE 里的保护标志位。其不同的位在表 3.1 中
有描述。
vm_flags:这个 VMA 的保护标志位和属性标志位。它们都定义在 中,
描述见表 4.3。
vm_rb:同链表一样,所有的 VMA 都存储在一个红黑树上以加快查找速度。这对于在发生缺页时能快速找到正确的区域非常重要,尤其是大量的映射区域。
vm_next_share:这个指针把由文件映射而来的 VMA 共享区域链接在一起(如共享库)。
vm_pprev_share:vm_next_share 的辅助指针。
vm_ops:包含指向与磁盘作同步操作时所需函数的指针。vm_ops 字段包含有指向 open(),close() 和 nopage() 的函数指针。
vm_pgoff:在已被映射文件里对齐页面的偏移。
vm_file:指向被映射的文件的指针。
vm_raend:预读窗口的结束地址。在发生错误时,一些额外的页面将被收回,这个值决定。
了这些额外页面的个数。
vm_private_data:一些设备驱动私有数据的存储,与内存管理器无关。
所有的区域按地址排序由指针 vm_next 链接成一个链表。寻找一个空闲的区间时,只需要遍历这个链表即可。但若在发生缺页中断时搜索 VMA 以找到一个指定区域,则是一个频繁操作。正因为如此,才有了红黑树,因为它平均只需要 O(logN) 的遍历时间。红黑树通过排序使得左结点的地址小于当前结点的地址,而当前结点的地址又小于右结点的地址。
4.4.1 内存区域的操作
VMA 提供三个操作函数,分别是 open(),close() 和 nopage() 。VMA 通过类型
vm_operations_struct 的 vma->vm_ops 提供上述几个操作函数 。该结构包含三个函数指针,它在 中的声明如下所示:
/* * These are the virtual MM functions - opening of an area, closing and * unmapping it (needed to keep files on disk up-to-date etc), pointer * to the functions called when a no-page or a wp-page exception occurs. */ struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused); };
每当创建或者删除一个区域时系统会调用 open() 和 close() 函数。只有一小部分设备使用这些操作函数,如文件系统和 system v 的共享区域。在打开或关闭区域时,需要执行额外的操作。例如 ,system V 中 open() 回调函数会递增使用共享段的 VMA 的数量。
我们关心的主要操作函数是 nopage() 回调函数,在发生缺页中断时 do_no_page() 会使用该回调函数。该回调函数负责定位该页面在高速缓存中的位置,或者分配一个新页面并填充请求的数据,然后返回该页面。
大多数被映射的文件会用到名为 generic_file_vm_ops 的 vm_operations_struct() 。它只注册一个函数名为 filemap_nopage() 的 nopage() 函数。该 nopage() 函数或者定位该页面在页面高速缓存中的位置,或者从磁盘上读取数据。该结构在 mm/filemap.c 中声明如下:
static struct vm_operations_struct generic_file_vm_ops = { nopage: filemap_nopage, };
4.4.2 有后援文件/设备的内存区域
如表 4.2 所列,在有后援文件的区域中,vm_file 引出了相关的 address_space。
address_space 结构包含一些与文件系统相关的信息,如必须写回到磁盘的脏页面数目 。该结构在 中声明如下:
struct address_space { struct list_head clean_pages; /* list of clean pages */ struct list_head dirty_pages; /* list of dirty pages */ struct list_head locked_pages; /* list of locked pages */ unsigned long nrpages; /* number of total pages */ struct address_space_operations *a_ops; /* methods */ struct inode *host; /* owner: inode, block_device */ struct vm_area_struct *i_mmap; /* list of private mappings */ struct vm_area_struct *i_mmap_shared; /* list of shared mappings */ spinlock_t i_shared_lock; /* and spinlock protecting it */ int gfp_mask; /* how to allocate the pages */ };
各字段简要描述如下。
clean_pages:不需要后援存储器同步的干净页面链表。
dirty_pages:需要后援存储器同步的脏页面链表。
locked_pages:在内存中被锁住的页面链表。
nrpages:在地址空间中正被使用且常驻内存的页面数。
a_ops:是一个操纵文件系统的函数结构。每一个文件系统都提供其自身的 address_space_operations,即便在某些时候它们使用普通的函数。
host:这个文件的索引节点。
i_mmap:使用 address_space 的私有映射链表。
i_mmap_shared:该地址空间中共享映射的 VMA 链表。
i_shared_lock:保护此结构的自旋锁。
gfp_mask:调用 __alloc_pages() 所要用到的掩码。
内存管理器需要定期将信息写回磁盘。但是内存管理器并不知道也不关心信息如何写回到磁盘,因此需要 a_ops 结构来调用相关的函数。它在 中的声明如下所示:
struct address_space_operations { int (*writepage)(struct page *); int (*readpage)(struct file *, struct page *); int (*sync_page)(struct page *); /* * ext3 requires that a successful prepare_write() call be followed * by a commit_write() call - they must be balanced */ int (*prepare_write)(struct file *, struct page *, unsigned, unsigned); int (*commit_write)(struct file *, struct page *, unsigned, unsigned); /* Unfortunately this kludge is needed for FIBMAP. Don't use it */ int (*bmap)(struct address_space *, long); int (*flushpage) (struct page *, unsigned long); int (*releasepage) (struct page *, int); #define KERNEL_HAS_O_DIRECT /* this is for modules out of the kernel */ int (*direct_IO)(int, struct inode *, struct kiobuf *, unsigned long, int); #define KERNEL_HAS_DIRECT_FILEIO /* Unfortunate kludge due to lack of foresight */ int (*direct_fileIO)(int, struct file *, struct kiobuf *, unsigned long, int); void (*removepage)(struct page *); /* called when page gets removed from the inode */ };
该结构中的字段都是函数指针,它们描述如下。
writepage:把一个页面写到磁盘。写到文件里的偏移量保存在页结构中。寻找磁盘块的工作则由具体文件系统的代码完成,参考 buffer.c:block_write_full_page() 。
readpage:从磁盘读一个页面。参考 buffer.c:block_read_full_page()。
sync_page:同步一个脏页面到磁盘。参考 buffer.c :blobk_sync_page() 。
prepare_write:在复制用户空间数据到将要写回磁盘的页面之前,调用此函数。对于一个日志文件系统,该操作保证了文件系统日志是最新的。而对于一般的文件系统而言,它也确保了分配所需要的缓冲区页面。参考 buffer.c:block_prepare_write() 。
commit_write:在从用户空间复制数据之后,会调用该函数将数据提交给磁盘。参考 buffer.c:block_commit_write() 。
bmap:映射一个磁盘块使得裸设备 I/O 操作能够执行。虽然它在后援为一个交换文件而非交换分区时也用于换出页面,但它还是主要与具体的文件系统有关。
flushpage:该函数确保在释放一个页面之前已不存在等待该页面的 I/O 操作。参考 buffer.c:discard_bh_page() 。
releasepage:该函数在释放掉页面之前将刷新所有与这个页面有关的缓冲区。参考 try_to_free_buffers()。
direct_IO:该函数在对一个索引节点执行直接 I/O 时使用。由于 #define 的存在,因此外部模块可以决定在编译时间该函数是否可用,因为它仅仅在 2.4.21 中被引入。
direct_fileIO:用于对 struct file 进行直接 I/O。并且,#define 也由于外部模块而存在,因为该 API 只在 2.4.22 中被引入。
removepage:一个候选回调函数,当页面从页面高速缓存中由 remove_page_from_inode_queue() 移除时使用。
4.4.3 创建内存区域
系统调用 mmap() 为一个进程创建新的内存区域。x86 中,mmap() 会调用 sys_mmap2(),而 sys_mmap2() 进一步调用 do_mmap2(),三个函数都使用相同的参数。do_mmap2() 负责获得 do_mmap_pgoff() 所需要的参数。而 do_mmap_pgoff() 才是所有体系结构中创建新区域的主要函数。
do_mmap2() 首先清空 flags 参数中的 MAP_DENYWRITE 和 MAP_EXECUTABLE 位,因为 Linux 用不到这两个标志位,这一点在 mmap() 的操作手册里有说明。如果映射一个文件,do_mmap2() 将通过文件描述符查找到相应的 struct file,并在调用 do_mmap_pgoff() 前获得 mm_struct mmap_sem 信号量。
do_mmap_pgoff 首先做一些合法检查。它首先检查在文件或设备被映射时,相应的文件系统和设备的操作函数是否有效。然后,它检查需要映射的大小是否与页面对齐,并且保证
不会在内核空间创建映射。最后,它必须保证映射的大小不会超过 pgoff 的范围以及这个进程没有过多的映射区域。
这个函数的余下部分比较大,大致有以下几个步骤:
参数的合法检查。
找出内存映射所需的空闲线性地址空间。如果系统提供了基于文件系统和设备的 get_unmapped_area() 函数,那么会调用它,否则将使用 arch_get_unmapped_area() 函数。
获得 VM 标志位,并根据文件存取权限对它们进行检查。
如果在映射的地方有旧区域存在,系统会修正它,以便新的映射能用这一部分的区域。
从 slab 分配器里分配一个 vm_area_struct,并填充它的各个字段。
把新的 VMA 链接到链表中。
调用与文件系统或设备相关的 mmap() 函数。
更新数据并返回。
(1)用户空间 mmap 函数
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
start:用户进程中要映射的用户空间的起始地址,通常为NULL(由内核来指定)
length:要映射的内存区域的大小
prot:期望的内存保护标志
flags:指定映射对象的类型
fd:文件描述符(由open函数返回)
offset:设置在内核空间中已经分配好的的内存区域中的偏移,例如文件的偏移量,大小为PAGE_SIZE的整数倍
返回值:mmap()返回被映射区的指针,该指针就是需要映射的内核空间在用户空间的虚拟地址
(2)内核空间响应函数
(a)sys_mmap2
// arch/i386/kernel/sys_i386.c asmlinkage long sys_mmap2(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff) { return do_mmap2(addr, len, prot, flags, fd, pgoff); }
(b)do_mmap2
static inline long do_mmap2( unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long fd, unsigned long pgoff) { int error = -EBADF; struct file * file = NULL; flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE); if (!(flags & MAP_ANONYMOUS)) { file = fget(fd); if (!file) goto out; } down_write(¤t->mm->mmap_sem); error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff); up_write(¤t->mm->mmap_sem); if (file) fput(file); out: return error; }
(c)do_mmap_pgoff
// mm/mmap.c unsigned long do_mmap_pgoff(struct file * file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, unsigned long pgoff) { struct mm_struct * mm = current->mm; struct vm_area_struct * vma, * prev; unsigned int vm_flags; int correct_wcount = 0; int error; rb_node_t ** rb_link, * rb_parent; if (file && (!file->f_op || !file->f_op->mmap)) return -ENODEV; if (!len) return addr; len = PAGE_ALIGN(len); if (len > TASK_SIZE || len == 0) return -EINVAL; /* offset overflow? */ if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) return -EINVAL; /* Too many mappings? */ if (mm->map_count > max_map_count) return -ENOMEM; /* Obtain the address to map to. we verify (or select) it and ensure * that it represents a valid section of the address space. */ addr = get_unmapped_area(file, addr, len, pgoff, flags); if (addr & ~PAGE_MASK) return addr; /* Do simple checking here so the lower-level routines won't have * to. we assume access permissions have been handled by the open * of the memory object, so we don't do any here. */ vm_flags = calc_vm_flags(prot,flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; /* mlock MCL_FUTURE? */ if (vm_flags & VM_LOCKED) { unsigned long locked = mm->locked_vm << PAGE_SHIFT; locked += len; if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur) return -EAGAIN; } if (file) { switch (flags & MAP_TYPE) { case MAP_SHARED: if ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE)) return -EACCES; /* Make sure we don't allow writing to an append-only file.. */ if (IS_APPEND(file->f_dentry->d_inode) && (file->f_mode & FMODE_WRITE)) return -EACCES; /* make sure there are no mandatory locks on the file. */ if (locks_verify_locked(file->f_dentry->d_inode)) return -EAGAIN; vm_flags |= VM_SHARED | VM_MAYSHARE; if (!(file->f_mode & FMODE_WRITE)) vm_flags &= ~(VM_MAYWRITE | VM_SHARED); /* fall through */ case MAP_PRIVATE: if (!(file->f_mode & FMODE_READ)) return -EACCES; break; default: return -EINVAL; } } else { vm_flags |= VM_SHARED | VM_MAYSHARE; switch (flags & MAP_TYPE) { default: return -EINVAL; case MAP_PRIVATE: vm_flags &= ~(VM_SHARED | VM_MAYSHARE); /* fall through */ case MAP_SHARED: break; } } /* Clear old maps */ munmap_back: vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); if (vma && vma->vm_start < addr + len) { if (do_munmap(mm, addr, len)) return -ENOMEM; goto munmap_back; } /* Check against address space limit. */ if ((mm->total_vm << PAGE_SHIFT) + len > current->rlim[RLIMIT_AS].rlim_cur) return -ENOMEM; /* Private writable mapping? Check memory availability.. */ if ((vm_flags & (VM_SHARED | VM_WRITE)) == VM_WRITE && !(flags & MAP_NORESERVE) && !vm_enough_memory(len >> PAGE_SHIFT)) return -ENOMEM; /* Can we just expand an old anonymous mapping? */ if (!file && !(vm_flags & VM_SHARED) && rb_parent) if (vma_merge(mm, prev, rb_parent, addr, addr + len, vm_flags)) goto out; /* Determine the object being mapped and call the appropriate * specific mapper. the address has already been validated, but * not unmapped, but the maps are removed from the list. */ vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); if (!vma) return -ENOMEM; vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = vm_flags; vma->vm_page_prot = protection_map[vm_flags & 0x0f]; vma->vm_ops = NULL; vma->vm_pgoff = pgoff; vma->vm_file = NULL; vma->vm_private_data = NULL; vma->vm_raend = 0; if (file) { error = -EINVAL; if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP)) goto free_vma; if (vm_flags & VM_DENYWRITE) { error = deny_write_access(file); if (error) goto free_vma; correct_wcount = 1; } vma->vm_file = file; get_file(file); error = file->f_op->mmap(file, vma); if (error) goto unmap_and_free_vma; } else if (flags & MAP_SHARED) { error = shmem_zero_setup(vma); if (error) goto free_vma; } /* Can addr have changed?? * * Answer: Yes, several device drivers can do it in their * f_op->mmap method. -DaveM */ if (addr != vma->vm_start) { /* * It is a bit too late to pretend changing the virtual * area of the mapping, we just corrupted userspace * in the do_munmap, so FIXME (not in 2.4 to avoid breaking * the driver API). */ struct vm_area_struct * stale_vma; /* Since addr changed, we rely on the mmap op to prevent * collisions with existing vmas and just use find_vma_prepare * to update the tree pointers. */ addr = vma->vm_start; stale_vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent); /* * Make sure the lowlevel driver did its job right. */ if (unlikely(stale_vma && stale_vma->vm_start < vma->vm_end)) { printk(KERN_ERR "buggy mmap operation: [<%p>]\n", file ? file->f_op->mmap : NULL); BUG(); } } vma_link(mm, vma, prev, rb_link, rb_parent); if (correct_wcount) atomic_inc(&file->f_dentry->d_inode->i_writecount); out: mm->total_vm += len >> PAGE_SHIFT; if (vm_flags & VM_LOCKED) { mm->locked_vm += len >> PAGE_SHIFT; make_pages_present(addr, addr + len); } return addr; unmap_and_free_vma: if (correct_wcount) atomic_inc(&file->f_dentry->d_inode->i_writecount); vma->vm_file = NULL; fput(file); /* Undo any partial mapping done by a device driver. */ zap_page_range(mm, vma->vm_start, vma->vm_end - vma->vm_start); free_vma: kmem_cache_free(vm_area_cachep, vma); return error; }
(d)file->f_op->mmap 的一个实例
goldfish中的实现
// drivers/staging/android/binder.c static struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, }; static int binder_mmap(struct file *filp, struct vm_area_struct *vma) { int ret; struct vm_struct *area; struct binder_proc *proc = filp->private_data; const char *failure_string; struct binder_buffer *buffer; if ((vma->vm_end - vma->vm_start) > SZ_4M) vma->vm_end = vma->vm_start + SZ_4M; if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE) printk(KERN_INFO "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n", proc->pid, vma->vm_start, vma->vm_end, (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags, (unsigned long)pgprot_val(vma->vm_page_prot)); if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) { ret = -EPERM; failure_string = "bad vm_flags"; goto err_bad_arg; } vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE; if (proc->buffer) { ret = -EBUSY; failure_string = "already mapped"; goto err_already_mapped; } area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP); if (area == NULL) { ret = -ENOMEM; failure_string = "get_vm_area"; goto err_get_vm_area_failed; } proc->buffer = area->addr; proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer; #ifdef CONFIG_CPU_CACHE_VIPT if (cache_is_vipt_aliasing()) { while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) { printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer); vma->vm_start += PAGE_SIZE; } } #endif proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL); if (proc->pages == NULL) { ret = -ENOMEM; failure_string = "alloc page array"; goto err_alloc_pages_failed; } proc->buffer_size = vma->vm_end - vma->vm_start; vma->vm_ops = &binder_vm_ops; vma->vm_private_data = proc; if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) { ret = -ENOMEM; failure_string = "alloc small buf"; goto err_alloc_small_buf_failed; } buffer = proc->buffer; INIT_LIST_HEAD(&proc->buffers); list_add(&buffer->entry, &proc->buffers); buffer->free = 1; binder_insert_free_buffer(proc, buffer); proc->free_async_space = proc->buffer_size / 2; barrier(); proc->files = get_files_struct(current); proc->vma = vma; /*printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/ return 0; err_alloc_small_buf_failed: kfree(proc->pages); proc->pages = NULL; err_alloc_pages_failed: vfree(proc->buffer); proc->buffer = NULL; err_get_vm_area_failed: err_already_mapped: err_bad_arg: printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n", proc->pid, vma->vm_start, vma->vm_end, failure_string, ret); return ret; } static struct vm_operations_struct binder_vm_ops = { .open = binder_vma_open, .close = binder_vma_close, }; static void binder_vma_open(struct vm_area_struct *vma) { struct binder_proc *proc = vma->vm_private_data; if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE) printk(KERN_INFO "binder: %d open vm area %lx-%lx (%ld K) vma %lx pagep %lx\n", proc->pid, vma->vm_start, vma->vm_end, (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags, (unsigned long)pgprot_val(vma->vm_page_prot)); dump_stack(); } static void binder_vma_close(struct vm_area_struct *vma) { struct binder_proc *proc = vma->vm_private_data; if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE) printk(KERN_INFO "binder: %d close vm area %lx-%lx (%ld K) vma %lx pagep %lx\n", proc->pid, vma->vm_start, vma->vm_end, (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags, (unsigned long)pgprot_val(vma->vm_page_prot)); proc->vma = NULL; binder_defer_work(proc, BINDER_DEFERRED_PUT_FILES); }
4.4.4 查找已映射内存区域
4.4.5 查找空闲内存区域
映射内存时,先得获取足够大的空闲区域,get_unmapped_area() 就用于查找一个空闲区域。
调用图如图 4.4 所示,获取空闲区域并不复杂。get_unmapped_area() 有很多参数:struct file 表示映射的文件或设备;pgoff 表示文件的偏移量;address 表示请求区域的起始地址;length 表示请求区域的长度;flags 表示此区域的保护标志位。
如果映射的是设备,比如视频卡,就还要使用 f_op->get_unmapped_area()。这是因为设备或文件有额外的操作要求,而通用代码并不能完成额外的要求,比如映射的地址必须对齐到一个特殊的虚拟地址。
如果没有特殊的要求,系统将调用体系结构相关的函数 arch_get_unmapped_area() 。并不是所有的体系结构都提供自己的函数,这时将调用 mm/mmap.c 中提供的通用版本函数。
4.4.6 插入内存区域
4.4.7 合并邻接区域
如果文件和权限都匹配,Linux 一般使用函数 merge_segments() [Hac02] 合并相邻的内存区域。其目的是减少 VMA 的数量,因为大量的操作会导致创建大量的映射,如系统调用 sys_mprotect()。该合并操作的开销很大,它会遍历大部分的映射,接着被删掉,特别是存在大量的映射时,merge_segments() 将会花费很长时间。
目前与上述函数等价的函数是 vma_merge(),它仅在 2 个地方用到。第 1 个是在 sys_mmap() 中,当映射匿名区域时会调用它,因为匿名区域通常可以合并。第 2 个是在 do_brk() 中,给区域扩展一个新分配的区域后,应该将这 2 个区域合并,这时就调用 vma_merge()。
vma_merge() 不仅合并两个线性区,还检查现有的区域能否安全地扩展到新区域,从而无需创建一个新区域。在没有文件或设备映射且两个区域的权限相同时,一个区域才能扩展其他地方也可能合并区域,只不过没有显式地调用函数。第 1 个就是在修正区域过程中系统调用 sys_mprotect() 时,如果两个区域的权限一致,就要确定它们是否要合并。第 2 个是在 move_vma() 中,它很可能把相似的区域移到一起。
4.4.8 重映射和移动内存区域
系统调用 mremap() 用于扩展或收缩现有区域,由函数 sys_mremap() 实现。在一个区域扩展时有可能移动该区域,或者在一个区域将要与其他区域相交并且没有指定标志位 MREMAP_FIXED 时,也可能移动该区域。调用图如图 4.6 所示。
移动一个区域时,do_mremap() 将首先调用 get_unmapped_area(),找到一个足以容纳扩展后的映射空闲区域,然后调用 move_vma() 把旧 VMA 移到新位置。move_vma() 调用图如图 4.7 所示。
move_vma() 首先检查新区域能否和相邻的 VMA 合并。如果不能合并,将创建一个新 VMA,每次分配一个 PTE。然后调用 move_page_tables() (调用图如图 4.8 所示),将旧映射中所有页表项都复制过来。尽管可能有更好的移动页表的方法,但是采用这种方法使得错误恢复更直观,因为相对而言,回溯更直接。
页面的内容并没有被复制,而是调用 zap_page_range() 将旧映射中的所有页都交换出去或者删除,通常缺页中断处理代码会将辅存、文件中的页交换回至内存,或调用设备相关函数 do_nopage() 。
4.4.9 对内存区域上锁
4.4.10 对区域解锁
系统调用 munlock() 和 munlockall() 分别是相应的解锁函数,它们分别由 sys_munlock() 和 sys_munlockall() 实现。它们比上锁函数简单得多,因为不必做过多的检查,它们都依赖于同一个函数 do_mmap() 来修整区域。
4.4.11 上锁后修正区域
在上锁或解锁时,VMA 将受到 4 个方面的影响,每个必须由 mlock_fixup() 修正。 上锁可能影响到所有的 VMA 时,系统就要调用 mlock_fixuo_all() 进行修正。第 2 个条件是被锁住区域地址的起始值,由 mlock_fixup_start() 处理,Linux 需要分配一个新的 VMA 来映射新区域。第 3 个条件是被锁住区域地址的结束值,由 mlock_fixup_end() 处理。 最后,mlock_fixup_middle() 处理映射区域的中间部分,此时需要分配 2 个新的 VMA。
值得注意的是创建上锁的 VMA 时从不合并,即使该区域被解锁也不能合并。一般而言,已经锁住某个区域的进程没必要再次锁住同一区域,因为经常开销处理器计算能力来合并和
分裂区域是不值得的。
4.4.12 删除内存区域
do_munmap() 负责删除区域。它相对于其他的区域操作函数,比较简单,它基本 上可分为 3 个部分。第 1 部分是修整红黑树,第 2 部分是释放和对应区域相关的页面和页表项,第 3 部分是如果生成了空洞就修整区域。
为保证红黑树已排好序,所有要删除的 VMA 都添加到称为 free 的链表,然后利用 rb_erase() 从红黑树中删除。如果区域还存在,那么在后来的修整中将以它们的新地址添加到系统中。
接下来遍历 free 所指向的 VMA 链表,即使删除线性区的一部分,系统也会调用 remove_shared_vm_struct() 把共享文件映射删掉。再次说明,如果仅是部分分删除,在修整中也会重新创建。zap_page_range() 删掉所有与区域相关的页面,而部分删除则调用 unmap_fixup 处理。
最后调用 free_pgtables() 释放所有和对应区域相关的页表项。这里注意到页表项并没有彻底释放完很重要。它反而释放所有的 PGD 和页目录项,因此,如果仅有一半的 PGD 用于映射,则不需要释放页表项。这是因为释放少量的页表项和数据结构代价很大,而且这些结构很小,另外它们还可能会再次被使用。
4.4.13 删除所有的内存区域
进程退出时,必须删除与其 mm_struct 相关联的所有 VMA,由函数 exit_mmap() 负责操作。这是个非常简单的函数,在遍历 VMA 链表前将刷新 CPU 高速缓存,依次删除每一个 VMA 并释放相关的页面,然后刷新 TLB 和删除页表项,这个过程在代码注释中有详细描述。
4.5 异常处理
VM 中很重要的一个部分就是如何捕获内核地址空间异常,这并不是内核的 bug。这部分不讨论如何处理诸如除数为零的异常错误,仅关注由于页面中断而产生的异常。有两种情况会发生错误的引用。第 1 种情况是进程通过系统调用向内核传递了一个无效的指针,内核必须能够安全地陷入,因为开始只检查地址是否低于 PAGE_OFFSET。第 2 种情况是内核使用 copy_from_user() 或 copy_to_user() 读写用户空间的数据。
编译时,连接器将在代码段中的 __ex_table 处创建异常表,异常表开始于 __start_ex_table,结束于 __stop_ex_table。每个表项的类型是 exception_table_entry,由可执行点和修整子程序二者组成。在产生异常时,缺页中断处理程序不能处理,它调用 search_exception_table() 查看是否为引起中断的指令提供了修整子程序,若系统支持模块,还要搜索每个模块的异常表。
4.6 缺页中断
进程线性地址空间里的页面不必常驻内存。例如,进程的分配请求并被立即满足,空间仅保留为满足 vm_area_struct 的空间。其也非常驻内存页面的例子有,页面可能被交换到后援存储器,还有就是写一个只读页面。
和大多操作系统一样,Linux 采用请求调页技术来解决非常驻页面的问题。在操作系统捕捉到由硬件发出的缺页中断异常时,它才会从后援存储器中调入请求的页面。由后援存储器的特征可知,采取页面预取技术可以减少缺页中断[MM87],但是 Linux 在这方面相当原始。在将一个页面读入交换区时,swapin_readahead() 会预取该页面后多达 2page_cluster 的页面,并放置在交换区中。不幸的是,很快要用到的页面只有一次机会邻近交换区,从而导致预约式换页技术很低效。Linux 将采用适合应用程序的预约式换页策略[KMC02]。
有两种类型的缺页中断,分别是主缺页中断和次缺页中断。当要费时地从磁盘中读取数据时,就会产生主缺页中断,其他的就是次缺页中断,或者是轻微的缺页中断。Linux 通过字段 task_struct→maj_fault 和 task_struct→min_fault 来统计各自的数目。
4.6.1 处理缺页中断
4.6.2 请求页面分配
(1)处理匿名页面
如果 vm_area_struct→ vm_ops 字段没有被填充或者没有提供 nopage() 函数,则调用 do_anonymous_page() 处理匿名访问。只有两中处理方式,第一次读和第一次写。由于是匿名页面,第一次读很简单,因为不存在数据,所以系统一般使用映射 PTE 的全零页 empty_zero_page,并且 PTE 是写保护的,所以如果进程要写页面就发生另一个缺页中断。在 x86 中,函数 mem_init() 负责把全局零页面归零。
如果是第一次写页面,就调用 alloc_page() 分配一个由 clear_user_highpage() 用零填充的空闲页(见第 7 章)。假定成功地分配了这样一个页面,mm_struct 中的 Resident Set Size (RSS) 字段将递增;在一些体系结构中,为保证高速缓存的一致性,当一个页面插入进程空间时要调用 flush_page_to_ram() 。然后页面插入到 LRU 链表中,以后就可以被页面回收代码处理。最后需要更新进程的页表项来反映新映射。
(2)处理文件/设备映射页
如果被文件或设备映射,VMA 中的 vm_operation_struct 将提供 nopage() 函数。如果是文件映射,函数 filemap_nopage() 将替代 nopage() 分配一个页面并从磁盘中读取一个页面大小的数据。如果页面由虚文件映射而来,就使用函数 shmem_nopage() (见第 12 章)。每种设备驱动程序将提供不同的 nopage() 函数,内部如何实现对我们来说并不重要,只要知道该函数返回一个可用的 struct page 即可。
在返回页面时,要先做检查以确定分配是否成功,如果不成功就返回相应的错误。然后检查提前 COW 失效是否发生。如果是向页面写,而在受管 VMA 中没有包括 VM_SHARED 标志,就会发生提前 COW 失效。提前 COW 失效是指分配一个新页面,在减少 nopage() 返回页面的引用计数前就将数据交叉地复制过来。
4.6.3 请求换页
4.6.4 写时复制(COW)页
深入理解Linux虚拟内存管理(一)4:https://developer.aliyun.com/article/1597699