1 从 Linux 内核的角度谈线程栈和进程栈-德赢Vwin官网 网
0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

从 Linux 内核的角度谈线程栈和进程栈

454398 来源: Chinaunix 作者:lvyilong316 2020-09-25 15:23 次阅读

1.进程栈

进程栈是属于用户态栈,和进程虚拟地址空间(Virtual Address Space)密切相关。那我们先了解下什么是虚拟地址空间:在32位机器下,虚拟地址空间大小为4G。这些虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元(MMU)硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

Linux内核将这4G字节的空间分为两部分,将最高的1G字节(0xC0000000-0xFFFFFFFF)供内核使用,称为内核空间。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。

Linux对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成(Memory Segment),主要的内存段如下:
-程序段(Text Segment):可执行文件代码的内存映射
-数据段(Data Segment):可执行文件的已初始化全局变量的内存映射
- BSS段(BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
-堆区(Heap) :存储动态内存分配,匿名的内存映射
-栈区(Stack) :进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
-映射段(Memory Mapping Segment):任何内存映射文件

而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制RLIMIT_STACK (一般为8M),我们可以通过ulimit来查看或更改RLIMIT_STACK的值。

【扩展阅读】:进程栈的动态增长实现

进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个缺页异常(page fault)。通过异常陷入内核态后,异常会被内核的expand_stack()函数处理,进而调用acct_stack_growth()来检查是否还有合适的地方用于栈的增长。

如果栈的大小低于RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号

动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

2.线程栈

从Linux内核的角度来说,其实它并没有线程的概念。Linux把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了task_struct中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和Linux中所谓线程的唯一区别。线程创建的时候,加上了CLONE_VM标记,这样线程的内存描述符将直接指向父进程的内存描述符

点击(此处)折叠或打开

if(clone_flags&CLONE_VM){

/*

*current 是父进程而 tsk 在 fork()执行期间是共享子进程

*/

atomic_inc(¤t->mm->mm_users);

tsk->mm=current->mm;

}

虽然线程的地址空间和进程一样,但是对待其地址空间的stack还是有些区别的。对于Linux进程或者说主线程,其stack是在fork的时候生成的,实际上就是复制了父亲的stack空间地址,然后写时拷贝(cow)以及动态增长。然而对于主线程生成的子线程而言,其stack将不再是这样的了,而是事先固定下来的,使用mmap系统调用(实际上是进程的堆的一部分),它不带有VM_STACK_FLAGS标记。这个可以从glibc的nptl/allocatestack.c中的allocate_stack()函数中看到:

点击(此处)折叠或打开

mem=mmap(NULL,size,prot,MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK,-1,0);

由于线程的mm->start_stack栈地址和所属进程相同,所以线程栈的起始地址并没有存放在task_struct中,应该是使用pthread_attr_t中的stackaddr来初始化task_struct->thread->sp(sp指向struct pt_regs对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的fork不同的地方。由于线程栈是从进程的地址空间中map出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的task_struct的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

3.进程栈和线程栈大小的调整

进程和线程的栈分别是多大呢?首先从我们熟悉的ulimit -s说起,熟悉linux的人都应该知道通过ulimit -s可以修改栈的大小,除此之外还有getrlimit/setrlimit两个函数:

点击(此处)折叠或打开

intgetrlimit(intresource,struct rlimit*rlim);

intsetrlimit(intresource,conststruct rlimit*rlim);

这两个函数当第一个参数传入RLIMIT_STACK时,可以设置和获取栈的大小,其作用和ulimit -s是一样的,只是单位不同,ulimit -s的单位是kB,而这两个函数的单位是B(字节),详细使用方法请参考man手册。

最后还有线程的pthread_attr_setstacksize/pthread_attr_getstacksize。

使用setrlimit和使用ulimit -s设置栈大小效果相同,这两种方式都是针对进程栈大小设置,只不过前者只真对当前进程,后者针对当前shell

而线程栈大小的关系就相对比较复杂点,前文说过线程大小是静态的,是在创建时就确定了的,当然如果使用pthread_attr_setstacksize可以在创建线程时指定线程栈大小,但如果不指定线程栈的话其默认大小是什么情况呢?想要了解线程栈的大小就要看glibc的线程创建函数,具体就是pthread_create->__pthread_create_2_1->allocate_stack。具体代码还是比较复杂的,这里简化为一个伪代码:

点击(此处)折叠或打开

limit=getlimit(RLIMIT_STACK)

if(limit==RLIMIT_INFINITY)

thread.rlimit=ARCH_STACK_DEFAULT_SIZE//2M

elseifthread.rlimit< PTHREAD_STACK_MIN //16k

thread.rlimit=PTHREAD_STACK_MIN

可以看出,线程默认栈大小和进程栈大小的关系:

1)如果ulimit(setrlimit)设置大小大于16k,则线程栈默认大小由ulimit(setrlimit)决定;

2)如果ulimit(setrlimit)设置大小小于16k,则线程栈默认大小为16;

3)如果ulimit(setrlimit)设置大小为无限制,则线程栈默认大小为2M;

所以我们如果使用ulimit设置进程栈大小是无限大其实栈大小反而相对比较小,这是为什么呢?前面我们已经讲过线程栈和进程栈的位置不同,线程栈其实是在进程的堆上分配的,并且不会动态增加,所以不可能设置一个无限大小的线程栈。

最后,我们再对进程栈和线程栈做一下总结和说明:

(1)ulimit -s决定进程栈的大小,但不是严格相等(实际测试稍大于ulimit -s设置

(2)创建线程时如果通过pthread_attr_setstacksize设置了线程栈大小,则使用该属性创建的线程栈大小就为其设置的值,但不影响线程默认属性的栈大小值,也不影响ulimit -s的值。

(3)线程一旦创建就无法在修改其栈大小了,即使使用setrlimit。

(4)pthread_attr_setstacksize/pthread_attr_getstacksize的作用是获取和设置线程属性中的栈大小的,而不获取设置线程栈大小的。可以再创建前设置好线程属性,这样使用该属性创建线程就能影响线程的栈大小了。但通过pthread_attr_init,pthread_attr_getstacksize是无法获取当前线程栈大小的,只能获取默认属性的线程栈大小,其值未必就是当前线程栈大小。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表德赢Vwin官网 网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11292

    浏览量

    209318
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19675
  • 内存映射
    +关注

    关注

    0

    文章

    14

    浏览量

    7415
  • 进程
    +关注

    关注

    0

    文章

    203

    浏览量

    13960
收藏 人收藏

    评论

    相关推荐

    Linux网络原理与实现

    本文尝试技术研发与工程实践(而非纯理论学习)角度,在原理与实现、监控告警、 配置调优三方面介绍内核5.10 网络。由于内容非常多,因此分为了几篇系列文章。
    发表于 08-10 08:58 3740次阅读

    进程线程的区别

    应用程序提供多个线程执行控制。 逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多
    发表于 12-12 09:28

    请问uCOS-II中的任务是进程还是线程

    个提问,然后上面的定义是百度的。按照定义任务应该是进程。有没大佬rtos的角度剖析一下进程线程的区别。
    发表于 06-03 05:07

    有关Linux系统的PBC (进程控制块)基础知识介绍

    ,比如打开的文件,挂起的信号,处理器状态,内核数据结构,内存映射地址空间等。在操作系统中,内核的调度对象时线程,而不是进程线程
    发表于 06-23 16:27

    用一个实例展示一下Linux内核帧的入和退过程

    1、Linux内核调试方法总结之帧  帧  帧和指针可以说是C语言的精髓。帧是一种特殊的
    发表于 11-04 15:47

    基于STM32的虚拟多线程(TI_BLE协议_ZStack协议)

    基于STM32的虚拟多线程,可以很好的用于裸机程序中,用于模拟小型操作系统的多线程概念。本实例参考了参考TI_BLE协议_ZStack协议
    发表于 06-14 10:42 6925次阅读
    基于STM32的虚拟多<b class='flag-5'>线程</b>(TI_BLE协议<b class='flag-5'>栈</b>_ZStack协议<b class='flag-5'>栈</b>)

    一文详解Linux内核回溯与妙用

    网上或多或少都能找到回溯的一些文章,但是讲的都并不完整,没有将内核回溯的功能用于实际的内核、应用程序调试,这是本篇文章的核心:尽可能引导读者将
    的头像 发表于 10-05 10:02 5376次阅读
    一文详解<b class='flag-5'>Linux</b><b class='flag-5'>内核</b>的<b class='flag-5'>栈</b>回溯与妙用

    Linux进程内核的认识

    在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的并不是原先用户空间中的
    发表于 05-12 08:53 623次阅读
    对<b class='flag-5'>Linux</b>的<b class='flag-5'>进程</b><b class='flag-5'>内核</b><b class='flag-5'>栈</b>的认识

    浅谈鸿蒙内核源码的

    上面的代码和鸿蒙内核方式一样,都采用了递减满的方式, 什么是递减满?
    的头像 发表于 04-24 11:21 1434次阅读
    浅谈鸿蒙<b class='flag-5'>内核</b>源码的<b class='flag-5'>栈</b>

    Linux线程进程的区别

    不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户
    的头像 发表于 08-24 15:37 1854次阅读
    <b class='flag-5'>Linux</b>下<b class='flag-5'>线程</b>与<b class='flag-5'>进程</b>的区别

    Linux中的进程线程内核以及中断

    首先, (stack) 是一种串列形式的 数据结构。这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:顶 top) 进行 推入
    的头像 发表于 05-14 09:30 696次阅读
    <b class='flag-5'>Linux</b>中的<b class='flag-5'>进程</b><b class='flag-5'>栈</b>、<b class='flag-5'>线程</b><b class='flag-5'>栈</b>、<b class='flag-5'>内核</b><b class='flag-5'>栈</b>以及中断<b class='flag-5'>栈</b>

    系统调用:用户内核的切换(上)

    当发生系统调用、产生异常,外设发生中断等事件时,会发生用户内核之间的切换, 本文系统调用角度分析用户
    的头像 发表于 07-31 11:27 869次阅读
    系统调用:用户<b class='flag-5'>栈</b>与<b class='flag-5'>内核</b><b class='flag-5'>栈</b>的切换(上)

    linux中的进程,线程,内核的区别

    大多数的处理器架构,都有实现硬件。有专门的指针寄存器,以及特定的硬件指令来完成 入/出 的操作。例如在 ARM 架构上,R13 (SP) 指针是堆栈指针寄存器,而 PUSH 是
    发表于 08-18 10:57 499次阅读
    <b class='flag-5'>linux</b>中的<b class='flag-5'>进程</b><b class='flag-5'>栈</b>,<b class='flag-5'>线程</b><b class='flag-5'>栈</b>,<b class='flag-5'>内核</b><b class='flag-5'>栈</b>的区别

    ethernetif_input和tcpip协议线程的作用

    tcpip协议线程是lwIP协议的核心线程,负责处理TCP/IP协议的各种功能,包括TCP连接管理、IP数据报的路由和转发、以及UDP
    的头像 发表于 03-20 10:01 1315次阅读

    Linux网络协议的实现

    网络协议是操作系统核心的一个重要组成部分,负责管理网络通信中的数据包处理。在 Linux 操作系统中,网络协议(Network Stack)负责实现 TCP/IP 协议簇,处理应用程序发起的网络
    的头像 发表于 09-10 09:51 298次阅读
    <b class='flag-5'>Linux</b>网络协议<b class='flag-5'>栈</b>的实现