Linux内存管理:Fixmaps(固定映射地址)和ioremap【转】

转自:https://rtoax.blog.csdn.net/article/details/114749083

目录

Fixmaps和ioremap

映射

ioremap工作原理

早期ioremap的使用

Links

相关阅读


Fix-Mapped地址是一组特殊的编译时地址,其对应的物理地址不必是线性地址减__START_KERNEL_map;

ioremap 功能是将给定的物理地址映射为虚拟地址。

Fixmaps和ioremap

Fix-Mapped地址是一组特殊的编译时地址,其对应的物理地址不必是线性地址减__START_KERNEL_map。每个固定映射的地址都映射一个页面框架,内核将其用作永远不会更改其地址的指针。这就是这些地址的重点。如评论所述:to have a constant address at compile time, but to set the physical address only in the boot process。您可能还记得,在最早的部分中,我们已经设置了level2_fixmap_pgt

  1.  
    NEXT_PAGE(level2_fixmap_pgt)
  2.  
    .fill 506,8,0
  3.  
    .quad level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE
  4.  
    .fill 5,8,0
  5.  
     
  6.  
    NEXT_PAGE(level1_fixmap_pgt)
  7.  
    .fill 512,8,0

如您所见level2_fixmap_pgt,紧随其后的level2_kernel_pgt是内核代码+数据+ bss。每个修订映射的地址都由一个整数索引表示,该整数索引fixed_addressesarch / x86 / include / asm / fixmap.h中的枚举中定义。例如,它包含以下各项的条目VSYSCALL_PAGE-如果启用了对旧版vsyscall页面的仿真,FIX_APIC_BASE则为本地apic等。在虚拟内存中,固定映射区域位于模块区域:

  1.  
    +-----------+-----------------+---------------+------------------+
  2.  
    | | | | |
  3.  
    |kernel text| kernel | | vsyscalls |
  4.  
    | mapping | text | Modules | fix-mapped |
  5.  
    |from phys 0| data | | addresses |
  6.  
    | | | | |
  7.  
    +-----------+-----------------+---------------+------------------+
  8.  
    __START_KERNEL_map __START_KERNEL MODULES_VADDR 0xffffffffffffffff

基本的虚拟地址和fix-mapped区域的大小由以下两个宏表示:

  1.  
    #define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
  2.  
    #define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)

__end_of_permanent_fixed_addressesfixed_addresses枚举的一个元素,正如我上面所写:每个固定映射的地址都由一个整数索引表示,该索引在中定义fixed_addressesPAGE_SHIFT确定页面的大小。例如,我们可以通过1 << PAGE_SHIFT表达式获得一页的大小。

在我们的例子中,我们需要获取修订映射区域的大小,而不仅仅是一个page,这就是为什么我们__end_of_permanent_fixed_addresses要获取修订映射区域的大小的原因。的__end_of_permanent_fixed_addresses是最后索引fixed_addresses枚举或换句话说,__end_of_permanent_fixed_addresses包含在一个固定的映射区域页面量。因此,如果将__end_of_permanent_fixed_addresses页面大小值的乘积,我们将获得固定映射区域的大小。以我的为例,它超过了536千字节。在您的情况下,它可能是一个不同的数字,因为大小取决于修订映射地址的数量,而修订映射地址的数量取决于内核的配置。

第二个FIXADDR_START宏只是从固定映射区域的最后一个地址中减去固定映射区域的大小,以获取其基本虚拟地址。FIXADDR_TOP是从vsyscall空间的基地址开始的四舍五入地址:

#define FIXADDR_TOP     (round_up(VSYSCALL_ADDR + PAGE_SIZE, 1<<PMD_SHIFT) - PAGE_SIZE)

fixed_addresses枚举被用作指数由获得虚拟地址fix_to_virt的功能。该功能的实现很容易:

  1.  
    static __always_inline unsigned long fix_to_virt(const unsigned int idx)
  2.  
    {
  3.  
    BUILD_BUG_ON(idx >= __end_of_fixed_addresses);
  4.  
    return __fix_to_virt(idx);
  5.  
    }

首先,它检查为该fixed_addresses枚举给出的索引是否大于或等于__end_of_fixed_addressesBUILD_BUG_ON宏,然后返回该__fix_to_virt宏的结果:

#define __fix_to_virt(x)        (FIXADDR_TOP - ((x) << PAGE_SHIFT))

在这里,我们向左移动fix-mapped区域的给定索引,该索引PAGE_SHIFT决定了我在上面所写的页面大小,并从区域FIXADDR_TOP的最高地址那里减去它fix-mapped

  1.  
    +-----------------+
  2.  
    | PAGE 1 | FIXADDR_TOP (virt address)
  3.  
    | PAGE 2 |
  4.  
    | PAGE 3 |
  5.  
    | PAGE 4 (idx) | x - 4
  6.  
    | PAGE 5 |
  7.  
    +-----------------+

有一个反函数,用于获取与给定虚拟地址相对应的修订映射区域的索引:

  1.  
    static inline unsigned long virt_to_fix(const unsigned long vaddr)
  2.  
    {
  3.  
    BUG_ON(vaddr >= FIXADDR_TOP || vaddr < FIXADDR_START);
  4.  
    return __virt_to_fix(vaddr);
  5.  
    }

virt_to_fix采用虚拟地址,检查该地址是间FIXADDR_STARTFIXADDR_TOP并调用__virt_to_fix其实现为宏:

#define __virt_to_fix(x)        ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT)

正如我们可以看到的,__virt_to_fix宏清除第一12在给定的虚拟地址位,从最后地址的减去它fix-mapped区域(FIXADDR_TOP),并转移该结果正确的PAGE_SHIFT12。让我解释一下它是如何工作的。

与前面的示例一样(在__fix_to_virt宏中),我们从修复映射区域的顶部开始。我们还从上到下从头到尾搜索与给定虚拟地址相对应的固定映射区域的索引。如您所见,首先我们将12使用x & PAGE_MASK表达式清除给定虚拟地址中的前几位。这使我们能够获取页面的基地址。如果给定的虚拟地址指向页面的开头/中间或结尾的某个位置,而不指向页面的基地址,则需要执行此操作。在下一步中,从中减去此值FIXADDR_TOP,这将为我们提供修订映射区域中相应页面的虚拟地址。最后,我们将这个地址的值除以PAGE_SHIFT。这为我们提供了与给定虚拟地址相对应的固定映射区域的索引。看起来可能很难,但是如果您逐步进行此操作,则可以确保该__virt_to_fix宏非常简单。

就这样。现在,我们对fix-mapped地址有所了解,但是接下来就足够了。

Fix-mapped地址在linux内核中的不同位置使用。IDT描述符存储在此处,英特尔可信执行技术UUID存储在fix-mappedFIX_TBOOT_BASE索引,Xen bootmap等开始的区域中...我们已经在linux内核初始化fix-mapped的第五部分中看到了一些地址。我们fix-mapped在早期ioremap初始化中使用area 。让我们更仔细地看一下它,并尝试了解ioremap它是什么,如何在内核中实现它以及它与fix-mapped地址的关系。

映射

Linux内核提供了许多不同的原语来管理内存。此时此刻,提出I/O memory。每个设备都通过对其寄存器的读/写操作来控制。例如,驱动程序可以通过写入设备的寄存器来关闭/打开设备,或者通过读取设备的寄存器来获取设备的状态。除寄存器外,许多设备还具有缓冲区,驱动程序可以在其中写入或读取内容。众所周知,目前有两种访问设备寄存器和数据缓冲区的方法:

  • 通过I / O端口;
  • 将所有寄存器映射到存储器地址空间;

在第一种情况下,设备的每个控制寄存器都具有多个输入和输出端口。设备驱动程序可以从一个端口读取和写入两个inout指示,我们已经看到了。如果您想了解当前注册的端口区域,可以通过访问来了解它们/proc/ioports

  1.  
    $ cat /proc/ioports
  2.  
    0000-0cf7 : PCI Bus 0000:00
  3.  
    0000-001f : dma1
  4.  
    0020-0021 : pic1
  5.  
    0040-0043 : timer0
  6.  
    0050-0053 : timer1
  7.  
    0060-0060 : keyboard
  8.  
    0064-0064 : keyboard
  9.  
    0070-0077 : rtc0
  10.  
    0080-008f : dma page reg
  11.  
    00a0-00a1 : pic2
  12.  
    00c0-00df : dma2
  13.  
    00f0-00ff : fpu
  14.  
    00f0-00f0 : PNP0C04:00
  15.  
    03c0-03df : vesafb
  16.  
    03f8-03ff : serial
  17.  
    04d0-04d1 : pnp 00:06
  18.  
    0800-087f : pnp 00:01
  19.  
    0a00-0a0f : pnp 00:04
  20.  
    0a20-0a2f : pnp 00:04
  21.  
    0a30-0a3f : pnp 00:04
  22.  
    0cf8-0cff : PCI conf1
  23.  
    0d00-ffff : PCI Bus 0000:00
  24.  
    ...
  25.  
    ...
  26.  
    ...

/proc/ioports提供有关哪个驱动程序使用I/O端口区域的哪个地址的信息。所有这些存储器区域的,例如0000-0cf7,用所要求保护的request_region功能从包括/ LINUX / ioport.h。实际上request_region是一个宏,定义为:

#define request_region(start,n,name)   __request_region(&ioport_resource, (start), (n), (name), 0)

如我们所见,它带有三个参数:

  • start -地区开始;
  • n -区域的长度;
  • name -请求者名称。

request_region分配I/O端口区域。通常在check_region之前调用该函数request_region以检查给定的地址范围是否可用以及release_region释放存储区域的函数。request_region返回指向该resource结构的指针。该resource结构表示系统资源的树状子集的抽象。我们已经resource在内核初始化过程的第五部分中看到了该结构,它看起来如下:

  1.  
    struct resource { //该resource结构表示系统资源的树状子集的抽象
  2.  
    resource_size_t start;
  3.  
    resource_size_t end;
  4.  
    const char *name;
  5.  
    unsigned long flags;
  6.  
    struct resource *parent, *sibling, *child;
  7.  
    };

并包含资源,名称等的开始和结束地址每一个resource结构包含指向的parentsiblingchild资源。因为它有一个父母和一个孩子,这意味着资源的每个子集都具有根resource结构。例如,对于I/O端口,它是以下ioport_resource结构:

  1.  
    struct resource ioport_resource = {
  2.  
    .name = "PCI IO",
  3.  
    .start = 0,
  4.  
    .end = IO_SPACE_LIMIT,
  5.  
    .flags = IORESOURCE_IO,
  6.  
    };
  7.  
    EXPORT_SYMBOL(ioport_resource);

或者iomem,它是iomem_resource结构:

  1.  
    struct resource iomem_resource = {
  2.  
    .name = "PCI mem",
  3.  
    .start = 0,
  4.  
    .end = -1,
  5.  
    .flags = IORESOURCE_MEM,
  6.  
    };

正如我之前提到的,request_regions它用于注册I / O端口区域,并且该宏在内核中的许多地方都使用。例如,让我们看一下drivers/char/rtc.c。此源代码文件在linux内核中提供了Real Time Clock接口。作为每个内核模块,rtc模块包含module_init定义:

module_init(rtc_init);

这里rtc_initrtc初始化函数。此功能在同一rtc.c源代码文件中定义。在rtc_init函数中,我们可以看到对函数的几个调用,例如rtc_request_region包装request_region

r = rtc_request_region(RTC_IO_EXTENT);

rtc_request_region被调用

r = request_region(RTC_PORT(0), size, "rtc");

RTC_IO_EXTENT是内存区域的大小,它是0x8"rtc"是该区域的名称,并且RTC_PORT是:

#define RTC_PORT(x)     (0x70 + (x))

因此,request_region(RTC_PORT(0), size, "rtc")我们使用来注册一个存储区域,该存储区域的起始位置为0x70,大小为0x8。让我们看一下/proc/ioports

  1.  
    ~$ sudo cat /proc/ioports | grep rtc
  2.  
    0070-0077 : rtc0

所以,我们明白了!好的,就是I / O端口了。与驱动程序通信的第二种方法是使用I/O内存。如前所述,这是通过将控制寄存器和设备内存映射到内存地址空间来实现的。I/O内存是设备通过总线向CPU提供的一组连续地址。内核没有直接使用任何内存映射的I / O地址。有一个特殊ioremap功能允许我们将总线上的物理地址转换为内核虚拟地址。换句话说,ioremap映射I / O物理内存区域以使其可从内核访问。该ioremap函数有两个参数:

  • 内存区域的开始;
  • 内存区域的大小;

I / O内存映射API提供了用于检查,请求和释放作为I / O内存的内存区域的功能。为此,有三个功能:

  • request_mem_region
  • release_mem_region
  • check_mem_region
  1.  
    ~$ sudo cat /proc/iomem
  2.  
    ...
  3.  
    ...
  4.  
    ...
  5.  
    be826000-be82cfff : ACPI Non-volatile Storage
  6.  
    be82d000-bf744fff : System RAM
  7.  
    bf745000-bfff4fff : reserved
  8.  
    bfff5000-dc041fff : System RAM
  9.  
    dc042000-dc0d2fff : reserved
  10.  
    dc0d3000-dc138fff : System RAM
  11.  
    dc139000-dc27dfff : ACPI Non-volatile Storage
  12.  
    dc27e000-deffefff : reserved
  13.  
    defff000-deffffff : System RAM
  14.  
    df000000-dfffffff : RAM buffer
  15.  
    e0000000-feafffff : PCI Bus 0000:00
  16.  
    e0000000-efffffff : PCI Bus 0000:01
  17.  
    e0000000-efffffff : 0000:01:00.0
  18.  
    f7c00000-f7cfffff : PCI Bus 0000:06
  19.  
    f7c00000-f7c0ffff : 0000:06:00.0
  20.  
    f7c10000-f7c101ff : 0000:06:00.0
  21.  
    f7c10000-f7c101ff : ahci
  22.  
    f7d00000-f7dfffff : PCI Bus 0000:03
  23.  
    f7d00000-f7d3ffff : 0000:03:00.0
  24.  
    f7d00000-f7d3ffff : alx
  25.  
    ...
  26.  
    ...
  27.  
    ...

这些地址的一部分来自该e820_reserve_resources函数的调用。我们可以在arch/x86/kernel/setup.c找到对此函数的调用,并且函数本身在arch/x86/kernel/e820.c.中定义e820_reserve_resources遍历e820映射并将内存区域插入到根iomem资源结构中。e820插入iomem资源中的所有内存区域具有以下类型:

  1.  
    static inline const char *e820_type_to_string(int e820_type)
  2.  
    {
  3.  
    switch (e820_type) {
  4.  
    case E820_RESERVED_KERN:
  5.  
    case E820_RAM: return "System RAM";
  6.  
    case E820_ACPI: return "ACPI Tables";
  7.  
    case E820_NVS: return "ACPI Non-volatile Storage";
  8.  
    case E820_UNUSABLE: return "Unusable memory";
  9.  
    default: return "reserved";
  10.  
    }
  11.  
    }

我们可以在/proc/iomem(如上所示)中看到它们。

ioremap工作原理

ioremap功能允许我们将总线上的物理地址转换为内核虚拟地址

现在,让我们尝试了解其ioremap工作原理。我们已经了解了一点ioremap,我们在第五部分中了解了Linux内核初始化。如果您已阅读此部分,则可以记住arch / x86 / mm / ioremap.c中的early_ioremap_init函数调用。的初始化是分为两个部分:有,我们可以前的正常使用初期可用且正常这是后可用初始化和调用。我们暂时一无所知,所以让我们考虑对的早期初始化。首先检查在页面中间目录边界上对齐的内容:ioremapioremapioremapvmallocpaging_initvmallocioremapearly_ioremap_initfixmap

BUILD_BUG_ON((fix_to_virt(0) + PAGE_SIZE) & ((1 << PMD_SHIFT) - 1));

关于BUILD_BUG_ON您的更多信息,可以在第一部分中阅读有关Linux Kernel初始化的信息。因此,BUILD_BUG_ON如果给定的表达式为true,则macro会引发编译错误。在检查之后的下一步中,我们可以early_ioremap_setupmm / early_ioremap.c中看到该函数的调用。此函数提供的通用初始化ioremapearly_ioremap_setup函数slot_virt使用早期Fixmap的虚拟地址填充数组。所有早期的修订图都__end_of_permanent_fixed_addresses在内存中。它们从FIX_BITMAP_BEGIN(顶部)开始,以FIX_BITMAP_END(向下)结束。实际上512,早期有一些临时的启动时映射ioremap

  1.  
    #define NR_FIX_BTMAPS 64
  2.  
    #define FIX_BTMAPS_SLOTS 8
  3.  
    #define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)

early_ioremap_setup

  1.  
    void __init early_ioremap_setup(void)
  2.  
    {
  3.  
    int i;
  4.  
     
  5.  
    for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
  6.  
    if (WARN_ON(prev_map[i]))
  7.  
    break;
  8.  
     
  9.  
    for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
  10.  
    slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
  11.  
    }

slot_virt和其它阵列在相同的源代码文件中定义:

  1.  
    static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
  2.  
    static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
  3.  
    static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;

slot_virt包含区域的虚拟地址fix-mappedprev_map数组包含早期ioremap区域的地址。请注意,我在上面写过:Actually there are 512 temporary boot-time mappings, used by early ioremap并且您可以看到所有数组都使用__initdata属性定义,这意味着该内存将在内核初始化过程后释放。后early_ioremap_setup完成其工作,我们正在页中间目录,早期的ioremap与开始early_ioremap_pmd刚刚得到页全局目录的基地址和计算给定的地址页面中间目录功能:

  1.  
    static inline pmd_t * __init early_ioremap_pmd(unsigned long addr)
  2.  
    {
  3.  
    pgd_t *base = __va(read_cr3_pa());
  4.  
    pgd_t *pgd = &base[pgd_index(addr)];
  5.  
    pud_t *pud = pud_offset(pgd, addr);
  6.  
    pmd_t *pmd = pmd_offset(pud, addr);
  7.  
    return pmd;
  8.  
    }

之后,我们bm_pte用零填充(早期的ioremap页面表条目)并调用pmd_populate_kernel函数:

  1.  
    pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
  2.  
    memset(bm_pte, 0, sizeof(bm_pte));
  3.  
    pmd_populate_kernel(&init_mm, pmd, bm_pte);

pmd_populate_kernel 带有三个参数:

  • init_mm-init进程的内存描述符(您可以在上一部分中了解它);
  • pmdioremap-Fixmaps 开头的页面中间目录;
  • bm_pte -早期ioremap页面表条目数组,其定义为:
static pte_t bm_pte[PAGE_SIZE/sizeof(pte_t)] __page_aligned_bss;

pmd_populate_kernel函数在arch / x86 / include / asm / pgalloc.h中定义,pmd使用给定的页面表项(bm_pte)填充作为参数提供的页面中间目录():

  1.  
    static inline void pmd_populate_kernel(struct mm_struct *mm,
  2.  
    pmd_t *pmd, pte_t *pte)
  3.  
    {
  4.  
    paravirt_alloc_pte(mm, __pa(pte) >> PAGE_SHIFT);
  5.  
    set_pmd(pmd, __pmd(__pa(pte) | _PAGE_TABLE));
  6.  
    }

在哪里set_pmd

#define set_pmd(pmdp, pmd)              native_set_pmd(pmdp, pmd)

并且native_set_pmd是:

  1.  
    static inline void native_set_pmd(pmd_t *pmdp, pmd_t pmd)
  2.  
    {
  3.  
    *pmdp = pmd;
  4.  
    }

就这样。早就ioremap可以使用了。early_ioremap_init函数中有几项检查,但是它们并不是那么重要,无论如何它们的初始化ioremap已完成。

早期ioremap的使用

一旦ioremap成功完成早期设置,我们就可以使用它。它提供两个功能:

  • early_ioremap
  • early_iounmap

用于将I / O物理地址映射/取消映射到虚拟地址。两种功能均取决于CONFIG_MMU配置选项。内存管理单元内存管理的特殊模块。该块的主要目的是将物理地址转换为虚拟地址。存储器管理单元pgdcr3控制寄存器中了解高级页表地址()。如果CONFIG_MMUoptions设置为n,则early_ioremap仅返回给定的物理地址,early_iounmap而不执行任何操作。如果CONFIG_MMUoption设置为y,则early_ioremap调用__early_ioremap将使用三个参数:

  • phys_addr-I/O存储器区域的基本物理地址,以映射到虚拟地址上;
  • size -I/O内存区域的大小;
  • prot -页表入口位。

首先__early_ioremap,我们将遍历所有早期的ioremap fixmap插槽,并搜索prev_map数组中的第一个空闲插槽。当我们找到它时,我们会记住它在slot变量中的编号并设置大小:

  1.  
    slot = -1;
  2.  
    for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
  3.  
    if (!prev_map[i]) {
  4.  
    slot = i;
  5.  
    break;
  6.  
    }
  7.  
    }
  8.  
    ...
  9.  
    ...
  10.  
    ...
  11.  
    prev_size[slot] = size;
  12.  
    last_addr = phys_addr + size - 1;

在下一个spte中,我们可以看到以下代码:

  1.  
    offset = phys_addr & ~PAGE_MASK;
  2.  
    phys_addr &= PAGE_MASK;
  3.  
    size = PAGE_ALIGN(last_addr + 1) - phys_addr;

在这里,我们PAGE_MASK用于清除phys_addr除前12位之外的所有位。PAGE_MASK宏定义为:

#define PAGE_MASK       (~(PAGE_SIZE-1))

我们知道页面的大小是4096字节或1000000000000二进制。PAGE_SIZE - 1将是111111111111,但使用~,我们将获得000000000000,但是使用时,~PAGE_MASK我们将111111111111再次获得。在第二行中,我们执行相同的操作,但是清除了前12位,并在第三行中获得了页面对齐的区域大小。我们获得了对齐的区域,现在我们需要获取新ioremap区域所占据的页面数,并fixed_addresses在接下来的步骤中计算出修正映射索引:

  1.  
    nrpages = size >> PAGE_SHIFT;
  2.  
    idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;

现在,我们可以fix-mapped使用给定的物理地址填充区域。在循环的每次迭代中,我们__early_set_fixmaparch/x86/mm/ioremap.c调用该函数,将给定的物理地址增加页面大小(即4096字节),并更新addresses索引和页面数:

  1.  
    while (nrpages > 0) {
  2.  
    __early_set_fixmap(idx, phys_addr, prot);
  3.  
    phys_addr += PAGE_SIZE;
  4.  
    --idx;
  5.  
    --nrpages;
  6.  
    }

__early_set_fixmap函数使用以下命令获取bm_pte给定物理地址的页表条目(存储在中,请参见上文):

pte = early_ioremap_pte(addr);

在下一步中,early_ioremap_pte我们使用pgprot_val宏检查给定的页面标志,并根据给定的标志进行调用set_ptepte_clear

  1.  
    if (pgprot_val(flags))
  2.  
    set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
  3.  
    else
  4.  
    pte_clear(&init_mm, addr, pte);

如您在上方所见,我们将FIXMAP_PAGE_IO标记传递给__early_ioremapFIXMPA_PAGE_IO扩展为:

(__PAGE_KERNEL_EXEC | _PAGE_NX)

标记,因此我们调用set_pte函数来设置页表条目,该条目的工作方式与set_pmdPTE相同(参见上面的内容)。PTEs在循环中进行所有设置后,我们现在可以看一下该__flush_tlb_one函数的调用:

__flush_tlb_one(addr);

此函数在arch/x86/include/asm/tlbflush.h定义,并根据的值进行调用__flush_tlb_single或:__flush_tlbcpu_has_invlpg

  1.  
    static inline void __flush_tlb_one(unsigned long addr)
  2.  
    {
  3.  
    if (cpu_has_invlpg)
  4.  
    __flush_tlb_single(addr);
  5.  
    else
  6.  
    __flush_tlb();
  7.  
    }

__flush_tlb_one函数使TLB中的给定地址无效。如您所见,我们更新了分页结构,但TLB没有通知更改,因此我们需要手动进行此操作。有两种方法可以做到这一点。首先是更新cr3控制寄存器,然后__flush_tlb函数执行此操作:

native_write_cr3(__native_read_cr3());

第二种方法是使用invlpg指令来使TLB条目无效。让我们看一下__flush_tlb_one实现。如您所见,首先进行功能检查cpu_has_invlpg,其定义为:

  1.  
    #if defined(CONFIG_X86_INVLPG) || defined(CONFIG_X86_64)
  2.  
    # define cpu_has_invlpg 1
  3.  
    #else
  4.  
    # define cpu_has_invlpg (boot_cpu_data.x86 > 3)
  5.  
    #endif

如果CPU支持该invlpg指令,我们将调用__flush_tlb_single扩展为以下内容的宏__native_flush_tlb_single

  1.  
    static inline void __native_flush_tlb_single(unsigned long addr)
  2.  
    {
  3.  
    asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
  4.  
    }

或调用__flush_tlb,它只会更新cr3我们所看到的寄存器。完成此步骤后,__early_set_fixmap功能执行完毕,我们可以返回到__early_ioremap实现。在为给定地址设置了fixmap区域后,我们需要prev_map使用slot索引将I / O Re-mapped区域的基本虚拟地址保存在以下位置:

prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);

并退回。

第二个函数early_iounmap取消映射I/O内存区域。此函数采用两个参数:基地址和I/O区域大小,通常看起来与十分相似early_ioremap。它还会通过Fixmap插槽,并查找具有给定地址的插槽。之后,它获取fixmap插槽的索引并进行调用__late_clear_fixmap__early_set_fixmap根据其after_paging_init值进行选择。它的调用方式__early_set_fixmap有一个不同early_ioremap:作为物理地址early_iounmap传递zero。最后,它将I / O存储区的地址设置为NULL

prev_map[slot] = NULL;

这就是fixmaps和有关的ioremap。当然,这部分内容并不涵盖的所有功能ioremap,仅涵盖早期的ioremap,但也包含普通的ioremap。但是在更详细地研究之前,我们需要知道更多的事情。

相关阅读

linux内存管理:kmap、vmap、ioremap

ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)

Linux内存管理:分页机制

Linux内存管理:内存描述之内存节点node

Linux内存管理:内存描述之内存区域zone

Linux内存管理:内存描述之内存页面page

Linux内存管理:内存描述之高端内存

内存管理:Linux Memory Management:MMU、段、分页、PAE、Cache、TLB

Linux内存管理:MMU那些事儿

【作者】张昺华
【大饼教你学系列】https://edu.csdn.net/course/detail/10393
【新浪微博】 张昺华--sky
【twitter】 @sky2030_
【微信公众号】 张昺华
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
原文地址:https://www.cnblogs.com/sky-heaven/p/15071402.html