Linux进程栈空间大小【转】
转自:https://www.tiehichi.site/2020/10/22/Linux%E8%BF%9B%E7%A8%8B%E6%A0%88%E7%A9%BA%E9%97%B4%E5%A4%A7%E5%B0%8F/
- 内核栈
- 用户栈大小
- 用户栈虚拟地址空间最大值
- 当前用户栈虚拟地址空间大小
- 栈顶地址随机化
- 线程的用户栈
分析过程基于Linux kernel 3.18.120
内核栈
Linux上进程的相关属性在内核中表示为task_struct,该结构体中stack成员指向进程内核栈的栈底:
struct task_struct {
|
我们知道Linux的子进程创建都是通过复制父进程的task_struct来进行的,所以可以从系统的0号进程着手分析进程内核栈的大小;0号进程为init_task:
struct task_struct init_task = INIT_TASK(init_task);
|
来看看init_task的stack字段的值:
|
init_task的stack字段实际上指向thread_union联合体中的thread_info,再来看一下thread_union的结构:
union thread_union {
|
所以init_task进程的内核栈就是init_thread_union.stack,而thread_info位于内核栈的栈底;内核栈声明为unsigned long类型的数组,其实际大小与平台相关,即为THREAD_SIZE的定义;对于arm32平台,它的定义为:
/* arch/arm/include/asm/thread_info.h */
|
而PAGE_SIZE的定义为
/* arch/arm/include/asm/page.h */
|
所以对于arm32平台,PAGE_SIZE大小为4k,THREAD_SIZE大小为8k;此时可以确定 init_task的内核栈大小为8k。
前面提到进程的创建是在内核中拷贝父进程的task_struct,来看一下这部分代码:
static struct task_struct *dup_task_struct(struct task_struct *orig)
|
在复制task_struct的时候,新的task_struct->stack通过alloc_thread_info_node来分配:
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
|
这里THREAD_SIZE_ORDER为1,所以分配了2个page,所以我们可以确定,进程的内核栈大小为8k。
用户栈大小
用户栈虚拟地址空间最大值
通过ulimit命令可以查看当前系统的进程用户栈的虚拟地址空间上限,单位为kB;
~ # ulimit -s
|
即当前系统中,用户栈的虚拟地址空间上限为8M;为了确认这个值的出处,使用strace,确认ulimit执行过程中,使用了哪些系统调用:
-> % strace sh -c "ulimit -s"
|
接着到内核中查找该系统调用的实现,函数名为SYSCALL_DEFINE4(prlimit64, .......)
/* kernel/sys.c */
|
函数的第一个参数为pid,第二个参数为资源的索引;这里可以理解为查找pid为0的进程中,RLIMIT_STACK的值;函数查找到pid对应的task_struct,然后调用do_prlimit
/* kernel/sys.c */
|
do_prlimit的实现为我们指明了到何处去查找RLIMIT_STACK的值,即tsk->signal->rlim + resource;我们知道0号进程为init_task,所以找到init_task->signal->rlim进行确认
/* include/linux/init_task.h */
|
接着找到INIT_RLIMITS宏的定义
/* include/asm-generic/resource.h */
|
_STK_LIM即为当前系统中,进程用户栈的虚拟地址空间上限:
/* include/uapi/linux/resource.h */
|
当前用户栈虚拟地址空间大小
可以从proc文件系统中,查看进程的虚拟地址空间分布;以init进程为例,其pid为1,可以通过以下命令查看init进程的虚拟地址空间分布,在arm32平台,内核版本3.18.120,init进程的用户栈空间大小为132kB:
~
|
仔细观察会发现,任意进程在启动后,其栈空间大小基本都是132kB;在分析原因之前,我们先来看一下进程的虚拟地址空间分布:

进程的虚拟地址空间大小为4GB,其中内核空间1GB,用户空间3GB,在arm32平台上,二者之间存在一个大小为16M的空隙;用户空间的准确大小为TASK_SIZE:
/* arch/arm/include/asm/memory.h */
|
即用户空间的地址范围为0x00000000~0xBEFFFFFF。
上图左侧为用户空间内的虚拟空间分布,分别为:用户栈(向下增长),内存映射段(向下增长),堆(向上增长)以及BSS、Data和Text;我们关注的重点在用户空间中的栈空间。
在Linux系统中,运行二进制需要通过exec族系统调用进行,例如execve、execl、execv等,而这些函数最终都会切换到kernel space,调用do_execve_common(),我们从这个函数开始分析:
static int do_execve_common(struct filename *filename,
|
函数中的bprm是类型为struct linux_binprm的结构体,主要用来存储运行可执行文件时所需要的参数,如虚拟内存空间vma、内存描述符mm、还有文件名和环境变量等信息:
struct linux_binprm {
|
接着回到do_execve_common函数,在调用bprm_mm_init初始化内存空间描述符时,第一次为进程的栈空间分配了一个页:
/*
|
这里的vma就是进程的栈虚拟地址空间,这段vma区域的结束地址设置为STACK_TOP_MAX,大小为PAGE_SIZE;这两个宏的定义如下:
/* arch/arm/include/asm/processor.h */
|
此时,进程的栈空间如下图所示:

继续回到do_execve_common()函数,到目前为止,内核还没有识别到可执行文件的格式,也没有解析可执行文件中各个段的数据;在exec_binprm()中,会遍历在内核中注册支持的可执行文件格式,并调用该格式的load_binary方法来处理对应格式的二进制文件:
/*
|
search_binary_handler()会依次调用系统中注册的可执行文件格式load_binary()方法;load_binary()方法中会自行识别当前二进制格式是否支持;以ELF格式为例,其注册的load_binary方法为load_elf_binary():
/* fs/binfmt_elf.c */
|
该函数的实现比较复杂,这里我们重点关注setup_arg_pages()函数。
int setup_arg_pages(struct linux_binprm *bprm,
|
前面我们已经初始化了一个页的栈空间,用来存放二进制文件名、参数和环境变量等;在setup_arg_pages()中,我们把前面这一个页的栈空间移动到stack_top的位置;在调用函数时,stack_top的值是randomize_stack_top(STACK_TOP),即一个随机地址,这里是为了安全性而实现的栈地址随机化;函数通过shift_arg_pages()将页移动到新的地址,移动后的栈如下图所示:

接着回到setup_arg_pages()函数,关注如下代码:
stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages */
|
expand_stack()函数用来扩展栈虚拟地址空间的大小,stack_base是新的栈基地址,这里的stack_expand是一个固定值,大小为128k,即此处将栈空间扩展128k的大小,扩展后栈空间如下:

所以扩展后的栈虚拟地址空间为4kB+128kB,刚刚好132kB.
栈顶地址随机化
前面介绍setup_arg_pages()函数移动栈顶的时候提到,出于安全原因,会将栈顶移动到一个随机的地址:
/*
|
这里randomize_stack_top(STACK_TOP)就是将STACK_TOP进行随机化处理,在我们的平台上。STACK_TOP与STACK_TOP_MAX的值相同,为0xBF000000;我们来分析一下randomize_stack_top()函数:
|
STACK_RND_MASK的值为0x7FF,PAGE_SHIFT为12;第一行将获取的随机值范围限制在0~0x7FF的范围内;第二行将该值左移12位,这样得到的随机数范围就变成了0~0x7FF000,可以理解为栈顶地址是在一个8MB的范围内取一个4kB对齐的随机值。
线程的用户栈
我们知道在Linux系统上,无论是进程还是线程,都是通过clone系统调用来创建,区别是传入的参数不同;为了确认创建线程时使用的参数,我准备了一个测试程序,然后使用strace来确认:
|
该程序的strace部分输出(在x86平台上运行):
clone(child_stack=0x7fd2500d0fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tid=[36747], tls=0x7fd2500d1700, child_tidptr=0x7fd2500d19d0) = 36747
|
我们可以看到调用clone的时候传入的flags,其中与内存相关最重要的flags是CLONE_VM;接着我们来看内核部分的源码,仍然从copy_process()函数开始:
/* kernel/fork.c */
|
在copy_mm中,检查了clone_flags,如果设置了CLONE_VM,那么将当前task_struct->mm指针赋值给新的task_struct->mm;所以我们可以得到结论,通过pthread库创建的线程,其内存是与主线程共享的。