Linux 内存与I/O访问
目录
TLB(Translation Lookaside Buffer)
DMA(Direct Memory Access,直接存储器访问)
内存空间与I/O空间
内存空间是必需的, 而I/O空间是可选的
内存管理单元(MMU)
作用:辅助操作系统进行内存管理, 提供虚拟地址和物理地址的映射、 内存访问权限保护和Cache缓存控制等硬件支持
TLB(Translation Lookaside Buffer)
即转换旁路缓存, TLB是MMU的核心部件, 它缓存少量的虚拟地址与物理地址的转换关系, 是转换表的Cache, 因此也经常被称为“快表”
TTW(Translation Table walk)
即转换表漫游, 当TLB中没有缓冲对应的地址转换关系时, 需要通过对内存中转换表的访问来获得虚拟地址和物理地址的对应关系。 TTW成功后, 结果应写入TLB中
当ARM要访问存储器时, MMU先查找TLB中的虚拟地址表。 如果ARM的结构支持分开的数据TLB(DTLB) 和指令TLB(ITLB) , 则除了取指令使用ITLB外, 其他的都使用DTLB。
若TLB中没有虚拟地址的入口, 则转换表遍历硬件并从存放于主存储器内的转换表中获取地址转换信息和访问权限(即执行TTW) , 同时将这些信息放入TLB, 它或者被放在一个没有使用的入口或者替换一个已经存在的入口。
ARM内TLB条目中的控制信息用于控制对对应地址的访问权限以及Cache的操作。
C(高速缓存) 和B(缓冲) 位被用来控制对应地址的高速缓存和写缓冲, 并决定是否进行高速缓存。
访问权限和域位用来控制读写访问是否被允许。 如果不允许, MMU则向ARM处理器发送一个存储器异常, 否则访问将被允许进行。
Linux内存管理
在Linux系统中, 进程的4GB内存空间被分为两个部分——用户空间与内核空间。 用户空间的地址一般分布为0~3GB(即PAGE_OFFSET, 在0x86中它等于0xC0000000) , 这样, 剩下的3~4GB为内核空间
用户进程只有通过系统调用(代表用户进程在内核态执行) 等方式才可以访问到内核空间
每个进程的用户空间都是完全独立、 互不相干的, 用户进程各自有不同的页表。 而内核空间是由内核负责映射, 它并不会跟着进程改变, 是固定的。 内核空间的虚拟地址到物理地址映射是被所有进程共享的, 内核的虚拟空间独立于其他程序
内核地址空间划分:
1、物理内存映射区
2、虚拟内存映射区
3、高端页面映射区
4、专用页面映射区
5、系统保留映射区
内存主要使用buddy算法进行管理,把空闲的页面以2的n次方为单位进行管理, 因此Linux最底层的内存申请都是以2n为单位的
Buddy算法最主要的优点是避免了外部碎片, 任何时候区域里的空闲内存都能以2的n次方进行拆分或合并。
内存存取
malloc():C库的malloc()函数一般通过 brk() 和 mmap() 两个系统调用从内核申请内存。
Linux内核总是采用按需调页(Demand Paging) , 因此当malloc() 返回的时候, 虽然是成功返回, 但是内核并没有真正给这个进程内存, 这个时候如果去读申请的内存, 内容全部是0, 这个页面的映射是只读的。 只有当写到某个页面的时候, 内核才在页错误后, 真正把这个页面给这个进程。
free()
内核空间内存动态申请
kmalloc()
void *kmalloc(size_t size, int flags);
//第一个参数是要分配的块的大小
//第二个参数为分配标志 最常使用标志为:GFP_KERNEL
依赖于__get_free_pages()来实现。
使用GFP_KERNEL标志申请内存时, 若暂时不能满足, 则进程会睡眠等待页, 即会引起阻塞, 因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNE申请内存。
由于在中断处理函数、 tasklet和内核定时器等非进程上下文中不能阻塞, 所以此时驱动应当使用
GFP_ATOMIC标志来申请内存。 当使用GFP_ATOMIC标志申请内存时, 若不存在空闲页, 则不等待, 直接返回
__get_free_pages()
Linux内核最底层用于获取空闲内存的方法, 因为底层的buddy算法以2n页为单位管理空闲内存, 所以最底层的内存申请总是以2n页为单位的。
__get_free_pages() 系列函数/宏包括get_zeroed_page()、__get_free_page() 和
__get_free_pages()
使用__get_free_pages() 系列函数/宏申请的内存应使用下列函数释放
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
vmalloc()
用于为较大的顺序缓冲区分配内存,同时能不能在原子上下文中使用,因为内部实现中使用了kmalloc
void *vmalloc(unsigned long size);
void vfree(void * addr);
内存池
非常经典的用于分配大量小对象的后备缓存技术。
//创建内存池
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn,mempool_free_t *free_fn, void *pool_data);
//分配和回收对象
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
//回收内存池
void mempool_destroy(mempool_t *pool);
I/O 访问
访问定位于I/O空间的端口函数:
//读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
//读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
//读写长字端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
//读写一串字节
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
//读写一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
//读写一串长字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
I/O 内存
在内核中访问I/O内存之前, 需首先使用ioremap() 函数将设备所处的物理地址映射到虚拟地址上。
void *ioremap(unsigned long offset, unsigned long size);
ioremap()与vmalloc()类似, 也需要建立新的页表, 但是它并不进行vmalloc()中所执行的内存分配行为。 ioremap() 返回一个特殊的虚拟地址, 该地址可用来存取特定的物理地址范围, 这个虚拟地址位于vmalloc映射区域。
通过ioremap() 获得的虚拟地址应该被iounmap() 函数释放, 其原型如下:
void iounmap(void * addr);
同样地,Linux提供devm_ioremap(),无需进行iounmap
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
unsigned long size);
在设备的物理地址(一般都是寄存器) 被映射到虚拟地址之后, 尽管可以直接通过指针访问这些地址, 但是Linux内核推荐用一组标准的API来完成设备内存映射的虚拟地址的读写。
读寄存器API
//读8bit的寄存器
readb
readb_relaxed()
//读16bit的寄存器
readw
readw_relaxed()
//读32bit的寄存器
readl
readl_relaxed()
//没有_relaxed后缀的版本包含一个内存屏障
写寄存器API
//读8bit的寄存器
writeb
writeb_relaxed()
//读16bit的寄存器
writew
writew_relaxed()
//读32bit的寄存器
writel
writel_relaxed()
//没有_relaxed后缀的版本包含一个内存屏障
I/O端口申请
Linux内核提供了一组函数以申请和释放I/O端口, 表明该驱动要访问这片区域。
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
释放I/O端口
void release_region(unsigned long start, unsigned long n);
I/O内存申请
此处的“申请”表明该驱动要访问这片区域, 它不会做任何内存映射的动作, 更多的是类似于“reservation”的概念。
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
I/O内存释放
void release_mem_region(unsigned long start, unsigned long len);
设备I/O端口和I/O内存访问流程
I/O端口访问
一种途径是直接使用I/O端口操作函数: 在设备打开或驱动模块被加载时申请I/O端口区域, 之后使用inb() 、 outb() 等进行端口访问, 最后, 在设备关闭或驱动被卸载时释放I/O端口范围。
I/O内存的访问
首先是调用request_mem_region()申请资源, 接着将寄存器地址通过ioremap()映射到内核空间虚拟地址, 之后就可以通过Linux设备访问编程接口访问这些设备的寄存器了。 访问完成后, 应对ioremap() 申请的虚拟地址进行释放, 并释放release_mem_region()申请的I/O内存资源。
内存映射与VMA
mmap()
一般情况下, 用户空间是不可能也不应该直接访问设备的, 但是, 设备驱动程序中可实现mmap函数, 这个函数可使得用户空间能直接访问设备的物理地址。
mmap实现了这样的一个映射过程: 它将用户空间的一段内存与设备内存关联, 当用户访问用户空间的这段地址范围时, 实际上会转化为对设备的访问。
这种能力对于显示适配器一类的设备非常有意义, 如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点像素将不再需要一个从用户空间到内核空间的复制的过程。
mmap必须以PAGE_SIZE为单位进行映射, 实际上, 内存只能以页为单位进行映射, 若要映射非PAGE_SIZE整数倍的地址范围, 要先进行页对齐, 强行以PAGE_SIZE的倍数大小进行映射。
驱动中mmap函数原型如下:
int(*mmap)(struct file *, struct vm_area_struct*);
mmap的系统调用原型:
caddr_t mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
当用户调用mmap(系统调用) 的时候, 内核会进行如下处理。
1) 在进程的虚拟空间查找一块VMA。
2) 将这块VMA进行映射。
3) 如果设备驱动程序或者文件系统的file_operations定义了mmap() 操作, 则调用它。
4) 将这个VMA插入进程的VMA链表中。
file_operations中mmap() 函数的第一个参数就是步骤1) 找到的VMA。
由mmap() 系统调用映射的内存可由munmap() 解除映射, 这个函数的原型如下:
int munmap(caddr_t addr, size_t len );
fault()函数
当访问的页不在内存里, 即发生缺页异常时, fault() 会被内核自动调用, 而fault()的具体行为可以自定义。 这是因为当发生缺页异常时, 系统会经过如下处理过程:
1) 找到缺页的虚拟地址所在的VMA。
2) 如果必要, 分配中间页目录表和页表。
3) 如果页表项对应的物理页面不存在, 则调用这个VMA的fault() 方法, 它返回物理页面的页描述符。
4) 将物理页面的地址填充到页表中。
DMA(Direct Memory Access,直接存储器访问)
无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制
DMA方式的数据传输由DMA控制器(DMAC) 控制, 在传输期间, CPU可以并发地执行其他任务。DMAC通过中断通知CPU数据传输已经结束, 然后由CPU执行相应的中断服务程序进行后处理
posted on 2022-08-13 16:15 DylanYeung 阅读(473) 评论(0) 编辑 收藏 举报