1. 前言

谈到Linux进程虚拟地址空间,还要从程序说起。本文通过分析程序的编译执行过程,分享了Linux进程虚拟地址空间的结构、组织和创建,并通过分析Linux内核源代码,总结了进程、进程虚拟地址空间和虚存区的关系。

2. 程序编译运行过程

我们编写好一个C程序后,经过预处理、编译、汇编、链接后,生成Linux中的可执行文件ELF文件。

3. 进程虚拟地址空间的结构

当我们运行可执行文件时,父进程通过fork系统调用创建子进程,可以理解为进程就是运行中的程序。在32位Linux系统中,每个进程都有4GB的虚拟地址空间,其中0-3GB是用户空间,3-4GB是内核空间。每个进程都以为自己独占整个4GB的地址空间,但实际上1GB的内核空间是所有进程共享的,独占的3GB用户空间也只是虚拟的。

那么进程虚拟地址空间结构是什么样子呢? 虚拟地址空间包含很多虚存区,它的结构和功能如下图所示。

内核中使用vm_area_struct描述进程虚拟地址空间中的虚存区,4.19内核中定义如下(include/linux/mm_types.h):

struct vm_area_struct {/* The first cache line has the info for VMA tree walking. */unsigned long vm_start;      /* Our start address within vm_mm. */unsigned long vm_end;      /* The first byte after our end addresswithin vm_mm. *//* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next, *vm_prev;struct rb_node vm_rb;......struct mm_struct *vm_mm;    /* The address space we belong to. */pgprot_t vm_page_prot;     /* Access permissions of this VMA. */unsigned long vm_flags;        /* Flags, see mm.h. */....../* Function pointers to deal with this struct. */const struct vm_operations_struct *vm_ops;/* Information about our backing store: */unsigned long vm_pgoff;        /* Offset (within vm_file) in PAGE_SIZEunits */struct file * vm_file;       /* File we map to (can be NULL). */void * vm_private_data;      /* was vm_pte (shared mem) */......} __randomize_layout;

关键信息:

  • vm_startvm_end指定了该虚存区在虚拟地址空间中的起始和结束地址。
  • 进程所有的vm_area_struct描述的虚存区由红黑树和双向链表来组织,红黑树的根节点位于mm_rb,双向链表开始于mm_struct->mmap(下文会介绍mm_struct),由vm_nextvm_prev分别指向双向链表的后继和前驱。

问:使用双向链表来组织进程的虚存区就好了,为什么还要用到红黑树呢?
答:内核对虚存区进行操作时,需要找到特定地址关联的区域,而扫描链表是一种很低效的操作,尤其对于数据密集型应用(存在大量虚存区)。红黑树是一种二叉排序树,其节点标记为红色或黑色,节点的红黑标记可以简化重新平衡树的过程,它具有普通查找树的所有性质,添加、查找和删除都可以在O(logn)时间内完成。因此,进程的虚存区还使用红黑树来组织,可以明显加快虚存区的扫描速度。

  • vm_mm是一个反向指针,指向该虚存区所属的mm_struct
  • vm_page_prot存储了该区域的访问权限。
  • vm_flags是描述该区域的一组标志。
  • vm_ops是一个指向很多方法集合的指针,这些方法用于在虚存区上执行各种操作,主要包括创建、删除等。
  • vm_file指向了一个被映射的文件。

进程的虚拟地址空间中包含了上述的虚存区,那么每个进程的虚拟地址空间又是如何管理的呢? 内核中使用mm_struct结构管理进程的虚拟地址空间,4.19内核中定义如下(include/linux/mm_types.h):

struct mm_struct {struct {struct vm_area_struct *mmap;       /* list of VMAs */struct rb_root mm_rb;u64 vmacache_seqnum;                   /* per-thread vmacache */......unsigned long task_size;   /* size of task vm space */unsigned long highest_vm_end;    /* highest vma end address */pgd_t * pgd;atomic_t mm_users;atomic_t mm_count;int map_count;         /* number of VMAs */......spinlock_t arg_lock; /* protect the below fields */unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;......} __randomize_layout;/** The mm_cpumask needs to be at the end of mm_struct, because it* is dynamically sized based on nr_cpu_ids.*/unsigned long cpu_bitmap[];
};

关键信息:

  • mmap指向了进程虚拟地址空间中虚存区双链表开始的节点。
  • mm_rb指向了虚存区红黑树的根节点。
  • task_size描述了进程虚拟地址空间长度。
  • pgd指向了页全局目录的初始地址。
  • start_codeend_code分别保存了可执行代码在虚存区的开始和结束地址, start_dataend_data分别保存了已初始化数据在虚存区的开始和结束地址。
  • start_brkbrk分别保存了堆区域的开始和结束地址, start_stack保存了栈的开始地址。
  • arg_startarg_end分别保存了参数列表的开始和结束地址, env_startenv_end分别保存了环境变量的开始和结束地址。

4. 进程虚拟地址空间的创建

那么进程的虚拟地址空间是怎么创建的呢? Linux通过fork()创建新进程后,使用wait()等待子进程完成。子进程通过exec系统调用执行load_elf_binary()装载ELF二进制文件,创建新进程的地址空间。

创建进程虚拟地址空间时,首先,需要设置PF_RANDOMIZE,如果全局变量randomize_va_space设置为1,则启用地址空间随机化机制。该设置会降低计算机的速度,但是可以提高系统的安全性,因为该设置使得内核不会为栈和内核映射区选择固定的位置,每次新进程启动时改变这些值的设置,从而攻击者无法依靠固定的地址找到栈。

然后,内核使用arch_pick_mmap_layout()选择进程虚拟地址空间的布局。其布局常见的有两种,分别是经典布局和新布局。经典布局适用于计算机提供了巨大的虚拟地址空间,这取决于操作系统的位数。经典布局如下图所示,栈起始于STACK_TOP,内存映射区起始于mm_struct并向上扩展。

经典布局的问题在于堆的使用空间很小,新布局使用固定值限制栈的最大长度,内存映射区从栈的末端向下扩展。

4.19内核中选择布局的函数如下(arch/x86/mm/mmap.c):

void arch_pick_mmap_layout(struct mm_struct *mm, struct rlimit *rlim_stack)
{if (mmap_is_legacy())mm->get_unmapped_area = arch_get_unmapped_area;elsemm->get_unmapped_area = arch_get_unmapped_area_topdown;arch_pick_mmap_base(&mm->mmap_base, &mm->mmap_legacy_base,arch_rnd(mmap64_rnd_bits), task_size_64bit(0),rlim_stack);#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES/** The mmap syscall mapping base decision depends solely on the* syscall type (64-bit or compat). This applies for 64bit* applications and 32bit applications. The 64bit syscall uses* mmap_base, the compat syscall uses mmap_compat_base.*/arch_pick_mmap_base(&mm->mmap_compat_base, &mm->mmap_compat_legacy_base,arch_rnd(mmap32_rnd_bits), task_size_32bit(),rlim_stack);
#endif
}

可以看到如果配置是legacy模式,则内核通过arch_get_unmapped_area函数设置经典布局,否则内核通过arch_get_unmapped_area_topdown设置内存映射自顶向下的新布局。arch_pick_mmap_base()函数选择内存映射的基地址。

最后,load_elf_binary函数调用setup_arg_pages在适当的位置创建栈。

5. 进程、虚拟地址空间和虚存区的关系

进程的虚拟地址空间的创建介绍完了,那么进程、进程的虚拟地址空间、虚存区它们是怎么联系起来的呢。通过分析内核源代码,姑且画出此图。

6. 结语

到此,我们了解了进程虚拟地址空间。那么,物理地址与虚拟地址它们有什么联系呢?Linux中物理地址是怎么管理和分配呢?下期文章继续分享,谢谢阅读。

参考资料:
《深入Linux内核架构》
《Linux内核设计与实现》
https://www.w3xue.com/exp/article/201811/10027.html
https://maodanp.github.io/2019/06/02/linux-virtual-space/
https://www.wulnut.top/2020/10/16/virtual_address/
https://www.cxyzjd.com/article/weixin_39832628/110994300
https://blog.csdn.net/gyx_2110/article/details/71329991

Linux进程虚拟地址空间相关推荐

  1. Linux虚拟内存和进程虚拟地址空间简述

    后台开发经常会问此类问题,虽说难度不大,但是知道和不知道还是有区别的.以下的内容总结自<深入理解Linux内核>第一章,仅仅是简述,没有深入研究,毕竟内存管理这一块内容超级多,感兴趣的同学 ...

  2. Linux内核虚拟地址空间,-3G的由来。各个进程的虚拟内存4G,内核总在3-4G。内核的虚拟空间地址-3G,总是指向物理内存的0-1G地址,各个进程的虚拟内核共享这个物理内存

    Linux内核地址空间划分 通常 32 位 Linux 内核地址空间划分 0~3G 为用户空间,3~4G 为内核空间.64 位内核地址空间划分是不同的. Linux内核高端内存 当内核模块代码或线程访 ...

  3. 【Linux】进程虚拟地址空间

    进程虚拟地址空间打破了我一直以来对于程序地址空间的认识,它真的好神奇. 我们首先来看一下下面这段代码: 1 #include<stdio.h>2 #include<unistd.h& ...

  4. 14 [虚拟化] 虚存抽象;Linux进程的地址空间

    14 [虚拟化] 虚存抽象:Linux进程的地址空间 南京大学操作系统课蒋炎岩老师网络课程笔记. 视频:https://www.bilibili.com/video/BV1N741177F5?p=14 ...

  5. 2.4父子进程虚拟地址空间情况

    内核区中,父进程和子进程的pid是不同的. 定义的局部变量pid在栈空间中,父子进程中栈空间中的pid不同,在父进程中为子进程的进程号,在子进程中为0 实际上,更准确来说,linux的fork()是通 ...

  6. 一篇长文叙述Linux内核虚拟地址空间的基本概括

    x86-32位虚拟地址空间 就我们所知,Linux内核一般将处理器的虚拟地址空间划分为两个部分.底部比较大的部分用于用户进程,顶部则专用于内核.虽然(在两个用户进程之间的)上下文切换期间会改变下半部分 ...

  7. 【读书笔记】【程序员的自我修养 -- 链接、装载与库(二)】进程虚拟地址空间、装载与动态链接、GOT、全局符号表、共享库的组织、DLL、C++与动态链接

    文章目录 前言 介绍 可执行文件的装载与进程 进程虚拟地址空间 装载方式 操作系统对可执行文件的装载 进程虚存空间分布 ELF文件的链接视图和执行视图 堆和栈 Linux 内核装载ELF & ...

  8. 父子进程虚拟地址空间情况

    父子进程虚拟地址空间情况 笔记来源于牛客网<Linux多进程开发> The child process and the parent process run in separate mem ...

  9. 【Linux】虚拟地址空间

    文章目录 1. 物理地址和虚拟地址定义 2. 虚拟地址空间分布 2.1 内核空间 2.2 栈(stack) 2.3 堆(heap) 2.4 BSS段 2.5 数据段(Data) 2.6 代码段(tex ...

最新文章

  1. flask中的request
  2. 【Verilog HDL】设计硬件电路时,如何避免生成锁存器?
  3. 概率模型分子动力学模拟五元环吡咯C4H5N
  4. 【完结】12大深度学习开源框架(caffe,tf,pytorch,mxnet等)快速入门项目
  5. 博客美化20150418
  6. android sha1是签名么,Android获取SHA1和MD5签名
  7. VTK:图片之ImageConvolve
  8. python web为什么不火_如何用纯 Python 写交互式 Web 应用?
  9. const constexpr C++ 解释
  10. Java笔记13-兄弟连在线考试系统
  11. 用Promise实现队列(爬一爬慕课网HTML代码)
  12. 【语音识别】之梅尔频率倒谱系数(mfcc)及Python实现
  13. 【CometOJ】CometOJ#8 解题报告
  14. 规范-阿里代码规范设置
  15. html实现手机截屏,iPhone手机如何实现网页长截图?
  16. 腾讯云轻量服务器和虚拟主机VPS有什么区别?
  17. Android 折叠屏适配最全的攻略在这里
  18. React 高阶组件(HOC)
  19. 软考高级 真题 2012年下半年 信息系统项目管理师 案例分析
  20. websocket中自动断开连接

热门文章

  1. java 读写文件[多种方法]
  2. 《网管员必读——网络测试、监控和实验》试读样章下载
  3. PIL 学习笔记(3)
  4. No Maybe Please
  5. 笔记react router 4(三)
  6. Setting Up YARN High Availability
  7. opengl加载多个3ds模型失败记
  8. 云计算&大数据 “下一幕”智能变革之力
  9. 模拟实现智能指针auto_ptr,scoped_ptr,shared_ptr
  10. python 的日志logging模块学习