这篇文章是介绍一下线程与栈相关的话题,文章比较长,主要会聊聊下面这些话题:
为了讲清楚线程与栈的关系,我们要从进程和线程之间的关系讲起,接下来开始第一部分。
第一部分:老生常谈之进程线程
网上很多文章都说,线程比较轻量级 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 */ );
参数释义如下:
接下来我们来看一个实际的例子,看看 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 参数:
标记 | 含义 |
---|---|
CLONE_VM | 共享虚拟内存 |
CLONE_FS | 共享与文件系统相关的属性 |
CLONE_FILES | 共享打开文件描述符表 |
CLONE_SIGHAND | 共享对信号的处置 |
CLONE_THREAD | 置于父进程所属的线程组中 |
伴随着5G、物联网、云计算、大数据等新一代信息技术的发展,人类社会进一步向智...
全段时间苹果发布更新了iOS12.3,还没过几天又发布了iOS12.4,不得不让人感叹,...
【51CTO.com原创稿件】运维工作从早期的人工运维,到自动化运维,如今走向了智能...
随着人工智能技术的成熟以及应用场景的不断丰富,人工智能技术为解决城市公共安...
根据澎湃新闻报道,世界经济论坛公开了一份关于机器人革命的报告,引起了全球范...
面对劲敌,当前短视频平台中的翘楚-抖音苦日子将至?也有不同的意见,认为微信上...
机器学习和人工智能这两种技术在许多领域广泛应用,尤其是在营销分析和网络安全...
近日,国家工信部和卫健委办公厅联合组织开展5G+医疗健康应用试点项目申报工作,...
机器人学习中的经典问题之一便是分拣:在一堆无序摆放的物品堆中,取出目标物品...
5G网络建设加快,超前布局6G 截止目前,我国累计建成的5G基站数量超过71.8万座,...