国嵌视频学习第十天——内存管理

Linux内存管理

对于立志从事内核开发(驱动、网络协议栈)的工程师来说,熟悉linux的内存管理系统非常重要

物理地址:出现在CPU地址总线上的寻址物理内存的地址信号,是地址变换的最终结果

线性地址(虚拟地址):32CPU架构下,可以表示4G的地址空间,用16进制表示就是0x000000000xffffffff

逻辑地址:程序经过编译后,出现在汇编程序中的地址

 

CPU要将一个逻辑地址转换位物理地址,需要两步:首先CPU利用段式内存管理单元,将逻辑地址转换成线性地址,再利用页式内存管理单元,把线性地址最终转换位物理地址。

——段式管理16位 CPU内部拥有20位的地址线,它的寻址范围就是220次方,也就是1M的内存空间。但是16CPU用于存放地址的寄存器(IP,SP...)只有16位,因此只能访问65536个存储单元,64K

为了能够访问1M的内存空间,CPU就采用了内存分段的管理模式,并在CPU内部加入了段寄存器。16CPU1M内存空间分为若干个逻辑段,每个逻辑段的要求如下:

1.逻辑段的起始地址(段地址)必须是16的倍数,即最后4个二进制位必须为0

2.逻辑段的最大容量为64K

物理地址的形成方式:

由于段地址必须是16的倍数,所以值的一般形式为XXXX0H,即前16位二进制位是变化的,后4位是固定的0,鉴于段地址的这种特性,可以只保存前16位二进制位来保存整个段基地址,所以每次使用时要用段寄存器左移补40(乘以16)来得到实际的段地址(因为寄存器是16位的,而地址线是20位,既然舍弃了低4位,那么为了得到实际的PA,就需要对其*16)

在确定了某个存储单元所属的段后,只是知道了该存储单元所属的范围(段地址->段地址+65536),如果想确定该内存单元的具体位置,还必须知道该单元在段内的偏移。有了段地址和偏移量,就可以唯一的确定内存单元在存储器中的具体位置。

逻辑地址=段基地址+段内偏移量

由逻辑地址得到物理地址的公式为:

PA=段寄存器的值*16 + 逻辑地址(偏移部分)

段寄存器是为了对内存进行分段管理而增加的,16CPU有四个段寄存器,程序可同时访问四个不同含义的段。

1).CS+IP:用于代码段的访问,CS指向存放程序的段基址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令

2)SS+SP:用于堆栈段的访问,SS指向堆栈段的基地址,SP指向栈顶,可以通过SSSP两个寄存器直接访问栈顶单元的内存物理位置

3)DS+BX:用于数据段的访问。DS中的值左移四位得到数据段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址

4)ES+BX:用于附加段的访问。ES中的值左移四位得到附加段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。

32位的pc的内存管理任然采用“分段”的管理模式,逻辑地址同样由段地址和偏移量两部分组成,32pc的内存管理和16pc的内存管理有相同之处也有不同之处,因为32pc采用了两种不同的工作方式:实模式和保护模式

1).实模式:在实模式下,32cpu的内存管理与16cpu是一致的

2)保护模式:段基地址长达32位,每个段的最大容量可达4G,段寄存器的值是段地址的“选择器(selector)”,用该“选择器”从内存中得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移量,这与16cpu的物理地址计算方式完全不同。

——页式管理从管理和效率的角度出发,线性地址被分为固定长度的组,称为页(page),例如32位的机器,线性地址最大可为4G,如果用4KB为一个页来划分,这样整个线性地址就被划分为220次方个页(虚拟的)

另一类“页”,称之为物理页,或者是页框、页桢。分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与线性地址页是相同的。(实在的)

页表项中存放的是物理页的基地址

cr3CPU中的寄存器,在创建一个进程时,将伴随创建页表、页目录。在做进程切换时会更新cr3寄存器

图中的一个页是4K(从offest12位即可看出)

这是所谓两级分页模型:页目录、页表

Linux内核的设计并没有全部采用intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了linux内核的涉及,而且为把linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制

所有段的基地址均为0

线性地址=基地址+逻辑地址(偏移量)。此时基地址为0,那么线性地址便是等于逻辑地址

由此可以看出,每个段的逻辑地址空间范围为0-4GB。因为每个段的基地址为0,因此,逻辑地址与线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在linux中所提到的逻辑地址和线性地址(虚拟地址),可以认为是一致的。看来,linux巧妙地把段机制给绕过去了,而完全利用了分页机制

因此,在linux中逻辑地址、虚拟地址、线性地址是一致的

前面介绍了i386的二级页管理架构,不过有些CPUalpha 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,用户看懂和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。

Linux4G的虚拟地址空间划分为两个部分——用户空间与内核空间。用户空间从00xbfffffff,内核空间从3G4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用访问内核空间

用户空间对应进程,所以每当进程切换,用户空间就会跟着变化(页表、页目录等信息要变)

进程空间

每个进程的用户空间都是完全独立、互不相干的、把同一个程序同时运行10次(为了能同时运行,让它们再返回前睡眠100秒),会看到10个进程使用的线性地址(虚拟地址)一模一样(但是它们的物理地址是不一样的)。Ps auxcat /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分配的物理页映射!)

使用KMAP4M这么个窗口,可以访问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结构的指针prevnext,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双向循环链表

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内核定时器

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值