0%

Linux 进程栈和线程栈的区别

线程栈的空间是开辟在那里的?
线程栈之间可以互访吗?
为什么在使用 pthread_attr_setstack 函数时,需要设置栈的大小,而进程 task_structmm_struct *mm 成员中却并没有却并没有 stack_size 这个成员项,怎么保存的栈大小呢?

虚拟地址空间

如图所示,具体可以参照 Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

虚拟地址空间

进程栈

进程用户空间的管理在 task_structmm_struct *mm 成员中体现, mm 中的成员定义了用户空间的布局情况如图。

用户空间的栈起始于 STACK_TOP, 如果设置了 PF_RANDOMIZE,则起始点会减少一个小的随机量,经过随机处理后,进程栈的起始地址将存放在 mm->start_stack 中,可以通过 cat /proc/xxx/stat 查看。

栈从上而下扩展,而用于内存映射的区域起始于 mm->mmap_base , mm->mmap_base 通过调用 mmap_base() 来初始化,为了确保栈不与 mmap 区域不发生冲突,两者之间设置了一个安全间隙mmap_base 函数源代码如下:

#define MIN_GAP (128*1024*1024)
#define MAX_GAP (TASK_SIZE/6*5)
static inline unsigned long mmap_base(struct mm_struct *mm)
{
  unsigned long gap = current->signal->rlim[RLIMIT_STACK].rlim_cur; // rlim_cur 默认为 8388608,及 8M, 可以使用 getrlimit(RLIMIT_STACK, &limit) 查看
  unsigned long random_factor = 0;
  if (current->flags & PF_RANDOMIZE)
    random_factor = get_random_int() % (1024*1024);
  if (gap < MIN_GAP) // 通过 MIN_GAP 来保证,进程栈的大小至少为 128MB
    gap = MIN_GAP;
  else if (gap > MAX_GAP) // 栈的最大空间为 TASK_SIZE/6*5, 及 2.5G
    gap = MAX_GAP;
  return PAGE_ALIGN(TASK_SIZE - gap - random_factor); // 通过保留 random_factor 空间大小的间隙来防止栈溢出
}

进程内存布局

进程栈位于虚拟地址空间的栈区,可以动态增长,上限为 ulimit -s

线程栈

线程包含了表示进程内执行环境必需的信息,其中包括进程中标示线程的线程 ID,一组寄存器值,栈,调度优先级和策略, 信号屏蔽字,errno 变量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本,程序的全局内存和堆内存,栈以及文件描述符,所以线程的 mm_struct *mm 指针变量和所属进程的 mm 指针变量相同。

在创建线程的时候,可以通过 pthread_attr_t 来初始化线程的属性,包括线程的栈布局信息,如栈起始地址 stackaddr , 栈大小 stacksize。 具体需要通过方法:

// stackaddr 指向为该线程开辟的空间,该空间可以使用 malloc 或者 mmap 来开辟,而不能来自进程的栈区。开辟的 stackaddr 所指向的动态空间需要自己负责释放。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

当然也可将线程栈的空间管理交给系统,如果想改变系统默认的栈大小 8MB,可以通过

// stacksize 最小值为 16384,单位为字节
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

pthread 创建线程流程可以参考 pthread_create 源码分析

线程栈在 linux 下起始地址跟大小保存在 pthread_attr_t

typedef struct __pthread_attr_s
{
    int __detachstate;  // 分离状态
    int __schedpolicy;// 调度策略
    struct __sched_param __schedparam;
    int __inheritsched;
    int __scope;// 线程优先级的有效范围
    size_t __guardsize;//
    int __stackaddr_set;
    void *__stackaddr;// 起始地址
    size_t __stacksize;// 表示堆栈的大小。

}pthread_attr_t;

由此看来线程栈在所属进程的堆区,线程与其所属的进程共享进程的虚拟地址空间,所以线程栈之间可以互访