信号
- 1. 信号的产生 - 信号发送前
- 1.1 键盘产生
- 1.2 异常
- 1.3 系统调用
- 1.4 软件条件
- 1.5 理解发送信号
- 2. 信号的保存 - 信号发送中
- 2.1 相关概念 & 内核结构
- 2.2 sigset_t 及一系列系统调用函数
- 3. 信号的处理 - 信号发送后
- 3.1 内核如何实现信号捕捉
- 3.2 sigaction
- 4. 可重入函数
- 5. volalite关键字
- 6. SIGCHLD信号
people change
前排声明:以后更新文章会慢一些吧,如上文所言,people change… (哈哈哈距离我刚开始写下这句话时候已过去一个月了,果然,人生总是在不经意的想法中出现转折,曾经认为理所当然的事儿… 所以顺其自然…
“而且我们都是学计算机的,我们知道如果把宇宙所有的原子结构保存,配上超强的算法,一切都是必然的,我们决定不了的”
“你一上升到这个高度我就讨论不了了”
…
“我只是想说顺其自然”
a zing never lies @小边小边别发愁
生活中有许多关于信号的场景:比如红绿灯,你daddy的脸色。。。
这些场景触发时,我立马就知道我接下来该做什么,这来自于天生or后天习得,并不是它们真的发生时我才知道该怎么做。同理,进程具有识别并处理信号的能力是远远早于信号产生的,这是由那些编写操作系统的工程师写好的。
在生活中,我们收到信号时,不一定会立即处理,因为当前我可能做着更重要的事儿。同理,信号随时都可能产生(异步),进程收到某种信号时,不一定是立即处理的,而是在合适的时候处理。
比如有一小伙儿跟你表白了,你虽然没有立即回复(此时你正向闺蜜群组求助哈哈哈),但你也记着要给个答复。既然有时信号不能被立即处理,进程就需要先将信号保存起来,以供合适时机处理。那应该保存在哪里呢?struct task_struct
,信号的发送即向进程控制块写入信号数据。task_struct是一个内核数据结构,内核不相信任何人,只相信自己,无论以何种方式发送信号,本质都是通过OS发送的,那一定是操作系统向task_struct写入信号数据。
kill -l 查看系统支持的信号列表
前31个(1 ~ 31)是普通信号,后31个(34 ~ 64)是实时信号 ——
一般进程收到信号的处理方案有三种情况 ——
- 默认动作:终止自己、暂停等
- 忽略动作:是一种信号处理的方式,只不过就是什么也不干
- 自定义动作(信号的捕捉):比如如下的
signal
方法,修改处理信号的动作,由默认动作变为自定义动作
1. 信号的产生 - 信号发送前
信号产生的方式有哪些呢?
1.1 键盘产生
写一段死循环函数,可以ctrl+c终止程序。我们键盘ctrl+C
时,本质是向指定进程发送2号信号SIGINT
。那你怎么证明?可以通过signal函数为信号指定处理函数:
signal - ANSI C signal handling
#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针 - 返回值为void,参数为int
sighandler_t signal(int signum, sighandler_t handler);
修改进程对信号的默认处理动作 ——
-
signal
:实际上这些大写的信号都是#define 整数
定义出来的宏 -
handler
:注册函数。注册时不是调用这个函数,只有信号到来时,函数才会被调用。
代码如下 ——
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
printf("get a signal: %d, pid: %d\n", signo, getpid());
exit(1);
}
int main()
{
int sig;
for(sig = 1; sig<=31; sig++)
{
//通过signal注册对1~31号信号的处理动作,改成我们自定义的动作
signal(sig, handler);
}
while(1)
{
printf("people change...pid: %d\n", getpid());
sleep(1);
}
return 0;
}
发送信号时,执行自定义方法 ——
信号的产生方式的其中一种是通过键盘产生,键盘产生的信号只能终止前台进程,杀掉后台进程&
得用kill命令 ——
kill -9 [pid]
我们还可以设置不同信号的处理方式 ——
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void handler(int signo)
{
switch(signo)
{
case 2:
printf("Ayayaya~\n");
break;
case 3:
printf("people change a lot\n");
break;
case 9:
printf("I am tired\n");
break;
default:
printf("signo: %d\n", signo);
break;
}
//exit(1);
}
int main()
{
int sig;
for(sig = 1; sig<=31; sig++)
{
//通过signal注册对1~31号信号的处理动作,改成我们自定义的动作
signal(sig, handler);
}
while(1)
{
printf("people change...pid: %d\n", getpid());
sleep(1);
}
return 0;
}
发现9号信号不可被捕捉(自定义) ——
为什么呢?we will talk about it later…
1.2 异常
信号产生,也可能由于程序中存在异常问题,使进程收到信号并退出。
写一段野指针解引用代码 ——
int* p = NULL;
*p = 100;
进程为什么会崩溃呢?本质是因为进程收到了11号信号SIGSEGV
。而且如果不exit会刷屏,说明不断收到11号信号。
除0 ——
int a = 10;
a /= 0;
除0收到的是8号信号SIGFPE
,浮点数异常 ——
综上,在win或Linux环境下,进程崩溃的本质是进程收到了对应的信号,并执行默认处理动作(杀死进程)。那为什么会收到信号呢?
软件上的错误通常会体现在硬件或其它软件上,而操作系统是硬件管理者,就应该对硬件的健康负责,找到有问题的进程并发送信号反馈,终止进程。我们在C++中的捕捉异常,实际上就是在处理信号。
进程崩溃时,你最关心的是崩溃的原因,这是通过waitpid()的status参数的低7位来获取退出信号。(忘了的宝子去复习进程控制吧~)
从前从前我们就知道,在Linux中,进程正常退出时,它的退出码和退出信号都会被设置;进程异常退出时,它的退出信号会被设置,表明进程退出的原因。另外我还想知道是在哪一行崩溃的喂?
从前从前我们挖了这样一个坑:如有必要,OS会设置退出信息中的core dump
标志位,并将进程在内存中的数据转储到磁盘中,方便我们后期调试。
在云服务器上,core dump被关掉了,我们把它打开~
ulimit -a 查看系统资源
ulimit -c 10240
这样就允许你core dump了,(但也不是所有的信号都会core dumped),此时再运行,还生成了core文件 ——
我们打开生成的core.10225,发现是人类看不懂的乱码,因为是把磁盘内容直接拷下来的,我们也不关心啦~
我们需要小小修改一下Makefile文件,编译时带上-g
选项,允许用gdb调试。用core-file命令,得知错误原因及错误行数 ——
这种方案叫做事后调试。曾经我们程序崩溃,我们只能注释掉定位或者一行一行的单步调试。
若进程出现异常时被core dump,core dump
位会被设置为1,我们来通过位操作获取退出信息 ——
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
if(fork() == 0)
{
while(1)
{
printf("I am child...\n");
int a = 10;
a /= 0;
}
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d, exit sig:%d, core dump flag: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1); return 0;
}
1.3 系统调用
通过系统调用产生信号 ——
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); //给任意进程发送任意信号
int raise(int sig); //给自己发任意信号
#include <stdlib.h>
void abort(void); //给自己发送SIGABRT信号
kill命令就是调用kill函数实现的 ——
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
static void Usage(const char* proc)
{
printf("Usage:\n\t%s signo who", proc);
}
// ./mytest signo who
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int signo = atoi(argv[1]);
int who = atoi(argv[2]);
kill(who, signo);
return 0;
}
1.4 软件条件
通过某种软件(OS),来触发信号的发送,在系统层面设置定时器,或某种操作导致条件不就绪等这样的场景下,触发的信号。
在进程间通信匿名管道的4种场景中,写端疯狂写,若此时读端关闭了读fd,则写端会**收到13号信号SIGPIPE
**退出,这就是一种软件条件触发的信号发送。今天我们再介绍 ——
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
设置一个计时器/闹钟,延迟n秒后发送14号信号SIGALRM
——
- 返回值:可以认为alarm被正常触发时,返回值就是0;或者是距离设定时间余下的秒数。
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
void handler(int signo)
{
printf("well, I recieved %d signal\n", signo);
}
int main()
{
int sig;
for(sig = 1; sig <= 31;sig++)
{
signal(sig, handler);
}
int ret = alarm(20);
sleep(5);
ret = alarm(0); //取消闹钟
printf("autually, it still left %d seconds\n", ret);
return 0;
}
我们不断打印count,统计1s中server对count可以递增到三万多次;然后屏蔽掉printf,而只是在捕捉信号处打印count,发现count达到了5亿次 ——
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
int count = 0;
//没有设置alarm信号(14)的捕捉/自定义动作, 执行默认动作:终止进程
//void handler(int signo)
//{
// printf("after 1s... count: %d\n", count); // exit(1);
//}
int main()
{
signal(SIGALRM, handler);
alarm(1); /
while(1)
{
//printf("people change...%d\n", count);
count++;
}
return 0;
}
因为IO是非常慢的。。
1.5 理解发送信号
信号产生方式的种类非常多,但就算信号产生的方式千差万别,最终一定是通过OS向目标进程发送信号。
如何理解OS给进程发送信号?朴素的理解就是OS发送信号数据给task_struct,我们要进一步来谈 ——
我们发现信号的编号是有规律的**[1, 31],进程内部一定要有对应的数据变量,来保存记录是否收到了对应的信号。那用什么数据类型,来标识进程是否收到信号**呢?唔唔唔!位图!
struct task_struct{
//进程的各种属性
uint32_t sigs;
};
比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号 ——
//比如这个,表示收到3号信号
00000000 00000000 00000000 00000100
综上,“信号的发送”更准确的说是信号的写入,即OS向指定进程的task_struct的信号位图(pending
位图)比特位 置为1。
2. 信号的保存 - 信号发送中
信号随时都可能产生(异步),进程收到某种信号时不一定是立即处理的,因为当前我可能做着更重要的事儿,而是把它暂存起来在合适的时候处理。怎么保存信号?合适又是什么时候?
2.1 相关概念 & 内核结构
信号有三张表:pending
、block
、handler
,我们姑且不关心它们底层的数据结构,其中pending
表就是用来写入接收到的信号的位图 ——
- 实际执行信号的处理动作称为信号递达(Delivery) —— 自定义捕捉、默认、忽略
- 信号从产生到递达之间的状态,称为信号未决(Pending) —— 本质是这个信号被暂存在task_struct的信号位图
pending
中。 - 进程可以选择阻塞信号(Block) —— 本质是OS允许进程暂时屏蔽指定信号,表示该信号仍然是未决的,该信号不会被递达,直到解除阻塞方可执行递达的动作
注意:阻塞和递达中的忽略有区别吗?yes,忽略是递达的一种方式,而阻塞是递达前的独立状态。
grep -ER 'SIG_DFL | SIG_IGN' /usr/include 递归查找一下这两个宏
#define SIG_DFL ((__sighandler_t)0) /* Default action */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
-
block
表:信号屏蔽字,阻塞位图,表示是否阻塞,不影响接收信号但影响递达信号。是一种状态位图,同样的也是uint32_t block;
无符号整数类型:比特位的位置,代表信号的编号;比特位的内容,代表信号是否被阻塞(屏蔽)。这些信号不会被递达直到解除阻塞。 -
pending
表:未决位图,保存的是已经收到但没有被递达的信号。比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。所以你看,发送信号时都要给pid和信号数。 -
handler
表:函数指针数组,每个信号的编号就是该数组的下标。实际上,我们执行signal
方法,对特定的信号注册自定义方法的本质就是,把handler函数地址填入到信号编号对应下标的 handler表中,这样执行的就是自定义方法。
内核中对信号会做类似如下检测 ——
int ishandled(int signo)
{
if(block & signo) { //该信号被block
//根本就不看是否收到该信号
}
else if(pending & signo) { //该信号没被block,且已经收到了
handler_array[signo](signo); /*回调函数*/
return 0;
}
return 1;
}
综上,这张表应该横着看,进程内置了识别信号的方式,[三元组] 是否被屏蔽→是否被递达(→handler),即你是谁?你现在能否处理?怎么处理?
2.2 sigset_t 及一系列系统调用函数
从上图来看,每个信号只有一个1bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这种类型无法直接对其进行位操作,因为这是极不安全的,就必须通过系统调用接口。因为不同OS对于sigset_t位图结构的实现是不一样的,因此不能让用户直接修改。它定义的变量和我们之前的int、double没有任何区别,都是在用户栈上。
标签:SIGCHLD,int,signo,handler,volalite,信号,进程,include From: https://blog.51cto.com/u_15091587/6320556