三言两语聊kernel:线程栈

Oops! MacOS不支持proc文件系统,这或许是BSD系统最值得吐槽的地方吧。无奈只好到linux机器上写了这篇文章。

下面是一个比较简单的多线程程序。程序如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
    int ret = 0;
    int i = 0;
    /* int stack_size = 128 * 1024; */

    pthread_attr_t attr;
    /* pthread_attr_setstacksize(&attr, stack_size); */

    for (i = 0; i < 3; i++) {
        ret = pthread_create(&thread[i], NULL, thread_func, NULL);
        if (ret) {
            perror("create");
            return -1;
        }
    }

    while (1) {
        ;
    }

    return 0;
}

上图是我的测试程序,我创建了3个线程。程序运行以后,我们可以通过/ proc/PID/task来看该程序有多少线程在运行:

然后我们来看一下进程的地址空间。 /proc/PID/maps就是进程的地址空间。如下所示:

可以看出,进程的地址空间从低到高依次是:进程代码段(标志含有x)、只读数据段、可读写数据段、堆、mmap区(文件映射和匿名映射,其中有文件名的行是文件映射),栈。

线程18438的栈:(0xb7570000-0xb6d70000)的值恰好是8M,线程栈默认大小是8M。(0xb6d70000-0xb6d6f000)的值是4K,这4K是保护页。

为什么这三个线程的栈都是8M?可以从ulimit命令来得出,这是进程的资源限制:

使用ulimit -a命令可以看出,进程资源限制中栈大小的限制是8194K,即8M.

那么,这个8M大小是不是可以更改的?以及后面会什么会有一个4K大小的保护页?这可以从glibc代码里面来获取答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.
__pthread_create_2_1
    /* 这里分配线程栈 */
    ALLOCATE_STACK

2.
/* allocate_stack就是具体的分配线程栈的函数: */
allocate_stack
    /*如果没有设置线程栈大小,就使用默认值*/
    size = attr > stacksize ?: __default_stacksize;
  /* ... */
    /* Try to get a stack from the cache. */
    pd = get_cached_stack (&size, &mem);
    /* 如果没有从cache申请到,就要mmap申请一块内存 */
    if (pd == NULL){
        /* MAP_PRIVATE | MAP_ANONYMOUS: 私有匿名映射 */
        mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
        /* 接着设置一个保护区,该区域的页表属性是PROT_NONE(Page can not be accessed) */
        mprotect(guard, guardsize, PROT_NONE);

对于设置为PROT_NONE的页,是不能访问的,那么访问到这个保护区时就出现错误,linux是靠这种机制来实现栈溢出保护的。

下面我们来调整线程栈:

  • 设置pthread_attr属性

可以看到此时的线程栈大小是: (0xb758f000-0xb756f000) = 128K.

  • 通过ulimit来统一设置当前shell下将要执行的程序的线程栈:

    ulimit -s  128

要注意的是, ulimit -s是针对shell的设置, 即只对当前shell fork的进程有效。如果在另外一个shell上起进程,则是没有效果的。参见 man手册:“Control the resources available to a process started by the shell, on systems that allow such control.”

Comments