- 作者: 雪山肥鱼
- 时间:20210802 22:47
- 目的:了解线程的栈 与 guard page
# 线程的双重特性
## 代码模拟 线程的二义性
# 栈的 guard page
## guard page 代码举例
## strace 代码追踪线程的栈
# 再谈栈和堆
## 再谈线程的栈
线程栈的双重特性
- 线程的栈是每个线程独有的,保存其运行状态和局部自动变量
- 栈在线程开始的时候初始化,每个线程的栈相互独立,因此栈是thread safe 的。
线程的栈是相对安全的,但是栈所占的内存空间,又属于整个进程。 这就是双重特性。
![](https://img.haomeiwen.com/i25953572/a4be20dde1680045.png)
进程是资源单位,线程是调度单位
栈显然是内存的资源。从资源的角度来看,每个线程的栈都属于进程。
但是从用法的角度来说,T1 T2 T3 T4 自己有自己的栈。栈中存放着各个层级函数的回溯,还有一些参数,临时变量等。
函数调度的越深,栈就一直往下降。函数越回退,栈就越浅。
这就造成了线程栈的二义性。
- 不像进程,每个程序有自己的页表,是不可能互相踏得。
- 线程T1 踏了 线程 T2 T3 的栈,从内存资源上来看是合法的,因为整个地址空间是相同得,地址转换又是一样得。
- 但是从用法上来讲,显然是不合法得.
代码模拟 线程得二义性
#include <limits.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
static char *p ; //bss -> 0
static void * thread_routine(void *arg) {
char buf[10] = {0};
p = buf;
while(1 ) {
printf("buf is %s\n", buf);
sleep(1);
}
return NULL;
}
int main(int argc, char ** argv) {
pthread_t thread_id1;
status = pthread_create(&thread_id1, NULL, thread_routine, NULL);
if(status != 0)
exit(-1);
int i = 0;
while(1) {
if(p) {
if(i++ %2 == 0) {
strcpy(p, "hello");
} else {
strcpy(p, "world");
}
sleep(4);
}
}
pthread_join(thread_id1, NULL);
return 0;
}
主线程一直在修改子线程得栈。验证了 线程得栈依旧属于内存。
![](https://img.haomeiwen.com/i25953572/048a39ab85e7baad.png)
用法上是绝对错误的,每个线程应该有独立的栈
如果linux做成每根线程不会去践踏另一根线程,那么 每根线程都需要有mmu, 都要有自己对应的页表。线程切换的时候就要切换page table。这就是进程的概念了,大家有各自的页表,每一段有不同的权限。
上述这种内存践踏,asan 是检测不出来的。
// 程序正常运行
gcc -g -fsanitize=address ./stack_corruption.c
如果修改成了:
while(1) {
if(p) {
if(i++ %2 == 0)
strcpy(p, "hello111111");
else
strcpy(p, "world");
}
}
gcc -g -fsanitize=address ./stack_corruption.c
./a.out 就有问题了。
![](https://img.haomeiwen.com/i25953572/ed122dc4878e504a.png)
栈的 guard page
![](https://img.haomeiwen.com/i25953572/5e9bd86449d4e7a6.png)
内核空间就不说啦,当讨论多线程的时候,就是指的用户空间。
每个线程的栈会存在Guard page 保护页, 这种保护页是没有权限的。当一个线程进入了另一个线程的guardpage, 出现page falult 则程序崩溃。
线程种 程函数调用的越深,用的临时变量越多,则栈的空间越大。飞到guard page时,直接page fault.
作用:
T1线程不会篡改T2 线程的数据,问题出在哪里就死在哪里。
让出错的现场就是出错的原因。利于debug。
guard page 代码举例
int pthread_attr_set_guardsize(pthread_attr_t * attr, size_t guardsize);
int pthread_attr_getguardsize(const pthread_attr_t * attr, size_t *guardsize);
/*
* thread_attr.c
*
* Create a thread using a non-default attributes object,
* thread_attr. The thread reports its existence, and exits. The
* attributes object specifies that the thread be created
* detached, and, if the stacksize attribute is supported, the
* thread is given a stacksize twice the minimum value.
*/
#include <limits.h>
#include <pthread.h>
#define _POSIX_THREAD_ATTR_STACKSIZE
void err_abort(int status, char *p)
{
perror("%s\n", p);
exit(status);
}
static void *access_overflow(void)
{
char q[11*1024];
int i = 10;
for (i = 10;i>=0;i--) {
printf("%s %d %p\n", __func__, i, &q[i*1024]);
q[i*1024] = 'a';
}
while(1);
printf("The thread is here2\n");
}
static void *thread_routine(void *arg)
{
char p[5*1014];
p[4999] = 'a';
p[0] = 'a';
access_overflow();
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t thread_id1, thread_id2;
pthread_attr_t thread_attr;
struct sched_param thread_param;
size_t stack_size, guard_size;
int status;
status = pthread_attr_init(&thread_attr);
if (status != 0)
err_abort(status, "Create attr");
/*
* Create a detached thread.
*/
status =
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
if (status != 0)
err_abort(status, "Set detach");
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
/*
* If supported, determine the default stack size and report
* it, and then select a stack size for the new thread.
*
* Note that the standard does not specify the default stack
* size, and the default value in an attributes object need
* not be the size that will actually be used. Solaris 2.5
* uses a value of 0 to indicate the default.
*/
status = pthread_attr_getstacksize(&thread_attr, &stack_size);
if (status != 0)
err_abort(status, "Get stack size");
printf("Default stack size is %u; minimum is %u\n",
stack_size, PTHREAD_STACK_MIN);
status = pthread_attr_getguardsize(&thread_attr, &guard_size);
if (status != 0)
err_abort(status, "Get guard size");
printf("Default guard size is %u\n", guard_size);
status = pthread_attr_setstacksize(&thread_attr, PTHREAD_STACK_MIN);
if (status != 0)
err_abort(status, "Set stack size");
#endif
status = pthread_create(&thread_id1, &thread_attr, thread_routine, NULL);
if (status != 0)
err_abort(status, "Create thread");
status = pthread_create(&thread_id2, &thread_attr, thread_routine, NULL);
if (status != 0)
err_abort(status, "Create thread");
while(1);
return 0;
}
运行结果:
![](https://img.haomeiwen.com/i25953572/d47c7297dae6d3d9.png)
代码流程描述:故意让栈越跑越深
- 初始化线程的属性 pthread_attr_init. 并没有对其进行任何赋值
- 拿到这个线程后,打印出其默认的栈大小 - 8M
- 拿到线程的 guard size - 4kb
- 修改线程栈大小 - 16kb
- 人为的让栈溢出
- Segmentation Fault
strace 代码追踪
![](https://img.haomeiwen.com/i25953572/2c7dfab052284247.png)
人为设定16kb 的 栈空间, 但是mmap了20kb 的空间。其中有4kb 是guard page.这4kb的的属性为 prot_none。
两根线程 栈的地址 相差就是 20kb。
再谈栈和堆
- 栈和堆从内核的内存管理来看,区别其实不大。
原因:都可以用mmap申请,从而得到一片vma的地址空间。
但是 堆具有 on-demand 属性, 需要使用的时候,才会帮你申请 - 栈的属性
栈的属性有多种,可以是MAP_STACK,也可以是MAP_GROWSDOWN。
GROUWSDOWN(有些像堆)可以让栈往下长, 而MAP_STACK是已经分配好栈的大小,固定了大小。 - MAP_STACK (since Linux 2.6.27)
Allocate the mapping at an address suitable for a process or thread stack. This flag is currently a no-op, but is used in the glibc threading implementation so that if some architectures require special treatment for stack allocations, support can later be transparently implemented for glibc. - MAP_GROWSDOWN
Used for stacks. Indicates to the kernel virtual memory system that the mapping should extend downward in memory.
再谈线程的栈
由上讨论可知,这里并不是像教科书中说的,栈都是 growsdown的,栈的申请方式可以是多样的,工程中不同的库,对于栈的使用是不同的。
而对于线程的栈来说,有两个层面。
- 栈的来源
来源可以是多样的,当然特也可以malloc(1M), 给thread使用。也可以申请一片栈给栈使用。
也可以growsdown,使用内存。 - 栈的使用
用法是固定的,都是先进后出。
当然 如果在其他库中,线程的栈使用的是growpdown,则使用的是linux内核中的关于growsdown的保护机制。
大概算法是 地址减去一页,会不会落到另一个vma中,比如栈不停增长,飞到了 数据段里。如果落到了,则崩溃。
内核中使用的就是以上growpdown的机制。
记住:pthread glibc是用户空间,内核是内核,也有单独一套
网友评论