对于包含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
- 分配指令:
低端内存区内分配内存区的分配指令:- kmalloc:小内存分配,slab算法
- 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()需要注意以下几点:
- 只能向kfree()传入kmalloc()分配的内存地址,否则会导致未定义行为。
- 同一块内存只能free一次,多次free同一地址会导致未定义行为。
- free已free的内存地址会导致未定义行为。
- kmalloc()的内存不会自动释放,必须手动调用kfree()释放。否则会导致内存泄漏。
- kfree()时传入的地址必须是kmalloc()的原始地址,不能在kmalloc()的基础上偏移一定字节后传入kfree()。
- 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()有以下不同:
- kzalloc()会在返回内存之前清零内存,kmalloc()返回的内存是未初始化的。
- 其他属性基本相同,如:
- 使用相同的伙伴系统分配内存。
- 返回的内存需要通过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()基本相同:
- 也是直接分配物理内存,性能高于kmalloc()但内存分配大小较固定。
- 使用不当也会导致内存泄漏和未定义行为,需要谨慎使用。
- 也有对应的free_pages()函数用于释放分配的内存。
- 也有其它类似函数,如__get_dma_pages()等。
所以,__get_free_pages()函数在以下情况下会非常有用: - 需要分配较大的连续内存块(多个物理页面)。
- 对性能有较高要求,不希望通过kmalloc()分配内存后再手动连接成较大块。
- 需要的内存大小可以通过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()也需要注意:
- 返回的内存页面需要手动通过free_page()释放,防止内存泄漏。
- 传入free_page()的地址必须是get_zeroed_page()的返回地址,否则会导致未定义行为。
- 同一内存页面不能重复free,否则也会导致未定义行为。
- 要积极释放物理内存页面,否则容易因分配和释放不均衡导致OOM。
- 内存页面类型必须匹配(内核空间与用户空间内存不能混用)。
- 直接操作物理内存,使用不当会导致各种问题,需要特别小心。
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()也需要注意:
- 返回的内存需要通过free_pages()释放,以防止内存泄漏。
- 传入free_pages()的地址和order必须与__get_dma_pages()相同,否则会导致未定义行为。
- 同一内存不能重复释放,否则也会导致未定义行为。
- 要及时调用free_pages()释放内存,否则容易OOM。
- __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()函数在以下情况下很有用:
- 需要分配较大块内存(通过较大的order),并且对性能要求不高。
- 普通内存空间不足,需要使用高端内存。
- 仅用于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()函数在以下情况下很有用:
- 需要较快速度访问内存,对性能有要求。
- 普通内存空间足够,不需要使用高端内存。
- 访问频繁,需要较快的速度。
与其他类似函数一样,__get_low_pages()也需要注意:
- 返回的内存需要通过free_pages()释放,防止内存泄漏。
- 传入free_pages()的地址和order必须正确,否则导致未定义行为。
- 同一内存不能重复释放,否则也会导致未定义行为。
- 要及时释放分配的内存,否则容易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()有以下主要特征:
- 分配的物理内存不连续,但提供连续的虚拟地址空间,方便使用。
- 使用页表来建立虚拟地址到物理地址的映射,所以分配和访问速度较慢。
- 分配的内存需要通过vfree()释放,否则会产生内存泄漏。
- 传入vfree()的地址必须是vmalloc()的返回地址,否则会导致未定义行为。
- 同一内存块不能重复释放,否则也会导致未定义行为。
- 要及时调用vfree()释放内存,否则容易导致OOM。
- 分配的内存类型必须匹配(内核空间与用户空间内存不能混用)。
vmalloc()适用于以下情况:
- 需要较大块内存(超过128K)时,kmalloc()就难以满足需求。
- 内核映像大小(initrd大小)较大时,也需要用vmalloc()分配。
- 在ioremap()之后,也会使用vmalloc()映射分配的I/O内存。
- 访问速度要求不高,以空间为主,可以接受页表带来的性能损耗。
2.3.1 vfree
vfree()函数用于释放通过vmalloc()分配的虚拟内存。
它的声明如下:
#include <linux/vmalloc.h>
void vfree(const void *addr);
vfree()函数会释放传入的虚拟地址addr开始的内存块。该内存块必须是通过vmalloc()分配的,否则会导致未定义行为。
参数addr是要释放的虚拟内存块的起始地址,必须是vmalloc()的返回值。
vfree()有以下主要特征:
- 会释放vmalloc()分配的虚拟连续地址到物理不连续地址的映射。
- addr传入的地址必须正确,否则会导致未定义行为。
- 同一内存块不能重复释放,否则也会导致未定义行为。
- 要及时调用vfree()释放内存,否则会产生内存泄漏,导致OOM。
- 只能释放与vmalloc()相匹配的内存,否则结果未定义。
- 会释放vmalloc()为映射创建的页表等数据结构,所以调用vfree()后不能再访问该内存。
vfree()主要用于释放较大块的内存,一般在以下情况使用:
- 不再需要vmalloc()分配的大块内存时,需要将其释放。
- 加载结束后的initrd需要释放内存。
- 模块卸载时需要释放内核映像等占用的大量内存。
- 其它分配大块连续虚拟内存后,使用结束需要释放场景。
2.4 kmalloc & vmalloc 的比较
kmalloc()、kzalloc()、vmalloc() 的共同特点是:
- 用于申请内核空间的内存;
- 内存以字节为单位进行分配;
- 所分配的内存虚拟地址上连续;
kmalloc()、kzalloc()、vmalloc() 的区别是:
- kzalloc 是强制清零的 kmalloc 操作;(以下描述不区分 kmalloc 和 kzalloc)
- kmalloc 分配的内存大小有限制(128KB),而 vmalloc 没有限制;
- kmalloc 可以保证分配的内存物理地址是连续的,但是 vmalloc 不能保证;
- kmalloc 分配内存的过程可以是原子过程(使用 GFP_ATOMIC),而 vmalloc 分配内存时则可能产生阻塞;
- 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()有以下主要特征:
- 它只能映射物理I/O内存,不能映射普通内存。
- 返回的虚拟地址可以像普通内存一样访问I/O内存,但实际上是通过映射实现的。
- 需要通过iounmap()释放映射,否则会产生内存泄漏。
- 传入iounmap()的地址必须是ioremap()的返回地址,否则会导致未定义行为。
- 同一I/O内存区不能重复映射,否则也会导致未定义行为。
- 要及时调用iounmap()释放映射,否则容易导致OOM。
- 映射过程需要消耗一定的CPU和内存资源。
ioremap()主要用于以下场景:
- 需要从用户空间访问物理I/O内存时,可以先通过ioremap()映射,然后像访问普通内存一样进行访问。
- 驱动程序需要在中断/底半部里访问I/O内存,这时候也需要先建立映射。
- 需要长期频繁访问I/O内存,映射后可以提高访问效率。
- 当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()有以下主要特征:
- 它只能释放通过ioremap()建立的I/O内存映射,不能释放普通内存。
- addr传入的地址必须正确,否则会导致未定义行为。
- 同一I/O内存区域的映射不能重复释放,否则也会导致未定义行为。
- 要及时调用iounmap()释放映射,否则会产生内存泄漏,并占用MMU资源。
- 释放映射后,该虚拟地址范围不可访问,否则未定义。
- 会释放映射过程中占用的页表、TLB等资源。
iounmap()主要用于以下场景:
- 不再需要访问I/O内存时,需要将其映射释放。
- 模块卸载时需要释放建立的I/O内存映射。
- 释放驱动程序不再使用的I/O内存虚拟地址。
- 卸载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位)
这些函数主要用于:
- 访问I/O内存:当某段物理内存被映射为I/O内存时,可以使用这些函数读/写。
- 访问I/O端口:I/O端口的内存映射基址也被定义为__iomem类型,可以用这些函数读写端口。
- 访问普通内存:对于某些体系结构,直接使用这些汇编函数访问内存会效率更高。但对于大部分体系结构,使用C语言的指针直接访问内存会更通用和可移植。
这些函数的参数如下:
- addr: 要访问的内存地址,必须是__iomem类型。
- value: 要写入的值,必须与访问内存宽度匹配(宽度由函数名中的b/w/l指定)。
这些函数需要注意的地方:
- addr必须是I/O内存或I/O端口的地址,否则会产生未定义行为。
- value的类型必须与读/写宽度匹配,否则也会产生未定义行为。
- 要按正确的对齐 accessing I/O registers 要按正确字节对齐方式访问I/O寄存器和内存。
- 如果编写模块驱动,记得声明为__init和__iomem等,提高可移植性。
- 要考虑端序问题,根据不同的体系结构选择合适的访问宽度。
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_7 | led2 | GPX2CON | 0x11000C40 | 28~31 | 0001 | GPX2DAT | 0x11000C44 | 7 |
GPX1_0 | led3 | GPX1CON | 0x11000C20 | 0~3 | 0001 | GPX1DAT | 0x11000C24 | 0 |
GPF3_4 | led4 | GPF3CON | 0x114001E0 | 16~19 | 0001 | GPF3DAT | 0x114001E4 | 4 |
GPF3_5 | led5 | GPF3CON | 0x114001E0 | 20~23 | -0001 | GPF3DAT | 0x114001E4 | 5 |
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参数表示文件映射时的偏移量。它有以下几个作用:
- 指定文件映射时的起始位置。例如文件大小为100字节,offset设置为20,那么mapping的区域就从文件偏移量20开始,长度为length指定的大小。
- 可以实现对同一个文件映射多次的目的。例如第一次mapping offset设置为0,length为60,得到一块内存区域。第二次mapping offset设置为60,length也为60,就可以获取文件剩余部分的映射。
- 实现对文件的部分区域映射。设置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的对应方法:
- close方法:当调用munmap系统调用释放一个VMA时,内核会调用vm_ops->close方法。
- fault方法:当一个VMA首次被访问,且页表项还未建立,会触发缺页异常,此时内核会调用vm_ops->fault方法来处理异常,建立页表项。
- page_mkwrite方法:当一个只读页面在进程尝试写访问时,会调用vm_ops->page_mkwrite方法来设置页表项为可写,完成页表同步。
- mprotect方法:当一个VMA的访问权限发生变化时,内核会调用vm_ops->mprotect方法来更改页表访问权限。
- mmap方法:当一个文件在进程初次访问时,内核需要建立文件和VMA之间的关联,会调用vm_ops->mmap方法。例如在文件映射区会调用文件系统的mmap方法。
- munmap方法:当调用munmap系统调用释放一段虚拟地址区间时,内核会调用vm_ops->munmap方法来完成操作。
- remap_pages方法:当调用remap_file_pages系统调用将文件页重映射到其他虚拟地址时,内核会调用vm_ops->remap_pages方法。
- 其他方法: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);
}