23、内核内存管理详解

内核内存管理详解

1. I/O端口访问

在完成区域预留后,可使用以下函数访问端口:

u8 inb(unsigned long addr)
u16 inw(unsigned long addr)
u32 inl(unsigned long addr)

这些函数分别用于读取8位、16位或32位大小的端口。还有用于写入数据的函数:

void outb(u8 b, unsigned long addr)
void outw(u16 b, unsigned long addr)
void outl(u32 b, unsigned long addr)

这些函数将8位、16位或32位的数据写入指定地址的端口。

PIO(Programmed I/O)使用不同的指令集来访问I/O端口或MMIO(Memory-Mapped I/O),这是一个缺点,因为PIO完成相同任务所需的指令比普通内存访问更多。例如,在MMIO中,1位测试只需一条指令,而PIO需要先将数据读入寄存器再进行位测试,这需要多条指令。

2. MMIO设备访问

MMIO设备的内存映射I/O与内存位于同一地址空间。内核使用通常由RAM使用的部分地址空间(实际上是HIGH_MEM)来映射设备寄存器,这样在该地址处就不是真正的内存(即RAM),而是I/O设备。因此,与I/O设备通信就像对专门用于该I/O设备的内存地址进行读写操作。

例如,如果需要访问i.MX6的IPU - 2分配的4MB内存映射空间(从0x02A00000到0x02DFFFFF),CPU(通过MMU)可能会分配给我地址范围0x10000000到0x10400000,当然这是虚拟地址。这不会消耗物理RAM(除了构建和存储页表项),只是占用地址空间,这意味着内核将不再使用这个虚拟内存范围来映射RAM。现在,在这个地址范围内的任何读写操作(例如0x10000004)都将被重定向到IPU - 2设备。

与PIO类似,有MMIO函数用于告知内核我们使用内存区域的意图,这只是纯粹的预留操作,函数如下:

struct resource* request_mem_region(unsigned long start,
                                    unsigned long len, char *name)
void release_mem_region(unsigned long start, unsigned long len)

可以通过读取 /proc/iomem 文件的内容来显示系统中实际正在使用的内存区域。

在访问内存区域之前(成功请求之后),必须通过调用特定于架构的函数将该区域映射到内核地址空间(这些函数利用MMU来构建页表,因此不能从中断处理程序中调用)。这些函数是 ioremap() iounmap() ,它们还处理缓存一致性问题:

void __iomem *ioremap(unsigned long phys_add, unsigned long size)
void iounmap(void __iomem *addr)

ioremap() 返回一个指向映射区域起始位置的 __iomem void指针。不要试图直接解引用这样的指针。内核提供了访问 ioremap 映射内存的函数:

unsigned int ioread8(void __iomem *addr);
unsigned int ioread16(void __iomem *addr);
unsigned int ioread32(void __iomem *addr);
void iowrite8(u8 value, void __iomem *addr);
void iowrite16(u16 value, void __iomem *addr);
void iowrite32(u32 value, void __iomem *addr);

ioremap vmalloc 一样会构建新的页表,但它实际上并不分配任何内存,而是返回一个特殊的虚拟地址,用于访问指定的物理地址范围。在32位系统中,MMIO占用物理内存地址空间来为内存映射I/O设备创建映射是一个缺点,因为这会阻止系统将被占用的内存用于一般的RAM用途。

3. __iomem cookie

__iomem 是内核使用的一个cookie,用于Sparse(内核使用的语义检查器,用于查找可能的编码错误)。要利用Sparse提供的功能,应在编译内核时启用它;如果未启用, __iomem cookie将被忽略。

在命令行中使用 C=1 可以启用Sparse,但需要先在系统上安装Sparse:

sudo apt-get install sparse

例如,在构建模块时,可以使用:

make -C $KPATH M=$PWD C=1 modules

或者,如果 makefile 编写良好,只需输入:

make C=1

__iomem 在内核中的定义如下:

#define __iomem    __attribute__((noderef, address_space(2)))

它可以保护我们避免使用有缺陷的驱动程序进行I/O内存访问。为所有I/O访问添加 __iomem 也是一种更严格的编程方式。由于在具有MMU的系统中,I/O访问也是通过虚拟内存进行的,这个cookie可以防止我们使用绝对物理地址,要求我们使用 ioremap() ,它将返回一个带有 __iomem cookie标记的虚拟地址:

void __iomem *ioremap(phys_addr_t offset, unsigned long size);

因此,我们可以使用专门的函数,如 ioreadX() iowriteX() 。不建议使用 readl() / writel() 函数,因为它们不进行完整性检查,安全性较低(不需要 __iomem ),而 ioreadX() / iowriteX() 系列函数只接受 __iomem 地址。

此外, noderef 是Sparse使用的一个属性,用于确保程序员不会解引用 __iomem 指针。即使在某些架构上可能可行,但不鼓励这样做,应使用专门的 ioreadX() / iowriteX() 函数,它具有可移植性,适用于所有架构。以下是Sparse在解引用 __iomem 指针时的警告示例:

#define BASE_ADDR 0x20E01F8
void * _addrTX = ioremap(BASE_ADDR, 8);

Sparse会因为类型初始化错误而发出警告:

warning: incorrect type in initializer (different address spaces)
expected void *_addrTX
got void [noderef] <asn:2>*

或者:

u32 __iomem* _addrTX = ioremap(BASE_ADDR, 8);
*_addrTX = 0xAABBCCDD; /* bad. No dereference */
pr_info("%x\n", *_addrTX); /* bad. No dereference */

Sparse仍然会发出警告:

Warning: dereference of noderef expression

而下面的示例会让Sparse满意:

void __iomem* _addrTX = ioremap(BASE_ADDR, 8);
iowrite32(0xAABBCCDD, _addrTX);
pr_info("%x\n", ioread32(_addrTX));

需要记住的两条规则是:
- 始终在需要的地方使用 __iomem ,无论是作为返回类型还是参数类型,并使用Sparse确保正确使用。
- 不要解引用 __iomem 指针,应使用专门的函数。

4. 内存(重新)映射

内核内存有时需要重新映射,可能是从内核空间到用户空间,也可能是从内核空间到内核空间。常见的用例是将内核内存重新映射到用户空间,但也有其他情况,例如需要访问高端内存。

5. kmap函数

Linux内核将其地址空间的896MB永久映射到物理内存的低896MB(低内存)。在4GB的系统中,内核只剩下128MB来映射其余的3.2GB物理内存(高内存)。由于永久的一对一映射,低内存可以由内核直接寻址。当涉及到高内存(896MB以上的内存)时,内核必须将请求的高内存区域映射到其地址空间,前面提到的128MB就是专门为此预留的。用于执行此操作的函数是 kmap() ,它用于将给定的页面映射到内核地址空间:

void *kmap(struct page *page);

page 是指向要映射的 struct page 结构的指针。当分配一个高内存页面时,它不能直接寻址。必须调用 kmap() 函数将高内存临时映射到内核地址空间,该映射将一直持续到调用 kunmap()

void kunmap(struct page *page);

这里的“临时”是指,一旦不再需要映射,就应立即取消映射。请记住,128MB不足以映射3.2GB的内存。最佳编程实践是在不再需要时取消高内存映射。因此,在每次访问高内存页面时都需要使用 kmap() - kunmap() 序列。

这个函数对高内存和低内存都适用。也就是说,如果页面结构位于低内存中,则只返回该页面的虚拟地址(因为低内存页面已经有永久映射)。如果页面属于高内存,则在内核的页表中创建一个永久映射并返回地址:

void *kmap(struct page *page)
{
    BUG_ON(in_interrupt());
    if (!PageHighMem(page))
        return page_address(page);
    return kmap_high(page);
}

下面是一个简单的流程图展示kmap和kunmap的使用流程:

graph TD;
    A[分配高内存页面] --> B[调用kmap];
    B --> C[访问高内存页面];
    C --> D[调用kunmap];
6. 映射内核内存到用户空间

映射物理地址是非常有用的功能,特别是在嵌入式系统中。有时,可能希望与用户空间共享部分内核内存。如前所述,CPU在用户空间运行时处于非特权模式。为了让进程访问内核内存区域,需要将该区域重新映射到进程地址空间。

6.1 使用remap_pfn_range

remap_pfn_range() 函数将物理内存(通过内核逻辑地址)映射到用户空间进程,对于实现 mmap() 系统调用特别有用。

在对文件(无论是设备文件还是其他文件)调用 mmap() 系统调用后,CPU将切换到特权模式并运行相应的 file_operations.mmap() 内核函数,该函数又会调用 remap_pfn_range() 。映射区域的内核页表项(PTE)将被导出并提供给进程,当然会有不同的保护标志。进程的VMA(Virtual Memory Area)列表将使用新的VMA条目(具有适当的属性)进行更新,该条目将使用PTE来访问相同的内存。

因此,内核不是通过复制来浪费内存,而是直接复制PTE。然而,内核和用户空间的PTE具有不同的属性。 remap_pfn_range() 的原型如下:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
             unsigned long pfn, unsigned long size, pgprot_t flags);

成功调用将返回0,失败则返回负错误码。 remap_pfn_range() 的大多数参数在调用 mmap() 方法时提供:
- vma :这是在 file_operations.mmap() 调用时内核提供的虚拟内存区域,对应于要进行映射的用户进程的VMA。
- addr :这是VMA应该开始的用户虚拟地址( vma->vm_start ),这将导致从 addr addr + size 的虚拟地址范围进行映射。
- pfn :表示要映射的内核内存区域的PFN(Page Frame Number),它对应于物理地址右移 PAGE_SHIFT 位的结果。需要考虑VMA偏移(映射必须开始的对象内的偏移)来计算PFN。由于VMA结构的 vm_pgoff 字段以页面数的形式包含偏移值,通过 PAGE_SHIFT 左移即可提取以字节为单位的偏移: offset = vma->vm_pgoff << PAGE_SHIFT 。最后, pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT
- size :这是以字节为单位的要重新映射的区域的大小。
- prot :表示新VMA请求的保护。驱动程序可以修改默认值,但应使用 vma->vm_page_prot 中的值作为基础,使用OR运算符,因为其中一些位已经由用户空间设置。一些标志如下:
- VM_IO :指定设备的内存映射I/O。
- VM_DONTCOPY :告诉内核在fork时不复制此VMA。
- VM_DONTEXPAND :防止VMA通过 mremap(2) 扩展。
- VM_DONTDUMP :防止VMA包含在核心转储中。

如果与I/O内存一起使用,可能需要修改此值以禁用缓存( vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); )。

6.2 使用io_remap_pfn_range

当涉及将I/O内存映射到用户空间时,前面讨论的 remap_pfn_range() 函数不再适用。在这种情况下,应使用 io_remap_pfn_range() ,其参数与 remap_pfn_range() 相同,唯一的区别是PFN的来源。其原型如下:

int io_remap_page_range(struct vm_area_struct *vma,
                        unsigned long virt_addr,
                        unsigned long phys_addr,
                        unsigned long size, pgprot_t prot);

在尝试将I/O内存映射到用户空间时,不需要使用 ioremap() ioremap() 用于内核目的(将I/O内存映射到内核地址空间),而 io_remap_pfn_range 用于用户空间目的。

只需将实际的物理I/O地址(右移 PAGE_SHIFT 以生成PFN)直接传递给 io_remap_pfn_range() 。即使在某些架构中, io_remap_pfn_range() 被定义为 remap_pfn_range() ,但在其他架构中并非如此。出于可移植性的考虑,应仅在PFN参数指向RAM的情况下使用 remap_pfn_range() ,在 phys_addr 指I/O内存的情况下使用 io_remap_pfn_range()

7. mmap文件操作

内核的 mmap 函数是 struct file_operations 结构的一部分,当用户执行 mmap(2) 系统调用将物理内存映射到用户虚拟地址时会执行该函数。内核通过通常的指针解引用将对该映射内存区域的任何访问转换为文件操作。甚至可以将设备物理内存直接映射到用户空间(见 /dev/mem )。本质上,对内存的写入就像写入文件一样,这只是一种更方便的调用 write() 的方式。

通常,出于安全考虑,用户空间进程不能直接访问设备内存。因此,用户空间进程使用 mmap() 系统调用请求内核将设备映射到调用进程的虚拟地址空间。映射完成后,用户空间进程可以通过返回的地址直接写入设备内存。

mmap 系统调用的声明如下:

mmap (void *addr, size_t len, int prot,
       int flags, int fd, ff_t offset);

驱动程序应定义 mmap 文件操作( file_operations.mmap )以支持 mmap(2) 。从内核的角度来看,驱动程序的文件操作结构( struct file_operations 结构)中的 mmap 字段具有以下原型:

int (*mmap) (struct file *filp, struct vm_area_struct *vma);

这里:
- filp :是指向驱动程序的打开设备文件的指针,它是 fd 参数转换的结果。
- vma :由内核分配并作为参数提供,它是指向用户进程的VMA的指针,映射将在此处进行。为了理解内核如何创建新的VMA,让我们回顾一下 mmap(2) 系统调用的原型:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

该函数的参数会影响VMA的一些字段:
| 参数 | 对VMA的影响 |
| ---- | ---- |
| addr | 用户空间的虚拟地址,映射应从此处开始,影响 vma->vm_start 。如果指定为 NULL (最具可移植性的方式),则自动确定正确的地址。 |
| length | 指定映射的长度,间接影响 vma->vm_end 。VMA的大小始终是 PAGE_SIZE 的倍数。如果 length <= PAGE_SIZE ,则 vma->vm_end - vma->vm_start == PAGE_SIZE ;如果 PAGE_SIZE < length <= (N * PAGE_SIZE) ,则 vma->vm_end - vma->vm_start == (N * PAGE_SIZE) 。 |
| prot | 影响VMA的权限,驱动程序可以在 vma->vm_pro 中找到这些权限。如前所述,驱动程序可以更新这些值,但不能更改它们。 |
| flags | 确定映射的类型,驱动程序可以在 vma->vm_flags 中找到。映射可以是私有或共享的。 |
| offset | 指定映射区域内的偏移,从而影响 vma->vm_pgoff 的值。 |

8. 在内核中实现mmap

由于用户空间代码不能访问内核内存, mmap() 函数的目的是导出一个或多个受保护的内核页表项(对应于要映射的内存),并复制用户空间页表,移除内核标志保护,并设置权限标志,以便用户可以在不需要特殊权限的情况下访问与内核相同的内存。

编写 mmap 文件操作的步骤如下:
1. 获取映射偏移并检查是否超出缓冲区大小:

unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
if (offset >= buffer_size)
    return -EINVAL;
  1. 检查映射大小是否大于缓冲区大小:
unsigned long size = vma->vm_end - vma->vm_start;
if (size > (buffer_size - offset))
    return -EINVAL;
  1. 获取对应于缓冲区偏移位置所在页面的PFN:
unsigned long pfn;
/* we can use page_to_pfn on the struct page structure
 * returned by virt_to_page
 */
/* pfn = page_to_pfn (virt_to_page (buffer + offset)); */
/* Or make PAGE_SHIFT bits right-shift on the physical
 * address returned by virt_to_phys
 */
pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT;
  1. 设置适当的标志,无论是否存在I/O内存:
    • 使用 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot) 禁用缓存。
    • 设置 VM_IO 标志: vma->vm_flags |= VM_IO
    • 防止VMA交换出去: vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP 。在内核版本低于3.7时,应仅使用 VM_RESERVED 标志。

综上所述,内核内存管理涉及多个方面,包括I/O端口访问、MMIO设备访问、内存映射以及将内核内存映射到用户空间等操作。在实际编程中,需要根据具体需求正确使用各种函数和机制,遵循相关的规则和最佳实践,以确保系统的稳定性和安全性。

内核内存管理详解

9. 总结与最佳实践

在前面的内容中,我们详细探讨了内核内存管理的多个关键方面,从I/O端口和MMIO设备的访问,到内存的映射和重新映射,以及将内核内存映射到用户空间的具体实现。为了帮助大家更好地理解和应用这些知识,下面总结一些关键要点和最佳实践。

9.1 关键要点总结
  • I/O端口访问 :使用 inb inw inl 进行读取, outb outw outl 进行写入。PIO在完成相同任务时比普通内存访问需要更多指令。
  • MMIO设备访问 :通过 request_mem_region release_mem_region 预留内存区域,使用 ioremap iounmap 进行映射和取消映射,使用 ioreadX iowriteX 函数进行读写操作。
  • __iomem cookie :用于防止错误的I/O内存访问,使用Sparse进行检查,遵循不直接解引用 __iomem 指针的规则。
  • 内存映射 kmap kunmap 用于将高内存页面映射到内核地址空间,使用时要遵循及时取消映射的原则。
  • 映射内核内存到用户空间 :使用 remap_pfn_range io_remap_pfn_range 将物理内存映射到用户空间,根据情况设置合适的保护标志。
  • mmap文件操作 :驱动程序需要实现 mmap 函数,根据 mmap 系统调用的参数设置VMA的相关字段。
9.2 最佳实践
  • 及时释放资源 :在使用 request_mem_region 预留内存区域后,不再使用时要及时调用 release_mem_region 释放。对于 kmap 映射的高内存页面,使用完后要及时调用 kunmap 取消映射。
  • 使用Sparse进行检查 :在开发过程中,启用Sparse工具,确保正确使用 __iomem ,避免直接解引用 __iomem 指针,提高代码的安全性和可移植性。
  • 合理设置保护标志 :在使用 remap_pfn_range io_remap_pfn_range 时,根据实际情况设置合适的保护标志,如禁用缓存、防止VMA交换等。
  • 遵循编程规范 :在实现 mmap 函数时,按照步骤进行偏移和大小检查,正确计算PFN,确保用户空间能够安全访问内核内存。
10. 常见问题及解决方案

在进行内核内存管理编程时,可能会遇到一些常见问题,下面列举一些并给出解决方案。

10.1 内存映射失败
  • 问题描述 :调用 ioremap remap_pfn_range io_remap_pfn_range 等映射函数时返回错误。
  • 可能原因 :内存区域未正确预留、物理地址无效、权限不足等。
  • 解决方案 :检查是否使用 request_mem_region 正确预留了内存区域,确保物理地址和大小参数正确,检查是否有足够的权限进行映射操作。
10.2 解引用 __iomem 指针错误
  • 问题描述 :直接解引用 __iomem 指针,导致Sparse发出警告或程序出现异常。
  • 可能原因 :未遵循不直接解引用 __iomem 指针的规则,使用了错误的访问方式。
  • 解决方案 :使用专门的 ioreadX iowriteX 函数进行读写操作,避免直接解引用 __iomem 指针。
10.3 高内存映射未及时取消
  • 问题描述 :使用 kmap 映射高内存页面后,未及时调用 kunmap 取消映射,导致内存资源耗尽。
  • 可能原因 :编程时未遵循及时取消映射的原则,逻辑错误。
  • 解决方案 :在每次访问高内存页面时,使用 kmap - kunmap 序列,确保映射在不需要时及时取消。
11. 示例代码综合应用

为了更好地理解内核内存管理的实际应用,下面给出一个综合示例代码,展示如何使用上述介绍的函数进行内存映射和访问。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define BASE_ADDR 0x20E01F8
#define MAP_SIZE 8

static struct resource *res;
static void __iomem *mapped_addr;

static int __init my_module_init(void)
{
    // 预留内存区域
    res = request_mem_region(BASE_ADDR, MAP_SIZE, "my_module");
    if (!res) {
        printk(KERN_ERR "Failed to request memory region\n");
        return -EBUSY;
    }

    // 映射内存区域
    mapped_addr = ioremap(BASE_ADDR, MAP_SIZE);
    if (!mapped_addr) {
        printk(KERN_ERR "Failed to ioremap memory region\n");
        release_mem_region(BASE_ADDR, MAP_SIZE);
        return -ENOMEM;
    }

    // 写入数据
    iowrite32(0xAABBCCDD, mapped_addr);

    // 读取数据
    unsigned int value = ioread32(mapped_addr);
    printk(KERN_INFO "Read value: 0x%x\n", value);

    return 0;
}

static void __exit my_module_exit(void)
{
    // 取消映射
    iounmap(mapped_addr);

    // 释放内存区域
    release_mem_region(BASE_ADDR, MAP_SIZE);

    printk(KERN_INFO "Module unloaded\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module for memory mapping");

这个示例代码展示了如何预留内存区域、进行内存映射、写入和读取数据,以及在模块卸载时取消映射和释放内存区域。

12. 未来发展趋势

随着计算机技术的不断发展,内核内存管理也在不断演进。以下是一些可能的未来发展趋势:

  • 更高效的内存映射机制 :为了满足日益增长的内存需求和提高系统性能,未来可能会出现更高效的内存映射算法和机制,减少映射开销。
  • 增强的安全性 :随着系统安全要求的提高,内核内存管理将更加注重安全性,例如加强对内存访问的权限控制和数据保护。
  • 支持新的硬件架构 :随着新的硬件架构的出现,内核内存管理需要不断适应和支持这些新架构,以充分发挥硬件的性能。

下面是一个简单的流程图展示内核内存管理的主要流程:

graph LR;
    A[开始] --> B[I/O端口或MMIO设备访问];
    B --> C{是否需要内存映射};
    C -- 是 --> D[预留内存区域];
    D --> E[进行内存映射];
    E --> F[读写操作];
    F --> G[取消映射];
    G --> H[释放内存区域];
    C -- 否 --> F;
    H --> I[结束];

通过本文的介绍,我们对内核内存管理有了更深入的了解。在实际开发中,要根据具体需求合理运用各种内存管理技术,遵循最佳实践,确保系统的稳定性和安全性。同时,关注未来的发展趋势,不断学习和适应新的技术变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值