|
1 | 1 | # 1.14 scheduler |
2 | 2 |
|
| 3 | +## 信号概念 |
| 4 | + |
| 5 | +信号是一种发给进程的通知,以告知有事件发生。有时信号也被称为软件中断。从中断用户控制流来说,信号和硬件中断是类似的;在大多场景下,信号何时到达进程是无法预测的。 |
| 6 | + |
| 7 | +信号可以由内核发给用户进程,可以用户进程发给自己,也可以用户进程发给其它的用户进程。 |
| 8 | + |
| 9 | +在进程内部,信号还可以发给某个具体的线程。在 Go 语言中,抢占的信号就是发给指定的线程的。 |
| 10 | + |
| 11 | +## 信号是易失的么 |
| 12 | + |
| 13 | +有一种传言认为信号是易失的,但实际上在早期的 unix 实现中才是易失的。这里的易失说的是完全没有把信号传递给相应的进程。由 POSIX.1-1990 标准定义之后实现的信号基本都是可靠信号了。 |
| 14 | + |
| 15 | +但不易失不代表你向同一个进程重复发送相同的信号 100 次,进程就能收到 100 次这个信号,这是怎么回事? |
| 16 | + |
| 17 | +## 不丢失不代表每次收到的信号都会被触发 |
| 18 | + |
| 19 | +当进程不能处理某个具体的信号时,内核会自动将该信号加入到 signal mask 中,以 block 住后面来的相同信号。 |
| 20 | + |
| 21 | +比如进程正在执行 SIGINT 的 sighandler,还没执行完。这时候 SIGINT 是在 sigmask 中的,如果又来了一个 SIGINT,内核会发现 sigmask 里有 SIGINT,便会把这个 SIGINT 放进 pending signals 的集合里。注意这里是个集合,所以同一个信号在 block 期间如果多次触发,那在 pending signals 的集合里会被去重,unblock 了以后,只会触发一次 sighandler。 |
| 22 | + |
| 23 | +TODO,画个图 |
| 24 | + |
| 25 | +实时信号比较特殊,不过不在我们讨论范围内,感兴趣的可以去看 TLPI。 |
| 26 | + |
| 27 | +## 信号处理 |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +信号处理涉及几个 syscall: |
| 32 | + |
| 33 | +### tigkill |
| 34 | + |
| 35 | +向某个进程的某个线程发信号。 |
| 36 | + |
| 37 | +### sigaction |
| 38 | + |
| 39 | +设置信号执行时的 sighandler, |
| 40 | + |
| 41 | +### sigaltstack |
| 42 | + |
| 43 | +修改信号执行时所用的函数栈。 |
| 44 | + |
| 45 | +## 简单的信号处理函数 |
| 46 | + |
| 47 | +简单起见,这里我们使用 C 来做演示: |
| 48 | + |
| 49 | +## 执行 non-local goto |
| 50 | + |
| 51 | +### goto 的限制 |
| 52 | + |
| 53 | +goto 虽然看起来无所不能,但实际上高级语言的 goto 是跳不出函数作用域的,比如下面这样的代码就没法通过编译: |
| 54 | + |
| 55 | +```go |
| 56 | +TODO |
| 57 | +``` |
| 58 | + |
| 59 | +在其它语言里也一样: |
| 60 | + |
| 61 | +```c |
| 62 | +TODO |
| 63 | +``` |
| 64 | + |
| 65 | +### non-local goto |
| 66 | + |
| 67 | +``` |
| 68 | +#include<stdio.h> |
| 69 | +#define __USE_GNU |
| 70 | +#include<signal.h> |
| 71 | +#include<ucontext.h> |
| 72 | +
|
| 73 | +void myhandle(int mysignal, siginfo_t *si, void* arg) |
| 74 | +{ |
| 75 | + ucontext_t *context = (ucontext_t *)arg; |
| 76 | + printf("Address from where crash happen is %x \n",context->uc_mcontext.gregs[REG_RIP]); |
| 77 | + context->uc_mcontext.gregs[REG_RIP] = context->uc_mcontext.gregs[REG_RIP] + 0x04 ; |
| 78 | +
|
| 79 | +} |
| 80 | +
|
| 81 | +int main(int argc, char *argv[]) |
| 82 | +{ |
| 83 | + struct sigaction action; |
| 84 | + action.sa_sigaction = &myhandle; |
| 85 | + action.sa_flags = SA_SIGINFO; |
| 86 | + sigaction(11,&action,NULL); |
| 87 | +
|
| 88 | + printf("Before segfault\n"); |
| 89 | +
|
| 90 | + int *a=NULL; |
| 91 | + int b; |
| 92 | + b =*a; // Here crash will hapen |
| 93 | +
|
| 94 | + printf("I am still alive\n"); |
| 95 | +
|
| 96 | + return 0; |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +## Go 语言中的信号式抢占 |
| 101 | + |
| 102 | +有了上述的储备知识,我们在看 Go 的信号式抢占前,至少知道了以下几个知识点: |
| 103 | + |
| 104 | +* 信号可以在任意位置打断用户代码 |
| 105 | +* 信号的处理函数可以自定义 |
| 106 | +* 信号的执行栈可以自定义 |
| 107 | +* 信号执行完毕之后默认返回用户代码的下一条汇编指令继续执行 |
| 108 | +* 可以通过修改 pc 寄存器的值,使信号处理完之后执行非本地的 goto,其实就是跳到任意的代码位置去 |
| 109 | + |
| 110 | +### SIGURG |
| 111 | + |
| 112 | +为什么选用了 SIGURG。 |
| 113 | + |
| 114 | +## gsignal |
| 115 | + |
| 116 | +gsignal 是一个特殊的 goroutine,类似 g0,每一个线程都有一个,创建的 m 的时候,就会创建好这个 gsignal,在 linux 中为 gsignal 分配 32KB 的内存: |
| 117 | + |
| 118 | +newm -> allocm -> mcommoninit -> mpreinit -> malg(32 * 1024) |
| 119 | + |
| 120 | +在线程处理信号时,会短暂地将栈从用户栈切换到 gsignal 的栈,执行 sighandler,执行完成之后,会重新切换回用户的栈继续执行用户逻辑。 |
| 121 | + |
3 | 122 | ## 流程概述 |
4 | 123 |
|
5 | | -## 信号处理与 gsignal |
| 124 | +抢占流程主要有两个入口,GC 和 sysmon。 |
6 | 125 |
|
7 | | -## 现场保存和恢复 |
| 126 | +TODO,control flow pic show |
8 | 127 |
|
9 | 128 | ### GC 抢占流程 |
10 | 129 |
|
11 | | -### retake 抢占流程 |
| 130 | +markroot -> fetch g from allgs -> suspendG -> scan g stack -> resumeG |
| 131 | + |
| 132 | +除了 running 以外,任何状态(如 dead,runnable,waiting)的 g 实际上都不是正在运行的 g,对于这些 g 来说,只要将其相应的字段打包返回就可以了。 |
| 133 | + |
| 134 | +running 状态的 g 正在系统线程上执行代码,是需要真正发送信号来抢占的。 |
| 135 | + |
| 136 | +### sysmon 抢占流程 |
| 137 | + |
| 138 | +preemptone -> asyncPreempt -> globalrunqput |
| 139 | + |
| 140 | +### sighandler |
| 141 | + |
| 142 | +```go |
| 143 | +func sighandler(...) |
| 144 | + if sig == sigPreempt { |
| 145 | + doSigPreempt(gp, c) |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +func doSigPreempt(gp *g, ctxt *sigctxt) { |
| 150 | + if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) { |
| 151 | + ctxt.pushCall(funcPC(asyncPreempt)) |
| 152 | + } |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +```go |
| 157 | +func (c *sigctxt) pushCall(targetPC uintptr) { |
| 158 | + // Make it look like the signaled instruction called target. |
| 159 | + pc := uintptr(c.rip()) |
| 160 | + sp := uintptr(c.rsp()) |
| 161 | + sp -= sys.PtrSize |
| 162 | + *(*uintptr)(unsafe.Pointer(sp)) = pc |
| 163 | + c.set_rsp(uint64(sp)) |
| 164 | + c.set_rip(uint64(targetPC)) |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +pushCall 其实就是把用户代码中即将执行的下一条指令的地址(即 pc 寄存器的值),保存在栈顶,然后 sp 寄存器下移(就是扩栈,栈从高地址向低地址增长)。 |
| 169 | + |
| 170 | +TODO,这里需要图 |
| 171 | + |
| 172 | +### 现场保存和恢复 |
| 173 | + |
| 174 | +在 sighandler 修改了信号返回时的 pc 寄存器,所以从 sighandler 返回之后,之前在 running 的 g 不是执行下一条指令,而是执行 pc 寄存器中保存的函数去了,这里就是 asyncPreempt。 |
| 175 | + |
| 176 | +asyncPreempt 是汇编实现的,分为三个部分: |
| 177 | + |
| 178 | +1. 将当前 g 的所有寄存器均保存在 g 的栈上 |
| 179 | +2. 执行 asyncPreempt2 |
| 180 | +3. 恢复 g 的所有寄存器,继续执行用户代码 |
| 181 | + |
| 182 | +TODO,图 |
| 183 | + |
| 184 | +## 总结 |
| 185 | + |
| 186 | +TODO,总览图 |
12 | 187 |
|
13 | 188 | ## 参考资料 |
| 189 | + |
| 190 | +1. The Linux Programming Interface |
| 191 | +2. [non-cooperative goroutine preemption](https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md) |
| 192 | +3. https://stackoverflow.com/questions/34989829/linux-signal-handling-how-to-get-address-of-interrupted-instruction |
0 commit comments