鸿蒙内核源码分析(信号生产篇) | 注意结构体的名字和作用.

news2024/11/25 12:26:04

信号生产

关于信号篇,本只想写一篇,但发现把它想简单了,内容不多,难度极大.整理了好长时间,理解了为何<<深入理解linux内核>>要单独为它开一章,原因有二

  • 信号相关的结构体多,而且还容易搞混.所以看本篇要注意结构体的名字和作用.
  • 系统调用太多了,涉及面广,信号的来源分硬件和软件.相当于软中断和硬中断,这就会涉及到汇编代码,但信号的处理函数又在用户空间,CPU是禁止内核态执行用户态代码的,所以运行过程需在用户空间和内核空间来回的折腾,频繁的切换上下文.

信号思想来自Unix,它老人家已经五十多岁了,但很有活力,许多方面几乎没发生大的变化.信号可以由内核产生,也可以由用户进程产生,并由内核传送给特定的进程或线程(组),若这个进程定义了自己的信号处理程序,则调用这个程序去处理信号,否则则执行默认的程序或者忽略.

信号为系统提供了一种进程间异步通讯的方式,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不可能知道信号到底什么时候到达。一般来说,只需用户进程提供信号处理函数,内核会想方设法调用信号处理函数,网上查阅了很多的关于信号的资料.个人想换个视角去看信号.把异步过程理解为生产者(安装和发送信号)和消费者(捕捉和处理信号)两个过程

信号分类

每个信号都有一个名字和编号,这些名字都以SIG开头,例如SIGQUITSIGCHLD等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。不过kill对于信号0有特殊的应用。啥用呢? 可用来查询进程是否还在. 敲下 kill 0 pid 就知道了.

信号分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号。

  • 不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31;

  • 可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64

  #define SIGHUP    1	//终端挂起或者控制进程终止
  #define SIGINT    2	//键盘中断(ctrl + c)
  #define SIGQUIT   3	//键盘的退出键被按下
  #define SIGILL    4	//非法指令
  #define SIGTRAP   5	//跟踪陷阱(trace trap),启动进程,跟踪代码的执行
  #define SIGABRT   6	//由abort(3)发出的退出指令
  #define SIGIOT    SIGABRT //abort发出的信号
  #define SIGBUS    7	//总线错误 
  #define SIGFPE    8	//浮点异常
  #define SIGKILL   9	//常用的命令 kill 9 123 | 不能被忽略、处理和阻塞
  #define SIGUSR1   10	//用户自定义信号1 
  #define SIGSEGV   11	//无效的内存引用, 段违例(segmentation     violation),进程试图去访问其虚地址空间以外的位置 
  #define SIGUSR2   12	//用户自定义信号2
  #define SIGPIPE   13	//向某个非读管道中写入数据 
  #define SIGALRM   14	//由alarm(2)发出的信号,默认行为为进程终止
  #define SIGTERM   15	//终止信号
  #define SIGSTKFLT 16	//栈溢出
  #define SIGCHLD   17	//子进程结束信号
  #define SIGCONT   18	//进程继续(曾被停止的进程)
  #define SIGSTOP   19	//终止进程  	 | 不能被忽略、处理和阻塞
  #define SIGTSTP   20	//控制终端(tty)上 按下停止键
  #define SIGTTIN   21	//进程停止,后台进程企图从控制终端读
  #define SIGTTOU   22	//进程停止,后台进程企图从控制终端写
  #define SIGURG    23	//I/O有紧急数据到达当前进程
  #define SIGXCPU   24	//进程的CPU时间片到期
  #define SIGXFSZ   25	//文件大小的超出上限
  #define SIGVTALRM 26	//虚拟时钟超时
  #define SIGPROF   27	//profile时钟超时
  #define SIGWINCH  28	//窗口大小改变
  #define SIGIO     29	//I/O相关
  #define SIGPOLL   29	//
  #define SIGPWR    30	//电源故障,关机
  #define SIGSYS    31	//系统调用中参数错,如系统调用号非法 
  #define SIGUNUSED SIGSYS//不使用

  #define _NSIG 65

信号来源

信号来源分为硬件类和软件类:

  • 硬件类
    • 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号;
    • 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程;
  • 软件类
    • 通过系统调用,发送signal信号:kill()raise()sigqueue()alarm()setitimer()abort()
      • kill 命令就是一个发送信号的工具,用于向进程或进程组发送信号.例如: kill 9 PID (SIGKILL)来杀死PID进程.
      • sigqueue():只能向一个进程发送信号,不能向进程组发送信号;主要针对实时信号提出,与sigaction()组合使用,当然也支持非实时信号的发送;
      • alarm():用于调用进程指定时间后发出SIGALARM信号;
      • setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;
      • abort():向进程发送SIGABORT信号,默认进程会异常退出。
      • raise():用于向进程自身发送信号;

信号与进程的关系

主要是通过系统调用 sigaction将用户态信号处理函数注册到PCB保存.所有进程的任务都共用这个信号注册函数sigHandler,在信号的消费阶段内核用一种特殊的方式’回调’它.

typedef struct ProcessCB {//PCB中关于信号的信息
    UINTPTR              sigHandler;   /**< signal handler */   //捕捉信号后的处理函数
    sigset_t             sigShare;     /**< signal share bit */	//信号共享位,64个信号各站一位
}LosProcessCB;
typedef unsigned _Int64 sigset_t; //一个64位的变量,每个信号代表一位.

struct sigaction {//信号处理机制结构体
	union {
		void (*sa_handler)(int); //信号处理函数——普通版
		void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数——高级版
	} __sa_handler;
	sigset_t sa_mask;//指定信号处理程序执行过程中需要阻塞的信号;
	int sa_flags;	 //标示位
					 //	SA_RESTART:使被信号打断的syscall重新发起。
					 //	SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
					 //	SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵 尸进程。
					 //	SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
					 //	SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
					 //	SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。
	void (*sa_restorer)(void);
};
typedef struct sigaction sigaction_t;

解读

  • 每个信号都对应一个位. 信号从1开始编号 [1 ~ 64] 对应 sigShare的[0 ~ 63]位,所以中间会差一个.记住这点,后续代码会提到.
  • sigHandler信号处理函数的注册过程,由系统调用sigaction(用户空间) -> OsSigAction(内核空间)完成绑定动作.
    #include <signal.h>
    int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
    int OsSigAction(int sig, const sigaction_t *act, sigaction_t *oact)
    {
        UINTPTR addr;
        sigaction_t action;

        if (!GOOD_SIGNO(sig) || sig < 1 || act == NULL) {
            return -EINVAL;
        }
        //将数据从用户空间拷贝到内核空间
        if (LOS_ArchCopyFromUser(&action, act, sizeof(sigaction_t)) != LOS_OK) {
            return -EFAULT;
        }

        if (sig == SIGSYS) {//鸿蒙此处通过错误的系统调用 来安装信号处理函数,有点巧妙. 
            addr = OsGetSigHandler();//获取进程信号处理函数
            if (addr == 0) {//进程没有设置信号处理函数时
                OsSetSigHandler((unsigned long)(UINTPTR)action.sa_handler);//设置进程信号处理函数——普通版
                return LOS_OK;
            }
            return -EINVAL;
        }

        return LOS_OK;
    }
*   `sigaction(...)`第一个参数是要安装的信号; 第二个参数与sigaction函数同名的结构体,这里会让人很懵,函数名和结构体一直,没明白为毛要这么搞? 结构体内定义了信号处理方法;第三个为输出参数,将信号的当前的sigaction结构带回.但鸿蒙显然没有认真对待第三个参数.把`musl`实现给阉割了.
*   对结构体的`sigaction`鸿蒙目前只支持信号处理函数——普通版,`sa_handler`表示自定义信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。
*   `sa_mask`指定信号处理程序执行过程中需要阻塞的信号。
*   `sa_flags`字段包含一些选项,具体看注释
*   `sa_sigaction`是实时信号的处理函数,`union`二选一.鸿蒙暂时不支持这种方式.

信号与任务的关系

typedef struct {//TCB中关于信号的信息
    sig_cb          sig;	//信号控制块,用于异步通信,类似于 linux singal模块
} LosTaskCB;
typedef struct {//信号控制块(描述符)
    sigset_t sigFlag;		//不屏蔽的信号标签集
    sigset_t sigPendFlag;	//信号阻塞标签集,记录因哪些信号被阻塞
    sigset_t sigprocmask; /* Signals that are blocked            */	//进程屏蔽了哪些信号
    sq_queue_t sigactionq;	//信号捕捉队列					
    LOS_DL_LIST waitList;	//等待链表,上面挂的是等待信号到来的任务, 可查找 OsTaskWait(&sigcb->waitList, timeout, TRUE)	理解						
    sigset_t sigwaitmask; /* Waiting for pending signals         */	//任务在等待阻塞信号
    siginfo_t sigunbinfo; /* Signal info when task unblocked     */	//任务解锁时的信号信息
    sig_switch_context context;	//信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换							
} sig_cb;

解读

  • 系列篇已多次说过,进程只是管理资源的容器,真正让cpu干活的是任务task,所以发给进程的信号最终还是需要分发给具体任务来处理.所以能想到的是关于任务部分会更复杂.
  • context信号处理很复杂的原因在于信号的发起在用户空间,发送需要系统调用,而处理信号的函数又是用户空间提供的, 所以需要反复的切换任务上下文.而且还有硬中断的问题,比如 ctrl + c ,需要从硬中断中回调用户空间的信号处理函数,处理完了再回到内核空间,最后回到用户空间.没听懂吧,我自己都说晕了,所以需要专门的一篇来说清楚信号的处理问题.本篇不展开说.
  • sig_cb结构体是任务处理信号的结构体,要响应,屏蔽哪些信号等等都由它完成,这个结构体虽不复杂,但是很绕,很难搞清楚它们之间的区别.笔者是经过一番痛苦的阅读理解后才明白各自的含义.并想通过用打比方的例子试图让大家明白.
  • 以下用追女孩打比方理解.任务相当于某个男,没错说的就是屏幕前的你,除了苦逼的码农谁会有耐心能坚持看到这里.64个信号对应64个女孩.允许一男同时追多个女孩,女孩也可同时被多个男追.女孩也可以主动追男的.理解如下:
  • waitList等待信号的任务链表,上面挂的是因等待信号而被阻塞的任务.众男在排队追各自心爱的女孩们,处于无所事事的挂起的状态,等待女孩们的出现.
  • sigwaitmask任务在等待的信号集合,只有这些信号能唤醒任务.相当于列出喜欢的各位女孩,只要出现一位就能让你满血复活.
  • sigprocmask指任务对哪些信号不感冒.来了也不处理.相当于列出不喜欢的各位女孩,请她们别来骚扰你,嘚瑟.
  • sigPendFlag信号到达但并未唤醒任务.相当于喜欢你的女孩来追你,但她不在你喜欢的列表内,结果是不搭理人家继续等喜欢的出现.
  • sigFlag记录不屏蔽的信号集合,相当于你并不反感的女孩们.记录来过的那些女孩(除掉你不喜欢的).

信号发送过程

用户进程调用kill()的过程如下:

kill(pid_t pid, int sig) - 系统调用   
|                    用户空间
---------------------------------------------------------------------------------------
|                    内核空间
SysKill(...)
|---> OsKillLock(...)
    |---> OsKill(.., OS_USER_KILL_PERMISSION)
        |---> OsDispatch()  //鉴权,向进程发送信号
            |---> OsSigProcessSend()    //选择任务发送信号
                |---> OsSigProcessForeachChild(..,ForEachTaskCB handler,..)
                    |---> SigProcessKillSigHandler() //处理 SIGKILL
                        |---> OsTaskWake() //唤醒所有等待任务
                        |---> OsSigEmptySet() //清空信号等待集
                    |---> SigProcessSignalHandler()
                        |---> OsTcbDispatch() //向目标任务发送信号
                            |---> OsTaskWake() //唤醒任务
                            |---> OsSigEmptySet() //清空信号等待集

流程

  • 通过 系统调用 kill 陷入内核空间

  • 因为是用户态进程,使用OS_USER_KILL_PERMISSION权限发送信号

    #define OS_KERNEL_KILL_PERMISSION 0U	//内核态 kill 权限
    #define OS_USER_KILL_PERMISSION   3U	//用户态 kill 权限
    
    
  • 鉴权之后进程轮询任务组,向目标任务发送信号.这里分三种情况:

    • SIGKILL信号,将所有等待任务唤醒,拉入就绪队列等待被调度执行,并情况信号等待集
    • SIGKILL信号时,将通过sigwaitmasksigprocmask过滤,找到一个任务向它发送信号OsTcbDispatch.

代码细节

    int OsKill(pid_t pid, int sig, int permission)
    {
        siginfo_t info;
        int ret;

        /* Make sure that the para is valid */
        if (!GOOD_SIGNO(sig) || pid < 0) {//有效信号 [0,64]
            return -EINVAL;
        }
        if (OsProcessIDUserCheckInvalid(pid)) {//检查参数进程 
            return -ESRCH;
        }

        /* Create the siginfo structure */ //创建信号结构体
        info.si_signo = sig;	//信号编号
        info.si_code = SI_USER;	//来自用户进程信号
        info.si_value.sival_ptr = NULL;

        /* Send the signal */
        ret = OsDispatch(pid, &info, permission);//发送信号
        return ret;
    }

    //信号分发
    int OsDispatch(pid_t pid, siginfo_t *info, int permission)
    {
        LosProcessCB *spcb = OS_PCB_FROM_PID(pid);//找到这个进程
        if (OsProcessIsUnused(spcb)) {//进程是否还在使用,不一定是当前进程但必须是个有效进程
            return -ESRCH;
        }
    #ifdef LOSCFG_SECURITY_CAPABILITY	//启用能力安全模式
        LosProcessCB *current = OsCurrProcessGet();//获取当前进程

        /* If the process you want to kill had been inactive, but still exist. should return LOS_OK */
        if (OsProcessIsInactive(spcb)) {//如果要终止的进程处于非活动状态,但仍然存在,应该返回OK
            return LOS_OK;
        }

        /* Kernel process always has kill permission and user process should check permission *///内核进程总是有kill权限,用户进程需要检查权限
        if (OsProcessIsUserMode(current) && !(current->processStatus & OS_PROCESS_FLAG_EXIT)) {//用户进程检查能力范围
            if ((current != spcb) && (!IsCapPermit(CAP_KILL)) && (current->user->userID != spcb->user->userID)) {
                return -EPERM;
            }
        }
    #endif
        if ((permission == OS_USER_KILL_PERMISSION) && (OsSignalPermissionToCheck(spcb) < 0)) {
            return -EPERM;
        }
        return OsSigProcessSend(spcb, info);//给参数进程发送信号
    }

    //给参数进程发送参数信号
    int OsSigProcessSend(LosProcessCB *spcb, siginfo_t *sigInfo)
    {
        int ret;
        struct ProcessSignalInfo info = {
            .sigInfo = sigInfo, //信号内容
            .defaultTcb = NULL, //以下四个值将在OsSigProcessForeachChild中根据条件完善
            .unblockedTcb = NULL,
            .awakenedTcb = NULL,
            .receivedTcb = NULL
        };
        //总之是要从进程中找个至少一个任务来接受这个信号,优先级
        //awakenedTcb > receivedTcb > unblockedTcb > defaultTcb
        /* visit all taskcb and dispatch signal */ //访问所有任务和分发信号
        if ((info.sigInfo != NULL) && (info.sigInfo->si_signo == SIGKILL)) {//需要干掉进程时 SIGKILL = 9, #linux kill 9 14
            (void)OsSigProcessForeachChild(spcb, SigProcessKillSigHandler, &info);//进程要被干掉了,通知所有task做善后处理
            OsSigAddSet(&spcb->sigShare, info.sigInfo->si_signo);
            OsWaitSignalToWakeProcess(spcb);//等待信号唤醒进程
            return 0;
        } else {
            ret = OsSigProcessForeachChild(spcb, SigProcessSignalHandler, &info);//进程通知所有task处理信号
        }
        if (ret < 0) {
            return ret;
        }
        SigProcessLoadTcb(&info, sigInfo);
        return 0;
    }
    //让进程的每一个task执行参数函数
    int OsSigProcessForeachChild(LosProcessCB *spcb, ForEachTaskCB handler, void *arg)
    {
        int ret;

        /* Visit the main thread last (if present) */	
        LosTaskCB *taskCB = NULL;//遍历进程的 threadList 链表,里面存放的都是task节点
        LOS_DL_LIST_FOR_EACH_ENTRY(taskCB, &(spcb->threadSiblingList), LosTaskCB, threadList) {//遍历进程的任务列表
            ret = handler(taskCB, arg);//回调参数函数
            OS_RETURN_IF(ret != 0, ret);//这个宏的意思就是只有ret = 0时,啥也不处理.其余就返回 ret
        }
        return LOS_OK;
    }

  • 如果是 SIGKILL信号,让spcb的所有任务执行SigProcessKillSigHandler函数,查看旗下的所有任务是否又在等待这个信号的,如果有就将任务唤醒,放在就绪队列等待被调度执行.
    //进程收到 SIGKILL 信号后,通知任务tcb处理.
    static int SigProcessKillSigHandler(LosTaskCB *tcb, void *arg)
    {
        struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//转参

        if ((tcb != NULL) && (info != NULL) && (info->sigInfo != NULL)) {//进程有信号
            sig_cb *sigcb = &tcb->sig;
            if (!LOS_ListEmpty(&sigcb->waitList) && OsSigIsMember(&sigcb->sigwaitmask, info->sigInfo->si_signo)) {//如果任务在等待这个信号
                OsTaskWake(tcb);//唤醒这个任务,加入进程的就绪队列,并不申请调度
                OsSigEmptySet(&sigcb->sigwaitmask);//清空信号等待位,不等任何信号了.因为这是SIGKILL信号
            }
        }
        return 0;
    }
  • SIGKILL信号,让spcb的所有任务执行SigProcessSignalHandler函数
    static int SigProcessSignalHandler(LosTaskCB *tcb, void *arg)
    {
        struct ProcessSignalInfo *info = (struct ProcessSignalInfo *)arg;//先把参数解出来
        int ret;
        int isMember;

        if (tcb == NULL) {
            return 0;
        }

        /* If the default tcb is not setted, then set this one as default. */
        if (!info->defaultTcb) {//如果没有默认发送方的任务,即默认参数任务.
            info->defaultTcb = tcb;
        }

        isMember = OsSigIsMember(&tcb->sig.sigwaitmask, info->sigInfo->si_signo);//任务是否在等待这个信号
        if (isMember && (!info->awakenedTcb)) {//是在等待,并尚未向该任务时发送信号时
            /* This means the task is waiting for this signal. Stop looking for it and use this tcb.
            * The requirement is: if more than one task in this task group is waiting for the signal,
            * then only one indeterminate task in the group will receive the signal.
            */
            ret = OsTcbDispatch(tcb, info->sigInfo);//发送信号,注意这是给其他任务发送信号,tcb不是当前任务
            OS_RETURN_IF(ret < 0, ret);//这种写法很有意思

            /* set this tcb as awakenedTcb */
            info->awakenedTcb = tcb;
            OS_RETURN_IF(info->receivedTcb != NULL, SIG_STOP_VISIT); /* Stop search */
        }
        /* Is this signal unblocked on this thread? */
        isMember = OsSigIsMember(&tcb->sig.sigprocmask, info->sigInfo->si_signo);//任务是否屏蔽了这个信号
        if ((!isMember) && (!info->receivedTcb) && (tcb != info->awakenedTcb)) {//没有屏蔽,有唤醒任务没有接收任务.
            /* if unblockedTcb of this signal is not setted, then set it. */
            if (!info->unblockedTcb) {
                info->unblockedTcb = tcb;
            }

            ret = OsTcbDispatch(tcb, info->sigInfo);//向任务发送信号
            OS_RETURN_IF(ret < 0, ret);
            /* set this tcb as receivedTcb */
            info->receivedTcb = tcb;//设置这个任务为接收任务
            OS_RETURN_IF(info->awakenedTcb != NULL, SIG_STOP_VISIT); /* Stop search */
        }
        return 0; /* Keep searching */
    }

解读

  • 函数的意思是,当进程中有多个任务在等待这个信号时,发送信号给第一个等待的任务awakenedTcb.
  • 如果没有任务在等待信号,那就从不屏蔽这个信号的任务集中随机找一个receivedTcb接受信号.
  • 只要不屏蔽 unblockedTcb就有值,随机的.
  • 如果上面的都不满足,信号发送给defaultTcb.
  • 寻找发送任务的优先级是 awakenedTcb > receivedTcb > unblockedTcb > defaultTcb

信号相关函数

信号集操作函数

  • sigemptyset(sigset_t *set):信号集全部清0;
  • sigfillset(sigset_t *set): 信号集全部置1,则信号集包含linux支持的64种信号;
  • sigaddset(sigset_t *set, int signum):向信号集中加入signum信号;
  • sigdelset(sigset_t *set, int signum):向信号集中删除signum信号;
  • sigismember(const sigset_t *set, int signum):判定信号signum是否存在信号集中。

信号阻塞函数

  • sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); 不同how参数,实现不同功能
    • SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集;
    • SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除;
    • SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集;
  • sigpending(sigset_t *set)):获取已发送到进程,却被阻塞的所有信号;
  • sigsuspend(const sigset_t *mask)):用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。

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

为了能够帮助到大家能够有规划的学习,这里特别整理了一套纯血版鸿蒙(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/2061412.html

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

相关文章

RTC碰到LXTAL低频晶振停振怎么办?

GD32F303的RTC模块框图如下图所示&#xff0c;RTC时钟源可选择HXTAL/128、LXTAL或IRC40K&#xff0c;一般为了实现更精准的RTC时间&#xff0c;MCU系统均会外挂32.768KHz LXTAL低频晶振&#xff0c;但由于低频晶振负阻抗较大&#xff0c;不容易起振&#xff0c;若外部电路布线、…

vue3 antdv3 去掉Modal的阴影背景,将圆角边框改为直角的显示,看上去不要那么的立体的样式处理。

1、来个没有处理的效果图&#xff1a; 这个有立体的效果&#xff0c;有阴影的效果。 2、要处理一下样式&#xff0c;让这个阴影的效果去掉&#xff1a; 图片的效果不太明显&#xff0c;但是阴影效果确实没有了。 3、代码&#xff1a; /* 去掉遮罩层阴影 */.ant-modal-mask {…

Maven命令传pom或者jar异常

上传命令&#xff1a;mvn deploy:deploy-file -Durlhttp://****&#xff1a;****/repository/chntdrools7741-releases -DrepositoryId**** -DfileD:/tempRepo/org/kie/kie-api-parent/7.69.0.Final/kie-api-parent-7.69.0.Final.pom -DpomFileD:/tempRepo/org/kie/kie-api-par…

三级_网络技术_39_综合题(命令)

一、 如下图所示&#xff0c;某校园网用10Gbps 的POS技术与Internet相连&#xff0c;POS接网的幅格式早SDH。路由协议的选择方案是校园网内部采用OSPF协议&#xff0c;校园网与lntemnet的连接使用静态路由协议。校园网内的路由器R1设为DHCP服务器&#xff0c;可分配的IP地址是…

【22-54】创建者模式(详解五大模式)

目录 一.创建者模式介绍 二.单例设计模式 2.1 单例模式的结构 2.2 单例模式的实现 2.2.1.1 饿汉式-方式1&#xff08;静态变量方式&#xff09; 2.2.1.2 饿汉式-方式2&#xff08;静态代码块方式&#xff09; 2.2.2.1 懒汉式-方式1&#xff08;线程不安全&#xff09; 2…

用手机写一本电子书

第1步、进入Andi.cn网站 第2步、点击登录&#xff0c;注册用户 第3步、点击去创作&#xff0c;进入创作页面 第4步、点击右下角的小笔&#xff0c;写一篇文章 第5步、下翻&#xff0c;点击提交按钮 第6步、再写一篇文章 第7步、点击栏目设计 第8步、进入栏目设计&#xff0c;点…

excel卓越之道笔记

excel快捷键 1.Alt+=一键求和 2.Tab补全函数名称 3.CONCAT可以连选,CONCATENATE只能一个单元格一个单元格点选 4.excel365用不了phonetic函数,但是可以用concat代替 5.textjoin连接标识码,在Arcgis中筛选出所需要素,也是很好用的 6.法1:alt+; 定位可见单元格,复制后只…

Linux入门——01常用命令

0.命令行解释器shell 用户无法直接给操作系统指令&#xff0c;需要经过shell,才能让操作系统明白。如果用户对操作系统非法操作&#xff0c;会有shell保护。shell本身也是一个进程&#xff0c;当然&#xff0c;用户给shell的指令&#xff0c;shell会派生出子进程进行执行&#…

Unity Protobuf3.21.12 GC 问题(反序列化)

背景&#xff1a;Unity接入的是 Google Protobuf 3.21.12 版本&#xff0c;排查下来反序列化过程中的一些GC点&#xff0c;处理了几个严重的&#xff0c;网上也有一些分析&#xff0c;这里就不一一展开&#xff0c;默认读者已经略知一二了。 如果下面有任何问题请评论区留言提…

【Kubernetes中如何对etcd进行备份和还原】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

不同路径

不同路径 思路&#xff1a; 法一&#xff1a;动态规划 const int N 110; class Solution { int dp[N][N];//dp[i][j]&#xff1a;从起点走到 i j的路径个数。 public:int uniquePaths(int m, int n) {for(int i1;i<n;i){dp[1][i]1;} for(int i1;i<m;i) dp[i][1]1;f…

day36.动态规划+重载操作符

动态规划好难啊(ಥ﹏ಥ) 终于搞懂0-1背包问题的二维数组转一维数组优化的问题了。如图所示: 将二维数组转换成一位数组的核心就是&#xff0c;dp[i][j]选取时&#xff0c;他的值只与dp[i-1][j]&#xff0c;也就是上一行有关&#xff0c;所以可以引出使用一维数组代替二维数组…

python 使用宝塔面板在云服务器上搭建 flask

打开宝塔面板到【网站】&#xff0c;选择【python项目】&#xff0c;点【添加python项目】 填上相关信息&#xff1a; 注意&#xff1a;项目端口是你打算在外网用来访问flask的端口号 勾选【放行端口】&#xff0c;并提交 到阿里云里&#xff0c;选择安全组 手动添加放行端口…

datawind可视化查询-其他函数

飞书文档学习链接:https://www.volcengine.com/docs/4726/47275 1. 用户名函数 用户名函数并非 ClickHouse 官方函数,而是与项目用户信息相结合,用于返回当前使用用户的指定信息的函数。 USERNAME()可返回当前用户的用户名,如下所示。该函数也可与其他函数组合使用 2. J…

51 无显式主键时 mysql 增加的 DB_ROW_ID

前言 这里主要是 探讨, 在我们创建了一个 无主键的数据表, 然后 mysql 会为我们增加的这一个 DB_ROW_ID 的相关 新建一个无主键字段的数据表如下 CREATE TABLE implicit_id_table (username varchar(16) DEFAULT NULL,age int(11) DEFAULT NULL ) ENGINEInnoDB DEFAULT CH…

MySQL范围分区分区表

什么是范围分区分区表&#xff1f; 范围分区是一种根据某个列的范围值来分割表数据的分区方式。在范围分区中&#xff0c;每个分区都有自己的范围条件&#xff0c;当插入数据时&#xff0c;MySQL会根据指定的范围条件将数据分配到相应的分区中。这种分区方式可以使得表的数据按…

2024前端面试题-css篇

1.p和div区别 p自带有一定margin-top和margin-bottom属性值&#xff0c;而div两个属性值为0&#xff0c;也便是两个p之间有不一定间距&#xff0c;而div没有。 2.对css盒模型的理解 标准盒模型&#xff1a;content不包括padding、border、margin ie盒模型&#xff1a;conten…

关于我的生信笔记开通《知识星球》

关于知识星球 1. 为什么到现在才开通《知识星球》 从很早关注我的同学应该了解小杜的知识分享历程&#xff0c;小杜是从2021年11月底开始进入此“坑”&#xff0c;一直坚持到现在&#xff0c;马上3年了&#xff08;24年11月底到期&#xff09;。自己也从一个小青年&#xff0…

【图文并茂】ant design pro 如何统一封装好 ProFormSelect 的查询请求

你仔细看上面的图片吧 经常有这样的需求吧。 这些列表都是查询出来的。 后端 你的后端必须要有 api 。 const getUsers handleAsync(async (req: Request, res: Response) > {const { email, name, live, current 1, pageSize 10 } req.query;const query: any {};…

的卢易表:批量处理Excel数据的自动化工具

的卢易表&#xff1a;批量处理Excel数据的自动化工具 简介 的卢易表是一个可以批量批量处理Excel数据的自动化工具。 自动化是其最大的特点&#xff0c;因为它可以根据配置好的选项自动处理excel数据。 批量是它另一个特点&#xff0c;因为可以做到自动化&#xff0c;所以你可…