当前位置:主页 > 查看内容

程序员的自我修养之线程与栈

发布时间:2021-09-08 00:00| 位朋友查看

简介:这篇文章是介绍一下线程与栈相关的话题,文章比较长,主要会聊聊下面这些话题: 进程与线程的本质区别,线程与内存共享 Linux pthread 与 Guard 区域 Hotspot 线程栈的 Guard 区域实现原理 你可能没有怎么听说过的 Yellow-Zone、Red-Zone Java StackOverflow……

  

这篇文章是介绍一下线程与栈相关的话题,文章比较长,主要会聊聊下面这些话题:

  • 进程与线程的本质区别,线程与内存共享
  • Linux pthread 与 Guard 区域
  • Hotspot 线程栈的 Guard 区域实现原理
  • 你可能没有怎么听说过的 Yellow-Zone、Red-Zone
  • Java StackOverflowError 的实现原理

为了讲清楚线程与栈的关系,我们要从进程和线程之间的关系讲起,接下来开始第一部分。

第一部分:老生常谈之进程线程

网上很多文章都说,线程比较轻量级 lightweight,进程比较重量级,首先我们来看看这两者到底的区别和联系在哪里。

clone 系统调用

在上层看来,进程和线程的区别确实有天壤之别,两者的创建、管理方式都非常不一样。在 linux 内核中,不管是进程还是线程都是使用同一个系统调用 clone,接下来我们先来看看 clone 的使用。为了表述的方便,接下来暂时用进程来表示进程和线程的概念。

clone 函数的函数签名如下。

  1. int clone(int (*fn)(void *), 
  2.           void *child_stack, 
  3.           int flags, 
  4.           void *arg, ... 
  5.           /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ ); 
  6.  

参数释义如下:

  • 第一个参数 fn 表示 clone 生成的子进程会调用 fn 指定的函数,参数由第四个参数 arg 指定
  • child_stack 表示生成的子进程的栈空间
  • flags 参数非常关键,正是这个参数区分了生成的子进程与父进程如何共享资源(内存、打开文件描述符等)
  • 剩下的参数,ptid、tls、ctid 与线程实现有关,这里先不展开

接下来我们来看一个实际的例子,看看 flag 对新生成的「进程」行为的影响。

clone 参数的影响

接下来演示 CLONE_VM 参数对父子进程行为的影响,这段代码当运行时的命令行参数包含 "clone_vm" 时,给 clone 函数的 flags 会增加 CLONE_VM。代码如下。

  1. static int child_func(void *arg) { 
  2.     char *buf = (char *)arg; 
  3.     // 修改 buf 内容 
  4.     strcpy(buf, "hello from child"); 
  5.     return 0; 
  6.  
  7. const int STACK_SIZE = 256 * 1024; 
  8. int main(int argc, char **argv) { 
  9.     char *stack = malloc(STACK_SIZE); 
  10.  
  11.     int clone_flags = 0; 
  12.     // 如果第一个参数是 clone_vm,则给 clone_flags 增加 CLONE_VM 标记 
  13.     if (argc > 1 && !strcmp(argv[1], "clone_vm")) { 
  14.         clone_flags |= CLONE_VM; 
  15.     } 
  16.     char buf[] = "msg from parent"
  17.  
  18.     if (clone(child_func, stack + STACK_SIZE, clone_flags, buf) == -1) { 
  19.         exit(1); 
  20.     } 
  21.     sleep(1); 
  22.     printf("in parent, buf:\"%s\"\n", buf); 
  23.     return 0; 

上面的代码在 clone 调用时,将父进程的 buf 指针传递到 child 进程中,当不带任何参数时,CLONE_VM 标记没有被设置,表示不共享虚拟内存,父子进程的内存完全独立,子进程的内存是父进程内存的拷贝,子进程对 buf 内存的写入只是修改自己的内存副本,父进程看不到这一修改。

编译运行结果如下。

  1. $ ./clone_test 
  2.  
  3. in parent, buf:"msg from parent" 

可以看到 child 进程对 buf 的修改,父进程并没有生效。

再来看看运行时增加 clone_vm 参数时结果:

  1. $ ./clone_test clone_vm 
  2.  
  3. in parent, buf:"hello from child" 

可以看到这次 child 进程对 buf 修改,父进程生效了。当设置了 CLONE_VM 标记时,父子进程会共享内存,子进程对 buf 内存的修改也会直接影响到父进程。

讲这个例子是为后面介绍进程和线程的区别打下基础,接下来我们来看看进程和线程的本质区别是什么。

进程与 clone

以下面的代码为例。

  1. pid_t gettid() { 
  2.     return syscall(__NR_gettid); 
  3. int main() { 
  4.     pid_t pid; 
  5.     pid = fork(); 
  6.     if (pid == 0) { 
  7.         printf("in child,  pid: %d, tid:%d\n", getpid(), gettid()); 
  8.     } else { 
  9.         printf("in parent, pid: %d, tid:%d\n", getpid(), gettid()); 
  10.     } 
  11.     return 0; 

使用 strace 运行输出结果如下

  1. clone(child_stack=NULL
  2. flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, 
  3. child_tidptr=0x7f75b83b4a10) = 16274 

可以看到 fork 创建进程对应 clone 使用的 flags 中唯一需要值得注意的 flag 是 SIGCHLD,当设置这个 flag 以后,子进程退出时,系统会给父进程发送 SIGCHLD 信号,让父进程使用 wait 等函数获取到子进程退出的原因。

可以看到 fork 调用时,父子进程没有共享内存、打开文件等资源,这样契合进程是资源的封装单位这个说法,资源独立是进程的显著特征。接下来我们来看看线程与 clone 的关系。

线程与 clone

这里以一段最简单的 C 代码来看看创建一个线程时,底层到底发生了什么,代码如下。

  1. #include <pthread.h> 
  2. #include <unistd.h> 
  3. #include <stdio.h> 
  4.  
  5. void *run(void *args) { 
  6.     sleep(10000); 
  7. int main() { 
  8.     pthread_t t1; 
  9.     pthread_create(&t1, NULL, run, NULL); 
  10.     pthread_join(t1, NULL); 
  11.     return 0; 

使用 gcc 编译上面的代码

  1. gcc -o thread_test thread_test.c -lpthread 

然后使用 strace 执行 thread_test,系统调用如下所示。

  1. mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fefb3986000 
  2.  
  3. clone(child_stack=0x7fefb4185fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fefb41869d0, tls=0x7fefb4186700, child_tidptr=0x7fefb41869d0) = 12629 
  4.  
  5. mprotect(0x7fefb3986000, 4096, PROT_NONE) = 0 

比较重要的是下面这些 flags 参数:

标记 含义
CLONE_VM 共享虚拟内存
CLONE_FS 共享与文件系统相关的属性
CLONE_FILES 共享打开文件描述符表
CLONE_SIGHAND 共享对信号的处置
CLONE_THREAD 置于父进程所属的线程组中

本文转载自网络,原文链接:https://mp.weixin.qq.com/s/vTPa6HDhCtrepTp-la3j-Q
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:高性能5G核心网,动力从何而来? 下一篇:没有了

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐