本篇通过学习mmap
的实现,将帮助解答《进程控制和通信(四) · PCB介绍 》中的一些问题,以及加深对虚拟内存的理解。
入口mmap
先看mmap
的入口,首先是检查一个PAGE
的大小以及偏移offset
是不是满足要求,然后再系统调用mmap(2)
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void *
__mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset)
{
MMAP_CHECK_PAGE_UNIT ();
if (offset & MMAP_OFF_LOW_MASK)
return (void *) INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
#ifdef __NR_mmap2
return (void *) MMAP_CALL (mmap2, addr, len, prot, flags, fd,
offset / (uint32_t) MMAP2_PAGE_UNIT);
#else
return (void *) MMAP_CALL (mmap, addr, len, prot, flags, fd,
MMAP_ADJUST_OFFSET (offset));
#endif
}
weak_alias (__mmap, mmap)
libc_hidden_def (__mmap)
|
首先关注PAGE
和offset
的检查部分:
1
2
3
|
MMAP_CHECK_PAGE_UNIT ();
if (offset & MMAP_OFF_LOW_MASK)
return (void *) INLINE_SYSCALL_ERROR_RETURN_VALUE (EINVAL);
|
对应的宏如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* This is the minimum mmap2 unit size accept by the kernel. An architecture
with multiple minimum page sizes (such as m68k) might define it as -1 and
thus it will queried at runtime. */
#ifndef MMAP2_PAGE_UNIT
# define MMAP2_PAGE_UNIT 4096ULL
#endif
#if MMAP2_PAGE_UNIT == -1
static uint64_t page_unit;
# define MMAP_CHECK_PAGE_UNIT() \
if (page_unit == 0) \
page_unit = __getpagesize ();
# undef MMAP2_PAGE_UNIT
# define MMAP2_PAGE_UNIT page_unit
#else
# define MMAP_CHECK_PAGE_UNIT()
#endif
|
一般设置一个PAGE
的大小是4096
,但是也支持动态获取,通过__getpagesize
可以实时地动态获取PAGE
大小。
MMAP_OFF_LOW_MASK
表示PAGE
的大小减一,例如PAGE
大小是4096
,那么对应的MMAP2_PAGE_UNIT
就是4095
,转换成二进制就是(011111111111
)。
1
2
|
/* Do not accept offset not multiple of page size. */
#define MMAP_OFF_LOW_MASK (MMAP2_PAGE_UNIT - 1)
|
为什么MMAP_OFF_LOW_MASK
要表示为MMAP2_PAGE_UNIT - 1
? 如果PAGE
一定是2^n
大小,那么MMAP2_PAGE_UNIT
的表示一定是后缀若干个1
。用offset & MMAP_OFF_LOW_MASK
判断,如果表达式为true
,则表示offset
末尾有1
,那么它一定不是PAGE
的整数倍;如果offset
不是PAGE
的整数倍,那么offset & MMAP_OFF_LOW_MASK
一定为true
吗?(是不是充要条件?)如果前提是,PAGE
的大小一定是2^n
,那么上述表述成立。可以大概证明一下:
如果offset
不是PAGE
的整数倍,假设offset
的值是n × PAGE + m
,n
和m
都是整数,且m < PAGE
。因为PAGE
是2^n
,二进制表示的首个数位是1
, 末尾有若刚个连续的0
,例如100000000000
,所以n × PAGE
末尾连续0
的个数大于或等于PAGE
末尾连续0
的个数。又因为m < PAGE
,所以m
的二进制数位长度小于PAGE
末尾连续0
的长度,这就会导致n × PAGE + m
末尾连续0
的个数小于PAGE
末尾连续0
的个数,因此offset & MMAP_OFF_LOW_MASK
为true
。
系统调用
do_mmap2
以下是mmap(2)
系统调用,都指向了do_mmap2
这个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#define PAGE_SHIFT 12
SYSCALL_DEFINE6(mmap2, unsigned long, addr, size_t, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, pgoff)
{
return do_mmap2(addr, len, prot, flags, fd, pgoff, PAGE_SHIFT-12);
}
SYSCALL_DEFINE6(mmap, unsigned long, addr, size_t, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, off_t, offset)
{
return do_mmap2(addr, len, prot, flags, fd, offset, PAGE_SHIFT);
}
|
do_mmap2
如下,也会检查offset
对齐之类,主要关注ksys_mmap_pgoff
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static inline long do_mmap2(unsigned long addr, size_t len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long off, int shift)
{
long ret = -EINVAL;
if (!arch_validate_prot(prot, addr))
goto out;
if (shift) {
if (off & ((1 << shift) - 1))
goto out;
off >>= shift;
}
ret = ksys_mmap_pgoff(addr, len, prot, flags, fd, off);
out:
return ret;
}
|
ksys_mmap_pgoff
ksys_mmap_pgoff
如下:
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
|
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
struct file *file = NULL;
//......
if (!(flags & MAP_ANONYMOUS)) {
audit_mmap_fd(fd, flags);
file = fget(fd);
if (!file)
return -EBADF;
if (is_file_hugepages(file))
len = ALIGN(len, huge_page_size(hstate_file(file)));
retval = -EINVAL;
if (unlikely(flags & MAP_HUGETLB && !is_file_hugepages(file)))
goto out_fput;
} else if (flags & MAP_HUGETLB) {
struct user_struct *user = NULL;
struct hstate *hs;
hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
if (!hs)
return -EINVAL;
len = ALIGN(len, huge_page_size(hs));
/*
* VM_NORESERVE is used because the reservations will be
* taken when vm_ops->mmap() is called
* A dummy user value is used because we are not locking
* memory so no accounting is necessary
*/
file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
VM_NORESERVE,
&user, HUGETLB_ANONHUGE_INODE,
(flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
if (IS_ERR(file))
return PTR_ERR(file);
}
//......
retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
//......
}
|
大致三个过程:
- 检查和设置
flag
- 内存对齐
- 执行
vm_mmap_pgoff
检查和设置flag
阶段,我比较关注MAP_HUGETLB
,这是Linux提供的大PAGE
支持,在man7中可以找到大概的介绍,在HUGE PAGE
模式下,可以支持2MB
甚至1GB
大小的PAGE
!大PAGE
可以减少IO访问的次数,同时也会带来大量的内存碎片。在《为什么 Linux 默认页大小是 4KB》中,作者有介绍PAGE
不同大小的影响。
vm_mmap_pgoff
下面是vm_mmap_pgoff
,这里比较接近mmap
的本质了,current->mm
拿到了当前进程的内存映射结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm;
unsigned long populate;
LIST_HEAD(uf);
ret = security_mmap_file(file, prot, flag);
if (!ret) {
if (down_write_killable(&mm->mmap_sem))
return -EINTR;
ret = do_mmap_pgoff(file, addr, len, prot, flag, pgoff,
&populate, &uf);
up_write(&mm->mmap_sem);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
mm_populate(ret, populate);
}
return ret;
}
|
大概分作以下几步:
- 检查文件安全性
mmap
加锁
mmap
映射
mmap
解锁
(还有populate
等步骤,不太明白,但是不影响对mmap
的基本了解,所以本文不过分追究。不过,有机会我还是得去了解了解的~~~基础不太行,先把这些基本的问题搞清楚吧~)
mmap
加锁和解锁逻辑不需要过分关注,只需要知道rw_semaphore
是个读写信号量,在这里实现了类似锁的功能,应该是为了保证数据一致性。
do_mmap
mmap
映射步骤调用的是do_mmap_pgoff
,如下,指向的是do_mmap
:
1
2
3
4
5
6
7
8
|
static inline unsigned long
do_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot, unsigned long flags,
unsigned long pgoff, unsigned long *populate,
struct list_head *uf)
{
return do_mmap(file, addr, len, prot, flags, 0, pgoff, populate, uf);
}
|
do_mmap
如下,摘取了部分片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/*
* The caller must hold down_write(¤t->mm->mmap_sem).
*/
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, vm_flags_t vm_flags,
unsigned long pgoff, unsigned long *populate,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
//......
/* 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 (offset_in_page(addr))
return addr;
//......
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
//......
}
|
先是检查和修改prot
和flags
,这里就不追究这些逻辑了。然后会做内存对齐,检查是否溢出,检查mapping count
是不是满了(意味着mapping
数量是有限的,为什么要有限呢?)之类的。
接着通过get_unmapped_area
获取一个unmapped
的地址,除了溢出检查外,get_unmapped_area
大致流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
get_area = current->mm->get_unmapped_area;
if (file) {
if (file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
} else if (flags & MAP_SHARED) {
/*
* mmap_region() will call shmem_zero_setup() to create a file,
* so use shmem's get_unmapped_area in case it can be huge.
* do_mmap_pgoff() will clear pgoff, so match alignment.
*/
pgoff = 0;
get_area = shmem_get_unmapped_area;
}
addr = get_area(file, addr, len, pgoff, flags);
|
如果传入了一个文件,那么用文件对应的get_unmapped_area
获取地址,或者利用进程的get_unmapped_area
或者利用shmem_get_unmapped_area
。现在,我们的疑问是,文件或者进程等的get_unmapped_area
是怎么mapping
出一块地址给我们的?
找到这么一段arch_get_unmapped_area,arch_get_unmapped_area
会被赋值给current->mm->get_unmapped_area
:
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
|
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;
struct vm_unmapped_area_info info;
unsigned long begin, end;
addr = mpx_unmapped_area_check(addr, len, flags);
if (IS_ERR_VALUE(addr))
return addr;
if (flags & MAP_FIXED)
return addr;
find_start_end(addr, flags, &begin, &end);
if (len > end)
return -ENOMEM;
if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr);
if (end - len >= addr &&
(!vma || addr + len <= vm_start_gap(vma)))
return addr;
}
info.flags = 0;
info.length = len;
info.low_limit = begin;
info.high_limit = end;
info.align_mask = 0;
info.align_offset = pgoff << PAGE_SHIFT;
if (filp) {
info.align_mask = get_align_mask();
info.align_offset += get_align_bits();
}
return vm_unmapped_area(&info);
}
|
主要逻辑是vm_unmapped_area
,在调用vm_unmapped_area
之前,会获取虚拟内存的开始地址和结束地址,然后写入vm_unmapped_area_info
再传入vm_unmapped_area
。
下面是vm_unmapped_area
的实现:
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
|
unsigned long unmapped_area(struct vm_unmapped_area_info *info)
{
//......
vma = rb_entry(mm->mm_rb.rb_node, struct vm_area_struct, vm_rb);
if (vma->rb_subtree_gap < length)
goto check_highest;
//......
while (true) {
/* Visit left subtree if it looks promising */
gap_end = vm_start_gap(vma);
if (gap_end >= low_limit && vma->vm_rb.rb_left) {
struct vm_area_struct *left =
rb_entry(vma->vm_rb.rb_left,
struct vm_area_struct, vm_rb);
if (left->rb_subtree_gap >= length) {
vma = left;
continue;
}
}
//......
}
found:
/* We found a suitable gap. Clip it with the original low_limit. */
if (gap_start < info->low_limit)
gap_start = info->low_limit;
/* Adjust gap address to the desired alignment */
gap_start += (info->align_offset - gap_start) & info->align_mask;
VM_BUG_ON(gap_start + info->length > info->high_limit);
VM_BUG_ON(gap_start + info->length > gap_end);
return gap_start;
}
|
这里的rb
前缀是指红黑树,使用红黑树来管理VMA(Virtual Memory Area)。
先是判断当前进程的虚拟内存块是不是有一个大于或等于length
大小的,只有满足这个条件,才会继续向下寻找。然后在VMA红黑树的左树找到最左侧的一个满足内存块gap
大于或等于length
的虚拟内存块,此时可以满足这个内存块是RB树里面最小的满足大于或等于length
大小的内存块。
这里存在一些盲区:
- VMA红黑树怎么构建的?什么时候构建的?
- 虚拟内存都是按块分配的吗?
最后是mmap_region
:
1
|
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
|
(TODO(这个月底之前吧):内容好多~~不知道的东西有点多,疑惑越来越大,有些问题不好假设了~~所以,mmap_region
这一小结先不写了,得去看看虚拟内存构建等知识)。
查阅其他资料了解到的,mmap_region
是负责创建虚拟内存区域的。会有merge vma,link vma等操作。
不过,总算可以知道,mmap
是映射了虚拟内存上的一块内存。估计程序加载进内存的时候,也会通过这个系统调用!
遗留问题
- VMA如何构建已经构建时机
- 现代一般架构下(比如X86,ARM等),CPU(加上Cache)可以直接操作硬盘吗?还是必须经过RAM?
mmap
可以虚拟地址直接映射到硬盘吗?还是必须经过RAM?
malloc
等和mmap
的关联和区别
番外
我最开始的疑问是,页表是如何构建以及怎么使用的?
因为我们使用的是虚拟内存,并且已经知道进程的内存在mm_struct
管理,但是mm_struct
也是虚拟内存上的,也就是说,如果要找到某个进程的页表,首先得找到这个进程的mm_struct
,因为虚拟内存的映射是不定的,那么得有一个先对固定的地址,使得内核可以找到进程的页表。
在mmap
这一篇中,没能解答这个问题,但是查阅一些资料了解到:
mm_struct
结构体中的pgd_t * pgd;
代表的是物理地址,pgd
指向的就是当前进程的页表,是物理内存上的。那么,只要拿到了pgd
,就可以拿到当前进程的也表了。又因为,task_struct
是内核管理的,内核的也表是固定的/事先知道的(这一点没有疑问,不然内核启动不了),所以每个进程的task_struct
又是可以在物理内存上找到的,进而每个进程的mm_struct
也是在物理内存上可以找到的(通过内核可以找到)。
所以,进程也表加载流程可以是:
- 切换进程
- 找到
pgd
- 通过
pgd
的物理地址,在内存上找到当前进程的页表
- MMU工作等
总结
期望本文可以加深大家对虚拟内存的理解,以上内容加上《进程控制和通信(四) · PCB介绍 》,我有以下结论:
- 进程的页表储存在物理内存上,通过
pgd
代表的物理地址可以找到
mm_struct
表示的是虚拟内存的地址,比如数据段/代码段等等,再通过页表映射到物理内存上
- Linux内核使用红黑树管理了内存块,
mmap
是在这些内存块上映射的
mmap
会有很多内存对齐的检查,在使用传入参数的时候,最好也要考虑对齐
mmap
是会阻塞的,会有读写信号量
mmap
分配的虚拟内存空间可能比实际需要的大
mmap
可能会失败,比如内存不足