一.Linux进程运行时分为用户态和内核态,用户态有其自有的内存布局,内核态有内核态的内存管理机制。进程栈的管理如图1所示。
图1 进程栈示意图
说明:1.内核栈的底端为thread_info结构体,栈顶开始为堆栈区,其中PT_SIZE为保存进程现场信息的区域。其中task变量指向进程的task_struct进程管理结构体。该结构体按ORDER分配,直接分配物理页。
2.task_struct为进程管理结构体,其中stack变量指向栈首地址。该结构体属于slab分配。
3.mm_struct用户管理进程用户态的内存空间,只用涉及用户态进程才分配该结构体,该结构体属于slab分配。
4.vm_area_struct 用户空间区管理结构体,比如管理用户栈,代码段,数据段等区域,该结构体属于slab分配。其中用户空间栈的管理在第三节讲解。
二.内核栈从何而来和如何使用
1.内核栈在其父进程调用fork时创建
图2 进程栈申请代码
说明:在龙芯2K/3A平台上该空间大小为16K;所有的进程都有自己的内核栈;0号进程(idle)的内核栈通过图3所示,其它所有进程都是调用dup_task_struct函数申请空间得到。
图3 init 进程内核栈生成代码
2.内核栈如何使用-进程工作于内核态时使用
2.1 场景1-程序运行在用户态,此时产生中断或者系统调用,用户态切换至内核态
此时需要在进入内核态的代码中保存用户态的现场,现场信息存储在图1的PT_SIZE处。
2.2场景2-程序运行在内核态(内核线程或软中断),此时产生中断
使用当前进程的内核栈(当前的sp),[sp sp-PT_SIZE]区间保存现场。
3.内核栈-架构细节
3.1 进程切换
图4 进程切换-内核栈
在进程切换时将进程内核栈的低地址(thread_info的地址)存放如$28寄存器,同时计算好栈顶指针存放如全局变量kernelsp中。
3.2 读取kernelsp-在进入中断处理中handle_int-SAVE_ALL-SAVE_SOME
图5 保存现场-内核栈
在保存现场时,如果是用户态进入内核态,首先通过get_saved_sp读取栈指针,然后保存现场。
疑点1:为什么没有进程切换要保存$28至内存变量,保存现场要读取内存变量。这样效率是否底下,是否可以直接使用$28来提高效率?
答:在架构中$28的别名为gp,用户态也要使用gp。所以内核态要保存$28至内存(用户态进入中断)。
疑点2:用户态不使用gp不就可以了吗
答:原则上是可以的,但是需要所有的应用程序及库在编译时使用“-G 0”选项。
疑点3:用户态为什么要使用gp?这是一个故事。
答:提高效率,请大家移步see mips run 9.4章节细读。
三.用户空间栈从何而来
1.如何查看用户空间栈的大小
图6 用户空间栈-查看方法
第一行代表代码段的大小,代码很小但是却占用4K,判断该系统下页的大小为4K。
[stack]行表示该进程用户空间栈虚拟地址的开始和结束解释,栈大小128K+4K,其中[]代表该vma为匿名映射。
2.什么时候创建的用户空间栈
2.1 以图7中的代码为例分析场景
图 7 用户空间栈-代码案例
2.2 场景1:fork后的用户空间对比
首先运行代码结果如图8所示:
图8 用户空间栈-任务ID
其中父进程的id是15604,子进程的id为15615。
在没有执行execve前,我们查看父子进程的maps信息,图9和图10表示。
图 9 用户空间栈-父进程
图 10 用户空间栈-子进程
通过图9和图10对比,发现父子进程的虚拟地址空间信息时一样的,用户空间栈使用的虚拟地址也是完全一样的,使用fork时子进程完全使用父进程的信息。
说明:虽然父子进程的栈虚拟地址是一样的,在内存中确实时两份的(物理地址时不一样),其中使用的写时复制技术,当父子进程那个先返回时会产生却页异常,后为该进程申请物理地址。
2.3 场景2:execve后的用户空间
运行execve后的用户空间如图11 所示,请对比图10。
图11 用户空间栈-执行execve后
由图可知,所有的用户空间区域的虚拟地址的开始地址和结束地址都不一样了。原来的进程管理的所有内存区域被回收,使用新生成的替换。
说明:生成新的地址空间区域请查看代码do_execve_common函数,旧的进程用户空间内存区域管理结构mm在函数load_elf_binary中进行回收。
2.4 场景3:init/systemd进程用户空间的生成方式
init/systemd进程称为1号进程,1号进程以外的用户进程都是1号进程/1号进程的子进程在用户态调用fork生成的用户空间或者调用exec生成新的用户空间。1号进程直接在内核态调用exec生成自己的用户空间,也是唯一一个没有在用户态通过系统调用生成用户空间的进程。
2.5 场景4:clone/pthread 编程
实际pthread编程最终也是调用clone来完成线程的创建。图12 为pthread编程调用的过程(可以反汇编libpthread.so文件查看)
图 12 用户空间-pthread编程
说明:pthread的栈空间使用的是父进程堆区域上的空间。线程的mm结构体与进程共享,所以查看线程内存区域没有意义,它与父进程共享。
可以使用ps -L 查看某进程所包含的线程的id(LWP)。
来源:CSDN
作者:yuanjunqing
链接:https://blog.csdn.net/yuanjunqing/article/details/104283137