国嵌视频学习第十天——内存管理
Linux内存管理对于立志从事内核开发(驱动、网络协议栈)的工程师来说,熟悉linux的内存管理系统非常重要物理地址:出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果线性地址(虚拟地址):在32位CPU架构下,可以表示4G的地址空间,用16进制表示就是0x00000000到0xffffffff逻辑地址:程序经过编译后,出现在汇编程序中的地址 CPU要
Linux内存管理
对于立志从事内核开发(驱动、网络协议栈)的工程师来说,熟悉linux的内存管理系统非常重要
物理地址:出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果
线性地址(虚拟地址):在32位CPU架构下,可以表示4G的地址空间,用16进制表示就是0x00000000到0xffffffff
逻辑地址:程序经过编译后,出现在汇编程序中的地址
CPU要将一个逻辑地址转换位物理地址,需要两步:首先CPU利用段式内存管理单元,将逻辑地址转换成线性地址,再利用页式内存管理单元,把线性地址最终转换位物理地址。
——段式管理:16位 CPU内部拥有20位的地址线,它的寻址范围就是2的20次方,也就是1M的内存空间。但是16位CPU用于存放地址的寄存器(IP,SP...)只有16位,因此只能访问65536个存储单元,64K
为了能够访问1M的内存空间,CPU就采用了内存分段的管理模式,并在CPU内部加入了段寄存器。16位CPU把1M内存空间分为若干个逻辑段,每个逻辑段的要求如下:
1.逻辑段的起始地址(段地址)必须是16的倍数,即最后4个二进制位必须为0
2.逻辑段的最大容量为64K
物理地址的形成方式:
由于段地址必须是16的倍数,所以值的一般形式为XXXX0H,即前16位二进制位是变化的,后4位是固定的0,鉴于段地址的这种特性,可以只保存前16位二进制位来保存整个段基地址,所以每次使用时要用段寄存器左移补4个0(乘以16)来得到实际的段地址(因为寄存器是16位的,而地址线是20位,既然舍弃了低4位,那么为了得到实际的PA,就需要对其*16)
在确定了某个存储单元所属的段后,只是知道了该存储单元所属的范围(段地址->段地址+65536),如果想确定该内存单元的具体位置,还必须知道该单元在段内的偏移。有了段地址和偏移量,就可以唯一的确定内存单元在存储器中的具体位置。
逻辑地址=段基地址+段内偏移量
由逻辑地址得到物理地址的公式为:
PA=段寄存器的值*16 + 逻辑地址(偏移部分)
段寄存器是为了对内存进行分段管理而增加的,16位CPU有四个段寄存器,程序可同时访问四个不同含义的段。
1).CS+IP:用于代码段的访问,CS指向存放程序的段基址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令
2)SS+SP:用于堆栈段的访问,SS指向堆栈段的基地址,SP指向栈顶,可以通过SS和SP两个寄存器直接访问栈顶单元的内存物理位置
3)DS+BX:用于数据段的访问。DS中的值左移四位得到数据段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址
4)ES+BX:用于附加段的访问。ES中的值左移四位得到附加段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。
32位的pc的内存管理任然采用“分段”的管理模式,逻辑地址同样由段地址和偏移量两部分组成,32位pc的内存管理和16位pc的内存管理有相同之处也有不同之处,因为32位pc采用了两种不同的工作方式:实模式和保护模式
1).实模式:在实模式下,32位cpu的内存管理与16位cpu是一致的
2)保护模式:段基地址长达32位,每个段的最大容量可达4G,段寄存器的值是段地址的“选择器(selector)”,用该“选择器”从内存中得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移量,这与16位cpu的物理地址计算方式完全不同。
——页式管理:从管理和效率的角度出发,线性地址被分为固定长度的组,称为页(page),例如32位的机器,线性地址最大可为4G,如果用4KB为一个页来划分,这样整个线性地址就被划分为2的20次方个页(虚拟的)
另一类“页”,称之为物理页,或者是页框、页桢。分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与线性地址页是相同的。(实在的)
页表项中存放的是物理页的基地址
cr3是CPU中的寄存器,在创建一个进程时,将伴随创建页表、页目录。在做进程切换时会更新cr3寄存器
图中的一个页是4K(从offest是12位即可看出)
这是所谓两级分页模型:页目录、页表
Linux内核的设计并没有全部采用intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了linux内核的涉及,而且为把linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制
所有段的基地址均为0
线性地址=基地址+逻辑地址(偏移量)。此时基地址为0,那么线性地址便是等于逻辑地址
由此可以看出,每个段的逻辑地址空间范围为0-4GB。因为每个段的基地址为0,因此,逻辑地址与线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在linux中所提到的逻辑地址和线性地址(虚拟地址),可以认为是一致的。看来,linux巧妙地把段机制给绕过去了,而完全利用了分页机制
因此,在linux中逻辑地址、虚拟地址、线性地址是一致的
前面介绍了i386的二级页管理架构,不过有些CPU(alpha 64位)使用三级,甚至四级架构,linux 2.6.29内核为每种cpu提供统一的界面,采用了四级页管理架构,来兼容二级、三级、四级管理架构的CPU
这是一个64位的4级分页,这四级分别为:
1.页全局目录(page global directory):即pgd,是多级页表的抽象最高层
2.页上级目录(page upper directory):pud
3.页中间目录(page middle directory):pmd,是页表的中间层
4.页表(page table entry):即pte
进程地址空间
虚拟内存
Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间是大小为3G,用户看懂和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。
Linux将4G的虚拟地址空间划分为两个部分——用户空间与内核空间。用户空间从0到0xbfffffff,内核空间从3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用访问内核空间
用户空间对应进程,所以每当进程切换,用户空间就会跟着变化(页表、页目录等信息要变)
进程空间
每个进程的用户空间都是完全独立、互不相干的、把同一个程序同时运行10次(为了能同时运行,让它们再返回前睡眠100秒),会看到10个进程使用的线性地址(虚拟地址)一模一样(但是它们的物理地址是不一样的)。Ps aux(cat /proc/<pid>/maps,该文件是进程所使用的地址信息)(该实验可以通过将进程唤醒10次而不用fork()函数达到目的)
创建进程fork()、程序载入execve()、动态内存分配malloc()等进程相关操作都需要分配内存给进程。这时进程申请和获得的不是物理地址,仅仅是虚拟地址
实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际页框的程序。该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了物理地址上。
内核内存分配
在应用程序中,常使用malloc函数进行动态内存分配,而在linux内核中,通常使用kmalloc来动态分配内存
Kmalloc原型是:
#include <linux/slab.h>
Void *kmalloc(size_t size, int flags)
参数:size :要分配的内存大小
Flags:分配标志,它控制kmalloc的行为:GFP+ATOMIC:用来在进程上下文之外的代码(包括中断处理)中分配内存,从不睡眠;GFP_KERNEL:进程上下文中的分配,可能睡眠(16M-896M);__GFP_DMA:这个标志要求分配能够DMA的内存区(物理地址在16M以下的页桢);__GFP_HIGHMEM:这个标志表示分配的内存位于高端内存(896M以上)
按页分配,如果模块需要分配大块的内存,那使用面向页的分配技术会更好:
Get_zeroed_page(unsigned int flags):返回指向新页面的指针,并将页面清零
__get_free_page(unsigned int flags):和get_free_page类似,但不清零页面
__get_free_pages(unsigned int flags, unsigned int order):分配若干个连续的页面,返回指向该内存区域的指针,但不清零这段内存区域
释放
当程序用完这些页,可以使用下列函数之一来释放它们(以免内存泄露):
——void free_page(unsigned long addr)
——void free_pages(unsigned long ddr, unsigned long order)
如果释放的和先前分配数目不等的页面,会导致系统错误
有三条路线可以达到物理页(连接入“空闲页框”的三条线)
第一条:用户空间malloc函数分配内存,此时分配的是虚拟地址空间,只有当真正去访问该空间的时候,相应的进程页表会发生请页异常而分配真正的物理地址空间
第二条:内核空间调用kmalloc时,会调用slab管理器。
第三条:内核空间调用vmalloc
Linux内核地址空间
内核空间:内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。
高端内存:物理内存896M以上的部分称之为高端内存
直接内存映射区(Direct Memory Region):从3G开始,最大896M的线性地址区间,我们称作直接内存映射区,这是因为该区域的线性地址和物理地址之间存在线性转换关系:
线性地址=3G + 物理地址
例:物理地址区间0x100000-0x200000映射到线性空间就是3G+0x100000-3G+0x200000
动态内存映射区(Vmalloc region):该区域的地址由内核函数vmalloc来进行分配,其特点是线性空间连续,但对应的物理空间不一定连续。Vmalloc分配的线性地址所对应物理页可能处于低端内存,也可能处于高端内存(用vmlloc分配的地址全在该区域内)
永久内存映射区(PKMap Region):对于896M以上的高端内存,可使用该区域来访问,访问方法:
1.使用alloc_page(__GFP_HIGHMEM)分配高端内存页(物理页).
2..使用kmap函数将分配到的高端内存映射到该区域(虚拟页,因为不能访问物理页,所以需要将alloc_page分配的物理页映射!)
使用KMAP的4M这么个窗口,可以访问896M以上所有物理地址
固定映射区(Fixing Mapping Region):PKMap区上面,有4M的线性空间,被称作固定映射区,它和4G顶端只有4K的隔离带。固定映射区中每个地址项都服务于特定的用途,如ACPI_BASE等
Linux内核链表
Include/linux/list.h
链表数据结构的定义:
Struct list_head
{
Struct list_head *next, *prev;
};
list_head结构包含两个纸箱list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双向循环链表
Linux内核中提供的链表操作主要有:
——初始化链表头:INIT_LIST_HEAD(list_head *head)
——插入节点:list_add(struct list_head * new, struct list_head *head)
list_add_tail(struct list_head *new, struct list_head *head)
——删除节点:list_del(struct list_head * entry)
——提取数据结构:list_entry(ptr, type, member):已知数据结构中的节点指针ptr,找出数据结构,例:list_entry(aup, struct autofs, list)
——遍历:list_for_each(struct list_head *pos, struct list_head *head)(list_for_each是个宏。不用对pos初始化且在程序中是不断地变化的)
例:srtuct list_head *entry;
struct list_head cs46xx_devs;//链表头
list_for_each(entry, &cs46xx_devs)
{
card = list_entry(entry, struct cs_card, list);
if(card->dev_midi == minor)
break;
}
Linux内核定时器
更多推荐
所有评论(0)