进程管理
进程概述
thread_info:保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,放在内核栈顶,其中嵌入指向task_struct的指针,arm64架构通过current宏直接得到task_struct得到thread_info的地址,可以看出是直接存在sp_el0寄存器中的
再看下include/asm-generic/current.h内核通用版本的实现
task_struct:通用的方式描述进程,内核使用slab管理task_stuct的分配和释放,包含了mm_struct这个内存描述结构
thread_union:将内核栈(向下增长)和thread_info(所以在栈顶)放在这个联合体
再看下系统调用进入内核之前会把系统调用号及参数啊放进寄存器,进入内核后通过save_all放入内核栈;由于是下图是引用别人的,stack指针讲道理指向栈顶指针可能更好理解
进程定义:一个处于执行期的程序及其相关资源(打开的文件,地址空间,挂起的信号,进程状态等)的总称
进程状态:TASK_RUNNING,TASK_INTERRUPTABLE,TASK_UNINTERRUPTABLE,__TASK_TRACED,__TASK_STOPPED,TASK_ZOMBIE,TASK_DEAD;前五个状态可以通过set_task_state设置
进程创建:fork没有复制所有信息,而是在被使用时才复制(写时拷贝),fork() -> clone() -> do_fork() -> copy_process(创建新的task_struct) -> dup_task_struct(分配一个内核栈,填充thread_info,task_struct)
线程创建:和创建进程类似,只不过在调用 clone 的时候,传入了一些参数标志,而且共享了地址空间、文件系统资源、文件描述符、信号处理程序fork():返回0的是子进程,大于0的是父进程,小于0则出错
内核进程创建:kthread_create;它没有独立地址空间,也不能到用户空间执行,可被调度和抢占
1 : 构造了一个kthread_create_info 结构体,将其挂接到 kthread_create_list链表上
2 : 唤醒内核线程kthreadd去创建线程
进程终结:当通过显式或隐式调用exit()主动退出,或者被信号或异常被动终结时,进程大部分都是靠do_exi来结束,此时处于僵死态,但它还占有内核栈,thread_info结构体,task_struct结构体;以向父进程提供信息,让其检索之后(调用wait()),将最后占用的内存释放掉
进程调度
调度器
分配进程运行时间,并通过调度算法,切换两个进程的上下文
优先级
静态优先级:范围为100-139(MAX_RT_PRIO - MAX_PRIO-1),内核2.6中的静态优先级相当于内核2.4中的nice值(-20,19),但转到MAX_RT_PRIO到MAX_PRIO-1取值范围,其公式为:static priority = nice + 20 + MAX_RT_PRIO;内核定义两个宏来实现此转化:nice_to_prio() and prio_to_nice();实时和非实时进程都一样可通过nice()和setpriority()改变,以影响基时间片
动态优先级:范围与静态优先级相同。动态优先级是调度器选择一个进程时实际参考的值。平均睡眠时间越大,bonus值越大,其动态优先级越大;dynamic priority = max( 100, min((static priority - bonus + 5), 139));实时进程的动态优先级不会改变
实时优先级:取值范围0-MAX_RT_PRIO-1。其大小可以通过sched_setscheduler()和sched_setparam()来改变
调度器类
允许不同的可动态添加的调度算法并存,总调度器根据调度器类的优先顺序,依次调度进行调度器类的中的进程,Stop_ask > RealTime > Fair > Idle_Task,通过sched_setscheduler可设置调度算法
CFS:完全公平调度器,通过一个虚拟时钟 vruntime= 实际运行时间 * 1024 / 进程权重(nice值为0,权重为1024),总是选择 vruntime 最小的进程,让其投入执行。他们被维护到一个以 vruntime 为顺序的rbtree 中;分配的可运行时间 = 调度周期 *(进程权重 / 所有进程权重之和)
FIFO:是让一个任务运行完再调度下一个任务,而顺序就是依照创建的先后,只有更高优先级的 SCHED_FIFO 或者 SCHED_RR 才能抢占它的任务
RR:依据时间片来调度的,当时间片用完时,无论这个进程优先级有多高,都不会在执行,而是进入就绪队列,等待下一个时间片到来
进程切换
自愿切换发生的时候,进程不再处于运行状态,比如由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态。
强制切换发生的时候,进程仍然处于运行状态(TASK_RUNNING),通常是由于被优先级更高的进程抢占(preempt),或者进程的时间片用完了。
进程自愿切换(Voluntary)和强制切换(Involuntary)的次数被统计在 /proc/<pid>/status,查看此值,判断进程对cpu的需求度。
触发抢占
给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换;触发抢占的时机如下:
周期性的时钟中断:调用scheduler_tick()检查进程的时间片是否耗尽,如果耗尽则触发抢占
唤醒进程的时候:如果优先级高于CPU上的当前进程,就会触发抢占;try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占
新进程创建的时候:如果新进程的优先级高于CPU上的当前进程,会触发抢占。相应的调度器核心层代码是sched_fork(),它再通过调度类的 task_fork方法触发抢占
进程修改nice值的时候:如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()
进行负载均衡的时候:进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占
执行抢占
从系统调用(syscall)返回用户态时
从中断返回用户态时
中断返回:中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占。preempt_schedule_irq()是对schedule()的包装。
使能抢占:当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候;
在preempt_enable()中,会最终调用 preempt_schedule 来执行抢占