1、概述
1.1 进程相关
1.1.1 进程和线程的区别
进程
1.进程是处于执行期的程序,进程=程序+执行
2.进程是资源封装的单位,拥有独立的资源空间,包含很多资源:打开的文件、挂起的信号量、内存管理、处理器状态、一个或多个执行线程或数据段等;
3.进程通常通过fork系统调用来创建;
4.新创建的进程可以通过exec创建地址空间(用户栈初始化等),并载入新的可执行程序
5.进程退出可以自愿退出或非自愿退出。
线程
1.线程是轻量级进程,是操作系统调度的最小单位
2.一个进程可以有多个线程线程共享进程的资源空间
3.线程通过clone方法来创建,会确定哪些资源与父进程共享,哪些资源线程独享
1.1.2 获取当前进程
在内核态,ARM64运行级别为EL1,SP_EL0寄存器在EL1上下文没有使用,利用SP_EL0寄存器存放当前进程描述符**task_struct
**的地址。
1.1.3 进程的3个数据结构
进程是资源封装的单位,linux有3个数据结构来组织进程描述符:
(1) 链表:所有task_struct
形成一张链表
(2) 树:所有task_struct
指向它的父、兄弟、子,所有task_struct形成树,pstree
可以看出进程树情况,父进程类似于子进程的监视器,子进程死父进程通过查看子进程的尸体(task_struct)可以知道子进程死亡原因,并将其清理
(3) 哈希表,通过PID可以快速检索出task_struct
1.1.4 进程的生命周期
TASK_RUNNING(运行和就绪)
:进程要么在CPU上执行,要么准备执行;
TASK_INTERRUPTIBLE(浅度睡眠)
:除了等IO ready唤醒,信号也可以唤醒,进程状态修改为TASK_RUNNING;
TASK_UNINTERRUPTIBLE(深度睡眠)
:一定要等到IO ready唤醒,不能被异步信号打断的挂起状态, 对于不可中断的执行流程会用到这个状态;
TASK_STOPPED(暂停)
:当进程收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU信号会暂停;
TASK_ZOMBLE(僵尸状态)
: 所有的资源都被free 了,只剩下task_struct了。因为linux认为应用态有很多不可以预知的行为导致进程挂掉,因此它会在进程挂掉时自动释放进程资源,只保留task_struct来给父进程看原因,一旦父进程通过waitpid返回则task_struct也被释放。
1.2 进程调度相关
1.2.1 调度器
通过调度策略来选用调度器,每个调度器都定义了一个调度类,每个调度类中实现了本调度器的关键算法。
所有的调度类链接在一起并通过优先级排列,大内核调度器(scheduler)调度的时候将按照调度类的优先级进行调度。
调度器是一个操作系统的核心部分。可以比作是CPU时间的管理员。调度器主要负责选择某些就绪的进程来执行。不同的调度器根据不同的方法挑选出最适合运行的进程。目前Linux支持的调度器主要包括如下:
Stop scheduler、Deadline scheduler、RT scheduler、CFS scheduler、Idle scheduler
优先级从高到低排列为:
stop_sched_class -> dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class
1.2.2 进程抢占
(1)设置TIF_NEED_RESCHED
标志
(2)对于某些情况下,不需要触发抢占,即不需要设置TIF_NEED_RESCHED标志,直接通过调用schedule 执行抢占,如:mutex, semaphore, waitequeue
等执行阻塞操作时会直接调用schedule执行抢占。
2、进程调度
首先简单提一下这个宏和函数的被调用关系:
schedule() --> context_switch() --> switch_to() --> __switch_to()
这里面,schedule
是主调度函数,当schedule()
需要暂停A进程的执行而继续B进程的执行时,就发生了进程之间的切换。进程切换主要有两部分:
1、切换全局页表项;
2、切换内核堆栈和硬件上下文。
这个切换工作由context_switch()
完成。其中switch_to
和__switch_to()
主要完成第二部分。更详细的,__switch_to()主要完成硬件上下文切换,switch_to主要完成内核堆栈切换。
阅读switch_to时请注意:这是一个宏,不是函数,它的参数prev, next, last不是值拷贝,而是它的调用者context_switch()的局部变量。局部变量是通过%ebp寄存器来索引的,也就是通过n(%ebp),n是编译时决定的,在不同的进程的同一段代码中,同一局部变量的n是相同的。在switch_to中,发生了堆栈的切换,即ebp发生了改变,所以要格外留意在任一时刻的局部变量属于哪一个进程。关于__switch_to()这个函数的调用,并不是通过普通的call来实现,而是直接jmp,函数参数也并不是通过堆栈来传递,而是通过寄存器来传递。
2.1 __schedule
在进程管理概述部分介绍过,进程的抢占分为触发抢占(设置TIF_NEED_RESCHED
标记)和执行抢占,执行抢占的过程就是调用了schedule,其核心函数为__schedule
。
2.1.1 schedule调度方法
__schedule()
是调度器的主函数,让调度器执行调度,并进入到__schedule()函数的方法主要有:
1.调用block的函数,如: mutex, semaphore, waitqueue
等等。
2.在中断时返回用户空间时会检查TIF_NEED_RESCHED
标志。例如:可以参考arch/x86/entry_64.S:
为了触发抢占,在定时器中断处理函数scheduler_tick()中,调度器会设置TIF_NEED_RESCHED 抢占标志;
3.唤醒一个进程并不真正会引发执行schedule()。唤醒只是添加一个进程到run-queue,仅此而已;假设,如果一个新的进程被添加到run-queue, 如果抢占当前运行的进程,唤醒操作会设置当前进程的TIF_NEED_RESCHED 标志,在第一次发生如下情形时,schedule()会被调用:
(1)如果kernel允许抢占(CONFIG_PREEMPT=y)
- 在系统调用或异常上下文,preempt_enable()中会执行schedule()抢占调度(这种情形一般是在wake_up()后执行spin_unlock()时);
- 在中断上下文,从中断处理函数返回到被中断的上下文
(2)如果kernel抢占被禁用 (CONFIG_PREEMPT is not set),那么在下一次遇到如下情形时会执行schedule()抢占调度
- cond_resched()调用
- schedule()调用
- 从系统调用或异常返回到用户空间时
- 从中断处理函数返回到用户空间时
2.2 switch_to
switch_to函数的精妙之处在于,switch_to执行的前半部分在旧进程上下文(记为进程A)执行,switch_to的后半部分在新选出进程的上下文(记为进程B)执行。准确的说,切换点位于cpu_switch_to。