详细介绍:Linux -程序地址空间
基本概念
程序地址空间是操作系统为每个进程分配的一块连续的虚拟内存区域。它包含了进程运行所需的所有代码、数据和堆栈等信息。程序地址空间的设计目的是为了提供一个隔离的环境,使得每个进程都可以独立运行而不会相互干扰。
如下图为程序地址空间的大概结构示意图:
示例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
// 未初始化全局变量,位于 BSS 段(未初始化数据区)
int g_unval;
// 已初始化全局变量,位于数据段(初始化数据区)
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
// 只读字符串常量,位于代码段(正文代码区,且为只读部分)
const char *str = "helloworld";
// main 函数是代码,位于代码段(正文代码区)
printf("code addr: %p\n", main);
// g_val 是已初始化全局变量,位于数据段(初始化数据区)
printf("init global addr: %p\n", &g_val);
// g_unval 是未初始化全局变量,位于 BSS 段(未初始化数据区)
printf("uninit global addr: %p\n", &g_unval);
// 静态局部变量,位于数据段(初始化数据区,因为显式初始化了)
static int test = 10;
// 动态分配的堆内存,位于堆区
char *heap_mem = (char *)malloc(10);
char *heap_mem1 = (char *)malloc(10);
char *heap_mem2 = (char *)malloc(10);
char *heap_mem3 = (char *)malloc(10);
// 打印堆内存地址,这些地址应位于堆区,且堆内存地址通常是逐渐升高分配的
printf("heap addr0: %p\n", heap_mem);
printf("heap addr1: %p\n", heap_mem1);
printf("heap addr2: %p\n", heap_mem2);
printf("heap addr3: %p\n", heap_mem3);
// test 是静态局部变量,位于数据段(初始化数据区)
printf("test static addr: %p\n", &test);
// heap_mem 等指针变量本身是局部变量,位于栈区
printf("stack addr0: %p\n", &heap_mem);
printf("stack addr1: %p\n", &heap_mem1);
printf("stack addr2: %p\n", &heap_mem2);
printf("stack addr3: %p\n", &heap_mem3);
// str 指向的字符串常量位于代码段(正文代码区的只读部分)
printf("read only string addr: %p\n", str);
// 命令行参数 argv 相关内容,位于命令行参数和环境变量区
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
// 环境变量 env 相关内容,位于命令行参数和环境变量区
for (int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}

地址从低到高依次为:
read only string addr: 0x5593646c0004
code addr: 0x5593646bf189
init global addr: 0x5593646c2010
test static addr: 0x5593646c2014
uninit global addr: 0x5593646c201c
heap addr0: 0x55936d6626b0
heap addr1: 0x55936d6626d0
heap addr2: 0x55936d6626f0
heap addr3: 0x55936d662710
stack addr0: 0x7ffd04139200
stack addr1: 0x7ffd04139208
stack addr2: 0x7ffd04139210
stack addr3: 0x7ffd04139218
argv[0]: 0x7ffd0413a56a
env[0]: 0x7ffd0413a578
env[1]: 0x7ffd0413a588
env[2]: 0x7ffd0413a59c
env[3]: 0x7ffd0413a5b9
env[4]: 0x7ffd0413a5c7
env[5]: 0x7ffd0413a5dc
env[6]: 0x7ffd0413a5f2
env[7]: 0x7ffd0413a612
env[8]: 0x7ffd0413a61e
env[9]: 0x7ffd0413a633
env[10]: 0x7ffd0413a642
env[11]: 0x7ffd0413a651
env[12]: 0x7ffd0413a662
env[13]: 0x7ffd0413ac44
env[14]: 0x7ffd0413ac64
env[15]: 0x7ffd0413ac8d
env[16]: 0x7ffd0413acbd
env[17]: 0x7ffd0413ace0
env[18]: 0x7ffd0413ad02
env[19]: 0x7ffd0413ad19
env[20]: 0x7ffd0413ad2d
env[21]: 0x7ffd0413ad4d
env[22]: 0x7ffd0413ad56
env[23]: 0x7ffd0413ad5e
env[24]: 0x7ffd0413ad73
env[25]: 0x7ffd0413ad92
env[26]: 0x7ffd0413adc7
env[27]: 0x7ffd0413adea
env[28]: 0x7ffd0413ae65
env[29]: 0x7ffd0413af38
env[30]: 0x7ffd0413af6e
env[31]: 0x7ffd0413af82
env[32]: 0x7ffd0413afda

由此可见,程序地址空间从低地址到高地址依次为:只读字符串常量、代码段、数据段(包括已初始化和未初始化的全局变量)、堆区、栈区、命令行参数和环境变量区。
虚拟地址
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
else if (id == 0)
{
// child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再读取
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ // parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

从上图可以看出,父进程和子进程的 g_val 变量地址是一样的,但它们的值是不同的。说明该地址不是物理地址。在Linux上,这种地址称为虚拟地址。而物理地址,⽤⼾是看不到的,由OS统⼀管理。OS负责将虚拟地址转化成物理地址。
进程地址空间
所以,程序地址空间应该称之为进程地址空间,因为每个进程都有自己独立的地址空间。
如上图,同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址上。
所以,进程地址空间是每个进程独立的虚拟内存区域,包含了进程运行所需的代码、数据和堆栈等信息。操作系统通过虚拟内存管理机制,将每个进程的虚拟地址空间映射到物理内存,从而实现进程间的隔离和保护。
虚拟内存管理
描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的 task_struct 结构中,有⼀个指向该进程的mm_struct结构体指针。
struct task_struct
{
/*...*/
//对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,
// 对于内核线程来说这部分为NULL。
struct mm_struct *mm;
// 该字段是内核线程使⽤的。当该进程是内核线程时,
// 它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,
// 这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
struct mm_struct *active_mm;
/*...*/
}
可以说, mm_struct 结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的 mm_struct ,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由 task_struct 到mm_struct ,进程的地址空间的分布情况:
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
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;
/*...*/
}
每⼀个进程都会有⾃⼰独⽴的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct
组织起来的,虚拟空间的组织⽅式有两种:
- 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
- 当虚拟区较多时,采取红黑树,由mm_rb指向这个红黑树。
每⼀个虚拟区间由 vm_area_struct 结构体描述:linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA,⽅便进程快速访问。
struct vm_area_struct
{
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct
{
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

为什么要有虚拟地址空间
虚拟地址空间的引入主要是为了实现以下几个目的:
- 内存隔离:每个进程都有自己独立的虚拟地址空间,防止进程之间相互干扰和破坏,提高系统的稳定性和安全性。
- 内存管理:虚拟地址空间使得操作系统可以更灵活地管理内存资源,包括内存分配、回收和共享等。
- 地址扩展:通过虚拟地址空间,进程可以使用比实际物理内存更大的地址空间,解决了物理内存不足的问题。
- 简化编程模型:程序员可以使用连续的内存地址进行编程,而不需要关心实际的物理内存布局,简化了编程复杂度。
- 支持多任务:虚拟地址空间使得操作系统可以同时运行多个进程,每个进程都有自己的独立环境,支持多任务处理。
。
2. 内存管理:虚拟地址空间使得操作系统可以更灵活地管理内存资源,包括内存分配、回收和共享等。
3. 地址扩展:通过虚拟地址空间,进程可以使用比实际物理内存更大的地址空间,解决了物理内存不足的问题。
4. 简化编程模型:程序员可以使用连续的内存地址进行编程,而不需要关心实际的物理内存布局,简化了编程复杂度。
5. 支持多任务:虚拟地址空间使得操作系统可以同时运行多个进程,每个进程都有自己的独立环境,支持多任务处理。
总之,虚拟地址空间是现代操作系统中不可或缺的一个概念,它为进程提供了一个安全、隔离和高效的运行环境。
浙公网安备 33010602011771号