本文简介:
由于Linux系统中提供了复杂的内存管理功能,所以内存的概念在Linux系统中变得相对复杂,出现了常规内存、高端内存、虚拟地址、逻辑地址、总线地址、物理地址、I/O内存、设备内存、预留内存等概念。本文将系统地讲解内存和I/O的访问编程,带您走出内存和I/O的概念迷宫。
11.1节讲解内存和I/O的硬件机制,主要涉及内存空间、I/O空间和MMU。
11.2节讲解Linux的内存管理、内存区域的分布、常规内存和高端内存的区别。
11.3节讲解Linux内存存取的方法,主要涉及内存动态申请以及通过虚拟地址存取物理地址的方法。
11.4节讲解设备I/O内存和I/O端口的访问流程,这一节对于编写设备驱动意义非常重大,设备驱动使用本节的方法访问物理设备。
11.5节讲解设备驱动中的DMA与CACHE一致性问题以及DMA编程方法。
11.1 CPU与内存和I/O
一、内存空间与I/O空间
在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的,它通过特定的指令in、out来访问。端口号标志了外设的寄存器地址。Intel语法的in、out指令格式如下:
IN 累加器, {端口号 | DX}
OUT {端口号|DX}, 累加器
目前,大多数嵌入式控制器如ARM、PowerPC等中并不提供I/O空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。
内存地址可以直接由C语言指针操作,例如在186处理器中执行如下代码:
unsigned char *p = (unsigned char *) 0xF000FF00;
*p = 11;
以上程序的意义为在绝对地址0xF0000+0xFF00(186使用16位段地址和16位偏移地址)写入11。
而在ARM、PowerPC等未采用段地址的处理器中,p指向的内存空间就是0xF000FF00,而*p = 11就是在该地址写入11。
再如,186处理器启动后会在绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000是段地址,0xFFF0为段内偏移)执行,请看下面的代码:
typedef void (*lpFunction) (); /*定义一个无参数、无返回类型的函数指针类型*/
lpFunction lpRest = (lpFunciton) 0xF000FFF0; /*定义一个函数指针,指向CPU启动后执行的第一条指令的位置*/
lpReset(); /*调用函数*/
在以上程序中,没有定义任何一个函数实体,但是程序中却执行了这样的函数调用:lpRest(),它实际上起到了“软重启”的作用,跳转到CPU启动后第一条要执行的指令的位置。因此,可以通过函数指针调用一个没有函数体的“函数”,本质上只是换一个地址开始执行。
即便在X86处理器中,虽然提供了I/O空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的I/O指令。因此,内存空间是必须的,而I/O空间是可选的。图11.1给出了内存空间和I/O空间的对比。
图11.1 内存空间和I/O空间
二、内存管理单元MMU
高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。操作系统内核借助MMU,可以让用户感觉到好像程序可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中物理内存的实际容量。
为了理解基本的MMU操作原理,需要先明确几个概念。
TLB:Translation Lookasize Buffer,即转换旁路缓存,TLB是MMU的核心部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,因此也经常被称为“快表”。
TTW:Transaction Table Walk,即转换表漫游,当TLB中没有缓冲对应的地址转换关系时,需要通过对内存中转换表(大多数处理器的转换表为多级页表,如图11.2所示)的访问来获得虚拟地址和物理地址的对应关系。TTW成功后,结果写入TLB。
图11.2 内存中的转换表
图11.3给出了一个典型的ARM处理器访问内存的过程,其他处理器也执行类似过程。当ARM要访问存储器时,MMU先查找TLB中的虚拟地址表。如果ARM的结构支持分开的数据TLB(DTLB)和指令TLB(ITLB),则除取指令使用ITLB外,其他的都使用DTLB。ARM处理器的MMU如图11.3所示。
图11.3 ARM的内存管理单元
若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器中的转换表中获取地址转换信息和访问权限(即执行TTW),同时将这些信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的入口。之后,在TLB条路中控制信息的控制下,当访问权限允许时,对真实物理地址的访问将在Cache或者内存中发生,如图11.4所示。
图11.4 ARM CPU进行数据访问的流程
ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限以及Cache的操作。
C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否高速缓存。
访问权限和域位用来控制读写访问是否被允许。如果不允许,则MMU将向ARM处理器发送一条存储器异常,否则访问将被允许进行。
上述描述的MMU机制针对的虽然是ARM处理器,但PowerPC、MIPS等其他处理器也均有类似的操作。
MMU具有虚拟地址和物理地址转换、内存访问权限保护等功能,这将使得Linux操作系统能单独为系统的每个用户进程分配独立的内存空间并保证用户空间不能访问内存空间的地址,为操作系统的虚拟内存管理模块提供硬件基础。
但是,MMU并非对所有处理器都是必须的,例如常用的SAMSUNG基于ARM7TDMI系列的S3C44B0X不附带MMU,其上无法运行老版的Linux,而只能运行改版的μcLinux,但是Linux2.6则支持带MMU的处理器,在嵌入式系统中,仍存在大量无MMU的处理器,Linux2.6为了更广泛地应用于嵌入式系统,融合了μcLinux,以支持MMU-less系统。如Dragonball、ColdFire、Hitachi H8/300等。
在S3C2410的vivi这个Bootloader中,建立了一个4GB虚拟地址与物理地址一一映射的一级页表,我们来追踪一下其创建过程。
vivi的main()函数首先会调用mem_map_init()函数创建页表,其后调用mmu_init()初始化并使能MMU。mem_map_init()函数如代码清单12.1所示。
代码清单12.1 vivi Bootloader的mem_map_init()函数
void mem_map_init(void){
#ifdef CONFIG_S3C2410_NAND_BOOT
/*CONFIG_S3C2410_NAND_BOOT = y,在文件include/autoconf.h中定义*/
mem_map_nand_boot();
/*最终调用mem_mepping_linear,建立页表*/
#else
mem_map_nor();
#endif
cache_clean_invalidate(); /*清空cache,使无效cache*/
tlb_invalidate(); /*使无效快表TLB*/
}
在mem_map_nand_boot()中会调用mem_mapping_linear()进行页表创建工作,如代码清单12.2所示。
代码清单12.2 vivi Bootloader中的页表创建
static inline void mem_mapping_linear(void){
unsigned long pageoffset, sectionNumber;
putstr_hex("MMU table base address = 0x%", (unsigned long) mmu_tlb_base);
/*4GB虚拟地址映射到相同的物理地址,均不能缓存*/
for(sectionNumber = 0; sectionNumber < 4096; sectionNumber++){
pageoffset = (sectionNumber << 20);
*(mmu_tlb_base) + (pageoffset >> 20) = pageoffset | MMU_SECDESC;
}
/*使DRAM的区域可缓存*/
/*SDRAM物理地址0x3000000-0x33ffffff,
DRAM_BASE=0x3000000,DRAM_SIZE=64M
*/
for(pageoffset = DRAM_BASE; pageoffset < (DRAM_BASE + DRAM_SIZE); pageoffset += SZ_1M){
//DPRINTK(3, "Make DRAM section cacheable: 0x%081x\n", pageoffset);
*(mmu_tlb_base + (pageoffset >> 20)) = pageoffset | MMU_SECDESC | MMU_CACHEABLE;
}
}
内核为ARM920T的S3C2410是一个32位的CPU,它的地址空间为4GB。共有4种内存映射模式,即Fault(无映射)、Coarse Page(粗页表)、Section(段)、Fine Page(细页表)。
代码清单12.2使用了ARM920T内存映射的Section模式(实际可等同于页大小为1MB的情况),4GB的虚拟空间被分成一个一个称为Section的单位。4GB的虚拟内存总共可以被分成4096个段(1MB * 4096 = 4GB),因此我们必须用4096个描述符来对这组段进行描述,每个描述符占用4个字节,故这组描述符的大小为16KB,这4096个描述符构成的表格就是转换表。对于SDRAM区域,在描述符中使能了Cache。
mmu_init()函数直接调用arm920_setup()初始化MMU,arm920_setup()函数包含一系列内嵌的汇编代码,用于控制S3C2410的CP15协处理器对MMU的处理。
11.2 Linux内存管理
对于包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问到的内存达到4GB。
在Linux系统中,进程的4GB内存空间被分为两个部分——用户空间和内核空间。用户空间地址一般分布为0~3GB(即PAGE_OFFSET,在0x86中它等于0xC0000000),这样,剩下的3~4GB为内核空间,如图11.5所示。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。用户进程只有通过系统调用(代表用户进程在内核态执行)等方式才可以访问到内核空间。
图11.5 用户空间与内核空间
每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。
Linux中1GB的内核地址空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区这几个区域,如图11.6所示。
一般情况下,物理内存映射区最大长度为896MB,系统的物理内存被顺序映射在内核空间的这个区域中。当系统物理内存大于896MB时,超过物理内存映射区的那部分内存被称为高端内存