5-5 Linux内存、IO与实例讲解
什么是物理地址
什么是虚拟地址
物理地址与虚拟地址的关系
Linux内存分配的常用方法及区别
I/O端口和I/O内存访问流程
地址类型
物理地址和页
内存映射和页结构
页表
1、用户虚拟地址(2^32)
1.1用户空间程序所有能看到的常规地址
1.2每个进程都有自己的虚拟空间
2、物理地址:该地址在处理器和系统内存之间使用
3、总线地址
3.1该地址在外围总线和内存之间使用
3.2它实现总线和主内存之间的重新映射
3.3通常他们与处理器使用的物理地主相同。
4、内核逻辑地址
4.1内核逻辑地址组成了内核的常规地址
4.2该地址映射了部分(或全部)内存,并经常被视为物理地址
4.3与物理地址是线性映射(一一映射的)(且是连续的)
4.4例如:kmalloc返回的是逻辑地址
5、内核虚拟地址
5.1内核虚拟地址和逻辑地址的相同之处在于,他们都将内核空间的地址映射到物理地址上。
5.2与物理地址不必是线性映射关系(不一定是连续的)
5.3例如:vmalloc与kmap都是返回内核虚拟地址
6、地址的转化
6.1 __pa(logical-addr)//逻辑地址–>物理地址
6.2 __va(vphysical-addr) //物理地址->逻辑地址
7、虚拟地址
7.1Linux操作系统采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间的大小为3G,用户看到和接触到的都是虚拟地址,无法看到时间的物理地址。利用这种虚拟的地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。
7.2Linux将4G的虚拟地址空间划分为两个部分:用户空间和内核空间。
用户空间:0x0—0xBF
FF FF FF即0-3G
内核空间:0xC—0xFF
FF FF FF即3G-4G
7.3用户进程通常情况下能访问到用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用间接访问内核空间。
8、物理地址和页
8.1物理地址被分成离散的大小相等单元,称为页
8.2Linux系统内部许多对内存的操作都是基于页的
8.3每个页的大小通常为4096个字节,集体的大小在<asm/page.h?中使用PAGE—_SIZE定义。
8.4内存地址,物理是虚拟的还是物理的,他们都为分为页号和一个页内的偏移量。
31-12:页号,11-0:PAGE_SHIFT页内偏移量。
8.5页帧号:忽略地址偏移量,并将除去偏移量的剩余位移到右端,称该结果为页帧号。
9、内存映射和页结构
9.1 page的数据结构:struct
page{…};
<linux\mm.h>
Atomic_t count; //对该页的访问计数。当计数值为0时,该页将放回给空闲链表。
Void* virtual;//如果页面被映射,则指向页的内核虚拟地址;如果未被映射为NULL
Unsigned long flags;
描述页状态的一系列标志
PG_locked表示内存中的页已经被锁住
PG_reserved
表示禁止内存管理系统访问该页。
9.2内核维护了一个或者多个page结构数组,用用来跟踪系统中的物理内存。
9.3 page结构指针与虚拟地址之间进行转换
Struct page* virt_to_page(void* kaddr);//内核逻辑地址
->page结构指针
Struct page*
pfn_to_page(int pfn);//页帧号->page结构指针
Void* kmap(struct page *page);//page结构指针->内核虚拟地址
Void kunmap(struct
page* page);//释放映射
//基于原子操作的kmap
Void* kmap_atomic(struct page *page,enum km_type type);//type参数指定使用哪个槽(专用页表入口)
Void kunmap_atomic(void* addr,enum km_type type);//KM_USER0和KM_USER1(针对在用户空间中直接运行的代码)
KM_IRQ0和KM_IRQ1(针对中端处理程序)
10、页表
10.1通常处理器必修使用某种机制,将虚拟地址转换为相应的物理地址,这个机制被称为页表。
10.2页表是一个多层树形结构,结构化的数组中包含了虚拟地址到物理地址的映射和相关的标志位。
10.3幸运的是,对驱动程序开发者来说,在2.6版内核中删除了对页表直接操作的需求。
11、分配内存
11.1用户空间内存动态申请:malloc/free
11.2内核空间内存动态申请:
Kmalloc()
__get_free_page()和相关函数
Vmalloc()极其辅助函数
12、 kmalloc()函数
Kmalloc申请的内存位于物理内核映射区域,而且在物理上也是连续的,与真实物理地址至于一个固定的偏移。
#include<linux/slab.h>
void* kmalloc(size_t size,int flags);
size:分配的内存大小(字节数)
flags: GRP_KERNEL:由运行在内核态的进程调用,分配内存,可能引起睡眠。说明该内存分配是有运行在内核态的进程调用。也就是说,调用它的函数是属于某个进程的,当空闲内存太少时,kmalloc函数会使当前进程进入睡眠,等待空闲页的出现。
GRP_ATOMIC:在中断处理函数、tasklet、内核定时器和持有自旋锁的时候申请内核内存,必修使用GFP_ATOMIC分配标志。
13、后备高速缓存:设备驱动程序常常会反复地分配很多同一个大小的内存块。为了满足这样的应用,内核实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)
14、_get_free_page和相关函数
14.1如果模块需要分配打开的内存,那么使用面向页的分配技术会更好
14.2分配页面函数或宏
unsigned long
get_zeroed_page(unsigned int flags);//返回指向新页面的指针,将页面清零
unsigned long
__get_free_page(unsigned int flags);//同上,但不清零页面
unsigned long
__get_free_pages(unsigned int flags, unsigned int order);//分配页面数为2^order ,分配若干个连续的页面,返回指向该内存区域的指针,但也不清零这些内存区域
14.3释放页面函数
void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order); //分配页面数为2^order
15、vmalloc及其辅助函数
15.1 vmalloc分配虚拟地址空间的连续区域,但这段区域在物理上可能是不连续的。
15.2 vamlloc不能用在原子上下文。因为它的内部实现使用了标志位GFP_KERNEL的kmalloc。
#include<linux/vmalloc.h>
void* vmalloc(unsigned long size);
void vfree(void* addr);
15.3 vmalloc
分配得到的地址是不能再微处理器之外使用的,当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA地址),就不能使用vmalloc;
15.4
使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。
15.5因为vmalloc不但获取内存,还要建立页表,它的开销比__get_free_pages大,因此,用vmalloc函数分配仅仅一夜的内存空间时不值得。
15.6通过vmalloc获得的内存使用起来效率不高,在大多数情况下不鼓励使用。尽可能直接与单个的页面打交道。
16、vmalloc及其辅助函数
void* ioreamap(unsigned long offset,unsigned long size);
void
iounmap(void* addr);
16.1
和vmalloc一样,ioremap也建立新的页表,但和vmalloc不同的是,ioremap并不实际分配内存。
16.2
使用ioremap()函数将设备所处的物理地址映射到虚拟地址。
16.3
为了保持可移植性,不应把ioremap返回的地址当做指向内存的指针而直接访问。相反应使用readb或其他I/O函数(完成设备内存映射的虚拟地址的读写)。
16.4vmalloc和kmalloc的区别
kmalloc使用的(虚拟)地址范围与物理内存是一一对应的;vmalloc使用的地址范围完全是虚拟的,每次分配都要通过对页表的设置来建立(虚拟)内存区域。vmalloc申请的内存不一定是连续的。
Vmalloc分配得到的地址是不能再微处理器之外使用的,因为他们只在处理器的内存单元上才有意义。当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA地址),就不能使用vmalloc;
通常,kmalloc分配小于128KB的内存,vmalloc可以分配更大的内存;
vmalloc不能再原子上下文中使用,因为它的内部实际调用了kmalloc(size,GFP_KERNEL);
17、虚拟地址与物理地址关系
17.1内核虚拟地址转化为物理地址
#define __pa(x)
((unsigned long)(x)-PAGE_OFFSET)//物理地址=虚拟地址-偏移地址(通常为3GB)
extern inline unsigned long virt_to_phys(volatile void* address){
return __pa(address);
}
17.2物理地址转化为内核虚拟地址
#define
__va(x) ((void*)((unsigned long)(x)+PAGE_OFFSET)) //虚拟地址=物理地址+偏移量(通常为3GB)
extern inline void*
phys_to_virt(unsigned long address){
return __va(address);
}
18、I/O端口和I/O内存访问接口
18.1 I/O端口的操作(asm-generic/io.h)
unsigned inb(unsigned
port);//读字节端口(8位宽)
void outb(unsigned char byte,unsigned port);//写字节端口(8位宽)
unsigned inw(unsigned
port);//读字端口(16位宽)
void outw(unsigned char byte,unsigned port);//写字端口(16位宽)
unsigned inl(unsigned
port);//读长字端口(32位宽)
void outl(unsigned char byte,unsigned port);//写长字端口(32位宽)
unsigned insb(unsigned port,void* addr, unsigned long count);//读一串字节
void outsb(unsigned port,void* addr, unsigned long count);//写一串字节
unsigned insw(unsigned port,void* addr, unsigned long count);//读一串字
void outsw(unsigned port,void* addr, unsigned long count);//写一串字
unsigned insl(unsigned port,void* addr, unsigned long count);//读一串长字
void outsl(unsigned port,void* addr, unsigned long count);//写一串长字
18.2 I/O内存(asm-generic/io.h)
在内核中访问I/O内存之前,需要先使用ioremap()函数将设备所处的物理地址映射到虚拟地址,ioremap()的原型如下(asm-generic/io.h):
Void* ioremap(unsigned long offset,unsigned long size);
访问函数:
unsigned int ioread8(void* addr);
//addr为通过ioremap获取的地址
unsigned int ioread16(void* addr);
unsigned int ioread32(void* addr);
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void iowrite8(u8 value,void* addr);
void iowrite16(u16 value,void* addr);
void iowrite32(u32 value,void* addr);
void writeb(unsigned value,address);
void writeb(unsigned value,address);
void
writeb(unsigned value,address);
18.3 I/O端口申请与释放
18.4 I/O内存的申请与释放
18.5
上述request_region()和request_men_region()都不是必须的,建议使用。其任务是检查申请的资源是否可用,如果申请可用则申请成功,并标志为已经使用,启动驱动想再次申请该资源时就会失败。
19、I/O端口和I/O内存操作函数代码分析
20、I/O端口的访问流程
(1)request_region()
//在设备驱动模块加载或open( )函数中进行
(2)inb()、outb()等 //在设备驱动中初始化、read()、write()、ioctl()等函数中进行
(3)release_region()
//在设备驱动模块卸载或release()函数中进行
21、I/O内存的访问流程
(1)request_mem_region()//在设备驱动模块加载或open(
)函数中进行
(2)
ioremap() //