系统调用是指使用类似函数调用的方式调用操作系统提供的API。虽然从概念上来说系统调用和函数调用差不多,但本质上它们有很大的不同(call vs int/syscall)

  1. 操作系统的代码位于内核地址空间,而CPU在执行用户代码时特权等级很低,无权访问需要最高优先级才能访问的内核地址空间的代码和数据,所以不能通过简单的call指令直接调用操作系统提供的函数,而需要使用特殊的指令进入操作系统内核完成指定的功能。
  2. 用户代码调用操作系统API不是根据函数名直接调用,而是需要根据操作系统为每个API提供的一个整型编号来调用,AMD64 Linux平台约定在进行系统调用时使用rax寄存器存放系统调用编号(PS:还有专门的寄存器),同时约定使用rdi, rsi, rdx, r10, r8和r9来传递前6个系统调用参数。
  3. 函数调用只需要切换PC 及栈寄存器 SP等几个寄存器,系统调用则涉及到整个cpu上下文(所有寄存器)的切换。不过并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。系统调用属于同进程内的 CPU 上下文切换,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

线程调度:操作系统什么时候会发起调度呢?总体来说操作系统必须要得到CPU的控制权后才能发起调度,那么当用户程序在CPU上运行时如何才能让CPU去执行操作系统代码从而让内核获得控制权呢?一般说来在两种情况下会从执行用户程序代码转去执行操作系统代码:

  1. 用户程序使用系统调用进入操作系统内核;
  2. 硬件中断。硬件中断处理程序由操作系统提供,所以当硬件发生中断时,就会执行操作系统代码。硬件中断有个特别重要的时钟中断,这是操作系统能够发起抢占调度的基础。

程序=数据结构 + 算法。调度器就是 基于 g/p/m/sched 等struct,提供初始化方法 schedinit ==> mcommoninit –> procresize –> newproc。代码go 生产g,在每个m 执行执行 mstart => mstart1 ==> schedule 来消费g

协程切换过程 schedule()

Goroutine 调度过程_虚拟内存

在大多数情况下都会调用 schedule 触发一次 Goroutine 调度,这个函数的主要作用就是从不同的地方查找待执行的 Goroutine:

func schedule() {
    _g_ := getg()
top:
    var gp *g
    var inheritTime bool
    // 有一定几率会从全局的运行队列中选择一个 Goroutine;为了保证调度的公平性,每个工作线程每进行61次调度就需要优先从全局运行队列中获取goroutine出来运行,
// 因为如果只调度本地运行队列中的goroutine,则全局运行队列中的goroutine有可能得不到运行
    if gp == nil {
        if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
            lock(&sched.lock)
            gp = globrunqget(_g_.m.p.ptr(), 1)
            unlock(&sched.lock)
        }
    }
    // 从当前处理器本地的运行队列中查找待执行的 Goroutine;
    if gp == nil {
        gp, inheritTime = runqget(_g_.m.p.ptr())
        if gp != nil