您的当前位置:首页正文

操作系统的调度基础

2024-11-09 来源:个人技术集锦

操作系统的cpu调度

把内核线程当成内核中的一个进程去理解。

任务系统的三个核心特征是:权限分级、数据隔离和任务切换。以X86_64架构为例,权限分级通过CPU的多模式机制和分段机制实现,数据隔离通过分页机制实现,任务切换通过中断机制和任务机制(TR/TSS)实现。

用户态内核态: 在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。

栈(stack): 栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

用户级线程: 用户级线程是指有关线程的管理工作都是由应用程序完成,内核意识不到用户级线程的存在,也不会对这些用户级线程进行调度。如果某个进程创建了多个用户级线程,那么所有这些用户级线程仅有一个对应的内核线程,在用户级线程策略中,内核是以进程为单位进行调度的,不管进程内有多少线程,内核一次只把一个进程分配给一个处理器,因此一个进程中只有一个线程可以执行,所以只有一个对应的内核线程。

内核级线程: 内核级线程是指由内核管理、只运行在内核态、不受用户态上下文拖累的线程。其依赖于操作系统核心,由内核的内部需求进行创建和撤销。内核线程的线程表位于内核中,包括了线程控制块,一旦线程阻塞,内核会从当前或者其他进程中重现选择一个线程保证程序的执行。用户应用程序通过API和系统调用(system call)来访问内核级线程。

内核级线程和用户级线程的关系: 内核线程是由操作系统维护的线程对象。它是能够被处理器调度和执行的实际线程。通常,系统线程是具有权限设置,优先级等的重量级对象。内核线程调度器负责调度内核线程。用户线程必须与内核线程相关联的原因在于用户线程本身只是用户程序中的一堆数据。内核线程是系统中的真正线程,所以为了让用户线程进步,用户程序必须让其调度程序接受用户线程,然后在内核线程上运行它。用户线程和内核线程之间的映射不一定是一对一(1:1)映射;您可以拥有多个用户线程共享相同的内核线程(每次只运行一个用户线程),并且您可以拥有单个用户线程,该线程在不同的内核线程(1:n)映射之间进行旋转。 内核级线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。

进程和线程在内核区别: 对于 Linux 来讲,所有的线程都当作进程来实现,因为没有单独为线程定义特定的调度算法,也没有单独为线程定义特定的数据结构(所有的线程或进程的核心数据结构都是 task_struct)。把内核线程当成内核中的一个进程去理解。

多核: 内核空间实现为每个内核支持线程设置了一个线程控制快,内核是根据该控制快而感知某个线程是否存在,并加以控制。设置了内核支持线程的系统,其调度是以内核线程为单位进行的。如果是用户级线程,操作系统看不到,就没法分配资源,无法发挥多核的价值。其中核心级线程不是两个栈,而是两套栈。因为用户级线程只会在用户栈里跑,但是使用核心级线程的程序,既需要在用户层跑,也需要在内核里跑。用户级线程的切换,是两个栈之间的切换。核心级线程的切换不是两个栈,而是两套栈。因为核心级线程必须到内核态,在用户态使用用户栈,在内核态不也得调用函数,也得使用一个栈,既要在用户态又得在内核态跑,一个核心级线程要两个栈组成的一套栈。

内核线程池Pool模型:当从⽤户态代码进⼊系统态代码调⽤的时候会涉及到上下⽂切换,这是要付出⼀定的代价的。很显然系统线程去创建去调度是要付出这些代价的,所以很多时候系统线程成本会⾮常的⾼,当我们频繁的去创建系统线程销掉系统线程这种代价实在太⼤了。在这基础上往往会实现这样的模型。在⽤户态抽象很多个执⾏单位,我们把这些⽤户态线程映射到少量的系统线程上⾯去,然后建⽴类似于 Pool 这样的⼀个概念可以复⽤的。内核态的系统线程专⻔负责执⾏,⽽⽤户态的线程负责存储状态,⽐如说线程栈状态,所有线程执⾏的线程栈是⽤来保存当时执⾏线程状态的,还包含寄存器相关的信息、局部变量,这样的好处是我们把建成Pool以后就不需要频繁的创建系统线程,只需要⽤户态去创建各种各样我们所需的这种抽象的专⻔⽤来存储状态的这种⽤户态线程,我们可以创建很多个,当我们创建好当需要执⾏的时候,把它绑定到⼀个系统线程上⾯去,然后去执⾏执⾏完了以后可以把这个系统线程释放掉,系统线程回到Pool⾥⾯只需要把这个状态杀掉,我们不需要消灭这个系统线程。

PCB与TCB: 在早期的操作系统实现中并不存在线程的概念,与之对应的是进程。也就是说当时的操作系统粗暴的将一个任务整体的描述为一条执行线,操作系统以任务为单位进行资源分配,并以任务为单位进行运行调度。这样分配的结果便是,每次进行运行调度即进程切换时,也要同时切换资源的权限,造成非常大的开销。于是后来的操作系统将资源分配单位和任务调度单位分离开来,也就是我们现在所说的进程与线程。以进程为单位分配系统资源,以线程为单位进行任务调度。PCB中包含了资源分配信息和运行调度信息。其中:进程状态、CPU排班法为进行运行调度时的依据,CPU寄存器、程序计数器为运行调度时保存和恢复现场的依据,存储管理器、会计信息、输入输出状态则是对进程拥有的资源的描述。对于TCB来说,不同操作系统有着不同的实现,但大致都是仅包含了运行调度时所需的信息,如线程状态、调度算法、CPU寄存器、PC计数器等。**Linux的进程控制块为一个由结构task_struct所定义的数据结构可以理解为PCB,而没有TCB的概念。**在linux中,线程和进程共用了一种数据结构(task_struct)。也就是说,linux并没有为线程设计另外的数据结构。linux中的线程是由进程模拟的。所以,linux中没有真正意义上的线程,相当于“假”的线程。注:windows操作系统中,线程就是真正意义上的线程。每一个线程都有一个”tcb”,每一个进程则都有一个”pcb”,两者各有自己的数据结构来表示。PCB的组织方式为链表:同一状态的进程其PCB成一链表,多个状态对应多个不同的链表。进程控制块PCB的内容

  • 进程描述信息:

    进程标识符(process ID),唯一,通常是一个整数;

    进程名,通常基于可执行文件名(不唯一);

    用户标识符(user ID);进程组关系(process group)

  • 进程控制信息:

    当前状态;

    优先级(priority);

    运行统计信息(执行时间、页面调度(缺页);

    进程间同步和通信;阻塞原因

  • CPU现场保护结构:寄存器值(通用、程序计数器PC、状态PSW(program status word),栈指针。

进程和线程切换的区别: 线程的切换只是指令的切换,而进程的切换还包括资源区的切换。进程切换:涉及PCB的修改。当运行的进程1要切换到进程2时,将当前CPU的状态用于更新进程1的PCB,即记录进程1运行的上下文。然后将进程2的PCB读入CPU,执行进程2。每个线程都需要创建一个自己的栈。在切换线程的时候需要切换栈。这就需要一个数据结构TCB(线程控制块)来存储栈的指针;每一个线程都有一个TCB,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构TCB。里面保存所有将来用于恢复线程环境所必须的信息,包括所有的寄存器值,线程的状态等等

  • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据

进程切换和系统调用的区别:

  • 系统调用是从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
    在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:
    系统调用的切换流程:系统调用触发中断,进入中断处理程序。若中断时,进程在执行用户态的代码,该中断会引起CPU特权级从ring3级到ring0级的切换,CPU会进行堆栈的切换,CPU会从当前任务的TSS中取到内核栈的段选择符和偏移值;CPU首先会上下文保存到内核栈,把原用户态的堆栈指针ss和esp压入内核态堆栈,随后把标志寄存器eflags的内容和此次中断的返回位置cs,eip压入内核态堆栈。当中断处理函数结束后,将恢复内核栈中的数据,并继续处理被中断的进程。每个系统调用内核要进行许多工作,耗时开销大。
    所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)。系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。所以,系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换。
  • 进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。简单来说是进程切换主要包括两部分工作: 切换全局页目录以加载一个新的地址空间、切换内核栈和硬件上下文,其中硬件上下文包括了内核执行新进程需要的全部信息,如CPU相关寄存器。

go协程调度器: Go调度器是Go runtime的一部分,且Go runtime已经内置于Go程序中。这意味着Go调度器运行在用户态,而不是内核态。当前Go调度器的实现不是抢占式,而是协作式的。协作式调度意味着调度器在做调度决策时,需要明确定义的用户态事件,这些事件发生在代码中的安全点。Go协作式调度器的出色之处就在于它看起来是抢占式的:你无法预测Go调度器将要执行的操作。这是因为协作调度器的决策权并不在开发者手中,而是在Go runtime上。把Go调度器看作是抢占式的,因为它的调度是不确定的,这是很重要的,并且这没什么大不了的。每一个Go程序都自带一个runtime,负责与OS进行交互,⽤户态只需要创建像⼤量的并发任务,中间通过调度器来实现这两个用户和内核上的绑定。

显示全文