开源鸿蒙内核源码分析系列 | 信号生产 | 年过半百 活力十足(转载)
信号生产
关于信号篇,本只想写一篇,但发现把它想简单了,内容不多,难度极大。整理了好长时间,理解了为何<<深入理解linux内核>>要单独为它开一章,原因有二:
- 信号相关的结构体多,而且还容易搞混。所以看本篇要注意结构体的名字和作用。
- 系统调用太多了,涉及面广,信号的来源分硬件和软件。相当于软中断和硬中断,这就会涉及到汇编代码,但信号的处理函数又在用户空间,CPU是禁止内核态执行用户态代码的,所以运行过程需在用户空间和内核空间来回的折腾,频繁的切换上下文。
信号思想来自Unix,它老人家已经五十多岁了,但很有活力,许多方面几乎没发生大的变化。信号可以由内核产生,也可以由用户进程产生,并由内核传送给特定的进程或线程(组),若这个进程定义了自己的信号处理程序,则调用这个程序去处理信号,否则则执行默认的程序或者忽略。
信号为系统提供了一种进程间异步通讯的方式,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不可能知道信号到底什么时候到达。一般来说,只需用户进程提供信号处理函数,内核会想方设法调用信号处理函数,网上查阅了很多的关于信号的资料。个人想换个视角去看信号。把异步过程理解为生产者(安装和发送信号)和消费者(捕捉和处理信号)两个过程。鉴于此,系列篇将分成两篇说明:
《信号生产 | 年过半百 活力十足》
《 信号消费 | 谁让CPU连续四次换栈运行》
本篇为信号生产篇。
信号分类
每个信号都有一个名字和编号,这些名字都以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():用于向进程自身发送信号。
信号与进程的关系
主要是通过系统调用 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(内核空间)完成绑定动作。
//初始化一个事件控制块
LITE_OS_SEC_TEXT_INIT UINT32 LOS_EventInit(PEVENT_CB_S eventCB)
{
UINT32 intSave;
intSave = LOS_IntLock();//锁中断
eventCB->uwEventID = 0; //其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生)
LOS_ListInit(&eventCB->stEventList);//事件链表初始化
LOS_IntRestore(intSave);//恢复中断
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信号时,将通过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;
}
//信号分发
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代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。
百文说内核 | 抓住主脉络
子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。百篇博客系列思维导图结构如下:
根据上图的思维导图,我们未来将要和大家一一分享以上大部分关键技术点的博客文章。
百万汉字注解.精读内核源码
如果大家觉得看文章不过瘾,想直接撸代码的话,可以去下面四大码仓围观同步注释内核源码:
gitee仓:
https://gitee.com/weharmony/kernel_liteos_a_note
github仓 :
https://github.com/kuangyufei/kernel_liteos_a_note
codechina仓:
https://codechina.csdn.net/kuangyufei/kernel_liteos_a_note
coding仓:
https://weharmony.coding.net/public/harmony/kernel_liteos_a_note/git/files
写在最后
我们最近正带着大家玩嗨OpenHarmony。如果你有用OpenHarmony开发的好玩的东东,或者有对OpenHarmony的深度技术剖析,想通过我们平台让更多的小伙伴知道和分享的,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:
合作邮箱:zzliang@atomsource.org