文章目录
- 一、初识信号
- 二、产⽣信号
-
- 1、键盘按键产生信号
- 2、系统调用产生信号
-
- [1)kill 函数](#1)kill 函数)
- [2)raise 函数](#2)raise 函数)
- [3)abort 函数](#3)abort 函数)
- 3、软件条件产生信号
- 4、硬件异常产生信号
一、初识信号
1、一个样例
信号是一种给进程发送的,用来进行事件异步通知的机制。
样例:
cpp
#include<iostream>
#include<unistd.h>
int main()
{
while(true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
运行结果:

我们按下Ctrl+C后,键盘输⼊产⽣⼀个硬件中断,被OS获取、解释成信号、发送给⽬标前台进程。 前台进程因为收到信号,根据信号做出相应的处理,这里的处理是进程退出。
2、signal 函数

Ctrl+C 是向前台进程发送 SIGINT(2 )信号。
证明如下:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
void handler(int sig)
{
std::cout << "我是: " << getpid() << ",我获得了一个信号: " << sig << std::endl;
}
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGINT, handler);
while(true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
运行结果:

3、信号处理
我们可以通过以下指令查看信号:kill -l

收到信号后的处理动作有以下三种:忽略处理(SIG_IGN)、默认处理(SIG_DFL)、自定义处理(handler)。
这里以Ctrl+C产生的SIGINT信号举例:
- SIGINT的默认处理是SIG_DFL,也就是说当我们按下Ctrl+C,默认进程就会被终止。
- 如果将SIGINT的处理动作设置为SIG_IGN,那么当我们按下Ctrl+C,不会对进程产生任何影响,也就是忽略这个信号的意思。
- 如果将SIGINT的处理动作设置为自定义处理,我们就需要自己定义一个处理函数,例如handler,当我们按下Ctrl+C时,系统就会默认执行handler这个函数,这个特性很重要!!!
二、产⽣信号

产生信号大体分为4种方式,分别是键盘按键、系统调用、软件条件、硬件异常。
1、键盘按键产生信号
1)基本操作
Ctrl+C(SIGINT 2):终止进程Ctrl+\(SIGQUIT 3):终止进程并⽣成core dump⽂件。Ctrl+Z(SIGTSTP 20):终止进程并将当前进程挂到后台。
这里测试一下Ctrl+Z:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{
std::cout << "我是进程: " << getpid() << std::endl;
signal(SIGTSTP, SIG_DFL);
while (true)
{
std::cout << "I am a process, I am waiting signal!" << std::endl;
sleep(1);
}
return 0;
}
运行结果:

当我们按下Ctrl+Z后,进程就被挂到后台了。
一些前后台有关的指令:
- jobs:查看所有后台任务
- fg 任务号:把任务切到前台运行
- bg 任务号:让挂起的任务在后台继续运行
2)键盘产生信号的过程
当键盘按下的时候,会向CPU发送硬件中断,然后CPU会识别到自身针脚具有硬件中断信息,接着CPU就会执行OS中处理键盘数据的代码。
2、系统调用产生信号
1)kill 函数

我们可以封装一个简单的kill指令:
cpp
#include<iostream>
#include<string>
#include<sys/types.h>
#include<signal.h>
// ./kill -9 1234
int main(int argc,char* argv[])
{
if(argc!=3)
{
std::cerr << "Usage: " << "./kill -signumber pid" << std::endl;
return 1;
}
int signumber = std::stoi(argv[1] + 1);
pid_t pid = std::stoi(argv[2]);
int n = kill(pid, signumber);
return 0;
}
执行一个死循环:

再通过封装的的kill指令发送9号信号来杀掉这个进程:

2)raise 函数

示例:一秒向自己发送一个2号信号
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signumber)
{
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(2, handler);
while (true)
{
sleep(1);
raise(2);
}
return 0;
}
结果:

3)abort 函数

示例:一秒执行一次abort函数
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void handler(int signumber)
{
std::cout << "获取了一个信号: " << signumber << std::endl;
}
int main()
{
signal(SIGABRT, handler);
while (true)
{
sleep(1);
abort();
}
return 0;
}
结果:

3、软件条件产生信号
1)alarm 函数

样例:通过alarm函数体会IO效率
cpp
#include<iostream>
#include<unistd.h>
int main()
{
int count = 0;
alarm(1); // 1秒钟
while(true)
{
std::cout << "count: " << count << std::endl; // IO
count++;
}
return 0;
}
运行结果:

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
std::cout << "count: " << count << std::endl;
exit(0);
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
count++;
}
return 0;
}
运行结果:

可以得出结论:IO会极大的影响效率
2)pause 函数

样例:实现一个基于 SIGALRM 信号的周期性任务调度器
cpp
#include <iostream>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <vector>
using func_t = std::function<void()>; // 函数类型
int gcount = 0; // 被唤醒的次数
std::vector<func_t> gfuncs; // 任务列表
void handler(int sig)
{
// 遍历执行所有任务
for (auto &f : gfuncs)
{
f();
}
std::cout << "fcout: " << gcount << std::endl;
int n = alarm(1); // 重设1s闹钟
std::cout << "剩余时间: " << n << std::endl;
}
int main()
{
std::cout << "我的进程pid: " << getpid() << std::endl;
gfuncs.push_back(
[]()
{
std::cout << "我是一个内核刷新操作" << std::endl;
});
gfuncs.push_back(
[]()
{
std::cout << "我是一个检测进程时间片的操作,如果时间片到了,我会切换进程" << std::endl;
});
gfuncs.push_back(
[]()
{
std::cout << "我是一个内存管理操作,定期清洁操作系统内部的内存碎片" << std::endl;
});
alarm(1); // 1s闹钟
signal(SIGALRM, handler);
while (true)
{
pause();
std::cout << "我醒来了..." << std::endl;
gcount++;
}
return 0;
}
运行结果:

3)什么是软件条件
软件条件就是:由软件层面的状态变化或操作触发信号的场景
比如定时器超时(alarm 函数设定时间到),向已关闭的管道写数据(触发 SIGPIPE 信号),或是 pause 函数让进程阻塞后被信号唤醒。满足这些条件时,系统会给对应进程发信号,提醒进程处理。
4)什么是系统闹钟
系统闹钟就是:操作系统提供的定时能力,让用户可以设置 "在未来某个时间点触发某个操作"。
定时器需要被管理:先描述,在组织。
先描述 :内核用 struct timer_list 结构体来描述一个定时器。
cpp
struct timer_list
{
struct list_head entry;
unsigned long expires; // 超时时间点
void (*function)(unsigned long); // 超时后要执行的回调函数
unsigned long data;
struct tvec_t_base_s *base;
};
再组织 :为了高效管理大量定时器,Linux 内核实际使用时间轮结构,可以把它抽象成一个最小堆,堆顶就是超时时间最短的那个定时器。这样内核可以快速找到 "下一个要触发的闹钟",并在它到期时执行对应的处理函数。

4、硬件异常产生信号
硬件异常就是:硬件检测到错误,通知内核,内核向当前进程发送对应信号
比如:
进程除零 :CPU 运算异常,内核发送 SIGFPE
非法访存 :MMU 异常,内核发送 SIGSEGV
1)模拟除0
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main()
{
signal(SIGFPE, SIG_DFL);
int a = 10;
a /= 0;
while(1);
return 0;
}
运行结果:

2)模拟野指针
cpp
#include <stdio.h>
#include <signal.h>
int main()
{
signal(SIGSEGV, SIG_DFL);
int *p = NULL;
*p = 100;
while (1);
return 0;
}
运行结果:

3)子进程异常退出 core dump

子进程模拟除零:
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
if (fork() == 0)
{
// 子进程
sleep(1);
int a = 10;
a /= 0;
exit(0);
}
// 父进程
int status = 0;
waitpid(-1, &status, 0);
printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);
return 0;
}
运行结果:

由于子进程除零异常,产生的SIGFPE信号默认会终止进程并触发Core Dump

注意:系统默认禁用core文件,可通过 ulimit 命令解限制。示例:ulimit -c 1024
针对上面生成的core文件我们就可以来进行事后调试:gdb ./test core

这样我们就可以直接定位到代码中出错的那一行。
注意 :调试时需要在代码编译的时候带-g选项。