Linux内核学习——进程地址空间

进程地址空间

在Linux中,所有进程之间都以虚拟方式共享内存。

地址空间

进程地址空间由进程可寻址的虚拟内存组成,使得每个进程都能有一个32bit或者64bit的连续地址空间。

进程只能访问有效内存内的物理内存区域,如果访问非法的内存区域,内核将会终止该进程,返回段错误。

内存区域可以包含:

  • text section;
  • data section;
  • 未初始化的全局变量,bss段;
  • 进程空间栈;
  • 内存映射文件,包括匿名的内存映射,比如由malloc()分配的内存;

内存描述符

内核使用内存描述符结构体来表示进程的地址空间——mm_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
struct mm_struct {
struct vm_area_struct * mmap; //指向虚拟区间(VMA)的链表
struct rb_root mm_rb; //指向线性区对象红黑树的根
struct vm_area_struct * mmap_cache; //指向最近找到的虚拟区间
unsigned long(*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);//在进程地址空间中搜索有效线性地址区
unsigned long(*get_unmapped_exec_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;
unsigned long free_area_cache; //内核从这个地址开始搜索进程地址空间中线性地址的空闲区域
pgd_t * pgd; //指向页全局目录
atomic_t mm_users; //次使用计数器,使用这块空间的个数
atomic_t mm_count; //主使用计数器
int map_count; //线性的个数
struct rw_semaphore mmap_sem; //线性区的读/写信号量
spinlock_t page_table_lock; //线性区的自旋锁和页表的自旋锁

struct list_head mmlist; //指向内存描述符链表中的相邻元素

/* 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代表的类型实际是typedef atomic_long_t
mm_counter_t _anon_rss;
mm_counter_t _swap_usage;

unsigned long hiwater_rss; //进程所拥有的最大页框数
unsigned long hiwater_vm; //进程线性区中最大页数

unsigned long total_vm, locked_vm, shared_vm, exec_vm;
//total_vm 进程地址空间的大小(页数)
//locked_vm 锁住而不能换出的页的个数
//shared_vm 共享文件内存映射中的页数

unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
//stack_vm 用户堆栈中的页数
//reserved_vm 在保留区中的页数或者在特殊线性区中的页数
//def_flags 线性区默认的访问标志
//nr_ptes 进程的页表数

unsigned long start_code, end_code, start_data, end_data;
//start_code 可执行代码的起始地址
//end_code 可执行代码的最后地址
//start_data已初始化数据的起始地址
// end_data已初始化数据的最后地址

unsigned long start_brk, brk, start_stack;
//start_stack堆的起始位置
//brk堆的当前的最后地址
//用户堆栈的起始地址

unsigned long arg_start, arg_end, env_start, env_end;
//arg_start 命令行参数的起始地址
//arg_end命令行参数的起始地址
//env_start环境变量的起始地址
//env_end环境变量的最后地址

unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

struct linux_binfmt *binfmt;

cpumask_t cpu_vm_mask; //用于惰性TLB交换的位掩码
/* Architecture-specific MM context */
mm_context_t context; //指向有关特定结构体系信息的表


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; //用于保护异步I/O上下文链表的锁
struct hlist_head ioctx_list;//异步I/O上下文
#endif
#ifdef CONFIG_MM_OWNER
struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef __GENKSYMS__
unsigned long rh_reserved[2];
#else
//有多少任务分享这个mm OOM_DISABLE
union {
unsigned long rh_reserved_aux;
atomic_t oom_disable_count;
};

/* base of lib map area (ASCII armour) */
unsigned long shlib_base;
#endif
};

在该结构体中,mm_users记录着使用该地址的进程数目,而mm_count则是mm_struct的主引用计数。比如有9个线程共享改地址,那么mm_users为9,mm_count为1。当mm_count为0时,需要销毁该结构体。

mmap和mm_rb描述相同的对象,该地址空间的全部内存区域,但前者是以链表形式存储,后者则是红黑树形式。一种用来高效遍历,另一种则是为了查询特定内存。

所有的mm_struct都通过自身mmlist连接在一个双向链表中。

分配内存描述符

在task_struct中的mm域存放着该进程的内存描述符,如果父子进程共享内存,需要设置CLONE_VM标识,这样的进程称作线程,在Linux内核中,线程就是共享资源的进程。

1
2
3
4
if (clone_flags & CLONE_VM){
atomic_inc(&current->mm->mm_users);
tsk->mm = current->mm;
}

撤销内存描述符

进程退出时,内核会调用exit_mm()函数,进程撤销工作。

mm_struct与内核线程

内核线程没有进程地址空间,也没有内存描述符,所以内核线程的mm域为空。

当一个进程被调度时,该进程的mm域指向的地址空间会被装载到内存。

虚拟内存区域

内存区域由vm_area_struct结构体描述,描述了指定地址空间内连续区间的一个独立内存范围。每个VMA对其mm_struct来说是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* This struct defines a memory VMM memory area. */
vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;

pgprot_t vm_page_prot;
unsigned long vm_flags;

/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;

/* For areas with an address space and backing store,
* font-size: 10px;">vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};

VMA标志

VMA是一种位标志,包含在vm_flags内,标记了该内存区域所包含的页面的行为和信息,不同于物理访问权限,反应的是内核处理要遵守的行为标准。

比如可读,写,执行,可共享

VMA操作

vm_area_struct结构体中的vm_ops域指向与指定内存区域相关的操作函数表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

/* notification that a previously read-only page is about to become
* writable, if an error is returned it will cause a SIGBUS */
int (*page_mkwrite)(struct vm_area_struct *vma, struct page *page);

/* called by access_process_vm when get_user_pages() fails, typically
* for use by special VMAs that can switch between memory and hardware
*/
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
#ifdef CONFIG_NUMA
/*
* set_policy() op must add a reference to any non-NULL @new mempolicy
* to hold the policy upon return. Caller should pass NULL @new to
* remove a policy and fall back to surrounding context--i.e. do not
* install a MPOL_DEFAULT policy, nor the task or system default
* mempolicy.
*/
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

/*
* get_policy() op must add reference [mpol_get()] to any policy at
* (vma,addr) marked as MPOL_SHARED. The shared policy infrastructure
* in mm/mempolicy.c will do this automatically.
* get_policy() must NOT add a ref if the policy at (vma,addr) is not
* marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
* If no [shared/vma] mempolicy exists at the addr, get_policy() op
* must return NULL--i.e., do not "fallback" to task or system default
* policy.
*/
struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
unsigned long addr);
int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
const nodemask_t *to, unsigned long flags);
#endif
};

  • 指定内存区域被加入到一个地址空间时,open函数被调用
  • 指定内存区域被删除时,close函数被调用

内存区域的树型结构和内存区域的链表结构

mmap域使用单独链表连接所有的内存区域对象,每个vm_area_struct结构体通过自身vm_next域连入链表。

而mm_rb则是用红黑树连接所有的内存区域。

操作内存区域

内核经常需要在某个内存区域上执行一些操作。

find_vma()

1
struct vm_area_struct * find_vma(strcut mm_struct *mm, unsigned long addr);

该函数在指定的地址空间中搜搜第一个vm_end大于addr的内存区域,如果找不到则返回NULL。并且该返回结果会被缓存在mmap_cache中,查找时会先从缓存找,搜不到则通过红黑树搜索。

find_vma_prev()

该函数返回第一个小于addr的内存区域。

find_vma_intersection()

该函数返回第一个和指定地址区间相交的VMA。

mmap()和do_mmap():创建地址区间

内核使用do_mmap()创建一个新的线性地址区间,不过不同的是,如果新创建的地址区间与一个已经存在的地址区间相邻,并且它们具有相邻的访问权限时,两个区间就会合并为一个。

1
2
3
4
5
6
7
8
9
10
11
12
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指定的文件。

mummap()和do_mummap():删除地址区间

1
int do_mummap(struct mm_struct *mm, unsigned long start, size_t len);

该函数从特定的进程地址空间中删除从地址start开始,长度为len字节的空间。

而mummap()则是对do)mummap()函数的一个简单封装。

页表

应用程序虽然面对的是虚拟内存,但真实操作的是物理内存,因此需要一个从虚拟内存到物理内存的转化机制。

在Linux中,是通过三级页表来实现的,顶级页表是页全局目录——PGD,包含了一个pgd_t的数组;而二级页表则是中间页目录——PMD,是一个pmd_t数组;最后一级则是页表项指向物理内存的页表。

为了提高效率,还引入了一个TLB的高速缓存器,在查询物理地址时首先在TLB中查询,找不到时采取内存中查找页表。