0%

环境:Ubuntu 20.04

代码很简单,在 main 函数里面定义一个长度为 1024K 的数组(一个 int 占 4 个字节):

1
2
3
4
int main()
{
int a[1024 * 1024 / 4] = {};
}

然后编译执行:

1
2
3
4
$ gcc main.cpp 
$ ./a.out

Segmentation fault (core dumped)

原因

为了保护操作系统,防止无限递归这种错误导致内存被耗光,系统对程序的栈大小有一个默认值的限制。

参考:https://stackoverflow.com/questions/1825964/c-c-maximum-stack-size-of-program

实际的限制可以使用 ulimit -s 来查看。

在我本机上,其实是先执行了 ulimit -s 1024 将栈大小限制为 1024K 了,然后我的数组就用了 1024K,另外还有函数调用本身需要保存一些信息(比如返回地址等),所以就超过了 1024K,也就是超出了栈的大小了。

解决方法

有一个解决方法是,将 ulimit -s 的值增大,比如,设置成 8M:

1
ulimit -s 8192

然后重新执行 ./a.out

1
$ ./a.out

再次执行就没有报错了,因为我的栈空间足够大了。

启发

之前使用 swoole 的时候,有时候有 SIGSEGV 这种错误,现在看来或许是有什么地方超出了栈的大小了。

在 linux 下,glibc 提供了几个函数来给用户实现用户态的上下文切换:

  • getcontext:初始化一个上下文参数
  • setcontext:根据参数还原上下文
  • makecontext:创建一个新的上下文
  • swapcontext:上下文切换

getcontext 和 setcontext

详细文档:https://linux.die.net/man/3/getcontext

1
2
3
4
#include <ucontext.h>

int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
  • getcontext: 保存当前上下文到第一个参数
  • setcontext: 设置当前上下文为第一个参数(成功调用的时候不会返回)。
    • 如果第一个参数是通过 getcontext 获取的,则调用之后继续往下执行,
    • 如果是通过 makecontext 创建的,程序会调用 makecontext 的第二个参数指向的函数,
    • 当函数执行完毕,转到 uc_link 指向的上下文,如果 uc_linknull,则线程退出。

makecontext

详细文档:https://man7.org/linux/man-pages/man3/swapcontext.3.html

1
2
3
4
5
#include <ucontext.h>

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *restrict oucp,
const ucontext_t *restrict ucp);
  • makecontext: 修改第一个参数为 getcontext 之后的 ucontext_t,如 getcontext(&uctx); makecontext(&uctx);
  • swapcontext: 保存当前上下文到第一个参数,然后切换到第二个参数对应的上下文

ucontext_t.uc_link 在当前上下文结束后的上下文

getcontext 和 setcontext 成功的时候返回 0,失败的时候返回 -1。

注意事项

  • OSX 下 ucontext_t 的栈大小要大于等于 32Kb,否则 makecontext 调用无效。
  • context 对应的函数里面需要用到栈的时候,是线程共享的栈,而不是 ucontext_t.uc_stack 指定的栈。所以给 ucontext_t.uc_stack 设置初始化的栈的时候,不用考虑 context 对应函数需要占用多少栈空间。

在 OSX 下使用 makecontext 官网文档的例子的时候,发现会进入一个死循环:

下面是完整代码(附注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#define _XOPEN_SOURCE
#include <ucontext.h>
#include <stdio.h>
#include <stdlib.h>

static ucontext_t uctx_main, uctx_func1, uctx_func2;

#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)

static void
func1(void)
{
printf("func1: started\n");
printf("func1: swapcontext(&uctx_func1, &uctx_func2)\n");
if (swapcontext(&uctx_func1, &uctx_func2) == -1)
handle_error("swapcontext");
printf("func1: returning\n");
}

static void
func2(void)
{
printf("func2: started\n");
printf("func2: swapcontext(&uctx_func2, &uctx_func1)\n");
if (swapcontext(&uctx_func2, &uctx_func1) == -1)
handle_error("swapcontext");
printf("func2: returning\n");
}

int
main(int argc, char *argv[])
{
// 16384 16K
char func1_stack[16384];
char func2_stack[16384];

if (getcontext(&uctx_func1) == -1)
handle_error("getcontext");
uctx_func1.uc_stack.ss_sp = func1_stack;
uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
uctx_func1.uc_link = &uctx_main;
makecontext(&uctx_func1, func1, 0);

// 1. 初始化 uctx_func2 上下文环境,这个上下文恢复的时候,rip 指向 getcontext 汇编之后的下一条指令。
if (getcontext(&uctx_func2) == -1)
handle_error("getcontext");
// 2. uctx_func2 的 rip 如果没有被成功修改为 func2,将 uctx_func2 上下文恢复的时候,会从下面这一行开始执行。因为 getcontext 的调用是成功的。
uctx_func2.uc_stack.ss_sp = func2_stack;
uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
/* Successor context is f1(), unless argc > 1 */
uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;

// 3.调用失败,但是不会有任何报错信息,也没有返回值。
// 如果调用成功,uctx_func2 的 rip 指向的是 func2,从而在恢复 uctx_func2 的时候,去执行 func2 函数。
makecontext(&uctx_func2, func2, 0);

printf("main: swapcontext(&uctx_main, &uctx_func2)\n");
// 4. 保存当前上下文到 uctx_main 中,还原到上下文 uctx_func2。
// 5. 由于 uctx_func2 指向 getcontext 的下一条指令,所以会从 `uctx_func2.uc_stack.ss_sp = func2_stack;` 这一行继续执行,进入一个死循环。
if (swapcontext(&uctx_main, &uctx_func2) == -1)
handle_error("swapcontext");

printf("main: exiting\n");
exit(EXIT_SUCCESS);
}

当在 OSX 下执行的时候,会无限输出 main: swapcontext(&uctx_main, &uctx_func2)

详细解答在: https://stackoverflow.com/questions/40299849/context-switching-is-makecontext-and-swapcontext-working-here-osx

OSX 的 makecontext 里面实现存在的问题:

源码:https://github.com/Apple-FOSS-Mirror/Libc/blob/2ca2ae74647714acfc18674c3114b1a5d3325d7d/x86_64/gen/makecontext.c

MINSIGSTKSZ 过大(32K),我们设置的栈大小只有 16K。

1
2
3
4
5
6
7
8
9
10
else if ((ucp->uc_stack.ss_sp == NULL) ||
(ucp->uc_stack.ss_size < MINSIGSTKSZ)) {
/*
* This should really return -1 with errno set to ENOMEM
* or something, but the spec says that makecontext is
* a void function. At least make sure that the context
* isn't valid so it can't be used without an error.
*/
ucp->uc_mcsize = 0;
}

总结来说就是:

  1. OSX 下的 MINSIGSTKSZ 为 32K,然后 OSX 里面的 makecontext 实现判断 ucontext_t 的栈大小小于 MINSIGSTKSZ 的时候,会直接返回,但是这个时候我们的调用是失败的, 因为成功的 makecontext 调用应该会将 ucontext_t 里面的 rip 修改为 func2 的地址。(rip 是保存下一条需要执行的指令的地址的寄存器)。

  2. getcontext 的作用是初始化一个 ucontext_t,初始化之后,这个 ucontext_t 指向 getcontext 调用的下一条指令(汇编层面的指令)。

结果就导致当我们调用 swapcontext 的时候,使用 uctx_func2 来恢复上下文环境的时候,实际上执行的是 getcontext 的下一条指令。然后后面调用 makecontext 仍然失败,从而无限循环。

解决方法

将栈的大小设置为 32K 或者更大,也就是把源码里面的 16384 修改为 32768。

通过代码描述一下这里的具体操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Parent interface {
Test()
}

type Child struct {
Name string `json:"name"`
}

func (c Child) Test() {
}

func createChild() Parent {
return Child{}
}

func test() Parent {
// c 是一个 Parent 类型
c := createChild()

s := `{"name":"123"}`
err := json.Unmarshal([]byte(s), &c)
if err != nil {
panic(err)
}

// 问题:这里能不能得到 name 的值 123?
fmt.Printf("name=%s\n", c.(Child).Name)

return c
}

答案:不行

问题描述

具体就是,我有一个结构体 Child 实现了接口 Parent,然后 Child 里面有 name 属性,然后在反序列化的时候,用指向 Parent,类型的值来接收。

结果就是,结构体里面字段都丢失了。

如果我们写成下面这样,就可能错过了发现错误的机会(我一开始就是这么写的):

1
_ = json.Unmarshal([]byte(s), &c)

如果打印错误的话,其实编译器已经给出了明确的报错信息:

1
2
panic: json: cannot unmarshal object into Go value of type body.Parent [recovered]
panic: json: cannot unmarshal object into Go value of type body.Parent

原因

在反序列化的时候,反射处理是拿不到那个值的具体类型的。

参考链接:https://stackoverflow.com/questions/32428797/unmarshal-to-a-interface-type

原答案:

You can marshal from an interface type variable, because the object exists locally, so the reflector knows the underlying type.

You cannot unmarshal to an interface type, because the reflector does not know which concrete type to give to a new instance to receive the marshaled data.

启动命令

1
docker run -d --rm --name=alert-mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 -v /usr/lnmp/alert-mysql-data:/var/lib/mysql mysql/mysql-server:8.0.26

参数说明:

  • -d: 后台运行
  • --rm: 停止之后删除容器
  • --name=alert-mysql: 指定容器名称
  • -p 3307:3306: 宿主机的 3307 端口映射到容器的 3306 端口
  • -e MYSQL_ROOT_PASSWORD=123456: mysql 的初始化密码
  • -v /usr/lnmp/alert-mysql-data:/var/lib/mysql: 宿主机的 /usr/lnmp/alert-mysql-data 映射到容器的 /var/lib/mysql

修改密码

默认的 root 用户只能在容器实例里面进行连接,在宿主机上连接不了。

如果我们想在容器外连接数据库,则需要进行以下操作:

1
2
3
4
5
$ docker exec -it mysqldb bash
# mysql -h localhost -u root -p
(now enter the passsword 123456 and hit enter)
mysql> create user 'root'@'%' identified WITH mysql_native_password by '123456';
mysql> grant all privileges on *.* to 'root'@'%' with grant option;