# 上下文切换

## 上下文，指的是什么呢？

一个任务的执行，本质上就是CPU执行其指令的过程，每执行一条指令，CPU内部寄存器状态都会发生一点改变，CPU内部寄存器的状态，其实就标识了任务当前的一个执行状态。这里记录任务状态的CPU寄存器信息就是我们常说的上下文。

## 上下文，如何表示呢？

现在，我们了解了上下文其实就是记录任务执行状态的寄存器信息。那表示一个上下文，需要用到CPU中的哪些寄存器呢？全部吗？还是一部分？

CPU中的寄存器数量跟具体CPU型号有关系，但是按照功能大致上可以划分为下面几类：

* 通用目的寄存器（AX\BX\CX\DX...)；
* 段寄存器(CS\DS\SS...)；
* 标识寄存器(EFLAGS)；
* 指令(IR)及指令指针寄存器(IP)；
* 其他寄存器，比如SP\GDTR\LDTR\CR3，等等。

更多信息可以参考\[Intel手册]\(<https://software.intel.com/content/www/us/en/develop/articles/intel-sdm.html>)。

处理器中的寄存器很多，但是并不都是用来记录任务执行状态的，比如IR用于保存指令数据，跟任务执行状态没关系，但是CS:IP记录了下一条待执行的指令，跟任务执行状态有关系。

对于表示任务执行状态的寄存器，在执行上下文切换时，要妥善的将当前任务的这些寄存器的值保存下来，通常会保存到进程地址空间中，或者内核特定的数据结构中，以备下次被调度时还原上下文信息。比如还原CS:IP以继续从上次停下的位置执行，还原SP、BP以让进程继续应用以前的栈空间，等等。

对于进程切换而言，还需要还原CR3寄存器（由内核从特定数据结构中读取并加载到CR3寄存器），它代表了进程的页表起始地址。

需要说明的是，我们这里更聚焦的是进程内的一些上下文切换，而不是通俗意义上的进程、线程上下文切换，有相似之处，但是还是尽早说明下比较好，这样读者再看到下面的一些类型定义时就不至于困惑。或者，您也可以与内核 **task\_struct** 结构体做个对比，会更有体会。

## 上下文，jmp\_buf

我们看下Linux中如何标识进程上下文信息。熟悉Linux下c语言开发的同学，应该知道setjmp, longjmp这两个函数。setjmp将上下文信息记录到`jmp_buf`，longjmp则可还原jmp\_buf中信息到寄存器，实现“跳转”。longjmp可以实现函数间的跳转，这与goto不同，goto只能实现函数内的跳转。

这里的jmp\_buf其实就是表示的栈上下文信息（stack context），以下是i386架构中 `struct __jmp_buf` 的定义，x86\_64架构就先不赘述了，读者可自行查阅。

```c
// 处理器架构：i386 
// Linux/arch/x86/um/shared/sysdep/archsetjmp_32.h
struct __jmp_buf {
unsigned int __ebx; // 通用数据寄存器之一
unsigned int __esp; // 栈指针寄存器(进程栈空间由高地址向低地址方向增长)
unsigned int __ebp; // 基址指针寄存器(记录了当前栈帧的起始地址
                    // 进入一个函数后首先执行的便是：
                    //   push %ebp; 
                    //   mov %esp, %ebp)
unsigned int __esi; // 源变址寄存器
unsigned int __edi; // 目的变址寄存器
unsigned int __eip; // 指令指针寄存器
                    // 程序计数器PC=CS:IP, 确定下一条待执行机器指令的地址
};
typedef struct __jmp_buf jmp_buf[1];
```

如果之前有写过汇编或者反汇编的经历，对这些寄存器应该不会太陌生。函数调用开头一般就是 "push epb", "mov ebp, esp", "sub $size, esp"的操作，函数返回前也常看到ret这种对eip进行修改的指令。

简单说，其实就是这些寄存器基本描述了一个任务的当前执行状态，当这个任务被切换出去的时候，需要保存这些寄存器信息到进程PCB（task\_struct）中，等任务下次重新被调度的时候，再从进程PCB中读取并还原到寄存器，任务就可以不间断地继续执行下去。

现在，我们介绍了任务的上下文如何表示，也了解了上下文如何保存、恢复，我们还需要了解更多信息。

## 上下文，sigjmp\_buf

前面提过了jmp\_buf，jmp\_buf有个不足之处，就是它没有考虑信号处理掩码的问题，glibc在此基础上做了改进，定义了一个新的类型`sigjmp_buf`，对应的也提供了函数sigsetjmp/siglongjmp。

```c
struct __jmp_buf_tag
{
/* NOTE: The machine-dependent definitions of `__sigsetjmp'
assume that a `jmp_buf' begins with a `__jmp_buf' and that
`__mask_was_saved' follows it. Do not move these members
or add others before it. */
__jmp_buf __jmpbuf; /* Calling environment. */
int __mask_was_saved; /* Saved the signal mask? */
__sigset_t __saved_mask; /* Saved signal mask. */
};
typedef struct __jmp_buf_tag jmp_buf[1];
```

sigjmp\_buf不仅保存了处理器硬件上下文信息（jmp\_buf），还保存了信号掩码，这些共同构成了任务的上下文信息。

## 上下文，ucontext\_t

在经历了jmp\_buf、sigjmp\_buf这样的改进之后，还是不能完全满足要求，我们还需要更加精细有力的上下文切换控制，ucontext\_t诞生了。

在System-V系统中，ucontext.h提供了两个新类型和4个新函数。

上下文对应的类型mcontext\_t是与机器相关的、不透明的，它作为ucontext\_t的一个内部成员。ucontext\_t定义则至少包含了如下字段：

```c
typedef struct ucontext {
    // pointer to the context that will be resumed when this context returns
    struct ucontext *uc_link;
    
    // the set of signals that are blocked when this context is active
    sigset_t         uc_sigmask;
    
    // the stack used by this context
    stack_t          uc_stack;
    
    // a machine-specific representation of the saved context
    mcontext_t       uc_mcontext;
    ...
} ucontext_t;
```

上下文相关的4个操作函数：

```c
int  getcontext(ucontext_t *);
int  setcontext(const ucontext_t *);
void makecontext(ucontext_t *, (void *)(), int, ...);
int  swapcontext(ucontext_t *, const ucontext_t *);
```

wikipedia提供了一个非常赞的示例，来演示如何实现任务切换，如下所示。初接触的读者开始会有点懵，可以先试着阅读下代码，猜测下可能的结果。然后我再来解释下这里的逻辑。

```c
#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>

/* The three contexts:
 *    (1) main_context1 : The point in main to which loop will return.
 *    (2) main_context2 : The point in main to which control from loop will
 *                        flow by switching contexts.
 *    (3) loop_context  : The point in loop to which control from main will
 *                        flow by switching contexts. */
ucontext_t main_context1, main_context2, loop_context;

/* The iterator return value. */
volatile int i_from_iterator;

/* This is the iterator function. It is entered on the first call to
 * swapcontext, and loops from 0 to 9. Each value is saved in i_from_iterator,
 * and then swapcontext used to return to the main loop.  The main loop prints
 * the value and calls swapcontext to swap back into the function. When the end
 * of the loop is reached, the function exits, and execution switches to the
 * context pointed to by main_context1. */
void loop(
    ucontext_t *loop_context,
    ucontext_t *other_context,
    int *i_from_iterator)
{
    int i;
    
    for (i=0; i < 10; ++i) {
        /* Write the loop counter into the iterator return location. */
        *i_from_iterator = i;
        
        /* Save the loop context (this point in the code) into ''loop_context'',
         * and switch to other_context. */
        swapcontext(loop_context, other_context);
    }
    
    /* The function falls through to the calling context with an implicit
     * ''setcontext(&loop_context->uc_link);'' */
} 
 
int main(void)
{
    /* The stack for the iterator function. */
    char iterator_stack[SIGSTKSZ];

    /* Flag indicating that the iterator has completed. */
    volatile int iterator_finished;

    getcontext(&loop_context);
    /* Initialise the iterator context. uc_link points to main_context1, the
     * point to return to when the iterator finishes. */
    loop_context.uc_link          = &main_context1;
    loop_context.uc_stack.ss_sp   = iterator_stack;
    loop_context.uc_stack.ss_size = sizeof(iterator_stack);

    /* Fill in loop_context so that it makes swapcontext start loop. The
     * (void (*)(void)) typecast is to avoid a compiler warning but it is
     * not relevant to the behaviour of the function. */
    makecontext(&loop_context, (void (*)(void)) loop,
        3, &loop_context, &main_context2, &i_from_iterator);
   
    /* Clear the finished flag. */      
    iterator_finished = 0;

    /* Save the current context into main_context1. When loop is finished,
     * control flow will return to this point. */
    getcontext(&main_context1);
  
    if (!iterator_finished) {
        /* Set iterator_finished so that when the previous getcontext is
         * returned to via uc_link, the above if condition is false and the
         * iterator is not restarted. */
        iterator_finished = 1;
       
        while (1) {
            /* Save this point into main_context2 and switch into the iterator.
             * The first call will begin loop.  Subsequent calls will switch to
             * the swapcontext in loop. */
            swapcontext(&main_context2, &loop_context);
            printf("%d\n", i_from_iterator);
        }
    }
    
    return 0;
}
```

下面的示例程序中创建了3个ucontext\_t变量，main\_context1在68行被设置，loop函数退出时loop函数绑定的loop\_context将被销毁，程序中会隐式调用setcontext(loop\_context.uc\_link)来切换到父上下文中去执行，即getcontext(\&main\_context1)对应的下一条指令地址处（70行）。

main函数执行时，什么时候激活loop函数呢，在while循环内swapcontext的时候（80行），执行到这里的时候，main函数将当前上下文信息存储到main\_context2，并切换到loop\_context去执行，执行什么呢？50行\~61行定义了loop\_context的父上下文，以及loop\_context绑定的函数执行时要采用的栈空间，当然还绑定了要执行的函数loop以及传递给loop的参数信息。loop函数将使用数组iterator\_stack\[SIGSTKSZ]作为自己的栈空间。

loop执行起来之后，会进入一个循环，每轮循环会更新一下变量i\_from\_iterator的值，然后呢，就执行上下文切换swapcontext(loop\_context, other\_context)，这里的other\_context刚好是main\_context2，切过去之后就是执行语句printf("%d\n", i\_from\_iterator)来完成变量的打印。然后进入while(1)循环，此时又会执行上下文切换继续执行loop函数，loop函数从上次停下来的地方继续执行，这样连续打印了0\~9。

然后，loop函数for循环结束，退出函数，loop\_context结束，此时函数退出前会隐式调用setcontext以切换到父上下文main\_context1去执行，由于前面main函数执行时已经将变量iterator\_finished更新为了1，所以此时即便从main\_context1继续执行，也不会再次进入while循环再次执行loop函数。

最后，程序正常结束。

我们可以通过 `gcc -o main main.c` 编译并且通过 `./main` 来运行，运行会得到如下结果：

```
$ ./main 
0
1
2
3
4
5
6
7
8
9
```

这里的volatile qualifier是为了避免编译器相关的优化，在这个示例中作用不大，如果是多线程+强一致CPU中还有点用。关于volatile的细节可以参考我的另一篇文章：\[你不认识的cc++ volatile]\(<https://www.hitzhangjie.pro/blog/2019-01-07-%E4%BD%A0%E4%B8%8D%E8%AE%A4%E8%AF%86%E7%9A%84cc++-volatile/>)。

总结一下，ucontext\_t提供了更灵活的上下文切换控制，是比jmp\_buf、sigjmp\_buf更好的选择。ucontext\_t的设计初衷，也是为了支持实现一些更加高级的流程控制、fiber、coroutine等。所以协程并不是什么新概念，它只是一种更细粒度的、更轻量的调度实体。

如果只实现协程其实也没什么大用，还需要大量的其他基础设施，如协程之间如何通信，如何做到同步、互斥，等等，这些才是决定好用不好用的问题。所以我们也能理解为什么协程库有五花八门的实现，但是没有哪个很出名的，但是go却能独领风骚，大致就是这个原因吧。

言归正传，我们是否可以直接利用ucontext\_t来实现协程呢？我们还需要考虑下上下文切换的开销才能决定。ucontext\_t的目的是通用，因此其内部mcontext\_t包含了所有的CPU寄存器信息，有些是没必要的，如果每次上下文切换都把寄存器信息保存一下、恢复一下，这里的开销我们人类感觉不到，但是机器是能感觉到的，必须要考虑下，这部分内容我们在下一节介绍。

当前比较火的libgo协程库，之前也是基于ucontext\_t来实现的，不过后面的版本做了些优化，去掉了一些冗余的寄存器的保存、恢复操作，可以参考libgo patches setcontext/getcontext。

> Google's Ian Lance Taylor explained in the commit improving libgo, "Currently, goroutine switches are implemented with libc getcontext/setcontext functions, which saves/restores the machine register states and also the signal context. This does more than what we need, and performs an expensive syscall. This CL implements a simplified version of getcontext/setcontext, in assembly, that only saves/restores the necessary part, i.e. the callee-save registers, and the PC, SP. A simplified version of makecontext, written in C, is also added. Currently this is only implemented on Linux/AMD64."

## 参考资料

1. setcontext, <https://en.wikipedia.org/wiki/Setcontext>
2. libgo cheaper context switch on x86\_64,  <https://gcc.gnu.org/legacy-ml/gcc-patches/2019-05/msg02155.html>
3. linux man(2)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://hitzhangjie.gitbook.io/libmill/basics/context-switching.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
