当前位置:Linux教程 - Linux资讯 - Linux内存观点(1)

Linux内存观点(1)

目录  每个子系统都给其它子系统提供了接口,你甚至不需要深入每个子系统的细节,仅仅搞清楚子系统的接口就可以进行内核级的程序开发了。  内核地址空间的布局  初始化和固定映射   Boot mem  高端内存  VM 和 vmalloc  物理内存管理  slab 管理  page cache  swap cache 和 swap file  虚存管理(vma)  swap out  swap in  mm fault handle  mmap  我的理解是这样:  1.可以分成两个部分讨论:    内核空间的内存管理  用户空间的内存管理    2  对于用户空间管理,正如你说的,核心是映射,映射操作由cpu自动完成的,但是如何映射是Linux定的。    正如数学中定义的,关于一个映射有3个要素;  定义域         映射规则    V    值域      因此要完成一个映射的定义,需要    在用户空间分配一个定义域(vm_area_strUCt的分配等操作);  在“物理地址”上分配一个值域(内核空间的分配----页面级分配器);  定义映射(页表操作);    3  对于其它操作,也可以从这个3要素来考虑    比如交换:    就是把一部分值域“搬迁”到“外设”中,映射原象一端固定住,“象”一端也跟着移到“外设”中    交换中的缺页中断  不过是把部分“值域”再搬回到内存中来           内核地址空间的布局  我们计算一下, 如果4G的空间都有映射那么页表占去了多少空间:一个页表4K(一个pte代表4K), pgd 中有1024 项(每一项代表4K空间 ),那么就需要 4K*(1024+1) = 4M+4k 的空间.  内核的pgd 是 swapper_pg_dir,静态分配, 系统初始化时把前768项空出来. 也就是只初始化了3G以上的空间, 编译时内核的虚拟地址从3G开始.这样内核通过这个页目录寻址.初始化时映射的这一部分空间称为预映射.预映射把所有物理内存映射到内核, 同时p--v 转换非常简单,使得内核无须维护自己的虚拟空间,并且能够方便的存取用户空间.  众所周知的,__pa 宏基于这样的预映射.内核拥有独立的pgd, 也就是说内核的虚拟空间是独立于其他程序的.这样以来和其他进程完全没有联系.那么我们所说的用户在低3G,内核在最高1G,为所有用户共享, 又是怎么回事呢? 其实很简单, 进程页表前768项指向进程的用户空间,如果进程要访问内核空间,如系统调用,则进程的页目录中768项后的项指向swapper_pg_dir的768项后的项。然后通过swapper_pg_dir来访问内核空间。一旦用户陷入内核,就使用内核的swapper_pg_dir(不是直接使用而是保持用户pgd 768后面的和 swapper_pg_dir 一致,共享内核页表{因为到内核不切换pgd?}看看do_page_fault ^_^ 的相关处理)进行寻址!  linux 把他的1G线性空间分成了几个部分:  1) Linux将整个4G线性地址空间分为用户空间和内核空间两部分,而内核地址空间又被划分为"物理内存区", "虚拟内存分配区", "高端页面映射区","专用页面映射区", "系统保留映射区"几个区域.    2) 在标准配置下, 物理区最大长度为896M,系统的物理内存被顺序映射在物理区中,在支持扩展页长(PSE)和全局页面(PGE)的机器上,物理区使用4M页面并作为全局页面来处理(呵呵,没有白白计算). 当系统物理内存大于896M时,超过物理区的那部分内存  称为高端内存,低端内存和高端内存用highmem_start_page变量来定界,内核在存取高端内存时必须将它们映射到"高端页面映射区".    3) Linux保留内核空间最顶部128K区域作为保留区,紧接保留区以下的一段区域为专用页面映射区,它的总尺寸和每一页的用途由fixed_address枚举结构在编绎时预定义,用__fix_to_virt(index)可获取专用区内预定义页面的逻辑地址.在专用页面区内为每个CPU预定义了一张高端内存映射页,用于在中断处理中高端页面的映射操作.    4) 距离内核空间顶部32M, 长度为4M的一段区域为高端内存映射区,它正好占用1个页帧表所表示的物理内存总量, 它可以缓冲1024个高端页面的映射.在物理区和高端映射区之间为虚存内存分配区, 用于vmalloc()函数,它的前部与物理区有8M隔离带, 后部与高端映射区有8K(2.4为4k?)的隔离带.    5) 当系统物理内存超过4G时,必须使用CPU的扩展分页(PAE)模式所提供的64位页目录项才能存取到4G以上的物理内.在PAE模式下, 线性地址到物理地址的转换使用3级页表,第1级页目录由线性地址的最高2位索引, 每一目录项对应1G的寻址空间,第2级页目录项以9位索引, 每一目录项对应2M的寻址空间, 第3级页目录项以9位索引,每一目录项对应4K的页帧. 除了页目录项所描述的物理地址扩展为36位外,64位和32位页目录项结构没有什么区别. 在PAE模式下,包含PSE位的中级页目录项所对应的页面从4M减少为2M.    内核的1G线性空间(灰色代表已经建立映射,只有物理区为完全映射)    物理区 8M隔离 vmalloc 区 8K隔离 4M的高端映射区 固定映射区 128K   保留区                     V  和物理区对应的物理内存 被映射到高端映射区的物理内存 其他高端物理内存  下面从代码中寻找一下根据(上面的分析好像不是2.4.0, ^_^):   下面的代码摘自 include/asm-386/pgtable.h  /* Just any arbitrary offset to the start of the vmalloc VM area: the  * current 8MB value just means that there will be a 8MB "hole" after the  * physical memory until the kernel virtual memory starts. That means that  * any out-of-bounds memory Accesses will hopefully be caught.  * The vmalloc() routines leaves a hole of 4kB between each vmalloced  * area for the same reason. ;)  */  #define VMALLOC_OFFSET (8*1024*1024)  #define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))  #define VMALLOC_VMADDR(x) ((unsigned long)(x))  #define VMALLOC_END (FIXADDR_START)  可以看出物理区 和 VM 区中间的那个空洞.而vmalloc区结束和固定映射区开始也应该是4k的空洞啊!     fixmap.h  fixed_addresses 看看这个结构就知道,高端内存映射区属于固定内存区的一种,并且每个cup一个.  enum fixed_addresses {  #ifdef CONFIG_X86_LOCAL_APIC  FIX_APIC_BASE, /* local (CPU) APIC) -- required for SMP or not */  #endif  #ifdef CONFIG_X86_IO_APIC  FIX_IO_APIC_BASE_0,  FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS-1,  #endif  #ifdef CONFIG_X86_VISWS_APIC  FIX_CO_CPU, /* Cobalt timer */  FIX_CO_APIC, /* Cobalt APIC Redirection Table */   FIX_LI_PCIA, /* Lithium PCI Bridge A */  FIX_LI_PCIB, /* Lithium PCI Bridge B */  #endif  #ifdef CONFIG_HIGHMEM  FIX_KMAP_BEGIN, /* reserved pte's for temporary kernel mappings */  FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,  #endif  __end_of_fixed_addresses  };  这个文件的以下定义也非常有意义:  /*  * used by vmalloc.c.  *  * Leave one empty page between vmalloc'ed areas and  * the start of the fixmap, and leave one page empty  * at the top of mem..  */  #define FIXADDR_TOP (0xffffe000UL)  #define FIXADDR_SIZE (__end_of_fixed_addresses need_resched = 1;  cpu_idle();  }  arch/i386/kernel/setup.c   void __init setup_arch(char **cmdline_p)  {  unsigned long bootmap_size;  unsigned long start_pfn, max_pfn, max_low_pfn;  int i;  .......  setup_memory_region(); //有的系统 e820 不太好使,可能伪造一个 bios e820  .......  init_mm.start_code = (unsigned long) &_text; //初始化 init_mm  ......  code_resource.start = virt_to_bus(&_text);  ......  data_resource.start = virt_to_bus(&_etext);  ......  #define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)  #define PFN_DOWN(x) ((x) >> PAGE_SHIFT)  #define PFN_PHYS(x) ((x) MAXMEM_PFN) {  highstart_pfn = MAXMEM_PFN;  printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",  pages_to_mb(highend_pfn - highstart_pfn));  }  #endif  /*  * Initialize the boot-time allocator (with low memory only):  */  bootmap_size = init_bootmem(start_pfn, max_low_pfn);  /*  * 把所有可用的低端内存注册于 bootmem allocator .  */  .......  /*  * Reserve the bootmem bitmap itself as well. We do this in two  * steps (first step was init_bootmem()) because this catches  * the (very unlikely) case of us accidentally initializing the  * bootmem allocator with an invalid RAM area.  */  reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +  bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));  /*  * reserve physical page 0 - it's a special BIOS page on many boxes,  * enabling clean reboots, SMP operation, laptop functions.  */  reserve_bootmem(0, PAGE_SIZE);  ......  paging_init();  ......  /*  * Request address space for all standard RAM and ROM resources  * and also for regions reported as reserved by the e820.  */  .....  request_resource(&iomem_resource, &vram_resource); //可以研究一下  /* request I/O space for devices used on all i[345]86 PCs */  .......   }  什么是e820? 跟着链接去看看.  boot mem 见专门章节吧!我们的重点是   arch/i386/mm/Init.c   /*  * paging_init() sets up the page tables - note that the first 8MB are  * already mapped by head.S.  *  * This routines also unmaps the page at virtual kernel address 0, so  * that we can trap those pesky NULL-reference errors in the kernel.  */  void __init paging_init(void)  {  pagetable_init(); //设置页表  __asm__( "movl %%ecx,%%cr3\n" ::"c"(__pa(swapper_pg_dir))); //重置cpu页目录  __flush_tlb_all();  #ifdef CONFIG_HIGHMEM  kmap_init();  #endif  { //计算管理区大小  unsigned long zones_size[MAX_NR_ZONES] = {0, 0, 0};  unsigned int max_dma, high, low;  }  return;  }    static void __init pagetable_init (void)  {  unsigned long vaddr, end;  pgd_t *pgd, *pgd_base;  int i, j, k;  pmd_t *pmd;  pte_t *pte;  /*  * This can be zero as well - no problem, in that case we exit  * the loops anyway due to the PTRS_PER_* conditions.  */  end = (unsigned long)__va(max_low_pfn*PAGE_SIZE); //首先设置低端内存  pgd_base = swapper_pg_dir;  .....  i = __pgd_offset(PAGE_OFFSET); //看到设置的虚拟空间了吧?  .....  /*  * Fixed mappings, only the page table structure has to be  * created - mappings will be set by set_fixmap():  */  vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;  fixrange_init(vaddr, 0, pgd_base);  #if CONFIG_HIGHMEM  /*  * Permanent kmaps:  */  vaddr = PKMAP_BASE;  fixrange_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);  pgd = swapper_pg_dir + __pgd_offset(vaddr);  pmd = pmd_offset(pgd, vaddr);  pte = pte_offset(pmd, vaddr);  pkmap_page_table = pte; //找到 pkmap区(4m)在内核虚拟空间所对应的页表   #endif  }  希望能够理清高端内存和固定映射的概念及管理方式.为此再看看 kmap_init()  /*  * NOTE: pagetable_init alloc all the fixmap pagetables contiguous on the  * physical space so we can cache the place of the first one and move  * around without checking the pgd every time.  */  #if CONFIG_HIGHMEM  pte_t *kmap_pte; //内核映射的页表  pgprot_t kmap_prot;   #define kmap_get_fixmap_pte(vaddr) pte_offset(pmd_offset(pgd_offset_k(vaddr), (vaddr)), (vaddr))  void __init kmap_init(void)  {  unsigned long kmap_vstart;  /* cache the first kmap pte */  kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);  kmap_pte = kmap_get_fixmap_pte(kmap_vstart);  kmap_prot = PAGE_KERNEL;  }  #endif /* CONFIG_HIGHMEM */       Boot mem          高端内存    在一般情况下,Linux在初始化时,总是尽可能的将所有的物理内存映射到内核地址空间中去。如果内核地址空间起始于0xC0000000,为vmalloc保留的虚拟地址空间是128M,那么最多只能有(1G-128M)的物理内存直接映射到内核空间中,内核可以直接访问。如果还有更多的内存,就称为高端内存,内核不能直接访问,只能通过修改页表映射后才能进行访问。    内存分区可以使内核页分配更加合理。当系统物理内存大于1G时,内核不能将所有的物理内存都预先映射到内核空间中,这样就产生了高端内存,高端内存最适于映射到用户进程空间中。预映射的部分可直接用于内核缓冲区,其中有一小块可用于DMA操作的内存,留给DMA操作分配用,一般不会轻易分配。内存分区还可以适应不连续的物理内存分布,是非一致性内存存取体系(NUMA)的基础。      先看看代码中的注释:  In linux\include\linux\mmzone.h(version 2.4.16, line 67)  /*  * On machines where it is needed (eg PCs) we divide physical memory  * into multiple physical zones. On a PC we have 3 zones:  *  * ZONE_DMA 896 MB only page cache and user processes  */  高端页面的映射  1)  高端物理页面共享一块4M的映射区域,该区域对齐于4M页边界,并用一张页表(pkmap_page_table)来完成映射操作。高端页面的映射地址由其页结构中virtual成员给出。  2)  高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。  3)  页缓冲尽可能地使用高端页面,当通过块结构刷新高端页面时,系统会在提交块设备> ,原请求块,同时中转块被释放。    还是结合源码看一看, 给我的感觉是这样的:  在 include/linux/highmem.h 中没有定义 CONFIG_HIGHMEM 时, 有   void *kmap(struct page *page) { return page_address(page); }  #define page_address(page) ((page)->virtual)  而在定义了CONFIG_HIGHMEM 时其定义变为:  include/linux/asm_i386/highmem.h  static inline void *kmap(struct page *page)  {  if (in_interrupt())  BUG();  if (page virtual;  if (!vaddr)  vaddr = map_new_virtual(page); //对于已经被冲掉的页面,需要重映射  pkmap_count[PKMAP_NR(vaddr)]++; //逻辑页面操作  if (pkmap_count[PKMAP_NR(vaddr)] 0~mem的映射有了,而且是固定不变的。vmalloc的部分(3G+mem+128M?, -固定映射区)的映射并不是总被定义,而且会变.vmalloc就是分一个页面,建一个映射,把分到的vm地址返回.vmalloc修改的是swapper_pg_dir ,基准页目录.这样的话也会产生一些问题,举一个例子(lucian_yao)   一个问题我没有想清楚:假定在某个时候用__vmalloc将[ 3.5G, 3.5G+1M ]映射到[ 2M, 3M ]这个时候进入进程A,然后在中断中访问[3.5G, 3.5G+1M]这段空间,由缺页fault将进程A的页表补上[3.5G, 3.5G+1M]映射到[ 2M, 3M ]然后,释放了[ 3.5G, 3.5G+1M ],再次重新分配[3.5G, 3.5G+1M],这个时候[3.5G, 3.5G+1M]映射到[ 5M, 6M ]但是这个时候(仍然使用进程A的页表)访问[3.5G, 3.5G+1M],由于没有出现缺页fault,访问到的实际地址是[ 2M, 3M ],这不是不对了吗?是不是我理解上有什么问题?    (jkl)  在进程的内核页目录中补上的是只是页目录项,而页表对所有进程来说是共用的,不管vfree()多大的内存块,在vmalloc()时新分配的页表不会被释放,当重新vmalloc()时,仍旧使用原来的页表。do_page_fault使得进程的内核页目录项与swapper_pg_dir保持同步,swapper_pg_dir的内核页目录项一旦建立就不再被改变,需要改变的只是共享的页表而已。  说的神些,kmalloc分配连续的物理地址,vmalloc分配连续的虚拟地址.并且有如下结论:  memory.c 主要负责一部分用户空间的虚存映射工作 也就是 0-3G的映射  vmalloc.c 主要负责内核空间高端的内存分配和映射    实际上:    用户空间 0 - 3G do_brk 调用来分配, 还有一些函数处理映射工作(比如memory.c中的函数)  内核空间 3G - 3G + mem kmalloc, __get_free_pages 来分配  内核空间 3G + mem + 隔离带 - 4G vmalloc    其中 mem 可以看成是内存的大小,会自动检测到,也可以由命令行指定.    memory.c 中的的copy_page_range。clear_page_tables等函数是用于 建立或撤消映射,do_wp_page,do_no_page,do_swap_page等用于页面故障时处理。而vmscan.c则是处理页面交换。      并且vmalloc为了捕获越界,vm中间是有洞的.关于这些,看看下面的讨论把:  提问:  vmalloc()函数分配内存的虚拟地址从3G+high_memory+hole_8M开始其中hign_memory为实际物理内存,hole_8M为8M的隔离带为什么要有8M的隔离带?这样岂不是很浪费虚拟地址空间吗?另外分配的内存块之间有一个4K的隔离页,这样是不是也很浪费虚拟地址空间?  (jkl)  Linux用这些空洞来检测存储器读写越界故障,当然空洞越大出现破坏性故障的可能性就越小,8M的空洞正好用两个页目录项来标记,4K的空洞用一个页表项来标记,由于一般物理内存远小于线性地址空间,因此这种浪费是微不足到的。    谢谢您的回答,第一次请教问题,就得到您的耐心回答,非常感谢我还是有点不明白:  那为何用两个页目录表,用一个不行吗?甚至用一个4k的页表不就够了吗?加一个空洞,是不是利用了页保护的属性?如果越界的话,不论空洞的大小都应该产生异常,对吗?不明白空洞越大越安全的原因另外3G+phymem+8M给vmaloc剩下的空间不多了,如果实际物理内存接近1G的话,是不是vmaloc函数就不能使用了?我感觉空间并不充裕  (jkl)  隔离带的大小是任意的,但如果隔离带不够大的话,有可能会被故障代码跨过引起破坏。当内核有代码引用到这些隔离带的地址时,这些地址对应的页目录项或页表项由于被标记为"不存在",就会产生页故障,这样就可以准确定位故障所在。如果物理内存非常大,造成内核虚拟空间不足时,可以减小内核的起始线性地址,通过减小用户程序的虚拟地址空间来增大内核的虚拟空间。如果物理内存超过2G,可通过一个内核补丁big Physical Memory for IA-32将应用程序与内核的页目录分开,尽管这样还是只能管理3.8G,如果物理内存还要大,就要使用64位的体系了。    物理内存管理  kernel页表  kernel的pgd是在开始setup_32时初始化的,它应该使cr3指向swapper_pg_dir(这个变量在arch\i386\kernel.head.s中),它的定义如下:  ENTRY(swapper_pg_dir)  .long 0x00102007  .fill __USER_PGD_PTRS-1,4,0  /* default: 767 entries */  .long 0x00102007  /* default: 255 entries */  .fill __KERNEL_PGD_PTRS-1,4,0  这里面只有两项非空,用户空间的第一项和kernel空间的第一项,它们都指向地址0x00102000,也就是pg0,这个页表中装的是从物理地址0-4M的内容。但是这个pgd并不会一直这样,在start_kernel中会调用paging_init把它完全重新改掉。paging_init将初始化线性空间从start_mem到end_mem的页表项。它做的第一件事就是把swapper_pg_dir的第一项清0,用于捕获null访问。然后它会进入一个循环,这个循环有两种结果,如果你的CPU是Pentium以上,那么它就把分成4M为单位的页。然后把填入pgd中;否则就按4K进行分页,如果pgd中的项为空,就从start_mem开始分配一页作为页表,然后在页表中顺序填入物理页帧的首地址,如果超过了end_mem,就在页表中填入0。    在swapper_pg_dir中的第一项和第768项都有一个指向pg0的项,因为,初始化的时候,其中有两个工作要做:  1.启动页机制,这个时候从物理地址寻址转为虚地址寻址,为确保平滑过渡,在低端(0到4M)的映射是恒同映射  2 跳到内核,实际上内存仍然保存在物理地址0-4M,这个时候,实际上是通过虚拟地址3G-4G访问的,为了平滑过渡,将3G-3G+4M的虚地址映射和0-4M虚地址一致,这样,开启页面映射后跳到内核时已经运行于3G以上的地址了,从这以后,内核自己的寻址都在3G以上了。  内核的内存分配主要会涉及到三组分配函数:   1)页面分配器   __get_free_pages()/__free_pages()   2)0xc0000000 ~ 0xc0000000+phymem   kmalloc()/kfree()   3) 0xc0000000+phymem+8M_hole ~ 4G   vmalloc()/vfree()   以下简要的给予介绍:  一:页面分配器:   页面分配器是最底层的内存分配,主要用于物理内存页的分配,在内核初始化时,调用paging_init创建swap_page_dir,使得从PAGE_OFFSET到PAGE_OFFSET+PhyMem的内核虚拟空间与0~PhyMem的物理空间建立起一一对应的关系。所以__get_free_pages返回的是实际物理+PAGE_OFFSET(由ADDRESS宏实现变换)。 页面分配器采用的是“伙伴”算法。主要涉及两个重要全局变量。  1)struct free_area_struct free_area[NR_MEM_TYPES][NR_MEM_LISTS]; 空闲块数组   2)mem_map_t * mem_map 逻辑页,标示每个物理页的使用情况,根据系统的实际内存,在内核初启时,由mem_init初始化。 具体实现请阅读源码及参看《UNIX高级教程 系统技术内幕》、  二:kmalloc/kree kmalloc  分配的是从PAGE_OFFSET~PAGE_OFFSET+PhyMem之间的内核空间,用于分配连续物理空间。将kmalloc返回值减去PAGE_OFFSET就是实际的物理地址。 我现在看的源码(2.2.14)kmalloc实现采用了slab分配器算法。具体的实现请参阅lucian_yao以前的贴子及 《UNIX高级教程 系统技术内幕》。  三:vmalloc/vfree   用于分配内核位于PAGE_OFFSET+PhyMem+8M_hole ~ 4G的虚拟空间, vmalloc的实现比较简单,主要是维护struct vm_struct vmlist链表 当然vmalloc会调用kmalloc以分配vm_struct,然后为虚拟空间创建页表。   这里我有一个疑问就是用vmalloc分配的内存似乎不会被swap出去,希望有高手指教。再有就是用malloc分配的位于数据段上端至brk之间的堆。 这部分似乎要用到sys_remap,sys_munmap系统调用,以后看源码再说吧。刚看了点皮毛,写出点心得,就是想暴露一下自己的一些模糊概念,以期有高手指正。   [jkl]内核要求实时性很高,可加载模块本身就是用vmalloc()分配的内存,它是不能允许极慢的磁盘交换的。       slab 管理  slab分配器在内存分配中起的作用    slab分配器通过页面级分配器获得页块后,做进一步的精细分配,  将这个页块分割成一个个的对象,有点类似c中的malloc  c, mfree的作用。   cache描述符    struct kmem_cache_s {  /* 1) each alloc & free */  /* full, partial first, then free */  struct list_head slabs;  struct list_head *firstnotfull;  unsigned int objsize;  unsigned int flags; /* constant flags */  unsigned int num; /* # of objs per slab */  spinlock_t spinlock;  #ifdef CONFIG_SMP  unsigned int batchcount;  #endif    /* 2) slab additions /removals */  /* order of pgs per slab (2^n) */  unsigned int gfporder;    /* force GFP flags, e.g. GFP_DMA */  unsigned int gfpflags;    size_t colour; /* cache colouring range */  unsigned int colour_off; /* colour offset */  unsigned int colour_next; /* cache colouring */  kmem_cache_t *slabp_cache;  unsigned int growing;  unsigned int dflags; /* dynamic flags */    /* constructor func */  void (*ctor)(void *, kmem_cache_t *, unsigned long);    /* de-constructor func */  void (*dtor)(void *, kmem_cache_t *, unsigned long);    unsigned long failures;    /* 3) cache creation/removal */  char name[CACHE_NAMELEN];  struct list_head next;  #ifdef CONFIG_SMP  /* 4) per-cpu data */  cpucache_t *cpudata[NR_CPUS];  #endif  #if STATS  unsigned long num_active;  unsigned long num_allocations;  unsigned long high_mark;  unsigned long grown;  unsigned long reaped;  unsigned long errors;  #ifdef CONFIG_SMP  atomic_t allochit;  atomic_t allocmiss;  atomic_t freehit;  atomic_t freemiss;  #endif  #endif  };    slabs用它将这个cache的slab连成一个链表  firstnotfull指向第一个不满的slab,当分配(复用)对象的时候,首先考虑在它指向的slab里分配.  objsize该cache中对象大小  flags  num对象个数    gfporder该cache中slab一个占用多少页面,当构造新的slab,按照这个大小向页面级分配器申请页面。  gfpflags申请页面时,向页面级分配器提出的要求,例如是否要求申请DMA的页面,是否要求申请是原子的(即页面分配器在分配的时候不能被阻塞)  colour colour的范围,这个cache的slab依次用0,1,...,colour-1,0,1,...为颜色。  colour_off这个cache中colour粒度,例如为一个L1-CACHE线。  colour_next下一个colour数,当cache分配一个新的slab时,采用这个colour,也就是colour * colour_off为slab空出的字节数  slabp_cache 当这个cache中的slab,其管理部分(slab描述符和kmem_bufctl_t数组)放在slab外面时,这个指针指向放置的通用cache  growing  dflags  ctor 指向对象的构造器,在这个cache创建一个新的slab时,对里面所有的对象都进行一次构造调用(参见slab的设计思想中关于对象复用部分)  dtor 指向对象的析构器,在这个cache销毁一个slab时,对里面所有的对象都进行一次析构调用  failures    name 这个cache的名字  next 用它和其它的cache串成一个链,在这个链上按照时钟算法定期地回收某个cache的部分slab      slab描述符    typedef struct slab_s {  struct list_head list;  unsigned long colouroff;  void *s_mem; /* including colour offset */  unsigned int inuse; /* num of objs active in slab */  kmem_bufctl_t free;  } slab_t;      list用于链表,这个链表将cache中所有的slab连接起来  colouroff这个slab中第一个对象距离slab起始位置(也就是页块起始位置)的字节数,实际上s_mem=页块首地址+colouroff  s_mem这个slab中第一个对象的起始位置  inuse这个slab中被使用的对象个数,用于调整slab格局,当inuse=0说明这个slab全空,将这个slab从部分满的slab段中移动到全空的slab段中  free第一个未用对象的ID, 当在这个slab"分配"(复用)对象时,首先用这个ID的对象。      通用cache索引结构  用这个结构组成的数组cache_sizes给不同尺寸的通用cache提供索引  typedef struct cache_sizes {  size_t cs_size;  kmem_cache_t *cs_cachep;  kmem_cache_t *cs_dmacachep;  } cache_sizes_t;  cs_size通用cache的对象尺寸  cs_cachep指向一个通用cache, 它的对象尺寸为cs_size  cs_dmacachep指向一个通用DMA的cache, 它的对象尺寸为cs_size        Slab分配器的结构    Slab 分配器用于管理内核的核心对象。    它有若干个 cache 组成。每个 cache 管理一个特定类的对象。    每个cache有若干个 slab (Slab分配器的名字可能就是怎么来的)组成,每个 slab  实际上就是若干个页面组成的一个页块。这个页块被细分成许多对象。  cache为管理这些slab, 通过 cache描述符( kmem_cache_t )以及指针将这些 slab  连起来。  验证  cache的数据结构中下面这个字段:  struct kmem_cache_s {    struct list_headslabs;  ... ...  }    与slab结构中下面字段:    typedef struct slab_s {  struct list_headlist;  ...  } slab_t;    共同构成这个链表.   slab如何管理它的对象    一个 slab 通过自己的 kmem_bufctl_t 数组,来管理它的空闲对象。这个数组的元素和该 slab中的对象是一一对应的。  初始化一个slab时,每个对象都是空的,所以这个数组每个元素(除最后一个)都指向下一个:  在kmem_cache_init_objs中  static inline void kmem_cache_init_objs (kmem_cache_t * cachep, slab_t * slabp, unsigned long ctor_flags)  {  int i;    for (i = 0; i num; i++) {  .. ...  slab_bufctl(slabp)[ i ] = i+1;  }  slab_bufctl(slabp)[i-1] = BUFCTL_END;  ... ...  }    分配对象时,在下面的语句中,    objp = slabp->s_mem + slabp->free*cachep->objsize;  slabp->free=slab_bufctl(slabp)[slabp->free];    取出free的数值1,计算对象1的位置即可。然后将free指向3.  回收(应该说将对象置为未用)时,将数组中对象对应的元素插入链表头即可:  slab_bufctl(slabp)[objnr] = slabp->free;  slabp->free = objnr;  cache如何管理它的slab    格局    一个cache的所有 slab 通过指针连成一个队列,这些 slab的排列始终保持一个格局: 全满的,部分满的,和全空的。  另外,cache 描述符有一个指针始终指向第一个不满的slab(首先可能是部分满的,其次是全空的),当它指向描述符本身的时候,说明没有不满的 slab了。当 slab 是否满的状态有变化时,cache会调整它的位置,以保持上述格局,例如一个部分满的 slab由于它的最后一个对象被设置为不使用,即它为全空的了,那么它将被调整到全空的slab部分中。    当分配一个新的对象时,cache 首先通过 firstnotfull 找到它的第一个不满的slab, 在那么分配对象。如果没有不满的slab,  则向页面级分配器申请一个页块,然后初始化为一个slab.    回收对象    当回收一个对象时,即便在这之后,这个对象所在的 slab 为全空,cache也不会将这个 slab  占用的页块还给页面级分配器。    回收slab    slab分配器算法提供两种回收slab的方式,一种是回收某个特定的cache的所有全空的slab,直到有用户又在该cache分配新的 slab为止( kmem_cache_shrink);一种是对所有的 cache 采用时钟算法,每次选择一个比较合适的 cache,回收它部分的空 slab( kmem_cache_reap ).    验证    每次分配的时候总是考察从firstnotfull指向的第一个不满的slab:  #define kmem_cache_alloc_one(cachep) ({ slab_t*slabp; /* Get slab alloc is to come from. */ { struct list_head* p = cachep->firstnotfull;/*slabs) goto  alloc_new_slab;/*簿褪撬狄凑飧鯿ache的slab全满了,要么就没有slab,这个时候要分配新的slab*/   slabp = list_entry(p,slab_t, list); } kmem_cache_alloc_one_tail(cachep, slabp);   })    在后面的kmem_cache_alloc_one_tail函数中在这个firstnotfull指向的slab中分配一个对象,如果这个slab因此而满了,则将firstnotfull指向下一个不满的slab:  static inline void * kmem_cache_alloc_one_tail (kmem_cache_t *cachep, slab_t *slabp)  {  ... ...  slabp->free=slab_bufctl(slabp)[slabp->free];  if (slabp->free == BUFCTL_END)/*firstnotfull = slabp->list.next;  ... ...  }    下面看看"释放"一个对象时,是如何保持队列的格局的:  static inline void kmem_cache_free_one(kmem_cache_t *cachep, void *objp)  {  ... ...  if (slabp->inuse-- ==cachep->num)/*inuse)/*firstnotfull;/*床糠致亩恿型凡?/    cachep->firstnotfull = &slabp->list;    if (slabp->list.next == t)  return;  list_del(&slabp->list);  list_add_tail(&slabp->list, t);  return;  }    moveslab_free:  /*  * was partial, now empty.  * c_firstnotfull might point to slabp  * FIXME: optimize  */  {  struct list_head *t = cachep->firstnotfull->prev;    list_del(&slabp->list);    list_add_tail(&slabp->list,&cachep->slabs);/*firstnotfull == &slabp->list)  cachep->firstnotfull = t->next;    return;    }    }    slab的管理部分  slab描述符和管理空闲对象用的数组(kmem_bufctl_t)不妨被称为slab的管理部分    slab的管理部分放置的位置  1. 管理部分可以和对象都放在slab里  2. 管理部分也可以放到slab外面(在某个通用的cache中,见通用cache)  1. 如果对象比较大,那么把管理部分放到slab里面,会浪费slab大量空间。举一个极端的例子,对象大小为2K, 页块为4K,那么如果把管理部分放到slab里面,这个页块就只能放一个对象,浪费的空间=4k-2k-管理部分的尺寸接近2K!    2. 但是放在外面会带来一些小小的效率上的损失。  如果管理部分和对象放在两个地方,那么一定是在不同的页块中。于是用户申请一个对象时,首先要访问slab管理部分,然后提供指向未用对象的指针,然后用户访问这个对象的地址。这样,完成一个流程需要访问两个页块,也就是在TLB上要"踩"上两个脚印(footprint).    如果管理部分和对象放在一个slab中,因而很有可能在一个页块中,因此完成这个流程只需在TLB上踩上一个脚印。在引起TLB失效的可能性上,前者比后者大,因而效率低。  Color  slab算法中利用slab的剩余空间来做平移,第1个slab不平移;第2个slab平移1个colour粒度;...;周而复始.  void __init kmem_cache_init(void)  {  size_t left_over;  init_MUTEX(&cache_chain_sem);  INIT_LIST_HEAD(&cache_chain);  kmem_cache_estimate(0, cache_cache.objsize, 0,  &left_over,&cache_cache.num);/*colour_next;/*colour_next++;/*colour_next >=cachep->colour)/*colour_next = 0;  offset *=cachep->colour_off;/*num * sizeof(kmem_bufctl_t) +sizeof(slab_t));  /*inuse = 0;  slabp->colouroff = colour_off;  slabp->s_mem =objp+colour_off;/*free指向该对象即可。为了保证这个cache中的slab格局(满,部分满,全空),必要的时候,要调整这个slab在链表中的位置,具体地说:  slab原本是满的,那么需要将它移动到部分满的slab中去(goto moveslab_partial)  slab原本是部分满的,现在空了,那么将它移动到空的slab中去(moveslab_free)  ~~~~~~~~  空间回收  ~~~~~~  所谓空间回收包括两个工作:  slab分配器把slab中的对象析构(如果有析构器的话)  将占用的页面交还给页面级分配器  slab的回收 kmem_slab_destroy  --------------------------------------------------  如果其中的对象有析构函数,则对这个slab中每个对象调用析构函数将这个slab占用的页面交还给页面级分配器.如果这个slab的管理部分在外面的话,还要到通用cache中free它的管理部分(将这个管理部分设为未用)    cache的页面回收:__kmem_cache_shrink  ------------------------------------------------------  特点是针对某个特定的cache,将它的全空的slab全部回收从这个cache的最后一个slab往前考察(注意cache的slab的格局),回收所有全空的slab kmem_slab_destroy,除非有其它用户在这个cache分配新的slab(这部分我还没有仔细考虑).    cache的页面回收: kmem_cache_reap  ------------------------------------------  在所有cache范围内考察,每次选择一个cache, 回收它的部分空的slab,这实际上是个垃圾回收的工作。所有在使用的cache描述符构成一个循环链表。为公平起见,reap采用时钟算法,每次从当前指针位置遍历 REAP_SCANLEN 个cache(当然可能中途结束遍历),对这些cache进行考察,选出最佳的cache,对它的页面进行回收:  (1)排除一些当前不能回收的cache  (2)计算剩下的cache中,每个cache可回收的slab个数,以及权重pages  (3)选出其中权重最大的,如果某个cache的可回收的slab个数已经达到要求(>=REAP_PERFECT),就结束遍历,对这个cache进行回收  回收:不回收这个cache中所有的空的slab, 只回收约80%, 方式和 __kmem_cache_shrink 一样    注:  用户接口指外部用户可以调用的接口,其它为内部调用接口  蓝色字是slab分配器的一件主要工作  绿色字是一件工作中的主要线索    一般分配一个对象,首先要创建一个cache来管理所有同类的对象(通用cache除外)。如果有cache了,那么就在其中分配一个对象。      (用户接口)  初始化一个cache (kmem_cache_create )  ------------------------------------  在进行一些合法性检查之后,首先在cache_cache中分配这个cache的描述符。  然后对一些尺寸进行处理,包括:size至少是字对齐的 对象对齐方式至少是字对齐的,如果要求是CACHE对齐,那么方式为CACHE对齐,或者是1/2CACHE对齐(如果对象尺寸它所在的页面 ----> cache 和 slab  第一个映射很容易(页对齐即可)在这个函数里主要设置第二个映射, 临时借用了page结构里的一个链表结构(这个结构list在页面级分配器管理空闲页面用,现在不使用)next, prev分配用来指向cache, slab.  page cache     swap cache 和 swap file     


[1] [2] 下一页 

(出处:http://www.sheup.com)


上一页 [1] [2]