万维网站域名,wordpress 下载功能,WordPress做成小程序,北京seo公司wyhseo1. 进程栈进程栈是属于用户态栈#xff0c;和进程虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间#xff1a;在 32 位机器下#xff0c;虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存#xff0c;页表由操作…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(current-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-spsp 指向 struct pt_regs 对象该结构体用于保存用户进程或者线程的寄存器现场。这些都不重要重要的是线程栈不能动态增长一旦用尽就没了这是和生成进程的 fork 不同的地方。由于线程栈是从进程的地址空间中 map 出来的一块内存区域原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct 的很多字段其中包括所有的 vma如果愿意其它线程也还是可以访问到的于是一定要注意。3. 进程栈和线程栈大小的调整进程和线程的栈分别是多大呢首先从我们熟悉的ulimit -s说起熟悉linux的人都应该知道通过ulimit -s可以修改栈的大小除此之外还有getrlimit/setrlimit两个函数int getrlimit(int resource, struct rlimit *rlim);int setrlimit(int resource, const struct 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 //2Melse if thread.rlimit PTHREAD_STACK_MIN //16kthread.rlimit PTHREAD_STACK_MIN
可以看出线程默认栈大小和进程栈大小的关系如果ulimit(setrlimit)设置大小大于16k则线程栈默认大小由ulimit(setrlimit)决定如果ulimit(setrlimit)设置大小小于16k则线程栈默认大小为16如果ulimit(setrlimit)设置大小为无限制则线程栈默认大小为2M所以我们如果使用ulimit设置进程栈大小是无限大其实栈大小反而相对比较小这是为什么呢前面我们已经讲过线程栈和进程栈的位置不同线程栈其实是在进程的堆上分配的并且不会动态增加所以不可能设置一个无限大小的线程栈。最后我们再对进程栈和线程栈做一下总结和说明ulimit -s决定进程栈的大小但不是严格相等实际测试稍大于ulimit -s设置创建线程时如果通过pthread_attr_setstacksize设置了线程栈大小则使用该属性创建的线程栈大小就为其设置的值但不影响线程默认属性的栈大小值也不影响ulimit -s的值。线程一旦创建就无法在修改其栈大小了即使使用setrlimit。pthread_attr_setstacksize/pthread_attr_getstacksize的作用是获取和设置线程属性中的栈大小的而不获取设置线程栈大小的。可以再创建前设置好线程属性这样使用该属性创建线程就能影响线程的栈大小了。但通过pthread_attr_initpthread_attr_getstacksize是无法获取当前线程栈大小的只能获取默认属性的线程栈大小其值未必就是当前线程栈大小。以上有不足的地方欢迎指出讨论觉得不错的朋友希望能得到您的转发支持同时可以持续关注我每天分享Linux C/C后台开发干货内容最后如果觉得学习资料难找的话可以添加小编的 LinuxC/C交流群 期待你的加入~