2012年(11)
分类: LINUX
2012-09-16 00:05:48
用户虚拟地址——系统分为用户空间和内核空间,用户空间使用的是用户虚拟地址(相关的概念是进程地址空间)。X86 32位平台上高1GB空间是内核空间,低3GB是用户空间。内核实际能够使用的内存大小还要减去内核代码所占用的空间。
物理地址——内存上实际的地址,内核采用page的方式管理物理内存,通常PAGE_SIZE是4KB。一个物理地址分为page frame number和page中的offset(低PAGE_SHIFT位)。
总线地址——通常总线地址与物理地址相同,但也可能与物理地址之间还有一层映射。
内核逻辑地址——内核逻辑地址与物理地址有着固定的偏移,分配内核逻辑地址不需要修改page table。指针(unsigned long*或者void*)可以直接操作内核逻辑地址。
内核虚拟地址——内核空间的地址都是内核虚拟地址,内核逻辑地址都是内核虚拟地址,但是内核虚拟地址不一定是内核逻辑地址,所以内核虚拟地址从范围上包括了内核逻辑地址。分配内核虚拟地址会修改page table(page table描述内核虚拟地址与内存物理地址之间的映射关系)。内核虚拟地址通过struct page数据结构来操作。
低地址(low memory)——有内核逻辑地址。
高地址(high memory)——没有内核逻辑地址,使用前需要先映射。
• Zone
物理内存大体上分为几个zone:
ZONE_DMA—DMA传输区,X86上是0MB-16MB
ZONE_NORMAL—普通区,X86上是16MB-896MB
ZONE_HIGHMEM—高端去,X86上是896MB以上,需动态映射
• Page
内核是通过page管理物理内存的,通常一个page的大小为4KB,所以一个1GB的内存就被分为262,144个pages。描述每一个page的数据结构为:
相关的操作函数有:
struct page *virt_to_page(void *kaddr);
struct page *pfn_to_page(int pfn);
void *page_address(struct page *page);//返回虚拟地址(如果存在的话)
void *kmap(struct page *page);//映射page到虚拟地址
void kunmap(struct page *page);
分配内存page的方法
请注意,若分配多个page,page之间都是物理连续的!
一个常用gfp_mask叫GFP_KERNEL(更多flag见下),这种情况下进程可能sleep,在进程sleep期间,内核会腾出内存空间至交换区用以获得可用内存。
• kmalloc()
struct dog *p = kmalloc(sizeof(struct dog), GFP_KERNEL);
kmalloc和用户空间的malloc非常相像,但是多出了flag,常见的flag有:
flag的使用规则为:
不能不管什么场合都用GFP_ATOMIC,因为GFP_KERNEL可以sleep,所以更有可能分配成功。
虽然kmalloc分配的是字节单位,但是它内部实现靠的是__get_free_pages!内核分配内存的方式还是很复杂的,它在page的基础上建立memory pool(内存池)。每一个内存池中存放的是大小固定对象(比如一个内存池专门放大小为32字节的数据,另一个内存池专门放大小为64字节的数据)。所以,在用kmalloc分配一个对象的时候,有可能会占用比对象本身还要大的内存,据说最小的分配会占据32字节(?)。
系统中可以创建自定义的memory pool,相关数据类型是mempool_t。
QUESTION:memory pool与page之间的关系不是很清楚
• vmalloc()
kmalloc分配的地址在物理上是连续的pages,在虚拟上也是连续的。
vmalloc分配的地址在虚拟上是连续的,不保证在物理是连续的pages。
值得注意的是kmalloc、__get_free_pages返回的也是虚拟地址,只是因为两者返回的虚拟地址和物理地址是有固定偏移的(PAGE_OFFSET),所以不用修改page table(一个虚拟地址与物理地址之间的转换表),而vmalloc(包括ioremap)的虚拟地址和物理地址之间的映射不固定,所以需要修改page table,而这会增大系统的开销(TLB)。因此,除非是分配比较大的内存(如加载module),才用vmalloc。
vmalloc分配的内存都处于VMALLOC_START与VMALLOC_END之间。
vmalloc可能会sleep,因为操作page table的时候可能会sleep。
QUESTION:Page table和MMU之间的关系
• Slab Layer
因为一些数据结构的allocate和free是很经常的操作,所以内核事先经分配好数据结构挂在free list上,当需要使用时不用分配,直接可以获取,用完之后再返回free list。像task_struct、inode、skb等数据类型都是这样分配的。
struct task_struct *tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
其中cache是指slab缓冲,并不是传统意义上的硬件cache。
• High Memory Mappings
高端内存的分配好之后返回page结构,需要手动的mapping
void *kmap(struct page *page)
• CPU专用数据
int cpu = get_cpu()
ptr = per_cpu_ptr(per_cpu_var, cpu);//会禁用内核抢占!
/* work with ptr */
put_cpu();//使能内核抢占
• 分配大内存
建议在启动时分配,因为此时内存碎片比较少
void *alloc_bootmem(unsigned long size);
LDD的第八章展示了几个例子
1.用kmalloc分配内存
2.用slab分配内存,比第1种快一些
3.用__get_free_pages分配内存,比第1种跟有效,没有内存碎片(是因为kmalloc每次分配都占用固定大小的内存)
• IO读写
ioremap主要用于将设备地址映射到虚拟地址,如PCI缓冲、frame buffer等(改变page table
IO读写方式可能与内存读写方式不同,比如时序不同,但是也由于南桥等芯片的存在,使得对于CPU或软件来说,IO读写与内存读写是一样的,不需要特别的命令。
IO读写的问题在于(1)IO不能借助cache来操作;(2)编译阶段和run time阶段,都有可能对指令优化,而“reorder”,这个需要通过“barrier()/mb()”等指令来防止。
Linux中IO读写有IO port和IO memory两种方式:
QUESTION:上述两种分别指memory region和port region,这两者本质什么区别?
IO port方式只在X86平台上使用(?)。IO port需要申请port region,通过request_region,并可以从/proc/ioports中查看,读写指令有inb()、outb()、inw()、outw()、inl()、outl()等。
IO memory方式是嵌入式平台上广泛使用的。这种情况下,访问IO资源(如寄存器)与访问内存的方式是一样的。IO memory方式先申请内存,然后通过ioremap将IO空间映射到申请到的内存地址中去:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
void *ioremap(unsigned long phys_addr, unsigned long size);
void iounmap(void * addr);
读写IO memory的语句为:
ioread8、ioread16、ioread32、iowrite8、iowrite16、iowrite32
readb、readw、readl、writeb、writew、writel
• 进程地址空间
Process address space进程地址空间,指的是一个进程在用户空间使用的内存情况,这当然指的是虚拟地址。在一个process看来,它可以享有系统中所有的物理内存,这当然也不是不可能的,首先,一个process不会将所有的内存空间都用尽,其次,若使用了过多的内存,也可以通过swap的方式交换至硬盘上去。一个process的地址空间与另一个process的地址空间是不同的,即两个process中同一个地址(虚拟内存地址)对应的物理内存地址是不一样的。当然两个process也可以共享进程地址空间,这时processes就是threads了。
进程地址空间指的是一个process在用户空间的内存情况,一个process在内核空间也是要占用内存的。特别的,对于内核process来说,它只有内核空间,没有用户空间,所以也就没有进程地址空间了。
进程地址空间中分为多个内存区域(virtual memory area,VMA),每个VMA都有读写权限等信息,而且可以用于不同用途,比如:
对可执行文件代码段(text section)的映射;
对可执行文件数据段(已初始化,data section)的映射;
对可执行文件未初始化段(bss section)的映射;
用户空间堆栈区;
其它经过映射的区域(文件、动态库、共享内存等);
若是非法的操作了某个VMA,就会报“Segment Fault”的错误。
VMA用vm_area_struct数据结构来描述。
• 内存描述符
在每个process的task_struct中有mm_struct结构体,描述了进程地址空间
内核进程没有进程地址空间(进程地址空间专指用户空间的内存情况),所以内核进程的mm为NULL。
每个process都有mm(mm_struct在task_struct的变量名),但是一个CPU上同一时刻只有一个活动process,所以一个CPU上只有一个active mm。
一个进程地址空间(mm_struct)中有多个内存区域(VMA),用于描述VMA的结构体为struct vm_area_struct。每个VMA有属性标志,比如:
VM_READ -- Pages can be read from.
VM_WRITE -- Pages can be written to.
VM_EXEC -- Pages can be executed.
一个进程的进程地址空间情况,可以通过下述方式查看:
start-end指虚拟地址的开始与结束;permission指VMA的权限;offset指映射文件当中的偏移;major和minor指文件所属的设备,一般是磁盘分区;inode指明哪个映射文件。
用pmap
注意libc.so(动态库)文件,它在物理内存中只有一份,但是每个进程都有一份映射。
通过mmap()可以映射一个文件到进程地址空间。(在零拷贝中广泛使用)
一个程序(program)被加载到进程地址空间中去运行,程序看到的都是虚拟内存地址。每个进程会有一个page table,它描述了虚拟内存地址和物理内存地址之间的对应关系。
Page table的缓存就保存在TLB(translation lookaside buffer)中。
• DMA
DMA操作需要在内存中分配DMA缓存,并需要这个DMA缓存的总线地址(总线地址和物理地址不一定一致),DMA缓存的总线地址用dma_addr_t数据类型表示。Linux有两种DMA映射(映射包括了分配内存和返回地址):
Coherent DMA mappings——永久映射
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
返回虚拟地址,DMA缓存的总线地址为dma_handle
Streaming DMA mappings——临时映射
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
还有Scatter/gather DMA,这使得DMA缓存在物理page上可以不用连续。
• 参考
Linux Device Drivers
Linux Kernel Development
http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory