我感觉学习操作系统首先要从内存分配和管理入手。
首先我们应该知道现代操作系统是以页为单位进行内存管理的,32位体系结构支持4KB的页。而64位体系结构支持8KB的页。页是用来分配的。怎样才干进行高效和充分的利用。这是内存管理单元(MMU)应当细致考虑的。
页分配
内核用结构体struct page表示每一个物理页。内核用这一结构来管理系统中全部的页,由于内核须要知道一个页是否空暇(也就是页有没有被分配)。假设页已经被分配,内核须要知道谁拥有这个页,拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页快速缓存。我们用页表来统一管理全部的struct page。另外内核用区对具有相似特性的页进行分组。
主要是存在以下两种制约:
- 一些硬件仅仅能用某些特定的内存地址运行DMA(直接内存訪问)
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。
这样,就有一些内存不能永久地映射到内核空间(高端内存)
- ZONE_DMA——这个区包括的页能用来运行DMA操作。
- ZONE_DMA32——同上,只是仅仅能被32位设备訪问。
- ZONE_NORMAL——这个区包括的都是能正常映射的页。
- ZONE_HIGHEM——高端内存,当中的页并不能永久映射到内核地址空间。
slab层
用于频繁使用的数据结构的缓存。且避免因频繁分配和使用导致的内存碎片。slab层是由快速缓存组成的,而每一个快速缓存能够由多个slab组成,slab由一个或多个物理上连续的页组成。每一个slab都包括一些缓存的数据结构。这样说还是非常抽象,举个inode的样例。
inode是磁盘文件在内存中的体现,会频繁地进行创建和释放,所以有必要进行缓存管理。在这里快速缓存是inode_cachep。它由多个slab组成,而每一个slab包括尽可能多的struct inode对象。
所以当我们须要一个新的inode结构时,不必现创建,仅仅需从部分满或空的slab返回一个指向已分配但未使用的inode结构的指针就可以。当内核使用完这个inode对象时。slab分配器就把该对象标记为空暇,留给后来者。
再举个进程控制块的样例。
我们知道进程在不停地创建和消除,而用struct task_struct去管理一个进程。不停的创建和释放task_struct会非常费时。所以内核初始化期间,在fork_init()中着手创建快速缓存:
struct kmem_cache *task_struct_cachep;(内核用这个全局变量存放指向task_struct快速缓存的指针) task_struct_cachep = kmem_cache_create("task_struct", sizeof(struct task_struct), ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL);
这样当我们创建进程(运行fork)时,仅仅需从这个快速缓存中索取就可以:
struct task_struct *tsk; tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL); if(!tsk) return NULL;
内核栈
我们在进程时要注意节省栈资源。要控制函数内的局部变量。尽量不要出现大型数组或大型结构体。尤其对于内核栈,一旦造成溢出,就会影响到内核数据(如thread_info)。所以应当优先考虑动态分配。另外一个进程的内核栈和中断栈是分开的,这样能够减轻内核栈的负担(一个内核栈仅仅占1页或2页)。