代码学习链接:

Linux 源代码 (v6.15.5) - Bootlin Elixir 交叉引用器

理论知识学习链接:小林coding | Java面试学习

进程虚拟内存空间

为了防止多进程运行时造成的内存地址冲突,内核引入了虚拟内存地址,为每个进程提供了一个独立的虚拟内存空间,使得进程以为自己独占全部内存资源。

image-20250624094800914

内核根据进程运行的过程中所需要不同种类的数据而为其开辟了对应的地址空间。分别为:

  • 用于存放进程程序二进制文件中的机器指令代码段
  • 用于存放程序二进制文件中定义的全局变量和静态变量数据段BSS段
    • 那些在代码中被我们指定了初始值的全局变量和静态变量在虚以内存空间中的存储区域我们叫做数据
    • 那些没有指定初始值的全局变量和静态变量在虚以内存空间中的存储区域我们叫做BSS段。这些未初
      始化的全局变量被加载进内存之后会被初始化为0值
  • 用于在程序运行过程中动态申请内存这里的堆指的是OS堆并不是VM中的堆
  • 用于存放这些动态链接库中的代码段,数据段,BSS段,以及通过mmp系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射匿名映射区
  • 用于存放函数调用过程中的局部变量和函数参数
  • 可以使用cat /proc/pid/maps或者pmap pid来查看某个进程的实际虚拟内存布局。

32位

在32位机器上,指针的寻址范围为232,所能表达的虚以内存空间为4GB。所以在32位机器上进程的虚以内存地址范围为:0x00000000-0 xFFFF FFFF。

其中用户态虚拟内存空间为3GB,虚拟内存地址范围为:0x00000000-0xC000000。

内核态虚拟内存空间为1GB,虚拟内存地址范围为:OxC000000-OxFFFF FFFF。

image-20250624095619822
  • 保留区:0x00000000到0x08048000这段虚拟内存地址是一段不可访问的保留区,因为在大多数操作系统中,数值比较小的地址通常被认为不是一个合法的地址,这块小地址是不充许访问的。比如在C语言中我们通常会将一些无效的指针设置为NULL指向这块不允许访问的地址。

  • 代码段和数据段,它们是从程序的二进制文件中直接加载进内存中

  • **BSS段:**BSS段中的数据也存在于二进制文件中,因为内核知道这些数据是没有初值的,所以在二进制文件中只会记录BSS段的大小,在加载进内存时会生成一段0填充的内存空间。

  • 堆空间:从图中的红色箭头我们可以知道在堆空间中地址的增长方向是从低地址到高地址增长。内核中使用start_brk标识堆的起始位置bk标识堆当前的结束位置:当堆申请新的内存空间时,只需要将brk指针增加对应的大小,回收地址时减少对应的大小即可。比如当我们通过malloc向内核申请很小的一块内存时(128K之内),就是通过改变bk位置实现的。堆空间的上边是一段待分配区域,用于扩展堆空间的使用。

  • 文件映射匿名映射区域:进程运行时所依赖的动态链接库中的代码段,数据段,BSS段就加载在这里。还有我们调用mmap映射出来的一段虚拟内存空间也保存在这个区域。注意:在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长

  • 栈空间:在这里会保存函数运行过程所需要的局部变量以及函数参数等函数调用信息。栈空间中的地址增长方向是从高地址向低地址增长。每次进程申请新的栈地址时,其地址值是在减少的。在内核中使用start_stack标识栈的起始位置,RSP寄存器中保存栈顶指针stack pointer,RBP寄存器中保存的是栈基地址。在栈空间的下边也有一段待分配区域用于扩展栈空间,

  • **内核空间:**进程虽然可以看到这段内核空间地址,但是就是不能访问。

进程虚拟内存空间的管理

1
2
3
4
5
6
7
8
9
10
11
12
struct task_struct
{
//进程id
pid_t pid;
//用于标识线程所属的进程pid
pid_t tgid;
//进程打开的文件信息
struct files_struct *files;
//内存描述符表示进程虚拟地址空间
struct mm_struct *mm
........
}

进程描述符task_struct结构中,有一个专门描述进程虚拟地址空间的内存描述符mm_struct结构,这个结构体中包含了前边几个小节中介绍的进程虚以内存空间的全部信息。

每个进程都有唯一的mm_struct结构体,也就是前边提到的每个进程的虚拟地址空间都是独立,互不干扰的。

fork创建流程

当我们调用fork()函数创建进程的时候,表示进程地址空间的mm_struct结构会随着进程描述符task_struct的创建而创建。

  1. 使用_do_fork()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    long _do_fork(unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *parent_tidptr,
    int __user *child_tidptr,
    unsigned long tls)
    {
    ........... 省略 ...........
    struct pid *pid;
    struct task_struct *p;

    ........... 省略 ...........
    // 为进程创建 task_struct 结构,用父进程的资源填充 task_struct 信息
    p = copy_process(clone_flags, stack_start, stack_size,
    child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
    ........... 省略 ...........
    }

    随后会在copy_process函数中创建task_struct结构,并拷贝父进程的相关资源到新进程的task_struct结构里,其中就包括拷贝父进程的虚拟内存空间mm_struct结构。子进程在新创建出来之后它的虚拟内存空间是和父进程的虚拟内存空间一模一样的,直接拷贝过来。

  2. copy_process函数

    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
    static __latent_entropy struct task_struct *copy_process(
    unsigned long clone_flags,
    unsigned long stack_start,
    unsigned long stack_size,
    int __user *child_tidptr,
    struct pid *pid,
    int trace,
    unsigned long tls,
    int node)
    {
    struct task_struct *p;
    // 创建 task_struct 结构,通过 dup_task_struct 复制父进程的 task_struct
    p = dup_task_struct(current, node);

    ....... 初始化子进程 ..........

    // 继承父进程打开的文件描述符
    retval = copy_files(clone_flags, p);
    // 继承父进程所属的文件系统
    retval = copy_fs(clone_flags, p);
    // 继承父进程注册的信号以及信号处理函数
    retval = copy_sighand(clone_flags, p);
    retval = copy_signal(clone_flags, p);
    // 继承父进程的虚拟内存空间
    retval = copy_mm(clone_flags, p);
    // 继承父进程的 namespaces
    retval = copy_namespaces(clone_flags, p);
    // 继承父进程的 IO 信息
    retval = copy_io(clone_flags, p);

    ........ 省略 ........
    // 分配 CPU
    retval = sched_fork(clone_flags, p);
    // 分配 pid
    pid = alloc_pid(p->nsproxy->pid_ns_for_children);
    ........ 省略 ........
    }

    copy_mm函数在这里完成了子进程虚拟内存空间mm_struct结构的的创建以及初始化

  3. copy_mm函数

    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
    static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
    {
    // 子进程虚拟内存空间
    struct mm_struct *mm, *oldmm;
    int retval;

    ....... 省略 .......

    tsk->mm = NULL;
    tsk->active_mm = NULL;

    // 获取父进程虚拟内存空间
    oldmm = current->mm;
    if (!oldmm)
    return 0;

    ....... 省略 .......

    if (clone_flags & CLONE_VM) {
    // 增加父进程虚拟内存空间的引用计数
    mmget(oldmm);
    // 直接将父进程的虚拟内存空间赋值给子进程(线程)
    // 线程共享其所属进程的虚拟内存空间
    mm = oldmm;
    goto good_mm;
    }

    retval = -ENOMEM;
    // 如果是fork系统调用创建出的子进程,则将父进程的虚拟内存空间以及相关页表拷贝到子进程中的mm_struct结构中。
    mm = dup_mm(tsk);
    if (!mm)
    goto fail_nomem;

    good_mm:
    // 将拷贝出来的父进程虚拟内存空间 mm_struct 赋值给子进程
    tsk->mm = mm;
    tsk->active_mm = mm;
    return 0;

    ....... 省略 .......
    }

    copy_mm函数首先会将父进程的虚拟内存空间current->mm赋值给指针oldmm。然后通过dup_mm函数将父进程的虚以内存空间以及相关页表拷贝到子进程的mm_struct结构中。最后将拷贝出来的mm_struct赋值给子进程的task_struct结构。

    ==特点:通过fork()函数创建出的子进程,它的虚拟内存空间以及相关页表相当于父进程虚拟内存空间的一份拷贝,直接从父进程中拷贝到子进程中。==

vfork/clone

而当我们通过vfork或者clone系统调用创建出的子进程,首先会设置CLONE_VM标识,这样来到copy_mm函数中就会进入if(clone_flags&CLONE_VM)条件中,在这个分支中会将父进程的虚拟内存空间以及相关页表直接赋值给子进程。这样一来父进程和子进程的虚拟内存空间就变成共享的了。也就是说父子进程之间使用的虚拟内存空间是一样的,并不是一份拷贝

子进程共享了父进程的虚以内存空间,这样子进程就变成了我们熟悉的线程,==是否共享地址空间几乎是进程和线程之间的本质区别。Liux内核并不区别对待它们,线程对于内核来说仅仅是一个共享特定资源的进程而已。==

==内核线程和用户态线程的区别就是内核线程没有相关的内存描述符mm_struct,内核线程对应的task_struct结构中的mm域指向Null,所以内核线程之间调度是不涉及地址空间切换的。==

当一个内核线程被调度时,它会发现自己的虚拟地址空间为Null,虽然它不会访问用户态的内存,但是它会访问内核内存,聪明的内核会将调度之前的上一个用户态进程的虚拟内存空间mm_struct直接赋值给内核线程,因为内核线程不会访问用户空间的内存,它仅仅只会访问内核空间的内存,所以直接复用上一个用户态进程的虚拟地址空间就可以避免为内核线程分配mm_struct和相关页表的开销,以及避免内核线程之间调度时地址空间的切换开销。

==父进程与子进程的区别,进程与线程的区别,以及内核线程与用户态线程的区别其实都是围绕着这个mm_struct展开的。==

虚拟内存管理

首先我们需要解析描述符task_struct结构

1
2
3
4
5
6
7
8
9
10
11
struct task_struct
//进程id
pid_t pid;
//用于标识线程所属的进程pid
pid_t tgid;
//进程打开的文件信息
struct files struct *files;
//内存描述符表示进程虚拟地址空间
struct mm struct *mm;
//-----------省略-----------
}

mm_struct

img

每个进程都有唯一的mm_struct结构体,也就是前边提到的每个进程的虚拟地址空间都是独立,互不干扰的。内核中采用了一个叫做内存描述符的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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
struct mm_struct {
/* 内存区域管理 */
struct vm_area_struct *mmap; /* VMA 链表头 - 所有内存区域的链表 */
struct rb_root mm_rb; /* VMA 红黑树根 - 用于快速查找内存区域 */

/* 地址空间缓存 */
u32 vmacache_seqnum; /* 每线程 vmacache 序列号 - 用于缓存虚拟地址查找 */

#ifdef CONFIG_MMU
/* 地址分配策略 */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags); /* 未映射区域分配函数指针 */
#endif

/* 地址空间布局 */
unsigned long mmap_base; /* mmap 区域基地址 - 内存映射的起始地址 */
unsigned long mmap_legacy_base; /* 自底向上分配的 mmap 基地址 - 兼容旧布局 */
unsigned long task_size; /* 任务虚拟空间大小 - 用户空间总大小 */
unsigned long highest_vm_end; /* 最高 vma 结束地址 - 最顶端内存区域的结束地址 */

/* 页表管理 */
pgd_t * pgd; /* 页全局目录指针 - 顶级页表 */

/* 引用计数 */
atomic_t mm_users; /* 用户空间使用者数量 - 共享此 mm 的线程数 */
atomic_t mm_count; /* 对 mm_struct 的引用计数 - 包括内核引用 */

/* 页表统计 */
atomic_long_t nr_ptes; /* PTE 页表页数量 - 页表页计数 */
#if CONFIG_PGTABLE_LEVELS > 2
atomic_long_t nr_pmds; /* PMD 页表页数量 - 中间页目录计数 */
#endif

/* 内存区域统计 */
int map_count; /* VMA 数量 - 当前映射的内存区域数 */

/* 同步机制 */
spinlock_t page_table_lock; /* 保护页表和一些计数器 */
struct rw_semaphore mmap_sem; /* 内存映射信号量 - 保护 VMA 修改 */

/* 全局内存列表 */
struct list_head mmlist; /* 可能被换出的 mm 列表 - 全局链接在 init_mm.mmlist */

/* 内存使用统计 */
unsigned long hiwater_rss; /* RSS 使用高水位标记 - 物理内存峰值 */
unsigned long hiwater_vm; /* 虚拟内存使用高水位标记 - 虚拟内存峰值 */

/* 详细内存统计 */
unsigned long total_vm; /* 映射的总页数 - 所有 VMA 的总大小 */
unsigned long locked_vm; /* 被锁定的页数 - 设置 PG_mlocked 的页面 */
unsigned long pinned_vm; /* 永久增加引用计数的页数 - 不可换出页 */
unsigned long shared_vm; /* 共享页数 - 文件映射的共享页面 */
unsigned long exec_vm; /* 可执行页数 - VM_EXEC & ~VM_WRITE 的页面 */
unsigned long stack_vm; /* 栈页数 - VM_GROWSUP/DOWN 的栈页面 */
unsigned long def_flags; /* 默认 VMA 标志 - 新 VMA 的默认保护标志 */

/* 内存段边界 */
unsigned long start_code, end_code; /* 代码段起止地址 */
unsigned long start_data, end_data; /* 数据段起止地址 */
unsigned long start_brk, brk; /* 堆起始地址和当前结束地址 */
unsigned long start_stack; /* 栈起始地址 */
unsigned long arg_start, arg_end; /* 命令行参数起止地址 */
unsigned long env_start, env_end; /* 环境变量起止地址 */

/* 辅助向量 */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* 保存的辅助向量 - 用于 /proc/PID/auxv */

/* RSS 统计 */
struct mm_rss_stat rss_stat; /* RSS 统计信息 */

/* 二进制格式 */
struct linux_binfmt *binfmt; /* 二进制格式处理程序 */

/* CPU 关联 */
cpumask_var_t cpu_vm_mask_var; /* 上次在此 mm 上运行过的 CPU 掩码 */

/* 架构特定上下文 */
mm_context_t context; /* 架构特定的 MMU 上下文 */

/* 标志位 */
unsigned long flags; /* 内存管理标志 - 必须使用原子位操作访问 */

/* 核心转储支持 */
struct core_state *core_state; /* 核心转储支持状态 */

#ifdef CONFIG_AIO
/* 异步 I/O */
spinlock_t ioctx_lock; /* AIO 上下文锁 */
struct kioctx_table __rcu *ioctx_table; /* AIO 上下文表 */
#endif

#ifdef CONFIG_MEMCG
/* 内存控制组 */
struct task_struct __rcu *owner; /* 此 mm 的规范所有者 */
#endif

/* 可执行文件链接 */
struct file __rcu *exe_file; /* /proc/<pid>/exe 符号链接指向的文件 */

#ifdef CONFIG_MMU_NOTIFIER
/* MMU 通知器 */
struct mmu_notifier_mm *mmu_notifier_mm; /* MMU 通知器管理 */
#endif

#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
/* 透明大页支持 */
pgtable_t pmd_huge_pte; /* 受 page_table_lock 保护的大页 PTE */
#endif

#ifdef CONFIG_CPUMASK_OFFSTACK
/* CPU 掩码分配 */
struct cpumask cpumask_allocation; /* 为 cpu_vm_mask 分配的 cpumask */
#endif

#ifdef CONFIG_NUMA_BALANCING
/* NUMA 平衡 */
unsigned long numa_next_scan; /* 下次标记 pte_numa 的时间 */
unsigned long numa_scan_offset; /* 扫描和设置 pte_numa 的起始点 */
int numa_scan_seq; /* 防止两个线程同时设置 pte_numa */
#endif

#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
/* TLB 刷新状态 */
bool tlb_flush_pending; /* 是否有批处理 TLB 刷新操作 */
#endif

/* 用户空间探针 */
struct uprobes_state uprobes_state; /* 用户空间探针状态 */

#ifdef CONFIG_X86_INTEL_MPX
/* Intel MPX 支持 */
void __user *bd_addr; /* 边界目录地址 */
#endif
};
  • start_codeend_code定义代码段的起始和结束位置,程序编译后的二进制文件中的机器码被加载进内存之后就存放在这里。

  • start_dataend_data定义数据段的起始和结束位置,二进制文件中存放的全局变量和静态变量被加载进内存中就存放在这里。

  • 后面紧挨着的是BSS段,用于存放未被初始化的全局变量和静态变量,这些变量在加载进内存时会生成一段0填充的内存区域(BSS段),BSS段的大小是固定的

  • 下面就是OS堆了,在堆中内存地址的增长方向是由低地址向高地址增长,start_brk定义堆的起始位置,brk定义堆当前的结束位置。

  • 接下来就是内存映射区,在内存映射区内存地址的增长方向是由高地址向低地址增长,mmap_base定义内存映射区的起始地址。进程运行时所依赖的动态链接库中的代码段,数据段,BSS段以及我们调用mmap映射出来的一段虚以内存空间就保存在这个区域。

  • start_stack是栈的起始位置在RBP寄存器中存储,栈的结束位置也就是栈顶指针stack_pointer在RSP寄存器中存储。在栈中内存地址的增长方向也是由高地址向低地址增长。

  • arg_startarg_end是参数列表的位置,env_startenv_end是环境变量的位置。它们都位于栈中的最高地址处。

img

mm_struct结构体中的**total_vm表示在进程虚拟内存空间中总共与物理内存映射的页的总数**。==注意映射这个概念,它表示只是将虚拟内存与物理内存建立关联关系,并不代表真正的分配物理内存。==

当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出。locked_vm就是被锁定不能换出的内存页总数,pinned_vm表示既不能换出,也不能移动的内存页总数。data_vm表示数据段中映射的内存页数目,exec_vm是代码段中存放可执行文件的内存页数目,stack_vm是栈中所映射的内存页数目,这些变量均是表示进程虚拟内存空间中的虚拟内存使用情况。

vm_area_struct

而在划分出的这些虚拟内存空间中如上图所示,里边又包含了许多特定的虚以内存区域,比如:代码段,数据段,堆,内存映射区,栈。那么这些虚拟内存区域在内核中又是如何表示的呢?一个新的结构体vm_area_struct,.正是这个结构体描述了这些虚拟内存区域VMA (virtual memory 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
struct vm_area_struct {
/* 第一缓存行:用于 VMA 树遍历的信息 */

unsigned long vm_start; /* 在所属 mm_struct 中的起始地址 */
unsigned long vm_end; /* 在所属 mm_struct 中的结束地址(下一个字节的地址) */

/* 按地址排序的 VMA 链表 */
struct vm_area_struct *vm_next; /* 下一个 VMA */
struct vm_area_struct *vm_prev; /* 前一个 VMA */

/* 红黑树节点 */
struct rb_node vm_rb; /* 用于将 VMA 插入 mm_struct 的红黑树 */

/*
* 此 VMA 左侧的最大空闲内存间隙(字节)
* 用于 get_unmapped_area 找到合适大小的空闲区域
*/
unsigned long rb_subtree_gap;

/* 第二缓存行开始 */

struct mm_struct *vm_mm; /* 所属的地址空间 (mm_struct) */
pgprot_t vm_page_prot; /* 此 VMA 的访问权限 */
unsigned long vm_flags; /* 标志位,定义在 mm.h */

/*
* 对于有后备存储的文件映射区域,
* 连接到 address_space->i_mmap 的区间树
*/
struct {
struct rb_node rb; /* 区间树节点 */
unsigned long rb_subtree_last; /* 子树最后结束地址 */
} shared;

/*
* 匿名映射和文件私有映射的链表管理
* - 文件的 MAP_PRIVATE VMA 可能在 i_mmap 树和 anon_vma 链表中(COW 后)
* - MAP_SHARED VMA 只能在 i_mmap 树中
* - 匿名 MAP_PRIVATE、栈或 brk VMA(文件为 NULL)只能在 anon_vma 链表中
*/
struct list_head anon_vma_chain; /* 序列化:mmap_sem & page_table_lock */
struct anon_vma *anon_vma; /* 序列化:page_table_lock */

/* 操作此结构的函数指针 */
const struct vm_operations_struct *vm_ops;

/* 后备存储信息 */
unsigned long vm_pgoff; /* 在 vm_file 中的偏移量(以 PAGE_SIZE 为单位) */
struct file *vm_file; /* 映射的文件(可为 NULL) */
void *vm_private_data; /* 私有数据(原用于共享内存的 vm_pte) */

#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU 系统的映射区域 */
#endif

#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* VMA 的 NUMA 策略 */
#endif
};

每个vm_area_struct结构对应于虚拟内存空间中的唯一虚拟内存区域VMA,vm_start指向了这块虚以内存区域的起始地址(最低地址),vm_start本身包含在这块虚拟内存区域内。vm_end指向了这块虚以内存区域的结束地址(最高地址),而vm_end本身包含在这块虚拟内存区域之外,所以vm_area_struct结构描述的是[vm_start,vm_end)这样一段左闭右开的虚拟内存区域。

image.png

虚拟内存区域在内核中是如何被组织的

  • 双向链表

    在内核中其实是通过一个struct vm_area_struct结构的双向链表将虚拟内存空间中的这些虚拟内存区域VMA串联起来的

    vm_area_struct结构中的vm_next,vm_prev指针分别指向VMA节点所在双向链表中的后继节点和前驱节点,内核中的这个VMA双向链表是有顺序的,所有VMA节点按照低地址到高地址的增长方向排序。双向链表中的最后一个VMA节点的vm_next指针指向NULL,双向链表的头指针存储在内存描述符struct mm_struct结构中的mmap中,正是这个mmap串联起了整个虚拟内存空间中的虚拟内存区域。

    image.png
  • 红黑树

    尤其在进程虚拟内存空间中包含的内存区☒域VMA比较多的情况下,使用红黑树查找特定虚拟内存区域的时间复杂度是O(IogN),可以显著减少查找所需的时间。

    每个VMA区域都是红黑树中的一个节点,通过struct vm_area_struct结构中的vm_rb将自己连接到红黑树中。

    而红黑树中的根节点存储在内存描述符struct mm_struct中的mm_rb中:

    image.png

虚拟内存区域的访问权限和行为规范

vm_page_protvm_flags都是用来标记vm_area_struct结构表示的这块虚以内存区域的访问权限和行为规范。

内核会将整块物理内存划分为一页一页大小的区域,以页为单位来管理这些物理内存,每页大小默认4K。而虚以内存最终也是要和物理内存一一映射起来的,所以在虚以内存空间中也有虚以页的概念与之对应,虚拟内存中的虚拟页映射到物理内存中的物理页。无论是在虚以内存空间中还是在物理内存中,内核管理内存的最小单位都是页。

  • ==vm_page_prot==偏向于定义底层内存管理架构中页这一级别的访问控制权限,它可以直接应用在底层页表中,它是一个具体的概念。虚拟内存区域VMA由许多的虚拟(page)组成,每个虚拟页需要经过页表的转换才能找到对应的物理页面。页表中关于内存页的访问权限就是由vm_page_prot决定的。

  • ==vm_flags==则偏向于定于整个虚拟内存区域的访问权限以及行为规范。描述的是虚拟内存区域中的整体信息,而不是虚拟内存区域中具体的某个独立页面。它是一个抽象的概念。可以通过vma->vm_page_prot=vm_get_page_prot(vma->vm_f1ags)实现到具体页面访问权限vm_page_prot的转换。

    标志 描述
    VM_READ 0x1 可读
    VM_WRITE 0x2 可写
    VM_EXEC 0x4 可执行
    VM_SHARED 0x8 共享映射,虚拟内存区域映射的物理内存是否可以在多进程之间共享
    VM_MAYREAD 0x10 可能可读
    VM_MAYWRITE 0x20 可能可写
    VM_MAYEXEC 0x40 可能可执行
    VM_GROWSDOWN 0x100 向下扩展的栈
    VM_DENYWRITE 0x800 拒绝写入文件
    VM_LOCKED 0x2000 内存锁定 (mlock)
    VM_IO 0x00004000 虚拟内存区域可以映射至设备 IO 空间中
    image.png

内存映射中的映射关系

接下来的三个属性anon_vma,vm_file,vm_pgoff分别和虚拟内存映射相关,虚拟内存区域可以映射到物理内存上,也可以映射到文件中,==映射到物理内存上我们称之为匿名映射,映射到文件中我们称之为文件映射。==

image.png

当我们调用malloc申请内存时,如果申请的是小块内存(低于128K)则会使用do_brk()系统调用通过调整堆中的brk指针大小来增加或者回收堆内存

如果申请的是比较大块的内存(超过128K)时,则会调用mmap在上图虚以内存空间中的文件映射与匿名映射区创建出一块VMA内存区域(这里是匿名映射)。这块匿名映射区域就用struct anon_vma结构表示。

当调用mmap进行文件映射时,vm_file属性就用来关联被映射的文件。这样一来虚拟内存区域就与映射文件关联了起来。vm_pgoff则表示映射进虚拟内存中的文件内容,在文件中的偏移。

当然在匿名映射中,vm_area_struct结构中的vm_file就为null,vm_pgoff也就没有了意义。vm_private_data则用于存储VMA中的私有数据。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int mmap_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;

// 检查映射大小是否合法
if (off + size > MYDEV_SIZE) {
pr_err("Invalid mmap offset or size\n");
return -EINVAL;
}

// 设置非缓存访问
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

// 关联虚拟内存区域到设备数据
vma->vm_ops = &vm_ops;
vma->vm_private_data = file->private_data;

return 0;
}

针对虚拟内存区域的相关操作

struct vm_area_struct结构中还有一个**vm_ops用来指向针对虚拟内存区域VMA的相关操作的函数指针**

1
2
3
4
5
6
7
struct vm_operations_struct{
void (*open)(struct vm_area_struct area);
void (*close)(struct vm_area_struct area);
vm_fault_t (*fault)(struct vm_fault *vmf);
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
.....
}
  • VMA 添加时:虚拟内存区域(VMA)加入进程地址空间时,内核自动调用其 回调函数。open
  • VMA 移除时:VMA 从进程地址空间解除映射时,内核自动调用其 回调函数。close
  • 缺页异常触发时:访问未驻留物理内存的页面(如未分配或已换出至交换区)引发缺页异常,内核调用 回调函数处理。 fault
  • 写时复制场景:尝试写入只读页面时(例如写时复制机制触发),内核调用page_mkwrite回调函数。