线程与栈那些事与Linux 高负载的系统化分析

本文深入探讨了线程与栈的关系,从进程线程的区别、Linux线程实现、Java线程栈的Guard区域到StackOverflowError的原理。通过实例解析了Linux中进程与线程的创建、内存共享以及如何通过clone系统调用来区分进程与线程的行为。同时,文章介绍了Linux线程栈中额外的4k Guard区域,以及Java线程栈的Yellow-Zone和Red-Zone机制,用于预防和处理栈溢出异常。最后,文章提供了Linux高负载问题的排查思路,包括R状态和D状态任务增多的情况,以及CPU iowait和idle高的分析方法。
摘要由CSDN通过智能技术生成

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

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

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

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

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

clone 系统调用

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

clone 函数的函数签名如下。


int clone(int (*fn)(void *),
          void *child_stack,
          int flags,
          void *arg, ...
          /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

参数释义如下:

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

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

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

static int child_func(void *arg) {
    char *buf = (char *)arg;
    // 修改 buf 内容
    strcpy(buf, "hello from child");
    return 0;
}

const int STACK_SIZE = 256 * 1024;
int main(int argc, char **argv) {
    char *stack = malloc(STACK_SIZE);

    int clone_flags = 0;
    // 如果第一个参数是 clone_vm,则给 clone_flags 增加 CLONE_VM 标记
    if (argc > 1 && !strcmp(argv[1], "clone_vm")) {
        clone_flags |= CLONE_VM;
    }
    char buf[] = "msg from parent";

    if (clone(child_func, stack + STACK_SIZE, clone_flags, buf) == -1) {
        exit(1);
    }
    sleep(1);
    printf("in parent, buf:\"%s\"\n", buf);
    return 0;
}

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

编译运行结果如下。


$ ./clone_test

in parent, buf:"msg from parent"

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

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


$ ./clone_test clone_vm

in parent, buf:"hello from child"

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

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

进程与 clone

以下面的代码为例。


pid_t gettid() {
    return syscall(__NR_gettid);
}
int main() {
    pid_t pid;
    pid = fork();
    if (pid == 0) {
        printf("in child,  pid: %d, tid:%d\n", getpid(), gettid());
    } else {
        printf("in parent, pid: %d, tid:%d\n", getpid(), gettid());
    }
    return 0;
}

使用 strace 运行输出结果如下:


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

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

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

线程与 clone

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


#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void *run(void *args) {
    sleep(10000);
}
int main() {
    pthread_t t1;
    pthread_create(&t1, NULL, run, NULL);
    pthread_join(t1, NULL);
    return 0;
}

使用 gcc 编译上面的代码


gcc -o thread_test thread_test.c -lpthread

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


mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fefb3986000

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

mprotect(0x7fefb3986000, 4096, PROT_NONE) = 0

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

可以看到,线程创建的本质是共享进程的虚拟内存、文件系统属性、打开的文件列表、信号处理,以及将生成的线程加入父进程所属的线程组中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值