网站开发合同范本 doc,网站的产品上传图片,wordpress搭建discuz,常州的平台公司我们在学习操作系统课程的时候#xff0c;应该都学过fork的概念。fork是一个系统调用#xff0c;用于将当前进程/线程分裂成完全相同的两个。
在网络上#xff0c;很多关于fork的文章都大同小异#xff0c;讲的都是很通用的fork的原理以及大致的过程。但是#xff0c;大家…我们在学习操作系统课程的时候应该都学过fork的概念。fork是一个系统调用用于将当前进程/线程分裂成完全相同的两个。
在网络上很多关于fork的文章都大同小异讲的都是很通用的fork的原理以及大致的过程。但是大家有没有想过一个问题用户程序调用fork()和内核下调用fork()背后的逻辑是不一样的。咱平时如果没有真的去写操作系统的话应该不会意识到这个问题。
虚拟地址空间分布
首先我们先来了解一下进程的虚拟地址空间是怎么分布的 虚拟地址空间的高地址部分为内核空间低地址部分为用户空间。各个进程之间的内核空间是共享的只有用户空间才是独占的。
每个进程都有1个内核栈这个内核栈位于内核空间。并且每个进程还有一个用户栈位于低地址部分。
当进程陷入内核态的时候将会使用内核栈进行处理当返回用户态的时候又会换回去使用用户栈。
需要注意的是用户栈在用户空间的映射是由操作系统指定的父子进程的用户栈的虚拟地址是相同的。而父子进程的内核栈的虚拟地址则是不同的。
用户态进程调用fork()
网络上的文章一般描述的是用户态下的fork。用户态的fork是这样的一个过程 首先用户进程发起系统调用陷入内核态。然后在fork系统调用的函数里面操作系统将会初始化pcb、线程结构体、对用户空间的内存的拷贝最后把子进程加入调度队列。
这里“内存拷贝”这一点就是关键所在也是众多文章没有提及的部分。
在用户态的fork中由于用户进程的栈空间位于就是位于用户空间之中并且用户栈一般是位于操作系统指定的地址上不同的进程的用户栈的基地址相同。又由于进程在返回用户态的时候内核栈是空的因此我们只需要将用户空间进行拷贝当子进程返回用户态的时候自然就能执行。这是理所当然的事情。
内核态进程的fork
对于在内核态下运行进程而言其具有在低地址空间的进程的栈也具有高地址空间部分的内核栈。进程正常运行时使用其低地址空间部分的栈发起了系统调用之后则会使用其高地址空间的内核栈。内核态进程的fork和用户进程的fork是相同的。
内核线程的fork
讲了这么久这才轮到我们的主角内核线程。内核线程的fork的过程与前面提到的两者是不同的。
首先我们需要认识一下内核线程。内核线程是内核中的一些线程他们共用同一个虚拟地址空间。并且他们运行时所使用的栈只有内核栈。也就是说父进程在系统调用返回的时候并不会执行切换到用户栈的操作因为根本不存在
那么这样对我们的fork有什么影响呢
必须拷贝内核栈
由于我们的内核线程只使用内核栈那就意味着fork()系统调用到来时内核栈中除了系统调用的栈帧以外还会有其他内容我们必须拷贝内核栈 如上图所示如果是用户进程/内核进程的fork由于其在发起fork()调用之前他们一直工作在自己的用户空间的栈上内核栈是空的。发起fork系统调用后内核栈中才会被压入一个fork调用所在的栈帧。由于进程最终都要返回到其用户栈上且离开内核的时候内核栈必须为空。因此我们不需要拷贝内核栈的内容只需要拷贝用户栈的内容。而用户栈就是位于用户空间内因此对用户空间的整体拷贝就能完成整个操作。
而内核线程不存在用户栈其所有运行操作都是在内核栈上进行的因此在发起fork调用之后fork调用所在的栈帧不是位于内核栈的底部。由于fork返回后计算机需要执行内核栈中已有栈帧的内容因此我们需要拷贝内核栈。
必须重写子进程的栈帧
看了上面之后可能很多人就会觉得那不就是直接拷贝内核栈然后子进程返回的时候直接切换到新的内核栈不就好了吗这就是一个很大的误区。
如果真的是直接拷贝栈然后换栈的话就必然会出错。
再回到文章开头的“虚拟地址空间分布”部分讲的“父子进程的内核栈的虚拟地址是不同的”。这句话非常重要。内核栈一般是从slab分配器中分配得来的一块内存地址而且我们也不能仿照对用户进程的操作那样将每个内核线程的内核栈映射到相同的地址处这显然是不可行的。
父子进程的内核栈的虚拟地址的不同使得我们必须重写栈帧中的内容。这是为什么呢首先我们需要理解栈帧的结构 当发生函数调用时处理器会把当前当前函数的返回地址、栈基址寄存器的值压入栈中。返回地址指的是被调用的函数返回时将会从哪个位置开始执行。栈基址寄存器值则指的是当前栈帧的基地址。注意不是内核栈的基地址这是很多人的一个误区。 每个栈帧的大小是不相同的处理器是通过这个值来区分不同的栈帧的。当要弹出一个栈帧时处理器把这个值赋值给栈指针寄存器这样就找到了上一个栈帧的起始地址。同样的上一个栈帧的起始地址部分存的值就是再上一个栈帧的起始的值。
明白了处理器是如何在栈帧之间跳转之后我们就能明白为什么必须重写内核栈的栈帧了直接拷贝内核栈后新的内核栈中的每个栈帧内的“栈基址寄存器值”的内容仍然是父进程的内核栈的地址。因此我们需要重写这个值让它指向新的内核栈中的对应地址这样才是正确的。
重写的方法不难但是有点绕口
计算子线程栈帧中某个位置A的栈基址寄存器值B相对于父线程的栈底的偏移量delta然后使用子线程的栈底的地址C减去delta得到子线程的该栈帧中的栈基址寄存器值D并将D填写到位置A中。
然后将D赋值给A重复上述过程直到子线程中的所有的栈基址寄存器值被重写。
最后把子线程的fork()栈帧中的栈指针进行重写子线程的内核栈就处理完成了。剩余的步骤就和普通的fork没有区别了。
重写的部分比较拗口因此在这里放对应的代码帮助理解
代码的对应链接在这里DragonOS/process.c at aa7dc4daa5e7f1cc165a9985773e2d2cb23a7281 · fslongjin/DragonOS · GitHub /*** brief 重写内核栈中的rbp地址** param new_regs 子进程的reg* param new_pcb 子进程的pcb* return int*/
static int process_rewrite_rbp(struct pt_regs *new_regs, struct process_control_block *new_pcb)
{uint64_t new_top ((uint64_t)new_pcb) STACK_SIZE;uint64_t old_top (uint64_t)(current_pcb) STACK_SIZE;uint64_t *rbp new_regs-rbp;uint64_t *tmp rbp;// 超出内核栈范围if ((uint64_t)*rbp old_top || (uint64_t)*rbp (old_top - STACK_SIZE))return 0;while (1){// 计算deltauint64_t delta old_top - *rbp;// 计算新的rbp值uint64_t newVal new_top - delta;// 新的值不合法if (unlikely((uint64_t)newVal new_top || (uint64_t)newVal (new_top - STACK_SIZE)))break;// 将新的值写入对应位置*rbp newVal;// 跳转栈帧rbp (uint64_t *)*rbp;}// 设置内核态fork返回到enter_syscall_int()函数内的时候rsp寄存器的值new_regs-rsp new_top - (old_top - new_regs-rsp);return 0;
}
小结
小结一下内核线程由于其在fork返回之后仍然使用内核栈而父子线程的内核栈的地址不同导致拷贝栈帧后需要重写子进程内核栈中每个栈帧内保存的栈基址寄存器值使其能够正常运行。
用户进程/内核进程的fork不需要这样操作的原因则是他们在fork返回后内核栈是空的。并且平时运行的时候具有独立的用户地址空间运行时的用户栈都被映射到了相同的虚拟地址处因此不需要重写也能正常运行。
欢迎加入DragonOS的开发
我发起了DragonOS操作系统项目目前还处于起步阶段欢迎感兴趣的朋友们加入
项目官网http://DragonOS.orghttp://dragonos.org/
GitHub地址GitHub - fslongjin/DragonOS: 一个64位的操作系统。An x86_64 operating system.一个64位的操作系统。An x86_64 operating system. Contribute to fslongjin/DragonOS development by creating an account on GitHub.https://github.com/fslongjin/DragonOS
开发交流群115763565
转载请注明来源内核线程的fork与普通的fork的区别 | | 龙进的博客
欢迎关注我的公众号“灯珑”让我们一起了解更多的事物~