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

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

  • 进程与线程的本质区别,线程与内存共享
  • 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 的关系。

<
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值