本文分析基于Android14(aosp分支android-14.0.0_r28)(内核分支common-android14-6.1)
前言
在我们之前的binder探索中,已遇到过很多通过syscall调用驱动函数的情况,比如:open、ioctl、mmap等。但这些只是它们在用户空间的封装,而binder_xxx是它们在内核中的名字,比如:binder_open、binder_mmap、binder_ioctl等。本篇我们就来分析其中几个重点函数,通过它们来了解binder驱动在内核中所做的操作。
// common/drivers/android/binder.c
const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
内核结构体(struct)
内核中定义了众多的结构体(struct)来表示系统中的抽象概念或数据集,这些struct的作用就是通过结构化的方式,描述这些抽象概念和数据集,因此它们也经常被称为xx描述符。而binder驱动运行过程中会涉及以下struct,因此我们先简单了解一下,方便后续学习驱动函数。
进程结构体
task_struct,进程描述符,内核描述一个进程的结构体,即进程相关的所有数据和信息都保存在这个结构体的字段中。因此,该结构体极其庞大且复杂,我们取其中入门级的字段作简要介绍。另外,内核中提供一个全局宏current,它是一个指向当前运行进程的task_struct的指针。有了它,内核开发者就可以随时使用当前进程相关的各种数据和信息。
虽然我们将task_struct称为进程描述符,但实际上内核也会用task_struct结构体来存储线程相关数据和信息。从内核的角度看,线程和进程有区别,但不大。
// common/include/linux/sched.h
struct task_struct {
// 进程/线程ID
// 主线程中:pid = tgid; 子线程中:pid ≠ tgid; 主线程和子线程的tgid相等
pid_t pid;
// 线程组ID
pid_t tgid;
// 指向当前进程的主线程结构体,即线程组中的领头线程,通常也是当前进程创建的第一个线程
struct task_struct *group_leader;
// 指向代表用户空间虚拟内存的结构的指针
// 进程内所有线程的mm指针都相等,不同进程则mm指针不一样。这就是我们常说的内存空间,进程间隔离,进程内共享。
struct mm_struct *mm;
// 与进程关联的文件系统信息
struct fs_struct *fs;
// 指向结构体files_struct,此结构管理着进程打开的所有文件描述符
// 进程打开的文件和内存一样,也是进程间隔离,进程内共享。
struct files_struct *files;
// 进程名,TASK_COMM_LEN = 16,由于字符数组最后一位为终止符Null,因此最大长度为15字节
char comm[TASK_COMM_LEN];
};
内存结构体
在学习相关结构体之前,我们需要先了解一些操作系统内存相关的基础知识:寻址空间、用户虚拟内存空间、内核虚拟内存空间及其组织结构。
在32位设备中,通常使用int整型来存放一个内存地址,因此我们可以获得最多2^32个地址(2的32次方)。而每个地址能存放一个字节(byte)的数据,因此逻辑上我们能使用的内存空间大小为:2^32 x 1 (byte) = 4,294,967,296 (byte) = 4,194,304 (KB) = 4096 (MB) = 4(GB)。这个4GB的空间就被叫做寻址空间,32位设备的寻址空间大小就为4GB。
当然,我们这里谈到的空间是逻辑上的概念,通常也叫做虚拟内存空间,它并不是物理内存(现在手机或电脑的物理内存基本都比4GB大)。系统通过页表映射将虚拟内存地址跟物理内存地址关联起来,进程只面向虚拟内存工作,而系统通过MMU(Memory Management Unit)将虚拟地址转换为物理地址。
进程运行所需的虚拟内存空间被分为了2部分,用户空间(3GB)和内核空间(1GB)。用户空间存放进程相关数据,而内核空间仅在进程处于内核态时才能被访问,存储的是内核运行时所需的数据结构。值得注意的是,内核空间是系统中所有进程共用的空间,换句话说就是所有进程处于内核态时面对的都是同样一块内存空间。由此可见,内核空间是相当宝贵的。
以下为进程的虚拟内存布局概况
内核主要通过2个结构体来管理一个进程的用户空间虚拟内存:
- mm_struct(内存描述符):包括内存映射、页表统计、区域划分、同步保护等数据
- vm_area_struct(虚拟内存区域描述符),简称vma,代表一块连续的虚拟内存空间。进程中的代码段、数据段、堆、栈、内存映射等都使用此结构来代表。
mm_struct(内存描述符)
// common/include/linux/mm_types.h
struct mm_struct {
// 用于管理进程所有虚拟内存区域(vma)的数据结构Maple Tree
// Maple Tree于Linux 6.1版本出现,用于代替之前的rbtree
struct maple_tree mm_mt;
// 从用户虚拟内存空间中获取一个未被映射区间的起始地址
// 注:这里设置的是默认函数,实际do_mmap时可能会根据映射类型执行指定的get_unmapped_area函数
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
// 内存映射区的基地址(默认从高地址向低地址增长)
unsigned long mmap_base;
// 进程用户空间边界地址(32位系统为0xC0000000)
unsigned long task_size;
// 指向Page Global Directory(顶级页表项)
pgd_t * pgd;
// 内存映射操作同步锁(比如:mmap、munmap)
// 写操作需独占锁,读操作可共享
struct rw_semaphore mmap_lock;
// start_code, end_code:Code段的起始和结束地址(对应可执行文件的.text段)
// start_data, end_data:Data段的起始和结束地址(对应.data和.bss段)
unsigned long start_code, end_code, start_data, end_data;
// start_brk:堆的起始地址
// brk:当前堆顶地址
// start_stack:用户空间栈的起始地址(向下增长)
unsigned long start_brk, brk, start_stack;
// arg_start, arg_end:命令行参数(argv)起始和结束地址
// env_start, env_end:环境变量(envp)起始和结束地址
unsigned long arg_start, arg_end, env_start, env_end;
// 指向与进程相关的可执行文件
struct file __rcu *exe_file;
};
vm_area_struct(虚拟内存区域描述符)
// common/include/linux/mm_types.h
/*
* This struct describes a virtual memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
// vma的起始和结束虚拟地址
// 左闭右开区间 [vm_start, vm_end)
unsigned long vm_start;
unsigned long vm_end;
// vma所属的内存描述符(mm_struct)
struct mm_struct *vm_mm;
// vma行为属性和访问权限,通过位记录各个标识的值
// 若想修改标识位,使用vm_flags_{init|reset|set|clear|mod}函数操作
// 常见的取值及含义请参考附录
const vm_flags_t vm_flags;
// vma操作函数表指针,vm_operations_struct存放vma的生命周期hook函数、自定义操作函数等
// 比如:open、close是vma加入或从进程移除时调用的函数,fault是产生缺页异常时调用的函数
const struct vm_operations_struct *vm_ops;
// 若当前为文件映射,则标识映射内容在文件中的起始偏移量,单位是PageSize
// 比如:vm_pgoff为2,PageSize是4KB,那么偏移量为4096 * 2 = 8192字节
// 若当前为匿名映射,即无文件关联,那么vm_pgoff = 0
unsigned long vm_pgoff;
// 若当前为文件映射,则指向映射的文件对象
// 若当前为匿名映射,则为NULL
struct file * vm_file;
};
我们可以通过系统命令查看当前进程的vma情况:cat /proc/<pid>/maps。
其中的每一行都代表一个vma,每一列的含义如下:
-
vm_start, vm_end:区域起始、结束地址
-
vm_flags权限标志:并不包含所有标志位,仅展示以下4位 读权限:r(可读)、-(不可读) 写权限:w(可写)、-(不可写) 执行权限:x(可执行)、-(不可执行) 共享属性:p(私有映射,写时复制)、s(共享映射)
-
偏移量:16进制偏移量(单位:字节),假设此列数值为offset,那么它与vm_pgoff的换算关系为:offset / page_size = vm_pgoff 比如:若page_size=4KB,那么第四行vma的vm_pgoff = 0x0000b000 / 0x1000 = 0xB = 11(十进制)
-
设备号:主设备号和次设备号(major:minor),映射文件所在设备的标识符。 若是匿名映射,则为00:00。
-
文件inode编号(十进制):若是匿名映射,则为0。
-
文件路径: 若是文件映射,则显示文件路径。 若是匿名映射,则显示[anon:xxxxxxx]。 以及其他特殊名称,比如:[stack]表示用户态栈,[vvar]表示内核与用户共享的只读数据等等。
binder_open
Android系统中所有binder通信进程都需要先通过此函数打开binder设备文件,才能继续后续的操作。而binder_open是在ProcessState实例初始化时调用的。
// common/drivers/android/binder.c
static int binder_open(struct inode *nodp, struct file *filp) {
// binder_proc_wrap是binder_proc的封装结构体
// 除了包含binder_proc,还包含dbitmap用于管理Binder资源
struct binder_proc_wrap *proc_wrap;
// 从驱动角度看,binder_proc就代表一个binder通信进程
struct binder_proc *proc, *itr;
... ...
// 为binder_proc_wrap申请空间,并将proc_wrap中的指针字段赋给proc变量
// 但此时proc还没有内容,接下来的代码将会初始化binder_proc中的各个变量
proc_wrap = kzalloc(sizeof(*proc_wrap), GFP_KERNEL);
if (proc_wrap == NULL)
return -ENOMEM;
proc = &proc_wrap->proc;
... ...
// current为内核的全局宏,它是包含当前进程信息的结构体
// 其中group_leader是当前进程所属线程组的主线程结构体(task_struct)指针
// 根据binder的上下文情景,下面这行代码其实就是将主线程结构体指针保存到binder_proc->tsk中
proc->tsk = current->group_leader;
... ...
// 初始化待处理的Binder工作项双向链表
INIT_LIST_HEAD(&proc->todo);
// 初始化进程冻结时保存被暂停事务的链表
init_waitqueue_head(&proc->freeze_wait);
// 设置binder_proc的进程调度策略
if (binder_supported_policy(current->policy)) {
proc->default_priority.sched_policy = current->policy;
proc->default_priority.prio = current->normal_prio;
} else {
proc->default_priority.sched_policy = SCHED_NORMAL;
proc->default_priority.prio = NICE_TO_PRIO(0);
}
// 从inode中获取binder设备结构体指针
if (is_binderfs_device(nodp)) {
// nodp->i_private中存放的是binder_device指针
// 它是在binderfs_binder_device_create函数中放入i_private中的
// binderfs_binder_device_create我们在上一篇文章中提到过
// 它的主要作用就是创建bidner_device和inode并初始化
binder_dev = nodp->i_private;
info = nodp->i_sb->s_fs_info;
binder_binderfs_dir_entry_proc = info->proc_log_dir;
} else {
binder_dev = container_of(filp->private_data,
struct binder_device, miscdev);
}
// 初始化binder_proc->binder_alloc
// binder_alloc是代表和管理binder数据缓冲区的结构体
// 但此时的binder_alloc并未分配虚拟空间,空间的分配需要等到mmap调用时
binder_alloc_init(&proc->alloc);
... ...
proc->pid = current->group_leader->pid;
... ...
// 初始化存放休眠binder线程的双向链表
INIT_LIST_HEAD(&proc->waiting_threads);
// 将创建好的binder_proc放入file->pricate_data中,后续会取出使用
filp->private_data = proc;
... ...
// 遍历全局的Binder进程列表,标记当前进程是否已注册
hlist_for_each_entry(itr, &binder_procs, proc_node) {
if (itr->pid == proc->pid) {
existing_pid = true;
break;
}
}
// 将此进程proc_node添加到全局binder进程哈希链表中
hlist_add_head(&proc->proc_node, &binder_procs);
... ...
// 若是新注册的进程,在debugfs中创建进程文件,文件名为pid
// binder_debugfs_dir_entry_proc即为上一篇中binder_init函数创建的binder/proc目录
if (binder_debugfs_dir_entry_proc && !existing_pid) {
... ...
proc->debugfs_entry = debugfs_create_file(strbuf, 0444,
binder_debugfs_dir_entry_proc,
(void *)(unsigned long)proc->pid,
&proc_fops);
}
// 若是新注册的进程,在binderfs中创建进程文件,文件名为pid
if (binder_binderfs_dir_entry_proc && !existing_pid) {
... ...
binderfs_entry = binderfs_create_file(binder_binderfs_dir_entry_proc,
strbuf, &proc_fops, (void *)(unsigned long)proc->pid);
... ...
}
return 0;
}
综上,binder_open主要工作就是创建binder_proc,将进程相关的数据保存在其中。并将其添加到全局binder进程哈希链表中,最后在debugfs和binderfs中创建进程文件。
binder_mmap
ProcessState类调用binder_open打开binder驱动文件之后,会立即通过binder_mmap尝试分配binder通信所需的虚拟内存空间。
// frameworks/native/libs/binder/ProcessState.cpp
ProcessState::ProcessState(const char* driver) {
// 打开Binder设备文件
base::Result<int> opened = open_driver(driver);
if (opened.ok()) {
// 通过系统调用mmap将用户虚拟空间和内核虚拟空间映射到同一块物理内存上
// 该内存区域大小为BINDER_VM_SIZE(1M - (内存页大小 * 2)),即Binder通信传输内容的大小限制
// Android 14及之前内存页大小都为4K
mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, opened.value(), 0);
... ...
}
... ...
}
通过syscall,最终会调用内核函数do_mmap。
// common/mm/mmap.c
unsigned long do_mmap(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) {
// 从当前全局描述符current中取出mm_struct(内存描述符)
struct mm_struct *mm = current->mm;
vm_flags_t vm_flags;
// 根据prot和flags,进行标志位检查和设置
... ...
// 若没有设置固定地址标识MAP_FIXED,且addr小于mmap_min_addr,那么将最终地址确定为页对齐后的地址
if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);
// 将映射区域长度进行页对齐
len = PAGE_ALIGN(len);
... ...
// 从当前进程的虚拟空间中,找到符合参数条件的一块空间,并返回它的起始地址
// get_unmapped_area函数中会根据当前参数,选择相应的函数执行查找逻辑
// 对于binder中的mmap来说,将使用内存描述符(mm_struct)中的默认函数来查找地址
addr = get_unmapped_area(file, addr, len, pgoff, flags);
// 检查、合并标志位,并统一设置到变量vm_flags中
... ...
// 准备就绪,mmap_region函数负责创建虚拟内存区域
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
... ...
return addr;
}
// common/mm/mmap.c
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf) {
... ...
ret = __mmap_region(file, addr, len, vm_flags, pgoff, uf);
... ...
return ret;
}
static unsigned long __mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf) {
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma = NULL;
struct vm_area_struct *next, *prev, *merge;
pgoff_t pglen = len >> PAGE_SHIFT;
unsigned long charged = 0;
unsigned long end = addr + len;
unsigned long merge_start = addr, merge_end = end;
pgoff_t vm_pgoff;
int error;
// 以当前内存描述符中的Maple Tree和映射空间的起始结束地址为参数,创建一个对应的结构体ma_state
// ma_state是操作Maple tree期间保存操作状态的结构体,简称为mas
MA_STATE(mas, &mm->mm_mt, addr, end - 1);
... ...
// mas_next返回mas指定条件区域的下一个vma区域指针
next = mas_next(&mas, ULONG_MAX);
// mas_prev返回mas指定条件区域的上一个vma区域指针
prev = mas_prev(&mas, 0);
// 根据next和prev判断,mas指定区域是否有可能与next和prev合并到一起
... ...
// 若可以,则调用vma_expand进行合并
if (vma &&
!vma_expand(&mas, vma, merge_start, merge_end, vm_pgoff, next)) {
khugepaged_enter_vma(vma, vm_flags);
goto expanded;
}
... ...
// 若不能合并,则为新的vma结构体申请空间,并设置起止地址、页偏移、访问权限等属性
vma = vm_area_alloc(mm);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
vma->vm_start = addr;
vma->vm_end = end;
vm_flags_init(vma, vm_flags);
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
... ...
if (file) {
// 文件映射,将新的vma与映射文件关联
vma->vm_file = get_file(file);
// mmap_file函数中会执行 file->f_op->mmap(file, vma)
// 调用了文件的mmap操作函数,在当前语境下,就是binder_mmap函数
error = mmap_file(file, vma);
... ...
} else if (vm_flags & VM_SHARED) {
// 共享内存映射,会映射到默认文件dev/zero
error = shmem_zero_setup(vma);
... ...
} else {
// 匿名映射,不关联任何文件
vma_set_anonymous(vma);
}
... ...
}
以上代码是mmap系统调用的简要流程,主要是根据参数要求为申请方找到合适的虚拟内存空间,并创建vma,加入到MapleTree中方便后续管理。在流程最后,调用了f_op->mmap文件自身的mmap函数,执行驱动文件自定义的行为。因此,接下来我们就看看binder驱动拿到内核分配的vma后做了些什么?
// common/drivers/android/binder.c
static int binder_mmap(struct file *filp, struct vm_area_struct *vma) {
// 将binder_open函数中放入的binder_proc取出
struct binder_proc *proc = filp->private_data;
// 检查一下传入的task_struct是不是当前正在执行的进程
if (proc->tsk != current->group_leader)
return -EINVAL;
... ...
// 为vma添加了VM_DONTCOPY和VM_MIXEDMAP标识
// VM_DONTCOPY:fork()时不会复制该vma区域
// VM_MIXEDMAP:该vma区域支持匿名页和文件页的混合映射
vm_flags_mod(vma, VM_DONTCOPY | VM_MIXEDMAP, VM_MAYWRITE);
// 设置vma的操作函数表
vma->vm_ops = &binder_vm_ops;
// 设置vam与binder_proc的关联
vma->vm_private_data = proc;
return binder_alloc_mmap_handler(&proc->alloc, vma);
}
接下来的binder_alloc_mmap_handler函数将着重构建binder_alloc和binder_buffer,这是两个重要的结构体,让我们先看看它的定义。
binder_alloc
// common/drivers/android/binder_alloc.h
// 代表Binder数据缓冲区的描述符
struct binder_alloc {
struct mutex mutex;
struct vm_area_struct *vma; // binder数据缓冲区的虚拟空间描述符
struct mm_struct *mm; // binder进程的内存描述符
void __user *buffer; // binder数据缓冲区虚拟空间起始地址,等于vma->vm_start
struct list_head buffers; // 当前进程所有binder_buffer的链表表头,按地址顺序存放
struct rb_root free_buffers; // 空闲buffer的红黑树
struct rb_root allocated_buffers; // 已分配buffer的红黑树
size_t free_async_space; // 异步通信(oneway)空间大小
struct binder_lru_page *pages; // 物理页的LRU链表
size_t buffer_size; // binder数据缓冲区大小
int pid;
size_t pages_high;
bool oneway_spam_detected; // 是否开启单向调用(Oneway)滥用检测
};
binder_buffer
// common/drivers/android/binder_alloc.h
// 代表一次Binder通信所需的数据缓冲区
struct binder_buffer {
struct list_head entry; // 链表节点,插入到binder_alloc的buffers链表中
// 红黑树节点
// 若是空闲缓冲区,按大小,插入binder_alloc的free_buffers中,
// 若是已分配缓冲区,按地址,插入binder_alloc的allocated_buffers中
struct rb_node rb_node;
unsigned free:1; // 1:空闲,可被分配,0:已分配,正在使用中
unsigned clear_on_free:1; // 释放缓冲区时是否需要清零
unsigned allow_user_free:1; // 是否允许用户空间主动释放该缓冲区
unsigned async_transaction:1; // 该缓冲区是否用于异步事务
unsigned oneway_spam_suspect:1; // 该缓冲区可能涉及单向调用(Oneway)滥用
unsigned debug_id:27; // 调试标识符,用于跟踪缓冲区的分配和释放
struct binder_transaction *transaction; //指向与该缓冲区关联的Binder事务
struct binder_node *target_node; //指向该缓冲区要发送到的目标Binder节点(server端)
size_t data_size; // 缓冲区中有效数据的实际大小
size_t offsets_size; // 缓冲区中偏移表(offsets table)的大小
size_t extra_buffers_size; // 额外缓冲区大小
void __user *user_data; // 指向用户空间的数据地址
int pid; // 该缓冲区所属进程ID
};
来看看函数中是怎么构建这两个结构体的。
// common/drivers/android/binder_alloc.c
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma) {
struct binder_buffer *buffer;
const char *failure_string;
int ret, i;
... ...
// 取vma大小和4M(SZ_4M)中较小的值
// 也就是说内核只允许最大4M的Binder缓冲区
// 但实际情况是ProcessState只会申请BINDER_VM_SIZE(1M - (内存页大小 * 2))
alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start,
SZ_4M);
alloc->buffer = (void __user *)vma->vm_start;
// 为binder_lru_page数组申请空间,数组的每一个元素都是binder_lru_page结构体,对应着一个物理页
// 完成之后pages指向数组第一个元素,且可以通过pages+下标访问数组其他元素
alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE,
sizeof(alloc->pages[0]),
GFP_KERNEL);
... ...
for (i = 0; i < alloc->buffer_size / PAGE_SIZE; i++) {
alloc->pages[i].alloc = alloc;
// 初始化lru为一个空节点,待后续使用
INIT_LIST_HEAD(&alloc->pages[i].lru);
}
// 申请一个binder_buffer的空间
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
... ...
// 将缓冲区用户空间起始地址赋给binder_buffer
buffer->user_data = alloc->buffer;
// 将binder_buffer添加到binder_alloc->buffers链表中
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1;
// 将binder_buffer添加到binder_alloc->free_buffers红黑树中
binder_insert_free_buffer(alloc, buffer);
// 将异步通信空间设置为整个缓冲区的一半
alloc->free_async_space = alloc->buffer_size / 2;
// 完成binder_alloc,且已有一个binder_buffer待使用
binder_alloc_set_vma(alloc, vma);
return 0;
}
综上,从用户空间调用mmap开始,就开启了Binder初始化数据缓冲区的流程,它大致分为两段:标准流程段和驱动流程段。
- 标准流程段:通过current->mm中的信息初始化一个用户虚拟内存空间vma。
- 驱动流程段:通过在file_operations中注册的操作函数mmap执行Binder驱动自身的逻辑,即创建binder_alloc和binder_buffer。
值得注意的是,直到binder_mmap结束我们仍未看到实际的物理内存与任何结构建立关联,即便是binder_alloc中的pages字段,也仅仅是创建了代表物理页的结构体,并没有实际物理页与之对应。这个过程就好像内核给用户空间开了一张支票,在没有兑现之前,只是一个凭据。什么时候兑现?答案是这个缓冲区需要写入数据时。
总结
本篇我们介绍了binder_open和binder_mmap两个驱动函数,要理解它们需要我们具备一定的Linux系统基础知识(进程线程、文件系统、内存管理方面)。总体来说,这两个函数都属于Binder初始化阶段函数。Binder驱动中最重要的核心函数binder_ioctl由于内容较多,我们放到下一篇,敬请期待!
附录
vm_area_struct中vm_flags常见取值及含义
标志 | 值(十六进制) | 含义 |
---|---|---|
VM_READ | 0x00000001 | 可读(对应 PROT_READ ) |
VM_WRITE | 0x00000002 | 可写(对应 PROT_WRITE ) |
VM_EXEC | 0x00000004 | 可执行(对应 PROT_EXEC ) |
VM_SHARED | 0x00000008 | 共享映射 |
VM_PRIVATE | 0x00000010 | 私有映射 |
VM_GROWSDOWN | 0x00000100 | 内存增长方向向下 |
VM_GROWSUP | 0x00000200 | 内存增长方向向上 |
VM_DONTCOPY | 0x00020000 | 禁止fork()时复制该区域 |