linux内核笔记——内核地址空间

来源:互联网 发布:郑州java培训 编辑:程序博客网 时间:2024/06/07 02:13

        记得毕业面试时面试官问我什么是进程,我还只会傻乎乎的回答进程就是正在运行的程序,随着知识的不断积累发现要准确描述进程这个抽象真不是一件容易的事,在谈内核地址空间前不妨再试着描述一下进程来作为本文的切入点。

        我的理解是:进程是处理器按照某段加载到内存的指令序列(可执行文件)执行时所涉及到的所有的数据、结构及执行环境的综合,围绕着task_struct结构,对应一个虚拟地址空间,包含了内存,打开的文件,访问的设备等资源。指令序列和数据存在于物理内存中,处理器处理进程时面向其虚拟地址空间,而从虚拟地址到物理内存的映射由进程的页表来完成。

        虽然不同进程的指令序列被加载到物理内存的不同位置,但都在自己对应的虚拟地址空间的固定区段(text段),虚拟地址start_code(0x08048000)——end_code之间。不同进程或许可以共用某段物理内存,但却彼此感知不到对方的虚拟地址空间,因为虚拟地址空间只是处理器的寻址空间,并不实际存在,它需要经过页表的转换才能定位到真正有意义的物理内存中。处理器对某个进程的寻址自然是其它进程感知不到的。

        虚拟地址空间的大小由处理器的通用寄存器容量决定,如IA-32架构中,处理器的通用寄存器大小是32bit,那么该处理器一次可以处理32比特的数据,所以该处理器的寻址范围是0x00000000——0xFFFFFFFF,一共是2^32,所以虚拟地址空间的大小就是2^32。而这2^32大小的地址空间,又进一步分为内核地址空间和用户地址空间。用户地址空间在低地址(靠近0),内核地址空间位于高地址,通常在IA-32架构中默认的分割比例是3:1,即内核地址空间的地址范围是0xC0000000-0xFFFFFFFF。这是虚拟地址空间的第一层划分。

        0xC0000000-0xFFFFFFFF这部分地址是内核地址空间,所有进程的这部分虚拟地址空间是共享的;也就是说进程切换时,这部分虚拟地址空间关联的物理内存是不变的,内核更换根页表时管理这部分虚拟地址空间的映射的那部分页表是不变的;也就是说处理器对0xC0000000-0xFFFFFFFF部分的寻址所关联到的物理内存,不随进程的切换而改变。

        那么这0xC0000000-0xFFFFFFFF部分的内核地址空间又是怎么管理的呢,我们只讨论IA-32架构,如图1对内核地址空间又进一步的划分。

图1

1.      低端内存直接映射区:该部分地址空间大小是896M,地址空间范围是0xC0000000——0xC0000000+896M。这部分虚拟地址空间和物理内存的前896M直接映射,映射关系如图2。内核用一对宏来进行该区域的虚拟地址和物理内存之间的转换。cpu寻址这段地址空间时,不需要经过页表(虽然页表中也存在对应的映射),只需要经过下面宏就可实现转换,反向映射也同样简单。

#define __pa(x) ((unsigned long)(x) & 0x3fffffff)#define __va(x) ((void *)((unsigned long)(x) | 0xc0000000))

        该部分地址空间映射DMANORMAL两部分内存,DMA为前16MNORMAL16M-896M部分。

图2

2.      隔离带:低端内存和vmalloc区之间的一个缺口,一般为8M地址空间,这8M的地址空间不关联任何物理页,即在根页表对应的两个跟页表项(IA-32中根页表一个页表项对应4M的虚拟地址空间)中指向下级页表的指针为空。它的作用是防止直接映射区访问越界。注意这里浪费的是处理器的寻址空间,并没有浪费任何物理内存。

3.      VMALLOC区:该部分地址空间用于分配虚拟地址空间上连续,但物理内存不连续的内存区(后续详细介绍)。该区域的大小不固定,如果启用了高端内存,该区域就截止于持久映射区前,如果没有启用高端内存,该区域截止于固定映射区前。

4.      隔离带:两页(8k)大小,作为VMALLOC区域和下一个区域的隔离带。

5.      持久映射:如果没启用高端内存支持,该区域不存在。也就是说该段内核地址空间专用于映射高端内存。该段内核地址空间的范围是PKMAP_BASE——FIXADDR_START

6.      固定映射:该区域根据类型映射高端内存,该区域对应每个处理器都有一系列的虚拟页用于每个处理器和物理内存建立各种类型的映射。

 

内核二进制码及内核的数据被装载在物理内存的前8M,内核地址空间的前0xC0000000-0xC00000000+8M,内核根页表的前两个页表项。

图3

第一个内存页0-4K,保留给BIOS使用,接下来的640K是系统保留区。紧跟着后面的是映射各种rom(通常是BIOS和显卡的ROM)。内核从1M处开始装在,先是内核的二进制程序,装载在_text和_etext之间,紧跟其后的是数据段,然后是bss段。

 

VMALLOC

位置:前面介绍了VMALLOC区的位置位于内核虚拟地址空间中直接映射区之后,虚拟地址范围范围从VMALLOC_STARTVMALLOC_END

功能:调用vmalloc函数时,会从该VMALLOC区申请一部分连续的虚拟地址空间,从物理内存申请一些连续或不连续的物理内存页,将其关联起来,并修改内核页表使内核通过该连续的虚拟地址访问到不连续的物理内存页帧。一般用于内核对模块的实现,因为模块的加载可能出现在任意时刻。如果加载模块时,低端映射区没有足够的连续物理内存可用,则可向该区域申请。

管理:内核管理vmalloc区是通过vm_struct结构来实现的,该结构用于内核根据哪些子区域被使用。

vm_struct结构如下:

struct vm_struct {struct vm_struct*next;void*addr;unsigned longsize;unsigned longflags;struct page**pages;unsigned intnr_pages;unsigned longphys_addr;};

        next:将vm_struct结构对象维护在一个链表中

        addr:该子区域的在虚拟地址空间中的起始地址

        size:该子区域的长度

        pagespage*数组,存放着指向该子区域关联的物理内存页的指针

        nr_pagespage*数组的长度

        每个vm_struct结构表示VMALLOC区域的一个子区,在VMALLOC区中,每个子区是一段连续的虚拟地址空间,子区与子区之间通过4K的虚拟地址空间(即一个虚拟内存页)如图4所示:

图4

        而vm_struct在内存中以链表形式组织在一起,链表头是全局变量struct vm_struct *vmlist;每个vm_struct结构实例在这个链表中按照其管理的虚拟地址进行排列。如图5所示,三个vm_struct结构实例以链表的形式组织在一起,每个实例关联了一段连续的虚拟地址空间和一些物理内存页。

图5

建立映射过程:

        该区域的映射通过调用函数vmalloc实现,vmalloc函数原型如下:

void *vmalloc(unsigned long size); 

        该函数调用层次图如下:主要工作有_vmalloc_node完成,而_vmalloc_node又分别调用get_vm_area_node_vmalloc_area_node来分别获取连续的虚拟地址和所需要的页帧。

图6

整个过程一共分三步

        简单总结起来该函数有三个动作:

          1.创建vm_struct结构。

          2.通过遍历vm_struct结构的链表,找到可以分配的虚拟地址空间的首地址,并将vm_struct结构插入到链表对应位置。

          3.申请物理页帧,将其对应的page的地址存放到vm_struct结构的pages数组中。

        申请虚拟内存的主要工作都在__get_vm_area_node函数中,在该函数中先创建了一个vm_struct结构,而后迭代vm_struct链表找到可以分配的虚拟地址,而后初始化刚刚创建的vm_struct结构,并将其添加到链表的对应位置;而在_vmalloc_area_node函数中为pages中的组个元素申请物理页帧。由于源码很清晰简单,所以这里就不贴源码了。

解除映射过程:

        通过调用vfree,传入要释放的虚拟地址就可以释放虚拟地址,将物理内存也返还给伙伴系统。

 

持久映射区:

位置:前文介绍了,只有启用了高端内存支持的情况下才有该区域,位于内核地址空间的PKMAP_BASEFIXMAP_START之间。

功能:将高端内存长期映射到内核地址空间中,通过kmap函数实现,kmap函数传入page*

管理:一个pkmap_count数组,一个page_address_maps哈希数组。

          pkmap_count数组的大小是持久映射区包含的虚拟内存页数,用于该区每个虚拟内存页被引用的次数。

          page_address_maps哈希数组用来管理该区域已经和高端内存建立映射的虚拟内存页,建立在page_address_map结构的基础上,page_address_map的结构如下。哈希数组的组织结构如图7

struct page_address_map {  struct page *page;       // 指向高端内存物理页帧的指针void *virtual;           // 虚拟内存的地址struct list_head list;   // 将节点链接在哈希数组中。};  static struct page_address_map page_address_maps[LAST_PKMAP]; 

图7

建立映射过程:

        调用kmap函数,传入一个物理页帧page的指针,该函数会返回一个虚拟地址,该函数执行的过程中已经建立了该物理页帧和返回的虚拟地址之间的关联,并修改了页表,将该虚拟地址在页表中对应的物理内存修改为传入的page

具体过程是:

          1.      Kmap函数先判断传入的page是低端内存还是高端内存,如果是低端内存,则调用page_address函数返回虚拟地址(page_address函数中仍然会判断page是低端内存还是高端内存,如果是低端内存通过直接映射关系计算器虚拟地址,如果是高端内存则迭代page_address_map哈希表找到对应的虚拟地址),如果传入的page是高端内存,则调用kmap_high函数处理。

          2.      进入在kmap_high函数中,说明入参page对应高端内存的物理页帧,仍然要先调用page_address判断该物理页是否已经关联了虚拟地址。如果已经关联则将对应的虚拟地址在pkmap_count中的项+1说明又一次被引用,返回该page对应的虚拟地址。如果没有关联,则调用map_new_virtual函数,给该page建立关联的物理地址。

          3.      map_new_virtual函数中,先通过扫描pkmap_count中的项为该page找到未使用的虚拟地址,然后修改页表,便于通过虚拟地址找到物理地址。然后调用set_page_address函数为page和虚拟地址建立映射结构page_address_map,并添加到图7所示的结构中。

解除映射过程:

        调用kunmap函数,传入虚拟地址,将该虚拟地址对应的pkmap_count数组中的计数减一,然后调用一个固定的函数,这个函数会将pkmap_count数组中计数为1的项设置为0,然后从也表中删除,然后删除page_address_maps哈希表中对应的vm_struct结构。

 

固定映射区

位置:位于虚拟地址空间的最末尾,固定映射区的虚拟内存页数是架构中处理器的个数和固定映射区映射类型乘积。

功能:同样是映射高端内存到内核地址空间,和持久映射不同的是,建立固定映射的时候不会进入睡眠,而建立持久映射时,如果扫描pkmap_count数组没有找到空闲位置时,就会进入睡眠状态。而固定映射不会进入睡眠状态,因此他比持久映射要更快速。

管理:该区域的管理比较简单,因为该内核虚拟地址区域的页数是cpu*type数,所以将该区域按照cpu个数平均分割,如图8

图8

 

未完待续

 


 

原创粉丝点击