ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

虚拟内存[02] Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈【转】

2020-04-27 18:54:41  阅读:268  来源: 互联网

标签:02 info thread 中断 线程 内核 进程 虚拟内存


转自:https://durant35.github.io/2017/10/29/VM_Stacks/

 Linux 中有几种栈?各种栈的内存位置?

关于栈

  • 函数调用栈的典型内存布局
    • 栈帧 (Stack Frame) 的边界由栈帧基地址指针 EBP 和 栈指针 ESP 界定,EBP指向当前栈帧底部 (高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部 (低地址);
    • 当程序执行时,ESP会随着数据的入栈和出栈而移动,因此函数中对大部分数据的访问都基于EBP进行。
  • 栈帧存放着参数局部变量恢复前一栈帧所需要的数据等。

进程栈

 进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈是属于用户态栈,和进程虚拟地址空间 (Virtual Address Space) 密切相关。

 


图: 32 位系统下进程地址空间默认布局(左)和进程地址空间经典布局(右)

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

 

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    gary@xxx:~$ ulimit -a
    core file size (blocks, -c) 0
    data seg size (kbytes, -d) unlimited
    scheduling priority (-e) 0
    file size (blocks, -f) unlimited
    pending signals (-i) 30980
    max locked memory (kbytes, -l) 64
    max memory size (kbytes, -m) unlimited
    open files (-n) 1024
    pipe size (512 bytes, -p) 8
    POSIX message queues (bytes, -q) 819200
    real-time priority (-r) 0
    stack size (kbytes, -s) 8192
    cpu time (seconds, -t) unlimited
    max user processes (-u) 30980
    virtual memory (kbytes, -v) unlimited
    file locks (-x) unlimited
  • 进程栈的动态增长实现
    • 进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个缺页异常 (page fault)
    • 通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长:
      • 如果栈的大小低于 RLIMIT_STACK,那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制;
      • 如果达到了最大栈空间的大小,就会发生栈溢出 (stack overflow),进程将会收到内核发出的段错误(segmentation fault) 信号。
    • 动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

线程栈

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

  • 虽然线程的地址空间和进程一样,但是在对待其地址空间中的 stack 上还是有些区别的。
    • 对于 Linux 进程或者说主线程,其 stack 是在 fork() 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。
    • 对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的,使用 mmap()系统调用从进程的地址空间中 mmap 出来的一块内存区域,它不带有 VM_STACK_FLAGS 标记。
      • 由于线程的 mm->start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct 中,应该是使用 pthread_attr_t 中的 stackaddr 来初始化 task_struct->thread->sp(sp 指向 struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)
      • 重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork() 不同的地方。
  • 线程栈是从进程的地址空间中 mmap 出来的一块内存区域,原则上是线程私有的;但是同一个进程的所有线程在生成的时候会浅拷贝线程生成者 task_struct 的很多字段,其中包括所有的 vma,因此如果愿意,其它线程也还是可以访问到的,于是一定要注意!
  • 为什么需要单独的线程栈?不能共享同一个进程栈吗?
    • Linux 调度程序中并没有区分线程和进程 (线程是调度的基本单位),当调度程序需要唤醒“进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;线程和父进程完全共享一份地址空间,如果栈也用同一个,那就会遇到以下问题:
      • 假如进程的栈指针初始值为 0x7ffc80000000:父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;
      • 接着调度器唤醒子线程 A1:
        • 如果此时 A1 的栈指针 esp 为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据;
        • 如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?
        • 无论栈指针被设置到哪个值,都会有问题不是吗?

进程内核栈

 在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。

 进程内核栈在进程创建的时候,通过 slab 分配器 从 thread_info_cache 缓存池中分配出来,大小为 THREAD_SIZE,一般来说是一个页大小 4K;

  • 为什么需要单独的进程内核栈?

     所有进程运行的时候,都可能通过系统调用陷入内核态继续执行:假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态;那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据,一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了。

  • 进程和子线程是否共享一个进程内核栈?

     线程和进程创建的时候都调用 dup_task_struct() 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node() 出来的,因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈(本来线程就是 CPU 调度的基本单位)。

  • 进程内核栈与current当前进程
    • 内核执行的大多数操作还是和某个特定的进程相关,内核代码可通过访问current来获得当前进程。

       内核开发者设计了一种能找到运行在相关 CPU 上的当前进程的机制;因为 current的引用会频繁发生,因此这种机制必须是快速的。

    • 内核将进程内核栈的头部一段空间用于存放thread_info结构体,该结构体中则记录了对应进程的描述符task_struct
  • 1
    2
    3
    4
    union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
    };
    • 有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info
    • 成功获取到 thread_info 后,直接取出它的 task 成员就成功得到了task_struct,也就是如下 current 宏的实现方法:
  • current 宏的实现方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    register unsigned long current_stack_pointer asm ("sp");

    static inline struct thread_info *current_thread_info(void)
    {
    return (struct thread_info *)
    (current_stack_pointer & ~(THREAD_SIZE - 1));
    }

    #define get_current() (current_thread_info()->task)

    #define current get_current()
    • 由于 thread_union 结构体是从thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cache 在 kmem_cache_create 创建的时候,保证了地址是 THREAD_SIZE对齐的;
    • 因此只需要对栈指针进行 THREAD_SIZE 对齐,即可获得 thread_union 的地址,也就获得了 thread_info 的地址:直接将 esp 的地址上 ~(THREAD_SIZE - 1)

中断栈

 进程陷入内核态的时候,需要内核栈来支持内核函数调用;中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。

  • 由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的(但是具体是否共享,这和具体处理架构密切相关)。
    • 中断栈独立于内核栈

       x86 上中断栈就是独立于内核栈的,独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c 的 irq_ctx_init() 函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈)。

      • 函数使用 __alloc_pages() 在低端内存区分配 2 个物理页面,也就是 8KB 大小的空间。
      • 这个函数还会为 softirq 分配一个同样大小的独立堆栈,如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。
    • ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。
  • 为什么需要单独中断栈?

    这个问题其实不对,ARM 架构就没有独立的中断栈。

References

·《Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈》
·《Linux虚拟地址空间布局以及进程栈和线程栈总结》

本文标题:虚拟内存[02] Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

文章作者:Gary

发布时间:2017-10-29, 22:01:00

最后更新:2020-03-28, 12:37:22

原始链接:http://durant35.github.io/2017/10/29/VM_Stacks/ 

标签:02,info,thread,中断,线程,内核,进程,虚拟内存
来源: https://www.cnblogs.com/sky-heaven/p/12788807.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有