
v48.05 鸿蒙内核源码分析(信号生产) | 年过半百 活力十足 原创 精华
德行:颜渊,闵子骞,冉伯牛,仲弓。言语:宰我,子贡。政事:冉有,季路。文学:子游,子夏。 《论语》:先进篇
百篇博客系列篇.本篇为:
v48.xx 鸿蒙内核源码分析(信号生产篇) | 年过半百,依然活力十足
进程管理相关篇为:
- v02.06 鸿蒙内核源码分析(进程管理) | 谁在管理内核资源
- v24.03 鸿蒙内核源码分析(进程概念) | 如何更好的理解进程
- v45.05 鸿蒙内核源码分析(Fork) | 一次调用 两次返回
- v46.05 鸿蒙内核源码分析(特殊进程) | 老鼠生儿会打洞
- v47.02 鸿蒙内核源码分析(进程回收) | 临终托孤的短命娃
- v48.05 鸿蒙内核源码分析(信号生产) | 年过半百 活力十足
- v49.03 鸿蒙内核源码分析(信号消费) | 谁让CPU连续四次换栈运行
- v71.03 鸿蒙内核源码分析(Shell编辑) | 两个任务 三个阶段
- v72.01 鸿蒙内核源码分析(Shell解析) | 应用窥伺内核的窗口
信号生产
关于信号篇,本只想写一篇,但发现把它想简单了,内容不多,难度极大.整理了好长时间,理解了为何<<深入理解linux内核>>要单独为它开一章,原因有二
- 信号相关的结构体多,而且还容易搞混.所以看本篇要注意结构体的名字和作用.
- 系统调用太多了,涉及面广,信号的来源分硬件和软件.相当于软中断和硬中断,这就会涉及到汇编代码,但信号的处理函数又在用户空间,CPU是禁止内核态执行用户态代码的,所以运行过程需在用户空间和内核空间来回的折腾,频繁的切换上下文.
信号思想来自Unix,它老人家已经五十多岁了,但很有活力,许多方面几乎没发生大的变化.信号可以由内核产生,也可以由用户进程产生,并由内核传送给特定的进程或线程(组),若这个进程定义了自己的信号处理程序,则调用这个程序去处理信号,否则则执行默认的程序或者忽略.
信号为系统提供了一种进程间异步通讯的方式,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不可能知道信号到底什么时候到达。一般来说,只需用户进程提供信号处理函数,内核会想方设法调用信号处理函数,网上查阅了很多的关于信号的资料.个人想换个视角去看信号.把异步过程理解为生产者(安装和发送信号)和消费者(捕捉和处理信号)两个过程.鉴于此,系列篇将分成两篇说明,本篇为信号生产篇:
信号分类
每个信号都有一个名字和编号,这些名字都以SIG
开头,例如SIGQUIT
、SIGCHLD
等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l
来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。不过kill
对于信号0有特殊的应用。啥用呢? 可用来查询进程是否还在. 敲下 kill 0 pid
就知道了.
信号分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号。
-
不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31;
-
可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64
信号来源
信号来源分为硬件类和软件类:
- 硬件类
- 用户输入:比如在终端上按下组合键
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():用于向进程自身发送信号;
- 通过系统调用,发送signal信号:
信号与进程的关系
主要是通过系统调用 sigaction
将用户态信号处理函数注册到PCB保存.所有进程的任务都共用这个信号注册函数sigHandler
,在信号的消费阶段内核用一种特殊的方式’回调’它.
解读
-
每个信号都对应一个位. 信号从1开始编号 [1 ~ 64] 对应
sigShare
的[0 ~ 63]位,所以中间会差一个.记住这点,后续代码会提到. -
sigHandler
信号处理函数的注册过程,由系统调用sigaction
(用户空间) ->OsSigAction
(内核空间)完成绑定动作.sigaction(...)
第一个参数是要安装的信号; 第二个参数与sigaction函数同名的结构体,这里会让人很懵,函数名和结构体一直,没明白为毛要这么搞? 结构体内定义了信号处理方法;第三个为输出参数,将信号的当前的sigaction结构带回.但鸿蒙显然没有认真对待第三个参数.把musl
实现给阉割了.- 对结构体的
sigaction
鸿蒙目前只支持信号处理函数——普通版,sa_handler
表示自定义信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。 sa_mask
指定信号处理程序执行过程中需要阻塞的信号。sa_flags
字段包含一些选项,具体看注释sa_sigaction
是实时信号的处理函数,union
二选一.鸿蒙暂时不支持这种方式.
信号与任务的关系
解读
- 系列篇已多次说过,进程只是管理资源的容器,真正让cpu干活的是任务
task
,所以发给进程的信号最终还是需要分发给具体任务来处理.所以能想到的是关于任务部分会更复杂. context
信号处理很复杂的原因在于信号的发起在用户空间,发送需要系统调用,而处理信号的函数又是用户空间提供的, 所以需要反复的切换任务上下文.而且还有硬中断的问题,比如 ctrl + c ,需要从硬中断中回调用户空间的信号处理函数,处理完了再回到内核空间,最后回到用户空间.没听懂吧,我自己都说晕了,所以需要专门的一篇来说清楚信号的处理问题.本篇不展开说.sig_cb
结构体是任务处理信号的结构体,要响应,屏蔽哪些信号等等都由它完成,这个结构体虽不复杂,但是很绕,很难搞清楚它们之间的区别.笔者是经过一番痛苦的阅读理解后才明白各自的含义.并想通过用打比方的例子试图让大家明白.- 以下用追女孩打比方理解.任务相当于某个男,没错说的就是屏幕前的你,除了苦逼的码农谁会有耐心能坚持看到这里.64个信号对应64个女孩.允许一男同时追多个女孩,女孩也可同时被多个男追.女孩也可以主动追男的.理解如下:
waitList
等待信号的任务链表,上面挂的是因等待信号而被阻塞的任务.众男在排队追各自心爱的女孩们,处于无所事事的挂起的状态,等待女孩们的出现.sigwaitmask
任务在等待的信号集合,只有这些信号能唤醒任务.相当于列出喜欢的各位女孩,只要出现一位就能让你满血复活.sigprocmask
指任务对哪些信号不感冒.来了也不处理.相当于列出不喜欢的各位女孩,请她们别来骚扰你,嘚瑟.sigPendFlag
信号到达但并未唤醒任务.相当于喜欢你的女孩来追你,但她不在你喜欢的列表内,结果是不搭理人家继续等喜欢的出现.sigFlag
记录不屏蔽的信号集合,相当于你并不反感的女孩们.记录来过的那些女孩(除掉你不喜欢的).
信号发送过程
用户进程调用kill()
的过程如下:
流程
-
通过 系统调用
kill
陷入内核空间 -
因为是用户态进程,使用
OS_USER_KILL_PERMISSION
权限发送信号 -
鉴权之后进程轮询任务组,向目标任务发送信号.这里分三种情况:
SIGKILL
信号,将所有等待任务唤醒,拉入就绪队列等待被调度执行,并情况信号等待集- 非
SIGKILL
信号时,将通过sigwaitmask
和sigprocmask
过滤,找到一个任务向它发送信号OsTcbDispatch
.
代码细节
-
如果是
SIGKILL
信号,让spcb
的所有任务执行SigProcessKillSigHandler
函数,查看旗下的所有任务是否又在等待这个信号的,如果有就将任务唤醒,放在就绪队列等待被调度执行. -
非
SIGKILL
信号,让spcb
的所有任务执行SigProcessSignalHandler
函数解读
- 函数的意思是,当进程中有多个任务在等待这个信号时,发送信号给第一个等待的任务
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 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
按功能模块:
- 前因后果 >> 总目录 | 调度故事 | 内存主奴 | 源码注释 | 源码结构 | 静态站点 |
- 基础工具 >> 双向链表 | 位图管理 | 用栈方式 | 定时器 | 原子操作 | 时间管理 |
- 加载运行 >> ELF格式 | ELF解析 | 静态链接 | 重定位 | 进程映像 |
- 进程管理 >> 进程管理 | 进程概念 | Fork | 特殊进程 | 进程回收 | 信号生产 | 信号消费 | Shell编辑 | Shell解析 |
- 编译构建 >> 编译环境 | 编译过程 | 环境脚本 | 构建工具 | gn应用 | 忍者ninja |
- 进程通讯 >> 自旋锁 | 互斥锁 | 进程通讯 | 信号量 | 事件控制 | 消息队列 |
- 内存管理 >> 内存分配 | 内存管理 | 内存汇编 | 内存映射 | 内存规则 | 物理内存 |
- 任务管理 >> 时钟任务 | 任务调度 | 任务管理 | 调度队列 | 调度机制 | 线程概念 | 并发并行 | CPU | 系统调用 | 任务切换 |
- 文件系统 >> 文件概念 | 文件系统 | 索引节点 | 挂载目录 | 根文件系统 | 字符设备 | VFS | 文件句柄 | 管道文件 |
- 硬件架构 >> 汇编基础 | 汇编传参 | 工作模式 | 寄存器 | 异常接管 | 汇编汇总 | 中断切换 | 中断概念 | 中断管理 |
百万汉字注解.精读内核源码
四大码仓中文注解 . 定期同步官方代码
鸿蒙研究站( weharmonyos ) | 每天死磕一点点,原创不易,欢迎转载,请注明出处。若能支持点赞则更佳,感谢每一份支持。
