Linux操作系统5-进程信号2(信号的4种产生方式,signal系统调用)
上篇文章:Linux操作系统5-进程信号1(信号基础)-CSDN博客
本篇Gitee仓库:myLerningCode/l25 · 橘子真甜/Linux操作系统与网络编程学习 - 码云 - 开源中国 (gitee.com)
本篇重点:信号的4种产生
目录
一. signal系统调用
二. 产生信号的4种方式
2.1 终端按键产生信号
2.2 系统调用/命令产生信号
a kill调用向其他进程发送信号
b raise向自己发送信号
2.3 硬件异常产生信号
a 除 0 异常
b 空指针解引用
2.4 软件产生信号
a pipe 读端退出,写端立马退出
b alarm定时器产生信号
一. signal系统调用
signal系统调用可以帮助我们自定义信号的行为。
//所需头文件
#include <signal.h>
//函数原型 当进程收到signum这个信号之后,执行handler中的代码
typedef void(* sighandler_t)(int) //函数指针
sighandler_t signal(int signum, sighandler_t handler);
//参数说明
signum 需要自定义行为的信号编号
handler 自定义行为的函数
//当某一个进程使用了signal系统调用之后,捕捉signum编号的信号就会执行下面的自定义行为
void handler(int signum)
{
//由程序员自定义
}
举例代码:
我们自定义了2号信号的行为(ctrl c 发送的信号就是2号信号)
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
while(true)
{
printf("进程[%d]收到信号[%d]\n",getpid(),signum);
sleep(1);
}
}
int main()
{
signal(2, handler);
while (true)
{
std::cout << "进程pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
我们定义一个死循环的进程,并且自定义2号信号的行为。如果该进程收到2号信号那么他就会执行handler 中的死循环代码
测试结果如下:
我们输入ctrl c 来测试一下。
可以看到,输入ctrl c之后该进程收到2号信号。并且再次ctrl c 之后仍执行自定义行为的代码
注意:在我们调用signal之后并不会执行handler中的方法,而是在收到2号信号后再调用
二. 产生信号的4种方式
2.1 终端按键产生信号
常见的比如 ctrl c 向当前的前台进程发送2号信号,ctrl \ 向当前前台进程发送3号信号。
代码测试:
我们自定义2号和3号信号的行为来测试 ctrl c 和 ctrl \:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
while(true)
{
printf("进程[%d]收到信号[%d]\n",getpid(),signum);
sleep(1);
}
}
int main()
{
//同时自定义2号信号和3号信号的行为
signal(2, handler);
signal(3, handler);
while (true)
{
std::cout << "进程pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
2.2 系统调用/命令产生信号
命令产生信号我们经常使用,就是 kill 信号 pid 即可向对应的进程发送对应的信号
a kill调用向其他进程发送信号
kill不仅仅在命令中可以发送信号,也能在代码中使用
//头文件
#include <sys/types.h>
#include <signal.h>
//函数原型
int kill(pid_t pid, int signum);
//参数
向 pid 这个进程编号的进程发送 signum 这个编号的信号
//返回值
成功返回0,失败返回-1,并且设置错误码
测试代码:
mykill.cpp
#include <iostream>
#include <sys/types.h>
#include <signal.h>
void Usage(const std::string &proc)
{
std::cout << "Usage\n"
<< proc << "pid signum\n ";
}
int main(int argc, char *argv[])
{
if (argc != 3)
Usage(argv[0]);
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int n = kill(pid, signo);
if(n < 0)
{
std::cout << "kill error"<<std::endl;
}
return 0;
}
该代码通过命令行参数获取键盘输入的信息,解析后执行kill
test.cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
while (true)
{
printf("进程[%d]收到信号[%d]\n", getpid(), signum);
sleep(1);
}
}
int main()
{
// 同时自定义2号信号和9号信号的行为
signal(2, handler);
signal(3, handler);
while (true)
{
std::cout << "进程pid:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
可以看到,我们可以通过kill系统调用向其他进程发送信号
b raise向自己发送信号
#include <signal>
int rasie(int sig);
//给自己发送sig这个信号
测试代码:
通过raise向自己发送3号信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
while (true)
{
printf("进程[%d]收到信号[%d]\n", getpid(), signum);
sleep(1);
}
}
int main()
{
// 同时自定义2号信号和9号信号的行为
signal(3, handler);
int cnt = 0;
while (true)
{
std::cout << "进程pid:" << getpid() << "[" << cnt++ << "]" << std::endl;
if (cnt == 5)
raise(3);
sleep(1);
}
return 0;
}
测试结果:
可以看到,第5次的时候,收到3号信号执行自定义行为。
2.3 硬件异常产生信号
信号不一定由用户发出,也有可能由OS发出。比如我们的 /0操作,越界访问操作。
a 除 0 异常
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
// 3.硬件异常产生信号
// 信号产生,不一定由用户显示发送。有可能由操作系统自动产生
while (true)
{
std::cout << "我正在运行..." << std::endl;
sleep(1);
int a = 10;
a /= 0;
}
return 0;
}
运行结果如下:
可以看到进程收到了 Floating point exception。这个其实就是8号信号
可以自定义8号信号的行为来证明:
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
void catchSig(int signo)
{
std::cout << "获取一个信号编号,编号是:" << signo << std::endl;
}
int main()
{
// 3.硬件异常产生信号
// 信号产生,不一定由用户显示发送。有可能由操作系统自动产生
signal(SIGFPE, catchSig);
while (true)
{
std::cout << "我正在运行..." << std::endl;
sleep(1);
int a = 10;
a /= 0; // 为什么除0 会终止进程? 当前进程会收到来自OS的信号
}
return 0;
}
运行结果如下:
可以看到,该进程收到了8号信号。
可是为什么一直打印这条信息呢?我们没有写死循环
分析如下:
1 OS怎么知道该进程 \0 了?
因为在cpu中有一个状态寄存器,这个寄存器中有一个状态标志位。如果我们有 \0 运算,就会导致结果溢出,此时这个寄存器就会将标志位由 0 设置为 1。说明该进程发生了运算异常。
当OS发现某一个进程的状态标志位是1,就会向其发送8号信号终止它!
2 为什么会一直打印信息?
一个进程不会一直占用CPU。当发送进程调度的时候,这个进程可能会被调走。此时进程会将自己的上下文信息保存到PCB中。当进程切换切换回来的时候,这个进程的状态标志位还是1,OS仍会向其发送8号信号,继续打印这条信息!
b 空指针解引用
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
void catchSig(int signo)
{
std::cout << "获取一个信号编号,编号是:" << signo << std::endl;
}
int main()
{
signal(SIGFPE, catchSig);
while (true)
{
std::cout << "我正在执行代码" << std::endl;
sleep(1);
int *p = nullptr;
*p = 1; //野指针
}
return 0;
}
运行结果如下:
可以看到,显示段错误。收到11号信号(非法访问内存)
原因分析:
我们的指针都是在虚拟内存上的,虚拟内存通过页表和MMU的映射到物理内存上(MMU是集成在CPU上的)。当我们发送非法访问的时候,MMU就会发送硬件异常,OS向进程发送11号信号进行终止。
不断打印的原因如上面。
2.4 软件产生信号
a pipe 读端退出,写端立马退出
在这篇文章中,我们看到。管道的读端退出,写端会收到13号信号退出
Linux操作系统4-进程间通信1(通信与管道实现通信)-CSDN博客
这就是一种软件异常产生的信号
b alarm定时器产生信号
//头文件
#include <unistd.h>
//函数原型
unsigned int alarm(unsigned int seconds);
//使用alarm可以设定闹钟,在输入的参数 seconds 秒之后
//OS会向当前进程发送 SIGALRM 信号,该信号的默认行为是终止该进程
测试代码:
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
void catchSig(int signo)
{
std::cout << "获取一个信号编号,编号是:" << signo << std::endl;
exit(1);
}
int main()
{
alarm(10);
int count = 0;
while (1)
{
std::cout << "hello world! " << count++ << std::endl;
sleep(1);
}
return 0;
}
运行结果:
可以看到10秒后,进程收到14号信号退出
通过alarm定义闹钟我们可以写出很多有用的代码。
任意一个进程都能通过alarm向OS中设置闹钟,OS会周期性检测这些闹钟,当闹钟到了之后OS就会向设置闹钟的进程发送信号。
这种超时的行为,全部是由软件构成的。所以称为 软件条件产生信号