# Linux mmap 原理 二
# do_munmap 函数
该函数将用于在指定固定映射时(必须从 addr 地址处映射),返回的 vma 中覆盖 addr + length 长度的虚拟内存域解除之前的映射。该函数较为简单:若找到需要映射的 vma,那么将其中包含 addr -> addr+length 的区间切割,然后解除映射,将这段虚拟地址交给外部函数使用。
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
{
unsigned long end;
struct vm_area_struct *mpnt, *prev, *last;
if ((start & ~PAGE_MASK) || start > TASK_SIZE || len > TASK_SIZE-start) // 映射起始地址必须页对齐,映射起始地址和映射长度不能超过用户态虚拟地址空间
return -EINVAL;
if ((len = PAGE_ALIGN(len)) == 0) // 映射长度为0
return -EINVAL;
// 找到第一个与起始地址重叠的 vma 结构(该函数返回时,若存在 addr 的映射地址被其他 vma 占据,那么 该 addr 将可能在 :prev -> end 与 mpnt-> end 的映射区间之间)
mpnt = find_vma_prev(mm, start, &prev);
if (!mpnt) // 若不存在重叠 vma,那么返回
return 0;
// 此时 start < mpnt->vm_end ,也即找到可能包含需要映射地址的 vma 结构(因为 vma 的结束地址地址)
if (is_vm_hugetlb_page(mpnt)) { // 若使用 hugetlbfs 文件系统映射,那么检测对齐 range,这里了解即可
int ret = is_aligned_hugepage_range(start, len);
if (ret)
return ret;
}
// 获取映射后的虚拟地址末尾,若当前找到的 vma 的起始地址大于映射后的结束地址,那么表名 需要映射的 start -> end 这一段虚拟地址没有 vma 占有,那么直接返回
end = start + len;
if (mpnt->vm_start >= end)
return 0;
// 此时:mpnt->vm_start < end,表明当前映射的地址区间 已经被之前的 vma 占有,那么此时需要解除映射
if (mpnt->vm_file && (mpnt->vm_flags & VM_EXEC)) // 通知 exec_unmap_notifier 链(了解即可),当前马上要解除映射
profile_exec_unmap(mm);
// 需要映射的起始地址包含在当前 vma 区间中,那么尝试对该 vma 切割
if (start > mpnt->vm_start) {
if (split_vma(mm, mpnt, start, 0))
return -ENOMEM;
prev = mpnt; // 由于上面设置的 new_below 参数为 0,此时 mpnt 将表示 切割后的低地址部分:mpnt->vm_start -> addr ,新切割的 vma 表示:addr -> mpnt->vm_end
}
// 切割后看看是否需要映射的 start -> end 区间是否已经切割完成,若没有,那么继续切割(这是为了避免出现:找到的 vma (start 1 -> end 1) ,需要映射的地址区间:start 2 -> end 2 , end 2 > end 1 情况)
last = find_vma(mm, end);
if (last && end > last->vm_start) { // 当前需要映射区间结束地址 仍然 大于 找到的后面的 vma区间,那么继续切割
if (split_vma(mm, last, end, 1))
return -ENOMEM;
}
mpnt = prev? prev->vm_next: mm->mmap; // 获取当前需要操作的 vma 结构,若存在 包含映射地址的 vma,那么取该 vma即可,否则 操作对象 为 全局 mmap 对象,因为需要解除映射的地址 并没有其他 vma 占有
// 当前进程页表自旋锁,将找到的 vma 区间解除映射
spin_lock(&mm->page_table_lock);
detach_vmas_to_be_unmapped(mm, mpnt, prev, end); // 将需要解除映射的vma从当前进程的vma 链表中移出
unmap_region(mm, mpnt, prev, start, end); // 删除需要解除映射的页表信息,同时刷新 tlb(因为 这些地址已经解除映射,其中缓存的 物理地址 不再需要,所以必须刷新 MMU 的 TLB)
spin_unlock(&mm->page_table_lock);
unmap_vma_list(mm, mpnt); // 对当前解除映射的 vma 变量修正
return 0;
}
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
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
# find_vma_prev 函数
该函数功能 与 find_vma 相同,但会在设置 *pprev 时,返回一个指向上一个 VMA 的指针。
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;
// 防止 addr 地址低于第一个 VMA
vma = mm->mmap;
rb_node = mm->mm_rb.rb_node;
while (rb_node) { // 遍历红黑树找到第一个可能包含 addr 的 vma
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)) // 上一个 vma 是最后一个 vma 或者 当前 addr 包含在下一个 vma 与 当前 vma 之间(注意看上面的 addr < vma_tmp->vm_end 判断 如果为 false,那么说明需要查找的 addr 的 在当前 vma 映射区间的上面,同时这里 addr < prev->vm_next->vm_end,表名在后面一个 vma 的下面,所以可以判定该 地址 包含在这两个 vma 映射区域之间)
break;
rb_node = rb_node->rb_right;
}
}
out:
*pprev = prev; // 保存找到的前一个 vma ,也即:addr > vma_tmp->vm_end 的第一个 vma
return prev ? prev->vm_next : vma; // 若 prev 存在,那么又由于上述循环的 break 的定义,所以我们返回 prev->vm_next 即可,否则 返回 全局 vma,也即整个 虚拟地址空间
}
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
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
# split_vma 函数
该函数将在地址 addr 处将 vma 拆分为两部分,一个新的 vma 将被分配给第一部分或插入第一部分的后面。可以从源码中看到:根据 new_below 来决定 新的 vma 和旧的 vma 分别用于管理哪一段地址,新创建的 vma 需要插入到链表和红黑树中。
// 当我们调用该函数时的判断:start > mpnt->vm_start ,然后 split_vma(mm, mpnt, start, 0) ,可以看到我们此时需要将 vma,切割为: mpnt->vm_start -> start ,start -> mpnt->vm_end 两部分
int split_vma(struct mm_struct * mm, struct vm_area_struct * vma,
unsigned long addr, int new_below)
{
struct vm_area_struct *new;
struct address_space *mapping = NULL;
if (mm->map_count >= MAX_MAP_COUNT) // 由于需要切分,会生成新的 vma 映射,所以这里需要再次检测是否超过 映射数量限制
return -ENOMEM;
new = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); // 新创建一个新的 vma 结构
if (!new)
return -ENOMEM;
// 由于分割的两段 vma 大部分属性都是一样的,所以这里直接全部复制,然后修正
*new = *vma;
INIT_LIST_HEAD(&new->shared); // 对于共享链表节点,需要单独初始化
// 根据设置来决定新的 vma 代表切割的两部分的哪一段,new_below 为 1 表明:新的 vma 为低地址那一段,否则为高地址那一段
if (new_below) // 若指定设置新 vma 的结束地址为 addr,此时 新的 vma 的虚拟地址 范围 为 旧的vma start -> addr
new->vm_end = addr;
else { // 否则我们正常将区间起始地址设置为 addr ,此时新的 vma 的虚拟地址 范围 为:addr -> 旧的vma end
new->vm_start = addr;
new->vm_pgoff += ((addr - vma->vm_start) >> PAGE_SHIFT); // 用于处理文件映射,也即当前文件映射的这段区间映射到了 新的 vma 中
}
if (new->vm_file) // 存在 文件映射,那么增加文件引用计数
get_file(new->vm_file);
if (new->vm_ops && new->vm_ops->open) // 存在 open 函数,那么回调
new->vm_ops->open(new);
if (vma->vm_file) // open函数回调后,我们保存对文件的 page cache(address_space)的引用
mapping = vma->vm_file->f_dentry->d_inode->i_mapping;
if (mapping) // 对 page cache 上锁
down(&mapping->i_shared_sem);
spin_lock(&mm->page_table_lock); // 对当前进程的页表上锁
// 修正 旧vma 的起始地址或结束地址,让新的 vma 和 旧的 vma 分别指向自己的那一段
if (new_below) {
vma->vm_start = addr;
vma->vm_pgoff += ((addr - new->vm_start) >> PAGE_SHIFT);
} else
vma->vm_end = addr;
__insert_vm_struct(mm, new); // 将新的 vma 插入到 vm链表和红黑树中
// 释放锁并返回
spin_unlock(&mm->page_table_lock);
if (mapping)
up(&mapping->i_shared_sem);
return 0;
}
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
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
# find_vma 函数
该函数用于从红黑树中找到包含 addr 的 vma 返回,同时 linux 为了加速访问,将最近查找使用的 vma 缓存在 mm->mmap_cache,下一次使用时将避免查找过程。
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { // 需要查找的地址没有被缓存命中,那么从红黑树中查找(为了加快访问速度,linux 将最近访问的 vma 缓存到 mmap_cache变量中)
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) { // 若需要查找的 addr 地址大于当前 vma 的结束地址,那么从左子树继续查找
vma = vma_tmp;
if (vma_tmp->vm_start <= addr) // 若当前 addr 正好包含在该 vma 中,那么结束循环
break;
rb_node = rb_node->rb_left;
} else // 否则从右子树查找
rb_node = rb_node->rb_right;
}
if (vma) // 缓存当前使用的 vma,下一次使用时不需要查表
mm->mmap_cache = vma;
}
}
return vma;
}
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
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
# detach_vmas_to_be_unmapped 函数
该函数用于将 struct vm_area_struct *vma 到 end 区间的 vma 从 红黑树中移出,同时修正 vma 链表。
static void detach_vmas_to_be_unmapped(struct mm_struct *mm, struct vm_area_struct *vma,
struct vm_area_struct *prev, unsigned long end)
{
struct vm_area_struct **insertion_point;
struct vm_area_struct *tail_vma = NULL;
insertion_point = (prev ? &prev->vm_next : &mm->mmap); // 将 vma 从链表中移出,那么需要修正 prev vma
do {
rb_erase(&vma->vm_rb, &mm->mm_rb); // 从红黑树中移出当前 vma
mm->map_count--;
tail_vma = vma;
vma = vma->vm_next; // 由于 链表按地址高低排序,所以这里我们直接遍历下一个 vma 即可
} while (vma && vma->vm_start < end); // 循环将 vma->start -> end 地址的 vma 从 红黑树中移出
*insertion_point = vma; // 修改链表的next 引用
tail_vma->vm_next = NULL; // 释放下一个节点的引用
mm->mmap_cache = NULL; // 清理 vma 缓存
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# vma_merge 函数
该函数用于:给定一个新的映射请求(addr,end,vm_flags,file,pgoff),确定它是否可以与它的前一个虚拟地址空间或后一个虚拟地址空间的 vma 合并,用于减少内存空洞(memory hole)也即减少外碎片。我们看到这里可以合并的标准为:
1、前驱 vma 的 prev->vm_end == addr ,也即 与 前驱 vma 地址相邻
2、后继 vma 的 prev->vm_end == next->vm_start ,也即 前驱 vma 与 后继 vma 地址相邻
static int vma_merge(struct mm_struct *mm, struct vm_area_struct *prev,
struct rb_node *rb_parent, unsigned long addr,
unsigned long end, unsigned long vm_flags,
struct file *file, unsigned long pgoff)
{
spinlock_t *lock = &mm->page_table_lock;
struct inode *inode = file ? file->f_dentry->d_inode : NULL;
struct semaphore *i_shared_sem;
if (vm_flags & VM_SPECIAL) // 若当前 VMA 的类型为特殊类型,那么不支持 合并(#define VM_SPECIAL (VM_IO | VM_DONTCOPY | VM_DONTEXPAND | VM_RESERVED),比如 vma 为 IO 操作虚拟地址空间等等)
return 0;
i_shared_sem = file ? &inode->i_mapping->i_shared_sem : NULL; // 若为 文件映射,那么获取文件inode 的共享锁,保证操作该 vma 所映射的文件时的线程安全
if (!prev) { // 若当前需要合并的vma 前面不存在已经分配的虚拟地址空间,那么将红黑树的父节点作为 前驱节点,同时跳转到 合并 后继的 vma 虚拟地址空间
prev = rb_entry(rb_parent, struct vm_area_struct, vm_rb);
goto merge_next;
}
// 此时前驱 vma 存在,那么尝试进行合并。合并条件:
if (prev->vm_end == addr && // 前驱 vma 与 当前待分配的地址相邻
is_mergeable_vma(prev, file, vm_flags) && // 前驱 vma 类型满足合并条件
can_vma_merge_after(prev, vm_flags, file, pgoff)) { // 看看前驱 vma 的文件映射是否可以合并后续的地址空间(主要用于检测 文件映射,看后续描述)
struct vm_area_struct *next;
...
prev->vm_end = end; // 此时确定可以合并,那么很简单,直接将 prev 的 结束地址修改为需要分配的地址空间的 end 地址即可,此时相当于对 prev 进行扩容
next = prev->vm_next; // 获取 prev 后继的 vma 并检测 是否支持对后继的 vma 进行合并,合并条件:
if (next && prev->vm_end == next->vm_start && // 后继 vma 存在 且 与 prev vma 相邻
can_vma_merge_before(next, vm_flags, file,
pgoff, (end - addr) >> PAGE_SHIFT)) { // 文件映射确定可以合并
prev->vm_end = next->vm_end; // 那么继续将 prev vma 的结束地址设置为 next->vm_end 此时继续对 prev vma 扩容
__vma_unlink(mm, next, prev); // 然后将 后继 vma 从链表和红黑树中移出
__remove_shared_vm_struct(next, inode); // 移出 后继 vma 对文件的引用
... // 释放资源并退出
return 1;
}
... // 释放资源并退出
return 1;
}
// 此时不存在 前驱 vma,那么直接尝试与 后继 vma 进行合并,流程如上,这里不做过多赘述
prev = prev->vm_next;
if (prev) {
merge_next:
if (!can_vma_merge_before(prev, vm_flags, file,
pgoff, (end - addr) >> PAGE_SHIFT))
return 0;
if (end == prev->vm_start) {
if (file)
down(i_shared_sem);
spin_lock(lock);
prev->vm_start = addr;
prev->vm_pgoff -= (end - addr) >> PAGE_SHIFT;
spin_unlock(lock);
if (file)
up(i_shared_sem);
return 1;
}
}
return 0;
}
// 检测 vma 是否可以合并
static inline int is_mergeable_vma(struct vm_area_struct *vma,
struct file *file, unsigned long vm_flags)
{
if (vma->vm_ops && vma->vm_ops->close) // 存在 close 操作不能合并(思考下为何?部分影响整体?)
return 0;
if (vma->vm_file != file) // 两个 vma 映射的 文件与当前 file 不相同,不能合并
return 0;
if (vma->vm_flags != vm_flags) // 两个 vma 的标志位不相同,不能合并
return 0;
if (vma->vm_private_data) // vma 存在私有数据,不能合并
return 0;
return 1;
}
// 检测 vma 是否可以合并后继的地址空间
static int can_vma_merge_after(struct vm_area_struct *vma, unsigned long vm_flags,
struct file *file, unsigned long vm_pgoff)
{
if (is_mergeable_vma(vma, file, vm_flags)) { // 必要条件为:当前 vma 必须支持 合并操作
unsigned long vma_size;
if (!file) // 随后检测 是否为 匿名映射,因为这里只检测 文件映射
return 1;
vma_size = (vma->vm_end - vma->vm_start) >> PAGE_SHIFT; // 获取当前 vma 的大小
if (vma->vm_pgoff + vma_size == vm_pgoff) // 若当前 vma 映射的 文件偏移正好 等于 扩容偏移量,那么返回 1(为了避免 vma 映射的文件偏移 小于 扩容后的大小,那么访问那不属于文件映射偏移的部分 将会导致错误)
return 1;
}
return 0;
}
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
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