【Linux内核大揭秘】程序地址空间
文章目录
- 什么是程序地址空间
- 地址空间的组成
- 虚拟内存技术
- 如何理解程序地址空间
- 页表
- 页表的细节
- 关于堆区
- 在Linux中如何查看各个分段的信息
- 总结
什么是程序地址空间
程序地址空间是一个程序在执行期间可以访问的内存范围。它由操作系统为每个进程分配,以确保进程之间不会相互干扰。地址空间包含了程序所需的所有内存区域,包括代码、已初始化和未初始化的数据、堆(heap)、栈(stack)等。
地址空间的组成
地址空间分为逻辑地址和物理地址两种:
- 逻辑地址:是程序在代码中使用的地址,不直接对应物理内存。每个进程都有独立的逻辑地址空间。
- 物理地址:是真正存储在内存中的位置。
虚拟内存技术
通过虚拟内存技术,操作系统将逻辑地址映射到物理地址。这种技术带来了以下优势:
- 内存隔离:每个进程可以使用相同的逻辑地址空间,而操作系统会隔离各自的实际内存,确保进程之间不会互相影响。
- 安全性和稳定性:这种隔离机制使得进程无法直接访问其他进程的内存,提高了系统的安全性和稳定性。
总结来说,程序地址空间通过虚拟内存和地址映射技术实现了进程的内存隔离,保障了多任务操作系统的安全和可靠性。
如何理解程序地址空间
通过一个现象来了解什么是虚拟内存技术。
下面写一个简单的程序:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int gval=100;
int main()
{printf("我是一个进程:pid:%d,ppid:%d\n",getpid(),getppid());pid_t id = fork();if(id==0){//childwhile(1){printf("我是子进程,pid:%d,ppid:%d,gval:%d,&gval:%p\n",getpid(),getppid(),gval,&gval);gval++;sleep(1);}}else{//parentwhile(1){printf("我是父进程,pid:%d,ppid:%d,gval:%d,&gval:%p\n",getpid(),getppid(),gval,&gval);sleep(1);}}return 0;
}
这个程序很简单,通过父进程创建了一个子进程,然后用一个全局变量来证明虽然父子进程共用一套代码,但是数据是分离开的。
我们分别打印全局变量的数据和全局变量的地址,然后子进程对应的全局变量需要做++操作。
我们来看看运行结果:
可以看见两个全局变量的地址是相同的,但是还是做到了数据独有呢?这是怎么做到的,通过这个现象,我们可以看出gval的地址肯定不是物理内存的地址,如果是物理内存的地址,如果地址相同,那么值也应该相同,所以这里面肯定是用什么结构把真实的物理内存给保护起来了,首先我们来看看我们以前学过的内存的结构。
这是我们以前了解到的内存的划分区域,从上面现象可以推出这不是程序地址空间,而是进程地址空间,操作系统为每个进程绘制了一个虚拟地址内存,让每个进程以为自己独占整个内存,进程彼此之间是不知道的,从而达到了一定程度上的隔离。
实际上所谓的进程虚拟地址空间本质上是一个内核数据结构(内似于PCB)。
这个内核数据结构叫做mm_struct,在PCB中有一个指针指向虚拟地址空间,PCB控制着这个虚拟地址空间,然后mm_struct通过映射,映射到真实的物理内存上。
我们画一个简图来理解这个概念:
如何证明确实在task_struct中有这样一个结构体指针呢,我们来看看Linux内核的原码:
可以看见task_struct内部确实有一个这样的指针,我们来看看mm_struct内部是什么样的:
可以看见在mm_struct中有一些start和end的成员变量,这些就代表各个区域的起始位置和末尾位置,地址也是一个数,所以我们可以用一个unsigned long类型来表示每个区域的起始位置和末尾位置。
虽然我们知道我们取到的地址不是物理内存地址,而是虚拟内存地址,中间是通过一层映射关系来将虚拟内存地址转化为物理内存地址的,那中间到底是怎么做到的,其实在这中间起着关键作用的,有一个内核数据结构叫做页表。
页表
什么是页表:
页表是操作系统内核用来管理虚拟地址和物理地址之间映射的一个数据结构。它的核心作用是支持虚拟内存,使得每个进程可以在自己的独立虚拟地址空间中运行,增强了内存隔离和安全性。
简单了解完页表后,我们来解释一下我们刚刚的现象,为什么父进程的gval不变,子进程的gval在改变,两个gval都指向同一块空间。
首先父进程创建子进程会以自己为模版创建一个PCB,内核会为子进程创建一个新的mm_struct,mm_struct的大部分字段和和父进程共享,页表也会被创建,所以这里物理地址指向的是同一块空间。
但是当我们修改gval的时候,物理内存会发生写实拷贝,父子进程不再共享gval。
子进程的gval对应的虚拟地址对应的页表的映射会改变,改变为写实拷贝过后的地址,这样当修改gval时,gval会修改,但是父进程的gval不会被修改,但是gval的地址都是相同的,是因为这是虚拟地址,子进程是以父进程为模版创建的。
页表的细节
关于页表,其实页表不存储物理地址和虚拟地址。
当中还存在权限的管理和标记位等等属性,这个权限管理指的是读写权限,就比如我们在C语言中遇到的下图:
这个都知道会崩溃,但是为什么会崩溃,其实是因为str对应的权限只有读,没有写的权限,所以会直接崩溃,这时系统层面上的错误,不是语法层面上的错误,所以语法是不会报错的。
有效位表示看目标数据是否存在于内存当中,如果该位为 0,意味着页面不在物理内存,访问该页面会触发缺页中断,操作系统会加载页面或进行错误处理。
页表其实还有很多属性,这里只陈述这两个属性。
关于堆区
我们new出来的空间是否是物理内存?—答案很显然不是的。
我们new或者malloc出来的空间也是虚拟内存,有一个问题就来了,结构体就那么大,但是堆区是动态的,那他是如何实现动态开辟的呢,刚刚我们提到了mm_struct有一段区域是存储begin和end的,我们只需要改变end和begin的数字,即可控制虚拟内存。
在Linux中如何查看各个分段的信息
readelf -S 文件名
总结
通过本篇文章,我们了解了 Linux 程序地址空间的基本结构和分布,包括代码段、数据段、堆、栈以及内核空间的划分。掌握程序地址空间的布局不仅能帮助我们理解进程的内存使用,还能为调试、性能优化和内存管理打下坚实基础。理解 mm_struct
、页表以及写时复制等机制,也为深入探索操作系统内核的内存管理提供了关键的思路。希望这些内容能让你在实际开发和学习中更好地应用这些知识,为系统性能和安全性提供支持。