页

  内核把物理页作为内存管理的基本单位,尽管处理器的小可寻址单位通常为字节,但是MMU(内存管理单元,管理内存并把虚拟地址转换为物理地址的硬件)通常以页作为单位进行处理。大多数32位体系结构支持4KB的页,内核用struct page结构表示系统的物理页,这个page与物理页相关,而并非与虚拟页相关。页的拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等。

  区

  由于有些页位于内存特定的物理地址上,所以不能将其用于一些特定的任务。故内核把页分成不同的区,分别是ZONE_DMA、ZONE_DMA32、ZONE_NORMAL和ZONE_HIGHMEM(高端内存,其中的页并不能地映射到内核地址空间)。每个区都用struct zone结构体表示,对于32位的X86上,ZONE_DMA包含的页在0-16MB的内存范围里,ZONE_NORMAL是从16MB到896MB的所有物理内存,ZONE_HIGHMEM是高于896MB的所有物理内存。需要注意的是,不是所有的体系结构都定义了全部区,比如X86-64没有ZONE_HIGHMEM区。

  如果需要以页为单位的一族连续物理页时,尤其只需要一两页时,有些低级页函数很有用。alloc_page(返回页结构指针)+page_address=__get_free_pages(返回逻辑地址指针)。

  如果需要连续的物理页,除了使用上面说的低级页函数外,还可以使用kmalloc函数,传递给这个函数的常用标志GFP_KERNEL和GPF_ATOMIC。其中GPF_ATOMIC用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方;GPF_KERNEL是常用分配方法,可能会阻塞,在睡眠安全时用在进程上下文中;GPF_NOIO这种分配可以阻塞,但不会启动磁盘I/O。

  如果不需要连续的物理地址,只要连续的虚拟地址,可以使用vmalloc函数,但是由于物理地址的不连续性,导致通过vmalloc获得的页必须一个个的进行映射,这导致比直接内存映射大的多的TLB(转换旁路缓存,用来缓存虚拟地址到物理地址的映射关系)抖动,所以,迫不得已不用该函数,典型的是为了获得大块内存时,比如模块动态加载。

  如果需要从高端内存进行分配,我们知道高端内存(物理地址高于896MB)中的页被映射到3GB-4GB上,我们应该使用alloc_pages获得页指针,而不能用__get_free_pages或kmalloc,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟空间。如果需要将获得的页映射到内核地址空间,可以使用kmap函数,这个函数可以睡眠,因此只能在进程上下文使用,因为允许映射的数量是有限的,所以当不需要高端内存时,应该用kunmap解除映射。如果需要获得的页临时映射到内核地址空间,可以使用kmap_atomic函数,它不能睡眠,可以用在中断处理程序中,同样释放用kunmap_atomic函数。

  如果需要创建和撤销很多大的数据结构,需要考虑建立slab高速缓存了,它能极大提高对象分配和回收的性能。Slab层不是频繁地分配和释放内存,而是为你把事先分配好的对象存放到高速缓存中,当你需要一块新的内存来存放数据结构时,slab层一般无须另外去分配内存,而只需要从高速缓存中得到这个对象。

  Slab层把不同的对象分为高速缓存组,每个高速缓存组中存放不同类型的对象,也是每个对象对应一个高速缓存。每一个高速缓存又被划分成多个slab,slab由一个或多个物理上连续的页组成。每个slab处于三种状态之一:满、部分满或空,当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配,如果没有部分满的slab,从空的slab中进行分配,如果没有空的slab,要创建一个slab,这样能减少碎片。每一个高速缓存用struct kmem_cache结构体表示,同时slab也有struct slab结构体。

  Slab分配器的使用

  struct kmem_cache *task_struct_cachep;

  task_struct_cachep=kmem_cache_create(“task_struct”,sizeof(task_struct),ARCH_MIN_TASKALIGN,SLAB_PANIC|SLAB_NOTRACK,NULL); //创建高速缓存

  struct task_struct *tsk;

  tsk=kmem_cache_alloc(task_struct_cachep,GPF_KERNEL); //从缓存中分配

  …..

  kmem_cache_free(task_struct_cachep,tsk);  //从缓存中释放

  kmem_cache_destroy(task_struct_cachep);  //撤销高速缓存

  内核栈

  每个进程都有两个页的内核栈,32位和64位体系结构的页面大小分别是4KB和8KB,所以内核栈的大小分别是8KB和16KB。由于连续两页有时难以寻找到,所以引入单页内核栈,当我们使用只有一个页面的内核栈的时候,中断处理程序不放在栈里了,我们为每个进程提供一个用于中断处理程序的栈,即中断栈,这样中断处理程序不用再和被中断进程共享一个内核栈。总的来说,内核栈可以是1页页可以是2页,栈的大小因此在4-16KB的范围,历史上,中断处理程序和被中断的进程共享一个栈,不过当1页栈被激活,中断处理程序获得自己的栈。