一篇读懂Linux内核-内核地址空间分布和进程地址空间( 二 )


mm_users记录了正在使用该地址的进程数目(比如有两个进程在使用,那就为2) 。mm_count是该结构的主引用计数,只要mm_users不为0,它就为1 。但其为0时,后者就为0 。这时也就说明再也没有指向该mm_struct结构体的引用了,这时该结构体会被销毁 。内核之所以同时使用这两个计数器是为了区别主使用计数器和使用该地址空间的进程的数目 。mmap和mm_rb描述的都是同一个对象:该地址空间中的全部内存区域 。不同只是前者以链表,后者以红黑树的形式组织 。所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间 。另外需要注意,操作该链表的时候需要使用mmlist_lock锁来防止并发访问,该锁定义在文件kernel/fork.c中 。内存描述符的总数在mmlist_nr全局变量中,该变量也定义在文件fork.c中 。
我前边说过的进程描述符中有一个mm域,这里边存放的就是该进程使用的内存描述符,通过current->mm便可以指向当前进程的内存描述符 。fork函数利用copy_mm()函数就实现了复制父进程的内存描述符,而子进程中的mm_struct结构体实际是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的 。通常,每个进程都有唯一的mm_struct结构体 。
前边也说过,在linux中,进程和线程其实是一样的,唯一的不同点就是是否共享这里的地址空间 。这个可以通过CLONE_VM标志来实现 。linux内核并不区别对待它们,线程对内核来说仅仅是一个共向特定资源的进程而已 。好了,如果你设置这个标志了,似乎很多问题都解决了 。不再要allocate_mm函数了,前边刚说作用 。而且在copy_mm()函数中将mm域指向其父进程的内存描述符就可以了,如下:
if (clone_flags & CLONE_VM) { /* * current is the parent process and * tsk is the child process during a fork() */ atomic_inc(¤t->mm->mm_users); tsk->mm = current->mm; }
最后,当进程退出的时候,内核调用exit_mm()函数,这个函数调用mmput()来减少内存描述符中的mm_users用户计数 。如果计数降为0,继续调用mmdrop函数,减少mm_count使用计数 。如果使用计数也为0,则调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中 。
但对于内核而言,内核线程没有进程地址空间,也没有相关的内存描述符,内核线程对应的进程描述符中mm域也为空 。但内核线程还是需要使用一些数据的,比如页表,为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新地址空间进行切换,内核线程将直接使用前一个进程的内存描述符 。回忆一下我刚说的进程调度问题,当一个进程被调度时,进程结构体中mm域指向的地址空间会被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间 。但我们这里的内核是没有mm域(为空),所以,当一个内核线程被调度时,内核发现它的mm域为NULL,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active域,使其指向前一个进程的内存描述符 。所以在需要的时候,内核线程便可以使用前一个进程的页表 。因为内核线程不妨问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同 。
内存区域由vm_area_struct结构体描述,定义在linux/mm.h中,内存区域在内核中也经常被称作虚拟内存区域或VMA.它描述了指定地址空间内连续区间上的一个独立内存范围 。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性 。结构体如下:
struct vm_area_struct { struct mm_struct *vm_mm; /* associated mm_struct */ unsigned long vm_start; /* VMA start, inclusive */ unsigned long vm_end; /* VMA end , exclusive */ struct vm_area_struct *vm_next; /* list of VMA's */ pgprot_t vm_page_prot; /* access permissions */ unsigned long vm_flags; /* flags */ struct rb_node vm_rb; /* VMA's node in the tree */ union { /* links to address_space->i_mmap or i_mmap_nonlinear */ struct { struct list_head list; void *parent; struct vm_area_struct *head; } vm_set; struct prio_tree_node prio_tree_node; } shared; struct list_head anon_vma_node; /* anon_vma entry */ struct anon_vma *anon_vma; /* anonymous VMA object */ struct vm_operations_struct *vm_ops; /* associated ops */ unsigned long vm_pgoff; /* offset within file */ struct file *vm_file; /* mApped file, if any */ void *vm_private_data; /* private data */ };
每个内存描述符都对应于地址进程空间中的唯一区间 。vm_mm域指向和VMA相关的mm_struct结构体 。两个独立的进程将同一个文件映射到各自的地址空间,它们分别都会有一个vm_area_struct结构体来标志自己的内存区域;但是如果两个线程共享一个地址空间,那么它们也同时共享其中的所有vm_area_struct结构体 。


推荐阅读