Linux -- 进程地址空间
目录
一 、程序地址空间
1、虚拟地址
2、进程地址空间
2.1 理解
2.2 区域划分
2.3 为什么要有虚拟地址空间
2.4 虚拟内存管理
一 、程序地址空间
# 相信大家对这幅图并不陌生了,这是我们的常说的内存布局分布图:
# 其中堆栈相对而生,栈向下生长(在栈上的变量先定义的地址更大),堆向上生长(在堆上的变量先定义的地址更小)。
# 我们可以通过以下代码来验证:
#include
#include
#include
int g_unval;
int g_val = 100;
int main(int argc, char* argv[], char* env[])
{
const char* str = "helloworld";
printf("code addr: %p
", main);
printf("init global addr: %p
", &g_val);
printf("uninit global addr: %p
", &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 addr: %p
", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p
", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p
", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p
", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p
", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p
", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p
", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p
", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p
", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p
", str);
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %p
", i, argv[i]);
}
for (int i = 0; env[i]; i++)
{
printf("env[%d]: %p
", i, env[i]);
}
return 0;
}
# 输出结果:
code addr: 0x559ecd086189
init global addr: 0x559ecd089010
uninit global addr: 0x559ecd08901c
heap addr: 0x559ecdaba6b0
heap addr: 0x559ecdaba6d0
heap addr: 0x559ecdaba6f0
heap addr: 0x559ecdaba710
test static addr: 0x559ecd089014
stack addr: 0x7ffeac370a10
stack addr: 0x7ffeac370a18
stack addr: 0x7ffeac370a20
stack addr: 0x7ffeac370a28
read only string addr: 0x559ecd087004
argv[0]: 0x7ffeac3726fe
env[0]: 0x7ffeac372705
env[1]: 0x7ffeac372715
env[2]: 0x7ffeac372723
env[3]: 0x7ffeac37273d
env[4]: 0x7ffeac372770
env[5]: 0x7ffeac37277c
env[6]: 0x7ffeac372791
env[7]: 0x7ffeac3727a0
env[8]: 0x7ffeac3727ab
env[9]: 0x7ffeac3727bc
env[10]: 0x7ffeac372dab
env[11]: 0x7ffeac372ddf
env[12]: 0x7ffeac372e01
env[13]: 0x7ffeac372e18
env[14]: 0x7ffeac372e23
env[15]: 0x7ffeac372e43
env[16]: 0x7ffeac372e4c
env[17]: 0x7ffeac372e63
env[18]: 0x7ffeac372e6b
env[19]: 0x7ffeac372e7e
env[20]: 0x7ffeac372e9d
env[21]: 0x7ffeac372ebf
env[22]: 0x7ffeac372f00
env[23]: 0x7ffeac372f68
env[24]: 0x7ffeac372f9e
env[25]: 0x7ffeac372fb2
env[26]: 0x7ffeac372fc5
env[27]: 0x7ffeac372fe8
1、虚拟地址
# 让我们再来看看这一段代码:
#include
#include
int gval = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("父:gval: %d, &gval: %p, pid: %d, ppid: %d
", gval, &gval, getpid(), getppid());
sleep(1);
gval++;
}
}
else
{
while(1)
{
printf("子:gval: %d, &gval: %p, pid: %d, ppid: %d
", gval, &gval, getpid(), getppid());
sleep(1);
}
}
return 0;
}
# 这里我们让子进程修改gval,父进程不修改,而因为写实拷贝,进程具有独立性,所以父进程的值不变,子进程的一直在修改,但是我们发现一个很奇怪的事情:父进程和子进程访问的gval地址是一样的,但是父进程和子进程看到gval的值却不同。
# 基于事实我们可以断定:这个地址不是物理内存的地址!否则同一个的地址的值那他一定是相同的!
# 那这个地址是什么呢?这个叫做虚拟地址,但是我们可以得出一个结论:平时我们在语言层面的地址全部都是虚拟地址!
2、进程地址空间
- 结论:一个进程一个虚拟地址空间!每个进程的task_struct对应一个虚拟地址空间!
- 结论:虚拟地址
2.1 理解
# 32位机器:2^32个地址,64为机器:2^64个地址,虚拟地址空间从低到高存在从全0到全F的编址,32位机器地址有2^32种组合,就有2^32个地址,每个地址对应1字节地址空间大小,所以32位机器就有4GB内存空间,64位以此类推,其中高地址的1GB是内核空间,剩下3GB是用户空间。
# 我们之前将那张布局图称为程序地址空间实际上是不准确的,那张布局图实际上应该叫做进程地址空间,进程地址空间本质是内存中的一种内核数据结构,在Linux
当中进程地址空间具体由结构体mm_struct
实现,其一般包含以下这些信息:
struct mm_struct
{
//代码区
unsigned int code_start;
unsigned int code_end;
//已初始化全局数据区
unsigned int init_data_start;
unsigned int init_data_end;
//未初始化全局数据区
unsigned int uninit_data_start;
unsigned int uninit_data_end;
//....栈区
unsigned int stack_start;
unsigned int stack_end;
// ...
};
# 在结构体mm_struct
当中,每一个的区域都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址大小一般为4G
,是由0x00000000
到0xffffffff
线性增长的,所以虚拟地址又叫做线性地址。
# 每个进程被创建时,其对应的进程控制块task_struct
和进程地址空间mm_struct
也随之被创建。而操作系统就可以通过进程的task_struct
找到对应的mm_struct
(因为task_struct
有一个结构体指针指向的是mm_struct
)。
# 然后我们就可以更加深入解释上面地址相同,值却不同的现象:首先父进程有自己的task_struct和mm_struct,该父进程创建的子进程也会有属于其自己的task_struct和mm_struct,父子进程的进程地址空间当中的各个虚拟地址分别通过页表映射到对应的物理内存,如下图:
# 我们代码进程存在父进程和子进程,我们又说一个进程有一个task_struct,一个虚拟地址空间有一张页表。而我们又说子进程的很多东西都是拷贝父进程的,部分自己的修改一下,所以子进程也要有task_struct、虚拟地址空间、页表,并且虚拟地址空间和页表也都是拷贝父进程的。既然是拷贝,所以子进程的虚拟地址空间里面也有一个val,而且页表的虚拟地址和物理地址都是一模一样,所以就发生浅拷贝,这就是为什么我们父子打印的地址相同。为什么我们的全局变量默认被父子进程共享?因为他们他的页表、映射关系是一样的,指向同一个物理内存。所以代码也有映射关系,也是浅拷贝,所以父子进程默认共享代码和数据。
# 相信大家看完上面的内容还是不太清楚什么是进程地址空间,下面我们通过一个形象的例子理解一下什么是进程地址空间:
2.2 区域划分
2.3 为什么要有虚拟地址空间
# 作用:
1、
2、
3、
2.4 虚拟内存管理
# 描述linux下进程的地址空间的所有的信息的结构体是mm_struct
(内存描述符)。每个进程只有一个mm_struct结构,在每个进程的task_struct结构中,有一个指向该进程的结构。
struct task_struct
{
/*...*/
struct mm_struct* mm; //对于普通的⽤⼾进程来说该字段指向他的虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL
struct mm_struct* active_mm; // 该字段是内核线程使⽤的。当该进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
/*...*/
}
# 可以说,mm_struct结构是对整个用户空间的描述。每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间才能互不干扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:
# 定位mm_struct文件所在位置和task_struct所在路径是一样的,不过他们所在文件是不一样的,mm_struct所在的文件是mm_types.h。
struct mm_struct
{
/*...*/
struct vm_area_struct* mmap;
struct rb_root mm_rb;
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指向这棵树。
# 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;
}
# 所以我们可以对上图在进行更细致的描述,如下图所示: