鸿蒙内核源码分析(Fork篇) | 一次调用,两次返回

news2025/5/11 9:53:38

笔者第一次看到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,相当于申请一个容器装资源.
  • 初始化这个容器OsForkInitPCBOsInitPCB 先把容器打扫干净,虚拟空间,地址映射表(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在鸿蒙内核的整个实现过程,务必结合系列篇其他篇理解,一次理解透彻,终生不忘.

经常有很多小伙伴抱怨说:不知道学习鸿蒙开发哪些技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、WebGL、元服务、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植等等)鸿蒙(HarmonyOS NEXT)技术知识点。

在这里插入图片描述

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

在这里插入图片描述

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):https://gitcode.com/HarmonyOS_MN

在这里插入图片描述

OpenHarmony 开发环境搭建

图片

《OpenHarmony源码解析》:https://gitcode.com/HarmonyOS_MN

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN

图片

写在最后

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙

  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2056726.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux memory cgroup的memory.move_charge_at_immigrate含义

1.内核文档 上面的例子说明&#xff1a; 最开始某个进程是在cgroup A中&#xff0c;后面要迁移到cgroup B中&#xff0c;那么进程的内存计数是否要完全迁入B中&#xff0c;就是通过memory.move_charge_at_immigrate控制&#xff0c;如果目标cgroup也就是B设置了1到该字段中&am…

猫头虎 分享:什么是Thrift?Thrift的简介、安装、用法详解入门教程

猫头虎 分享&#xff1a;什么是Thrift&#xff1f;Thrift的简介、安装、用法详解入门教程 今天猫头虎带大家一起探讨 一个在人工智能和分布式系统开发中十分重要的工具——Thrift。无论你是AI开发者还是大数据工程师&#xff0c;了解和掌握Thrift都将极大地提高你的跨语言服务…

高性能web服务器

目录 一、简介 &#xff08;一&#xff09;nginx-高性能的web服务端 &#xff08;二&#xff09;用户访问体验 二、I/O模型 &#xff08;一&#xff09;概念 &#xff08;二&#xff09;网络I/O模型 &#xff08;三&#xff09;阻塞型 I/O 模型 &#xff08;四&#xf…

AI系列-黑神话:悟空

今天的朋友圈被黑神话悟空刷圈了&#xff0c;喝的咖啡都是黑神话联名版本。四年磨一剑的本地游戏&#xff0c;身边也不少小伙伴用金钱支持了&#xff0c;属于现象级的游戏产品。游戏引擎后续是否可以结合AI文生图&#xff0c;小说编写、文生视频。加快大作的快速生成 &#xff…

Effective-Java-Chapter8-方法

https://github.com/clxering/Effective-Java-3rd-edition-Chinese-English-bilingual/blob/dev/Chapter-8/Chapter-8-Introduction.md 准则一 检查参数的有效性 首先对于方法要写详细的文档&#xff0c;例如参数要求&#xff0c;抛出什么异常以源码为例&#xff1a; 又比如…

分享一个基于微信小程序的反诈科普平台springboot(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

Web开发:ORM框架之Freesql的入门和技巧使用小结

目录 零、官网链接 一、字段映射表 二、查询 1.freesql独特封装&#xff1a;between关键字 2.分页&#xff08;每页 20 条数据&#xff0c;查询第 1 页&#xff09; 3.Withsql&#xff08;子查询&#xff0c;不建议&#xff09; 3.简单查询、映射查询 4.参数查询、自定义…

RK3568笔记五十六:yolov8_obb旋转框训练部署

若该文为原创文章,转载请注明原文出处。 本文基于rknn_model_zoo和山水无移大佬的博客和代码训练模型并部署到正点原子的ATK-DLRK3568板子测试。 https://github.com/ultralytics/ultralytics 一、训练 1、环境搭建 使用的是AUTODL环境,yolov8-obb数据集不大,也可以使用c…

NIO中的异步—ChannelFuture、CloseFuture以及异步提升在NIO中的应用

ChannelFuture 客户端调用connect后返回值为ChannelFuture对象&#xff0c;我们可以利用ChannelFuture中的channel()方法获取到Channel对象。 由于上述代为为客户端实现&#xff0c;若想启动客户端实现连接操作&#xff0c;必须编写服务端代码&#xff0c;实现如下&#xff1a;…

TCP协议为什么是三次握手和四次挥手

1.一次握手&&二次握手 一次握手就能成功的话&#xff0c;也就代表着不需要进行确认&#xff0c;那么万一有恶意的服务器一直发送SYN&#xff0c;而服务器需要维护大量的连接&#xff0c;维护连接又需要成本&#xff0c;那么就很容易引发SYN洪水&#xff0c;导致服务器…

Linux中的exec族函数

exec 系列函数用于替换当前进程的用户空间代码和数据&#xff0c;从而执行一个新的程序。调用 exec 系列函数不会创建新的进程&#xff0c;但会用新程序的代码和数据替换当前进程&#xff0c;因此调用 exec 后&#xff0c;进程的 ID 保持不变&#xff0c;但进程的行为变为执行新…

计算机毕业设计 教师科研管理系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

第1章-05-通过浏览器控制台安装JQuery.js库

🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌,CSDN博客专家,阿里云社区专家博主,2023年CSDN全站百大博主。 🏆数年电商行业从业经验,历任核心研发工程师,项目技术负责人。 🏆本文已收录于专栏:Web爬虫入门与实战精讲。 🎉欢迎 👍点赞✍评论⭐收…

大数据背景下基于Python语言的单车租赁商业数据可视化分析

注&#xff1a;源码在最后&#xff0c;只是一次实验记录,不足之处请指教。 一 研究背景及意义 在大数据时代&#xff0c;商业领域的数据量迅速增长&#xff0c;如何有效地利用这些数据成为企业决策和优化成为重要的研究课题。单车租赁作为一种新兴的共享经济模式&#xff0c;其…

健韵坊(详细项目实战一)Spring系列 + Vue3

这一次来一个项目改造的项目实战&#xff0c;基于很久之前的一个demo项目&#xff0c;来实现一个改造优化和部署上线的项目实战。&#xff08;就当是接手*山项目并且加以改造的一个实战吧。&#xff09; 之前是一个关于运动的一个项目&#xff08;其实之前连名字都没想好hhhh&…

vue3 响应式 API:watch()、watchEffect()

watch() 基本概念 watch()用于监视响应式数据的变化&#xff0c;并在数据变化时执行相应的回调函数。可以监视单个响应式数据、多个响应式数据的组合&#xff0c;或者一个计算属性。 返回值 返回一个函数&#xff0c;调用这个函数可以停止监视。 特点 watch() 默认是懒侦听的&…

【Linux网络】select函数

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 select函数介绍select函数参数介绍select函数返回值select的工作流程TCP服务器【多路复用版】 select函数介绍 在Linux网络编程中&#xff0c;select 函数是一种非常有用的IO多路复用技术&#xff0…

秃姐学AI系列之:LeNet + 代码实现

目录 LeNet MNIST数据集 LeNet模型图 ​编辑 总结 代码实现&#xff1a;卷积神经网络 LeNet LeNet&#xff08;LeNet-5&#xff09;由两个部分组成&#xff1a;卷积编码器核全连接层密集块 检查模型 LeNet 卷积神经网络里面最为著名的一个网络&#xff0c;80年代末提出…

【vue教程】七. Vue 的动画和过渡

文章目录 往期列表回顾本章涵盖知识点Vue 的内置动画系统基本的进入和离开过渡列表过渡 CSS 过渡CSS 过渡基础Vue 中的 CSS 过渡 JavaScript 动画使用 JavaScript 钩子 第三方动画库的使用集成 Animate.css 实例演示创建一个简单的动画应用 结语 往期列表 【vue教程】一. 环境…

iOS18升级出现白苹果、无法重启等问题,要怎么解决

随着苹果iOS 18系统beta版本的推出&#xff0c;不少用户在私信说升级后遇到了白苹果和无法重启等问题。这些问题不仅影响了大家的正常使用&#xff0c;还会导致数据丢失和系统崩溃。本文将详细介绍iOS 18升级后出现白苹果、无法重启等问题的原因及解决方法&#xff0c;帮助大家…