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

网上很多文章都说,线程比较轻量级 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_testin parent, buf:"msg from parent"

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

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

$ ./clone_test clone_vmin 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) = 0x7fefb3986000clone(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) = 12629mprotect(0x7fefb3986000, 4096, PROT_NONE) = 0

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

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

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

值得注意的是 mmap 申请的内存大小不是 8M 而是 8M + 4K

8392704 = 8 * 1024 * 1024 + 4096

为什么会多这 4K,我们接下来的第二部分线程与栈中会详细阐述。

第二部分:线程与栈

前面内容中,我们看到通过 strace 查看线程创建过程中的 8M 的栈大小,实际上会分配多 4k 的空间,这是一个很有意思的问题,我们来详细看看。

线程与 Guard 区域

线程的栈是一个比较“奇怪”的产物,一方面线程的栈是线程独有,里面保存了线程运行状态、局部变量、函数调用等信息。另外一方面,从资源管理的角度而言,所有线程的栈都属于进程的内存资源,线程和父进程共享资源,进程中其它线程自然可以修改任意线程的栈内存。


以下面的代码为例,这段代码创建了两个线程 t1、t2,对应的运行函数是 runnable1 和 runnable2。t1 线程将 buf 数组的地址复制给全局指针 p,t1 线程每隔 1s 打印一次 buf 数组的内容,t2 线程每隔 3s 修改一次 p 指针指向地址的内容。

#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
static char *p;
void *runnable1(void *args) {char buf[10] = {0};p = buf;while (1) {printf("buffer: %s\n", buf);sleep(1);}
}void *runnable2(void *args) {int index = 0;while (1) {if (p) {strcpy(p, index++ % 2 == 0 ? "say hello" : "say world");}sleep(3);}
}
int main() {pthread_t t1, t2;pthread_create(&t1, NULL, runnable1, NULL);pthread_create(&t2, NULL, runnable2, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}

编译运行上面的代码,结果输出如下

$ ./thread_stack_testbuf:
buf:
buf:
buf: say hello
buf: say hello
buf: say hello
buf: say world
buf: say world
buf: say world
buf: say hello
buf: say hello

可以看到线程 2 直接修改了线程 1 栈中数组的内容。这种行为是 linux 中完全合法,不会报任何错误。如果可以这么随意的访问到其它线程的内容是一个非常危险的事情,比如栈越界,将会造成其它线程的数据错乱。

为了能减缓栈越界带来的影响,操作系统引入了 stack guard 的概念,就是给每个线程栈多分配一页(4k)或多页内存,这片内存不可读、不可写、不可执行,只要访问就会造成段错误。

我们以一个实际的例子来看栈越界,代码如下所示。

static void *thread_illegal_access(void *arg) {sleep(1);char p[1];int i;for (i = 0; i < 1024; ++i) {printf("[%d] access address: %p\n", i, &p[i * 1024]);p[i * 1024] = 'a';}
}
static void *thread_nothing(void *arg) {sleep(1000);return NULL;
}int main() {pthread_t t1;pthread_t t2;pthread_create(&t1, NULL, thread_nothing, NULL);pthread_create(&t2, NULL, thread_illegal_access, NULL);char str[100];sprintf(str, "cat /proc/%d/maps > proc_map.txt", getpid());system(str);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}

编译上面的 c 文件,使用 strace 执行,部分系统调用如下所示。

// thread 1
mmap(NULL, 8392704,PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK,-1, 0) = 0x7f228d615000mprotect(0x7f228d615000, 4096, PROT_NONE) = 0clone(child_stack=0x7f228de14fb0,                                                                                                     flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,   parent_tidptr=0x7f228de159d0, tls=0x7f228de15700, child_tidptr=0x7f228de159d0) = 9696// thread 2
mmap(NULL, 8392704,PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f228ce14000
mprotect(0x7f228ce14000, 4096, PROT_NONE) = 0
clone(child_stack=0x7f228d613fb0,                                                                                                     flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,   parent_tidptr=0x7f228d6149d0, tls=0x7f228d614700, child_tidptr=0x7f228d6149d0) = 9697

在 linux 中,一个线程栈的默认大小是 8M(8388608),但是这里 mmap 分配的内存块大小却是 8392704(8M+4k),这里多出来的 4k 就是 stack guard 大小。

分配了 8M+4k 的内存以后,随即使用 mprotect 将刚分配的内存块的 4k 地址的权限改为了 PROT_NONE, PROT_NONE 表示拒绝所有访问,不可读、不可写、不可执行。第二个线程创建的过程一模一样,这里不再赘述,两个线程的内存布局如下所示。

$ ./thread_test
[0] access address: 0x7ffff6feef0b
[1] access address: 0x7ffff6fef30b
[2] access address: 0x7ffff6fef70b
[3] access address: 0x7ffff6fefb0b
[4] access address: 0x7ffff6feff0b
[5] access address: 0x7ffff6ff030b
[1]    18133 segmentation fault  ./thread_test

我们可以看到最后 access 导致段错误的地址是 0x7ffff6ff030b,这个地址正好位于线程 1 的 guard 区域内。最后一个合法的范围还处于 t2 的线程栈的合法区域中,如下所示。


原文链接:https://juejin.im/post/6844904197335285774

linux存储--线程与栈(十五)相关推荐

  1. linux exec 二程序,二十五、Linux 进程与信号---exec函数

    25.1 介绍 在用 fork 函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序 当进程调用一种 exec 函数时,该进程完全由新程序代换,替换原有进程的正文,而新程序则从其 m ...

  2. 一图带你入门Linux 存储I/O栈

    发现了一个内核大佬 的 Linux 存储I/O栈,很清晰!!! 原地址如下: http://ilinuxkernel.com/?p=1559 [侵删]

  3. LINUX学习基础篇(十五)软件包管理

    LINUX学习基础篇(十五)软件包管理 软件包管理 软件包分类 源码包 二进制包 选择 依赖性 rpm包安装 rpm包命名规则 rpm包安装和卸载 服务命令 rpm查询命令 验证 数字证书 rpm中文 ...

  4. Linux的基本学习(十五)——认识系统服务

    Linux的基本学习(十五)--认识系统服务 前言 继续学习Linux 什么是daemon与服务(service) 从CentOS 7.x开始,传统的init已经被抛弃,取而代之的是systemd 什 ...

  5. 【正点原子Linux连载】第三十五章 Linux内核顶层Makefile详解 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  6. LINUX下线程默认栈大小的设置

    默认的大小(8M) linux下默认的栈可以通过以下命令查看 ulimit -s 如果没有更改过,默认的值为8192k = 8192/1024=8M 通过代码也可以查看, pthread_attr_i ...

  7. 【正点原子Linux连载】第四十五章 pinctrl和gpio子系统实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

    1)实验平台:正点原子阿尔法Linux开发板 2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434 2)全套实验源码+手册+视频下载地址: ...

  8. linux 多核 系统时钟,Linux时间子系统之(十五):clocksource

    Linux时间子系统之(十五):clocksource 作者:linuxer 发布于:2014-12-1 19:03 分类:时间子系统 一.前言 和洋葱一样,软件也是有层次的,内核往往需要对形形色色的 ...

  9. Linux应用开发【第十五章】存储设备应用开发

    文章目录 15 存储设备应用开发 15.1 SD/TF卡,U盘使用步骤 15.1.1 SD/TF卡,U盘的硬件接口 15.1.2 确定设备点 15.1.3 分区 15.1.4 格式化并挂载 15.1. ...

最新文章

  1. YEX黄建:脱离比特币谈区块链,要么真傻,要么装疯卖傻丨区块链十人谈
  2. 如果地府需要一个后台管理系统,你会如何设计?
  3. Oracle中group by用法
  4. 按部就班——图解配置IIS5的SSL安全访问
  5. JVM监控工具有哪些
  6. [蓝桥杯][算法提高VIP]分分钟的碎碎念(dfs)
  7. mysql教程丿it教程网_MySQL整体
  8. u盘装xp/win7/ubuntu/fedora总结
  9. LW_OOPC介绍(转载)
  10. 光模块和光纤收发器的区别
  11. 05 linux shell脚本 变量的取用echo;变量的设置和修改;变量的使用规范以及示例
  12. 诚之和:没了“全网最低价”,薇娅李佳琦靠什么支撑背后公司上市?
  13. 最优化方法——0.618法matlab实现
  14. 设计数据库中常见的规范
  15. 不破不立 WCA-IeSF全球电竞高峰论坛的五大革新
  16. 超市会员管理系统,数据库课程设计
  17. Ansys ncode Designlife19.0疲劳与裂纹扩展分析资料教程
  18. 曲柄压力机的离合器和制动系统设计
  19. Centos7下zabbix安装与部署,设置中文(保姆级图文)【网络工程】
  20. html写签到程序,如果写自动签到程序的模板

热门文章

  1. python读取excel文件-Python 读写excel文件
  2. python培训班哪些比较好-南京Python培训机构哪家比较好
  3. python新手项目-推荐 13 个 Python 新手练级项目
  4. python在财务中的应用实训报告-DATATOM | 大数据实训
  5. python适合做后端开发吗-转行IT做后端开发,学python还是java?
  6. python json.loads()中文问题-解决Python下json.loads()中文字符出错的问题
  7. gcc编译报错:undefined reference to `std::cout‘
  8. java课设电子门禁_Door门禁系统.doc
  9. UVa11019 Matrix Matcher(hash+kmp)
  10. UVa11464 - Even Parity(递推法)