首页 文章 精选 留言 我的

精选列表

搜索[harmonyos开发],共10000篇文章
优秀的个人博客,低调大师

鸿蒙内核源码分析(信号生产篇) | 生产异步通讯信号的过程 | 百篇博客分析HarmonyOS源码 | v48.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号 关于信号篇,本只想写一篇,但发现把它想简单了,内容不多,难度极大.整理了好长时间,理解了为何<<深入理解linux内核>>要单独为它开一章. 1.是信号相关的结构体多,而且还容易搞混.所以看本篇要注意结构体的名字和作用. 2.是系统调用太多了,涉及面广,信号的来源分硬件和软件.相当于软中断和硬中断,这就会涉及到汇编代码,但信号的处理函数又在用户空间,CPU是禁止内核态执行用户态代码的,所以运行过程需在用户空间和内核空间来回的折腾,频繁的切换上下文. 信号为系统提供了一种进程间异步通讯的方式,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不可能知道信号到底什么时候到达。一般来说,只需用户进程提供信号处理函数,当有信号到达的时候,由内核空间异步回调用户空间的信号处理函数,其异步过程可理解为生成者和消费者.鉴于此,系列篇将分成 信号生产 和 信号消费 两篇说明: 本篇是信号生产,说概念,说结构体,理清信号和进程,任务的关系.最后说发送过程. 另外一篇专门说信号处理过程,围绕信号切换上下文sig_switch_context来说. 信号分类 每个信号都有一个名字和编号,这些名字都以SIG开头,例如SIGQUIT、SIGCHLD等等。 信号定义在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():用于向进程自身发送信号; 信号与进程相关结构体 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结构地址输出.但鸿蒙显然没有认真对待第三个参数. sigaction()可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体. 鸿蒙目前支持信号处理函数——普通版,sa_handler表示自定义函数捕捉信号,或者说用户进程向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用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信号时,将通过sigwaitmask和sigprocmask过滤,找到一个任务向它发送信号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; } OsDispatch() 函数的代码如下: //信号分发 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; } 解读 OsSigProcessSend 如果是 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代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。 鸿蒙源码百篇博客 往期回顾 v48.xx (信号生产篇) | 生产异步通讯信号的过程 < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 内核是如何描述cpu的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号量篇) | 信号量解决任务同步问题 | 中文注解HarmonyOS源码 | v29.02

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚信号量 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 基本概念 信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。 信号量运作原理 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量, 等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。 信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。 当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。 信号量长什么样? typedef struct { UINT8 semStat; /**< Semaphore state *///信号量的状态 UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量 UINT16 maxSemCount; /**< Max number of available semaphores *///有效信号量的最大数量 UINT32 semID; /**< Semaphore control structure ID *///信号量索引号 LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去 } LosSemCB; semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 鸿蒙内核源码分析(总目录) 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务. 初始化信号量模块 #ifndef LOSCFG_BASE_IPC_SEM_LIMIT #define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数 #endif LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信号量初始化 { LosSemCB *semNode = NULL; UINT32 index; LOS_ListInit(&g_unusedSemList);//初始 /* system resident memory, don't free */ g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));//分配信号池 if (g_allSem == NULL) { return LOS_ERRNO_SEM_NO_MEMORY; } for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) { semNode = ((LosSemCB *)g_allSem) + index;//拿信号控制块, 可以直接g_allSem[index]来嘛 semNode->semID = SET_SEM_ID(0, index);//保存ID semNode->semStat = OS_SEM_UNUSED;//标记未使用 LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上 } if (OsSemDbgInitHook() != LOS_OK) { return LOS_ERRNO_SEM_NO_MEMORY; } return LOS_OK; } 分析如下: 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量 信号ID范围从 [0,1023] 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上. 小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善. 创建信号量 LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle) { unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个 LOS_ListDelete(unusedSem);//从空闲链表上摘除 semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到. 进程,线程等结构体也都是这么干的. semCreated->semCount = count;//设置数量 semCreated->semStat = OS_SEM_USED;//设置可用状态 semCreated->maxSemCount = maxCount;//设置最大信号数量 LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了. *semHandle = semCreated->semID;//参数带走 semID OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count); return LOS_OK; ERR_HANDLER: OS_RETURN_ERROR_P2(errLine, errNo); } 分析如下: 从未使用的空闲链表中拿首个信号量供分配使用. 信号量的最大数量和信号量个数都由参数指定. 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量 申请信号量 LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) { UINT32 intSave; LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体 UINT32 retErr = LOS_OK; LosTaskCB *runTask = NULL; if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) { OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID); } if (OS_INT_ACTIVE) { PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n"); OsBackTrace(); return LOS_ERRNO_SEM_PEND_INTERR; } runTask = OsCurrTaskGet();//获取当前任务 if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) { OsBackTrace(); return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK; } SCHEDULER_LOCK(intSave); if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) { retErr = LOS_ERRNO_SEM_INVALID; goto OUT; } /* Update the operate time, no matter the actual Pend success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } else if (!timeout) { retErr = LOS_ERRNO_SEM_UNAVAILABLE; goto OUT; } if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n"); OsBackTrace(); retErr = LOS_ERRNO_SEM_PEND_IN_LOCK; goto OUT; } runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量 retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文 if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task runTask->taskSem = NULL; retErr = LOS_ERRNO_SEM_TIMEOUT; } OUT: SCHEDULER_UNLOCK(intSave); return retErr; } 分析如下: 这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看. 先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK) if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } 其余申请失败的原因有: 信号量ID超出范围(默认1024) 中断发生期间 系统任务 信号量状态不对,信号量ID不匹配 以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办? 任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeout 定 timeout 值分以下三种模式: 无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请信号量时,入参 timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后, 超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。 在 OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务. 释放信号量 LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched) { LosSemCB *semPosted = NULL; LosTaskCB *resumedTask = NULL; if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) { return LOS_ERRNO_SEM_INVALID; } semPosted = GET_SEM(semHandle); if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) { return LOS_ERRNO_SEM_INVALID; } /* Update the operate time, no matter the actual Post success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPosted->semCount == OS_SEM_COUNT_MAX) {//当前信号资源不能大于最大资源量 return LOS_ERRNO_SEM_OVERFLOW; } if (!LOS_ListEmpty(&semPosted->semList)) {//当前有任务挂在semList上,要去唤醒任务 resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒 resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值 OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态 if (needSched != NULL) {//参数不为空,就返回需要调度的标签 *needSched = TRUE;//TRUE代表需要调度 } } else {//当前没有任务挂在semList上, semPosted->semCount++;//信号资源多一个 } return LOS_OK; } LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle) { UINT32 intSave; UINT32 ret; BOOL needSched = FALSE; SCHEDULER_LOCK(intSave); ret = OsSemPostUnsafe(semHandle, &needSched); SCHEDULER_UNLOCK(intSave); if (needSched) {//需要调度的情况 LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令 LOS_Schedule();////发起调度 } return ret; } 分析如下: 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务. semList上的任务是在OsTaskWait中挂入的.都在等这个信号. 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空. 掌握信号量的核心是理解 LOS_SemPend 和 LOS_SemPost 编程示例 本实例实现如下功能: 测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。 Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。 Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量,Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。 20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。 Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。 /* 任务ID */ static UINT32 g_testTaskId01; static UINT32 g_testTaskId02; /* 测试任务优先级 */ #define TASK_PRIO_TEST 5 /* 信号量结构体id */ static UINT32 g_semId; VOID Example_SemTask1(VOID) { UINT32 ret; printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n"); /* 定时阻塞模式申请信号量,定时时间为10ticks */ ret = LOS_SemPend(g_semId, 10); /*申请到信号量*/ if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } /* 定时时间到,未申请到信号量 */ if (ret == LOS_ERRNO_SEM_TIMEOUT) { printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n"); /*永久阻塞模式申请信号量*/ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); printf("Example_SemTask1 wait_forever and get sem g_semId .\n"); if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } } } VOID Example_SemTask2(VOID) { UINT32 ret; printf("Example_SemTask2 try get sem g_semId wait forever.\n"); /* 永久阻塞模式申请信号量 */ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); if (ret == LOS_OK) { printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n"); } /* 任务休眠20 ticks */ LOS_TaskDelay(20); printf("Example_SemTask2 post sem g_semId .\n"); /* 释放信号量 */ LOS_SemPost(g_semId); return; } UINT32 ExampleTaskEntry(VOID) { UINT32 ret; TSK_INIT_PARAM_S task1; TSK_INIT_PARAM_S task2; /* 创建信号量 */ LOS_SemCreate(0,&g_semId); /* 锁任务调度 */ LOS_TaskLock(); /*创建任务1*/ (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1; task1.pcName = "TestTsk1"; task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task1.usTaskPrio = TASK_PRIO_TEST; ret = LOS_TaskCreate(&g_testTaskId01, &task1); if (ret != LOS_OK) { printf("task1 create failed .\n"); return LOS_NOK; } /* 创建任务2 */ (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2; task2.pcName = "TestTsk2"; task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task2.usTaskPrio = (TASK_PRIO_TEST - 1); ret = LOS_TaskCreate(&g_testTaskId02, &task2); if (ret != LOS_OK) { printf("task2 create failed .\n"); return LOS_NOK; } /* 解锁任务调度 */ LOS_TaskUnlock(); ret = LOS_SemPost(g_semId); /* 任务休眠40 ticks */ LOS_TaskDelay(40); /* 删除信号量 */ LOS_SemDelete(g_semId); /* 删除任务1 */ ret = LOS_TaskDelete(g_testTaskId01); if (ret != LOS_OK) { printf("task1 delete failed .\n"); return LOS_NOK; } /* 删除任务2 */ ret = LOS_TaskDelete(g_testTaskId02); if (ret != LOS_OK) { printf("task2 delete failed .\n"); return LOS_NOK; } return LOS_OK; } 运行结果: Example_SemTask2 try get sem g_semId wait forever. Example_SemTask1 try get sem g_semId ,timeout 10 ticks. Example_SemTask2 get sem g_semId and then delay 20ticks . Example_SemTask1 timeout and try get sem g_semId wait forever. Example_SemTask2 post sem g_semId . Example_SemTask1 wait_forever and get sem g_semId . 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号量篇) | 信号量解决任务同步问题 | 中文注解HarmonyOS源码 | v29.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚信号量 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 基本概念 信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。 信号量运作原理 信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量, 等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。 信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。 当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。 信号量长什么样? typedef struct { UINT8 semStat; /**< Semaphore state *///信号量的状态 UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量 UINT16 maxSemCount; /**< Max number of available semaphores *///有效信号量的最大数量 UINT32 semID; /**< Semaphore control structure ID *///信号量索引号 LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去 } LosSemCB; semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 鸿蒙内核源码分析(总目录) 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务. 初始化信号量模块 #ifndef LOSCFG_BASE_IPC_SEM_LIMIT #define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数 #endif LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信号量初始化 { LosSemCB *semNode = NULL; UINT32 index; LOS_ListInit(&g_unusedSemList);//初始 /* system resident memory, don't free */ g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));//分配信号池 if (g_allSem == NULL) { return LOS_ERRNO_SEM_NO_MEMORY; } for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) { semNode = ((LosSemCB *)g_allSem) + index;//拿信号控制块, 可以直接g_allSem[index]来嘛 semNode->semID = SET_SEM_ID(0, index);//保存ID semNode->semStat = OS_SEM_UNUSED;//标记未使用 LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上 } if (OsSemDbgInitHook() != LOS_OK) { return LOS_ERRNO_SEM_NO_MEMORY; } return LOS_OK; } 分析如下: 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量 信号ID范围从 [0,1023] 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上. 小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善. 创建信号量 LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle) { unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个 LOS_ListDelete(unusedSem);//从空闲链表上摘除 semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到. 进程,线程等结构体也都是这么干的. semCreated->semCount = count;//设置数量 semCreated->semStat = OS_SEM_USED;//设置可用状态 semCreated->maxSemCount = maxCount;//设置最大信号数量 LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了. *semHandle = semCreated->semID;//参数带走 semID OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count); return LOS_OK; ERR_HANDLER: OS_RETURN_ERROR_P2(errLine, errNo); } 分析如下: 从未使用的空闲链表中拿首个信号量供分配使用. 信号量的最大数量和信号量个数都由参数指定. 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量 申请信号量 LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) { UINT32 intSave; LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体 UINT32 retErr = LOS_OK; LosTaskCB *runTask = NULL; if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) { OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID); } if (OS_INT_ACTIVE) { PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n"); OsBackTrace(); return LOS_ERRNO_SEM_PEND_INTERR; } runTask = OsCurrTaskGet();//获取当前任务 if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) { OsBackTrace(); return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK; } SCHEDULER_LOCK(intSave); if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) { retErr = LOS_ERRNO_SEM_INVALID; goto OUT; } /* Update the operate time, no matter the actual Pend success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } else if (!timeout) { retErr = LOS_ERRNO_SEM_UNAVAILABLE; goto OUT; } if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n"); OsBackTrace(); retErr = LOS_ERRNO_SEM_PEND_IN_LOCK; goto OUT; } runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量 retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文 if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task runTask->taskSem = NULL; retErr = LOS_ERRNO_SEM_TIMEOUT; } OUT: SCHEDULER_UNLOCK(intSave); return retErr; } 分析如下: 这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看. 先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK) if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了 semPended->semCount--;//资源少了一个 goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 } 其余申请失败的原因有: 信号量ID超出范围(默认1024) 中断发生期间 系统任务 信号量状态不对,信号量ID不匹配 以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办? 任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeout 定 timeout 值分以下三种模式: 无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请信号量时,入参 timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后, 超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。 在 OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务. 释放信号量 LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched) { LosSemCB *semPosted = NULL; LosTaskCB *resumedTask = NULL; if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) { return LOS_ERRNO_SEM_INVALID; } semPosted = GET_SEM(semHandle); if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) { return LOS_ERRNO_SEM_INVALID; } /* Update the operate time, no matter the actual Post success or not */ OsSemDbgTimeUpdateHook(semHandle); if (semPosted->semCount == OS_SEM_COUNT_MAX) {//当前信号资源不能大于最大资源量 return LOS_ERRNO_SEM_OVERFLOW; } if (!LOS_ListEmpty(&semPosted->semList)) {//当前有任务挂在semList上,要去唤醒任务 resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒 resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值 OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态 if (needSched != NULL) {//参数不为空,就返回需要调度的标签 *needSched = TRUE;//TRUE代表需要调度 } } else {//当前没有任务挂在semList上, semPosted->semCount++;//信号资源多一个 } return LOS_OK; } LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle) { UINT32 intSave; UINT32 ret; BOOL needSched = FALSE; SCHEDULER_LOCK(intSave); ret = OsSemPostUnsafe(semHandle, &needSched); SCHEDULER_UNLOCK(intSave); if (needSched) {//需要调度的情况 LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令 LOS_Schedule();////发起调度 } return ret; } 分析如下: 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务. semList上的任务是在OsTaskWait中挂入的.都在等这个信号. 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空. 掌握信号量的核心是理解 LOS_SemPend 和 LOS_SemPost 官方代码 /* 任务ID */ static UINT32 g_testTaskId01; static UINT32 g_testTaskId02; /* 测试任务优先级 */ #define TASK_PRIO_TEST 5 /* 信号量结构体id */ static UINT32 g_semId; VOID Example_SemTask1(VOID) { UINT32 ret; printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n"); /* 定时阻塞模式申请信号量,定时时间为10ticks */ ret = LOS_SemPend(g_semId, 10); /*申请到信号量*/ if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } /* 定时时间到,未申请到信号量 */ if (ret == LOS_ERRNO_SEM_TIMEOUT) { printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n"); /*永久阻塞模式申请信号量*/ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); printf("Example_SemTask1 wait_forever and get sem g_semId .\n"); if (ret == LOS_OK) { LOS_SemPost(g_semId); return; } } } VOID Example_SemTask2(VOID) { UINT32 ret; printf("Example_SemTask2 try get sem g_semId wait forever.\n"); /* 永久阻塞模式申请信号量 */ ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER); if (ret == LOS_OK) { printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n"); } /* 任务休眠20 ticks */ LOS_TaskDelay(20); printf("Example_SemTask2 post sem g_semId .\n"); /* 释放信号量 */ LOS_SemPost(g_semId); return; } UINT32 ExampleTaskEntry(VOID) { UINT32 ret; TSK_INIT_PARAM_S task1; TSK_INIT_PARAM_S task2; /* 创建信号量 */ LOS_SemCreate(0,&g_semId); /* 锁任务调度 */ LOS_TaskLock(); /*创建任务1*/ (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1; task1.pcName = "TestTsk1"; task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task1.usTaskPrio = TASK_PRIO_TEST; ret = LOS_TaskCreate(&g_testTaskId01, &task1); if (ret != LOS_OK) { printf("task1 create failed .\n"); return LOS_NOK; } /* 创建任务2 */ (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2; task2.pcName = "TestTsk2"; task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task2.usTaskPrio = (TASK_PRIO_TEST - 1); ret = LOS_TaskCreate(&g_testTaskId02, &task2); if (ret != LOS_OK) { printf("task2 create failed .\n"); return LOS_NOK; } /* 解锁任务调度 */ LOS_TaskUnlock(); ret = LOS_SemPost(g_semId); /* 任务休眠40 ticks */ LOS_TaskDelay(40); /* 删除信号量 */ LOS_SemDelete(g_semId); /* 删除任务1 */ ret = LOS_TaskDelete(g_testTaskId01); if (ret != LOS_OK) { printf("task1 delete failed .\n"); return LOS_NOK; } /* 删除任务2 */ ret = LOS_TaskDelete(g_testTaskId02); if (ret != LOS_OK) { printf("task2 delete failed .\n"); return LOS_NOK; } return LOS_OK; } 运行结果: Example_SemTask2 try get sem g_semId wait forever. Example_SemTask1 try get sem g_semId ,timeout 10 ticks. Example_SemTask2 get sem g_semId and then delay 20ticks . Example_SemTask1 timeout and try get sem g_semId wait forever. Example_SemTask2 post sem g_semId . Example_SemTask1 wait_forever and get sem g_semId . 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v28.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇开始介绍进程通讯(IPC) 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇幅. 进程间为何要通讯 ? 鸿蒙内核默认支持 64个进程 和 128个任务. 内核设计尽量不去打扰它们,让各人过好各自的日子, 但大家毕竟在一口锅里吃饭, 不可能不与外界联系, 联系就得有方法,有规矩. 举两个应用场景说明下通讯的必要性: 一.被动式 广为熟知的shell命令 kill 9 13 ,是通过 shell任务给 13号进程发送一个干掉它的信号. #define SIGKILL 9 //常用的命令 kill 9 13 这是被动式通讯的场景,至于为什么要干掉你,原因可能很多啊,很可能是检测到13占用内存太多了,也可能13太低调长期不活跃,启动新进程发现没位置了,得收了你.反正系统必须得有对付你的抓手. 二.主动式的 ,比如要访问某些公共资源(全局变量,消息队列),而资源有限或具有排他性,别人正在使用导致你不能用, 所以需统一管理,要用就必须要先申请,按规矩办事,毕竟和谐社会没规矩不成方圆. 总之大概有以下几种需求需要通讯: (1).数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间 (2).共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。 (3).通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 (4).资源共享: 多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。 (5).进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 内核目录和系列篇更新 内核有个专门的IPC目录,详见如下. 可直接点击查看注解源码. 进程间通讯方式 查看源码注解点击一下 .c 文件 1.管道pipe(fs_syscall.c) 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。 调用pipe系统函数即可创建一个管道。有如下特质: 其本质是一个伪文件(实为内核缓冲区) 由两个文件描述符引用,一个表示读端,一个表示写端。 规定数据从管道的写端流入管道,从读端流出。 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 管道的局限性: ① 数据自己读不能自己写。 ② 数据一旦被读走,便不在管道中存在,不可反复读取。 ③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。 ④ 只能在有公共祖先的进程间使用管道。 常见的通信方式有,单工通信、半双工通信、全双工通信。 鸿蒙的管道实现很简单, 详细看SysPipe函数. 2.信号(los_signal.c) 信号是用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程被调度执行并传递给它为止。 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。 进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。 #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 13 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //软件终止(software termination) #define SIGSTKFLT 16 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 3.消息队列(los_queue.c) 基本概念 队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的 不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。 任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时, 挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时, 挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将 读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。 消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。 队列特性 消息以先进先出的方式排队,支持异步读写。 读队列和写队列都支持超时机制。 每读取一条消息,就会将该消息节点设置为空闲。 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。 一个任务能够从任意一个消息队列接收和发送消息。 多个任务能够从同一个消息队列接收和发送消息。 创建队列时所需的队列空间,默认支持接口内系统自行动态申请内存的方式,同时也支持将用户分配的队列空间作为接口入参传入的方式。 4.共享内存(shm.c) 共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存 不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候, 其它进程都会察觉到这个更改。 (共享内存篇)| 正在更新中... 5.信号量(los_sem.c) 基本概念 信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。 一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分两种情况: 0,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务。 正值,表示该信号量当前可被获取。 以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同: 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量, 然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他 需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时, 建议使用二值信号量,一种类似于互斥锁的机制。 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量, 任务1才得以进入Ready或Running态,从而达到了任务间的同步。 使用场景 在多任务系统中,信号量是一种非常灵活的同步方式,可以运用在多种场合中,实现锁、同步、资源计数等功能, 也能方便的用于任务与任务,中断与任务的同步中。信号量常用于协助一组相互竞争的任务访问共享资源。 (信号量篇)|正在更新中... 6.互斥锁 (los_mux.c) : 基本概念 互斥锁又称互斥型信号量,是一种特殊的二值性信号量,用于实现对临界资源的独占式处理。 另外,互斥锁可以解决信号量存在的优先级翻转问题。 任意时刻互斥锁只有两种状态,开锁或闭锁。当任务持有时,这个任务获得该互斥锁的所有权, 互斥锁处于闭锁状态。当该任务释放锁后,任务失去该互斥锁的所有权,互斥锁处于开锁状态。 当一个任务持有互斥锁时,其他任务不能再对该互斥锁进行开锁或持有。 详见: (互斥锁篇) | 为何任务会因拥有锁而被迫改变优先级 ? 7.快锁 (los_futex.c) futex 是Fast Userspace muTexes的缩写(快速用户空间互斥体),是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。 8.事件 (los_event.c) 基本概念 事件(Event)是一种任务间通信的机制,可用于任务间的同步。 多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。 多对多同步模型:多个任务等待多个事件的触发。 事件特点 任务通过创建事件控制块来触发事件或等待事件。 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。 事件仅用于任务间的同步,不提供数据传输功能。 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。 多个任务可以对同一事件进行读写操作。 支持事件读写超时机制。 事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。 使用场景 队列用于任务间通信,可以实现消息的异步处理。同时消息的发送方和接收方不需要彼此联系,两者间是解耦的。 (事件驱动篇)|正在更新中... 9.文件消息队列 (hm_liteipc.c) 基于文件实现的消息队列,特点是队列中消息数量多(256个),传递消息内容大(可达到1K) #define IPC_MSG_DATA_SZ_MAX 1024 //最大的消息内容 1K ,posix最大消息内容 64个字节 #define IPC_MSG_OBJECT_NUM_MAX 256 //最大的消息数量256 ,posix最大消息数量 16个 (消息队列篇)|正在更新中... 以上为鸿蒙内核的九种进程间通讯方式,源码注解已基本完成,可前往Fork查看. 每一种通讯方式博客将详细说明,正在更新中... 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

鸿蒙内核源码分析(自旋锁篇) | 汇编到令人心碎的自旋锁 | 中文注解HarmonyOS源码 | v26.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony > 本篇说清楚自旋锁 读本篇之前建议先读鸿蒙内核源码分析(总目录)进程+线程篇. 概述 自旋锁顾名思义,是一把自动旋转的锁,这很像厕所里锁,进入前标记是绿色可用的,进入格子间后,手一带,里面的锁转个圈,外面标记变成了红色表示在使用,外面的只能等待.这是形象的比喻,但实际也是如此. 在多CPU核环境中,由于使用相同的内存空间,存在对同一资源进行访问的情况,所以需要互斥访问机制来保证同一时刻只有一个核进行操作,自旋锁就是这样的一种机制。 自旋锁是指当一个线程在获取锁时,如果锁已经被其它线程获取,那么该线程将循环等待,并不断判断是否能够成功获取锁,直到获取到锁才会退出循环。因此建议保护耗时较短的操作,防止对系统整体性能有显的影响。 自旋锁与互斥锁比较类似,它们都是为了解决对共享资源的互斥使用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。但是两者在调度机制上略有不同,对于互斥锁,如果锁已经被占用,锁申请者会被阻塞;但是自旋锁不会引起调用者阻塞,会一直循环检测自旋锁是否已经被释放。 虽然都是共享资源竞争,但自旋锁强调的是CPU核间的竞争,而互斥量强调的是任务(包括同一CPU核)之间的竞争. 自旋锁使用流程 自旋锁用于多CPU核的情况,解决的是CPU之间竞争资源的问题.使用流程很简单,三步走。 创建自旋锁:使用LOS_SpinInit初始化自旋锁,或者使用SPIN_LOCK_INIT初始化静态内存的自旋锁。 申请自旋锁:使用接口LOS_SpinLock/LOS_SpinTrylock/LOS_SpinLockSave申请指定的自旋锁,申请成功就继续往后执行锁保护的代码;申请失败在自旋锁申请中忙等,直到申请到自旋锁为止。 释放自旋锁:使用LOS_SpinUnlock/LOS_SpinUnlockRestore接口释放自旋锁。锁保护代码执行完毕后,释放对应的自旋锁,以便其他核申请自旋锁。 自旋锁长什么样? typedef struct Spinlock {//自旋锁结构体 size_t rawLock;//记录次数 #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) // 死锁检测模块开关 UINT32 cpuid; //持有锁的CPU VOID *owner; //持有锁任务 const CHAR *name; //锁名称 #endif } SPIN_LOCK_S; 结构体很简单,里面有个宏,用于死锁检测,默认情况下是关闭的.所以真正的被使用的变量只有rawLock一个.但C语言代码中找不到变量的变化过程,而是通过一段汇编代码来实现.看完本篇会明白也只能通过汇编代码来实现自旋锁. 几个关键C函数 #if (LOSCFG_KERNEL_SMP == YES) //多CPU核情况下讨论自旋锁才有意义 //申请指定的自旋锁,如果无法获取锁,会一直循环等待 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinLock(SPIN_LOCK_S *lock) { LOS_TaskLock();//1.先告诉CPU记录有个任务上了锁 LOCKDEP_CHECK_IN(lock);//2.检查自旋锁 ArchSpinLock(&lock->rawLock);//3.自旋锁工作主体,一段汇编代码 LOCKDEP_RECORD(lock);//4.记录自旋锁 } //尝试申请指定的自旋锁,如果无法获取锁,直接返回失败,而不会一直循环等待 LITE_OS_SEC_ALW_INLINE STATIC INLINE INT32 LOS_SpinTrylock(SPIN_LOCK_S *lock) { LOS_TaskLock(); INT32 ret = ArchSpinTrylock(&lock->rawLock); if (ret == LOS_OK) { LOCKDEP_CHECK_IN(lock); LOCKDEP_RECORD(lock); } else { LOS_TaskUnlock(); } return ret; } //释放指定的自旋锁 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinUnlock(SPIN_LOCK_S *lock) { LOCKDEP_CHECK_OUT(lock); ArchSpinUnlock(&lock->rawLock);//注意ArchSpinUnlock是一个汇编函数 见于 los_dispatch.s /* restore the scheduler flag */ //恢复调度标签 LOS_TaskUnlock(); } #endif 代码也是很简单,真正涉及自旋锁的部分就是这三个函数. ArchSpinLock(&lock->rawLock); ArchSpinTrylock(&lock->rawLock) ArchSpinUnlock(&lock->rawLock); 可以说掌握了这三个函数就掌握了自旋锁,但这三个函数全由汇编实现.见于los_dispatch.S文件 因为系列篇已有两篇讲过汇编代码,所以很容易理解这三段代码.函数的参数此时由 r0记录,即r0保存了lock->rawLock的地址. 下面逐一说明自旋锁的汇编代码. ArchSpinLock 汇编代码 FUNCTION(ArchSpinLock) @对变量上锁 mov r1, #1 @r1=1 1: @循环的作用,因SEV是广播事件.不一定lock->rawLock的值已经改变了 ldrex r2, [r0] @r0 = &lock->rawLock, 即 r2 = lock->rawLock cmp r2, #0 @r2和0比较 wfene @不相等时,说明资源被占用,CPU核进入睡眠状态 strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1,成功写入则r2=0 cmpeq r2, #0 @再来比较r2是否等于0,如果相等则获取到了锁 bne 1b @如果不相等,继续进入循环 dmb @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中 bx lr @此时是一定拿到锁了,跳回调用ArchSpinLock函数 看懂了这段汇编代码就理解了自旋锁实现的真正机制,为什么一定要用汇编来实现. 因为CPU宁愿睡眠也非拿要到锁不可的, 注意这里可不是让线程睡眠,而是让CPU进入睡眠状态,能让CPU进入睡眠的只能通过汇编实现.C语言根本就写不出让CPU真正睡眠的代码. ArchSpinTrylock 汇编代码 如果不看下面这段汇编代码,你根本不可能知道 ArchSpinTrylock 和 ArchSpinLock的真正区别是什么. FUNCTION(ArchSpinTrylock) @对变量尝试上锁 mov r1, #1 @r1=1 mov r2, r0 @r2 = r0 ldrex r0, [r2] @r2 = &lock->rawLock, 即 r0 = lock->rawLock cmp r0, #0 @r0和0比较 strexeq r0, r1, [r2] @尝试令lock->rawLock=1,成功写入则r0=0,否则 r0 =1 dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 bx lr @跳回调用ArchSpinTrylock函数 比较两段汇编代码可知,ArchSpinTrylock即没有循环也不会让CPU进入睡眠,直接返回了,而ArchSpinLock会睡了醒, 醒了睡,一直守到丈夫( lock->rawLock = 0的广播事件发生)回来才肯罢休. 笔者注释到这段代码时,那真是心潮澎湃, 心碎了老一地, 真想给 ArchSpinLock 立一个贞节牌坊 ! ArchSpinUnlock 汇编代码 FUNCTION(ArchSpinUnlock) @释放锁 mov r1, #0 @r1=0 dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 str r1, [r0] @令lock->rawLock = 0 dsb @数据同步隔离 sev @给各CPU广播事件,唤醒沉睡的CPU们 bx lr @跳回调用ArchSpinUnlock函数 } 代码中涉及到几个不常用的汇编指令,一一说明: 汇编指令之 WFI / WFE / SEV WFI(Wait for interrupt):等待中断到来指令. WFI一般用于cpuidle,WFI 指令是在处理器发生中断或类似异常之前不需要做任何事情。 在鸿蒙源码分析系列篇(总目录)线程篇中已说过,每个CPU都有自己的idle任务,CPU没事干的时候就待在里面,就一个死循环守着WFI指令,有中断来了就触发CPU起床干活. 中断分硬中断和软中断,系统调用就是通过软中断实现的,而设备类的就属于硬中断,都能触发CPU干活. 具体看下CPU空闲的时候在干嘛,代码超级简单: LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) //CPU没事干的时候待在这里 { while (1) {//只有一个死循环 Wfi();//WFI指令:arm core 立即进入low-power standby state,等待中断,进入休眠模式。 } } WFE(Wait for event):等待事件的到来指令.WFE 指令是在SEV指令生成事件之前不需要执行任何操作,所以用WFE的地方,后续一定会对应一个SEV的指令去唤醒它. WFE的一个典型使用场景,是用在自旋锁中,spinlock的功能,是在不同CPU core之间,保护共享资源。使用WFE的流程是: 1.开始之初资源空闲 2.CPU核1 访问资源,持有锁,获得资源 3.CPU核2 访问资源,此时资源不空闲,执行WFE指令,让core进入low-power state(睡眠) 4.CPU核1 释放资源,释放锁,释放资源,同时执行SEV指令,唤醒CPU核2 5.CPU核2 获得资源 另外说一下 以往的自旋锁,在获得不到资源时,让CPU核进入死循环,而通过插入WFE指令,则大大节省功耗. SEV(send event):发送事件指令,SEV是一条广播指令,它会将事件发送到多处理器系统中的所有处理器,以唤醒沉睡的CPU. SEV和WFE的实现很像设计模式的观察者模式. 汇编指令之 LDREX / STREX LDREX用来读取内存中的值,并标记对该段内存的独占访问: LDREX Rx, [Ry] 上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。 如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。 而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值: STREX Rx, Ry, [Rz] 如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。 而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。 一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。 总结 自旋锁用于解决CPU核间竞争资源的问题 因为自旋锁会让CPU陷入睡眠状态,所以锁的代码不能太长,否则容易导致意外出现,也影响性能. 必须由汇编代码实现,因为C语言写不出让CPU进入真正睡眠,核间竞争的代码. 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee| Github| CSDN| Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< CSDN| 开源中国| WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v21.02

鸿蒙内核源码注释>> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新 本篇说清楚任务的问题. 在鸿蒙内核线程(thread)就是任务(task),也可以叫作业.线程是对外的说法,对内就叫任务.跟王二毛一样, 在公司叫你王董,回到家里还有领导,就叫二毛啊.这多亲切.在鸿蒙内核是大量的task,很少看到thread,只出现在posix层.当一个东西理解就行. 读本篇之前建议先读鸿蒙内核源码分析(必读故事篇) | 西门和金莲的那点破事 < CSDN | OSCHINA | WeHarmony | 源动力 >,里面有对任务生活场景式的比喻. 任务(LosTaskCB)原始真身如下,本篇一一剖析它,看看它的五脏六腑里到底长的是个啥. typedef struct { VOID *stackPointer; /**< Task stack pointer */ //非用户模式下的栈指针 UINT16 taskStatus; /**< Task status */ //各种状态标签,可以拥有多种标签,按位标识 UINT16 priority; /**< Task priority */ //任务优先级[0:31],默认是31级 UINT16 policy; //任务的调度方式(三种 .. LOS_SCHED_RR ) UINT16 timeSlice; /**< Remaining time slice *///剩余时间片 UINT32 stackSize; /**< Task stack size */ //非用户模式下栈大小 UINTPTR topOfStack; /**< Task stack top */ //非用户模式下的栈顶 bottom = top + size UINT32 taskID; /**< Task ID */ //任务ID,任务池本质是一个大数组,ID就是数组的索引,默认 < 128 TSK_ENTRY_FUNC taskEntry; /**< Task entrance function */ //任务执行入口函数 VOID *joinRetval; /**< pthread adaption */ //用来存储join线程的返回值 VOID *taskSem; /**< Task-held semaphore */ //task在等哪个信号量 VOID *taskMux; /**< Task-held mutex */ //task在等哪把锁 VOID *taskEvent; /**< Task-held event */ //task在等哪个事件 UINTPTR args[4]; /**< Parameter, of which the maximum number is 4 */ //入口函数的参数 例如 main (int argc,char *argv[]) CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name */ //任务的名称 LOS_DL_LIST pendList; /**< Task pend node */ //如果任务阻塞时就通过它挂到各种阻塞情况的链表上,比如OsTaskWait时 LOS_DL_LIST threadList; /**< thread list */ //挂到所属进程的线程链表上 SortLinkList sortList; /**< Task sortlink node */ //挂到cpu core 的任务执行链表上 UINT32 eventMask; /**< Event mask */ //事件屏蔽 UINT32 eventMode; /**< Event mode */ //事件模式 UINT32 priBitMap; /**< BitMap for recording the change of task priority, //任务在执行过程中优先级会经常变化,这个变量用来记录所有曾经变化 the priority can not be greater than 31 */ //过的优先级,例如 ..01001011 曾经有过 0,1,3,6 优先级 INT32 errorNo; /**< Error Num */ UINT32 signal; /**< Task signal */ //任务信号类型,(SIGNAL_NONE,SIGNAL_KILL,SIGNAL_SUSPEND,SIGNAL_AFFI) sig_cb sig; //信号控制块,这里用于进程间通讯的信号,类似于 linux singal模块 #if (LOSCFG_KERNEL_SMP == YES) UINT16 currCpu; /**< CPU core number of this task is running on */ //正在运行此任务的CPU内核号 UINT16 lastCpu; /**< CPU core number of this task is running on last time */ //上次运行此任务的CPU内核号 UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores */ //CPU亲和力掩码,最多支持16核,亲和力很重要,多核情况下尽量一个任务在一个CPU核上运行,提高效率 UINT32 timerCpu; /**< CPU core number of this task is delayed or pended */ //此任务的CPU内核号被延迟或挂起 #if (LOSCFG_KERNEL_SMP_TASK_SYNC == YES) UINT32 syncSignal; /**< Synchronization for signal handling */ //用于CPU之间 同步信号 #endif #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关 LockDep lockDep; #endif #if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的 SchedStat schedStat; /**< Schedule statistics */ //调度统计 #endif #endif UINTPTR userArea; //使用区域,由运行时划定,根据运行态不同而不同 UINTPTR userMapBase; //用户模式下的栈底位置 UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */ UINT32 processID; /**< Which belong process *///所属进程ID FutexNode futex; //实现快锁功能 LOS_DL_LIST joinList; /**< join list */ //联结链表,允许任务之间相互释放彼此 LOS_DL_LIST lockList; /**< Hold the lock list */ //拿到了哪些锁链表 UINT32 waitID; /**< Wait for the PID or GID of the child process */ //等待孩子的PID或GID进程 UINT16 waitFlag; /**< The type of child process that is waiting, belonging to a group or parent, a specific child process, or any child process */ #if (LOSCFG_KERNEL_LITEIPC == YES) UINT32 ipcStatus; //IPC状态 LOS_DL_LIST msgListHead; //消息队列头结点,上面挂的都是任务要读的消息 BOOL accessMap[LOSCFG_BASE_CORE_TSK_LIMIT];//访问图,指的是task之间是否能访问的标识,LOSCFG_BASE_CORE_TSK_LIMIT 为任务池总数 #endif } LosTaskCB; 结构体还是比较复杂,虽一一都做了注解,但还是不够清晰,没有模块化.这里把它分解成以下六大块逐一分析: 第一大块:多核CPU相关块 #if (LOSCFG_KERNEL_SMP == YES) //多CPU核支持 UINT16 currCpu; /**< CPU core number of this task is running on */ //正在运行此任务的CPU内核号 UINT16 lastCpu; /**< CPU core number of this task is running on last time */ //上次运行此任务的CPU内核号 UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores */ //CPU亲和力掩码,最多支持16核,亲和力很重要,多核情况下尽量一个任务在一个CPU核上运行,提高效率 UINT32 timerCpu; /**< CPU core number of this task is delayed or pended */ //此任务的CPU内核号被延迟或挂起 #if (LOSCFG_KERNEL_SMP_TASK_SYNC == YES) UINT32 syncSignal; /**< Synchronization for signal handling */ //用于CPU之间 同步信号 #endif #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关 LockDep lockDep; #endif #if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的 SchedStat schedStat; /**< Schedule statistics */ //调度统计 #endif #endif 鸿蒙内核支持多CPU,谁都知道多CPU当然好,效率高,快嘛,但凡事有两面性,在享受一个东西带来好处的同时,也得承担伴随它一起带来的麻烦和风险.多核有哪些的好处和麻烦,这里不展开说,后续有专门的文章和视频说明.任务可叫线程,或叫作业.CPU就是做作业的,多个CPU就是有多个能做作业的,一个作业能一鼓作气做完吗? 答案是:往往不行,因为现实不允许,作业可以有N多,而CPU数量非常有限,所以经常做着A作业被老板打断让去做B作业.这老板就是调度算法.A作业被打断回来接着做的还会是原来那个CPU吗? 答案是:不一定. 变量cpuAffiMask叫CPU亲和力,它的作用是可以指定A的作业始终是同一个CPU来完成, 也可以随便,交给调度算法,分到谁就谁来,这方面可以不挑. 第二大块:栈空间 VOID *stackPointer; /**< Task stack pointer */ //非用户模式下的栈指针 UINT32 stackSize; /**< Task stack size */ //非用户模式下栈大小 UINTPTR topOfStack; /**< Task stack top */ //非用户模式下的栈顶 bottom = top + size UINTPTR userArea; //使用区域,由运行时划定,根据运行态不同而不同 UINTPTR userMapBase; //用户模式下的栈底位置 UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */ 关于栈空间看过很多文章说每个任务都有自己独立的用户栈空间和内核栈空间,其实这种说法不严谨,至少在鸿蒙内核不能这么去理解,否则会有很多疑惑,解释不通的.CPU做作业就需要场地,栈空间就是给CPU做作业的场地.公司(内核设计者)规定用户的作业由用户提供场地, 用户场地就叫用户栈空间,但有些作业比较敏感不方便在用户现场完成,需要回到公司指定场地做,公司的场地就叫内核栈空间.所以准确的说法是每个任务都有自己独立的用户栈空间和共用一个内核栈空间.每个CPU在公司都有自己独立的办公位,不可能每个用户作业在公司内部有对应办公位的,明显的浪费内核资源,行不通的.而那些敏感作业就叫系统调用. 第三大块:资源竞争/同步 VOID *taskSem; /**< Task-held semaphore */ //task在等哪个信号量 VOID *taskMux; /**< Task-held mutex */ //task在等哪把锁 VOID *taskEvent; /**< Task-held event */ //task在等哪个事件 UINT32 eventMask; /**< Event mask */ //事件屏蔽 UINT32 eventMode; /**< Event mode */ //事件模式 FutexNode futex; //实现快锁功能 LOS_DL_LIST joinList; /**< join list */ //联结链表,允许任务之间相互释放彼此 LOS_DL_LIST lockList; /**< Hold the lock list */ //拿到了哪些锁链表 UINT32 signal; /**< Task signal */ //任务信号类型,(SIGNAL_NONE,SIGNAL_KILL,SIGNAL_SUSPEND,SIGNAL_AFFI) sig_cb sig; 公司的资源是有限的,CPU自己也是公司的资源,除了它还有其他的设备,比如做作业用的黑板,用户A,B,C都可能用到,狼多肉少,咋搞? 互斥量(taskMux,futex)能解决这个问题,办事前先拿锁,拿到了锁的爽了,没有拿到的就需要排队,在lockList上排,注意lockList是个双向链表,它是内核最重要的结构体,开篇就提过,没印象的看这篇 鸿蒙内核源码分析(双向链表篇) | 谁是内核最重要结构体? < CSDN | OSCHINA | WeHarmony | 源动力 >,上面挂都是等锁进房间的西门大官人.这是互斥量的原理,解决任务间资源紧张的竞争性问题. 另外一个是用于任务的同步的信号量(sig_cb),任务和任务之间是会有关联的,现实生活中公司的A,B用户之间本身有业务往来的正常,CPU在帮B做作业的时候发现前置条件是需要A完成某项作业才能进行,这时B就需要主动让出CPU先办完A的事.这就是信号量的原理,解决的是任务间的同步问题. 第四大块:任务调度 前面说过了作业N多,做作业的只有几个人,单核CPU等于只有一个人干活.那要怎么分配CPU,就需要调度算法. UINT16 taskStatus; /**< Task status */ //各种状态标签,可以拥有多种标签,按位标识 UINT16 priority; /**< Task priority */ //任务优先级[0:31],默认是31级 UINT16 policy; //任务的调度方式(三种 .. LOS_SCHED_RR ) UINT16 timeSlice; /**< Remaining time slice *///剩余时间片 CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name */ //任务的名称 LOS_DL_LIST pendList; /**< Task pend node */ //如果任务阻塞时就通过它挂到各种阻塞情况的链表上,比如OsTaskWait时 LOS_DL_LIST threadList; /**< thread list */ //挂到所属进程的线程链表上 SortLinkList sortList; /**< Task sortlink node */ //挂到cpu core 的任务执行链表上 是简单的先来后到(FIFO)吗? 当然也支持这个方式.鸿蒙内核用的是抢占式调度(policy),就是可以插队,比优先级(priority)大小,[0,31]级,数字越大的优先级越低,跟考试一样,排第一才是最牛的,鸿蒙排0的最牛! 想也想得到内核的任务优先级都是很高的,比如资源回收任务排第5,定时器任务排第0.够牛了吧.普通老百姓排多少呢?默认28级,惨!!! 另外任务有时间限制timeSlice,叫时间片,默认20ms,用完了会给你重置,发起重新调度,找出优先级高的执行,阻塞的任务(比如没拿到锁的,等信号量同步的,等读写消息队列的)都挂到pendList上,方便管理. 第五大块:任务间通讯 #if (LOSCFG_KERNEL_LITEIPC == YES) UINT32 ipcStatus; //IPC状态 LOS_DL_LIST msgListHead; //消息队列头结点,上面挂的都是任务要读的消息 BOOL accessMap[LOSCFG_BASE_CORE_TSK_LIMIT];//访问图,指的是task之间是否能访问的标识,LOSCFG_BASE_CORE_TSK_LIMIT 为任务池总数 #endif 这个很重要,解决任务间通讯问题,要知道进程负责的是资源的管理功能,什么意思?就是它不并负责内容的生产和消费,它只负责管理确保你的内容到达率和完整性.生产者和消费者始终是任务.进程管了哪些东西系列篇有专门的文章,自行翻看.liteipc是鸿蒙专有的通讯消息队列实现.简单说它是基于文件的,而传统的ipc消息队列是基于内存的.有什么区别也不在这里讨论,已有专门的文章分析. 第六大块:辅助工具 要知道任务对内核来说太重要了,是任务让CPU忙里忙外的,那中间出差错了怎么办,怎么诊断你问题出哪里了,就需要一些工具,比如死锁检测,比如占用CPU,内存监控 如下: #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) //死锁检测开关 LockDep lockDep; #endif #if (LOSCFG_KERNEL_SCHED_STATISTICS == YES) //调度统计开关,显然打开这个开关性能会受到影响,鸿蒙默认是关闭的 SchedStat schedStat; /**< Schedule statistics */ //调度统计 #endif 以上就是任务的五脏六腑,看清楚它鸿蒙内核的影像会清晰很多! 喜欢就请加点源动力吧 各大站点搜 "鸿蒙内核源码分析",快速找到组织. 或者来点更简单点的方式 鸿蒙内核源码注释>> 精读内核源码,中文注解分析,深挖地基工程,大脑永久记忆,四大码仓每日同步更新 鸿蒙内核源码分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新

优秀的个人博客,低调大师

鸿蒙内核源码分析(用栈方式篇) | 栈是构建底层运行的基础 | 中文注解HarmonyOS源码 | v20.03

鸿蒙内核源码注释中文版 < Gitee仓 | CSDN仓 | Github仓 | Coding仓 >精读内核源码,中文注解分析,深挖地基工程,构建底层网图,四大码仓每日同步更新 鸿蒙源码分析系列篇 < CSDN | OSCHINA | WeHarmony | 公众号 >问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新 精读内核源码就绕不过汇编语言,鸿蒙内核有6个汇编文件,读不懂它们就真的很难理解以下问题. 1.系统调用是如何实现的? 2.CPU是如何切换任务和进程上下文的? 3.硬件中断是如何处理的? 4.main函数到底是怎么来的? 5.开机最开始发生了什么? 6.关机最后的最后又发生了什么? 以下是一个很简单的C文件编译成汇编代码后的注解.读懂这些注解会发现汇编很可爱,甚至还会上瘾,并没有想象中的那么恐怖,读懂它会颠覆你对汇编和栈的认知. #include <stdio.h> #include <math.h> int square(int a,int b){ return a*b; } int fp(int b) { int a = 1; return square(a+b,a+b); } int main() { int sum = 1; for(int a = 0;a < 100; a++){ sum = sum + fp(a); } return sum; } //编译器: armv7-a clang (trunk) square(int, int): sub sp, sp, #8 @sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @第一个参数入栈 str r1, [sp] @第二个参数入栈 ldr r1, [sp, #4] @取出第一个参数给r1 ldr r2, [sp] @取出第二个参数给r2 mul r0, r1, r2 @执行a*b给R0,返回值的工作一直是交给R0的 add sp, sp, #8 @函数执行完了,要释放申请的栈空间 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 fp(int): push {r11, lr} @r11(fp)/lr入栈,保存调用者main的位置 mov r11, sp @r11用于保存sp值,函数栈开始位置 sub sp, sp, #8 @sp减去8,意思为给fp分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @先保存参数值,放在SP+4,此时r0中存放的是参数 mov r0, #1 @r0=1 str r0, [sp] @再把1也保存在SP的位置 ldr r0, [sp] @把SP的值给R0 ldr r1, [sp, #4] @把SP+4的值给R1 add r1, r0, r1 @执行r1=a+b mov r0, r1 @r0=r1,用r0,r1传参 bl square(int, int)@先mov lr, pc 再mov pc square(int, int) mov sp, r11 @函数执行完了,要释放申请的栈空间 pop {r11, lr} @弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 main: push {r11, lr} @r11(fp)/lr入栈,保存调用者的位置 mov r11, sp @r11用于保存sp值,函数栈开始位置 sub sp, sp, #16 @sp减去8,意思为给main分配栈空间,只用2个栈空间完成计算 mov r0, #0 @初始化r0 str r0, [r11, #-4] @作用是保存SUM的初始值 str r0, [sp, #8] @sum将始终占用SP+8的位置 str r0, [sp, #4] @a将始终占用SP+4的位置 b .LBB1_1 @跳到循环开始位置 .LBB1_1: @循环开始位置入口 ldr r0, [sp, #4] @取出a的值给r0 cmp r0, #99 @跟99比较 bgt .LBB1_4 @大于99,跳出循环 mov pc .LBB1_4 b .LBB1_2 @继续循环,直接 mov pc .LBB1_2 .LBB1_2: @符合循环条件入口 ldr r0, [sp, #8] @取出sum的值给r0,sp+8用于写SUM的值 str r0, [sp] @先保存SUM的值,SP的位置用于读SUM值 ldr r0, [sp, #4] @r0用于传参,取出A的值给r0作为fp的参数 bl fp(int) @先mov lr, pc再mov pc fp(int) mov r1, r0 @fp的返回值为r0,保存到r1 ldr r0, [sp] @取出SUM的值 add r0, r0, r1 @计算新sum的值,由R0保存 str r0, [sp, #8] @将新sum保存到SP+8的位置 b .LBB1_3 @无条件跳转,直接 mov pc .LBB1_3 .LBB1_3: @完成a++操作入口 ldr r0, [sp, #4] @SP+4中记录是a的值,赋给r0 add r0, r0, #1 @r0增加1 str r0, [sp, #4] @把新的a值放回SP+4里去 b .LBB1_1 @跳转到比较 a < 100 处 .LBB1_4: @循环结束入口 ldr r0, [sp, #8] @最后SUM的结果给R0,返回值的工作一直是交给R0的 mov sp, r11 @函数执行完了,要释放申请的栈空间 pop {r11, lr} @弹出r11和lr,lr是专用标签,弹出就自动复制给lr寄存器 bx lr @子程序返回,跳转到lr处等同于 MOV PC, LR 这个简单的汇编并不是鸿蒙的汇编,只是先打个底,由浅入深, 但看懂了它基本理解鸿蒙汇编代码没有问题, 后续将详细分析鸿蒙内核各个汇编文件的作用. 开始分析上面的汇编代码. 第一: 上面的代码和鸿蒙内核用栈方式一样,都采用了递减满栈的方式, 什么是递减满栈? 递减指的是栈底地址高于栈顶地址,满栈指的是SP指针永远在栈顶.一定要理解递减满栈,否则读不懂内核汇编代码.举例说明: square(int, int): sub sp, sp, #8 @sp减去8,意思为给square分配栈空间,只用2个栈空间完成计算 str r0, [sp, #4] @第一个参数入栈 str r1, [sp] @第二个参数入栈 ldr r1, [sp, #4] @取出第一个参数给r1 ldr r2, [sp] @取出第二个参数给r2 mul r0, r1, r2 @执行a*b给R0,返回值的工作一直是交给R0的 add sp, sp, #8 @函数执行完了,要释放申请的栈空间 bx lr @子程序返回,等同于mov pc,lr,即跳到调用处 首句汇编的含义就是申请栈空间, sp = sp - 8 ,一个栈内单元(栈空间)占4个字节,申请2个栈空间搞定函数的计算,仔细看下代码除了在函数的末尾 sp = sp + 8 又恢复在之前的位置的中间过程,SP的值是没有任务变化,它的指向是不动的, 这跟很多人对栈的认知是不一样的,它只是被用于计算,例如 ldr r1, [sp, #4] 的意思是取出SP+4这个虚拟地址的值给r1寄存器,SP的值并没有改变的,为什么要+呢,因为SP是指向栈顶的,地址是最小的. 满栈就是用栈过程中对地址的操作不能超过SP,所以你很少在计算过程中看到 把sp-4地址中的值给某个寄存器, 除非是特别的指令,否则不可能有这样的指令. 第二: sub sp, sp, #8 和 add sp, sp, #8 是成对出现的,这就跟申请内存,释放内存的道理一样,这是内核对任务的运行栈管理方式,一样用多少申请多少,用完释放.空间大小就是栈帧,这是栈帧的本质含义. 第三: push {r11, lr} 和 pop {r11, lr} 也是成对出现的,主要是用于函数调用,例如 A -> B, B要保存A的栈帧范围和指令位置, lr保存是是A函数执行到哪个指令的位置, r11干了fp的工作,其实就是指向 A的栈顶位置,如此B执行完后return回A的时候,先mov pc,lr 内核就知道改执行A的哪条指令了,同时又知道了A的栈顶位置. 第四: 频繁出现的R0寄存器的作用用于传参和返回值, A调用B之前,假如有两个参数,就把参数给r0 ,r1记录,充当了A的变量, 到了B中后,先让 r0,r1入栈,目的是保存参数值, 因为 B中要用r0,r1 ,他们变成B的变量用了. 返回值都是默认统一给r0保存. B中将返回值给r0,回到A中取出R0值对A来说这就是B的返回值. 这是以上为汇编代码的分析,追问两个问题 第一:如果是可变参数怎么办? 100个参数怎么整, 通过寄存器总共就12个,不够传参啊 第二:返回值可以有多个吗? 请在评论区留言 喜欢就点赞+关注下吧 8-) 您的关注真的很重要 :) 作者邮箱:weharmony@126.com 鸿蒙内核源码注释中文版 < Gitee仓 | CSDN仓 | Github仓 | Coding仓 >精读内核源码,中文注解分析,深挖地基工程,构建底层网图,四大码仓每日同步更新 鸿蒙源码分析系列篇 < CSDN | OSCHINA | WeHarmony | 公众号 >问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新

优秀的个人博客,低调大师

鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙看这篇或许真的够了 | 百篇博客分析HarmonyOS源码 | v50.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 编译鸿蒙 本篇记录编译鸿蒙的过程,以备后续不用再去一大堆无效的误导式软文中搜寻芝麻大点有用的信息,那样真挺费时的. 编译环境 先安装 Docker Desktop 下载windows版本一直下一步. 在windows下拉取openharmony-docker官方镜像,Docker方式获取编译环境 强烈推荐这么做. docker pull swr.cn-south-1.myhuaweicloud.com/openharmony-docker/openharmony-docker:0.0.3 2.36G, 拉取看网速, 大概10分钟后成功了,有了镜像 PS E:\harmony\kernel_liteos_a_note> docker images REPOSITORY TAG IMAGE ID CREATED SIZE swr.cn-south-1.myhuaweicloud.com/openharmony-docker/openharmony-docker 0.0.3 50d0aa6ea9ba 2 weeks ago 2.36GB vscode对docker的管理插件非常的强大,管理镜像和容器的工作就交给它了. 启动docker,创建好容器,本文的选择是这样的,当然大家可以灵活处理,命名. 容器创建成功后可以在 vscode 右键容器inspect查看到绑定的目录. "HostConfig": { "Binds": [ "E:\\harmony\\code-1.0:/home/harmony", "E:\\harmony\\docker:/home/docker" ], 本文这样做的目的为了在windows上能方便的查看文件.harmony目录用于下载编译源码目录,另外一个docker放其他无关文件 repo方式下载源码 进入容器,vscode右键容器 Attach shell root@5e3abe332c5a:/home/harmony#repo init -u https://gitee.com/openharmony/manifest.git -b master --no-repo-verify root@5e3abe332c5a:/home/harmony#repo sync -c 这个过程也是比网速.慢慢等吧. 下载完成之后的样子 root@5e3abe332c5a:/home/harmony# ls applications base build build.py developtools device docs domains drivers foundation kernel ohos_config.json prebuilts test third_party utils vendor 编译过程 在源码的根目录执行如下命令安装hb root@5e3abe332c5a:/home/harmony#python3 -m pip install --user build/lite 设置编译路径,选择当前路径 root@5e3abe332c5a:/home/harmony#hb set [OHOS INFO] Input code path: . OHOS Which product do you need? (Use arrow keys) hisilicon ❯ ipcamera_hispark_aries wifiiot_hispark_pegasus ipcamera_hispark_taurus 直接回车,代表选择了ipcamera_hispark_aries,这三个对应平台的关系如下 Hi3518:ipcamera_hispark_aries@hisilicon Hi3861:wifiiot_hispark_pegasus@hisilicon Hi3516:ipcamera_hispark_taurus@hisilicon 执行编译,过程大概20分钟 root@5e3abe332c5a:/home/harmony#hb build -f 查看编译结果 每个的目录含义如下 目录名 描述 applications 应用程序样例,包括wifi-iot,camera等 base 基础软件服务子系统集&硬件服务子系统集 build 组件化编译、构建和配置脚本 docs 说明文档 domains 增强软件服务子系统集 drivers 驱动子系统 foundation 系统基础能力子系统集 kernel 内核子系统 prebuilts 编译器及工具链子系统 test 测试子系统 third_party 开源第三方组件 utils 常用的工具集 vendor 厂商提供的软件 build.py 编译脚本文件 out 编译后生成 编译输出 out为编译结果输出目录 out/hispark_aries/ipcamera_hispark_aries 进入目录 root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries# ls args.gn build.log bundle_daemon_tool.map dev_tools libs NOTICE_FILE OHOS_Image.asm rootfs suites toggleButtonTest.map userfs vendor bin build.ninja config etc liteos.bin obj OHOS_Image.bin rootfs_jffs2.img test toolchain.ninja userfs_jffs2.img bm_tool.map build.ninja.d data foundation.map media_server.map OHOS_Image OHOS_Image.map server.map test_info unstripped usr 系列篇会详细讲解启动过程,此处进入bin目录瞅瞅都有些啥好宝贝. root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# ls ai_server module_ActsAbilityMgrTest.bin module_ActsGraphVersionTest.bin module_ActsJFFS2CapabilityTest.bin module_ActsNFSTest.bin module_ActsSurfaceTest.bin os_dump apphilogcat module_ActsBootstrapTest.bin module_ActsHeapBaseTest.bin module_ActsJFFS2DACTest.bin module_ActsParameterTest.bin module_ActsSysApiTest.bin query.bin appspawn module_ActsBundleMgrTest.bin module_ActsHilogTest.bin module_ActsJFFS2Test.bin module_ActsPMSTest.bin module_ActsTimeApiTest.bin shell bundle_daemon module_ActsColorTest.bin module_ActsIoApiTest.bin module_ActsKvStoreTest.bin module_ActsProcessApiTest.bin module_ActsTransformTest.bin tftp CalcSubTest.bin module_ActsDyloadTest.bin module_ActsIpcMqTest.bin module_ActsListTest.bin module_ActsRectTest.bin module_ActsUiInterfaceTest1.bin wms_server foundation module_ActsFutexApiTest.bin module_ActsIpcPipeTest.bin module_ActsLwipTest.bin module_ActsSamgrTest.bin module_ActsUtilApiTest.bin hilogcat module_ActsGeometyr2dTest.bin module_ActsIpcSemTest.bin module_ActsMathApiTest.bin module_ActsSchedApiTest.bin module_ActsVFATCapabilityTest.bin init module_ActsGraphicMathTest.bin module_ActsIpcShmTest.bin module_ActsMemApiTest.bin module_ActsSecurityDataTest.bin module_ActsVFATDACTest.bin media_server module_ActsGraphMemApiTest.bin module_ActsIpcSignalTest.bin module_ActsNetTest.bin module_ActsSoftBusTest.bin module_ActsVFATTest.bin 这难道都是ELF格式可执行程序? 用readelf命令试下shell就知道了.果然是shell程序,加载它将创建激动人心的shell进程 root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# readelf -h shell ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: ARM Version: 0x1 Entry point address: 0x1000 Start of program headers: 52 (bytes into file) Start of section headers: 25268 (bytes into file) Flags: 0x5000200, Version5 EABI, soft-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 11 Size of section headers: 40 (bytes) Number of section headers: 27 Section header string table index: 26 再随便选择一个module_ActsListTest.bin看下,这些是鸿蒙用于测试的代码生成的.也是一个个的独立程序.可以在工程里找到他们的身影list_test.cpp,ActsListTest.json等文件 root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# readelf -h module_ActsListTest.bin ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: ARM Version: 0x1 Entry point address: 0xb000 Start of program headers: 52 (bytes into file) Start of section headers: 172256 (bytes into file) Flags: 0x5000200, Version5 EABI, soft-float ABI Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 11 Size of section headers: 40 (bytes) Number of section headers: 27 Section header string table index: 26 解读 仔细比较下这两个ELF的头文件看哪里不一样 Entry point address: 0x1000 Entry point address: 0xb000 这是程序的入口地址,也就是大家熟悉的main()函数的地址,用户程序是从这个位置开始执行. readelf的功能很强大,有兴趣的可以玩下这个命令,看看elf里面究竟装的啥. root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# readelf -help readelf: option requires an argument -- 'p' Usage: readelf <option(s)> elf-file(s) Display information about the contents of ELF format files Options are: -a --all Equivalent to: -h -l -S -s -r -d -V -A -I -h --file-header Display the ELF file header -l --program-headers Display the program headers --segments An alias for --program-headers -S --section-headers Display the sections' header --sections An alias for --section-headers -g --section-groups Display the section groups -t --section-details Display the section details -e --headers Equivalent to: -h -l -S -s --syms Display the symbol table --symbols An alias for --syms --dyn-syms Display the dynamic symbol table -n --notes Display the core notes (if present) -r --relocs Display the relocations (if present) -u --unwind Display the unwind info (if present) -d --dynamic Display the dynamic section (if present) -V --version-info Display the version sections (if present) -A --arch-specific Display architecture specific information (if any) -c --archive-index Display the symbol/file index in an archive -D --use-dynamic Use the dynamic section info when displaying symbols -x --hex-dump=<number|name> Dump the contents of section <number|name> as bytes -p --string-dump=<number|name> Dump the contents of section <number|name> as strings -R --relocated-dump=<number|name> Dump the contents of section <number|name> as relocated bytes -z --decompress Decompress section before dumping it -w[lLiaprmfFsoRtUuTgAckK] or --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index,=links,=follow-links] Display the contents of DWARF debug sections --dwarf-depth=N Do not display DIEs at depth N or greater --dwarf-start=N Display DIEs starting with N, at the same depth or deeper --ctf=<number|name> Display CTF info from section <number|name> --ctf-parent=<number|name> Use section <number|name> as the CTF parent --ctf-symbols=<number|name> Use section <number|name> as the CTF external symtab --ctf-strings=<number|name> Use section <number|name> as the CTF external strtab -I --histogram Display histogram of bucket list lengths -W --wide Allow output width to exceed 80 characters @<file> Read options from <file> -H --help Display this information -v --version Display the version number of readelf 具体的加载过程和elf格式后续有专门的篇幅详细介绍,此处不做说明. 用户进程 用grep过滤下干扰的*.bin,剩下的就是平台(Hi3518)需要内核创建的用户态进程. root@5e3abe332c5a:/home/harmony/out/hispark_aries/ipcamera_hispark_aries/bin# ls | grep -v .bin ai_server apphilogcat appspawn bundle_daemon foundation hilogcat init media_server os_dump shell tftp wms_server 这些服务含义和仓库如下. ai_server AI业务子系统 https://gitee.com/openharmony/ai_engine apphilogcat 小型系统的流水日志功能 https://gitee.com/openharmony/hiviewdfx_hilog_lite base\hiviewdfx\hilog_lite\services\apphilogcat\hiview_applogcat.c appspawn 应用孵化模块进程 https://gitee.com/openharmony/startup_appspawn_lite bundle_daemon 用户程序框架内部工具接口 https://gitee.com/openharmony/appexecfwk_appexecfwk_lite foundation foundation系统进程 https://gitee.com/openharmony/distributedschedule_safwk_lite hilogcat 管理日志打印 https://gitee.com/openharmony/hiviewdfx_hilog_lite base\hiviewdfx\hilog_lite\services\hilogcat\hiview_logcat.c init 用户态祖宗进程 1号进程, -> 鸿蒙内核源码分析(特殊进程篇) media_server 播放模块框架实现 https://gitee.com/openharmony/multimedia_media_lite os_dump 备份文件系统 utils\native\lite\os_dump\os_dump.c shell 窥视内核的窗口 3号进程, -> 鸿蒙内核源码分析(shell篇) tftp 传输文件 third_party\curl\lib\tftp.c wms_server 窗口管理服务 https://gitee.com/openharmony/graphic_wms 在整个项目工程中能轻易找到他们的入口函数.比如appspawn的启动过程 //base\startup\appspawn_lite\services\src\main.c int main(int argc, char * const argv[]) { sleep(1); HILOG_INFO(HILOG_MODULE_HIVIEW, "[appspawn] main, enter."); // 1. ipc module init HOS_SystemInit(); // 2. register signal for SIGCHLD SignalRegist(); // 3. keep process alive HILOG_INFO(HILOG_MODULE_HIVIEW, "[appspawn] main, entering wait."); while (1) { // pause only returns when a signal was caught and the signal-catching function returned. // pause only returns -1, no need to process the return value. (void)pause(); } } 鸿蒙将服务做成了组件,为最上层的应用程度提供管理/工具类的服务.但这些都是framework层的工作,超出了内核源码分析的范畴.希望后续有机会能去剖析它们. 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v50.xx (编译环境篇) | 编译鸿蒙看这篇或许真的够了 < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 比自旋锁丰满许多的互斥锁 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 进入 >> oschina | csdn | 51cto | 掘金 | harmony 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处.

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册