鸿蒙内核源码分析(fork篇) | 一次调用,两次返回 | 百篇博客分析OpenHarmony源码 | v45.03
百篇博客系列篇.本篇为:
进程管理相关篇为:
- v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内核资源 | 51.c.h .o
- v24.xx 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 51.c.h .o
- v45.xx 鸿蒙内核源码分析(Fork篇) | 一次调用,两次返回 | 51.c.h .o
- v46.xx 鸿蒙内核源码分析(特殊进程篇) | 龙生龙凤生凤老鼠生儿会打洞 | 51.c.h .o
- v47.xx 鸿蒙内核源码分析(进程回收篇) | 临终前如何向老祖宗托孤 | 51.c.h .o
- v48.xx 鸿蒙内核源码分析(信号生产篇) | 年过半百,依然活力十足 | 51.c.h .o
- v49.xx 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 51.c.h .o
笔者第一次看到fork时,说是一次调用,两次返回,当时就懵圈了,多新鲜,真的很难理解.因为这足以颠覆了以往对函数的认知, 函数调用还能这么玩,父进程调用一次,父子进程各返回一次.而且只能通过返回值来判断是哪个进程的返回.所以一直有几个问题缠绕在脑海中.
- fork是什么? 外部如何正确使用它.
- 为什么要用fork这种设计? fork的本质和好处是什么?
- 怎么做到的? 调用fork()使得父子进程各返回一次,怎么做到返回两次的,其中到底发生了什么?
- 为什么
pid = 0
代表了是子进程的返回? 为什么父进程不需要返回 0 ?
直到看了linux内核源码后才搞明白,但系列篇的定位是挖透鸿蒙的内核源码,所以本篇将深入fork函数,用鸿蒙内核源码去说明白这些问题.在看本篇之前建议要先看系列篇的其他篇幅.如(任务切换篇,寄存器篇,工作模式篇,系统调用篇 等),有了这些基础,会很好理解fork的实现过程.
fork是什么
先看一个网上经常拿来说fork的一个代码片段.
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
char *message;
int n;
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
}
if (pid == 0) {
message = "This is the child\n";
n = 6;
} else {
message = "This is the parent\n";
n = 3;
}
for(; n > 0; n--) {
printf(message);
sleep(1);
}
return 0;
}
pid < 0
fork 失败pid == 0
fork成功,是子进程的返回pid > 0
fork成功,是父进程的返回fork
的返回值这样规定是有道理的。fork
在子进程中返回0,子进程仍可以调用getpid
函数得到自己的进程id,也可以调用getppid
函数得到父进程的id。在父进程中用getpid
可以得到自己的进程id,然而要想得到子进程的id,只有将fork
的返回值记录下来,别无它法。- 子进程并没有真正执行
fork()
,而是内核用了一个很巧妙的方法获得了返回值,并且将返回值硬生生的改写成了0,这是笔者认为fork
的实现最精彩的部分.
运行结果
$ ./a.out
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
这个程序的运行过程如下图所示。
解读
fork()
是一个系统调用,因此会切换到SVC模式运行.在SVC栈中父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同.从案例的执行上可以看出,fork 之后的代码父子进程都会执行,即代码段指向(PC寄存器)是一样的.实际上fork只被父进程调用了一次,子进程并没有执行
fork
函数,但是却获得了一个返回值,pid == 0
,这个非常重要.这是本篇说明的重点.从执行结果上看,父进程打印了三次(This is the parent),因为 n = 3. 子进程打印了六次(This is the child),因为 n = 6. 而子程序并没有执行以下代码:
pid_t pid;
char *message;
int n;
子进程是从
pid = fork()
后开始执行的,按理它不会在新任务栈中出现这些变量,而实际上后面又能顺利的使用这些变量,说明父进程当前任务的用户态的数据也复制了一份给子进程的新任务栈中.被fork成功的子进程跑的首条代码指令是
pid = 0
,这里的0是返回值,存放在R0
寄存器中.说明父进程的任务上下文也进行了一次拷贝,父进程从内核态回到用户态时恢复的上下文和子进程的任务上下文是一样的,即 PC寄存器指向是一样的,如此才能确保在代码段相同的位置执行.执行
./a.out
后 第一条打印的是This is the child
说明fork()
中发生了一次调度,CPU切到了子进程的任务执行,sleep(1)
的本质在系列篇中多次说过是任务主动放弃CPU的使用权,将自己挂入任务等待链表,由此发生一次任务调度,CPU切到父进程执行,才有了打印第二条的This is the parent
,父进程的sleep(1)
又切到子进程如此往返,直到 n = 0, 结束父子进程.但这个例子和笔者的解读只解释了fork是什么的使用说明书,并猜测其中做了些什么,并没有说明为什么要这样做和代码是怎么实现的. 正式结合鸿蒙的源码说清楚为什么和怎么做这两个问题?
为什么是fork
fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。
系列篇已经写了40+多篇,已经很容易理解一个程序运行起来就需要各种资源(内存,文件,ipc,监控信息等等),资源就需要管理,进程就是管理资源的容器.这些资源相当于干活需要各种工具一样,干活的工具都差不多,实在没必再走流程一一申请,而且申请下来会发现和别人手里已有的工具都一样, 别人有直接拿过来使用它不香吗? 所以最简单的办法就是认个干爹,让干爹拷贝一份干活工具给你.这样只需要专心的干好活(任务)就行了. fork的本质就是copy,具体看代码.
fork怎么实现的?
//系统调用之fork ,建议去 https://gitee.com/weharmony/kernel_liteos_a_note fork 一下? :P
int SysFork(void)
{
return OsClone(CLONE_SIGHAND, 0, 0);//本质就是克隆
}
LITE_OS_SEC_TEXT INT32 OsClone(UINT32 flags, UINTPTR sp, UINT32 size)
{
UINT32 cloneFlag = CLONE_PARENT | CLONE_THREAD | CLONE_VFORK | CLONE_VM;
if (flags & (~cloneFlag)) {
PRINT_WARN("Clone dont support some flags!\n");
}
return OsCopyProcess(cloneFlag & flags, NULL, sp, size);
}
STATIC INT32 OsCopyProcess(UINT32 flags, const CHAR *name, UINTPTR sp, UINT32 size)
{
UINT32 intSave, ret, processID;
LosProcessCB *run = OsCurrProcessGet();//获取当前进程
LosProcessCB *child = OsGetFreePCB();//从进程池中申请一个进程控制块,鸿蒙进程池默认64
if (child == NULL) {
return -LOS_EAGAIN;
}
processID = child->processID;
ret = OsForkInitPCB(flags, child, name, sp, size);//初始化进程控制块
if (ret != LOS_OK) {
goto ERROR_INIT;
}
ret = OsCopyProcessResources(flags, child, run);//拷贝进程的资源,包括虚拟空间,文件,安全,IPC ==
if (ret != LOS_OK) {
goto ERROR_TASK;
}
ret = OsChildSetProcessGroupAndSched(child, run);//设置进程组和加入进程调度就绪队列
if (ret != LOS_OK) {
goto ERROR_TASK;
}
LOS_MpSchedule(OS_MP_CPU_ALL);//给各CPU发送准备接受调度信号
if (OS_SCHEDULER_ACTIVE) {//当前CPU core处于活动状态
LOS_Schedule();// 申请调度
}
return processID;
ERROR_TASK:
SCHEDULER_LOCK(intSave);
(VOID)OsTaskDeleteUnsafe(OS_TCB_FROM_TID(child->threadGroupID), OS_PRO_EXIT_OK, intSave);
ERROR_INIT:
OsDeInitPCB(child);
return -ret;
}
### OsForkInitPCB
STATIC UINT32 (UINT32 flags, LosProcessCB *child, const CHAR *name, UINTPTR sp, UINT32 size)
{
UINT32 ret;
LosProcessCB *run = OsCurrProcessGet();//获取当前进程
ret = OsInitPCB(child, run->processMode, OS_PROCESS_PRIORITY_LOWEST, LOS_SCHED_RR, name);//初始化PCB信息,进程模式,优先级,调度方式,名称 == 信息
if (ret != LOS_OK) {
return ret;
}
ret = OsCopyParent(flags, child, run);//拷贝父亲大人的基因信息
if (ret != LOS_OK) {
return ret;
}
return OsCopyTask(flags, child, name, sp, size);//拷贝任务,设置任务入口函数,栈大小
}
//初始化PCB块
STATIC UINT32 OsInitPCB(LosProcessCB *processCB, UINT32 mode, UINT16 priority, UINT16 policy, const CHAR *name)
{
UINT32 count;
LosVmSpace *space = NULL;
LosVmPage *vmPage = NULL;
status_t status;
BOOL retVal = FALSE;
processCB->processMode = mode; //用户态进程还是内核态进程
processCB->processStatus = OS_PROCESS_STATUS_INIT; //进程初始状态
processCB->parentProcessID = OS_INVALID_VALUE; //爸爸进程,外面指定
processCB->threadGroupID = OS_INVALID_VALUE; //所属线程组
processCB->priority = priority; //进程优先级
processCB->policy = policy; //调度算法 LOS_SCHED_RR
processCB->umask = OS_PROCESS_DEFAULT_UMASK; //掩码
processCB->timerID = (timer_t)(UINTPTR)MAX_INVALID_TIMER_VID;
LOS_ListInit(&processCB->threadSiblingList);//初始化孩子任务/线程链表,上面挂的都是由此fork的孩子线程 见于 OsTaskCBInit LOS_ListTailInsert(&(processCB->threadSiblingList), &(taskCB->threadList));
LOS_ListInit(&processCB->childrenList); //初始化孩子进程链表,上面挂的都是由此fork的孩子进程 见于 OsCopyParent LOS_ListTailInsert(&parentProcessCB->childrenList, &childProcessCB->siblingList);
LOS_ListInit(&processCB->exitChildList); //初始化记录退出孩子进程链表,上面挂的是哪些exit 见于 OsProcessNaturalExit LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);
LOS_ListInit(&(processCB->waitList)); //初始化等待任务链表 上面挂的是处于等待的 见于 OsWaitInsertWaitLIstInOrder LOS_ListHeadInsert(&processCB->waitList, &runTask->pendList);
for (count = 0; count < OS_PRIORITY_QUEUE_NUM; ++count) { //根据 priority数 创建对应个数的队列
LOS_ListInit(&processCB->threadPriQueueList[count]); //初始化一个个线程队列,队列中存放就绪状态的线程/task
}//在鸿蒙内核中 task就是thread,在鸿蒙源码分析系列篇中有详细阐释 见于 https://my.oschina.net/u/3751245
if (OsProcessIsUserMode(processCB)) {// 是否为用户模式进程
space = LOS_MemAlloc(m_aucSysMem0, sizeof(LosVmSpace));//分配一个虚拟空间
if (space == NULL) {
PRINT_ERR("%s %d, alloc space failed\n", __FUNCTION__, __LINE__);
return LOS_ENOMEM;
}
VADDR_T *ttb = LOS_PhysPagesAllocContiguous(1);//分配一个物理页用于存储L1页表 4G虚拟内存分成 (4096*1M)
if (ttb == NULL) {//这里直接获取物理页ttb
PRINT_ERR("%s %d, alloc ttb or space failed\n", __FUNCTION__, __LINE__);
(VOID)LOS_MemFree(m_aucSysMem0, space);
return LOS_ENOMEM;
}
(VOID)memset_s(ttb, PAGE_SIZE, 0, PAGE_SIZE);//内存清0
retVal = OsUserVmSpaceInit(space, ttb);//初始化虚拟空间和进程mmu
vmPage = OsVmVaddrToPage(ttb);//通过虚拟地址拿到page
if ((retVal == FALSE) || (vmPage == NULL)) {//异常处理
PRINT_ERR("create space failed! ret: %d, vmPage: %#x\n", retVal, vmPage);
processCB->processStatus = OS_PROCESS_FLAG_UNUSED;//进程未使用,干净
(VOID)LOS_MemFree(m_aucSysMem0, space);//释放虚拟空间
LOS_PhysPagesFreeContiguous(ttb, 1);//释放物理页,4K
return LOS_EAGAIN;
}
processCB->vmSpace = space;//设为进程虚拟空间
LOS_ListAdd(&processCB->vmSpace->archMmu.ptList, &(vmPage->node));//将空间映射页表挂在 空间的mmu L1页表, L1为表头
} else {
processCB->vmSpace = LOS_GetKVmSpace();//内核共用一个虚拟空间,内核进程 常驻内存
}
#ifdef LOSCFG_SECURITY_VID
status = VidMapListInit(processCB);
if (status != LOS_OK) {
PRINT_ERR("VidMapListInit failed!\n");
return LOS_ENOMEM;
}
#endif
#ifdef LOSCFG_SECURITY_CAPABILITY
OsInitCapability(processCB);
#endif
if (OsSetProcessName(processCB, name) != LOS_OK) {
return LOS_ENOMEM;
}
return LOS_OK;
}
//拷贝一个Task过程
STATIC UINT32 OsCopyTask(UINT32 flags, LosProcessCB *childProcessCB, const CHAR *name, UINTPTR entry, UINT32 size)
{
LosTaskCB *childTaskCB = NULL;
TSK_INIT_PARAM_S childPara = { 0 };
UINT32 ret;
UINT32 intSave;
UINT32 taskID;
OsInitCopyTaskParam(childProcessCB, name, entry, size, &childPara);//初始化Task参数
ret = LOS_TaskCreateOnly(&taskID, &childPara);//只创建任务,不调度
if (ret != LOS_OK) {
if (ret == LOS_ERRNO_TSK_TCB_UNAVAILABLE) {
return LOS_EAGAIN;
}
return LOS_ENOMEM;
}
childTaskCB = OS_TCB_FROM_TID(taskID);//通过taskId获取task实体
childTaskCB->taskStatus = OsCurrTaskGet()->taskStatus;//任务状态先同步,注意这里是赋值操作. ...01101001
if (childTaskCB->taskStatus & OS_TASK_STATUS_RUNNING) {//因只能有一个运行的task,所以如果一样要改4号位
childTaskCB->taskStatus &= ~OS_TASK_STATUS_RUNNING;//将四号位清0 ,变成 ...01100001
} else {//非运行状态下会发生什么?
if (OS_SCHEDULER_ACTIVE) {//克隆线程发生错误未运行
LOS_Panic("Clone thread status not running error status: 0x%x\n", childTaskCB->taskStatus);
}
childTaskCB->taskStatus &= ~OS_TASK_STATUS_UNUSED;//干净的Task
childProcessCB->priority = OS_PROCESS_PRIORITY_LOWEST;//进程设为最低优先级
}
if (OsProcessIsUserMode(childProcessCB)) {//是否是用户进程
SCHEDULER_LOCK(intSave);
OsUserCloneParentStack(childTaskCB, OsCurrTaskGet());//拷贝当前任务上下文给新的任务
SCHEDULER_UNLOCK(intSave);
}
OS_TASK_PRI_QUEUE_ENQUEUE(childProcessCB, childTaskCB);//将task加入子进程的就绪队列
childTaskCB->taskStatus |= OS_TASK_STATUS_READY;//任务状态贴上就绪标签
return LOS_OK;
}
//把父任务上下文克隆给子任务
LITE_OS_SEC_TEXT VOID OsUserCloneParentStack(LosTaskCB *childTaskCB, LosTaskCB *parentTaskCB)
{
TaskContext *context = (TaskContext *)childTaskCB->stackPointer;
VOID *cloneStack = (VOID *)(((UINTPTR)parentTaskCB->topOfStack + parentTaskCB->stackSize) - sizeof(TaskContext));
//cloneStack指向 TaskContext
LOS_ASSERT(parentTaskCB->taskStatus & OS_TASK_STATUS_RUNNING);//当前任务一定是正在运行的task
(VOID)memcpy_s(childTaskCB->stackPointer, sizeof(TaskContext), cloneStack, sizeof(TaskContext));//直接把任务上下文拷贝了一份
context->R[0] = 0;//R0寄存器为0,这个很重要, pid = fork() pid == 0 是子进程返回.
}
解读
- 系统调用是通过
CLONE_SIGHAND
的方式创建子进程的.具体有哪些创建方式如下:#define CLONE_VM 0x00000100 //子进程与父进程运行于相同的内存空间
#define CLONE_FS 0x00000200 //子进程与父进程共享相同的文件系统,包括root、当前目录、umask
#define CLONE_FILES 0x00000400 //子进程与父进程共享相同的文件描述符(file descriptor)表
#define CLONE_SIGHAND 0x00000800 //子进程与父进程共享相同的信号处理(signal handler)表
#define CLONE_PTRACE 0x00002000 //若父进程被trace,子进程也被trace
#define CLONE_VFORK 0x00004000 //父进程被挂起,直至子进程释放虚拟内存资源
#define CLONE_PARENT 0x00008000 //创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
#define CLONE_THREAD 0x00010000 //Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
此处不展开细说,进程之间发送信号用于异步通讯,系列篇有专门的篇幅说信号(signal),请自行翻看.
- 可以看出fork的主体函数是
OsCopyProcess
,先申请一个干净的PCB,相当于申请一个容器装资源. - 初始化这个容器
OsForkInitPCB
,OsInitPCB
先把容器打扫干净,虚拟空间,地址映射表(L1表),各种链表初始化好,为接下来的内容拷贝做好准备. OsCopyParent
把家族基因/关系传递给子进程,谁是你的老祖宗,你的七大姑八大姨是谁都得告诉你知道,这些都将挂到你已经初始化好的链表上.OsCopyTask
这个很重要,拷贝父进程当前执行的任务数据给子进程的新任务,系列篇中已经说过,真正让CPU干活的是任务(线程),所以子进程需要创建一个新任务LOS_TaskCreateOnly
来接受当前任务的数据,这个数据包括栈的数据,运行代码段指向,OsUserCloneParentStack
将用户态的上下文数据TaskContext
拷贝到子进程新任务的栈底位置, 也就是说新任务运行栈中此时只有上下文的数据.而且有最最最重要的一句代码context->R[0] = 0;
强制性的将未来恢复上下文R0
寄存器的数据改成了0, 这意味着调度算法切到子进程的任务后, 任务干的第一件事是恢复上下文,届时R0
寄存器的值变成0,而R0=0
意味着什么? 同时LR/SP
寄存器的值也和父进程的一样.这又意味着什么?- 系列篇寄存器篇中以说过返回值就是存在R0寄存器中,
A()->B()
,A拿B的返回值只认R0
的数据,读到什么就是什么返回值,而R0寄存器值等于0,等同于获得返回值为0, 而LR寄存器所指向的指令是pid=返回值
, sp寄存器记录了栈中的开始计算的位置,如此完全还原了父进程调用fork()
前的运行场景,唯一的区别是改变了R0
寄存器的值,所以才有了pid = 0;//fork()的返回值,注意子进程并没有执行fork(),它只是通过恢复上下文获得了一个返回值.
if (pid == 0) {
message = "This is the child\n";
n = 6;
}
由此确保了这是子进程的返回.这是
fork()
最精彩的部分.一定要好好理解.OsCopyTask``OsUserCloneParentStack
的代码细节.会让你醍醐灌顶,永生难忘. - 父进程的返回是
processID = child->processID;
是子进程的ID,任何子进程的ID是不可能等于0的,成功了只能是大于0. 失败了就是负数return -ret;
OsCopyProcessResources
用于赋值各种资源,包括拷贝虚拟空间内存,拷贝打开的文件列表,IPC等等.OsChildSetProcessGroupAndSched
设置子进程组和调度的准备工作,加入调度队列,准备调度.LOS_MpSchedule
是个核间中断,给所有CPU发送调度信号,让所有CPU发生一次调度.由此父进程让出CPU使用权,因为子进程的调度优先级和父进程是平级,而同级情况下子进程的任务已经插到就绪队列的头部位置OS_PROCESS_PRI_QUEUE_ENQUEUE
排在了父进程任务的前面,所以在没有比他们更高优先级的进程和任务出现之前,下一次被调度到的任务就是子进程的任务.也就是在本篇开头看到的$ ./a.out
This is the child
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
- 以上为fork在鸿蒙内核的整个实现过程,务必结合系列篇其他篇理解,一次理解透彻,终生不忘.
鸿蒙内核源码分析.总目录
v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o
百万汉字注解.百篇博客分析
百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee| github| csdn| coding >
百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< 51cto| csdn| harmony| osc >
关注不迷路.代码即人生
QQ群:790015635 | 入群密码: 666
原创不易,欢迎转载,但请注明出处.
鸿蒙内核源码分析(fork篇) | 一次调用,两次返回 | 百篇博客分析OpenHarmony源码 | v45.03的更多相关文章
- 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 | 百篇博客分析OpenHarmony源码 | v71.01
子曰:"我非生而知之者,好古,敏以求之者也." <论语>:述而篇 百篇博客系列篇.本篇为: v71.xx 鸿蒙内核源码分析(Shell编辑篇) | 两个任务,三个阶段 ...
- 鸿蒙内核源码分析(管道文件篇) | 如何降低数据流动成本 | 百篇博客分析OpenHarmony源码 | v70.01
百篇博客系列篇.本篇为: v70.xx 鸿蒙内核源码分析(管道文件篇) | 如何降低数据流动成本 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...
- 鸿蒙内核源码分析(文件句柄篇) | 深挖应用操作文件的细节 | 百篇博客分析OpenHarmony源码 | v69.01
百篇博客系列篇.本篇为: v69.xx 鸿蒙内核源码分析(文件句柄篇) | 深挖应用操作文件的细节 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说 ...
- 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 百篇博客分析OpenHarmony源码 | v49.04
百篇博客系列篇.本篇为: v49.xx 鸿蒙内核源码分析(信号消费篇) | 谁让CPU连续四次换栈运行 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁 ...
- 鸿蒙内核源码分析(信号生产篇) | 信号安装和发送过程是怎样的? | 百篇博客分析OpenHarmony源码 | v48.03
百篇博客系列篇.本篇为: v48.xx 鸿蒙内核源码分析(信号生产篇) | 年过半百,依然活力十足 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管 ...
- 鸿蒙内核源码分析(进程回收篇) | 老父亲如何向老祖宗临终托孤 ? | 百篇博客分析OpenHarmony源码 | v47.01
百篇博客系列篇.本篇为: v47.xx 鸿蒙内核源码分析(进程回收篇) | 临终前如何向老祖宗托孤 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管 ...
- 鸿蒙内核源码分析(特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 | 百篇博客分析OpenHarmony源码 | v46.02
百篇博客系列篇.本篇为: v46.xx 鸿蒙内核源码分析(特殊进程篇) | 龙生龙凤生凤老鼠生儿会打洞 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁 ...
- 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 百篇博客分析OpenHarmony源码 | v24.01
百篇博客系列篇.本篇为: v24.xx 鸿蒙内核源码分析(进程概念篇) | 进程在管理哪些资源 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内 ...
- 鸿蒙内核源码分析(内存管理篇) | 虚拟内存全景图是怎样的 | 百篇博客分析OpenHarmony源码 | v12.04
百篇博客系列篇.本篇为: v12.xx 鸿蒙内核源码分析(内存管理篇) | 虚拟内存全景图是怎样的 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有 ...
随机推荐
- Spring-Boot-Bean的使用,@Repository,@Service,@Controller,@Component
前言 在Spring MVC的时候,我们使用xml来配置bean,如今的Spring boot推荐我们使用元注解的发生,那就听Spring Boot的推荐,下面我就为大家来介绍下Spring Boot ...
- 刷题-力扣-63. 不同路径 II
63. 不同路径 II 题目链接 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/unique-paths-ii/ 著作权归领扣网络所有.商业转 ...
- 你不知道的echarts,前端鲍哥带你研究!
前言 相信不少前端小伙伴刚接触 e-charts 肯定有点陌生,但是echarts咱不清楚,charts我们应该很熟悉,没错,echarts 就是我们日常可见的图表,不同的是 echarts 是用代码 ...
- LeetCode《买卖股票的最佳时机》系列题目,最详解
目录 说在前面 引例:只能交易一次 一.动态数组定义 二.状态转移方程 三.初始化 四.优化 无限制买卖 一.动态数组定义 二.状态转移方程 三.初始化 四.优化 交易 2 次,最大利润? 一.动态数 ...
- 用GUI实现java版贪吃蛇小游戏
项目结构 新建一个JFrame窗口,作为程序入口 public class GameStart{ public static void main(String[] args) { JFrame jFr ...
- gimp 缩放图片 python script
滤镜 -> Python Fu -> 控制台,复制粘贴下面代码. 然后 scale_image(800, 800) 图片自动缩放为 800*800 了 这个代码是傻傻的缩放,你可以把它改成 ...
- uboot常用命令及其使用
环境变量设置 setenv 设置一个环境变量 # 格式:setenv key vlaue setenv bootdelay 5 # 设置uboot启动延时5s 删除一个环境变量 uboot对于一个没有 ...
- K8S的核心概念
1.Pod -- 是最小部署单元 -- 是一组容器的集合 -- Pod中的容器共享网络 -- 生命周期是短暂的 2.controller -- 确保预期的pod副本的数量 -- 确保所有的node运行 ...
- Python习题集(九)
每天一习题,提升Python不是问题!!有更简洁的写法请评论告知我! https://www.cnblogs.com/poloyy/category/1676599.html 题目 已知一个数列:1. ...
- Flask(2)- 第一个 Flask Application
安装 flask Flask 是一个 Web 框架,使用它首先需要安装 pip3 install flask 导入 Flask 模块 import flask 最简单的一个栗子 主代码 from fl ...