Chinaunix首页 | 论坛 | 博客
  • 博客访问: 85148
  • 博文数量: 11
  • 博客积分: 386
  • 博客等级: 一等列兵
  • 技术积分: 240
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-02 17:11
文章分类

全部博文(11)

文章存档

2012年(11)

我的朋友

分类: LINUX

2012-09-16 00:05:48

 内存地址类型

用户虚拟地址——系统分为用户空间和内核空间,用户空间使用的是用户虚拟地址(相关的概念是进程地址空间)。X86 32位平台上高1GB空间是内核空间,低3GB是用户空间。内核实际能够使用的内存大小还要减去内核代码所占用的空间。

物理地址——内存上实际的地址,内核采用page的方式管理物理内存,通常PAGE_SIZE4KB。一个物理地址分为page frame numberpage中的offset(低PAGE_SHIFT位)。

总线地址——通常总线地址与物理地址相同,但也可能与物理地址之间还有一层映射。

内核逻辑地址——内核逻辑地址与物理地址有着固定的偏移,分配内核逻辑地址不需要修改page table。指针(unsigned long*或者void*)可以直接操作内核逻辑地址。

内核虚拟地址——内核空间的地址都是内核虚拟地址,内核逻辑地址都是内核虚拟地址,但是内核虚拟地址不一定是内核逻辑地址,所以内核虚拟地址从范围上包括了内核逻辑地址。分配内核虚拟地址会修改page tablepage 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,144pages。描述每一个page的数据结构为:

  1. struct page {
  2.     unsigned long flags;
  3.     atomic_t _count;
  4.     atomic_t _mapcount;
  5.     unsigned long private;
  6.     struct address_space *mapping;
  7.     pgoff_t index;
  8.     struct list_head lru;
  9.     void *virtual;
  10. };
    其中的virtual就是page在虚拟内存中的地址。有的物理内存pagevirtual值为NULL,它可能属于高端内存(HIGHMEM),需要动态的映射之后,才会有虚拟地址。描述物理内存和虚拟内存之间的映射关系是通过page table进行的。

相关的操作函数有:

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的方法

请注意,若分配多个pagepage之间都是物理连续的!

一个常用gfp_maskGFP_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

QUESTIONmemory poolpage之间的关系不是很清楚

 

vmalloc()

kmalloc分配的地址在物理上是连续的pages,在虚拟上也是连续的。

vmalloc分配的地址在虚拟上是连续的,不保证在物理是连续的pages

值得注意的是kmalloc__get_free_pages返回的也是虚拟地址,只是因为两者返回的虚拟地址和物理地址是有固定偏移的(PAGE_OFFSET),所以不用修改page table(一个虚拟地址与物理地址之间的转换表),而vmalloc(包括ioremap)的虚拟地址和物理地址之间的映射不固定,所以需要修改page table,而这会增大系统的开销(TLB)。因此,除非是分配比较大的内存(如加载module),才用vmalloc

vmalloc分配的内存都处于VMALLOC_STARTVMALLOC_END之间。

vmalloc可能会sleep,因为操作page table的时候可能会sleep

QUESTIONPage tableMMU之间的关系

 

Slab Layer

因为一些数据结构的allocatefree是很经常的操作,所以内核事先经分配好数据结构挂在free list上,当需要使用时不用分配,直接可以获取,用完之后再返回free list。像task_structinodeskb等数据类型都是这样分配的。

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读写的问题在于(1IO不能借助cache来操作;(2)编译阶段和run time阶段,都有可能对指令优化,而“reorder”,这个需要通过“barrier()/mb()”等指令来防止。

LinuxIO读写有IO portIO memory两种方式:

QUESTION:上述两种分别指memory regionport region,这两者本质什么区别?

IO port方式只在X86平台上使用(?)。IO port需要申请port region,通过request_region,并可以从/proc/ioports中查看,读写指令有inb()outb()inw()outw()inl()outl()等。

IO memory方式是嵌入式平台上广泛使用的。这种情况下,访问IO资源(如寄存器)与访问内存的方式是一样的。IO memory方式先申请内存,然后通过ioremapIO空间映射到申请到的内存地址中去:

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的语句为:

ioread8ioread16ioread32iowrite8iowrite16iowrite32

readbreadwreadlwritebwritewwritel

 

进程地址空间

Process address space进程地址空间,指的是一个进程在用户空间使用的内存情况,这当然指的是虚拟地址。在一个process看来,它可以享有系统中所有的物理内存,这当然也不是不可能的,首先,一个process不会将所有的内存空间都用尽,其次,若使用了过多的内存,也可以通过swap的方式交换至硬盘上去。一个process的地址空间与另一个process的地址空间是不同的,即两个process中同一个地址(虚拟内存地址)对应的物理内存地址是不一样的。当然两个process也可以共享进程地址空间,这时processes就是threads了。

进程地址空间指的是一个process在用户空间的内存情况,一个process在内核空间也是要占用内存的。特别的,对于内核process来说,它只有内核空间,没有用户空间,所以也就没有进程地址空间了。

进程地址空间中分为多个内存区域(virtual memory areaVMA),每个VMA都有读写权限等信息,而且可以用于不同用途,比如:

对可执行文件代码段(text section)的映射;

对可执行文件数据段(已初始化,data section)的映射;

对可执行文件未初始化段(bss section)的映射;

用户空间堆栈区;

其它经过映射的区域(文件、动态库、共享内存等);

若是非法的操作了某个VMA,就会报“Segment Fault”的错误。

VMAvm_area_struct数据结构来描述。

 

内存描述符

在每个processtask_struct中有mm_struct结构体,描述了进程地址空间

  1. struct mm_struct {
  2.     struct vm_area_struct *mmap; /* list of memory areas */
  3.     struct rb_root mm_rb; /* red-black tree of VMAs */
  4.     struct vm_area_struct *mmap_cache; /* last used memory area */
  5.     unsigned long free_area_cache; /* 1st address space hole */
  6.     pgd_t *pgd; /* page global directory */
  7.     atomic_t mm_users; /* address space users */
  8.     atomic_t mm_count; /* primary usage counter */
  9.     int map_count; /* number of memory areas */
  10.     struct rw_semaphore mmap_sem; /* memory area semaphore */
  11.     struct list_head mmlist; /* list of all mm_structs */
  12.     unsigned long start_code; /* start address of code */
  13.     unsigned long end_code; /* final address of code */
  14.     unsigned long start_data; /* start address of data */
  15.     unsigned long end_data; /* final address of data */
  16.     unsigned long start_brk; /* start address of heap */
  17.     unsigned long brk; /* final address of heap */
  18.     unsigned long start_stack; /* start address of stack */
  19.     unsigned long arg_start; /* start of arguments */
  20.     unsigned long arg_end; /* end of arguments */
  21. };//有删减
    在fork()程序中可为每一个新的process开辟一个进程地址空间,它是通过allocate_mm()分配一个mm_struct。但若fork()传给其系统调用clone()的标志中置位了CLONE_VM,则不会新分配mm_struct,而是将现有processmm_struct赋给新的process,这样新的(child)和原有的(parentprocess就共享了同一个进程地址空间,他们也可以叫做threads
  1. if (clone_flags & CLONE_VM) {
  2.     /* current is the parent process and
  3.     * tsk is the child process during a fork()*/
  4.     atomic_inc(&current->mm->mm_users);
  5.     tsk->mm = current->mm;
  6. }

     内核进程没有进程地址空间(进程地址空间专指用户空间的内存情况),所以内核进程的mmNULL

每个process都有mmmm_structtask_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()可以映射一个文件到进程地址空间。(在零拷贝中广泛使用)

 


Page table

一个程序(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


阅读(1768) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~