第三讲 进程地址空间
2015-10-28 21:03
141 查看
序
回忆前两讲,内核获取动态内存有三种方法:用__get_free_pages()从分区页框分配器获得页框;用
kmem_cache_alloc()或
kmalloc()使用slab分配器为专用或通用对象分配块;用
vmalloc()或
vmalloc_32()获得一块非连续的内存区。
不论是否阻塞,如果所请求内存区得到满足,当函数成功返回时候,它们函数返回分配空间的起始线性地址或一个页描述符的线性地址。对应的物理内存已经分配完毕。这些操作的过程相对简单易懂。
但是当用户进程请求分配内存时,内核会采用不同的方法。原因有二:
进程的请求被认为是不急迫的,例如进程调用malloc()获得内存时候,并不意味着进程很快访问所获得的内存。因此,内核总是尽量推迟给用户态进程分配动态内存。
由于用户进程不可信任,因此内核必须能随时准备捕获用户态进程引起的所有寻址错误。
因此,内核使用一种新的资源成功实现了对进程的动态内存的推迟分配。当用户态进程请求动态内存时,并没有获得请求的页框,而仅仅获得对一个新的线性地址区的使用权,这个线性地址区就成为进程地址空间的一部分。这个区间叫做“线性区”。线性区是由起始地址、长度和一些访问权限来定义,起始地址和长度都是4096的倍数。
本讲讲以线性区为基础讲述与进程地址空间相关的内容。首先介绍进程地址空间的概念,然后介绍用于描述进程地址空间的内存描述符,以及其中最重要的线性区的概念。内核对进程线性区与物理地址的映射处理在缺页异常处理程序中完成,因此之后插入对的详细解读。最后是进程地址空间的创建和删除过程,以及特殊线性区——堆的管理。
进程的地址空间
进程的地址空间由允许进程使用的全部线性地址组成。每个进程所看到的线性地址集合不同,一个进程的地址和另一个进程的地址间没有什么关系。内核可以动态修改进程的地址空间。如下几种情况下进程可以获得新的线性区:
1.当用户新建一个进程时候。
2.正在运行的进程装入一个完全不同的程序的时候(exec函数)。
3.正在运行的进程对一个或部分文件执行内存映射时候。
4.进程项用户态堆栈增加数据直至堆栈用完的时候。
5.进程创建一个IPC共享线性区与其他合作进程共享数据的时候。
6.进程通过
malloc()这样的函数扩展堆。
在第一讲中提到过Linux进程运行后的虚拟空间分配。从逻辑上,Unix程序的线性地址空间传统上划分为几个称为段的区间。
正文段:包含程序的可执行代码。
已初始化数据段:包含已初始化的数据,也就是初值存放在可执行文件中的静态变量和全局变量。
未初始化数据段:包含未初始化数据,历史上这个段被称为bss段。
堆栈段:包含程序的堆栈,堆栈中有返回地址、参数和被执行函数的局部变量。
从2.6.9开始,Linux内核引入了灵活线性区布局的功能,与经典布局相比,两者只在文件内存映射与匿名映射时线性区的位置上有区别,如下图所示。在典型布局下,这些区域从整个用户态地址空间的1/3(即0x40000000)开始,新的区域向上增长。在灵活布局中,这些区域紧接用户态堆栈尾,新区域向下增长。
灵活布局的好处是使得堆空间和其他线性区可以灵活扩展。
在/proc目录的某指定进程的maps文件中,可以看到进程执行时的线性区情况。
内存描述符
在缺页异常处理程序一节将看到,确定一个进程当前所拥有的地址空间是内核的基本任务。因此需要用一种方式描述进程的地址空间。内核使用内存描述符来描述。内存描述符结构类型为mm_struct,下一讲讲到进程时将会看到,进程描述符的mm字段就指向这个结构。
==================== include/linux/mm_types.h 222 315 ==================== struct mm_struct { struct vm_area_struct * mmap; /* list of VMAs */ struct rb_root mm_rb; struct vm_area_struct * mmap_cache; /* last find_vma result */ #ifdef CONFIG_MMU unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); void (*unmap_area) (struct mm_struct *mm, unsigned long addr); #endif unsigned long mmap_base; /* base of mmap area */ unsigned long task_size; /* size of task vm space */ unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */ unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ pgd_t * pgd; atomic_t mm_users; /* How many users with user space? */ atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ int map_count; /* number of VMAs */ struct rw_semaphore mmap_sem; spinlock_t page_table_lock; /* Protects page tables and some counters */ struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung * together off init_mm.mmlist, and are protected * by mmlist_lock */ unsigned long hiwater_rss; /* High-watermark of RSS usage */ unsigned long hiwater_vm; /* High-water virtual memory usage */ unsigned long total_vm, locked_vm, shared_vm, exec_vm; unsigned long stack_vm, reserved_vm, def_flags, nr_ptes; unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ /* * Special counters, in some configurations protected by the * page_table_lock, in other configurations by being atomic. */ struct mm_rss_stat rss_stat; struct linux_binfmt *binfmt; cpumask_t cpu_vm_mask; /* Architecture-specific MM context */ mm_context_t context; /* Swap token stuff */ /* * Last value of global fault stamp as seen by this process. * In other words, this value gives an indication of how long * it has been since this task got the token. * Look at mm/thrash.c */ unsigned int faultstamp; unsigned int token_priority; unsigned int last_interval; unsigned long flags; /* Must use atomic bitops to access the bits */ struct core_state *core_state; /* coredumping support */ #ifdef CONFIG_AIO spinlock_t ioctx_lock; struct hlist_head ioctx_list; #endif #ifdef CONFIG_MM_OWNER /* * "owner" points to a task that is regarded as the canonical * user/owner of this mm. All of the following must be true in * order for it to be changed: * * current == mm->owner * current->mm != mm * new_owner->mm == mm * new_owner->alloc_lock is held */ struct task_struct __rcu *owner; #endif #ifdef CONFIG_PROC_FS /* store ref to file /proc/<pid>/exe symlink points to */ struct file *exe_file; unsigned long num_exe_file_vmas; #endif #ifdef CONFIG_MMU_NOTIFIER struct mmu_notifier_mm *mmu_notifier_mm; #endif /* How many tasks sharing this mm are OOM_DISABLE */ atomic_t oom_disable_count; };
所有的内存描述符存放在一个双向链表中,每个描述符在mmlist字段存放链表相邻元素的地址。链表的第一个元素是进程0使用的内存描述符init_mm的mm_list字段,mmlist_lock自旋锁保护多处理器系统对链表的同时访问。
mm_users和mm_count字段不同。
start_code、end_code表示程序的源代码所在线性区的起始和终止线性地址。
start_data、end_data表示程序的初始化数据在线性区的起始和终止线性地址,这两个字段指定的线性区大体上与数据段对应。
start_brk、brk存放堆的起始和终止地址。
start_stack是正好在main()的返回地址之上的地址。
arg_start、arg_end是命令行参数所在的堆栈部分的起始地址和终止地址。
env_start、env_end环境变量所在的堆栈部分的起始地址和终止地址。
get_unmapped_area函数指针在初始化时可能被指定两个可能值。在线性区的处理一节中讨论。
mmap、mm_rb、mmlist、mmap_cache字段在下一小节讨论。
内核线程的内存描述符
内核线程永远不会访问低于TASK_SIZE(在x86上等于0xC0000000)的地址,他们不使用线性区,因此内存描述符的很多字段对于内核线程没有意义。在第一讲中讲过,所有用户进程和内核线程的最高1GB线性地址空间的页表项都是相同的,为了避免无用的TLB和高速缓存刷新,内核线程使用了一组最近运行的普通进程的页表。因此在每个进程描述符中包含了两个内存描述符指针:mm和active_mm。
mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的内存描述符。对于普通进程而言,这两个字段存放相同的指针。但是内核线程不拥有任何内存描述符,因此mm总是为NULL,当内核线程运行时,它的active_mm字段被初始化为前一个运行进程的active_mm值。在下一讲进程中的schedule函数中讨论。
另一方面,一旦内核态的一个进程修改高端线性地址的页表项,那么就系统中所有进程页表集合中相应表项都需要更新。这将遍历所有进程的页表集合,是一个非常费力的操作,因此,Linux采用一种延迟方式:当一个高端地址必须重新映射时,内核就更新根目录在swapper_pg_dir主内核页全局目录中的常规页表集合。这个页全局目录由init_mm变量的pgd字段所指向。
在随后的“处理非连续内存区访问”一节,我们将描述缺页处理程序如何在非必要时维护存放在常规页表中的扩展信息。
线性区
Linux通过vm_area_struct结构体实现线性区。==================== include/linux/mm_types.h 130 186 ==================== 130 struct vm_area_struct { 131 struct mm_struct * vm_mm; /* The address space we belong to. */ 132 unsigned long vm_start; /* Our start address within vm_mm. */ 133 unsigned long vm_end; /* The first byte after our end address 134 within vm_mm. */ 135 136 /* linked list of VM areas per task, sorted by address */ 137 struct vm_area_struct *vm_next, *vm_prev; 138 139 pgprot_t vm_page_prot; /* Access permissions of this VMA. */ 140 unsigned long vm_flags; /* Flags, see mm.h. */ 141 142 struct rb_node vm_rb; 143 144 /* 145 * For areas with an address space and backing store, 146 * linkage into the address_space->i_mmap prio tree, or 147 * linkage to the list of like vmas hanging off its node, or 148 * linkage of vma in the address_space->i_mmap_nonlinear list. 149 */ 150 union { 151 struct { 152 struct list_head list; 153 void *parent; /* aligns with prio_tree_node parent */ 154 struct vm_area_struct *head; 155 } vm_set; 156 157 struct raw_prio_tree_node prio_tree_node; 158 } shared; 159 160 /* 161 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma 162 * list, after a COW of one of the file pages. A MAP_SHARED vma 163 * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack 164 * or brk vma (with NULL file) can only be in an anon_vma list. 165 */ 166 struct list_head anon_vma_chain; /* Serialized by mmap_sem & 167 * page_table_lock */ 168 struct anon_vma *anon_vma; /* Serialized by page_table_lock */ 169 170 /* Function pointers to deal with this struct. */ 171 const struct vm_operations_struct *vm_ops; 172 173 /* Information about our backing store: */ 174 unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE 175 units, *not* PAGE_CACHE_SIZE */ 176 struct file * vm_file; /* File we map to (can be NULL). */ 177 void * vm_private_data; /* was vm_pte (shared mem) */ 178 unsigned long vm_truncate_count;/* truncate_count or restart_addr */ 179 180 #ifndef CONFIG_MMU 181 struct vm_region *vm_region; /* NOMMU mapping region */ 182 #endif 183 #ifdef CONFIG_NUMA 184 struct mempolicy *vm_policy; /* NUMA policy for the VMA */ 185 #endif 186 };
每个线性区描述符表示一个线性地址区间。其中vm_start字段包含区间的第一个线性地址,vm_end字段是结束地址+1。两者相减就是线性区的长度。vm_mm字段指向进程的mm_struct内存描述符。
vm_ops是一些对线性区操作的方法。可能的操作方法如下所示。
==================== include/linux/mm.h 188 232 ==================== 188 /* 189 * These are the virtual MM functions - opening of an area, closing and 190 * unmapping it (needed to keep files on disk up-to-date etc), pointer 191 * to the functions called when a no-page or a wp-page exception occurs. 192 */ 193 struct vm_operations_struct { 194 void (*open)(struct vm_area_struct * area); 195 void (*close)(struct vm_area_struct * area); 196 int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); 197 198 /* notification that a previously read-only page is about to become 199 * writable, if an error is returned it will cause a SIGBUS */ 200 int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); 201 202 /* called by access_process_vm when get_user_pages() fails, typically 203 * for use by special VMAs that can switch between memory and hardware 204 */ 205 int (*access)(struct vm_area_struct *vma, unsigned long addr, 206 void *buf, int len, int write); 207 #ifdef CONFIG_NUMA 215 int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new); 216 227 struct mempolicy *(*get_policy)(struct vm_area_struct *vma, 228 unsigned long addr); 229 int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from, 230 const nodemask_t *to, unsigned long flags); 231 #endif 232 };
进程的线性区间从来不重叠,并且在权限匹配时,内核会将新分配的线性区与紧邻的现有线性区进行合并。
图示了四种增加或删除一个线性区时内核的操作。
当新增的线性区与原有线性区地址连续且权限相同时候,两个线性区被合并为同一个。
当新增的线性区与原有线性区地址连续但权限不同时候,两个线性区仍为两个。
当删除的线性区位于原有线性区一端时,现有线性区区域被缩小。
当删除的线性区位于原有线性区中间时,将创建两个较小的线性区。
这些操作的具体实现在后面线性区的处理小节中介绍。这里也可以看出,删除一个线性区有可能失败,因为有可能需要额外的线性区描述符。
线性区数据结构
进程所拥有的线性区是通过一个简单的顺序链表链接在一起,按照升序排列。每个vm_area_struct元素的vm_next字段指向链表的下一个元素,内核通过内存描述符的mmap字段索引到第一个线性区描述符。内存描述符的map_count字段是进程拥有的线性区数目,默认最多为65536个,但可以通过写/proc/sys/vm/max_map_count文件来修改这个限定值。
内核经常需要查找包含指定线性地址的线性区,例如对一个线性地址的访问,内核需要判断是否合法,这时候需要查找该地址是否包含在某个线性区内,且访问权限如何。由于链表是顺序的,查找复杂度是O(n)。但是当进程内的线性区很多时,这种查找、插入、删除将花费很长时间。例如面向对象的数据库应用,其线性区有成百上千个,这时候与内存相关的系统调用将变得异常低效。
为此,Linux2.6把内存描述符放在一个红黑树的数据结构中。红黑树的首部由内存描述符的mm_rb指向。在红黑树中,每个节点通常有两个孩子:左孩子与右孩子。树中元素被排序。对于每个节点N,N的左树上所有元素都在N之前,N的右树上所有元素都在N之后。此外红黑树还必须满足下列4条规则:
1.每个节点必须或红或黑。
2.树根必须为黑。
3.红节点的孩子必须为黑。
4.从一个节点到后代叶子节点的每个路径都包含相同数量的黑节点。当统计黑节点时,空指针也算黑节点。
满足这样特性的红黑树高度与节点数为对数关系:2*log(n+1)。这样的数据结构使得查找、插入、删除都异常高效,复杂度变为O(logn)。
这样Linux使用链表和红黑树两种数据结构来保存进程的线性区。当插入或删除一个线性区描述符时,内核通过红黑树搜索前后元素,并用搜索结果快速更新链表而不用扫描链表。
一般来说,红黑树用来确定含有指定地址的线性区,而链表通常在扫描整个线性区集合时来使用。
线性区访问权限
在第一讲内存寻址时讲过,Linux主要采用页式内存管理,每个页表项中有Read/Write、Present或User/Supervisor等标志,每个页描述符的flags字段也有不同的标志。第一种标志被80x86使用,第二种标志被Linux使用。现在又增加了第三种标志,即线性区的相关标志,在vm_area_struct的vm_flags字段中。==================== include/linux/mm.h 66 136 ==================== 66 /* 67 * vm_flags in vm_area_struct, see mm_types.h. 68 */ 69 #define VM_READ 0x00000001 /* currently active flags */ 70 #define VM_WRITE 0x00000002 71 #define VM_EXEC 0x00000004 72 #define VM_SHARED 0x00000008 73 74 /* mprotect() hardcodes VM_MAYREAD >> 4 == VM_READ, and so for r/w/x bits. */ 75 #define VM_MAYREAD 0x00000010 /* limits for mprotect() etc */ 76 #define VM_MAYWRITE 0x00000020 77 #define VM_MAYEXEC 0x00000040 78 #define VM_MAYSHARE 0x00000080 79 80 #define VM_GROWSDOWN 0x00000100 /* general info on the segment */ 81 #if defined(CONFIG_STACK_GROWSUP) || defined(CONFIG_IA64) 82 #define VM_GROWSUP 0x00000200 83 #else 84 #define VM_GROWSUP 0x00000000 85 #endif 86 #define VM_PFNMAP 0x00000400 /* Page-ranges managed without "struct page", just pure PFN */ 87 #define VM_DENYWRITE 0x00000800 /* ETXTBSY on write attempts.. */ 88 89 #define VM_EXECUTABLE 0x00001000 90 #define VM_LOCKED 0x00002000 91 #define VM_IO 0x00004000 /* Memory mapped I/O or similar */ 92 93 /* Used by sys_madvise() */ 94 #define VM_SEQ_READ 0x00008000 /* App will access data sequentially */ 95 #define VM_RAND_READ 0x00010000 /* App will not benefit from clustered reads */ 96 97 #define VM_DONTCOPY 0x00020000 /* Do not copy this vma on fork */ 98 #define VM_DONTEXPAND 0x00040000 /* Cannot expand with mremap() */ 99 #define VM_RESERVED 0x00080000 /* Count as reserved_vm like IO */ 100 #define VM_ACCOUNT 0x00100000 /* Is a VM accounted object */ 101 #define VM_NORESERVE 0x00200000 /* should the VM suppress accounting */ 102 #define VM_HUGETLB 0x00400000 /* Huge TLB Page VM */ 103 #define VM_NONLINEAR 0x00800000 /* Is non-linear (remap_file_pages) */ 104 #define VM_MAPPED_COPY 0x01000000 /* T if mapped copy of data (nommu mmap) */ 105 #define VM_INSERTPAGE 0x02000000 /* The vma has had "vm_insert_page()" done on it */ 106 #define VM_ALWAYSDUMP 0x04000000 /* Always include in core dumps */ 107 108 #define VM_CAN_NONLINEAR 0x08000000 /* Has ->fault & does nonlinear pages */ 109 #define VM_MIXEDMAP 0x10000000 /* Can contain "struct page" and pure PFN pages */ 110 #define VM_SAO 0x20000000 /* Strong Access Ordering (powerpc) */ 111 #define VM_PFN_AT_MMAP 0x40000000 /* PFNMAP vma that is fully mapped at mmap time */ 112 #define VM_MERGEABLE 0x80000000 /* KSM may merge identical pages */ 113 114 /* Bits set in the VMA until the stack is in its final location */ 115 #define VM_STACK_INCOMPLETE_SETUP (VM_RAND_READ | VM_SEQ_READ) 116 117 #ifndef VM_STACK_DEFAULT_FLAGS /* arch can override this */ 118 #define VM_STACK_DEFAULT_FLAGS VM_DATA_DEFAULT_FLAGS 119 #endif 120 121 #ifdef CONFIG_STACK_GROWSUP 122 #define VM_STACK_FLAGS (VM_GROWSUP | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT) 123 #else 124 #define VM_STACK_FLAGS (VM_GROWSDOWN | VM_STACK_DEFAULT_FLAGS | VM_ACCOUNT) 125 #endif 126 127 #define VM_READHINTMASK (VM_SEQ_READ | VM_RAND_READ) 128 #define VM_ClearReadHint(v) (v)->vm_flags &= ~VM_READHINTMASK 129 #define VM_NormalReadHint(v) (!((v)->vm_flags & VM_READHINTMASK)) 130 #define VM_SequentialReadHint(v) ((v)->vm_flags & VM_SEQ_READ) 131 #define VM_RandomReadHint(v) ((v)->vm_flags & VM_RAND_READ) 132 133 /* 134 * special vmas that are non-mergable, non-mlock()able 135 */ 136 #define VM_SPECIAL (VM_IO | VM_DONTEXPAND | VM_RESERVED | VM_PFNMAP)
这些标志描述了这个线性区本身如何增长等、其中的页包含什么内容、进程访问权限等等。
其中前四个是页访问权限,有读、写、执行和共享访问权限四种,还可以任意组合成16种可能情况,例如有可能允许一个线性区中的页可以执行但是不可以读取。
这种保护方案通过页表项的保护位来实现,由分页单元直接执行检查。页表标志的初值存放在vm_area_struct描述符的vm_page_prot字段中。当增加一个页时,内核根据vm_page_prot字段的值设置相应页表项中的标志。
但是,线性区的访问权限并不能直接转换成页保护位,这是因为:
80x86处理器的页表只有两个保护位,即R/W和U/S标志。线性区所包含页在用户态进程总能访问,因此U/S标志必须总置为1。这样页表保护位对权限的表达能力非常有限。
在写时复制等情况下,即使线性区允许对一个页的访问,但仍必须产生一个缺页异常。
启用PAE的处理器,在所有64位页表中支持NX标志。
在不支持PAE的内核中,Linux采取以下规则以克服80x86处理器的硬件限制:读访问权限总是隐含着执行访问权限,反之亦然;写访问权限总是隐含着读访问权限。为了实现写时复制技术中能推迟页框分配,只要相应的页不是由多个进程所共享,那么这种页框都是写保护的。
因此,16种可能组合要根据以下规则精简:
如果页具有写和共享两种访问权限,那么R/W位被置1.
如果页有读或执行权限,但是既没有写也没有共享权限,那么R/W位被清0.
如果页没有任何访问权限,那么Present位清0,这样每次访问都产生缺页异常。然而为了与真正的缺页区分,Linux还把Page size位置为1.(这里还是违背了x86的设计意图)
精简后的访问权限的每种组合所对应的后的保护位存放在protection_map数组的16个元素中。
==================== mm/mmap.c 73 76 ==================== pgprot_t protection_map[16] = { __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111, __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111 }; =============== arch/x86/include/asm/pgtable_types.h 136 153 =============== /* xwr */ #define __P000 PAGE_NONE #define __P001 PAGE_READONLY #define __P010 PAGE_COPY #define __P011 PAGE_COPY #define __P100 PAGE_READONLY_EXEC #define __P101 PAGE_READONLY_EXEC #define __P110 PAGE_COPY_EXEC #define __P111 PAGE_COPY_EXEC #define __S000 PAGE_NONE #define __S001 PAGE_READONLY #define __S010 PAGE_SHARED #define __S011 PAGE_SHARED #define __S100 PAGE_READONLY_EXEC #define __S101 PAGE_READONLY_EXEC #define __S110 PAGE_SHARED_EXEC #define __S111 PAGE_SHARED_EXEC
线性区的处理
线性区的操作有分配和释放两种,分别在do_map()和do_ummap()中实现。这两个函数在下面两小节描述。本节先看要实现这些操作所需要的一些底层处理函数。查找给定地址的最邻近区
find_vma()查找线性区的vm_end字段大于addr的第一个线性区的位置,并返回这个线性区描述符的地址。如果不存在这样的线性区,就返回NULL。
=============== mm/mmap.c 1592 1626 =============== /* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) { struct vm_area_struct *vma = NULL; if (mm) { /* Check the cache first. */ /* (Cache hit rate is typically around 35%.) */ vma = mm->mmap_cache; if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { struct rb_node * rb_node; rb_node = mm->mm_rb.rb_node; vma = NULL; while (rb_node) { struct vm_area_struct * vma_tmp; vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); if (vma_tmp->vm_end > addr) { vma = vma_tmp; if (vma_tmp->vm_start <= addr) break; rb_node = rb_node->rb_left; } else rb_node = rb_node->rb_right; } if (vma) mm->mmap_cache = vma; } } return vma; }
每个内存描述符有一个mmap_cache字段,它保存进程最后一次引用线性区的描述符地址。根据局部性原理,利用这个字段可以加速搜索。该函数就从这个值开始,如果它指定的线性区就是要找的,可以直接返回。这种做法似曾相识,我们在前一讲的永久内核映射中遇到过。
如果mmap_cache不是想要的,就在红黑树中查找。
函数
find_vma_prev()与
find_vma()类似,不同的是它把函数选中的前一个线性区描述符的指针赋值给附加参数pprev。
=============== mm/mmap.c 1630 1663 =============== /* Same as find_vma, but also return a pointer to the previous VMA in *pprev. */ struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev) { struct vm_area_struct *vma = NULL, *prev = NULL; struct rb_node *rb_node; if (!mm) goto out; /* Guard against addr being lower than the first VMA */ vma = mm->mmap; /* Go through the RB tree quickly. */ rb_node = mm->mm_rb.rb_node; while (rb_node) { struct vm_area_struct *vma_tmp; vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); if (addr < vma_tmp->vm_end) { rb_node = rb_node->rb_left; } else { prev = vma_tmp; if (!prev->vm_next || (addr < prev->vm_next->vm_end)) break; rb_node = rb_node->rb_right; } } out: *pprev = prev; return prev ? prev->vm_next : vma; }
还有一个函数
find_vma_prepare()确定新线性区在红黑树的位置,并返回前一个线性区的地址和要插入的叶子节点的父节点的地址。这里不再分析其源码。
查找一个与给定的地址区间相重叠的线性区
find_vma_intersection()函数查找与某个线性地址区间相重叠的第一个线性区。由start_addr和end_addr指定这个区间。
=============== include/linux/mm.h 1377 1386 =============== /* Look up the first VMA which intersects the interval start_addr..end_addr-1, NULL if none. Assume start_addr < end_addr. */ static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr) { struct vm_area_struct * vma = find_vma(mm,start_addr); if (vma && end_addr <= vma->vm_start) vma = NULL; return vma; }
它利用find_vma寻找比start_addr大的线性区间,如果这个区间存在且起始地址小于指定的结束地址,则返回这个线性区间,否则返回NULL。
查找一个空间的地址空间
get_unmapped_area()搜查进程的地址空间以找到一个可以使用的线性地址区间。len参数指定区间的长度,而非空的addr参数指定从哪个地址开始查找。如果查找成功,函数返回这个新区间的起始地址,否则返回错误码-ENOMEM。
=============== mm/mmap.c 1560 1588 =============== unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { unsigned long (*get_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); unsigned long error = arch_mmap_check(addr, len, flags); if (error) return error; /* Careful about overflows.. */ if (len > TASK_SIZE) return -ENOMEM; get_area = current->mm->get_unmapped_area; if (file && file->f_op && file->f_op->get_unmapped_area) get_area = file->f_op->get_unmapped_area; addr = get_area(file, addr, len, pgoff, flags); if (IS_ERR_VALUE(addr)) return addr; if (addr > TASK_SIZE - len) return -ENOMEM; if (addr & ~PAGE_MASK) return -EINVAL; return arch_rebalance_pgtables(addr, len); }
函数首先检查区间长度是否在用户态地址区间TASK_SIZE(通常是3GB)内,然后根据file指针判断线性地址空间是用于文件内存映射还是匿名内存映射,从而调用不同的get_area函数指针。最后判断找到的地址是否合法,如果合法就返回该地址。
因此这个函数的核心是get_area,对于文件内存映射的情况,我们留到文件系统一讲中再分析,这里看正常情况,current->mm->get_unmapped_area。
如本讲开始所讲,在进程创建之初,根据进程内存是经典布局还是灵活布局,get_unmapped_area被初始化为两种情况。
=============== arch/x86/mm/mmap.c 125 136 =============== /* * This function, called very early during the creation of a new * process VM image, sets up which VM layout function to use: */ void arch_pick_mmap_layout(struct mm_struct *mm) { if (mmap_is_legacy()) { mm->mmap_base = mmap_legacy_base(); mm->get_unmapped_area = arch_get_unmapped_area; mm->unmap_area = arch_unmap_area; } else { mm->mmap_base = mmap_base(); mm->get_unmapped_area = arch_get_unmapped_area_topdown; mm->unmap_area = arch_unmap_area_topdown; } }
我们这里只讨论经典布局。
=============== mm/mmap.c 1388 1442 =============== unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags) { struct mm_struct *mm = current->mm; struct vm_area_struct *vma; unsigned long start_addr; if (len > TASK_SIZE) return -ENOMEM; if (flags & MAP_FIXED) return addr; if (addr) { addr = PAGE_ALIGN(addr); vma = find_vma(mm, addr); if (TASK_SIZE - len >= addr && (!vma || addr + len <= vma->vm_start)) return addr; } if (len > mm->cached_hole_size) { start_addr = addr = mm->free_area_cache; } else { start_addr = addr = TASK_UNMAPPED_BASE; mm->cached_hole_size = 0; } full_search: for (vma = find_vma(mm, addr); ; vma = vma->vm_next) { /* At this point: (!vma || addr < vma->vm_end). */ if (TASK_SIZE - len < addr) { /* * Start a new search - just in case we missed * some holes. */ if (start_addr != TASK_UNMAPPED_BASE) { addr = TASK_UNMAPPED_BASE; start_addr = addr; mm->cached_hole_size = 0; goto full_search; } return -ENOMEM; } if (!vma || addr + len <= vma->vm_start) { /* * Remember the place where we stopped the search: */ mm->free_area_cache = addr + len; return addr; } if (addr + mm->cached_hole_size < vma->vm_start) mm->cached_hole_size = vma->vm_start - addr; addr = vma->vm_end; } }
这个函数分配从低端向高端地址移动的线性区。函数再次检查区间长度是否在用户态地址区间TASK_SIZE(通常是3GB)内。如果addr不为0,就试图从addr开始分配区间,这里会把addr的值调整为4KB对齐。
如果addr为0或前面的搜索失败,函数就扫描用户态线性地址空间以查找一个可用空间。为了提高搜索速度,会从最近被分配的线性区后面开始,如果找不到就从1/3处重新开始。这里1/3的原因如本讲开始处所述。
========= arch/x86/include/asm/processor.h 966 966 ======== #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))
调用
find_vma()找到第一个线性区终点位置后,有三种可能情况:
如果所请求区间大于正待扫描的线性地址空间部分(TASK_SIZE - len < addr),就从三分之一处重新开始搜索;如果已完成第二次搜索,就返回错误。
如果刚扫描过的线性区后面的空闲区没有足够的大小(!vma || addr + len <= vma->vm_start),就继续找下一个线性区。
如果不是以上两种情况,那么找到了所要的空闲区,返回addr。
向内存描述符链表中插入一个线性区
insert_vm_struct()函数在线性区对象链表和内存描述符的红黑树中插入一个vm_area_struct结构。
=============== mm/mmap.c 2301 2330 =============== /* Insert vm structure into process list sorted by address * and into the inode's i_mmap tree. If vm_file is non-NULL * then i_mmap_lock is taken here. */ int insert_vm_struct(struct mm_struct * mm, struct vm_area_struct * vma) { struct vm_area_struct * __vma, * prev; struct rb_node ** rb_link, * rb_parent; /* * The vm_pgoff of a purely anonymous vma should be irrelevant * until its first write fault, when page's anon_vma and index * are set. But now set the vm_pgoff it will almost certainly * end up with (unless mremap moves it elsewhere before that * first wfault), so /proc/pid/maps tells a consistent story. * * By setting it to reflect the virtual start address of the * vma, merges and splits can happen in a seamless way, just * using the existing file pgoff checks and manipulations. * Similarly in do_mmap_pgoff and in do_brk. */ if (!vma->vm_file) { BUG_ON(vma->anon_vma); vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT; } __vma = find_vma_prepare(mm,vma->vm_start,&prev,&rb_link,&rb_parent); if (__vma && __vma->vm_start < vma->vm_end) return -ENOMEM; if ((vma->vm_flags & VM_ACCOUNT) && security_vm_enough_memory_mm(mm, vma_pages(vma))) return -ENOMEM; vma_link(mm, vma, prev, rb_link, rb_parent); return 0; }
函数先调用
find_vma_prepare()在红黑树中查找vma该位于何处,然后又调用
vma_link()插入到链表和红黑树中,以及做其他一些处理。这里不再分析其源码。
分配线性地址区间
现在开始讨论如何分配一个新的线性地址区间。为此do_mmap()函数为当前进程创建并初始化一个新的线性区。分配成功后,可以把这个新的线性区与进程已有的其他线性区进行合并。
=============== include/linux/mm.h 1307 1318 =============== static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) { unsigned long ret = -EINVAL; if ((offset + PAGE_ALIGN(len)) < offset) goto out; if (!(offset & ~PAGE_MASK)) ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT); out: return ret; }
函数参数中,file和offset用于将文件映射到内存,这在文件系统一讲中讨论;addr和len指定查找的起始和长度;prot是线性区所含页的访问权限,包括VM_READ、VM_WRITE、VM_EXEC及空;flag指定其他标志,可能取值有:
MAP_GROWSDOWN、MAP_LOCKED、MAP_DENYWRITE和MAP_EXCUTABLE:字面意思。
MAP_SHARED和MAP_PRIVATE:前一个表示线性区的页可以被几个进程共享,后一个相反。
MAP_FIXED:起始地址必须为addr指定值。
MAP_ANONYMOUS:没有文件与这个线性区相关联(在文件系统中详述)。
MAP_NORESERVE:函数不必预告检查空闲页框的数目。
MAP_POPULATE:函数应该为线性区建立的映射提前分配需要的页框。只在映射文件和IPC共享线性区时有意义。
MAP_NOBLOCK:只在MAP_POPULATE时有意义:分配页框时,函数不能阻塞。
函数先检查len和offset值,然后执行
do_mmap_pgoff()函数。这里假设新的线性地址区间映射的不是磁盘文件,我们来看这个函数。
=============== mm/mmap.c 962 1101 =============== /* * The caller must hold down_write(¤t->mm->mmap_sem). */ 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 inode *inode; unsigned int vm_flags; int error; unsigned long reqprot = prot; /* * Does the application expect PROT_READ to imply PROT_EXEC? * * (the exception is when the underlying filesystem is noexec * mounted, in which case we dont add PROT_EXEC.) */ if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC)) if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC))) prot |= PROT_EXEC; if (!len) return -EINVAL; if (!(flags & MAP_FIXED)) addr = round_hint_to_min(addr); /* Careful about overflows.. */ len = PAGE_ALIGN(len); if (!len) return -ENOMEM; /* offset overflow? */ if ((pgoff + (len >> PAGE_SHIFT)) < pgoff) return -EOVERFLOW; /* Too many mappings? */ if (mm->map_count > sysctl_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_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; if (flags & MAP_LOCKED) if (!can_do_mlock()) return -EPERM; /* mlock MCL_FUTURE? */ if (vm_flags & VM_LOCKED) { unsigned long locked, lock_limit; locked = len >> PAGE_SHIFT; locked += mm->locked_vm; lock_limit = rlimit(RLIMIT_MEMLOCK); lock_limit >>= PAGE_SHIFT; if (locked > lock_limit && !capable(CAP_IPC_LOCK)) return -EAGAIN; } inode = file ? file->f_path.dentry->d_inode : NULL; 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(inode) && (file->f_mode & FMODE_WRITE)) return -EACCES; /* * Make sure there are no mandatory locks on the file. */ if (locks_verify_locked(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; if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) { if (vm_flags & VM_EXEC) return -EPERM; vm_flags &= ~VM_MAYEXEC; } if (!file->f_op || !file->f_op->mmap) return -ENODEV; break; default: return -EINVAL; } } else { switch (flags & MAP_TYPE) { case MAP_SHARED: /* * Ignore pgoff. */ pgoff = 0; vm_flags |= VM_SHARED | VM_MAYSHARE; break; case MAP_PRIVATE: /* * Set pgoff according to addr for anon_vma. */ pgoff = addr >> PAGE_SHIFT; break; default: return -EINVAL; } } error = security_file_mmap(file, reqprot, prot, flags, addr, 0); if (error) return error; return mmap_region(file, addr, len, flags, vm_flags, pgoff); }
1.函数先对权限做调整,策略如前所述。然后检查参数确定是否无法分配。
2.调用
get_unmapped_area()获得新线性区的线性地址区间。
3.通过把存放在prot和flags参数中的值进行组合来计算新线性区描述符的标志vm_flags并对请求的属性进行检查。
4.最后调用
mmap_region()来做最主要的创建和初始化工作。
=============== mm/mmap.c 1217 1373 =============== unsigned long mmap_region(struct file *file, unsigned long addr, unsigned long len, unsigned long flags, unsigned int vm_flags, unsigned long pgoff) { struct mm_struct *mm = current->mm; struct vm_area_struct *vma, *prev; int correct_wcount = 0; int error; struct rb_node **rb_link, *rb_parent; unsigned long charged = 0; struct inode *inode = file ? file->f_path.dentry->d_inode : NULL; /* Clear old maps */ error = -ENOMEM; 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 (!may_expand_vm(mm, len >> PAGE_SHIFT)) return -ENOMEM; /* * Set 'VM_NORESERVE' if we should not account for the * memory use of this mapping. */ if ((flags & MAP_NORESERVE)) { /* We honor MAP_NORESERVE if allowed to overcommit */ if (sysctl_overcommit_memory != OVERCOMMIT_NEVER) vm_flags |= VM_NORESERVE; /* hugetlb applies strict overcommit unless MAP_NORESERVE */ if (file && is_file_hugepages(file)) vm_flags |= VM_NORESERVE; } /* * Private writable mapping: check memory availability */ if (accountable_mapping(file, vm_flags)) { charged = len >> PAGE_SHIFT; if (security_vm_enough_memory(charged)) return -ENOMEM; vm_flags |= VM_ACCOUNT; } /* * Can we just expand an old mapping? */ vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL); if (vma) 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_zalloc(vm_area_cachep, GFP_KERNEL); if (!vma) { error = -ENOMEM; goto unacct_error; } vma->vm_mm = mm; vma->vm_start = addr; vma->vm_end = addr + len; vma->vm_flags = vm_flags; vma->vm_page_prot = vm_get_page_prot(vm_flags); vma->vm_pgoff = pgoff; INIT_LIST_HEAD(&vma->anon_vma_chain); 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; if (vm_flags & VM_EXECUTABLE) added_exe_file_vma(mm); /* Can addr have changed?? * * Answer: Yes, several device drivers can do it in their * f_op->mmap method. -DaveM */ addr = vma->vm_start; pgoff = vma->vm_pgoff; vm_flags = vma->vm_flags; } else if (vm_flags & VM_SHARED) { error = shmem_zero_setup(vma); if (error) goto free_vma; } if (vma_wants_writenotify(vma)) { pgprot_t pprot = vma->vm_page_prot; /* Can vma->vm_page_prot have changed?? * * Answer: Yes, drivers may have changed it in their * f_op->mmap method. * * Ensures that vmas marked as uncached stay that way. */ vma->vm_page_prot = vm_get_page_prot(vm_flags & ~VM_SHARED); if (pgprot_val(pprot) == pgprot_val(pgprot_noncached(pprot))) vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); } vma_link(mm, vma, prev, rb_link, rb_parent); file = vma->vm_file; /* Once vma denies write, undo our temporary denial count */ if (correct_wcount) atomic_inc(&inode->i_writecount); out: perf_event_mmap(vma); mm->total_vm += len >> PAGE_SHIFT; vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT); if (vm_flags & VM_LOCKED) { if (!mlock_vma_pages_range(vma, addr, addr + len)) mm->locked_vm += (len >> PAGE_SHIFT); } else if ((flags & MAP_POPULATE) && !(flags & MAP_NONBLOCK)) make_pages_present(addr, addr + len); return addr; unmap_and_free_vma: if (correct_wcount) atomic_inc(&inode->i_writecount); vma->vm_file = NULL; fput(file); /* Undo any partial mapping done by a device driver. */ unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end); charged = 0; free_vma: kmem_cache_free(vm_area_cachep, vma); unacct_error: if (charged) vm_unacct_memory(charged); return error; }
1.调用
find_vma_prepare()来确定处理新区间之前的线性区对象的位置,以及在红黑树中新线性区的位置。
find_vma_prepare()还检查是否还存在与新区间重叠的区域,如果存在,调用
do_munmap()删除新的区间,然后重新寻找。删除线性区间在下一小节介绍。
2.检查进程对线性区大小的限制,是否允许插入新的线性区。
3.如果flags参数中有MAP_NORESERVE标志,没看明白。
4.检查私有区间,没看明白。
5.调用
vma_merge()来与相邻线性区间合并。若扩展成功,跳到第10步。
6.扩展不成功,需要为新线性区分配描述符。用slab接口申请一个vm_area_struct数据结构并初始化。
7.跳过文件映射;如果不映射文件且线性区是共享的,调用
shmem_zero_setup()来初始化。
8.对于写时复制的共享区域,有进程对其进行写操作时候需要复制,这时对该区域属性特殊标注。
9.调用
vma_link()把新线性区插入到线性区链表和红黑树中。
10.增加内存描述符total_vm字段中进程地址空间大小。
11.如果设置了VM_LOCKED标志,就调用
mlock_vma_pages_range()锁定地址,还会立即分配线性区的所有页,并把它们锁在RAM中。这一步需要操作页表完成。
12.最后函数返回新线性区的线性地址。
释放线性地址区间
内核使用do_munmap()函数从当前进程的地址空间删除一个线性地址区间。
=============== mm/mmap.c 2041 2120 =============== int do_munmap(struct mm_struct *mm, unsigned long start, size_t len) { unsigned long end; struct vm_area_struct *vma, *prev, *last; if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start) return -EINVAL; if ((len = PAGE_ALIGN(len)) == 0) return -EINVAL; /* Find the first overlapping VMA */ vma = find_vma_prev(mm, start, &prev); if (!vma) return 0; /* we have start < vma->vm_end */ /* if it doesn't overlap, we have nothing.. */ end = start + len; if (vma->vm_start >= end) return 0; /* * If we need to split any vma, do it now to save pain later. * * Note: mremap's move_vma VM_ACCOUNT handling assumes a partially * unmapped vm_area_struct will remain in use: so lower split_vma * places tmp vma above, and higher split_vma places tmp vma below. */ if (start > vma->vm_start) { int error; /* * Make sure that map_count on return from munmap() will * not exceed its limit; but let map_count go just above * its limit temporarily, to help free resources as expected. */ if (end < vma->vm_end && mm->map_count >= sysctl_max_map_count) return -ENOMEM; error = __split_vma(mm, vma, start, 0); if (error) return error; prev = vma; } /* Does it split the last one? */ last = find_vma(mm, end); if (last && end > last->vm_start) { int error = __split_vma(mm, last, end, 1); if (error) return error; } vma = prev? prev->vm_next: mm->mmap; /* * unlock any mlock()ed ranges before detaching vmas */ if (mm->locked_vm) { struct vm_area_struct *tmp = vma; while (tmp && tmp->vm_start < end) { if (tmp->vm_flags & VM_LOCKED) { mm->locked_vm -= vma_pages(tmp); munlock_vma_pages_all(tmp); } tmp = tmp->vm_next; } } /* * Remove the vma's, and unmap the actual pages */ detach_vmas_to_be_unmapped(mm, vma, prev, end); unmap_region(mm, vma, prev, start, end); /* Fix up all other VM information */ remove_vma_list(mm, vma); return 0; }
函数主要有两个阶段:第一阶段,扫描进程所拥有的线性区链表,并把包含在进程地址空间的线性地址区间的所有线性区从链表中解除链接。第二阶段,更新进程页表,并把第一阶段找到并标识出的线性区删除。
这里不对其进行详细分析,我们来看其中用到的
split_vma()函数和
unmap_region()函数。前者的主体是
__split_vma(),它把与线性地址区间交叉的线性区划分成两个较小的区,一个在线性地址区外部,另一个在线性地址区内部。
=============== mm/mmap.c 1946 2021 =============== /* * __split_vma() bypasses sysctl_max_map_count checking. We use this on the * munmap path where it doesn't make sense to fail. */ static int __split_vma(struct mm_struct * mm, struct vm_area_struct * vma, unsigned long addr, int new_below) { struct mempolicy *pol; struct vm_area_struct *new; int err = -ENOMEM; if (is_vm_hugetlb_page(vma) && (addr & ~(huge_page_mask(hstate_vma(vma))))) return -EINVAL; new = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL); if (!new) goto out_err; /* most fields are the same, copy all, and then fixup */ *new = *vma; INIT_LIST_HEAD(&new->anon_vma_chain); if (new_below) new->vm_end = addr; else { new->vm_start = addr; new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT); } pol = mpol_dup(vma_policy(vma)); if (IS_ERR(pol)) { err = PTR_ERR(pol); goto out_free_vma; } vma_set_policy(new, pol); if (anon_vma_clone(new, vma)) goto out_free_mpol; if (new->vm_file) { get_file(new->vm_file); if (vma->vm_flags & VM_EXECUTABLE) added_exe_file_vma(mm); } if (new->vm_ops && new->vm_ops->open) new->vm_ops->open(new); if (new_below) err = vma_adjust(vma, addr, vma->vm_end, vma->vm_pgoff + ((addr - new->vm_start) >> PAGE_SHIFT), new); else err = vma_adjust(vma, vma->vm_start, addr, vma->vm_pgoff, new); /* Success. */ if (!err) return 0; /* Clean everything up if vma_adjust failed. */ if (new->vm_ops && new->vm_ops->close) new->vm_ops->close(new); if (new->vm_file) { if (vma->vm_flags & VM_EXECUTABLE) removed_exe_file_vma(mm); fput(new->vm_file); } unlink_anon_vmas(new); out_free_mpol: mpol_put(pol); out_free_vma: kmem_cache_free(vm_area_cachep, new); out_err: return err; }
1.首先调用kmem_cache_alloc()获得线性区描述符,并用vma描述符来初始化。
2.变量new_below表示线性地址区间的起始地址在vma的内部还是外部来做不同赋值。
3.如果定义了新线性区的open方法,执行open操作。
4.把新线性区的描述符链接到线性区链表mm->mmap和红黑树mm->mm_rb。此外,函数还对红黑树进行调整。
5.成功返回0,否则做清理工作并返回错误码。
unmap_region()函数遍历线性区链表并释放它们的页框。
=============== mm/mmap.c 1891 1912 =============== /* * Get rid of page table information in the indicated region. * * Called with the mm semaphore held. */ static void unmap_region(struct mm_struct *mm, struct vm_area_struct *vma, struct vm_area_struct *prev, unsigned long start, unsigned long end) { struct vm_area_struct *next = prev? prev->vm_next: mm->mmap; struct mmu_gather *tlb; unsigned long nr_accounted = 0; lru_add_drain(); tlb = tlb_gather_mmu(mm, 0); update_hiwater_rss(mm); unmap_vmas(&tlb, vma, start, end, &nr_accounted, NULL); vm_unacct_memory(nr_accounted); free_pgtables(tlb, vma, prev? prev->vm_end: FIRST_USER_ADDRESS, next? next->vm_start: 0); tlb_finish_mmu(tlb, start, end); }
1.调用
lru_add_drain(),这与回收页框有关,以后再讲。
2.调用
tlb_gather_mmu()初始化每CPU变量mmu_gathers。在x86中这个变量存放内存描述符指针mm。再把mmu_gathers变量的地址保存在局部变量tlb中。
3.调用unmap_vms扫描线性地址空间的所有页表项,再释放相应的页。
4.调用
free_pgtables()回收在上一步已清空的进程页表。
5.调用
tlb_finish_mmu()刷新TLB,做一些回收页框相关的工作。
缺页异常处理程序
在Linux的内存管理子系统中,很多情况都不做真正的页框分配,这在多数时间能大幅提高效率。例如前面说过,当进程申请空间时,内核只扩展其线性区,并没有真正分配页框。但是稍后当进程访问这个地址时,由于对应的页表项为空,将触发缺页异常。这种情况引起的缺页异常属于“正常”情况,而编程错误也有可能触发缺页异常,这属于真正的“异常”。所有的缺页异常都由缺页异常处理程序解决。
主处理流程
80x86上的缺页中断服务程序是do_page_fault()函数,它需要区分多种引起异常的情况以分别处理。
实际情况比上图复杂得多,因为内核还需要区分一些更细节的特殊情况。其详细流程图如下所示。
我们来分段仔细分析其代码。
do_page_fault()函数输入参数有寄存器值和错误码。如果错误码第0位清0,则异常由访问一个不存在的页引起;否则异常由无效的访问权限引起。如果第1位清0,则异常由读访问或执行访问引起;否则异常由写访问引起。如果第2位清0,则异常发生在内核态,否则异常发生在用户态。
=============== arch/x86/mm/fault.c 947 968=============== /* * This routine handles page faults. It determines the address, * and the problem, and then passes it off to one of the appropriate * routines. */ dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code) { struct vm_area_struct *vma; struct task_struct *tsk; unsigned long address; struct mm_struct *mm; int fault; int write = error_code & PF_WRITE; unsigned int flags = FAULT_FLAG_ALLOW_RETRY | (write ? FAULT_FLAG_WRITE : 0); tsk = current; mm = tsk->mm; /* Get the faulting address: */ address = read_cr2();
1.在发生异常时CPU硬件将引起缺页的线性地址存放在CR2中。首先读CR2来获取并保存在address中。
=============== arch/x86/mm/fault.c 981 997 =============== /* * We fault-in kernel-space virtual memory on-demand. The * 'reference' page table is init_mm.pgd. * * NOTE! We MUST NOT take any locks for this case. We may * be in an interrupt or a critical region, and should * only copy the information from the master page table, * nothing more. * * This verifies that the fault happens in kernel space * (error_code & 4) == 0, and that the fault was not a * protection error (error_code & 9) == 0. */ if (unlikely(fault_in_kernel_space(address))) { if (!(error_code & (PF_RSVD | PF_USER | PF_PROT))) { if (vmalloc_fault(address) >= 0) return;
对于访问内核空间,如果是内核试图访问不存在的页框引起的异常,就用
vmalloc_fault()处理,这种情况下将该进程的页表与内核参考页表同步。在稍后详细说明。
=============== arch/x86/mm/fault.c 1042 1049 =============== /* * If we're in an interrupt, have no user context or are running * in an atomic region then we must not take the fault: */ if (unlikely(in_atomic() || !mm)) { bad_area_nosemaphore(regs, error_code, address); return; }
3.如果异常在中断或内核线程中发生,则调用
bad_area_nosemaphore()处理,该函数对于来自用户模式的异常发送一个SIGSEGV信号标明段错误;对于内核空间的异常,调用
fixup_exception()处理,这个函数稍后说明。
=============== arch/x86/mm/fault.c 1084 1110 =============== vma = find_vma(mm, address); if (unlikely(!vma)) { bad_area(regs, error_code, address); return; } if (likely(vma->vm_start <= address)) goto good_area; if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) { bad_area(regs, error_code, address); return; } if (error_code & PF_USER) { /* * Accessing the stack below %sp is always a bug. * The large cushion allows instructions like enter * and pusha to work. ("enter $65535, $31" pushes * 32 pointers and then decrements %sp by 65535.) */ if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) { bad_area(regs, error_code, address); return; } } if (unlikely(expand_stack(vma, address))) { bad_area(regs, error_code, address); return; }
4.如果地址来源于用户空间,先通过一系列方法来检查该地址是否合法。检查过程会覆盖以下几种情况:
没有通过
find_vma()发现结束地址大于该访问地址的线性区,则访问无效;
否则如果找到的线性区包含该地址,则正常处理;
否则要访问的地址小于找到线性区的起始地址,如果该线性区不具有VM_GROWSDOWN标志,则是栈以外的线性区,该访问无效;
否则是在访问栈空间以外的地址,栈是自顶向下增长的,只有压栈溢出才会出现这种情况,且在x86中溢出值最多不会超过一个特定限度,否则就是无效访问;
最后是有效的栈溢出。
最终有三种结果:调用
bad_area()处理非法地址和无效访问,或跳转到good_area来处理正常的请求,或调用
expand_stack()扩展栈区域。
这里也可以看出Linux的一个特性:当程序使用的栈空间大于默认分配的栈空间时候,系统会自动扩展栈空间,使得程序能正常运行。
处理进程地址空间外的错误地址
如果要访问的地址空间不属于进程的地址空间,那么调用bad_area()处理,它最终调用
__bad_area_nosemaphore()。
=============== arch/x86/mm/fault.c 706 707 =============== /* User mode accesses just cause a SIGSEGV */ if (error_code & PF_USER) { =============== arch/x86/mm/fault.c 726 740 =============== /* Kernel addresses are always protection faults: */ tsk->thread.cr2 = address; tsk->thread.error_code = error_code | (address >= TASK_SIZE); tsk->thread.trap_no = 14; force_sig_info_fault(SIGSEGV, si_code, address, tsk, 0); return; } if (is_f00f_bug(regs, address)) return; no_context(regs, error_code, address); }
该函数判断当错误发生在用户模式时,给进程发送一个信号。关于信号相关的内容在中断、异常和信号一节中涉及。进程收到该信号后可以读出出错的地址、错误码等信息,做相应处理。
2.如果错误发生在内核态,则先检测是否是x86的一个F00F bug并处理,否则调用
no_context()处理。
no_context()区分两种情况,一种是异常由系统调用的参数引起,一种是内核缺陷。前一种情况在今后讲到系统调用时再分析,后一种情况会引起内核“Oops”。
处理进程地址空间内的错误地址
如果要访问的地址空间属于进程的地址空间,那么属于正常访问,跳转到good_area标签来处理。=============== arch/x86/mm/fault.c 1112 1132 =============== /* * Ok, we have a good vm_area for this memory access, so * we can handle it.. */ good_area: if (unlikely(access_error(error_code, vma))) { bad_area_access_error(regs, error_code, address); return; } /* * If for any reason at all we couldn't handle the fault, * make sure we exit gracefully rather than endlessly redo * the fault: */ fault = handle_mm_fault(mm, vma, address, flags); if (unlikely(fault & VM_FAULT_ERROR)) { mm_fault_error(regs, error_code, address, fault); return; }
1.即使地址合法,也需要检查访问权限是否合法,通过access_error()来检查。可能有以下几种情况:
在写访问的情况下,线性区需要有写访问权限,否则访问无效;
如果是读存在的页,该异常一定是硬件检测到的权限异常;
如果读不存在的页,则线性区需要有读或执行权限,否则访问无效。
几种情况下只要是无效访问,最后都调用bad_area_access_error()统一处理。
2.如果是合法访问,调用
handle_mm_fault()处理,有许多情况将进入这个分支,将在后面一一详细说明。
3.
handle_mm_fault()函数可能发生页映射建立失败,这种情况下调用
mm_fault_error()处理,它根据失败情况可能终止该进程,也可能发送SIGBUS信号给进程。
=============== arch/x86/mm/fault.c 1134 1155 =============== /* * Major/minor page fault accounting is only done on the * initial attempt. If we go through a retry, it is extremely * likely that the page will be found in page cache at that point. */ if (flags & FAULT_FLAG_ALLOW_RETRY) { if (fault & VM_FAULT_MAJOR) { tsk->maj_flt++; perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, 0, regs, address); } else { tsk->min_flt++; perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, 0, regs, address); } if (fault & VM_FAULT_RETRY) { /* Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk * of starvation. */ flags &= ~FAULT_FLAG_ALLOW_RETRY; goto retry; } }
1.如果页建立成功,则
handle_mm_fault()函数返回VM_FAULT_MAJOR或VM_FAULT_MINOR,分别对应于数据在块设备中或数据已在内存中,这时内核更新进程的一些统计量。
2.如果返回VM_FAULT_RETRY,则跳转到异常处理较早的位置重试,且仅允许重试一次。这发生在
handle_mm_fault()进行了磁盘页交换操作的情况下,一般重新试一次就可以获得正常的映射。
handle_mm_fault()函数不依赖于底层体系结构,该函数确认在各级页目录中对应异常地址的各目录项目都存在。最后调用
handle_pte_fault()分析缺页异常的原因。
1.如果被访问的页不存在,则内核分配一个新的页框并适当初始化。这种技术称为请求调页。
2.如果被访问的页存在但只读,那么内核分配一个新的页框并用旧页框的数据初始化,这种技术称为写时复制。
请求调页
请求调页指的是一种动态内存分配技术,它把页框的分配推迟到不能再推迟为止,由此引起一个缺页异常。这是进程内存管理的灵魂思想。其背后的原因在于,进程开始运行的时候并不访问其地址空间中的全部地址;事实上有一部分地址也许永远也不会被使用。另外程序的局部性原理保证了在程序执行的每个阶段,真正引用的进程页只有一小部分,因此临时不用的页所在的页框可以由其他进程来使用。这样可以在物理内存有限的情况下,使系统从总体上获得更大的吞吐量。这些优点的代价是系统额外的开销,即每次这样的情况都会触发缺页异常。然而局部性原理保证了缺页异常是一种低概率事件。
缺页异常处理请求调页时分为两种情况,若是内核将页框回收导致,则必须从系统的某个交换区换入,这在页面回收中再详述;若是进程从未访问过该页,则要分配新的页框并初始化,这进一步分为两类。一是该页映射了一个磁盘文件,一是该页未映射文件。我们只讨论后一种情况。
这时调用
do_anonymous_page()处理。它调用
alloc_zeroed_user_highpage_movable()在高端内存区申请一个新页,并清空其内容。接下来将页加入到进程的页表,并更新高速缓存或MMU。
do_anonymous_page()涉及了一些细节,这里不再详细分析。
写时复制
在下一讲和后一节都会看到,内核在建立进程时复制父进程的线性空间和页表,即与父进程共享页框。但是当其中一个进程需要改写这个地址空间的共享页框时,会产生一个缺页异常,这时内核把这个页复制到一个新的页框并改写,而原来的页框仍然是写保护的,等另一个进程试图写入时再检查写进程是否是这个页框的唯一属主,如果是,就把页框标记为对这个进程可写。本节描述Linux如何实现写时复制。在缺页异常处理中,这种情况最终要调用
do_wp_page(),我们省去一些细节,关注主要部分。其代码流程图如下所示。
1.内核首先调用
vm_normal_page(),通过页表项找到页描述符,本质上这个函数基于
pte_pfn()和
pfn_to_page(),前者查找与页表项相关的页号,而后者确定与页号相关的页描述符。
2.在用
page_cache_get()获取页之后,接下来
anon_vma_prepare()准备好逆向映射机制的数据结构,以接受一个新的匿名区域。
3.由于异常的来源是需要将一个充满有用数据的页框复制到新页框,因此内核调用
alloc_page_vma()分配一个新页框。
4.
cow_user_page()接下来将异常页框的数据复制到新页框,进程随后可以对新页框进行写操作。
5.然后使用
page_remove_rmap(),删除到原来的只读页的逆向映射。
6.新页框添加到页表,此时也必须更新CPU的高速缓存。
7.最后,使用
lru_cache_add_active()将新分配的页放置到LRU缓存的活动列表上,并通过
page_add_anon_rmap()将其插入到逆向映射数据结构。
此后,用户空间进程可以向页写入数据。
处理内核空间地址
上一讲中提到,内核在更新非连续内存区对应的页表项时是非常懒惰的。事实上,vmalloc()和
vfree()函数只更新主内核页表,即init_mm.pgd和它的子页表。
但是正常情况下进程和内核线程都不直接使用主内核页表。因此对非连续内存区的访问会引发缺页异常。这种情况在
vmalloc_fault()中处理。
=============== arch/x86/mm/fault.c 259 288 =============== static noinline __kprobes int vmalloc_fault(unsigned long address) { unsigned long pgd_paddr; pmd_t *pmd_k; pte_t *pte_k; /* Make sure we are in vmalloc area: */ if (!(address >= VMALLOC_START && address < VMALLOC_END)) return -1; WARN_ON_ONCE(in_nmi()); /* * Synchronize this task's top level page-table * with the 'reference' page table. * * Do _not_ use "current" here. We might be inside * an interrupt in the middle of a task switch.. */ pgd_paddr = read_cr3(); pmd_k = vmalloc_sync_one(__va(pgd_paddr), address); if (!pmd_k) return -1; pte_k = pte_offset_kernel(pmd_k, address); if (!pte_present(*pte_k)) return -1; return 0; }
函数首先取出CR3寄存器中的当前进程页全局目录地址(这是个物理地址),然后用__va()宏转换为虚拟地址后传入
vmalloc_sync_one()函数,由其计算并设置页上级目录和页中间目录,再调用
pte_offset_kernel()函数计算并设置页表项。其计算方法是从init_mm中复制。
这个过程中出现目录项为空的情况下,都返回-1表示错误。
创建和删除进程的地址空间
在本将的第一节中讲述了进程获得一个新的线性区的六种情况。除此之外,fork()系统调用要求为子系统创建一个完整的新地址空间,进程结束时,内核撤销它的地址空间。这一节我们讨论这两种操作。创建进程的地址空间
在下一讲进程管理中,我们会介绍到当创建一个新的进程时内核调用copy_mm()函数,这个函数将建立新进程的所有页表和内存描述符,从而创建进程的地址空间。
=============== kernel/exit.c 709 756 =============== static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) { struct mm_struct * mm, *oldmm; int retval; tsk->min_flt = tsk->maj_flt = 0; tsk->nvcsw = tsk->nivcsw = 0; #ifdef CONFIG_DETECT_HUNG_TASK tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw; #endif tsk->mm = NULL; tsk->active_mm = NULL; /* * Are we cloning a kernel thread? * * We need to steal a active VM for that.. */ oldmm = current->mm; if (!oldmm) return 0; if (clone_flags & CLONE_VM) { atomic_inc(&oldmm->mm_users); mm = oldmm; goto good_mm; } retval = -ENOMEM; mm = dup_mm(tsk); if (!mm) goto fail_nomem; good_mm: /* Initializing for Swap token stuff */ mm->token_priority = 0; mm->last_interval = 0; if (tsk->signal->oom_score_adj == OOM_SCORE_ADJ_MIN) atomic_inc(&mm->oom_disable_count); tsk->mm = mm; tsk->active_mm = mm; return 0; fail_nomem: return retval; }
1.调用
clone()函数(设置了CLONE_VM标志)创建的轻量级进程共享同一地址空间。
2.否则就要调用
dum_mm()创建一个新的地址空间。
dum_mm()调用的函数较多,这里不逐行分析其源码。这个函数为新进程申请一个新的内存描述符,并用当前进程的内存描述符值初始化它,再改变其中一些字段,然后分配页全局目录,最后复制父进程的线性区和父进程的页表。在这个过程中,它会把所有私有的、可写的页所对应的页框标记为只读的,以便这种页框能用写时复制机制进行处理。
从上面可以看出,内核倾向于用复制来创建进程的地址空间。事实上下一讲将看到,新进程本身就是复制而来的。当设置了CLONE_VM标志时,它直接复制描述符指针,从而完全共享地址空间。否则复制内容,但通过复制页表实现共享页框。只有其中的一个进程试图写时,这个页就被复制一份。一段时间后,新进程将获得与父进程不同的地址空间。
删除进程的地址空间
当进程结束时,内核调用exit_mm()函数释放进程的地址空间。
=============== kernel/exit.c 647 704 =============== /* * Turn us into a lazy TLB process if we * aren't already.. */ static void exit_mm(struct task_struct * tsk) { struct mm_struct *mm = tsk->mm; struct core_state *core_state; mm_release(tsk, mm); if (!mm) return; /* * Serialize with any possible pending coredump. * We must hold mmap_sem around checking core_state * and clearing tsk->mm. The core-inducing thread * will increment ->nr_threads for each thread in the * group with ->mm != NULL. */ down_read(&mm->mmap_sem); core_state = mm->core_state; if (core_state) { struct core_thread self; up_read(&mm->mmap_sem); self.task = tsk; self.next = xchg(&core_state->dumper.next, &self); /* * Implies mb(), the result of xchg() must be visible * to core_state->dumper. */ if (atomic_dec_and_test(&core_state->nr_threads)) complete(&core_state->startup); for (;;) { set_task_state(tsk, TASK_UNINTERRUPTIBLE); if (!self.task) /* see coredump_finish() */ break; schedule(); } __set_task_state(tsk, TASK_RUNNING); down_read(&mm->mmap_sem); } atomic_inc(&mm->mm_count); BUG_ON(mm != tsk->active_mm); /* more a memory barrier than a real lock */ task_lock(tsk); tsk->mm = NULL; up_read(&mm->mmap_sem); enter_lazy_tlb(mm, current); /* We don't want this task to be frozen prematurely */ clear_freeze_flag(tsk); if (tsk->signal->oom_score_adj == OOM_SCORE_ADJ_MIN) atomic_dec(&mm->oom_disable_count); task_unlock(tsk); mm_update_next_owner(mm); mmput(mm); }
1.首先
mm_release()函数唤醒在tsk->vfork_done补充原语上睡眠的进程。一般只有现有进程通过
vfork()系统调用被创建时,相应的等待队列才会为非空。关于
vfork()的知识下一讲进程管理里介绍。
2.然后函数用信号量对下面的操作做串行化保护。
3.接下来函数递增内存描述符的主使用计数器,重新设置进程描述符的mm字段,并使处理器处于懒惰TLB模式。
4.最后调用
mmput()函数释放局部描述符表,线性区描述符表和页表。不过,因为
exit_mm()已经弟增了主使用计数器,所以并不释放内存描述符本身。直到要把正被终止的进程从本地CPU撤消时,才由
finishi_task_switch()函数释放内存描述符。这在下一讲描述。
堆的管理
堆是Unix进程中一个特殊的线性区,用于满足进程的动态内存请求。内存描述符中的start_brk和brk字段分别限定了这个区的开始和结束地址。进程可以使用下面的API来请求和释放动态内存:
malloc(size)
free(addr)
calloc(n, size):请求一个包含n个size大小元素的数组。如果分配成功,将数组元素初始化为零,并返回第一个元素的线性地址。
realloc(addr):改变前两个函数分配的内存区字段的大小。
brk(addr):直接修改堆的大小。返回值是线性区新的结束地址(进程必须检查这个地址和请求的地址值addr是否一致)。
sbrk(incr):类似于brk(),但是incr参数指定是增加还是减少堆大小。
其中只有
brk()是以系统调用的方式实现的,其他都是基于
brk()和
mmap()系统实现的C语言函数库。当用户态的进程调用
brk()时,内核执行系统调用
brk()。库函数
brk()与系统调用
brk()的区别在今后讲述。
=============== mm/mmap.c 246 299 =============== SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long rlim, retval; unsigned long newbrk, oldbrk; struct mm_struct *mm = current->mm; unsigned long min_brk; down_write(&mm->mmap_sem); #ifdef CONFIG_COMPAT_BRK min_brk = mm->end_code; #else min_brk = mm->start_brk; #endif if (brk < min_brk) goto out; /* * Check against rlimit here. If this check is done later after the test * of oldbrk with newbrk then it can escape the test and let the data * segment grow beyond its set limit the in case where the limit is * not page aligned -Ram Gupta */ rlim = rlimit(RLIMIT_DATA); if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim) goto out; newbrk = PAGE_ALIGN(brk); oldbrk = PAGE_ALIGN(mm->brk); if (oldbrk == newbrk) goto set_brk; /* Always allow shrinking brk. */ if (brk <= mm->brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk)) goto set_brk; goto out; } /* Check against existing mmap mappings. */ if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) goto out; /* Ok, looks good - let it rip. */ if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) goto out; set_brk: mm->brk = brk; out: retval = mm->brk; up_write(&mm->mmap_sem); return retval; }
该函数首先验证地址参数brk是否越界,因为堆不能与进程代码所在的线性区重叠。再验证堆空间是否超过进程的限制。如果是,立即返回。
然后将地址调整为页对齐,再与内存描述符的brk字段比较,如果是缩小堆,调用
do_munmap()实现。
否则是扩大堆,检查扩大后是否与进程的其他线性地址重叠。
最后一切顺利,调用
do_brk()函数分配空间。该函数实际上是仅处理匿名线性区的
do_mmap()的简化版,且比
do_mmap()快,因为它假定线性区不映射磁盘上的文件,从而避免了对线性区对象的几个字段的检查。
相关文章推荐
- 小小君的OC--类之间的复合调用
- codeforces 591B Rebranding
- 编程思想之多线程与多进程(2)-线程优先级与线程安全
- takePic and Videos
- Dijkstra算法和Floyd算法
- Oracle函数之Grouping/Grouping_id
- Codeforces Gym 100792K King's Rout(优先队列+拓扑排序)
- iOS开发 关于iBeacon的一些记录
- 链表
- 浏览器打开就是全屏的代码说明
- php读取邮件
- 【UI】android如何绘制一个饼图
- iOS中UIView翻转效果实现
- DLL中的main函数
- 图像处理(二)laplacian锐化
- Java - 继承(基础)
- node.js_in_practice(intermediate)
- LeetCode 124: Binary Tree Maximum Path Sum
- JavaWeb Cookie
- android 64 sd卡读写的操作