进程栈管理分析-基于龙芯64位处理器

一世执手 提交于 2020-02-13 00:56:18

 

 

一.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)。

 

 

 

 

 

 

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!