目录
传统艺能😎
小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
1319365055
🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我
概念🤔
信号是一个生活中随处可见的概念,没什么深层隐晦的地方,一个信号包含了各种信息,比如你快递到了,你该吃饭了或者你该睡觉了等等
首先在 Linux 系统中,我们首先需要 明 白 信 号 的 产 生 是 异 步 的 \color{red} {明白信号的产生是异步的} 明白信号的产生是异步的,比如有以下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1){
printf("hello signal!\n");
sleep(1);
}
return 0;
}
运行结果是死循环,我们要终止死循环的话通常会直接 ctrl c:
这里的 ctrl c 动作就是一个信号,本质上是因为输入产生了一个硬中断,ctrl c 被翻译成 2 号信号送到操作系统,操作系统再送到前台进程,然后前台进程退出。
这里所谓的 2 号信号我们可以听过 signal
函数进行捕捉,声明为:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:要捕捉的信号编号
handler:捕捉信号的处理方法,处理方法的参数是 int,返回值是 void
这里就以对 ctrl c 的捕捉为例:
可以看到这里系统确实收到另一个 2 号信号!
信号是进程之间事件异步通知的一种方式,属于 软 中 断 \color{red} {软中断} 软中断
ctrl c 产生的信号只能发给前台进程,在命令后面加 & 就可以放到后台运行,这样 Shell 不必等待进程结束就可以接收新命令,启动新的进程。当然 Shell 可以同时运行一个前台进程和任意多个后台进程,但是只有前台进程才能接到像 ctrl c 这种控制信号
该进程的用户空间代码执行到任何地方都可能收到 SIGINT
信号(ctrl c 对应的 2 号信号)而终止,所以信号相对于进程的控制流程来说是异步的。
信号发送🤔
我们可以通过 kill -l 命令查看 Linux 中所有的信号:
其中1-31号信号是普通信号,34-64号信号是实时信号,普通信号和实时信号各自都有31个,每个信号都有一个编号和一个宏定义名称:
信号记录🤔
实际上进程接收一个信号,该信号是被记录在该进程的进程控制块当中的。我们都知道进程控制块本质上就是一个结构体变量,我们主要就是记录一个信号是否产生,因此我们可以用一个 32 位的位图来记录信号是否产生
其中比特位的位置代表信号的编号,而比特位的内容就代表是否收到对应信号,比如第 6 个比特位是 1 就表明收到了 6 号信号
信号产生🤔
首先我们要知道信号是如何产生的
收到信号的本质就是进程内的信号位图被修改,也就是进程的数据被修改,而只有操作系统才有资格修改进程的数据,因为操作系统是进程的管理者。也就是说,信号的产生本质上就是操作系统直接去修改目标进程的 task_struct 中的信号位图。
常见信号处理方式🤔
对于一个信号我们有三种处理方式:
- 执行该信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
- 直接忽略信号
Linux 我们可以使用 man 指令来查看处理方式:
man 7 signal
终端按键产生信号🤔
最开始我们对于死循环程序输入 ctrl c 进行终止的动作就是通过终端按键产生信号,但是其实除了 ctrl c
以外,ctrl \
也可以终止程序:
那么问题来了, ctrl c 和 ctrl \ 的区别在哪里?
ctrl c 实际上是发送 2 号信号SIGINT
,而 ctrl \ 实际上是发送3号信号SIGQUIT
。查看这两个信号的默认处理动作,可以看到这两个信号的 Action 是不一样的,2 号信号是 Term,而 3 号信号是 Core
Term 和 Core 都代表终止进程,但是 Core 在终止进程时会进行 核 心 转 储 \color{red} {核心转储} 核心转储
核心转储😋
嘛是核心转储?其实可以理解为运行时报错日志,我们知道在一个代码运行结束后如果有报错,我们可以通过退出码得知错误信息,但是如果是一个运行的代码错误,我们只能通过核心转储获悉错误原因
在运行中如果崩溃了,我们一般会直接进行 debug 调试,但是有些特殊情况下我们会用到核心转储,核心转储的本质是一个 磁 盘 文 件 \color{red} {磁盘文件} 磁盘文件,也叫核心转储文件,它是操作系统在进程收到信号而终止后,会将进程地址空间的内容,状态和其他信息转而存储到磁盘中,一般命名为core.pid
我们我们通过 ulimit -a
指令查看当前资源限制设定,这里我们看到在云服务器里面,核心转储功能是默认关闭的,因为 core
文件大小为 0:
我们可以自己通过 ulimit -c
来改变 core 文件的大小:
这里有了 core 文件就证明核心转储功能已经被我们开发♂出来了,再次使用 ctrl \
就会出现 core dump
信息(因为我的服务器搞了汉化包,这里的硬核翻译比较尴尬,吐核就是 core dump):
且在当前目录下会产生一个 core+一串数字后缀的文件,这一串数字其实就是这次核心转储的进程的 PID
:
ulimit 指令改变的是 shell 的 Resource Limit,这里 signal 的 PCB
也是由 shell 复制来的,所以 signal 也具有和 shell 相同的 Resource Limit
如何调试🤔
我们下面这个简单的除0场景为例:
崩溃后就会出现报错信息:
接下来查看 core dump 文件:
在进入 gdb 调试模式,使用core-file + core文件名
命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码:
8 号信号对应的就是算术错误。
core dump 标识在讲 waitpid
时就有说过:
pid_t waitpid(pid_t pid, int *status, int options);
waitpid 的第二个参数 status 是一个输出型参数,他用于获取子进程的退出状态,他是一个整型参数,但是我们不能单纯将它看成一个整数,因为他的不同比特位有不同的含义:
我们只关注 status 的 16 位,如果进程时正常退出,次低 8 位就会记录退出状态,也就是我们说的退出码,如果是因为信号异常退出,低 8 位的第一位就是 core dump,记录了核心转储:
我们此时就可以开启系统的核心转储功能,编写一个代码实现父进程 fork 子进程,并在子进程里面进行野指针的实现,在 *p = 100 执行时,系统会终止并且会立即进行核心转储,我们在 waitpid 后查看对应的终止信号:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
if (fork() == 0){
//child
printf("I am running...\n");
int *p = NULL;
*p = 100;
exit(0);
}
//father
int status = 0;
waitpid(-1, &status, 0);
printf("exitCode:%d, coreDump:%d, signal:%d\n",
(status >> 8) & 0xff, (status >> 7) & 1, status & 0x7f);
return 0;
}
效果如下:
因此 core dump 标志就是表示当前进程是否进行了核心转储!
那么还有其他组合键操作吗?我们可以通过代码捕捉所有信号,并将收到信号的默认动作改为直接打印出该信号的编号:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signal)
{
printf("get a signal:%d\n", signal);
}
int main()
{
int signo;
for (signo = 1; signo <= 31; signo++){
signal(signo, handler);
}
while (1){
sleep(1);
}