文章目录
2 信号的产生
2.1 键盘组合键
像ctrl+c
:2号信号
ctrl+\
:3号信号
注意,不是所有的信号都能被signal()
函数捕捉,如19号信号,9号信号
2.2 命令和函数调用
2.2.1 kill命令
可以在bash中使用,也可以使用下面的系统调用函数
KILL(2)
NAME
kill - send signal to a process
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
模拟实现一下kill命令
// myKill.cc
int main(int argc, char* argv[])
{
if(argc != 3) {
cout << "error!" << endl;
exit(1);
}
int sigNum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, sigNum);
if(n == -1) {
perror("kill");
exit(2);
}
return 0;
}
2.2.2 raise()函数
raise(3)
NAME
raise - send a signal to the caller
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <signal.h>
int raise(int sig);
相当于kill(getpid(), sig)
void MyHandler(int sigNo)
{
if(sigNo == 2) {
cout << "Signal 2 two has been captured" << endl;
exit(2);
}
}
int main()
{
// 捕捉该信号,使用SIGINT或者2都可以
// signal(2, MyHandler);
signal(SIGINT, MyHandler);
int cnt = 5;
while(cnt--) {
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
raise(SIGINT);
return 0;
}
2.2.3 abort()函数
abort(3)
NAME
abort - cause abnormal process termination
LIBRARY
Standard C library (libc, -lc)
相当于给自己发6号信号 6) SIGABRT
void MyHandler(int sigNo)
{
// if(sigNo == 2) {
// cout << "Signal 2 two has been captured" << endl;
// exit(2);
// }
cout << "Signal has been captured" << endl;
}
int main()
{
// 捕捉该信号,使用SIGINT或者2都可以
signal(SIGABRT, MyHandler);
int cnt = 5;
while(cnt--) {
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
abort();
raise(SIGINT);
return 0;
}
我们发现,MyHandler()函数并未将该进程退出,而进程却退出了。
当我们使用bash的kill命令时,该进程并未退出。所以该函数有自己的自定义行为(上图已将myProcess程序改为死循环)
2.3 硬件异常
2.3.1 除0异常
// mySignal.cc
void handler(int sigNum)
{
cout << "Get a signal, num is " << sigNum << endl;
}
int main()
{
signal(SIGFPE, handler);
int a = 10;
cout << "before div" << endl;
sleep(5);
a /= 0;
cout << "after div" << endl;
return 0;
}
2.3.2 空指针异常
void handler(int sigNum)
{
cout << "Get a signal, num is " << sigNum << endl;
}
int main()
{
signal(SIGSEGV, handler);
int *p = nullptr;
cout << "before point error" << endl;
sleep(5);
*p = 1234;
cout << "after point error" << endl;
return 0;
}
可见,进程收到异常信号后,并没有主动退出,而是一直在刷屏打印。所以说捕捉完异常,打印完消息后,最好让该进程退出,使用exit()
。
关于一直打印的原因,看2.3.3和2.3.4,由于handle()
方法并没有退出进程,所以进程一直被调度,每次调度的时候,都会有硬件问题,进程无法执行后序代码,就会一直打信息。
2.3.3 OS如何感知这些异常–除0异常
一些预备知识:
CPU里面有一个状态寄存器(Program Status Word, PSW),用于存储当前处理器的状态信息。这些信息对于控制CPU的操作和行为至关重要。通常包含的状态标志位如下:
- 进位标志(Carry Flag):
- 用于表示算术运算中的进位或借位情况。在加法运算中,如果结果超出了寄存器的大小,进位标志会被设置。
- 零标志(Zero Flag):
- 当算术或逻辑运算的结果是零时,该标志被设置。
- 符号标志(Sign Flag):
- 用于表示最近一次算术运算结果的符号。在有符号数运算中,如果结果是负数,符号标志会被设置。
- 溢出标志(Overflow Flag):
- 用于表示算术运算中的溢出情况。当运算结果超出了寄存器的表示范围时,溢出标志被设置。
- 奇偶校验位(Parity Flag):
- 用于表示最近一次运算结果的最低有效字节中1的个数。如果1的个数是偶数,则奇偶校验位被设置。
- 辅助进位标志(Auxiliary Carry Flag):
- 在某些CPU架构中,辅助进位标志用于表示在BCD(二进制编码的十进制)运算中的进位情况。
- 中断使能标志(Interrupt Enable Flag):
- 控制CPU对中断的响应。如果该标志被设置,CPU将允许中断;如果被清除,则CPU将忽略中断请求。
- 方向标志(Direction Flag):
- 在某些CPU架构中,方向标志用于确定字符串操作(如串行比较或复制)的方向。
- 陷阱标志(Trap Flag):
- 在某些CPU架构中,陷阱标志用于控制异常或陷阱的生成。
PC(Program Counter):PC是一个通用术语,用于描述大多数CPU架构中用于存储下一条指令地址的寄存器。
EIP(Extended Instruction Pointer):EIP是x86架构CPU中的一个寄存器,用于存储下一条将要执行的指令的内存偏移地址。它是在实模式和保护模式下使用的。在64位的x86-64架构中,EIP被扩展为RIP(Register Instruction Pointer),是一个64位寄存器。
在讨论x86架构时,我们使用EIP(或RIP在x86-64架构中);而在讨论其他架构时,我们可能会使用PC来描述类似的功能。
进程上下文,是指操作系统在某一时刻为执行当前进程所准备和维护的一组数据和状态信息。上下文包括所有CPU寄存器的值、程序计数器、程序状态字、堆栈指针、内存管理信息、I/O状态等,这些信息共同定义了当前进程的执行状态。
进程上下文切换发生在CPU从执行一个进程切换到执行另一个进程时。上下文切换涉及保存当前进程的上下文,加载新进程的上下文,然后将控制权传递给新进程。上下文切换是操作系统多任务处理机制的核心部分,允许多个进程共享同一台机器的CPU资源。
上下文切换包括但不限于以下步骤:
- 保存当前进程的寄存器和状态信息。
- 更新当前进程的内存管理信息。
- 将控制权转交给操作系统的调度器。
- 调度器选择另一个进程执行。
- 加载新选中进程的上下文到CPU。
- 恢复新进程的执行,从上次离开的地方继续运行。
CPU也是硬件,OS是硬件的管理者,当出现异常时,OS有权利也有义务知道
2.3.4 OS如何感知这些异常–空指针异常
CPU上集成了一个MMU单元
MMU(Memory Management Unit,内存管理单元)是计算机体系结构中的一个重要组件,负责管理CPU与内存之间的交互。MMU的主要功能包括:
- 地址转换:MMU将程序使用的逻辑地址或虚拟地址转换为物理地址,这个过程称为地址翻译。这使得多个进程可以同时运行在同一个物理内存上,而不会相互冲突。
- 内存保护:MMU可以为不同的内存区域设置保护属性,确保进程不能访问不属于它的内存区域,从而防止内存破坏和数据泄露。
- 内存访问控制:MMU控制对内存的访问权限,例如区分可读、可写和可执行权限,以及用户模式和内核模式的访问权限。
- 分页管理:在支持分页的系统中,MMU使用页表来管理虚拟地址到物理地址的映射。页表包含了虚拟页和物理页之间的对应关系。
- 缓存管理:MMU与CPU缓存协同工作,管理数据的缓存和一致性,确保CPU访问的是最新和正确的数据。
- 内存分配:在某些系统中,MMU还参与内存的分配和回收,例如在分页系统中,MMU可以跟踪哪些内存页正在使用,哪些是空闲的。
- 异常处理:当发生内存访问违规时,如访问不存在的页面或违反访问权限,MMU会触发异常或中断,操作系统将捕获这些异常并进行处理。
- 透明扩展:在32位系统中,MMU可以提供对更大物理内存的支持,即使CPU的地址总线不足以直接寻址所有物理内存。
CPU也是硬件,OS是硬件的管理者,当出现异常时,OS有权利也有义务知道
2.4 软件条件
2.4.1 13)SIGPIPE信号
看[该文章](进程间通信-CSDN博客的2.6,上面介绍了,当管道的读端关闭,写端便会变得没有意义,OS会给写端发一个13)SIGPIPE信号
2.4.2 14)SIGALRM信号
看下面的函数
alarm(2) System Calls Manual
NAME
alarm - set an alarm clock for delivery of a signal
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
DESCRIPTION
alarm() arranges for a SIGALRM signal to be delivered to the
calling process in seconds seconds.
If seconds is zero, any pending alarm is canceled.
In any event any previously set alarm() is canceled.
alarm()将SIGALRM信号以秒为单位发送给调用进程。
如果seconds为0,则任何待处理的告警将被取消。
在任何情况下,之前设置的alarm()都会被取消。
RETURN VALUE
alarm() returns the number of seconds remaining until any
previously scheduled alarm was due to be delivered, or zero if
there was no previously scheduled alarm.
Alarm()返回任何先前计划的闹钟所剩余的秒数,如果没有先前计划的闹钟,则返回零。
void handler(int sigNum)
{
cout << "Get a signal, num is " << sigNum << endl;
exit(1);
}
int main()
{
signal(SIGALRM, handler);
alarm(5); // 5s后发信号
while(true) {
cout << "Process is running" << endl;
sleep(1);
}
}
当我们去掉上面第4行的exit(1);
后,会:
发现并没有一直刷屏,因为闹钟不是异常,响了就没有然后了。
2.4.3 用闹钟让程序每隔一段时间打印一段话
void work()
{
cout << "this is log..." << endl;
}
void handler(int sigNum)
{
work();
alarm(5);
}
int main()
{
signal(SIGALRM, handler);
alarm(5); // 5s后发信号
while(true) {
cout << "Process is running" << endl;
sleep(1);
}
return 0;
}
2.4.4 闹钟的返回值
void work()
{
cout << "this is log..." << endl;
}
void handler(int sigNum)
{
work();
int n = alarm(5);
cout << "上一个闹钟的剩余时间" << n << endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(100);
int cnt = 0;
while(true) {
cout << "Process is running" << endl;
cnt++;
sleep(1);
if(cnt == 5)
raise(SIGALRM);
}
}
2.5 core Dump
2.5.1 引言
使用man 7 signal
命令
看到Action里有Term和Core
之前在学习进程等待时,有这个图
core dump标志就是用来标志被信号所杀的进程所收到的信号的Action
是Term
还是Core
2.5.2 代码和结果
现在有下面的代码
pid_t id = fork();
if(id == 0) {
// child
while (true)
{
cout << "I am child, pid is " << getpid() << endl;
sleep(1);
}
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) {
cout << "child quit info\nrid: " << rid << " exit signal: " << (status & 0x7f)
<< " core dump:" << ((status >> 7) & 1) << endl;
}
运行这个进程,分别使用Action为Term
和Core
的终止这两个进程
可以看到,即便使用了3号Action为Core的信号,core dump仍然是0
这是因为在云服务器中,core功能默认是关闭的
2.5.3 设置core file size
可以使用ulimit -c [size]
来设置大小
设置完成之后再来实验
不但core dump被设置,而且生成了一个文件
打开系统的core dump(核心转储)功能,一旦进程出异常,OS会将进程在内存中的运行信息,给我dump(转储)到进程的当前目录(磁盘)
形成core.pid文件
2.5.4 作用以及如何使用
作用就是当代码出现运行时错误时,可以详细知道是哪一行,出现了什么错误
使用的时候需要g++的参数,加上-g,告诉GCC产生能被GNU调试器使用的调试信息,以调试程序
修改一下程序
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int a = 10;
int b = 0;
a /= b;
cout << "a=" << a << endl;
return 0;
}
当出现运行时错误时,这种方式可以直接定位到出错行。先运行,再调试,这属于事后调试