内核除了管理本身的内存外,还必须管理进程的地址空间,即系统中每个用户空间进程所看到的内存。Linux采用虚拟内存技术,系统中的所有进程之间以虚拟方式共享内存。对每个进程来说,它们好像都可以访问整个系统的所有物理内存;即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。
进程地址空间由每个进程中的线性地址区组成,而且更为重要的特点是内核允许进程使用该空间中的地址。每个进程都有一个32或64位的flat地址空间,空间的具体大小取决于体系结构。flat描述的是地址空间范围是一个独立的连续区间。通常情况下,每个进程都有惟一的这种flat地址空间,进程地址空间之间彼此互不相干。两个不同的进程可以在鸽子地址空间的相同地址内存放不同的数据。进程之间也可以选择共享地址空间,称这样的进程为线程。
内存地址是一个给定的值,它要在地址空间范围之内。在地址空间中,我们更为关心的是进程有权访问的虚拟内存地址区间,这些可被访问的合法地址区间被称为内存区域(memory area),通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。
进程只能访问有效范围内的内存地址。每个内存区域也具有相应进程必须遵循的特定访问属性,如果一个进程访问了不在有效范围中的地址,或以不正确的方式访问有效地址,那么内核就会终止该进程,并返回“段错误”信息。
内存区域可以包含各种内存对象,比如:
1. 可执行文件代码的内存映射,称为代码段(text section)
2. 可执行文件的已初始化全局变量的内存映射,称为数据段(data section)
3. 包含未初始化全局变量,也就是bss段的零页(页面中的信息全部为0值,可用于映射bss段等目的)的内存映射。
术语“BSS”是block started by symbol的缩写。因为未初始化的变量没有对应的值,所以不需要存放在可执行对象中。但是因为C标准强制规定未初始化的全局变量要被赋予特殊的默认值,所以内核要将未赋值的变量从可执行代码载入到内存中,然后将零页映射到该片内存上,于是这些未初始化的变量就被赋予了0值,这样避免了在目标文件中显式地进行初始化,减少空间浪费。
4. 用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射。
5. 每一个诸如C库或动态连接程序等共享库的代码段,数据段和bss也会被载入进程的地址空间。
6. 任何内存映射文件
7. 任何共享内存段
8. 任何匿名的内存映射,比如由malloc()分配的内存。
进程地址空间中的任何有效地址都只能位于惟一的区域。这些内存区域不能相互覆盖。在执行的进程中,每个不同的内存片段都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等。
- 内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间相关的全部信息:
- 在<include/linux/sched.h>中
- 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 */
- 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);
- 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
- */
- /* Special counters, in some configurations protected by the
- * page_table_lock, in other configurations by being atomic.
- */
- mm_counter_t _file_rss;
- mm_counter_t _anon_rss;
- 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 */
- 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 char dumpable:2;
- /* coredumping support */
- int core_waiters; /*内核转储等待线程 */
- struct completion *core_startup_done,/*core 开始完成 */ core_done/*core结束完成 */;
- /* aio bits */
- rwlock_t ioctx_list_lock; /* AIO IO链表锁*/
- struct kioctx *ioctx_list; /* AIO IO链表*/
- };
内核同时使用mm_count和mm_users这两个计数器是为了区别主使用计数器(mm_count)和使用该地址空间的进程数目(mm_users)。
mmap和mm_rb这两个不同的数据结构体描述的对象是相同的:该地址空间中的全部内存区域。mmap是以链表形式存放的,这样利于简单高效地遍历所有元素;而mm_rb以红黑树形式存放,适合搜索指定元素。
所有mm_struct结构体通过自身的mmlist域连接成一个双向链表中,首元素是init_mm内存描述符,它代表init进程的地址空间。操作该链表的时候,需要使用mmlist_lock锁(定义在kernel/fork.c中)来防止并发访问。
内存描述符的总数存放在mmlist_nr全局变量(定义在kernel/fork.c中)中。
- 分配内存描述符
进程的进程描述符中,mm域存放着该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。
fork()函数利于copy_mm()函数复制父进程的内存描述符,就是将current->mm域给子进程,子进程中的mm_struct结构实际上是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。
如果父进程希望和子进程共享地址空间,可以在调用clone()时,设置CLONE_VM标志。我们把这样的进程称为线程。
-
- /* SLAB cache for mm_struct structures (tsk->mm) */
- static struct kmem_cache *mm_cachep;
- #define allocate_mm() (kmem_cache_alloc(mm_cachep, GFP_KERNEL))
- 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;
- 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;
- tsk->mm = mm;
- tsk->active_mm = mm;
- return 0;
- fail_nomem:
- return retval;
- }
- 销毁内存描述符
-
当进程退出时,内核会调用exit_mm()函数,该函数执行一些常规的销毁工作,同时更新一些统计量。
- 在<kernel/Exit.c>中
- /*
- * 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;
- mm_release(tsk, mm);
- if (!mm)
- return;
- /*
- * Serialize with any possible pending coredump.
- * We must hold mmap_sem around checking core_waiters
- * and clearing tsk->mm. The core-inducing thread
- * will increment core_waiters for each thread in the
- * group with ->mm != NULL.
- */
- down_read(&mm->mmap_sem);
- if (mm->core_waiters) {
- up_read(&mm->mmap_sem);
- down_write(&mm->mmap_sem);
- if (!--mm->core_waiters)
- complete(mm->core_startup_done);
- up_write(&mm->mmap_sem);
- wait_for_completion(&mm->core_done);
- 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);
- task_unlock(tsk);
- mmput(mm); //减少内存描述符中的mm_users用户计数
- }
- 在fork.c中
- /* Please note the differences between mmput and mm_release.
- * mmput is called whenever we stop holding onto a mm_struct,
- * error success whatever.
- *
- * mm_release is called after a mm_struct has been removed
- * from the current process.
- *
- * This difference is important for error handling, when we
- * only half set up a mm_struct for a new process and need to restore
- * the old one. Because we mmput the new mm_struct before
- * restoring the old one. . .
- * Eric Biederman 10 January 1998
- */
- void mm_release(struct task_struct *tsk, struct mm_struct *mm)
- {
- struct completion *vfork_done = tsk->vfork_done;
- /* Get rid of any cached register state */
- deactivate_mm(tsk, mm);
- /* notify parent sleeping on vfork() */
- if (vfork_done) {
- tsk->vfork_done = NULL;
- complete(vfork_done);
- }
- /*
- * If we're exiting normally, clear a user-space tid field if
- * requested. We leave this alone when dying by signal, to leave
- * the value intact in a core dump, and to save the unnecessary
- * trouble otherwise. Userland only wants this done for a sys_exit.
- */
- if (tsk->clear_child_tid
- && !(tsk->flags & PF_SIGNALED)
- && atomic_read(&mm->mm_users) > 1) {
- u32 __user * tidptr = tsk->clear_child_tid;
- tsk->clear_child_tid = NULL;
- /*
- * We don't check the error code - if userspace has
- * not set up a proper pointer then tough luck.
- */
- put_user(0, tidptr);
- sys_futex(tidptr, FUTEX_WAKE, 1, NULL, NULL, 0);
- }
- }
- /*
- * Decrement the use count and release all resources for an mm.
- */
- void mmput(struct mm_struct *mm)
- {
- might_sleep();
- if (atomic_dec_and_test(&mm->mm_users)) {
- exit_aio(mm);
- exit_mmap(mm);
- if (!list_empty(&mm->mmlist)) {
- spin_lock(&mmlist_lock);
- list_del(&mm->mmlist);
- spin_unlock(&mmlist_lock);
- }
- put_swap_token(mm);
- mmdrop(mm);
- }
- }
- /*
- * Called when the last reference to the mm
- * is dropped: either by a lazy thread or by
- * mmput. Free the page directory and the mm.
- */
- void fastcall __mmdrop(struct mm_struct *mm)
- {
- BUG_ON(mm == &init_mm);
- mm_free_pgd(mm);
- destroy_context(mm);
- free_mm(mm);
- }
- 在sched.h中
- static inline void mmdrop(struct mm_struct * mm)
- {
- if (atomic_dec_and_test(&mm->mm_count))
- __mmdrop(mm);
- }
- mm_struct与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中mm域为空。这正式内核线程的真正含义,它们没有用户上下文。
内核线程并不需要访问任何用户空间的内存,以为内核线程在用户空间没有任何页,所以它们并不需要有自己的内存描述符和页表。尽管如此,即使访问内核内存,内核线程饿还是需要使用一些数据的,比如页表。为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新地址空间进行切换,内核系拿出将直接使用前一个进程的内存描述符。
当一个进程被调度时,该进程的mm域执行的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm为空。当内核线程被调度时,内核发现它的mm为空,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符,在需要时,内核线程可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中的和内核内存相关的信息。