信号产生之后,是发给进程的,进程要在合适的时候执行对应的动作
进程在没有收到信号的时候,就已经知道了哪种信号对应哪一个动作,进程具有识别信号并处理信号的能力,远远早于信号的产生
进程收到某种信号的时候,并不一定是立即处理的,而是在合适的时候,因为信号随时都有能产生(异步的),但进程当前可能在执行更重要的事
因此信号不能被立即处理的时候,收到的信号需要被保存起来,保存在struct task_struct
中,信号的本质也是数据,所以信号的发送就变成了往进程的task_struct
中写入信号数据,这个操作由操作系统来完成,因而无论我们的信号是如何发送的,本质都是底层在通过操作系统发送的
信号产生前:信号产生的各种方式 -> 信号产生中:信号的保存方式 -> 信号产生后:信号处理的方式
各种信号(64个)
ctrl + c
发出的就是2号信号
typedef void (*sighandler_t)(int)
:为一个函数指针
signum
:信号对应的整型值
handler
:修改进程对信号的默认处理动作
#include
#include
#include
void handler(int signum)
{
printf("\nrecieve a signal, signal num: %d, process pid: %d\n", signum, getpid());
}
int main()
{
// 通过signal注册对2号信号的处理动作,改成我们自己自定义的动作
signal(2, handler);
while (1) {
printf("hello, my pid: %d\n", getpid());
sleep(1);
}
return 0;
}
注意:注册函数的时候,不会调用这个函数,只有当信号到来的时候,这个函数才会被调用
使用ctrl + c
向该进程发出2号信号
使用kill命令同样可以向该进程发出2号信号
2号信号无法退出,可以使用
ctrl + \
退出进程
信号产生的方式其中一种就是通过键盘产生,注意键盘产生的信号只能用来终止前台进程,加上&
运行的后台进程不行
一般而言,进程收到信号的处理方式有三种
#include
#include
#include
void handler(int signum)
{
switch(signum) {
case 2:
printf("hello, this is No.2, pid: %d\n", getpid());
break;
case 3:
printf("hello, this is No.3, pid: %d\n", getpid());
break;
case 9:
printf("hello, this is No.9, pid: %d\n", getpid());
break;
default:
printf("hello, this is No.%d, pid: %d\n", signum, getpid());
break;
}
}
int main()
{
int sig = 1;
// 注册1~31号信号
for (; sig <= 31; sig++) {
signal(sig, handler);
}
while (1) {
printf("hello, my pid: %d\n", getpid());
sleep(1);
}
return 0;
}
发现9号信号不可以被捕捉,即无法自定义动作
程序中存在异常问题,会导致进程收到信号退出
#include
int main()
{
while (1) {
int *p = NULL;
*p = 100;
printf("hello, my pid: %d\n", getpid());
sleep(1);
}
return 0;
}
程序会发生段错误:Segmentation fault,即进程的崩溃,这是因为进程收到了11号信号
在Windows或Linux下,进程崩溃的本质就是收到了对应的信号,然后进程执行信号的默认处理动作
当进程崩溃时,我们需要知道崩溃的原因,waitpid()
的status
会携带进程异常退出的原因
进程如果异常的时候,core-dump该位置会被设置为1
我们还需要知道程序是在哪一行崩溃
core dump
标志位,并将进程在内存中的数据转储到磁盘当中,方便后期调试云服务器上core file
默认是关闭的,这种情况下程序崩溃会发生:
core file
打开之后
8398为异常进程的pid
如何查看异常情况(事后调式)
-g
core-file [core文件名]
,即可查看异常的详细信息
验证打开core file
之后异常退出后core dump
被设置为1
#include
#include
#include
#include
int main()
{
if (fork() == 0) {
while (1) {
printf("I am child process...\n");
int a = 10;
a /= 0;
}
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d, exit sig: %d, core dump: %d\n", (status >> 8) & 0xff, status & 0x7f, (status >> 7) & 1);
return 0;
}
模拟实现一个kill命令
#include
#include
#include
#include
#include
#include
static void Usage(const char *proc)
{
printf("Usage: \n\t %s signal who\n", proc);
}
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);
printf("signal: %d, who: %d\n", signo, who);
return 1;
}
可以给当前进程发送指定的信号,即自己给自己发信号
#include
#include
int main()
{
raise(11);
return 0;
}
成功返回0,错误返回-1
使当前进程接收到信号而异常终止
#include
#include
int main()
{
abort();
return 0;
}
就像exit函数一样,abort函数总是会成功的,所以没有返回值
通过某种软件(OS)来触发信号的发送,系统层面设置了定时器,或者某种操作而导致条件不就绪等这样的场景下,触发信号的发送
举例:进程间通信时,当读端不仅不读,而且还关闭了读fd,写端一直在写,最终进程会收到SIGPIPE(13),就是一种典型的软件条件触发的信号发送
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM
信号,该信号的默认处理动作是终止当前进程
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数
打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
#include
#include
int main()
{
int ret = alarm(20);
while (1) {
printf("I am a process, ret = %d\n", ret);
sleep(5);
int res = alarm(0); //取消闹钟
printf("res = %d\n", res);
}
return 0;
}
利用alarm来比较有IO与无IO的情况下,CPU执行效率
有IO
#include
#include
int count = 0;
int main()
{
alarm(1); // 没有设置alarm的捕捉动作,就执行默认动作,终止进程
while(1) {
printf("hello, %d\n",count++);
}
return 0;
}
无IO
#include
#include
#include
#include
int count = 0;
void handlerAlarm(int signum) {
printf("hello, %d\n", count);
exit(1);
}
int main()
{
signal(SIGALRM, handlerAlarm);
alarm(1); // 没有设置alarm的捕捉动作,就执行默认动作,终止进程
while(1) {
count++;
}
return 0;
}
无IO的情况下会快很多
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过OS向目标进程发送信号
产生信号的方式,其实都是OS发送信号数据给task_struct
struct task_struct中有进程的各种属性,那么其中也一定有对应的数据变量,来保存是否收到了对应的信号,而信号的编号也是有规律的1~31
进程中采用uint32_t sigs;
——位图结构来标识该进程是否收到信号
第31个<--------------------------------------------第1个
0000 0000 0000 0000 0000 0000 0000 0000
比特位的位置(第几个)代表的就是哪一个信号,比特位的内容(0或1),代表的就是是否收到了信号
故本质是OS向指定进程的task_struct中的信号位图写入比特1,即完成信号的发送,也可以说是信号的写入