32位Linux内核空间地址映射--------总结

网上的文章往往只是截取了一部分知识出来,对一个知识点不能有一个完整的认识,这对我们构建自己的知识体系很是不利。所以我把几篇关于linux内存空间地址映射的文章放在一起,方便对比学习。

对于32位机,总的来说物理内存被分为高端和低端内存两部分。我们可以使用virt_to_phys() 和 phys_to_virt() 方法用于896MB以下低端内存的虚拟地址和物理内存地址之间换算关系。高端内存的虚拟地址和物理内存地址之间不存在如此简单的换算关系。内核将高端内存划分为3部分:VMALLOC_START ~ VMALLOC_END、KMAP_BASE ~ FIXADDR_START和FIXADDR_START ~ 4G。对于高端内存的3部分,内存映射有三种方式:映射到”内核动态映射空间”(noncontiguous memory allocation),持久内核映射(permanent kernel mapping),临时映射(temporary kernel mapping)。


第一篇文章

高端内存出现的原因

再32位linux系统中,经典的内存分配方式就是3:1的分配方式,再虚拟地址中,0~3G是用户空间,3G ~ 4G是内核空间,再物理地址中,刚好相反, 0 ~ 1G是内核空间,剩下的是物理空间,很明显。这样设计的方便了程序员写程序,也方便了物理内存的扩充

在虚拟地址和物理地址建立映射的时候,用户空间的映射采用了段页式映射,但是内核空间并非如此,在一开始的时候,内核空间采用了固定映射的方式,也就是直接将虚拟地址 - 3G = 物理地址,但是,这样会带来的问题是这样内核只能访问1G的空间,也就是说,无论物理地址多么大内核也之恩呢访问1G的空间(其实在本质上,除了一些特殊的系统调用,内核不会访问用户空间,并且,在64位的情况下,高端内存也不存在)

所以出现了高端内存,想要解决1G映射的问题

高端内存实现

我们可以把这1G,划分成两部分,一部分用来fix-mapping,一部分用来dynamic-mapping。以x86为例,实际中的做法是,0xC0000000-0xF7FFFFFF的896MB用作fix-mapping,0xF8000000-0xFFFFFFFF的128MB用作dynamic-mapping,前者仍然对应于物理地址的0x00000000-0x37FFFFFF,后者就是所谓的high memory。
在这里插入图片描述
在这里插入图片描述

可以看出来,high memory在使用的时候会建立临时映射,使用完毕后会进行归还,相对应的物理地址如果在没有建立映射的时候是没有用的

额外知识点

在这里插入图片描述

上图中PAGE_OFFSET通常为0xC000 0000,而high_memory指的是0xF7FF FFFF,在物理内存映射区和和第一个vmalloc区之间插入的8MB的内存区是一个安全区,其目的是为了“捕获”对内存的越界访问。处于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存区。

Linux内核可以采用三种不同的机制将页框映射到高端内存区,分别叫做永久内核映射、临时内核映射以及非连续内存分配。


第二篇文章

内核空间映射到物理内存是从最低地址0x00000000开始,如下图所示

在这里插入图片描述

假设内核地址空间的简单线性地址转换关系为:物理地址 = 虚拟地址 – 0xC0000000,内核虚拟地址空间对应的物理内存范围0x00000000 ~ 0x40000000,只有1G,显然不合理。

内核地址空间划分3部分:ZONE_DMA(16M)、ZONE_NORMAL(16 ~ 896M)和ZONE_HIGHMEM(896 ~ 1024M)。

ZONE_DMA、ZONE_NORMAL线性映射属于线性映射区,高端内存ZONE_HIGHMEM(0xF8000000~0xFFFFFFFF)动态映射属于vmalloc区,高端内存又分为几部分分别采用vmalloc、固定、临时映射(不详述了)。

内核想访问高于896M的物理地址时,从0xF8000000 ~ 0xFFFFFFFF虚拟地址空间中取一部分与想要访问的物理内存建立映射即填充内核PTE页表(内核页表),访问完成之后内核释放0xF8000000 ~ 0xFFFFFFFF中的虚拟地址空间,以便其它进程访问,如下图。(采用这128M的虚拟空间,建立临时地址映射,完成了对所有高于896M物理内存的访问)

在这里插入图片描述


第三篇文章《浅析线性地址到物理地址的转换》

一. 概念介绍:

1.线性地址(linear address)(也称虚拟地址virtual address):是一个32位无符号整数,用来表示高达4GB的地址。

2.物理地址(physical address):实际地址。

3.VM 即虚拟内存 ,PM 即物理内存

4.(1)PGD(Page Global Directory) 即页全局目录。

(2)PUD(Page Upper Directory)即页上级目录。

(3)PMD(Page Middle Directory)即页中间目录。

(4)PT即页面表, PT中的表项称为PTE, 是“Page Table Entry”的缩写。

5.offset 即位移量,偏移量。

二. 模型分析:

Linux从2.6.11开始采用四级分页模型:(为了适用于32位和64位系统),linux目前采用的也是四级分页机制,所以我的讨论也基于此。

  1. 线性地址

不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图1:
在这里插入图片描述

图1

在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。

  1. 页表

多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

  1. 页表项

从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图2如下:

在这里插入图片描述

图2

这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。

不管是哪一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。

上图中的物理页基地址是 个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最 终要访问数据所在物理页的基地址。

三. 转换机制介绍:

如下图3所示,线性地址到物理地址的转换机制结构图:

在这里插入图片描述

图3

基本过程如下:

1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。

2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。

3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。

4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。

5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。

6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。

7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。

8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。

9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。

10.第五次读取内存得到最终要访问的数据。

整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单 位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

举例分析:

例一:

某任务加载后,在4GB虚拟地址空间创建了一个段,起始地址为0x00800000, 段界限为0x5000,字节粒度。当前任务执行时,段寄存器DS指向该段。又假设执行了下面一条指令: mov edx, [0x1050],问此时对应的物理地址是多少?

答:此时,段部件会输出线性地址0x00801050。在没有开启分页机制时, 这就是要访问的物理地址。但现在开启了分页机制,所以这是一个下虚拟地址,要经过页部件转换,才能得到物理地址。

如下图4所示,处理器的页部件专门负责线性地址到物理地址的转换工作。它首先将段部件送来的32位线性地址分为3段,分别是高10位,中间10位,低12位。高10位是页目录的索引,中间10位是页表的索引,低12位则作为页内偏移量来用。

在这里插入图片描述

图4

当前任务页目录的物理地址在处理器的CR3寄存器中,假设它的内容为0x00005000 。段管理部件输出的线性地址是0x00801050 。其二进制的形式如图中给出。高10位是十六进制的0x002。它是页目录表内的索引,处理器将它乘以4(因为每个目录项4字节)。作为偏移量访问页目录。最终处理器从物理地址00005008处取得页表的物理地址0x08001000。

线性地址的中间10位为0x001,处理器用它作为页表索引取得页的物理地址。将该值乘以4,作为偏移量访问页表。最终,处理器又从物理地址08001004处取得页的物理地址,这就是我们一直努力寻找的那个页。

页的物理地址是0x0000C000,而线性地址的低12位是数据所在的页内偏移量,故处理器将它们相加,得到物理地址0x0000C050,这就是线性地址0x00801050所对应的物理地址,要访问的数据就在这里。

当任务加载时,操作系统先创建虚拟的段,并根据段地址的高20位决定它要用到哪些页目录项和页表项。然后,寻找空闲的页,将原本应该写入段中的数据写到一个或者多个页中,并将页的物理地址填写到相对应的页表项中。只有这样做了,当程序运行的时候,才能以相反的顺序进行地址变换,并找到正确的数据。

例二:

通过以下的程序来分析虚拟内存到线性地址再到物理内存的映射,我们还以X86为例:

#include <stdio.h>

int greeting(){
        printf("Hello world!/n");
        return 0;
}

int
main (){
        greeting();
        return 0;
}

编译该程序,并查看反汇编文件:
在这里插入图片描述

这里我们主要看main和greeting的调用:
在这里插入图片描述

第一步:通过页目录表找到页面表
hello程序执行后,调用函数greeting,实验楼环境是64位的系统,为了方便我们将前32位为0的不看。这里的虚拟地址也就是线性地址为0x0040052d
call 0x0040052d
分解后的结果是:
0000 0000 0100 0000 0000 0101 0010 1101
第1个段位(高10位):
0000 0000 01
对映十进制的1,也就是在页目录表的偏移1找到其页面表的物理地址,也就是页面表的指针,它的低12位是0。

第二步:通过页面表找到页的起始物理地址高
接下来是线性地址的第二个段位(中间10位):
00 0000 0000
对映十进制的0,,也就是在刚才找到的页面表的偏移0,找到目标页的起始物理地址,高20位有效的地址,低12位填充为0.

第三步:得到最终的物理地址
通过找到的页起始物理地址,加上线性地址的第三个段位的偏移地址得到最终的物理地址.
例如:
第三个段位:0101 0010 1101
对映16进制为0x52d。
如果目标页的起始物理地址为:0x740000,那么最终的物理地址就是:
0x740000+0x52d=0x74052d

分析完成。

四. 为什么采用分页机制:

其实它的主要目的在于实现虚拟存储器。分页是通过内存控制单元(MMU)进行的。分页使得线性地址转换为物理地址。在分页中,为了效率起见,将线性地址分成固定长度,称为页,与页长度一致的是页框(物理页)。每个页框包含一个页。把线性地址映射到物理地址的数据结构称为页表。页表存放在主存之中,并在启用分页之前由内核对其进行初始化。

另外,因为分页机制的存在,程序使用的都是线性地址空间,而不再直接是物理地址。这好像是操作系统位应用程序提供了一个不依赖于硬件(物理内存)的平台,应用程序不必关心实际上有多少物理内存,也不必关心正在使用的是哪一段内存,甚至不必关心某一个地址是在物理内存里面还是在硬盘中。只要像操作系统申请就行,而操作系统全权负责了这其中的转换工作。


第四篇文章《linux 高端内存页框管理:永久内核映射、临时内核映射以及非连续内存分配》

摘要:高端内存页框的内核映射分为三种情况:永久内核映射、临时内核映射和非连续内存映射。那么这三者有什么区别和联系呢?临时内核映射如何保证不会被阻塞呢?本文主要为你解答这些疑问,并详细探讨高端内存映射的前两种方式。

本文来源:linux 高端内存页框管理:永久内核映射、临时内核映射以及非连续内存分配 http://blog.csdn.net/trochiluses/article/details/13016023

1.高端内存的区域划分

内核将高端内存划分为3部分:VMALLOC_START ~ VMALLOC_END、KMAP_BASE ~ FIXADDR_START和FIXADDR_START ~ 4G。
在这里插入图片描述

对于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。

对应高端内存的3部分,高端内存映射有三种方式:
映射到”内核动态映射空间”(noncontiguous memory allocation)
这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。

持久内核映射(permanent kernel mapping)
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可(注意理解这句话:一个页表(不是页表项),大小为4K,可以映射4M的空间),内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

临时映射(temporary kernel mapping)
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。

这块空间具有如下特点:
(1)每个 CPU 占用一块空间
(2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。

当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。

896M边界以上的页框并不映射在内核线性地址空间的第4个GB,因此内核不能直接访问它们。所以,返回所分配页框线性地址的页分配器函数并不对高端内存可用。

在64位平台上不存在这个问题,因为可以使用的线性地址空间大于能安装的RAM,也就是说这些体系结构的ZONE_HIGHMEM是空的。linux使用如下方法来使用高端内存:

1)高端内存页框的分配只能通过alloc_pages( )函数和它的快捷函数alloc_page( )。这些函数不返回线性地址,而是返回第一分配页框的页描述符的线性地址。

2)没有线性地址的高端内存中的页框不能被内核访问。

内核采用三种不同的机制将页框映射到高端内存:永久内核映射、临时内核映射、非连续内存分配。本节讨论前两种。

建立永久内核映射可能阻塞当前进程;也就是高端内存上没有页表项可以用作页框的窗口的时候。因此,这种方法不能用在中断处理函数和可延迟函数。临时内核映射不会阻塞当前进程,但是只有很少的临时内核映射可以建立起来。

需要注意的是,无论哪种方法,128M的线性地址用于高端内存映射,无法保证寻址范围同时到达的物理内存。

2.永久内核映射:注意,下列多有函数应用的范围是内核空间

宏定义与关键变量定义:

pkmap_page_table:高端内存主内核页表中,一个用于永久内核映射的专用页表锁在的地址

LAST_PKMAP: 上述页表所含有的表项(512或者1024)

PKMAP_BASE:该页表所映射线性地址的start地址

pkmap_count:对页表项提供计数器的数组

page_address_htable:散列表,用于记录高端页框与永久内核映射的线性地址之间的关系

page_address_map:一个数据结构,包含指向页描述符的指针和分配给页框的线性地址;用于为高端内存的每个页框提供当前映射,它被包含在page_address_htable这个hansh表中

关键函数:

page_address( page):返回页框对应的线性地址

Void * kmap(struct page * page):返回对应page的线性地址

Void * kmap_high(struct page * page): 同上,不过接受的参数是高端内存的页框描述符

map_new_virtual( ):插入页框的物理地址到pkmap_page_table,在page_address_htable散列表中加入一个元素

它是高端页框到内核地址空间的长期映射。使用主内核页表中的一个专门页表,地址存放在pkmap_page_table变量中。页表中的表项数由LAST_PKMAP宏产生。页表照样包含512或者1024项,这取决于PAE是否激活,因此,内核一次访问最多2M或者4M的高端内存。

该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包涵LAST_PKMAP个计数器,pkmap_page_table页表中的每一个项都有一个。我们区分下列三种情况

计数器为0:对应页表项没有映射任何的高端内存页框,并且是可用的。

计数器为1:对应的页表项没有映射任何内存页框,但是它不可用,因为从它最后一次使用以来,对应的TLB表项还未被刷新。

计数器为n:相应的页表项映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。

当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。

为了记录高端内存页框与永久内核映射的线性地址之间的联系,内核使用了page_address_htable散列表。该表包含一个page_address_map数据结构,用于为高端内存的每个页框进行当前映射。而该数据结构还包涵一个指向页描述符号的指针和分配给该页框的线性地址。

对应数据结构关系图如下:
在这里插入图片描述

page_address()函数返回页框对应的线性地址,如果页框在高端内存中并没有被映射,则返回NULL。这个函数接受一个页描述符指针page作为参数,并区分以下两种情况:

1)页框不在高端内存中:

__va( ( unsigned long) (page - meme_map) << 12)

  1. 页框在高端内存中,该函数就得到page_address_htable中寻找。如果在散列表中找到页框,page_address()就返回它的线性地址,否则就返回NULL。

代码实现如下:

void *kmap(struct page *page)
{
        might_sleep();
        if (!PageHighMem(page))
                return page_address(page);
        return kmap_high(page);
}
如果页框确实属于高端内存,那么调用kmap_high()函数如下:
 /* We cannot call this from interrupts, as it may block.
 */
void *kmap_high(struct page *page)
{       
        unsigned long vaddr;
 
        /*
         * For highmem pages, we can't trust "virtual" until
         * after we have the lock.
         */
        lock_kmap();
        vaddr = (unsigned long)page_address(page);//检查页框是否已经被映射
        if (!vaddr)//没有被映射
                vaddr = map_new_virtual(page);//将页框的物理地址插入到pkmap_page_table并在pkmapa_address_table散列表中加入一个元素
        pkmap_count[PKMAP_NR(vaddr)]++;//页框的线性地址对应的计数器+1
        BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
        unlock_kmap();
        return (void*) vaddr;
}<span style="font-size:14px">
</span>


其中的一些宏定义内容如下:
#define PKMAP_BASE              (PAGE_OFFSET - PMD_SIZE)
#define LAST_PKMAP              PTRS_PER_PTE
#define LAST_PKMAP_MASK         (LAST_PKMAP - 1)
#define PKMAP_NR(virt)          (((virt) - PKMAP_BASE) >> PAGE_SHIFT)
#define PKMAP_ADDR(nr)          (PKMAP_BASE + ((nr) << PAGE_SHIFT))

map_new_virtual( )函数本质上是两个嵌套循环,完成的工作是:插入物理地址到hashtable和在对应hashtable中增加一个元素,代码如下:
static inline unsigned long map_new_virtual(struct page *page)
{
        unsigned long vaddr;
        int count;
 
start:
        count = LAST_PKMAP;//固定映射的页表项个数
        /* Find an empty entry */
        for (;;) {
                last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;//与掩码进行按位与运算,避免数据过长造成的溢出
                if (!last_pkmap_nr) {//last_pkmap_nr==0,说明它原来已经到达最大值(注意与运算)
                        flush_all_zero_pkmaps();
                        count = LAST_PKMAP;
                }
                if (!pkmap_count[last_pkmap_nr])
                        break;  /* Found a usable entry */
                if (--count)
                        continue;
 
                /*
                 * Sleep for somebody else to unmap their entries
                 */
                {
                        DECLARE_WAITQUEUE(wait, current);
 
                        __set_current_state(TASK_UNINTERRUPTIBLE);
                        add_wait_queue(&pkmap_map_wait, &wait);
                        unlock_kmap();
                        schedule();
                        remove_wait_queue(&pkmap_map_wait, &wait);
                        lock_kmap();
                        /* Somebody else might have mapped it while we slept */
                        if (page_address(page))
                                return (unsigned long)page_address(page);
 
                        /* Re-start */
                        goto start;
                }
        }
        vaddr = PKMAP_ADDR(last_pkmap_nr);
        set_pte_at(&init_mm, vaddr,
                   &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
 
        pkmap_count[last_pkmap_nr] = 1;
        set_page_address(page, (void *)vaddr);
 
        return vaddr;
}

然后,kunmap()函数撤销原来有kmap()建立的永久内核映射。如果页处在高端内存,调用kunmap_high()函数。代码如下:

250 void kunmap_high(struct page *page)
251 {
252         unsigned long vaddr;            
253         unsigned long nr;               
254         unsigned long flags;
255         int need_wakeup;
256         
257         lock_kmap_any(flags);
258         vaddr = (unsigned long)page_address(page);
259         BUG_ON(!vaddr);//嵌入式汇编有关的bug处理
260         nr = PKMAP_NR(vaddr);//(((virt) - PKMAP_BASE) >> PAGE_SHIFT)页号
261 
262         /*
263          * A count must never go down to zero
264          * without a TLB flush!
265          */
266         need_wakeup = 0;
267         switch (--pkmap_count[nr]) {
268         case 0:
269                 BUG();
270         case 1://没有进程在使用页
271                 /*
272                  * Avoid an unnecessary wake_up() function call.
273                  * The common case is pkmap_count[] == 1, but
274                  * no waiters.
275                  * The tasks queued in the wait-queue are guarded
276                  * by both the lock in the wait-queue-head and by
277                  * the kmap_lock.  As the kmap_lock is held here,
278                  * no need for the wait-queue-head's lock.  Simply
279                  * test if the queue is empty.
280                  */
281                 need_wakeup = waitqueue_active(&pkmap_map_wait);//唤醒
282         }
283         unlock_kmap_any(flags);
284 
285         /* do wake-up, if needed, race-free outside of the spin lock */
286         if (need_wakeup)
287                 wake_up(&pkmap_map_wait);//唤醒由map_new_virtual()添加在等待队列中的进程
288 }
289 

3.临时内核映射:和进程控制有关

临时内核映射实现简单,可以用在中断处理程序和可延迟函数的内部(这些函数不能被阻塞),因为临时内核映射从来不阻塞当前进程,因为它被设计成是原子的。对比永久内核映射,发现如果页框暂时没有空闲的虚拟地址可以映射,那么永久内核映射将要被阻塞。

建立临时内核映射禁用内核抢占,这是必须的,因为映射对于每个处理器都是独特的,如果没有禁用抢占,那么哪个任务在哪个CPU上运行是不确定的。(这一段需要结合进程管理加以理解)

撤销临时内核映射的函数实际上可以不进行任何实质性的操作,它仅仅允许内核抢占即可(这样新的进程被调度,可以直接使用临时内核映射区域,覆盖原来的映射关系)。

每个CPU都有它自己的包含13个窗口的集合,它们用enum km_type数据结构表示。该数据结构定义的每个符号,标识了一个窗口的线性地址。

<span style="font-size:14px"> </span> 7 enum km_type {
  8         KM_BOUNCE_READ,
  9         KM_SKB_SUNRPC_DATA,
 10         KM_SKB_DATA_SOFTIRQ,
 11         KM_USER0,
 12         KM_USER1,
 13         KM_BIO_SRC_IRQ,
 14         KM_BIO_DST_IRQ,
 15         KM_PTE0,
 16         KM_PTE1,
 17         KM_IRQ0,
 18         KM_IRQ1,
 19         KM_SOFTIRQ0,
 20         KM_SOFTIRQ1,
 21         KM_L1_CACHE,
 22         KM_L2_CACHE,
 23         KM_TYPE_NR
 24 };

其中,内核要确保同一个窗口永远不会被两个不同的控制路径同时使用。最后一个符号非线性地址,但由每个CPU用来产生不同的可用窗口数。

km_type的每一个符号都是固定映射的线性地址的一个下标。enum fixed_addresses数据结构包含符号FIX——KMAP——BEGIN和FIX_KMP_END;把后者的值赋成下标FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。在这种方式下,系统中的每个CPU有KM-TYPE-NR个固定映射的线性地址。此外,内核用fix_to

_virt(FIX_KMAP_BEGIN )线性地址对应的页表项的地址初始化kmap_pte变量。

 39 void *kmap_atomic(struct page *page, enum km_type type)
 40 {
 41         unsigned int idx;
 42         unsigned long vaddr;
 43         void *kmap;
 44 
 45         pagefault_disable();//有关锁和内核抢占机制
 46         if (!PageHighMem(page))
 47                 return page_address(page);
 48 
 49         debug_kmap_atomic(type);//debug点
 50 
 51         kmap = kmap_high_get(page);//类似kmap_high的功能,只有这个函数返回非空指针,才可以调用kmap_high()
 52         if (kmap)
 53                 return kmap;
 54 
 55         idx = type + KM_TYPE_NR * smp_processor_id();//指明需要使用的线性地址
 56         vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);//固定映射的线性地址转化成虚拟地址
 57 #ifdef CONFIG_DEBUG_HIGHMEM
 58         /*
 59          * With debugging enabled, kunmap_atomic forces that entry to 0.
 60          * Make sure it was indeed properly unmapped.
 61          */
 62         BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
 63 #endif
 64         set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);//设置页表项:线性地址,page 页框信息
 65         /*
 66          * When debugging is off, kunmap_atomic leaves the previous mapping
 67          * in place, so this TLB flush ensures the TLB is updated with the
 68          * new mapping.
 69          */
 70         local_flush_tlb_kernel_page(vaddr);//刷新TLB无效
 71 
 72         return (void *)vaddr;
 73 }


#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
68 #define cpu_set_pte_ext(ptep,pte,ext)   processor.set_pte_ext(ptep,pte,ext)
 
6 #define TOP_PTE(x)      pte_offset_kernel(top_pmd, x)
311 /* Find an entry in the third-level page table.. */
312 extern inline pte_t * pte_offset_kernel(pmd_t * dir, unsigned long address)
313 {
314         pte_t *ret = (pte_t *) pmd_page_vaddr(*dir)
315                 + ((address >> PAGE_SHIFT) & (PTRS_PER_PAGE - 1));
316         smp_read_barrier_depends(); /* see above */
317         return ret;
318 }
 
371 #define mk_pte(page, pgprot)   pfn_pte(page_to_pfn(page), (pgprot))

————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/trochiluses/article/details/13016023

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值