百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格…
百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | csdn | harmony >
笔者第一次看到fork时,说是一次调用,两次返回,当时就懵圈了,多新鲜,真的很难理解.因为这足以颠覆了以往对函数的认知, 函数调用还能这么玩,父进程调用一次,父子进程各返回一次.而且只能通过返回值来判断是哪个进程的返回.所以一直有几个问题缠绕在脑海中.
fork是什么? 外部如何正确使用它.
为什么要用fork这种设计? fork的本质和好处是什么?
怎么做到的? 调用fork()使得父子进程各返回一次,一个函数不是只能返回一次吗,怎么做到返回两次的,其中到底发生了什么?
为什么pid == 0 代表了是子进程的返���? 为什么父进程不需要返回 0 ?
直到看了linux内核源码后才搞明白了这些问题,但系列篇的定位挖透鸿蒙的内核源码,所以本篇将深入fork函数,从鸿蒙角度去说明白这些问题. 在看本篇之前一定要先看系列篇的其他篇幅.如(任务切换篇,寄存器篇,工作模式篇,系统调用篇 等等),有了这些基础,会很好理解fork的实现过程.
fork是什么
先看一个网上经常拿来说fork的一个代码片段.
1 | #include <sys/types.h> |
1 | pid < 0 |
fork 失败
1 | pid == 0 |
fork成功,是子进程的返回
1 | pid > 0 |
fork成功,是父进程的返回
fork的返回值这样规定是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。
子进程并没有真正执行fork(),而是内核用了一个很巧妙的方法获得了返回值,并且将返回值硬生生的改写成了0,这是笔者认为fork的实现最精彩的部分.
运行结果
1 | $ ./a.out |
这个程序的运行过程如下图所示。
解读
fork 是一个系统调用,因此会切换到SVC模式运行.在SVC栈中父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同.
从案例的执行上可以看出,fork 之后的代码父子进程都会执行,即代码段指向(PC寄存器)是一样的.实际上fork只被父进程调用了一次,子进程并没有执行
1 | fork |
函数,但是却获得了一个返回值,
1 | pid == 0 |
,这个非常重要.这是本篇说明的重点.
从执行结果上看,父进程打印了三次(This is the parent),因为 n = 3. 子进程打印了六次(This is the child),因为 n = 6. 而子程序并没有执行以下代码:
1 | pid_t pid; |
子进程是从
1 | pid = fork() |
后开始执行的,按理它不会在新任务栈中出现这些变量,而实际上后面又能顺利的使用这些变量,说明父进程当前任务的用户态的数据也复制了一份给子进程的新任务栈中.
被fork成功的子进程跑的首条代码指令是
1 | pid = 0 |
,这里的0是返回值,存放在
1 | R0 |
寄存器中.说明父进程的任务上下文也进行了一次拷贝,父进程从内核态回到用户态时恢复的上下文和子进程的任务上下文是一样的,即 PC寄存器指向是一样的,如此才能确保在代码段相同的位置执行.
执行
1 | ./a.out |
后 第一条打印的是
1 | This is the child |
说明
1 | fork() |
中发生了一次调度,CPU切到了子进程的任务执行,
1 | sleep(1) |
的本质在系列篇中多次说过是任务主动放弃CPU的使用权,将自己挂入任务等待链表,由此发生一次任务调度,CPU切到父进程执行,才有了打印第二条的
1 | This is the parent |
,父进程的
1 | sleep(1) |
又切到子进程如此往返,直到 n = 0, 结束父子进程.
但这个例子和笔者的解读只解释了fork是什么的使用说明书,并猜测其中做了些什么,并没有说明为什么要这样做和代码是怎么实现的. 正式结合鸿蒙的源码说清楚为什么和怎么做这两个问题?
为什么是fork
fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。 系列篇已经写了40+多篇,已经很容易理解一个程序运行起来就需要各种资源(内存,文件,ipc,监控信息等等),资源就需要管理,进程就是管理资源的容器.这些资源相当于干活需要各种工具一样,干活的工具都差不多,实在没必再走流程一一申请,而且申请下来会发现和别人手里已有的工具都一样, 别人有直接拿过来使用它不香吗? 所以最简单的办法就是认个干爹,让干爹拷贝一份干活工具给你.这样只需要专心的干好活(任务)就行了. fork的本质就是copy,具体看代码.
fork怎么实现的?
1 | //系统调用之fork ,建议去 https://gitee.com/weharmony/kernel_liteos_a_note fork 一下? :P |
1 | //拷贝一个Task过程 |
解读
可以看出fork的主体函数是
1 | OsCopyProcess |
,先申请一个干净的PCB,相当于申请一个容器装资源.
初始化这个容器
1 | OsForkInitPCB |
,
1 | OsInitPCB |
先把容器打扫干净,虚拟空间,地址映射表(L1表),各种链表初始化好,为接下来的内容拷贝做好准备.
1 | OsCopyParent |
把家族基因/关系传递给子进程,谁是你的老祖宗,你的七大姑八大姨是谁都得告诉你知道,这些都将挂到你已经初始化好的链表上.
1 | OsCopyTask |
这个很重要,拷贝父进程当前执行的任务数据给子进程的新任务,系列篇中已经说过,真正让CPU干活的是任务(线程),所以子进程需要创建一个新任务
1 | LOS_TaskCreateOnly |
来接受当前任务的数据,这个数据包括栈的数据,运行代码段指向,
1 | OsUserCloneParentStack |
将用户态的上下文数据
1 | TaskContext |
拷贝到子进程新任务的栈底位置, 也就是说新任务运行栈中此时只有上下文的数据.而且有最最最重要的一句代码
1 | context->R[0] = 0; |
强制性的将未来恢复上下文
1 | R0 |
寄存器的数据改成了0, 这意味着调度算法切到子进程的任务后, 任务干的第一件事是恢复上下文,届时
1 | R0 |
寄存器的值变成0,而
1 | R0=0 |
意味着什么? 同时
1 | LR/SP |
寄存器的值也和父进程的一样.这又意味着什么?
系列篇寄存器篇中以说过返回值就是存在R0寄存器中,
1 | A()->B() |
,A拿B的返回值只认
1 | R0 |
的数据,读到什么就是什么返回值,而R0寄存器值等于0,等同于获得返回值为0, 而LR寄存器所指向的指令是
1 | pid=返回值 |
, sp寄存器记录了栈中的开始计算的位置,如此完全还原了父进程调用
1 | fork() |
前的运行场景,唯一的区别是改变了
1 | R0 |
寄存器的值,所以才有了
1 | pid = 0;//fork()的返回值,注意子进程并没有执行fork(),它只是通过恢复上下文获得了一个返回值. |
由此确保了这是子进程的返回.这是
1 | fork() |
最精彩的部分.一定要好好理解.
1 | OsCopyTask``OsUserCloneParentStack |
的代码细节.会让你醍醐灌顶,永生难忘.
父进程的返回是
1 | processID = child->processID; |
是子进程的ID,任何子进程的ID是不可能等于0的,成功了只能是大于0. 失败了就是负数
1 | return -ret; |
1 | OsCopyProcessResources |
用于赋值各种资源,包括拷贝虚拟空间内存,拷贝打开的文件列表,IPC等等.
1 | OsChildSetProcessGroupAndSched |
设置子进程组和调度的准备工作,加入调度队列,准备调度.
1 | LOS_MpSchedule |
是个核间中断,给所有CPU发送调度信号,让所有CPU发生一次调度.由此父进程让出CPU使用权,因为子进程的调度优先级和父进程是平级,而同级情况下子进程的任务已经插到就绪队列的头部位置
1 | OS_PROCESS_PRI_QUEUE_ENQUEUE |
排在了父进程任务的前面,所以在没有比他们更高优先级的进程和任务出现之前,下一次被调度到的任务就是子进程的任务.也就是在本篇开头看到的
1 | $ ./a.out |
以上为fork在鸿蒙内核的整个实现过程,务必结合系列篇其他篇理解,一次理解透彻,终生不忘.
鸿蒙源码百篇博客 往期回顾
v45.03 (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | harmony >
v44.03 (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | harmony >
v43.03 (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | harmony >
v42.03 (中断切换篇) | 中断切换到底在切换什么? < csdn | harmony >
v41.03 (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | harmony >
v40.03 (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | harmony >
v39.03 (异常接管篇) | 社会很单纯,复杂的是人 < csdn | harmony >
v38.03 (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | harmony >
v37.03 (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | harmony >
v36.03 (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | harmony >
v35.03 (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | harmony >
v34.03 (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | harmony >
v33.03 (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | harmony >
v32.03 (CPU篇) | 内核是如何描述CPU的? < csdn | harmony >
v31.03 (定时器篇) | 内核最高优先级任务是谁? < csdn | harmony >
v30.03 (事件控制篇) | 任务间多对多的同步方案 < csdn | harmony >
v29.03 (信号量篇) | 信号量解决任务同步问题 < csdn | harmony >
v28.03 (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | harmony >
v27.03 (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | harmony >
v26.03 (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | harmony >
v25.03 (并发并行篇) | 怎么记住并发并行的区别? < csdn | harmony >
v24.03 (进程概念篇) | 进程在管理哪些资源? < csdn | harmony >
v23.02 (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | harmony >
v22.02 (汇编基础篇) | CPU在哪里打卡上班? < csdn | harmony >
v21.02 (线程概念篇) | 是谁在不断的折腾CPU? < csdn | harmony >
v20.02 (用栈方式篇) | 栈是构建底层运行的基础 < csdn | harmony >
v19.02 (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | harmony >
v18.02 (源码结构篇) | 内核500问你能答对多少? < csdn | harmony >
v17.02 (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | harmony >
v16.02 (内存规则篇) | 内存管理到底在管什么? < csdn | harmony >
v15.02 (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | harmony >
v14.02 (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | harmony >
v13.02 (源码注释篇) | 热爱是所有的理由和答案 < csdn | harmony >
v12.02 (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | harmony >
v11.02 (内存分配篇) | 内存有哪些分配方式? < csdn | harmony >
v10.02 (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | harmony >
v09.02 (调度故事篇) | 用故事说内核调度 < csdn | harmony >
v08.02 (总目录) | 百万汉字注解 百篇博客分析 < csdn | harmony >
v07.02 (调度机制篇) | 任务是如何被调度执行的? < csdn | harmony >
v06.02 (调度队列篇) | 就绪队列对调度的作用 < csdn | harmony >
v05.02 (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | harmony >
v04.02 (任务调度篇) | 任务是内核调度的单元 < csdn | harmony >
v03.02 (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | harmony >
v02.02 (进程管理篇) | 进程是内核资源管理单元 < csdn | harmony >
v01.09 (双向链表篇) | 谁是内核最重要结构体? < csdn | harmony >
参与贡献
访问注解仓库地址
Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request
新建 Issue
喜欢请大方 点赞+关注+收藏 吧
关注「鸿蒙内核源码分析」公众号,百万汉字注解 + 百篇博客分析 => 深挖鸿蒙内核源码
各大站点搜 “鸿蒙内核源码分析” .欢迎转载,请注明出处.
本文标题: 鸿蒙内核源码分析(fork篇) - fork是如何做到调用一次,返回两次的 - - 百篇博客分析HarmonyOS源码 - v45.03
本文作者: OSChina
发布时间: 2021年04月15日 10:16
最后更新: 2025年04月03日 11:07
原始链接: https://haoxiang.eu.org/d056fdd5/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!