JOS
多CPU支持
支持"symmetric multiprocessing" (SMP), 启动阶段CPU分为BSP和AP,BSP负责初始化系统和启动操作系统,AP由BSP激活。哪一个CPU是BSP由硬件和BISO决定,到目前位置所有JOS代码都运行在BSP上。在SMP系统中,每个CPU都有一个对应的local APIC(LAPIC),负责传递中断。CPU通过内存映射IO(MMIO)访问它对应的APIC,这样就能通过访问内存达到访问设备寄存器的目的。BSP读取mp configuration table中保存的CPU信息,初始化cpus数组,ncpu(总共多少可用CPU),bootcpu指针(指向BSP对应的CpuInfo结构)。然后BSP通过在内存上写值向AP传递中断来启动其他cpu(为他们设置寄存器值等操作)。
CPU私有数据:1.内核栈 2.TSS 3.env 4.寄存器
显然以上私有数据都需要创建新的一份。
多CPU执行内核代码,需要锁来避免竞争,使用CAS机制实现一个内核锁就可以。当然这个粒度太大,在linux内核中有各种粒度的实现。
协作调度
实现yield函数由进程调用主动让出cpu,显然需要将yield注册为一项系统调用,由内核来真正的做切换进程的工作。这里的调度也就是最简单的FIFO。
fork
提供系统调用fork给用户创建进程的能力,fork()拷贝父进程的地址空间和寄存器状态到子进程。父进程从fork()返回的是子进程的进程ID,而子进程从fork()返回的是0。父进程和子进程有独立的地址空间,任何一方修改了内存,不会影响到另一方。
基于写时复制的原理,子进程只需要在一开始拷贝父进程的页目录就可以,当真正触发写操作的时候再在缺页处理函数里做真正的拷贝。因为用户进程中已经有拷贝需要的所有信息(物理页位置等),所以只需要在用户进程中调用用户进程自己的缺页处理函数就可以。所以:
- 需要在进程fork的时候就设置新进程的缺页处理函数
- 同时fork的时候也要对页复制做对应处理(共享,写时复制,只读三种情况不同)
- 因为要在用户进程中处理异常,所以需要新建一个用户异常栈保存用于异常处理的函数需要的参数。
- 缺页中断发生时:trap()->trap_dispatch()->page_fault_handler() 这个page_fault_handler会进入汇编代码然后给用户异常栈赋好值再切换到用户栈,基于刚赋好的值,用户进程会直接执行真正的用户缺页处理函数,在这个却也处理函数里会判断是否因为写时复制导致的触发,是的话就拷贝这个物理页到新的地方然后建立虚拟地址到物理页的映射关系。
- 还有一点需要注意的是内核代码组织十分严格,所以默认内核态不会出现缺页异常,一旦出现可能内核被攻击了,所以在一开始page_fault_handler里需要判断由内核进程触发的话就要panic整个系统。
定时
外部时钟中断强制进入内核,内核判断当前周期到了没,可以将中断号+偏移量来控制时钟周期,到了就触发对应的处理函数。拿时间片轮转调度进程举例:在SMP上首先通过LAPIC来通知各个cpu然后让出进程。
IPC
Inter-Process communication
进程间通信,这里进程间通信使用使两个进程的虚拟地址指向同一块物理页的机制来完成。调用recv的进程阻塞(让出cpu),调用send的进程陷入内核查找对应的recv进程,和其要接受到的虚拟地址,首先将要发送的物理地址找到,然后修改recv进程的要接受到的虚拟地址对应的页表项,将其映射到那个要发送的物理地址处。然后设置接收进程为就绪态等待内核调度。
Linux Kernel
涉及进程调度、锁、进程通信
进程调度
调度器
核心调度器
调度器的实现基于两个函数:周期性调度器函数和主调度器函数。这些函数根据现有进程的优先级分配CPU时间。这也是为什么整个方法称之为优先调度的原因。
a.主调度器函数 在内核中的许多地方,如果要将CPU分配给与当前活动进程不同的另一个进程,都会直接调用主调度器函数(schedule)。 主调度器负责将CPU的使用权从一个进程切换到另一个进程。周期性调度器只是定时更新调度相关的统计信息。cfs队列实际上是用红黑树组织的,rt队列是用链表组织的。
b.周期性调度器函数
周期性调度器在scheduler_tick中实现,如果系统正在活动中,内核会按照频率HZ自动调用该函数。该函数主要有两个任务如下:
- 更新相关统计量:管理内核中与整个系统和各个进程的调度相关的统计量。其间执行的主要操作是对各种计数器加1。比如运行时间。
- 激活负责当前进程的调度类的周期性调度方法。
调度类
为方便添加新的调度策略,Linux内核抽象一个调度类sched_class,允许不同进程有针对性的选择调度算法。
运行队列
每个处理器有一个运行队列,结构体是rq。rq是描述就绪队列,其设计是为每一个CPU就绪队列,本地进程在本地队列上排序。cfs和rt。
调度进程
主动调度进程的函数是schedule() ,它会把主要工作委托给__schedule()去处理。
函数__shcedule的主要处理过程如下:
-
调用pick_next_task()以选择下一个进程。
-
调用context_switch()以切换进程。 调用context_switch:
-
切换用户虚拟地址空间
-
切换寄存器
主动调度
即JOS中的主动让出,依赖系统调用
周期调度
内核依赖时钟来调度,JOS中也有
SMP调度
这个调度旨在
- 均衡多处理器的负载
- 可以设置进程的处理器亲和性,即允许进程在哪些处理器上执行。
- 可以把进程从一个处理器迁移到另一个处理器。
进程的迁移只能发生在同一调度域内,调度域由若干个CPU组成
锁
用于保护内核数据
- 原子操作:这些是最简单的锁操作。它们保证简单的操作,诸如计数器加1之类,可以不中断地原子执行。即使操作由几个汇编语句组成,也可以保证。
- 自旋锁:这些是最常用的锁选项。它们用于短期保护某段代码,以防止其他处理器的访问。在内核等待自旋锁释放时,会重复检查是否能获取锁,而不会进入睡眠状态(忙等待)。当然,如果等待时间较长,则效率显然不高。
- 信号量:这些是用经典方法实现的。在等待信号量释放时,内核进入睡眠状态,直至被唤醒。唤醒后,内核才重新尝试获取信号量。互斥量是信号量的特例,互斥量保护的临界区,每次只能有一个用户进入。
- 读者/写者锁:这些锁会区分对数据结构的两种不同类型的访问。任意数目的处理器都可以对数据结构进行并发读访问,但只有一个处理器能进行写访问。事实上,在进行写访问时,读访问是无法进行的。
IPC
管道
管道可以看为文件,有文件描述符,但是在系统目录树上无法找到,因为它存在于一个特殊的vfs:pipefs(因为是vfs对象,所以无磁盘映像)中。所以在已安装的文件系统中没有相应的映像,可以使用pipe系统调用创建新管道,他返回一对文件描述符,然后进程通过fork将文件描述符传递给它的子进程,由此与进程共享管道。进程使用read读第一个fd,write写第二个fd,一个fd用来读一个用来写。
FIFO
FIFO在文件系统中有磁盘索引节点,虽然不占用数据块但是与内核的一个缓冲区关联,数据就在这个缓冲区里。而且FIFO的fd只有一个,可以read和write使用一个fd。
System V IPC
三种:
- 操作信号量同步其他进程
- 发送消息或接收消息(存放在消息队列中)
- 与其他进程share一段内存(JOS相似)
POSIX消息队列
相比于System V IPC消息队列,POSIX消息队列:
- 基于文件的应用接口
- 支持消息优先级
- 用于阻塞接收发送的超时机制
- 支持消息到达的异步通知(信号或线程创建来实现)