1.7 linux 内核管理子系统
1.7.1 内存管理子系统
1,管理模型
内存管理系统可以分为两部分,分别是内核空间内存管理和用户空间内存管理:
内存管理子系统的职责是:进程请求内存时分配可用内存,进程释放内存后回收内存,以及跟踪系统内存使用情况。现代操作系统要求能够使多个程序共享系统资源,同时要求内存限制对于开发者是透明的。在这种情况下,虚拟内存应运而生。虚拟内存可以使得进程可以访问比实际内存大得多的空间,并且使得多个程序共享内存显得更加有效。
当程序从内存中取得数据的时候,需要使用地址指出需要访问的内存位置(注意:这个地址是虚拟地址,他们组成的进程的虚拟地址空间)。每个进程都有自己的虚拟地址空间,这样做的好处是可以防止非法读取或覆盖其他进程的数据(虚拟地址允许进程使用超过物理内存的内存空间,因此操作系统可以给每个进程提供独立的虚拟线性地址空间)。
页:作为内存管理的基本单元,页的许多状态需要被记录下来(比如,内核需要知道什么时候可以被回收),因此内核为内核中的每个页都准备了页描述符struct page{}。系统在初始化时根据物理内存的大小建立起一个page结构数组mem_map,作为物理页面的“仓库”。
2,物理内存分配
物理地址有896M直接映射到虚拟地址的内存空间,这是一一对应的映射,只有起始地址不一样,偏移是一样的。这个大小大多是固定的,哪怕你的内存超过1G,太小了就另外说了,当你内存很大的时候,超过896M时,剩余的那些内存怎么办呢?这多出来的叫做高端内存,如果你使用vmalloc申请空间,就会在高端内存中分配,如果你使用kmalloc申请空间,就会在小于896的内存中分配。所以还是很讲究的。
伙伴系统(伙伴算法):每当页面被分配和回收的时候,系统都会遇到外部碎片或内存碎片的问题(即页面散布在内存中,即使可用页面足够多,但是无法分配大块的连续页面)。为了解决这个问题,Linux系统提供了伙伴算法。伙伴算法原理:伙伴系统把内存中空闲块组成链表,将不同大小的空闲内存块组织起来,虽然大小不一样,但是都是2的幂次方。当系统中有进程释放内存的时候,伙伴系统就会搜索与所释放块大小相等的可用空闲内存块,如果找到相邻的空闲块,就将其合并成两倍于自身大小的块。这种合并的块称为伙伴。
slab分配器:为了减少内存分配、初始化、销毁和释放的代价,通常会把经常使用的内存区以缓存的方式对待,并加以维护(即比如系统经常会使用task_struct,这就将该结构体以缓存方式常驻内存),当进程不在需要该内存区时,就会把该内存放入缓冲区。由此可见,slab实际上由许多缓存组成。缓存分为 “专用”和“通用”。专用缓存保存特定对象的内存区,比如各种描述符,比如进程描述符"struct task_struct"。
3,虚拟地址转换为物理地址
首先将32位的虚拟地址的高10位取出来作为偏移,这个偏移加上CR3寄存器里面的一级页表基地址,就是存储二级页表基地址的单元的地址,根据该单元存储的二级页表的基地址找到页表,然后取出32位虚拟地址的中间10位作为偏移,将二级页表的基地址和偏移相加得到物理页表的基地址的存储单元的基地址,从该单元取出物理页表的基地址加上32位虚拟地址的低12位就是物理页表的物理地址。
只有实实在在的去访问虚拟地址所对应的内存时,才会分配内存,如果不访问,则拿到的只是一个虚拟地址。
1.7.2 进程管理子系统
1,程序与进程
程序:存放在磁盘上的一系列代码和数据的可执行映像,是一个静止的实体。
进程:是一个执行中的程序,它是动态的实体。
2,进程的四要素
(1)有一段程序供其执行。这段程序不一定是某个进程所专有,可以与其他进程共用。
(2)有进程专用的内核空间堆栈
(3)在内核中有一个task_struct 数据结构,即通常所说的“进程控制块”。有了这个数据结构,进程才能成为内核调度的一个基本单位,接受内核的调度。
(4)有独立的用户空间。
3,进程的状态
4,进程描述
在Linux内核代码中,线程、进程都使用结构task_struct(sched.h)来表示,它包含了大量描述进程/线程的信息,其中比较重要的有:
pid_t pid;//进程号
long state;//进程状态
int prio;//进程优先级
5,进程调度
从就绪的进程中选出最适合的一个来执行。
(1)调度策略
SCHED_NORMAL(SCHED_OTHER):普通的分时进程
SCHED_FIFO:先入先出的实时进程
SCHED_RR:时间片轮转的实时进程
SCHED_BATCH:批处理进程
SCHED_IDLE:只在系统空闲时才能够被调度执行的进程
(2)调度时机
(1)主动式
在内核中直接调用schedule()。当进程需要等待资源而暂时停止运行时,会把自己的状态置于挂起(睡眠),并主动请求调度,让出CPU。
范例:
current->state = TASK_INTERRUPTIBLE;
schedule();
(2)被动式
被动式调度又名抢占式调度。分为:用户态抢占(Linux2.4、Linux2.6)和内核态抢占(Linux2.6)。
1,用户抢占
用户抢占发生在:a.从系统调用返回用户空间;b.从中断处理程序返回用户空间。
内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,即发生用户抢占。
a.当某个进程耗尽它的时间片时,会设置need_resched标志
b.当一个优先级更高的进程进入可执行状态的时候,也会设置need_resched标志。
2,内核抢占
用户态抢占缺陷:进程/线程一旦运行到内核态,就可以一直执行,直到它主动放弃或时间片耗尽为止。这样会导致一些非常紧急的进程或线程将长时间得不到运行,降低整个系统的实时性。
改进方式:允许系统在内核态也支持抢占,更高优先级的进程/线程可以抢占正在内核态运行的低优先级进程/线程。
内核抢占可能发生在:a.中断处理程序完成,返回内核空间之前;b.当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等。
在支持内核抢占的系统中,某些特例下是不允许抢占的:
(1). 内核正在运行中断处理。
(2). 内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。
(3). 进程正持有spinlock自旋锁、writelock/readlock读写锁等,当持有这些锁时,不应该被抢占,否则由于抢占将可能导致其他进程长期得不到锁,而让系统处于死锁状态。
(4). 内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。
抢占计数
为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占计数。这一变量被设置在进程的thread_info结构中。每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_count就减1,同时进行可抢占的判断与调度。
(3)调度步骤
Schedule函数工作流程如下:
(1)清理当前运行中的进程。
(2)选择下一个要运行的进程。
(3)设置新进程的运行环境。
(4)进程上下文切换。