《趣谈Linux》总结四:内存管理

本文深入探讨Linux内存管理,包括进程内存空间规划、虚拟地址与物理地址映射、内核空间布局以及物理内存的组织形式。详细介绍了虚拟地址的分段和分页机制,以及物理内存的分配策略如伙伴系统和slub分配器。内容涵盖用户态和内核态的内存映射、缺页异常处理和页面换出等核心概念。
摘要由CSDN通过智能技术生成

16 内存管理(上)-规划进程内存空间布局

每个进程应该有自己的内存空间。
内存空间都是独立的、相互隔离的。
对于每个进程来讲,看起来应该都是独占的。

16.1 独享内存空间的原理

内存都被分成一块一块儿的,都编好了号,这些一块一块的地址是实实在在的地址,通过这个地址我们就能够定位到物理内存的位置;
如果所有进程都使用这些地址,同时发生同一个位置的写操作时很容易起冲突

所以使用虚拟地址:物理地址对于进程不可见,谁也不能直接访问这个物理地址。
操作系统会给进程分配一个虚拟地址。
所有进程看到的这个地址都是一样的,里面的内存都是从0开始编号。
在程序里面,指令写入的地址是虚拟地址。
操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行
的时候,写入的是不同的物理地址,这样就不会冲突了。

16.2 规划虚拟地址空间

通过16.1的原理可以看出,操作系统的内存管理主要分为三个方面:
第一,物理内存的管理
第二,虚拟地址的管理
第三,虚拟地址和物理地址如何映射

用户态的进程使用虚拟地址,内核态的也基本都是使用虚拟地址,只有内存管理系统才能使用物理地址;

16.2.1 用户态(普通进程)的角度

  • 需求:

代码需要放在内存里面;
全局变量,例如max_length;
常量字符串"Input the string length : ";
函数栈,例如局部变量num是作为参数传给generate函数的,这里面涉及了函数调用,局部变量,函数参
数等都是保存在函数栈上面的;
堆,malloc分配的内存在堆里面;
这里面涉及对glibc的调用,所以glibc的代码是以so文件的形式存在的,也需要放在内存里面。

  • 实现:通过分段

(0号到29号会议室)
在这里插入图片描述
Text Segment是存放二进制可执行代码的位置,Data Segment存放静态常量,BSS Segment存放未初始化的静态变量;
在二进制执行文件里面,就有这三个部分,这里就是把二进制执行文件的三个部分加载到内存里面。

接下来是堆(Heap)段。堆是往高地址增长的,是用来动态分配内存的区域,malloc就是在这里面分配
的。

接下来的区域是Memory Mapping Segment。这块地址可以用来把文件映射进内存用的,如果二进制的执
行文件依赖于某个动态链接库,就是在这个区域里面将so文件映射到了内存中。

再下面就是栈(Stack)地址段。主线程的函数调用的函数栈就是用这里的。

如果普通进程还想进一步访问内核空间,是没办法的。
如果需要进行更高权限的工作,就需要调用系统调用,进入内核。

16.2.2 内核的角度

  • 需求:

内核的代码要在内存里面;
内核中也有全局变量;
每个进程都要有一个task_struct;
每个进程还有一个内核栈;
在内核里面也有动态分配的内存;
虚拟地址到物理地址的映射表放在哪里?

  • 实现:

一旦进入了内核,就换了一副视角。

刚才是普通进程的视角,觉着整个空间是它独占的,没有其他进程存在。
当然另一个进程也这样认为,因为它们互相看不到对方。
这也就是说,不同进程在相同地址上放的东西都不一样。

但是到了内核里面,无论是从哪个进程进来的,看到的都是同一个内核空间,看到的都是同一个进程列表。
虽然内核栈是各用个的,但是如果想知道的话,还是能够知道每个进程的内核栈在哪里的。
所以,如果要访问一些公共的数据结构,需要进行锁保护
也就是说,不同的进程进入到内核后,进入的内存区域是相同的(30号到39号会议室是同一批会议室。)
在这里插入图片描述
内核的代码访问内核的数据结构,大部分的情况下都是使用虚拟地址的,虽然内核代码权限很大,但是能够
使用的虚拟地址范围也只能在内核空间,也即内核代码访问内核数据结构。
只能用30号到39号这些编号,不能用0到29号,因为这些是被进程空间占用的。
而且,进程有很多个。你现在在内核,但是你不知道当前指的0号是哪个进程的0号。
在内核里面也会有内核的代码,同样有Text Segment、Data Segment和BSS Segment:内核启动的时候,内核代码也是ELF格式的。

17 内存管理(下):物理地址和虚拟地址的映射

可以使用分段机制:
在这里插入图片描述
分段机制下的虚拟地址由两部分组成:段选择子和段内偏移量。

段选择子保存在段寄存器里面。
段选择子里面最重要的是段号,用作段表的索引。
段表里面保存的是这个段的基地址、段的界限和特权等级等。

虚拟地址中的段内偏移量应该位于0和段界限之间。如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

案例:将每个进程的虚拟空间分成以下4个段,用0~3来编号(如图左边)。
每个段在段表中有一个项(如图中间),在物理空间中,段的排列如图右边所示:
在这里插入图片描述
如果要访问段2中偏移量600的虚拟地址,我们可以计算出物理地址为,段2基地址2000 + 偏移量600 =2600。

在Linux里面,段表全称段描述符表(segment descriptors),放在全局描述符表GDT(Global Descriptor
Table)里面,段描述符表里面存储着一个个的段表项

一个段表项由段基地址base、段界限limit,还有一些标识符组成;
对于64位的和32位的系统,都定义了内核代码段、内核数据段、用户代码段和用户数据段。

另外,还会定义四个段选择子,指向段描述符表项

在Linux中,所有的段的起始地址都是一样的,都是0,并没有分段,这可以看出并没有使用到全部的分段功能;
而分段的作用,则是可以做权限审核,例如用户态DPL是3,内核态DPL是0。当用户态试图访问内核态的时候,会因为权限不足而报错。

Linux倾向于另外一种从虚拟地址到物理地址的转换方式,称为分页(Paging)。

对于物理内存,操作系统把它分成一块一块大小相同的页,这样更方便管理;
例如有的内存页面长时间不用了,可以暂时写到硬盘上,称为换出
一旦需要的时候,再加载进来,叫作换入
这样可以扩大可用物理内存的大小,提高物理内存的利用率。

换入和换出都是以页为单位的;
页面的大小一般为4KB;
为了能够定位和访问每个页,需要有个页表,保存每个页的起始地址,再加上在页内的偏移量,组成线性地址,就能对于内存中的每个位置进行访问了:
在这里插入图片描述
虚拟地址分为两部分:页号和页内偏移。
页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。

案例:虚拟内存中的页通过页表映射为了物理内存中的页
在这里插入图片描述
32位环境下,虚拟地址空间共4GB。(4194304kb)(kb/kb=b)
如果分成4KB一个页,那就是1M个页。(4096kb)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值