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

news2024/11/19 15:14:37

目录

  • 1、linux内核管理内存
    • 1.1 页
    • 1.2 区
      • 1.2.1 了解x86系统的内核地址映射区:
      • 1.2.2 了解32位ARM系统的内核地址映射区:
  • 2、内存存取
    • 2.1 kmalloc
      • 2.1.1 kfree
      • 2.1.2 kzalloc
    • 2.2 __get_free_page函数族
      • 2.2.1 free_page
      • 2.2.2 __get_free_pages()
      • 2.2.3 free_pages
      • 2.2.4 get_zeroed_page
      • 2.2.5 __get_dma_pages
      • 2.2.6 __get_high_pages()
      • 2.2.7 __get_low_pages()
    • 2.3 vmalloc
      • 2.3.1 vfree
    • 2.4 kmalloc & vmalloc 的比较
  • 3、IO访问
    • 3.1 IO端口法
    • 3.2 IO内存法
      • 3.2.1 ioremap
      • 3.2.2 iounmap
      • 3.2.3 readb/readw/readl和writeb/writew/writel
    • 3.3 IO内存访问实例
  • 4、将设备地址映射到用户空间
    • 4.1 应用层接口
      • 4.1.1 函数mmap
      • 4.1.2 munmap函数
    • 4.2 内核层
      • 4.2.1 mmap函数
      • 4.2.2 vm_area_struct结构体
      • 4.2.3 struct vm_operations_struct
      • 4.2.4 struct mm_struct *vm_mm;
      • 4.2.5 内核自带的驱动实例

对于包含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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/502563.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

20230502 - 二叉树1 | 二叉树理论基础、二叉树的递归遍历

1、二叉树理论基础篇 二叉树可以链式存储&#xff0c;也可以顺序存储。 用数组来存储二叉树如何遍历的呢&#xff1f; 如果父节点的数组下标是 i&#xff0c;那么它的左孩子就是 i * 2 1&#xff0c;右孩子就是 i * 2 2。 深度优先遍历 前序遍历&#xff08;递归法&…

Android 页面滑动悬浮资源位动画+滑动监听解决方案

一、介绍 在日常业务开发过程中&#xff0c;我们有好多资源位悬浮在页面上&#xff0c;特别是电商以及促销页面&#xff0c;有些公司恨不得把整个页面像叠汉堡一样&#xff0c;一层一层加内容&#xff0c;目的是想让更多的人通过他们的资源来完成更便捷的操作。 但是资源是会覆…

HarmonyOS版的“抖音”长啥样?有图有真相

“鸿蒙系统实战短视频App 从0到1掌握HarmonyOS”系列课程是面向HarmonyOS实战的视频教程&#xff0c;该课程会通过构建一个真实的短视频App来向读者展示HarmonyOS的全过程。 本节将演示基于HarmonyOS短视频App的核心功能。通过了解该App的功能&#xff0c;也能初步对本课程的内…

C++——类和对象(4)

作者&#xff1a;几冬雪来 时间&#xff1a;2023年5月8日 内容&#xff1a;C类和对象内容讲解 目录 前言&#xff1a; 1.操作符重载&#xff08;续&#xff09;&#xff1a; 前置和后置&#xff1a; 日期减日期&#xff1a; <<操作符&#xff1a; 结尾&#xff…

顶级白帽黑客必备的十大黑客技术

1.熟悉Linux系统和命令行操作&#xff1a; Linux是黑客的基石&#xff0c;几乎所有黑客工具和技术都是在Linux平台上运行的&#xff0c;熟悉Linux系统和命令行操作是必须的。 2.掌握网络协议和TCP/IP模型&#xff1a; 了解TCP/IP模型、网络协议和通信流程是黑客攻击的基础&a…

Python-exe调用-控制台命令行执行-PyCharm刷新文件夹

文章目录 1.控制台命令行执行1.1.os.system()1.2.subprocess.getstatusoutput()1.3.os.popen() 2.PyCharm刷新文件夹3.作者答疑 1.控制台命令行执行 主要三种方式实现。 1.1.os.system() 它会保存可执行程序中的打印值和主函数的返回值&#xff0c;且会将执行过程中要打印的…

Diesel 基础

Diesel 2.0.4 官网 github API Documentation 一个安全的&#xff0c;可扩展的ORM和Rust查询构建器 Diesel去掉了数据库交互的样板&#xff0c;在不牺牲性能的情况下消除了运行时错误。它充分利用了Rust的类型系统来创建一个“感觉像Rust”的低开销查询构建器。 支持数据库…

操作系统基础知识介绍之可靠性与可用性(包含MTTF、MRBF、MTTR等)

计算机是在不同的抽象层上设计和构建的。 我们可以通过计算机递归地下降&#xff0c;看到组件将自身放大为完整的子系统&#xff0c;直到我们遇到单个晶体管。 尽管有些故障很普遍&#xff0c;例如断电&#xff0c;但许多故障仅限于模块中的单个组件。 因此&#xff0c;一个模块…

Python每日一练:小艺的口红(暴力、二分、图论三种方法)代写匿名信

文章目录 前言0、题目一、暴力查找二、二分查找三、有序二叉树总结&#xff08;代写匿名信&#xff09; 前言 很明显小艺的口红问题是考的是查找算法&#xff0c;对于这种一次性查找&#xff0c;直接暴力就行了&#xff0c;当然咱是为了学习&#xff0c;所以用来练练各种查找&…

【Linux】基础IO_文件系统

环境&#xff1a;centos7.6&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅 相关文章推荐&#xff1a; 【Linux】冯.诺依曼体系结构与操作系统 【C/进阶】如何对文件进行读写&#xff08;含二进制&#xff09;操作&#xff1f; 【Linux】基础…

【9种】ElasticSearch分词器详解,一文get!!!| 博学谷狂野架构师

ElasticSearch 分词器 作者: 博学谷狂野架构师GitHub&#xff1a;GitHub地址 &#xff08;有我精心准备的130本电子书PDF&#xff09; 只分享干货、不吹水&#xff0c;让我们一起加油&#xff01;&#x1f604; 概述 分词器的主要作用将用户输入的一段文本&#xff0c;按照一定…

二十一、线索转换1:点击转换按钮加载信息、搜索市场活动

功能需求 *在线索转换页面,展示:fullName,appellation,company,owner 流程图 代码实现 1.ClueMapper /*** 通过id查询线索详情* param id 线索id* return 对应id的线索*/Clue selectClueForDetailById(String id); ClueMapper.xml <select id"selectClueForDetailByI…

考研数学武忠祥 高等数学0基础课笔记

函数和映射 常见的函数 取整函数的基本性质 函数的有界性 例题 sinx 是从-1到1的&#xff0c;但是x是无界的 遇到这种带sin的&#xff0c;就要试着取特殊值&#xff0c;让它为1或者为0 函数的单调性 函数的奇偶性 函数的周期性 举例 数学中Q表示有理数集&#xff0c;下面…

Docker服务编排(Docker Compose) :部署上线nginx+springboot项目

Docker服务编排(Docker Compose) 微服务应用一般包含若干个微服务每个微服务一般会部署多个实例&#xff0c;如果每个微服务需要手动启停 维护工作量大 从Dockerfile build image 或者去dockerhub拉去image 创建多个容器 管理容器 Docker Compose 一个编排多容器分布式…

设计原则之【接口隔离原则】

文章目录 一、什么是接口隔离原则二、实例三、总结接口隔离原则与单一职责原则的区别 一、什么是接口隔离原则 接口隔离原则&#xff08;Interface Segregation Principle, ISP&#xff09;是指用多个专门的接口&#xff0c;而不使用单一的总接口&#xff0c;客户端不应该依赖…

自学Java多久可以就业?Java这样自学实习都能过万!

昨天有00后学妹私信小源&#xff0c;她在学校已经学过java、C等相关课程&#xff0c;未来想走计算机的方向&#xff0c;问我建议还要学习什么语言&#xff0c;是否需要继续学Java?今天好程序员简单介绍下Java必学的技术&#xff01;想自学Java的真的可以试一试&#xff01; 一…

哈希数据结构的概念、实现和应用

一、认识哈希表 1.unordered_set和unordered_map STL实现了两个数据结构unordered_map和unordered_set顾名思义&#xff0c;因为底层的实现方式不同&#xff0c;它们成为了无序的map和set&#xff0c;但是它们的使用与普通的map和set是一样的。 我们可以通过代码测试两种数据…

【力扣--622】设计循环队列

&#x1f58a;作者 : D. Star. &#x1f4d8;专栏 : 数据结构 &#x1f606;今日分享 : 丢脸其实并没有那么可怕&#xff0c;我们可以从另一个角度来想&#xff1a;别人能够记住我了&#xff0c;而且过了还有多少人能记得我呢&#xff1f;虽然这种出场不太优雅&#x1f606; 设…

python制作散点动图

目录 示例1&#xff1a;简单的散点图示例2&#xff1a;添加颜色和大小示例3&#xff1a;实时更新动图完整代码 本教程将介绍如何使用Python制作散点动图。我们将通过三个示例代码&#xff0c;从易到难&#xff0c;逐步说明如何使用Python绘制出散点动图。 示例1&#xff1a;简单…

图书馆客流人数统计分析系统方案

智慧客流人数统计分析系统可以帮助图书馆管理者更好地管理人群流量。系统能够自动统计区域内的人流量高峰期&#xff0c;并通过数据分析提供更加合理的管控&#xff0c;从而提区域内人群流动性&#xff0c;避免拥堵的情况。 AI客流视觉监控 客流量管控分析系统意义 讯鹏客流量管…