【嵌入式环境下linux内核及驱动学习笔记-(10-内核内存管理)】

本文详细介绍了Linux内核如何管理内存,包括页和区的概念,以及kmalloc、__get_free_pages、vmalloc等内存分配函数的使用。此外,还讨论了IO访问方法,如IO端口法和IO内存法,以及ioremap和iounmap在设备内存映射中的作用。最后,提到了将设备地址映射到用户空间的mmap函数及其在内核层的实现。
摘要由CSDN通过智能技术生成

对于包含MMU(内存管理单元)的处理器而言,linux系统以虚拟内存的方式为每个进程分配最大4GB的内存。这真的4GB的内存空间被分为两个部分–用户空间 与 内核空间。用户空间地地址分布为0~3GB,剩下的3 ~ 4GB 为内核空间。如下图。

在这里插入图片描述
用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只有通过系统调用(代表用户进程在内核态执行)等方式才可以访问到内核空间。

每个进程的用户空间都是独立的,互不相干,用户进程各自有不同的页表。而内核空间是由内核负责映射,内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其它程序。

1、linux内核管理内存

内核把物理内存以页(page)为单位进行划分。内存管理单元MMU也以页为单位进行处理,因此虚拟内存也把页做为最小的单位。

1.1 页

一般,对于32位系统,一页有4Kbyte,对于64位系统,一页有8Kbyte。这就间味着,如果一个32位系统有1G的物理内存,则按4K一页划分,则物理内存会被划分为262144页。

内核用struct page结构体表示系统的每个物理页,以下是一个简化版的page结构,用于说明其功能:

#incluce <linux/mm_types.h>
struct page{
	unsigned long flags;
	atomic_t   	_count;
	atomic_t 	_mapcount;
	unsigned long 	private;
	struct address_space	*mapping;
	pgoff_t	index;
	struct list_head	lru;
	void	*virtual;
}

flags:用来存放页的状态,每一个bit单独表示一种状态,最多可以表示32种状态,这些标志定义在<linux/page-flags.h>中。这些状态包括页是不是脏的,是不是被锁定在内存中。

enum pageflags {
	PG_locked,		/* Page is locked. Don't touch. */
	PG_error,
	PG_referenced,
	PG_uptodate,
	PG_dirty,
	PG_lru,
	PG_active,
	PG_waiters,		/* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
	PG_slab,
	PG_owner_priv_1,	/* Owner use. If pagecache, fs may use*/
	PG_arch_1,
	PG_reserved,
	PG_private,		/* If pagecache, has fs-private data */
	PG_private_2,		/* If pagecache, has fs aux data */
	PG_writeback,		/* Page is under writeback */
	PG_head,		/* A head page */
	PG_mappedtodisk,	/* Has blocks allocated on-disk */
	PG_reclaim,		/* To be reclaimed asap */
	PG_swapbacked,		/* Page is backed by RAM/swap */
	PG_unevictable,		/* Page is "unevictable"  */
#ifdef CONFIG_MMU
	PG_mlocked,		/* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
	PG_uncached,		/* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
	PG_hwpoison,		/* hardware poisoned page. Don't touch */
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
	PG_young,
	PG_idle,
#endif
	__NR_PAGEFLAGS,

	/* Filesystems */
	PG_checked = PG_owner_priv_1,

	/* SwapBacked */
	PG_swapcache = PG_owner_priv_1,	/* Swap page: swp_entry_t in private */

	/* Two page bits are conscripted by FS-Cache to maintain local caching
	 * state.  These bits are set on pages belonging to the netfs's inodes
	 * when those inodes are being locally cached.
	 */
	PG_fscache = PG_private_2,	/* page backed by cache */

	/* XEN */
	/* Pinned in Xen as a read-only pagetable page. */
	PG_pinned = PG_owner_priv_1,
	/* Pinned as part of domain save (see xen_mm_pin_all()). */
	PG_savepinned = PG_dirty,
	/* Has a grant mapping of another (foreign) domain's page. */
	PG_foreign = PG_owner_priv_1,

	/* SLOB */
	PG_slob_free = PG_private,

	/* Compound pages. Stored in first tail page's flags */
	PG_double_map = PG_private_2,

	/* non-lru isolated movable page */
	PG_isolated = PG_reclaim,
};

_count:存放页的引用计数(即这一页被引用了多少次)。当计数值变为-1时,就说明这页没有被引用,就可以分配了。要调用page_count()函数进行检查,返回0表示页空闲。

virtual:是页的虚拟地址。指页在虚拟内存中的地址。

内核用page这一结构来管理系统中的所有的页。内核需要知道一个页是否空闲,页被谁拥有等。

1.2 区

由于硬件的一些特殊性,比如:

  • 某些硬件只能用某些特定的内存地址来执行DMA。
  • 一些体系结构的物理寻址范围比虚拟寻址范围大得多,这样就有一些内存不能永久地映射到内核空间上。

因此,内核把页划分为不同的区(zone)。内核使用区对具有相似特性的页进行分组。主要定义了四种区:
(以下区在linux/mmzone.h头文件中定义)

  • ZONE_DMA 这个区包含的页能用来执行DMA操作。
  • ZONE_DMA32 和ZONE_DMA类似,该区包含的页面只能被32位设备访问。
  • ZONE_NORMAL 这个区包含的都是能正常映射的页。
  • ZONE_HIGHEM 这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。

这里要注意的是,区的实际使用和分布是与体系结构相关的。

1.2.1 了解x86系统的内核地址映射区:

在这里插入图片描述

内核地址空间划分图:
1、3G~3G+896M 为 低端内存

  • 特点:低端区与物理内存是直接映射关系( 映射方法: 虚拟地址 = 3G + 物理地址)
  • 低端内存再细分为:ZONE_DMA、ZONE_NORMAL
  • 分配指令:
    低端内存区内分配内存区的分配指令:
    1. kmalloc:小内存分配,slab算法
    2. get_free_page:整页分配,2的n次方页,n最大为10

2、大于3G+896M 为 高端内存

  • 特点:虚拟地址连续,物理地址不连续
  • 可再细分为:vmalloc区、持久映射区、固定映射区
  • 在高端内存区分配内存块的分配方式:vmalloc

1.2.2 了解32位ARM系统的内核地址映射区:

在这里插入图片描述

2、内存存取

在linux内核空间中申请内存涉及的函数主要包括kmalloc()、__get_free_pages()和vamlloc()等。

Kmalloc和__get_free_pages申请的内存位于DMA和常规区域的映射区,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移。

vmalloc()在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc()申请的虚拟内存和物理内存之间与没有简单的换算关系。

2.1 kmalloc

kmalloc()是Linux内核中用于动态内存分配的函数。它的声明和头文件如下:

#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);

参数:

  • size:要分配的内存块的大小,以字节为单位。
  • flags:分配标志,指定分配的内存属性,如GFP_KERNEL表示分配内核内存等。在头文件**</include/linux/gfp.h>**中定义。

这些gfp_t类型中的宏表示的具体含义如下:

  • GFP_ATOMIC: 分配内能在中断/中断上下文中安全使用的内存,这种分配不会睡眠,因此比较适合在原子上下文中使用。
  • GFP_KERNEL: 分配内核内存,这是最常用的标志,允许睡眠和I/O。
  • GFP_KERNEL_ACCOUNT: 与GFP_KERNEL类似,但会对分配进行计数。
  • GFP_NOWAIT: 分配内存时不允许睡眠,要么立即分配成功,要么失败。
  • GFP_NOIO: 分配内存时不允许进行I/O操作。
  • GFP_NOFS: 分配内存时不允许触发文件系统操作。
  • GFP_USER: 分配用户内存,会睡眠和I/O。
  • GFP_DMA: 分配DMA可访问的内存,用于DMA传输。
  • GFP_DMA32: 只分配32位DMA可访问的内存。
  • GFP_HIGHUSER: 分配用户内存中高端内存部分。
  • GFP_HIGHHUSER_MOVABLE: 同GFP_HIGHUSER,分配的内存可以移动。

注意:
使用GFP_KERNEL标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNEL申请内存。所以此时驱动应当使用GFP_ATOMIC标志来申请内存。当使用GFP_ATOMIC标志申请内存时,若不存在空闲页,则不等待,直接返回。

返回值:
kmalloc()函数返回分配得到的内存块的地址,如果分配失败则返回NULL。

kmalloc()分配的内存属于伙伴系统(slub/slab分配器)管理的内存,这些内存会存放在缓存中,并在需要时分配给请求的进程或内核代码。这样可以提高内存分配和释放的效率,避免频繁的物理页分配和释放。

2.1.1 kfree

使用kmalloc分配的内存应该用kfree()释放。kfree()函数会释放ptr指向的内存空间,并将其归还给伙伴系统(slub/slab分配器)。
它的声明如下:

#include <linux/slab.h>
void kfree(const void *ptr);

参数:
ptr指向要释放的内存块的地址。

与kmalloc()类似,kfree()也是Linux内核中常用的内存管理函数,用于释放动态分配的内存。
使用kmalloc()和kfree()需要注意以下几点:

  1. 只能向kfree()传入kmalloc()分配的内存地址,否则会导致未定义行为。
  2. 同一块内存只能free一次,多次free同一地址会导致未定义行为。
  3. free已free的内存地址会导致未定义行为。
  4. kmalloc()的内存不会自动释放,必须手动调用kfree()释放。否则会导致内存泄漏。
  5. kfree()时传入的地址必须是kmalloc()的原始地址,不能在kmalloc()的基础上偏移一定字节后传入kfree()。
  6. kmalloc()和kfree()操作的内存必须是同一类型的(如内核空间和用户空间的内存不能混用)。

2.1.2 kzalloc

函数用于分配已清零的内存。

#include <linux/slab.h>
void *kzalloc(size_t size, gfp_t flags);

kzalloc()函数会分配size字节大小的内存,并在返回之前将分配的内存清零。它使用伙伴系统(slab/slub分配器)分配内存,所以返回的内存是未初始化的,需要清零后才能使用。

如果内存分配成功,kzalloc()会返回分配的内存地址。如果失败,则返回NULL。

参数:

  • size: 要分配内存的大小,以字节为单位。
  • flags: 分配条件,与kmalloc()相同,指定内存类型、是否可以睡眠等。

与kmalloc()相比,kzalloc()有以下不同:

  1. kzalloc()会在返回内存之前清零内存,kmalloc()返回的内存是未初始化的。
  2. 其他属性基本相同,如:
  • 使用相同的伙伴系统分配内存。
  • 返回的内存需要通过kfree()释放,否则会内存泄漏。
  • 传入kfree()的地址必须是kzalloc()的返回地址,否则导致未定义行为。
  • 同一内存不能重复释放,否则也会导致未定义行为。
  • 要及时调用kfree()释放内存,否则容易OOM。
  • 分配的内存类型必须匹配(内核空间与用户空间内存不能混用)。
    kzalloc()很适用于需要已清零内存的场景,可以保证返回的内存是干净的,以免产生未初始化变量等问题。除此之外,其性质与kmalloc()基本相同。

2.2 __get_free_page函数族

声明如下:

#include <linux/gfp.h>
unsigned long __get_free_page(gfp_t flags);

作用
__get_free_page()函数用于分配一个物理页面(page)的内存。

参数
参数flags指定分配的条件,与kmalloc()中的flags参数相同,用于指定分配类型、是否可以睡眠等。常用的值是GFP_KERNEL和GFP_USER。

**返回值 **
__get_free_page()函数会从伙伴系统中分配一个物理页面的内存,并返回该页面的虚拟地址。如果分配失败,则返回0。

注意:

由于__get_free_page()直接分配物理内存页面,所以它的性能略高于kmalloc(),但是分配的内存大小是固定的(一般为4KB),浪费可能比较大。所以,只有当需要较大块内存且性能要求较高时,才会选择__get_free_page()函数。

与__get_free_page()相关的其他函数有:

  • free_page(addr): 释放由__get_free_page()分配的内存页面,addr传入__get_free_page()的返回地址。
  • __get_dma_pages(): 分配DMA可访问的内存页面。
  • __get_high_pages(): 分配高端内存页面。
  • __get_low_pages(): 分配普通内存页面。
  • __get_vm_area(): 分配不连续的物理页面并映射为连续的虚拟地址空间,类似vmalloc()。
  • __get_free_pages():分配2的n次方页,返回指向第一页逻辑地址的指针。
  • get_zeroed_page():只分配一页,让其内容填充0,返回指向其逻辑地址的指针

需要注意,这些函数直接分配物理内存,所以使用不当很容易导致内存泄漏或未定义行为。 分配的内存大小也比较固定,不够灵活,因此只有在需要较大内存块且性能要求高的情况下才会选用。

2.2.1 free_page

函数用于释放通过__get_free_page()分配的单个物理内存页面。
它的声明如下:

#include <linux/gfp.h>
void free_page(unsigned long addr);

参数addr表示要释放的物理页面的虚拟地址,必须是__get_free_page()的返回值。
free_page()函数会释放addr指向的物理内存页面,并将其返还给伙伴系统。

2.2.2 __get_free_pages()

函数用于分配多个连续的物理内存页面。

#include <linux/gfp.h>
unsigned long __get_free_pages(gfp_t flags, unsigned int order);

__get_free_pages()函数会从伙伴系统中分配2的order次幂个连续页面的内存,并返回第一个页面的虚拟地址。如果分配失败,则返回0。

参数:

  • flags: 分配条件,与__get_free_page()相同。
  • order: 要分配页面的个数,以2的order次幂个页面为单位。例如order为2表示4个页面,order为3表示8个页面,以此类推。

__get_free_pages()函数允许分配较大的内存块,连续的多个页面。除此之外,其其他属性与__get_free_page()基本相同:

  1. 也是直接分配物理内存,性能高于kmalloc()但内存分配大小较固定。
  2. 使用不当也会导致内存泄漏和未定义行为,需要谨慎使用。
  3. 也有对应的free_pages()函数用于释放分配的内存。
  4. 也有其它类似函数,如__get_dma_pages()等。
    所以,__get_free_pages()函数在以下情况下会非常有用:
  5. 需要分配较大的连续内存块(多个物理页面)。
  6. 对性能有较高要求,不希望通过kmalloc()分配内存后再手动连接成较大块。
  7. 需要的内存大小可以通过order参数的2的n次幂大小很好满足。
    除此之外,对于更灵活或较小的内存分配,kmalloc()函数会更加适用。所以两者可以视情况而用,以达到最佳的效果。

2.2.3 free_pages

函数用于释放通过__get_free_pages()分配的多个连续物理内存页面。
它的声明如下:

#include <linux/gfp.h>
void free_pages(unsigned long addr, unsigned int order);

它在头文件中声明。
参数:

  • addr: 要释放的第一个物理页面的虚拟地址,必须是__get_free_pages()的返回值。
  • order: 要释放的页面个数,以2的order次幂个页面为单位,必须与__get_free_pages()传入的order参数大小相同。
    free_pages()函数会释放从addr开始的2的order次幂个物理内存页面,并将内存返还给伙伴系统。

2.2.4 get_zeroed_page

函数用于分配一个已清零的物理内存页面。

#include <linux/gfp.h>
unsigned long get_zeroed_page(gfp_t flags);

get_zeroed_page()功能与__get_free_page()基本相同,但是它会在返回内存页面之前,将页面清零。
参数
flags 指定分配条件,与__get_free_page()相同。

get_zeroed_page()会返回一个已清零的物理页面虚拟地址,如果分配失败则返回0。

与__get_free_page()一样,get_zeroed_page()也需要注意:

  1. 返回的内存页面需要手动通过free_page()释放,防止内存泄漏。
  2. 传入free_page()的地址必须是get_zeroed_page()的返回地址,否则会导致未定义行为。
  3. 同一内存页面不能重复free,否则也会导致未定义行为。
  4. 要积极释放物理内存页面,否则容易因分配和释放不均衡导致OOM。
  5. 内存页面类型必须匹配(内核空间与用户空间内存不能混用)。
  6. 直接操作物理内存,使用不当会导致各种问题,需要特别小心。
    get_zeroed_page()适用于需要已清零内存的场景,除此之外,其性质与__get_free_page()基本相同。

与get_zeroed_page()对应,我们还有:

  • get_zeroed_pages(): 分配多个连续的已清零物理内存页面。
  • kzalloc(): 在用户空间分配已清零内存,更加安全灵活,适用于绝大多数场景。

2.2.5 __get_dma_pages

函数用于分配DMA可访问的物理内存页面。

#include <linux/gfp.h>
unsigned long __get_dma_pages(gfp_t flags, unsigned int order);

__get_dma_pages()函数会从伙伴系统中分配2的order次幂个DMA可访问的物理内存页面,并返回第一个页面的虚拟地址。如果分配失败,则返回0。
参数:

  • flags: 分配条件,与__get_free_pages()相同,指定是否可以睡眠等。
  • order: 要分配的页面个数,以2的order次幂个页面为单位。

与__get_free_pages()类似,__get_dma_pages()也需要注意:

  1. 返回的内存需要通过free_pages()释放,以防止内存泄漏。
  2. 传入free_pages()的地址和order必须与__get_dma_pages()相同,否则会导致未定义行为。
  3. 同一内存不能重复释放,否则也会导致未定义行为。
  4. 要及时调用free_pages()释放内存,否则容易OOM。
  5. __get_dma_pages()分配的内存只能用于DMA,不能在非DMA场景下使用。

除__get_dma_pages()之外,我们还有:

  • get_dma_pages(): 分配已清零的DMA内存页面。
  • dma_alloc_coherent(): 在用户空间分配DMA内存,更加安全和实用。
    根据需求的不同,可以选择底层的__get_dma_pages()或是更高层的dma_alloc_coherent(),得到最佳效果。

2.2.6 __get_high_pages()

函数用于分配高端内存(high memory)的物理页面。

#include <linux/gfp.h>
unsigned long __get_high_pages(gfp_t flags, unsigned int order);

__get_high_pages()函数会从伙伴系统中分配2的order次幂个高端内存的物理页面,并返回第一个页面的虚拟地址。如果分配失败,则返回0。
参数:

  • flags: 分配条件,与__get_free_pages()相同,指定是否可以睡眠等。
  • order: 要分配页面的个数,以2的order次幂个页面为单位。

高端内存的物理地址范围较大,超出普通内存的直接映射范围,所以访问高端内存会较慢。但其优点是较少使用,较容易分配较大的连续内存块。

__get_high_pages()函数在以下情况下很有用:

  1. 需要分配较大块内存(通过较大的order),并且对性能要求不高。
  2. 普通内存空间不足,需要使用高端内存。
  3. 仅用于I/O映射等不需要频繁访问的场景。

与其他类似函数一样,__get_high_pages()也需要注意:

1、返回的内存需要通过free_pages()释放,防止内存泄漏。
2、传入free_pages()的地址和order必须正确,否则导致未定义行为。
3、同一内存不能重复释放,否则也会导致未定义行为。
4、要及时释放分配的内存,否则容易OOM。

除__get_high_pages()之外,我们还有:

  • get_high_pages(): 分配已清零的高端内存页面。
  • kmalloc(GFP_HIGHUSER): 在用户空间分配高端内存,更加安全和实用。

2.2.7 __get_low_pages()

函数用于分配普通内存(low memory)的物理页面。

#include <linux/gfp.h> 
unsigned long __get_low_pages(gfp_t flags, unsigned int order);

__get_low_pages()函数会从伙伴系统中分配2的order次幂个普通内存的物理页面,并返回第一个页面的虚拟地址。如果分配失败,则返回0。
参数:

  • flags: 分配条件,与__get_free_pages()相同,指定是否可以睡眠等。
  • order: 要分配页面的个数,以2的order次幂个页面为单位。

普通内存的物理地址范围较小,可以直接映射,所以访问速度较快。但其缺点是空间较小,不易分配较大的连续内存块。

__get_low_pages()函数在以下情况下很有用:

  1. 需要较快速度访问内存,对性能有要求。
  2. 普通内存空间足够,不需要使用高端内存。
  3. 访问频繁,需要较快的速度。

与其他类似函数一样,__get_low_pages()也需要注意:

  1. 返回的内存需要通过free_pages()释放,防止内存泄漏。
  2. 传入free_pages()的地址和order必须正确,否则导致未定义行为。
  3. 同一内存不能重复释放,否则也会导致未定义行为。
  4. 要及时释放分配的内存,否则容易OOM。

除__get_low_pages()之外,我们还可以使用:

  • get_low_pages(): 分配已清零的普通内存页面。
  • kmalloc(): 在用户空间分配普通内存,更加安全和实用,适用于绝大多数场景。

2.3 vmalloc

vmalloc()函数用于分配不连续的物理内存,并将其映射为连续的虚拟地址空间。

#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);

vmalloc()函数会分配size字节的物理内存,并建立从连续虚拟地址到不连续物理地址的映射,从而提供一块连续的虚拟地址空间给调用者。
如果分配成功,vmalloc()返回连续虚拟地址空间的起始地址。如果失败,返回NULL。

参数
size指定要分配的内存大小,以字节为单位。

vmalloc()有以下主要特征:

  1. 分配的物理内存不连续,但提供连续的虚拟地址空间,方便使用。
  2. 使用页表来建立虚拟地址到物理地址的映射,所以分配和访问速度较慢。
  3. 分配的内存需要通过vfree()释放,否则会产生内存泄漏。
  4. 传入vfree()的地址必须是vmalloc()的返回地址,否则会导致未定义行为。
  5. 同一内存块不能重复释放,否则也会导致未定义行为。
  6. 要及时调用vfree()释放内存,否则容易导致OOM。
  7. 分配的内存类型必须匹配(内核空间与用户空间内存不能混用)。

vmalloc()适用于以下情况:

  1. 需要较大块内存(超过128K)时,kmalloc()就难以满足需求。
  2. 内核映像大小(initrd大小)较大时,也需要用vmalloc()分配。
  3. 在ioremap()之后,也会使用vmalloc()映射分配的I/O内存。
  4. 访问速度要求不高,以空间为主,可以接受页表带来的性能损耗。

2.3.1 vfree

vfree()函数用于释放通过vmalloc()分配的虚拟内存。
它的声明如下:

#include <linux/vmalloc.h>
void vfree(const void *addr);

vfree()函数会释放传入的虚拟地址addr开始的内存块。该内存块必须是通过vmalloc()分配的,否则会导致未定义行为。
参数addr是要释放的虚拟内存块的起始地址,必须是vmalloc()的返回值。

vfree()有以下主要特征:

  1. 会释放vmalloc()分配的虚拟连续地址到物理不连续地址的映射。
  2. addr传入的地址必须正确,否则会导致未定义行为。
  3. 同一内存块不能重复释放,否则也会导致未定义行为。
  4. 要及时调用vfree()释放内存,否则会产生内存泄漏,导致OOM。
  5. 只能释放与vmalloc()相匹配的内存,否则结果未定义。
  6. 会释放vmalloc()为映射创建的页表等数据结构,所以调用vfree()后不能再访问该内存。

vfree()主要用于释放较大块的内存,一般在以下情况使用:

  1. 不再需要vmalloc()分配的大块内存时,需要将其释放。
  2. 加载结束后的initrd需要释放内存。
  3. 模块卸载时需要释放内核映像等占用的大量内存。
  4. 其它分配大块连续虚拟内存后,使用结束需要释放场景。

2.4 kmalloc & vmalloc 的比较

kmalloc()、kzalloc()、vmalloc() 的共同特点是:

  1. 用于申请内核空间的内存;
  2. 内存以字节为单位进行分配;
  3. 所分配的内存虚拟地址上连续;

kmalloc()、kzalloc()、vmalloc() 的区别是:

  1. kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
  2. kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
  3. kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
  4. kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
  5. kmalloc 分配内存的开销小,因此 kmalloc 比 vmalloc 要快;

一般情况下,内存只有在要被 DMA 访问的时候才需要物理上连续,但为了性能上的考虑,内核中一般使用 kmalloc(),而只有在需要获得大块内存时才使用 vmalloc()。

3、IO访问

设备通常会提供一组寄存器来控制设备、读写设备、获取设备状态。这些寄存器的访问入口可能位于独立的I/O空间中,具有独立的访问地址,也可能位于内存空间中,与内存地址共享内存地址空间。
当设备的寄存器入口位于独立的IO空间时,通常被称为I/O端口;对应的外设访问方式也称为IO端口法。
当设备的寄存器入口位于内存空间时,对应的内存空间被称为I/O内存。对应的外设访问方式也称为IO内存法。

3.1 IO端口法

在x86系统上,linux提供了一系列的函数用来访问定位于IO空间的端口。

这些函数都是用于I/O端口操作的内联汇编函数,声明如下:

unsigned char inb(unsigned short port);
void outb(unsigned char value, unsigned short port);

unsigned short inw(unsigned short port);
void outw(unsigned short value, unsigned short port);

unsigned int inl(unsigned short port); 
void outl(unsigned int value, unsigned short port);

void insb(unsigned short port, void *addr, unsigned long count);
void outsb(unsigned short port, const void *addr, unsigned long count); 

void insw(unsigned short port, void *addr, unsigned long count);
void outsw(unsigned short port, const void *addr, unsigned long count);  

void insl(unsigned short port, void *addr, unsigned long count); 
void outsl(unsigned short port, const void *addr, unsigned long count);

这些函数的作用如下:

  • inb/outb: 读/写1个字节
  • inw/outw: 读/写2个字节(16位)
  • inl/outl: 读/写4个字节(32位)
  • insb/outsb: 读/写count个字节
  • insw/outsw: 读/写count个16位字
  • insl/outsl: 读/写count个32位字

port参数指定I/O端口地址,value是要写入的值,addr是存储读取数据的缓冲区地址,count是要读/写的数据量。

3.2 IO内存法

在arm系统中,外设的寄存器入口是参与到内存的统一编址空间的,也即访问它们就像对一个内存空间的读写是一样的。

在linux系统的内核中访问I/O内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。

3.2.1 ioremap

ioremap()函数用于映射物理I/O内存到虚拟地址空间。

#include <asm/io.h>
void __iomem *ioremap(phys_addr_t offset, size_t size);

ioremap()函数会创建从offset开始的size字节I/O内存的虚拟映射。如果映射成功,它会返回虚拟地址空间的起始地址,如果失败则返回NULL。

参数:

  • offset: 要映射的物理I/O内存的起始物理地址。
  • size: 要映射的I/O内存大小,以字节为单位。

__iomem是一个类型修饰符,用于指明某个指针指向的内存区域是I/O内存。
在C语言中,指针只有两种类型:

  • 普通指针:指向普通内存(RAM)
  • 函数指针:指向函数
    但是在内核开发中,还需要操作I/O内存。为了区分普通指针和指向I/O内存的指针,就使用__iomem类型修饰符。
    定义__iomem类型的指针时,需要在原有的指针类型前加上__iomem,如:
char __iomem *io_ptr;  // 指向I/O内存的字符指针

ioremap()有以下主要特征:

  1. 它只能映射物理I/O内存,不能映射普通内存。
  2. 返回的虚拟地址可以像普通内存一样访问I/O内存,但实际上是通过映射实现的。
  3. 需要通过iounmap()释放映射,否则会产生内存泄漏。
  4. 传入iounmap()的地址必须是ioremap()的返回地址,否则会导致未定义行为。
  5. 同一I/O内存区不能重复映射,否则也会导致未定义行为。
  6. 要及时调用iounmap()释放映射,否则容易导致OOM。
  7. 映射过程需要消耗一定的CPU和内存资源。

ioremap()主要用于以下场景:

  1. 需要从用户空间访问物理I/O内存时,可以先通过ioremap()映射,然后像访问普通内存一样进行访问。
  2. 驱动程序需要在中断/底半部里访问I/O内存,这时候也需要先建立映射。
  3. 需要长期频繁访问I/O内存,映射后可以提高访问效率。
  4. 当I/O内存只有物理地址,需要建立映射才能在内核访问。

除ioremap()之外,我们还有ioremap_nocache()和ioremap_cache()函数。根据不同的Cache需求,选择不同的映射函数,可以获得最佳效果。

3.2.2 iounmap

iounmap()函数用于释放通过ioremap()建立的I/O内存虚拟映射。

#include <asm/io.h>
void iounmap(volatile void __iomem *addr);

iounmap()函数会释放传入的I/O内存虚拟地址addr开始的映射。addr必须是ioremap()返回的映射起始地址,否则会导致未定义行为。
参数addr是要释放映射的虚拟地址。

iounmap()有以下主要特征:

  1. 它只能释放通过ioremap()建立的I/O内存映射,不能释放普通内存。
  2. addr传入的地址必须正确,否则会导致未定义行为。
  3. 同一I/O内存区域的映射不能重复释放,否则也会导致未定义行为。
  4. 要及时调用iounmap()释放映射,否则会产生内存泄漏,并占用MMU资源。
  5. 释放映射后,该虚拟地址范围不可访问,否则未定义。
  6. 会释放映射过程中占用的页表、TLB等资源。

iounmap()主要用于以下场景:

  1. 不再需要访问I/O内存时,需要将其映射释放。
  2. 模块卸载时需要释放建立的I/O内存映射。
  3. 释放驱动程序不再使用的I/O内存虚拟地址。
  4. 卸载initrd等需要释放映射的大块I/O内存。

3.2.3 readb/readw/readl和writeb/writew/writel

这些函数都是用于内存读/写的汇编函数,声明和作用如下:

#include <asm/io.h>
unsigned char readb(const volatile void __iomem *addr);
unsigned short readw(const volatile void __iomem *addr);
unsigned int readl(const volatile void __iomem *addr);

void writeb(u8 value, volatile void __iomem *addr); 
void writew(u16 value, volatile void __iomem *addr);  
void writel(u32 value, volatile void __iomem *addr);
  • readb/writeb: 读/写1个字节
  • readw/writew: 读/写2个字节(16位)
  • readl/writel: 读/写4个字节(32位)

这些函数主要用于:

  1. 访问I/O内存:当某段物理内存被映射为I/O内存时,可以使用这些函数读/写。
  2. 访问I/O端口:I/O端口的内存映射基址也被定义为__iomem类型,可以用这些函数读写端口。
  3. 访问普通内存:对于某些体系结构,直接使用这些汇编函数访问内存会效率更高。但对于大部分体系结构,使用C语言的指针直接访问内存会更通用和可移植。

这些函数的参数如下:

  • addr: 要访问的内存地址,必须是__iomem类型。
  • value: 要写入的值,必须与访问内存宽度匹配(宽度由函数名中的b/w/l指定)。

这些函数需要注意的地方:

  1. addr必须是I/O内存或I/O端口的地址,否则会产生未定义行为。
  2. value的类型必须与读/写宽度匹配,否则也会产生未定义行为。
  3. 要按正确的对齐 accessing I/O registers 要按正确字节对齐方式访问I/O寄存器和内存。
  4. 如果编写模块驱动,记得声明为__init和__iomem等,提高可移植性。
  5. 要考虑端序问题,根据不同的体系结构选择合适的访问宽度。

3.3 IO内存访问实例

在FS4412华清的ARM开发板上,用字符驱动的方式控制板上的LED灯的亮灭。应用层发出控制指令,控制LED。

电路原理图
在这里插入图片描述

这里每个SOC上的GPIO引脚(GPX2_7 ,GPX1_0,GPF_4,GPF3_5)分别控制着LED2-LED5。当GPIO引脚高电平时,开关三极管导通,LED二级管发光。反之则关闭。因此,驱动程序主要是通过控制GPIO引脚输出低电平或高电平来控制LED的亮灭。

GPIO端口的控制是通过对应的寄存器的控制实现的,通过查阅芯片手册,可以知道GPIO端口对应的寄存器地址,寄存器设置规则等。以GPX2_7的寄存器为例,其基地址,偏移量,状态位的状态如下:
在这里插入图片描述
以GPX2_7口为例,要设置为输出模式,就要把寄存器GPX2CON(地址为01100 0c40)的第28-31位设为0x01。如果要让该端口输出高电平,则需要把寄存器GPX2DAT(地址为0x1100 0c44)的第七位设为1,输出低电平则把GPX2DAT的第七位设为0。

下面是涉及到LED的所有端口的地址等信息, 查阅至SOC芯片手册

GPIO口led号控制寄存器地址控制字对应位号设置2进制值数据寄存器地址对应位号
GPX2_7led2GPX2CON0x11000C4028~310001GPX2DAT0x11000C447
GPX1_0led3GPX1CON0x11000C200~30001GPX1DAT0x11000C240
GPF3_4led4GPF3CON0x114001E016~190001GPF3DAT0x114001E44
GPF3_5led5GPF3CON0x114001E020~23-0001GPF3DAT0x114001E45

public.h头文件

/********
public.h
*******/
#ifndef _H_PUBLIC_
#define _H_PUBLIC_

#include <asm/ioctl.h>

#define LED_ON _IO('a',1)
#define LED_OFF _IO('a',0)

#endif

驱动代码:

/*************************************************************************
	> File Name: led-access.c
 ************************************************************************/

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/types.h>
#include <asm/io.h>

#include "public.h"

#define GPX2CON 0x11000c40  //led2对应控制寄存器的地址
#define GPX2DAT 0x11000c44 //led2对应数据寄存器的地址

/*1、定义重要的变量及结构体*/
struct x_dev_t {
    struct cdev  my_dev;  //cdev设备描述结构体变量
    atomic_t have_open;   //记录驱动是否被打开的原子变量
    unsigned long volatile __iomem *gpx2con; //指向对应控制寄存器的虚拟地址
    unsigned long volatile __iomem *gpx2dat; //指向对应数据寄存器的虚拟地址
};

struct x_dev_t *pcdev;

/*所有驱动函数声明*/
int open (struct inode *, struct file *);
int release (struct inode *, struct file *);
long unlocked_ioctl (struct file *pf, unsigned int cmd, unsigned long arg);
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
    .owner = THIS_MODULE,
    .open = open,
    .release = release,
    .unlocked_ioctl = unlocked_ioctl,
};

void led_init(void){
    //设置GPX2CON的28-31位为0b0001,输出模式
    pcdev->gpx2con = ioremap(GPX2CON,4);
    pcdev->gpx2dat = ioremap(GPX2DAT,4);
    writel((readl(pcdev->gpx2con) & (~(0xf << 28))) | (0x1 << 28) , pcdev->gpx2con );
}

void led_cntl(int cmd){
    if  (cmd ){  //开
        writel(readl(pcdev->gpx2dat)|(1 << 7),pcdev->gpx2dat );
    }else{
        writel(readl(pcdev->gpx2dat)&(~(1<< 7)), pcdev->gpx2dat);
    }
}

static int __init my_init(void){
    int unsucc =0;
    dev_t devno;
    int major,minor;
    pcdev = kzalloc(sizeof(struct x_dev_t), GFP_KERNEL);
    /*2、创建 devno */
    unsucc = alloc_chrdev_region(&devno , 0 , 1 , "led2");
    if (unsucc){
        printk(" creating devno  faild\n");
        return -1;
    }
    major = MAJOR(devno);
    minor = MINOR(devno);
    printk("devno major = %d ; minor = %d;\n",major , minor);

    /*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
    /*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
    cdev_init(&pcdev->my_dev , &fops);
    pcdev->my_dev.owner = THIS_MODULE;
    /*4、注册cdev结构体到内核链表中*/
    unsucc = cdev_add(&pcdev->my_dev,devno,1);
    if (unsucc){
        printk("cdev add faild \n");
        return 1;
    }
    //初始化原子量have_open为1
    atomic_set(&pcdev->have_open,1);
    
    //初始化led2
    led_init();
    printk("the driver led2 initalization completed\n");
    return 0;
}

static void  __exit my_exit(void)
{
    cdev_del(&pcdev->my_dev);
    unregister_chrdev_region(pcdev->my_dev.dev , 1);
    printk("***************the driver timer-second  exit************\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = container_of(pnode->i_cdev,struct x_dev_t , my_dev);
    pf->private_data = (void *)p;
    //在open函数中对原子量have_open进行减1并检测。=0,允许打开文件,<0则不允许打开
    if (atomic_dec_and_test(&p->have_open)){
        printk("led2  driver is opened\n");
        return 0 ;
    }else{
        printk("device led2 can't be opened again\n");
        atomic_inc(&p->have_open);//原子量=-1,记得这里要把原子量加回到0
        return -1;
    }   

}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
    struct x_dev_t *p = (struct x_dev_t *)pf->private_data;
    printk("led2 is closed \n");
    iounmap(p->gpx2con);
    iounmap(p->gpx2dat);
    atomic_set(&p->have_open,1);
    return 0;
}

long unlocked_ioctl (struct file *pf, unsigned int cmd, unsigned long arg){
    switch(cmd){
        case LED_ON:
            led_cntl(1);
            break;
        case LED_OFF:
            led_cntl(0);
            break;
        default:
            break;
    }
    return 0;
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");

应用层测试代码:

/*************************************************************************
	> File Name: test.c
	> Created Time: Wed 19 Apr 2023 02:33:42 PM CST
 ************************************************************************/

#include<stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "public.h"



int  main (int argc , char **argv){
    int fd0;
    if (argc <2){
        printf("argument is too less\n");
        return -1;
    }else{
        fd0 = open(argv[1] , O_RDONLY );
        while (fd0){
            printf("led on......\n");
            ioctl(fd0,LED_ON);
            sleep(2);
            printf("led off......\n");
            ioctl(fd0,LED_OFF);
            sleep(2);
        }
   }
    close(fd0);
    return 0;


}

4、将设备地址映射到用户空间

4.1 应用层接口

4.1.1 函数mmap

mmap函数的声明如下:

#include <sys/mman.h> 
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:

  • addr:映射区的起始地址,一般设置为NULL让系统自动分配地址
  • length:映射区的长度,必须大于0
  • prot:映射区的保护模式,可以是PROT_READ、PROT_WRITE、PROT_EXEC等

prot参数表示内存映射区的访问权限,它可以取的值有:

  • PROT_READ: 表示内存映射区具有读权限,可以读取内存映射区中的数据。
  • PROT_WRITE: 表示内存映射区具有写权限,可以向内存映射区写入数据。
  • PROT_EXEC: 表示内存映射区具有执行权限,映射区中的数据可以被执行。
  • PROT_NONE: 表示内存映射区没有任何访问权限。
    这些值可以通过|操作符组合使用
  • flags:映射类型,可以是MAP_PRIVATE、MAP_SHARED、MAP_ANONYMOUS等

具体flags可以取的值:

  • MAP_PRIVATE: 表示映射的内存区域只能被当前进程访问,父进程创建的映射会被子进程继承。写时复制技术使父子进程的内存区域互不影响。
  • MAP_SHARED: 表示映射的内存区域能被所有映射该区域的进程访问。数据的写入会影响其他进程访问的数据。
  • MAP_ANONYMOUS: 表示映射匿名内存区域,而不是文件的内容。
  • MAP_FIXED: 表示使用addr参数指定的内存地址,如果该地址已经被占用或无法访问,会导致mmap调用失败。
  • MAP_GROWSDOWN: 表示内存映射区可以向下扩展,扩展后的区域会与当前区域相邻,并具有相同的访问权限。
  • MAP_DENYWRITE: 表示其他进程将无法对该区域进行写操作,而当前进程仍然可以进行写操作。
  • fd:被映射的文件描述符,如果映射匿名区则为-1
  • offset:文件映射的偏移量

offset参数表示文件映射时的偏移量。它有以下几个作用:

  1. 指定文件映射时的起始位置。例如文件大小为100字节,offset设置为20,那么mapping的区域就从文件偏移量20开始,长度为length指定的大小。
  2. 可以实现对同一个文件映射多次的目的。例如第一次mapping offset设置为0,length为60,得到一块内存区域。第二次mapping offset设置为60,length也为60,就可以获取文件剩余部分的映射。
  3. 实现对文件的部分区域映射。设置offset和length可以映射文件中的一定区域,实现部分映射。

mmap函数的作用是建立内存映射,将文件或者匿名区域映射到调用进程的地址空间。这有以下用途:

  • 文件映射:将文件内容映射到内存,实现文件I/O访问,提高性能。
  • 匿名映射:获取一块匿名内存区域,实现进程间通信。
  • 设备映射:将设备内存区域映射到进程,实现设备I/O。

针对以上三种用途的第一种文件映射,举例

#include <sys/mman.h>
#include <sys/stat.h>       
#include <fcntl.h>           
#include <stdio.h>

int main() {
    int fd = open("file.txt", O_RDONLY);
    
    // 映射偏移0后size为30的区域
    char *p1 = mmap(NULL, 30, PROT_READ, MAP_PRIVATE, fd, 0); 
    
    // 映射偏移30后size为20的区域
    char *p2 = mmap(NULL, 20, PROT_READ, MAP_PRIVATE, fd, 30);  
    
    // 多次映射实现了对同一个文件不同区域的访问
    printf("%s\n", p1); 
    printf("%s\n", p2);  
    
    munmap(p1, 30);
    munmap(p2, 20);
    close(fd);
}
这个例子打开file.txt文件,第一次mmap偏移量设置为0,映射前30个字节,得到p1指向的内存区域。
第二次mmap偏移量设置为30,映射30-50个字节,得到p2指向的内存区域。
这样通过两次mmap就可以分别访问文件不同的区域。offset参数实现了多次mmap的目的。 

针对以上三种用途的第二种匿名映射,举例

匿名映射:获取一块匿名内存区域,实现进程间通信。
c
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *p = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    sprintf(p, "Hello");
    
    pid_t pid = fork();
    if (pid == 0) {  // 子进程
        printf("Child see: %s\n", p);
        strcpy(p, "Hi");
    } else {        // 父进程
        sleep(1);   // 等待子进程执行
        printf("Parent see: %s\n", p);
    }
}

针对以上三种用途第三种设备映射,举例

#include <sys/mman.h>
#include <sys/types.h>  
#include <sys/stat.h>   
#include <fcntl.h>      
#include <stdio.h>

int main() {
    int fd = open("/dev/mem", O_RDWR);
    
    // 获取总线的物理内存起始地址
    unsigned long phy_base = 0x1000; 
    // 获取要映射的内存大小 
    size_t map_size = 0x100;  
    
    // 偏移量设置为phy_base,映射物理地址0x1000开始的256字节内存区域 
    char *p = mmap(NULL, map_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, (off_t)phy_base);
    
    *p = 0x12; // 向mapped区域第一个字节写0x12
    
    // 读取物理内存0x1004这个地址的值
    unsigned char val = *((unsigned char*)(p + 0x4)); 
    printf("val = %x\n", val);
    
    munmap(p, map_size); 
    close(fd);
}

4.1.2 munmap函数

munmap函数用于解除内存映射,释放mmap所分配的资源。其函数原型为:

#include <sys/mman.h> 
int munmap(void *addr, size_t length);
  • addr: 要解除映射的内存区域起始地址,必须与mmap调用时的addr参数相同。
  • length: 要解除映射的内存区域长度,必须与mmap调用时的length参数相同。

munmap会释放通过mmap分配的所有资源,包括:

  • 释放内存页表项
  • 释放物理内存
  • 释放文件资源(如果映射的是文件)

所以当一个内存映射区域不再需要时,必须调用munmap function释放其资源,否则可能导致资源泄露。

4.2 内核层

一般情况下,用户空间是不可能也不应该直接访问设备的,但是有时候为了提高效率,需要将用户的一段内存与设备内存关联,当用户访问用户空间这段地址范围时,相当于对设备进行访问。比如对显示适配器一类的设备非常有用。

这时,在驱动的开发时,就需要用到struct file_operations结构体的一个成员.mmap函数,来实现该种映射。

该函数与应用层的mmap函数相对应。应用层执行的mmap函数取终是调用.mmap函数实现的。具体内核做了以下:

在这里插入图片描述

4.2.1 mmap函数

file_operations结构体中的mmap成员对应内核中的文件mmap操作。其原型为:

int (*mmap) (struct file *filp, struct vm_area_struct *vma);
  • filp: 要mmap的文件句柄
  • vma: 包含映射区域信息的vm_area_struct结构体指针

这个函数用来映射文件内容到进程地址空间,实现文件内容的共享映射。
在用户空间调用mmap系统调用时,内核会调用文件inode的f_op->mmap来映射文件。

使用例子:

#include <linux/mm.h>
#include <linux/file.h>

struct file_operations my_fops = {
    .mmap = my_mmap,
    ...
}

int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long phys = __pa(someaddr);
    unsigned long len = vma->vm_end - vma->vm_start;
    
    if (vma->vm_flags & VM_WRITE) {
        // file is writable
    } else {
        // file is read only
    }
    
    // map the file contents to process VM
    remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT, 
                    len, vma->vm_page_prot);
                    
    // 更新文件大小(如果文件正在增长)
    filp->f_mapping->host->i_size = ...  
}

注意:
对filp->f_mapping->host进行修改的代码必须锁住文件i_mutex避免竞争。
remap_pfn_range函数会建立物理页到虚拟内存区域的映射。

4.2.2 vm_area_struct结构体

内核在调用.mmap函数之前会根据应用层的传参完成VMA结构体的构造,并把VMA结构体传递给.mmap函数。

VMA结构体就是vm_area_struct。表示进程的一个虚拟内存区域,结构体主要内容如下如下:

struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

	atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
}

说明

  • vm_start: 虚拟内存区域的起始地址
  • vm_end: 虚拟内存区域的结束地址
  • vm_next和vm_prev: 用于连接多个 VMA 构成双向链表
  • vm_rb: 用于将多个 VMA 构成红黑树,以地址排序
  • vm_mm: 指向所属的 mm_struct,表示所属的进程
  • vm_flags: 标志位,表示区域的访问权限(读/写/执行)、映射类型(共享/私有)等
  • vm_page_prot: 页级保护,控制如何在页表中设置访问权限
  • vm_pgoff: 文件映射时的偏移量(page offset)
  • vm_file: 如果映射了文件,此处指向文件结构体file;否则为NULL(匿名映射)
  • vm_private_data: 私有数据,由mmap的文件op->mmap函数设置,一般 unused
  • vm_ops: 指向vm_operations_struct 结构体,定义此VMA的操作如munmap、mprotect等
  • anon_vma: 匿名VMA链表,将匿名VMA连接 together
  • vm_policy: NUMA策略,用于在NUMA系统中调度页表和cpu
    这些字段记录了每个虚拟内存区域的详细信息,如起止地址、权限、所属文件、匿名链表等,使得内核可以方便地管理和操作进程的虚拟内存。

4.2.3 struct vm_operations_struct

vm_area_struct中的vm_ops指针指向vm_operations_struct结构体,定义了该VMA的操作方法,如munmap、mprotect等。

#include <linux/mm.h>

struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
    int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
                  void *buf, int len, int write);
    int (*mmap)(struct vm_area_struct *vma);
    int (*mprotect)(struct vm_area_struct *vma, unsigned long start, 
                    unsigned long end, unsigned long prot);
    int (*mremap)(struct vm_area_struct *vma);
    int (*remap_pages)(struct vm_area_struct *vma, unsigned long addr, 
                       unsigned long size, unsigned long pgoff);
    int (*populate)(struct vm_area_struct *vma, unsigned long addr, 
                    unsigned long len); 
    void (*close)(struct vm_area_struct *vma);
    int (*madvise)(struct vm_area_struct *vma, unsigned long start,
                    unsigned long end, int advice);                  
};

这些方法定义了针对该VMA的各种操作,如munmap对应close方法,mprotect对应mprotect方法,软件缺页错误对应fault方法等。
当用户空间调用mmap等系统调用操作VMA时,内核会调用对应vm_ops的方法来完成操作。
内核会在以下情况下调用vm_operations_struct的对应方法:

  1. close方法:当调用munmap系统调用释放一个VMA时,内核会调用vm_ops->close方法。
  2. fault方法:当一个VMA首次被访问,且页表项还未建立,会触发缺页异常,此时内核会调用vm_ops->fault方法来处理异常,建立页表项。
  3. page_mkwrite方法:当一个只读页面在进程尝试写访问时,会调用vm_ops->page_mkwrite方法来设置页表项为可写,完成页表同步。
  4. mprotect方法:当一个VMA的访问权限发生变化时,内核会调用vm_ops->mprotect方法来更改页表访问权限。
  5. mmap方法:当一个文件在进程初次访问时,内核需要建立文件和VMA之间的关联,会调用vm_ops->mmap方法。例如在文件映射区会调用文件系统的mmap方法。
  6. munmap方法:当调用munmap系统调用释放一段虚拟地址区间时,内核会调用vm_ops->munmap方法来完成操作。
  7. remap_pages方法:当调用remap_file_pages系统调用将文件页重映射到其他虚拟地址时,内核会调用vm_ops->remap_pages方法。
  8. 其他方法:open、access、populate、madvise等,分别在VMA打开、访问检查、预读等情况下调用。
    所以,内核会在需要对一个VMA进行访问权限控制、地址翻译、属性变更等管理操作时,调用vm_operations_struct中对应的方法来完成。
    这使得内核可以根据VMA的类型选择正确的操作方法,是Linux虚拟内存管理的重要机制。

4.2.4 struct mm_struct *vm_mm;

vm_area_struct中的vm_mm指针指向mm_struct结构体,表示该VMA所属的进程。
表示一个进程的地址空间,包含了描述地址空间所需的所有信息。其定义如下:

#include <linux/mm_types.h>,
c
struct mm_struct {
    struct vm_area_struct * mmap;     /* list of VMAs */
    struct rb_root mm_rb;
    u32 vmacache_seqnum;                   /* per-thread vmacache */
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
    unsigned long mmap_base;        /* base of mmap area */
    unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
    unsigned long task_size;        /* size of task vm space */
    ...
}; 

主要包含:

  • mmap: VMA双向链表的头,链接该mm_struct的所有VMA
  • mm_rb: VMA红黑树的根,以地址排序VMA
  • mmap_base和task_size: 模拟地址空间的起始和大小
  • get_unmapped_area: 获取未映射区域的方法
  • 以及其他许多字段

所以mm_struct包含了描述一个进程完整地址空间的所有信息:

  • VMA链表和红黑树,记录每个内存映射区
  • address space的边界和大小
  • 分配未映射空间的方法
  • 页表,swapper等信息

vm_area_struct的vm_mm指针指向所属的mm_struct,这样每个VMA都知道自己属于哪个进程的地址空间,并且内核也可以通过vm_mm链接到描述整个地址空间的信息。
所以vm_mm是连接VMA和整个进程地址空间信息的关键字段。内核通过它可以方便地获得某个VMA所属进程的完整地址空间信息。 (

4.2.5 内核自带的驱动实例

在linxux 4.15版本的目录/drivers/video/fbdev/core/fbmem.c。是一个完整的驱动程序,摘出其中的.mmap程序供参考

static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
	struct fb_info *info = file_fb_info(file);
	struct fb_ops *fb;
	unsigned long mmio_pgoff;
	unsigned long start;
	u32 len;

	if (!info)
		return -ENODEV;
	fb = info->fbops;
	if (!fb)
		return -ENODEV;
	mutex_lock(&info->mm_lock);
	if (fb->fb_mmap) {
		int res;

		/*
		 * The framebuffer needs to be accessed decrypted, be sure
		 * SME protection is removed ahead of the call
		 */
		vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
		res = fb->fb_mmap(info, vma);
		mutex_unlock(&info->mm_lock);
		return res;
	}

	/*
	 * Ugh. This can be either the frame buffer mapping, or
	 * if pgoff points past it, the mmio mapping.
	 */
	start = info->fix.smem_start;
	len = info->fix.smem_len;
	mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
	if (vma->vm_pgoff >= mmio_pgoff) {
		if (info->var.accel_flags) {
			mutex_unlock(&info->mm_lock);
			return -EINVAL;
		}

		vma->vm_pgoff -= mmio_pgoff;
		start = info->fix.mmio_start;
		len = info->fix.mmio_len;
	}
	mutex_unlock(&info->mm_lock);

	vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
	/*
	 * The framebuffer needs to be accessed decrypted, be sure
	 * SME protection is removed
	 */
	vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
	fb_pgprotect(file, vma, start);

	return vm_iomap_memory(vma, start, len);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑牛唱剧本

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值