0%

context switch

[TOC]

概述

上下文切换(以下简称CS)的定义,http://www.linfo.org/context_switch.html 此文中已做了详细的说明,这里我又偷懒不详细解释了:) 只提炼以下几个关键要点:

  • context(这里我觉得叫process context更合适)是指CPU寄存器和程序计数器在任何时间点的内容
  • CS可以描述为kernel执行下面的操作
      1. 挂起一个进程,并储存该进程当时在内存中所反映出的状态
      2. 从内存中恢复下一个要执行的进程,恢复该进程原来的状态到寄存器,返回到其上次暂停的执行代码然后继续执行
      3. CS只能发生在内核态(kernel mode)
      4. system call会陷入内核态,是user mode => kernel mode的过程,我们称之为mode switch,但不表明会发生CS(其实mode switch同样也会做很多和CS一样的流程,例如通过寄存器传递user mode 和 kernel mode之间的一些参数)
      5. 一个硬件中断的产生,也可能导致kernel收到signal后进行CS

进程上下文切换开销都有哪些

直接开销

直接开销就是在切换时,cpu必须做的事情,包括:

  • 1、切换页表全局目录

  • 2、切换内核态堆栈

  • 3、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)

    • ip(instruction pointer):指向当前执行指令的下一条指令
    • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
    • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
    • cr3:页目录基址寄存器,保存页目录表的物理地址
    • ……
  • 4、刷新TLB

  • 5、系统调度器的代码执行

间接开销

间接开销主要指的是虽然切换到一个新进程后,由于各种缓存并不热,速度运行会慢一些。如果进程始终都在一个CPU上调度还好一些,如果跨CPU的话,之前热起来的TLB、L1、L2、L3因为运行的进程已经变了,所以以局部性原理cache起来的代码、数据也都没有用了,导致新进程穿透到内存的IO会变多。 其实我们上面的实验并没有很好地测量到这种情况,所以实际的上下文切换开销可能比3.5us要大。

为什么说线程太多,cpu切换线程会浪费很多时间?

cpu在执行代码的时候【以下说明只在linux平台上,win我不会】。该程序已经是ELF executable file 且该文件内部按ELF格式存储了机器指令+数据。同时该文件必须引用linux 的核心api库【动态库】libc.so 及linux-x86-64.so 核心文件。启动的时候操作系统会识别该文件的ELF文件头信息【引入的api库提供了核心的execve函数用来执行程序】,进行判断,如果是ELF executable file就会把机器指令+数据装载到内存中去运行。

如果该文件不是elf executable file就会读取该文件的第一行数据并当作解释器来运行。

当程序启动时[如/bin/java demo],会读取后面的源码文件【如java 会execve(bin/java…) 再read 该的elf信息】,后面会打开demo.class文件并读取内容【做各种所谓的骚处理】其它语言【go,py,php】同理。

启动后就是一个进程了并且默认是从主线程开始执行,主线程结束,整个进程结束。如果开启了多个线程【每个线程都有一个入口函数】,当线程数量小于或等于cpu核心数时,理论上是并发执行,否则则是模拟”并发执行“。

当cpu切换到当前进程时执行某个或是某几个【多核时】线程时,可能会原因阻塞,锁等情况,被其它线程抢占运行,那么当前的线程的现场执行的上下文数据就要缓存起来以备切换回来时要能还原运行,而这些数据就要暂存到寄存中,如果线程数量过多,切换频繁,数据来回读写,那么当前进程的一堆线程执行性能就会慢慢下降,这些代码在执行的时候是执行机器指令,大家在源码里的代码虽然可能是一行,但是机器指令却是多条,执行到一半,cpu就切换到其线程中了,那当前线程执行到哪,现在的数据是什么总得存起来,以便后面切换时恢复,来回折腾这样好吗?寄存器不会发火吗?

多线程开得越多,cpu都忙在切换(切换还比较耗时,耗时在 3us 级别)上面了,代码执行的时间就会越来越少,执行一条指令立马被人偷袭抢占切换,当前进程启动的多线程执行时间就会越来越少,等半天才执行几条指令。

一旦线程让出cpu, 调度器就需要判断ready 队列中的所有线程哪些需要支持。当前选择哪个执行。

系统调用

触发条件

从用户态到内核态切换可以通过三种方式,或者说会导致从用户态切换到内核态的操作:

  • 系统调用,这个上面已经讲解过了,在我公众号之前的文章也有讲解过。其实系统调用本身就是中断,但是软件中断,跟硬中断不同。系统调用机制是使用了操作系统为用户特别开放的一个中断来实现,如 Linux 的 int 80h 中断。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,会触发由当前运行进程切换到处理此异常的内核相关进程中
  • 外围设备中断:外围设备完成用户请求的操作之后,会向CPU发出中断信号,这时CPU会转去处理对应的中断处理程序。

开销

当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。

当发生用户态到内核态的切换时,会发生如下过程(本质上是从“用户程序”切换到“内核程序”)

  • 设置处理器至内核态。
  • 保存当前寄存器(栈指针、程序计数器、通用寄存器)。
  • 将栈指针设置指向内核栈地址。
  • 将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址。

而之后从内核态返回用户态时,又会进行类似的工作。

如何避免频繁切换

用户态和内核态之间的切换有一定的开销,如果频繁发生切换势必会带来很大的开销,所以要想尽一切办法来减少切换。这也是面试常考的问题。

3.1 减少线程切换

因为线程的切换会导致用户态和内核态之间的切换,所以减少线程切换也会减少用户态和内核态之间的切换。那么如何减少线程切换呢?

  • 无锁并发编程。多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换。(为什么加锁和释放锁会导致上下文切换,看文末的补充解释)
  • CAS算法。使用CAS避免加锁,避免阻塞线程
  • 使用最少的线程。避免创建不需要的线程
  • 协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

参考

进程上下文切换 – 残酷的性能杀手(上)

http://www.linfo.org/context_switch.html

为什么说线程太多,cpu切换线程会浪费很多时间?

进程/线程上下文切换会用掉你多少CPU?

系统调用