一、系统内存布局
1. 内核地址空间
默认情况下,在32位系统中,系统的地址空间按照3/1G进行划分,其中高于0xC000000的1G空间属于内核地址空间,而用户空间使用低3G字节的空间。
内存一般按照PAGE_SIZE进行划分管理,通常把物理内存称为帧,页面帧号(PFN);
页面表是虚拟内存到物理地址的映射。
linux的内核地址空间只占有高1G的地址空间,把这1G的地址空间分为低896M和高128M空间。
其中低896M空间是和物理内存页帧连续的一一映射的,也是固定映射的(称为低端内存,映射产生的地址称为逻辑地址),映射产生的逻辑地址可以减去一个offset值获取物理页帧;
其中内核高128M地址空间映射的物理内存页帧不是连续的,是可以随时映射和取消的。
2. 用户地址空间
每个进程在内核中都表示为struct task_struct的实例,它表征并描述进程。每个进程都被赋予一个内核映射表struct mm_struct{}结构,内核全局变量current指向当前进程,字段*mm指向内存映射表mm_struct{}。
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
unsigned long mmap_base;
unsigned long task_size;
unsigned long highest_vm_end;
pgd_t *pgd; // 指向进程的一个一级页表
atomic_t mm_users;
atomic_t mm_count;
atomic_long_t nr_ptes;
int map_count;
ulong hiwater_rss;
ulong hiwater_vm;
ulong total_vm;
ulong locked_vm;
ulong pinned_vm;
ulong data_vm;
ulong exec_vm;
ulong stack_vm;
ulong def_flags;
ulong start_code, end_code, start_data, end_data; // 进程各个虚拟内存区域
ulong start_brk, end_brk, start_stack;
ulong arg_start, arg_end, env_start, env_end;
struct task_struct *owner;
struct user_namespace *user_ns;
struc file __rcu *exe_file;
/*省略一些不关注的成员变量*/
};
进程各个虚拟内存区域:
堆栈空间内存区域 | |
空洞 | |
内存映射表 | |
空洞 | |
堆空间内存区域 | |
空洞 | |
BSS段内存区域 未初始化的变量 | |
数据段内存区域 初始化了的变量 | |
文本段(text)内存区域 指令、常量 |
3. 内存区域
内核使用虚拟内存区域struct vm_area_struct{}跟踪进程内存映射。VMA是独立于处理器的结构,具有权限和访问标志。每个VMA区域有base、length,其在虚拟空间是连续的,但是不会映射到连续的物理页面帧。查看/proc/<pid>/maps可获取进程<pid>所使用的虚拟内存区域。
二、内存映射表和MMU
对于进程,需要访问的所有页面必须存在于该进程任意一个VMA中。每个PTE对应于页面和帧之间的映射。
linux使用四级分页模式,页面全局目录PGD、页面上部目录PUD、页面中部目录PMD、页面标PTE。几乎所有的32位cpu都只支持PGD和PTE两级目录。
在MMU执行之前,TLB加速了虚拟地址到物理地址的映射。
三、内存分配机制
linux不同分配器的依赖关系
1. 页面分配器
页面在内核中表示为struct page
//返回page
struct page *alloc_pages(gfp_t mask, unsigned int order);
void __free_pages(struct page* page, unsigned int order);
// 直接返回虚拟地址的分配方式
unsigned long __get_free_pages(gfp_t mask, unsigned int order);
unsigned long get_zeroed_page(gfp_t mask);
void free_pages(unsigned long addr, gfp_t mask);
//虚拟地址和page*的互相转换
struct page* virt_to_page(void *kaddr);
void *page_to_virt(struct page* pg);
2. SLAB分配器
使用伙伴算法进行分配,分配的大小必须为2的幂;
3. kmalloc分配系列
kmalloc依赖于slab分配器,它返回的内存在物理内存和虚拟内存中都是连续的。在arm和x86体系结构中,kmalloc每次分配的最大内存为4M,总可分配的内存量为128MB。
/*其中flag可为:GFP_KERNEL, GFP_ATOMIC, GFP_DMA;
*/
void *kmalloc(size_t size, gfp_t flag);
void kfre(void *ptr);
/*类似的函数簇:*/
void *kzalloc(size_t size, gfp_t flag);
void kzfree(void *p);
void *kzalloc(size_t n, size_t size, gfp_t flag);
void *krealloc(void *p, size_t newsize, gfp_t flag);
4. vmalloc分配系列
vmalloc分配的内存始终来自HIGH_MEM区域,返回的地址不能转换为总线地址或物理地址(因为其内存区域与物理内存不是一一映射),不能被用作DMA。vmalloc比kmalloc慢,因为每次分配必须检索内存、分配页面表PTE。在linux中可以查看/proc/vmallocinfo查看vmalloc分配的内存。
void *vmalloc(unsigned long size);
void *vzalloc(unsigned long size);
void vfree(void *vaddr);
三、I/O内存访问硬件
访问设备寄存器时,内核根据系统架构不同提供两种可能的操作。通过IO端口(PIO)和通过内存映射输入输出(MMIO),前者PIO适用于x86架构,因为x86提供了in/out汇编指令,io和内存是不同的地址空间;后者MMIO适用于ARM架构,IO和内存是同一个地址空间。
1 PIO设备访问方式
在PIO系统中,用于I/O的端口地址空间,只有有限的65536个地址。内核导出一些函数来处理I/O端口,在访问任何IO端口区域之前,必须使用request_region()把要使用的IO端口通知内核。在/proc/ioports中可以查看IO端口的占用情况。
/*申请IO端口区间*/
struct resource *request_region(ulong start, ulong len, char *name);
void release_region(ulong start, ulong len);
/*访问IO端口*/
u8 inb(ulong addr);
u16 inw(ulong addr);
u32 inl(ulong addr);
void outb(u8 b, ulong addr);
void outw(u16 w, ulong addr);
void outl(u32 l, ulong addr);
2 MMIO设备访问方式
内核通常使用HIGH_MEM地址空间来映射设备寄存器。内核提供了函数request_mem_region用于注册空间,注册后可在/proc/iomem查看空间使用情况。在注册内存空间后,同样需要io_remap()把物理地址映射为虚拟地址。
// 注册MMIO内存地址空间,此处占用的是物理地址空间
struct resource *request_mem_region(ulong start, ulong len, char *name);
void release_mem_region(ulong start, ulong len);
// 映射物理地址
void __iomem *ioremap(ulong phyaddr, ulong size);
void iounmap(void __iomem *virt_addr);
unsigned int ioread8(void __iomem *addr);
unsigned int ioread16(void __iomem *addr);
unsigned int ioread32(void __iomem *addr);
void iowrite8(u8 val, void __iomem *addr);
void iowrite16(u16 val, void __iomem *addr);
void iowrite32(u32 val, void __iomem *addr);
四、内存重映射
内核有时需要重新映射内存,常常会出现把内存映射到用户空间的需求。
1. 单Page映射kmap
kmap用于把指定内存页面映射到内核虚拟地址空间。内核HIGHMEM中的128M地址空间,被用于映射物理地址空间除低896MB外的任一内存页帧。
void *kmap(struct page *page);
void kunmap(struct page *page);
2 映射内核内存到用户空间
内核提供了函数remap_pfn_range/io_remap_pfn_range将物理内存页帧PFN或IO地址空间映射到用户空间进程,它主要用于实现mmap系统调用。
int remap_pfn_range(struct vm_area_struct *vma, ulong addr, ulong pfn,
ulong size, pgprot_t flags);
/*
其中 vma是要映射的vma虚拟内存区域
addr: 是vma->vm_start
pfn:是要映射的物理页面帧号pfn,可以通过pfn=virt_to_phys(buffer+offset) >> PAGE_SHIFT获得,或者通过pfn = page_to_pfn(virt_to_page(buffer+offset));获得
flags: 代表VMA所要求的保护,
*/
// 类似的,对于io内存,使用io_remap_pfn_range方式,这个函数是ioremap的对位函数
int remap_pfn_range(struct vm_area_struct *vma, ulong virt_addr, ulong phy_addr,
ulong size, pgprot_t flags);