Skip to content

Commit ddcbe4c

Browse files
committed
add signal
1 parent 6154de2 commit ddcbe4c

File tree

3 files changed

+188
-3
lines changed

3 files changed

+188
-3
lines changed

1.14/signal_based_preemption.md

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,192 @@
11
# 1.14 scheduler
22

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+
![](../images/signal.png)
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+
3122
## 流程概述
4123

5-
## 信号处理与 gsignal
124+
抢占流程主要有两个入口,GC 和 sysmon。
6125

7-
## 现场保存和恢复
126+
TODO,control flow pic show
8127

9128
### GC 抢占流程
10129

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,总览图
12187

13188
## 参考资料
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

images/signal.png

300 KB
Loading

map.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,12 @@ search:
667667
}
668668
```
669669

670+
## 缩容
671+
672+
Go 的 map 是不会缩容的,除非你把整个 map 删掉:
673+
674+
https://github.com/golang/go/issues/20135
675+
670676
## 扩容
671677

672678
扩容触发在 mapassign 中,我们之前注释过了,主要是两点:

0 commit comments

Comments
 (0)