文章目录
- 0.浅谈中断信号
- 1.初识信号
- 2.中断信号
- 3.信号的产生
- 测试:SIGINT
- 4.core dump核心转储
- 5.系统接口产生信号
- 5.1kill给指定发
- 5.2raise向自己发
- 5.3abort自己给自己发6
- 6.由于软件条件不满足产生信号
- 6.1SIGPIPE
- 6.2SIGALRM
- 7. 硬件异常产生信号
- 7.1除零错误
- 7.2野指针/越界访问
0.浅谈中断信号
在Linux系统下,中断信号和中断设备是两个核心概念,它们在系统稳定性、响应速度和进程管理中起着至关重要的作用。
中断信号是一种软件机制,用于通知进程发生了某种特定事件。这些事件可能包括用户操作(如按下Ctrl+C)、硬件故障或系统状态的改变等。当中断信号被触发时,系统会向相关的进程发送一个信号,进程可以选择忽略该信号、执行默认操作或捕获信号并执行自定义的操作。常见的中断信号包括SIGINT(用户按下Ctrl+C时发送)、SIGKILL(强制终止进程)和SIGSTOP(暂停进程的执行)等。通过使用信号处理函数,进程可以在收到信号时执行特定的操作,如保存数据、清理资源等,从而确保系统的稳定性和数据的完整性。
另一方面,中断设备是硬件与操作系统交互的一种方式。当硬件设备需要CPU的注意或处理时,它会发送一个中断信号给CPU。这个信号告诉CPU暂时停止当前正在执行的程序,转而处理该中断请求。每个硬件设备都有一个唯一的中断号,用于在系统内标识和定位相应的中断处理程序。当中断发生时,CPU会根据中断号找到对应的中断处理程序,并执行相应的操作。这样,操作系统就能够有效地管理和调度各种硬件设备,确保它们得到及时的处理和响应。
总的来说,中断信号和中断设备在Linux系统中协同工作,共同实现了事件驱动的处理机制。它们确保了系统在面临各种事件和请求时能够做出及时、准确的响应,从而保证了系统的稳定性和性能。
shell命令:od + 文件:以二进制方式打开一个文件
1.初识信号
信号和信号量是两个东西,没有关系
Linux信号和信号量在操作系统中各自扮演着重要的角色,但它们的目的、功能和使用方式有着明显的差异。
首先,从定义上来看,Linux信号是一种软件中断,用于通知进程某个异步事件的发生。当进程接收到信号时,它会根据信号的类型执行相应的操作。这种通信机制使得进程可以响应各种事件,例如终止、挂起、恢复等。而信号量则是一个特殊的变量,用于控制多个进程对共享资源的访问。信号量的值表示可用资源的数量,进程在访问共享资源前需要先获取信号量,访问完成后释放信号量。
其次,从功能上来看,信号主要用于进程间的异步通信,通知进程某个事件的发生。这种通信方式无需进程之间的显式协调,可以在任何时候由系统或其他进程发送。而信号量则主要用于同步进程对共享资源的访问,防止多个进程同时访问同一资源导致的数据不一致或冲突。
最后,从使用方式上来看,信号的发送和接收通常通过系统调用实现,例如kill函数用于发送信号,而进程可以通过设置信号处理函数来响应接收到的信号。而信号量的操作则包括初始化、P操作(等待)和V操作(信号),这些操作需要原子性地执行以确保数据的一致性。
总的来说,Linux信号和信号量都是进程间通信和同步的重要机制,但它们在定义、功能和使用方式上有所不同。信号主要用于异步通信,而信号量则主要用于同步对共享资源的访问。
生活中,有哪些信号相关的场景呢?
红绿灯 闹钟 王者荣耀 信号转向灯 狼烟
1.为什么认识这些信号呢? 记住了对应场景下的信号 +如何应对这个信号做出相应动作
2.接受过教育,经历过事情能够识别这个信号
3.如果特定信号没有产生,通过以往的经验依旧知道应该做出什么动作
4.收到这个信号的时候,可能不会立即处理这个信号(当下有更重要的事)
5.信号在无法立即被处理的时候,要被临时的记住
什么是Linux信号
本质是一种通知机制,用户 or 操作系统通过发送一定的信号通知进程,某些事件已经发生,可以在后续进行处理
进程如何处理?
a. 进程要处理信号,必须具备信号“识别”的能力(能够接收到信号 + 知道做出什么反应)
b. 进程通过程序员的代码设置能够“识别”信号呢
c. 信号产生是随机的,进程可能正在忙自己的事情,信号可能不是立即处理的
d. 信号会临时记录,方便后续在合适的时候进行处理
e. 一般而言,信号的产生相对于进程而言是异步的
信号如何产生
举例: ctrl+c:本质是通过键盘组合键向目标进程发送2号信号使其退出
信号处理的常见方式
a. 默认(进程自带的,程序员写好的逻辑)
b.忽略 (信号处理的一种方式,不做处理)
c. 自定义动作(捕捉信号)
如何理解组合键变成信号
键盘的工作方式是通过中断方式进行 能够识别组合键如ctrl+c
OS解释组合键得出组合键要传达的信号 -> 查找进程列表 -> 找到前台运行的进程 -> OS写入信号到进程内部的位图结构
如何理解信号被进程保存
进程PCB内部必须具有保存信号的相关数据结构(位图,unisgned int)
信号用位图存储 信号位图存放在进程PCB中 比特位表示哪一个信号 1表示产生 向进程发送信号实际上就是修改对应进程得到信号位图结构
信号发送的本质
信号位图是在task struct->task struct内核数据结构->OS
OS 向 目标进程 写信号,OS直接修改pcb中的指定的位图结构,完成“发送”信号的过程
同步/异步/互斥
在计算机层面,同步、异步、互斥是三个核心概念,它们各自描述了不同的并发或并行操作中的行为特性。以下是对这些概念的简要解释:
同步(Synchronization)
同步指的是多个进程或线程在执行过程中,需要按照某种特定的顺序或条件来协调它们的执行。换句话说,同步确保某些操作或事件按照预期的顺序发生。当多个线程或进程需要共享或访问相同的资源时,同步就变得尤为重要,因为它可以防止数据竞争和不一致。
同步机制通常包括互斥锁(mutexes)、信号量(semaphores)、条件变量(condition variables)等,这些机制用于控制对共享资源的访问,确保在某一时刻只有一个线程或进程可以访问。
异步(Asynchrony)
异步指的是进程或线程的执行不需要按照严格的顺序,或者某些操作可以在没有等待其他操作完成的情况下开始。异步操作不会阻塞调用线程或进程,允许它们继续执行其他任务,直到异步操作完成后再进行处理。
异步编程常用于I/O密集型任务,如文件读写、网络请求等,因为这些操作通常涉及到等待外部系统响应,使用异步可以显著提高程序的响应性和吞吐量。
互斥(Mutual Exclusion)
互斥是同步的一种形式,用于确保在任意时刻只有一个线程或进程可以访问特定的共享资源或代码段。这是通过互斥锁来实现的,当一个线程或进程获得锁时,其他尝试访问该资源的线程或进程将被阻塞,直到锁被释放。
互斥对于保护共享资源免受数据竞争和不一致的影响至关重要。它确保了在任何给定时间,只有一个执行线程可以修改数据,从而维护了数据的一致性和完整性。
简而言之,同步和异步关注的是任务执行的顺序和方式,而互斥则是同步的一种具体实现方式,用于保护共享资源不被同时访问。这些概念在并发编程、多线程、分布式系统等领域中非常重要。
理解信号
信号是在操作系统中用于通知进程发生某个事件的一种机制。它是一种异步的通信方式,可以用于进程间的通信和进程与操作系统之间的通信。
信号的产生:信号可以由多种事件触发,例如用户按下某个特定的键、通过系统调用向进程发信号、软件错误、硬件异常等。当事件发生时,操作系统会向相应的进程发送一个信号。
信号的发送和接收:信号的发送是由操作系统负责的,而信号的接收是由进程负责的。进程具有识别信号并执行相应动作的能力,进程可以通过注册信号处理函数signal来指定对某个特定信号的处理方式。
信号的处理:进程接收到信号后,可以采取不同的处理方式。常见的处理方式包括忽略信号、执行默认的信号处理动作、执行自定义的信号处理函数等。需要注意的是,信号是一种异步的通信方式,进程无法预测信号何时到达。进程可能正在处理优先级更高,更重要的任务,所以信号的处理工作可能不是立即执行的。进程会临时记录下对应的信号,方便后续进行处理。
操作系统提供了一些函数和系统调用来处理信号,例如signal函数用于注册信号处理程序,kill函数用于向进程发送信号。
进程如何记录接收到的信号
在进程PCB内部保存了信号位图字段,用于记录该进程是否收到了对应信号。
信号位图在进程PCB中,而PCB属于内核数据结构,由操作系统进行信号位图的写入。
发送信号的本质
操作系统直接修改目标进程PCB中的信号位图字段,将对应信号的比特位由0置1,完成信号的发送。
常见的信号
SIGHUP:挂起信号(1)
SIGINT:中断信号,通常由Ctrl+C触发(2)
SIGQUIT:退出信号,通常由Ctrl+\触发(3)
SIGILL:非法指令信号(4)
SIGABRT:异常终止信号(6)
SIGFPE:浮点异常信号(8)
SIGKILL:强制终止信号,无法被忽略或捕获(9)
SIGSEGV:段错误信号(11)
SIGPIPE:管道破裂信号(13)
SIGALRM:定时器到期信号(14)
SIGTERM:终止信号,用于正常终止进程(15)
SIGUSR1:用户自定义信号1(10)
SIGUSR2:用户自定义信号2(12)
如何查看信号
- kill -l命令可以查看系统定义的信号列表。[1,31]是普通信号,没有32,33信号,[34,64]是实时信号,共有62个信号
2. man 7 signal命令查看信号的详细描述
为什么没有32,33号信号?
在Linux中,信号是一种在进程间通信的机制,用于通知进程某个事件的发生。在Linux的信号集中,编号为1~31的信号是传统UNIX支持的信号,被称为非实时信号或不可靠信号。这些信号不支持排队,如果多个这样的信号同时发送给进程,它们可能会被合并成一个处理,这可能导致信号丢失。
编号为32~63的信号是后来扩充的,被称为实时信号或可靠信号。这些信号支持排队,即使多个相同的实时信号同时发送给进程,它们也会保持各自的状态,不会被合并,因此不会出现信号丢失的情况。
至于32号和33号信号,它们原本应该是用于实时信号的一部分,但这两个信号(SIGCANCEL和SIGSETXID)被glibc(GNU C Library)内部征用了,用于实现线程的取消。因此,从内核层面来看,32号信号本应是最小的实时信号(SIGRTMIN),但由于被glibc征用,glibc将SIGRTMIN设置成了34号信号。
所以,32号和33号信号并不是标准的实时信号,而是被用于特定的内部目的。因此,在查看或处理Linux信号时,需要注意这两个信号的特殊处理情况。
2.中断信号
- 中断设备比如说8259这样的硬件单元 说CPU的针脚 中断向量表啊 上下文线程保存和线程恢复
- 每一个设备都有自己对应的中断编号 简称中断号 中断号可以理解就是个数组用函数指针的函数指针数组的方式和一张表来对应
- 当一个设备一旦触发了中断 直接通过硬件的方式向计算机比如CPU某些针脚上发送对应的中断信息 中断信息对应的中断号以硬件的方式被写入到特定的寄存器 操作系统识别对应的寄存器内部的中断号 根据中断编号查找自己内置相关的中断处理方法 调用中断的方法完成硬件到软件的行为
- 中断分很多类比如键盘中断
中断里面最重要的有两类中断
一类是网卡中断 网卡发送消息接收消息 硬件上先收到消息 软件上操作系统通过中断去知道数据
第二类中断时钟中断 OS不断收到中断信号去轮询获取诸多信息:某个进程时间片用完没有,哪个进程要调度等等。
信号是被保存下来的 — 保存下来就有他的意义 — 无法立即处理先记录下来有时间再处理
3.信号的产生
捕捉特定信号的函数
当调用signal(signum, handler)时,进程会将其内部的信号处理函数表(或称为信号映射表)中与信号signum相对应的位置设置为handler。**当该进程接收到信号signum时,**操作系统会查找这个表,找到对应的处理函数并执行。如果没有接收到该信号则不做处理。对于handler,如果显示设置了,则调用显示设置的函数,否则调用默认配置的函数。
测试:SIGINT
发送信号的方式
- ctrl+c
- ctrl+/
- kill -SIGINT pid
- kill -2 pid
ctrl+c 已经无法终止,那怎么终止?
开发环境 – 发布环境 – 测试环境 – 生产环境
即:敲代码 – 合并编译 – 测试 – 项目上线(用户可以获取)
现在我们用的云服务器实际上是开发/发布/测试/生产集一体的环境
4.core dump核心转储
0. 一般是为了调试,由OS将当前进程在内存中的相关核心数据,转存到磁盘中
- 一般而言,云服务器(生产环境)的核心转储功能是被关闭的!
- 打开与关闭:当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中
只在当前的会话中打开云服务器core dump
ulimit -c 10240
打开后 kill -8 pid后的不同(形成了一个以pid为后缀的文件)
这个文件有什么用?【试错调试】定位错误代码 显示错误原因
gdb + exe调试模式下:
core-file + core文件
验证进程等待的core dump标记位
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(1);
int a = 100;
a /= 0;
exit(0);
}
int status = 0;
waitpid(id, &status, 0);
cout << "父进程:" << getpid() << " 子进程:" << id
<< " exit sig: " << (status & 0x7F) //0111 1111
<< " is core: " << ((status >> 7) & 1) << endl;
return 0;
}
为什么生产环境一般都是要关闭core dump?
在生产环境中关闭核心转储(core dump)的主要原因有以下几点:
磁盘空间:核心转储文件通常很大,因为它们包含了进程在崩溃时的完整内存映像。如果频繁发生核心转储,这些大文件会迅速占用大量的磁盘空间,可能导致磁盘空间不足,影响系统的正常运行。
隐私和安全性:核心转储文件可能包含敏感信息,如密码、密钥或其他机密数据。如果核心转储文件没有被妥善管理和保护,这些信息可能会被未授权的人员访问,造成安全风险。
性能影响:生成核心转储文件是一个相对耗时的操作,因为它需要复制进程的内存内容到磁盘。在生产环境中,这种性能开销可能是不希望的,特别是在需要快速响应和高吞吐量的场景下。
故障排查:虽然核心转储文件对于调试和故障排查非常有用,但在生产环境中,通常更倾向于使用日志和监控工具来追踪和定位问题。核心转储文件可能不是首选的故障排查手段,因为它们通常包含大量的数据,需要专业的知识和工具来分析。
自动化管理:在生产环境中,系统的稳定性和可靠性是至关重要的。关闭核心转储可以避免因手动管理这些文件而引入的潜在错误和复杂性。
因此,为了保持生产环境的稳定性、安全性和性能,通常会关闭核心转储功能。然而,在开发或测试环境中,开启核心转储可能是一个好的做法,以便在程序崩溃时能够获取详细的调试信息。
5.系统接口产生信号
5.1kill给指定发
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
//传参错误 提示用户传参格式
static void Usage(string proc)
{
cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}
// ./mykill 2 pid
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signumber = atoi(argv[1]);
int procid = atoi(argv[2]);
kill(procid, signumber);
return 0;
}
5.2raise向自己发
5.3abort自己给自己发6
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
int main(int argc, char *argv[])
{
cout << "Start running..." << endl;
sleep(1);
abort();// ==> raise(6) ==> kill(getpid(), 6)
//通常用来终止进程
return 0;
}
代码退出进程的方式
return/exit/_exit/abort
.
怎么理解通过系统调用接口产生信号?
用户调用系统接口 ->OS提取参数(pid,signum)-> OS向目标进程写信号 -> 修改对应进程的信号位图结构 -> 进程后续会处理信号 -> 执行对应的处理动作
6.由于软件条件不满足产生信号
6.1SIGPIPE
管道:读端不进行读的操作并且还关闭了读端,写端一直在写 ⇒ 占用内存/浪费资源/没有意义
0S会通过发送信号自动终止对应的写端进程 ⇒ 13)SIGPIPE
如何验证?
1.创建匿名管道
2.让父进程进行读取,子进程进行写入
3.父子通信一段时间
4.父进程关闭读端 并且调用wait/waitpid等待子进程 子进程一直写入就行
5.OS终止子进程,子进程退出,父进程waitpid拿到子进程的status
6.提取退出信号 ⇒ SIGPIPE
测试代码
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n != -1);
(void)n;
pid_t id = fork();
assert(id != -1);
if (id == 0) // 子进程一直写
{
close(pipefd[0]);
string msg = "I am child";
char send_buffer[128];
while (true)
{
snprintf(send_buffer, sizeof(send_buffer), "%s[%d]", msg.c_str(), getpid());
write(pipefd[1], send_buffer, strlen(send_buffer));
sleep(1);
}
}
close(pipefd[1]);
char receive_buffer[128];
int count = 5;
while (count--)
{
ssize_t s = read(pipefd[0], receive_buffer, sizeof(receive_buffer) - 1);
if (s > 0)
{
receive_buffer[s] = 0; // 字符串自定义约定
cout << "father[" << getpid() << "] get a msg: " << receive_buffer << endl;
}
}
close(pipefd[0]); // 关闭读端
int status = 0;
waitpid(id, &status, 0);
if (WIFEXITED(status))
{
printf("child_pid: %d exit_code: %d\n", id, WEXITSTATUS(status));
}
else
{
printf("child_pid: %d exit_signal: %d core_dump: %d\n", id, status & (0x7f), (status >> 7) & 1);
}
return 0;
}
6.2SIGALRM
调用alarm函数可以设定一个闹钟 告诉内核在seconds秒之后给当前进程发SIGALRM信号 该信号的默认处理动作是终止当前进程。
alarm的返回值
在Linux中,alarm() 函数定义在 <unistd.h> 头文件中。当你调用 alarm() 函数并为其提供一个参数(以秒为单位的时间),它将设置一个定时器,当定时器到期时,会向进程发送一个 SIGALRM 信号。
alarm() 函数的返回值是:
如果之前没有设置任何定时器,那么它返回0。
如果之前已经设置了一个定时器,那么它返回之前定时器的剩余时间(以秒为单位),也就是说,返回的是从当前时间到之前设置的定时器到期时间的剩余秒数。
这个函数通常用于确保某个操作在一定时间内完成,如果超时,则通过 SIGALRM 信号来进行处理。需要注意的是,如果再次调用 alarm(),它会取消之前的定时器并设置新的定时器。
此外,需要注意的是,alarm() 设置的定时器只会在进程接收到其他信号之前被发送,这意味着如果进程长时间阻塞在某个系统调用上(例如 read() 或 sleep()),那么定时器可能会延迟发送,直到系统调用返回。
如果你需要更精确或更复杂的定时器控制,可能需要考虑使用其他机制,如 setitimer() 或 timer_create() 等函数。
alarm返回值的作用
alarm() 函数的返回值在编程中有几个重要的用途:
了解之前的定时器状态:如果之前已经设置了一个定时器,alarm() 的返回值表示那个定时器的剩余时间。这允许你了解之前定时器的状态,以及是否还有足够的时间等待它到期。
避免定时器重叠:如果你打算设置一个新的定时器,但希望避免与之前的定时器重叠,你可以使用 alarm() 的返回值来决定是否需要取消或调整之前的定时器。例如,如果返回值大于你希望设置的新定时器的时长,那么你可以决定不设置新的定时器,因为之前的定时器仍然有足够的时间才会到期。
处理定时器重置:当你再次调用 alarm() 时,之前的定时器会被取消,新的定时器会被设置。返回的剩余时间可以帮助你了解之前的定时器被取消时还有多长时间才会到期,这对于某些需要精确控制时间间隔的应用程序来说可能是重要的。
调试和日志记录:在开发和调试过程中,alarm() 的返回值可以用来记录定时器的状态和变化,这有助于跟踪和理解程序的行为。
决定是否需要处理 SIGALRM 信号:如果 alarm() 返回0,说明之前没有设置定时器,因此你可能不需要为 SIGALRM 信号设置处理函数。如果返回了一个非零值,那么你可能需要准备处理这个信号,尤其是当你的程序逻辑依赖于定时器的到期时。
总的来说,alarm() 的返回值为你提供了关于定时器状态的重要信息,这可以帮助你更好地管理和控制你的程序中的定时器行为。然而,需要注意的是,alarm() 函数相对简单,并且其使用受到一些限制(例如,它只能设置一个定时器,且定时器的精度和可靠性可能不如其他更现代的定时器机制)。在复杂的程序中,可能需要考虑使用更高级的定时器API。
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
//long int or long long int
uint64_t count = 0;
void catchSig(int signum)
{
cout << "进程捕捉到了一个信号" << signum << " Pid: " << getpid()
<< " final count: " << count << endl;
}
int main(int argc, char *argv[])
{
// 验证1s之内 一共会进行多少次count++
// 1. count最大只到10w:cout + 网络远距离发送 = IO
alarm(1); // 这个闹钟一旦触发就自动移除 一次性闹钟
/*
int count = 0;
while (true)
{
cout << "count: " << count << endl;
count++;
}
*/
// 2. 如果单纯向计算算力呢?
signal(SIGALRM, catchSig);
while (true)
count++;
return 0;
}
- sleep()是自己写的代码 要考虑放在哪里以实现轮询操作
闹钟是只要把闹钟先写好 之后的代码块无需增加轮询访问 只用编写测试代码即可 - IO的效率其实非常低 尤其是带上网络(差别在10^3 ) cpu 内存 外设 (纳秒 微秒 毫秒)
定时器的使用与成绩
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <cstring>
using namespace std;
typedef function<void(int)> func;
vector<func> funSet;
uint64_t count = 0;
void showCount(int signum = 0)
{
cout << "进程捕捉到了一个信号" << signum << " Pid: " << getpid()
<< " final count: " << count << endl;
}
void showLog(int test = 0)
{
cout << "日志功能" << endl;
}
void showUser(int test = 0)
{
pid_t id = fork();
if (id == 0)
{
// cout << "current user: "; 线程不安全
char msg[32] = "current user: ";
write(1, msg, strlen(msg));
execl("/usr/bin/whoami", "whoami", (char *)NULL);
exit(1);
}
wait(nullptr);
}
void Timed_Tasks(int test = 0)
{
// flush_data
// 断开连接 用户长时间不登录 再次登录要输入密码
}
// 定时器功能
void catchSig(int signum)
{
for (auto &fun : funSet)
{
fun(signum);
}
alarm(1);
}
void addFun()
{
funSet.push_back(showCount);
funSet.push_back(showLog);
funSet.push_back(showUser);
}
int main()
{
addFun();
signal(SIGALRM, catchSig);
alarm(1); // 这个闹钟一旦触发就自动移除
while (true)
count++;
return 0;
}
如何理解软件条件给进程发送信号
诸多进程会使用诸多闹钟,内核中存在大量闹钟,先描述再组织,闹钟节点链表链接,定时轮询,闹钟到时触发信号。
a. OS先识别到某种软件条件触发或者不满足
b. OS 构建信号,发送给指定的进程
7. 硬件异常产生信号
7.1除零错误
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout << "进程捕捉到了一个信号:" << signum << " Pid: " << getpid() << endl;
//exit(1);
}
int main()
{
signal(SIGFPE, handler);
int a = 100;
a /= 0;
return 0;
}
如何理解除零?
- 执行运算这个操作的是硬件 – CPU
- 程序运行成为进程,执行到“除”这个动作,硬件无法完成除法运算,并产生异常信号。将CPU内部的状态寄存器的溢出标记位置为1,OS识别到计算时有溢出信号,从
task_struct* current
获取当前运行的进程pid,发送SIGFPE给pid,进程做出相应处理。
在联合软硬件的角度来看,除零是一个涉及硬件运算和软件逻辑处理的问题。
首先,从硬件层面来看,当处理器遇到除零指令时,它会尝试执行除法运算。然而,由于除数的值为零,这个运算在硬件层面是无法完成的。因此,硬件会返回一个错误状态或者特定的异常信号,以指示这个操作是无效的。
接下来,从软件层面来看,当操作系统或应用程序捕获到这个由硬件产生的异常信号时,它会根据预设的错误处理机制来响应。这可能包括记录错误信息、终止程序的执行、或者触发一个特定的错误处理函数。具体如何处理取决于软件的设计和实现。
联合软硬件来看,除零操作会导致以下情况发生:
硬件无法完成除法运算,并产生异常信号。
软件捕获到这个异常信号,并根据预设的机制进行错误处理。
这个过程展示了软件和硬件之间的紧密合作。硬件负责执行指令并返回结果或异常状态,而软件则负责处理这些结果或异常,确保系统的稳定性和可靠性。
需要注意的是,除零在数学上是无意义的,因此在实际编程中应该避免这种情况的发生。通过合理的代码设计和错误检查机制,可以确保程序在遇到除零情况时能够做出适当的处理,避免程序崩溃或产生不可预测的结果。
一旦出现硬件异常,进程一定会退出吗
不一定!一般默认是退出,如果不退出,我们也做不了什么
当发生除零错误,进程收到了“float point exception”后的默认处理动作是让进程退出,如果我们在代码里捕获信号,修改自定义行为,不让进程退出,由于溢出标记位仍然为1,此时OS只要识别到“1”,就会向进程发出信号,进程捕获到信号又不退出,如此陷入了死循环。
这个错误是由于OS将计算的结果返回前检测到溢出标记位为1,我们能做的就是在自定义捕获信号里让进程退出并输出错误信息(这实际上就是OS默认的行为,只不过我们为了了解中间过程,故意这么干)
7.2野指针/越界访问
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout << "进程捕捉到了一个信号:" << signum << " Pid: " << getpid() << endl;
//exit(1);
}
int main()
{
/*
signal(SIGFPE, handler);
int a = 100;
a /= 0;
*/
signal(SIGSEGV, handler);
int *p = nullptr;
*p = 100;
return 0;
}
0. 野指针/越界访问 不一定报错!
- 二者必须通过地址,找到目标位置
- 语言上面的地址是虚拟地址
- 将虚拟地址转成物理地址需要页表 + MMU(Memory Manager Unit硬件)
硬件常识
- 几乎任何的外设,常见的硬件,都有可能有寄存器。
- 访问磁盘数据:进程将目标地址放入磁盘寄存器,磁盘读取后去查找。
- 基于外设式的编程—串口式编程,外设有数据/指令寄存器,同样也是驱动程序驱动外设的原理。
野指针和越界访问是编程中常见的错误情况,它们涉及到硬件、软件、页表、内存管理单元(MMU)以及信号等多个层面的交互。下面我将结合这些概念,从硬件和软件的角度解释野指针或越界访问时发生了什么。
野指针和越界访问的基本概念
野指针是指向一个不可知地址的指针,它可能是由于指针未初始化、指针越界访问或指针指向的空间被释放等原因造成的。而越界访问则是指针访问了超出其分配空间范围的内存。
从硬件角度看
在硬件层面,内存是由一系列的地址空间组成的,每个地址对应一个固定的内存单元。当CPU通过指针访问内存时,它会生成一个逻辑地址,这个地址需要经过MMU(内存管理单元)的转换,变成物理地址,才能访问实际的内存单元。
野指针或越界访问时,CPU会尝试访问一个不正确的逻辑地址。由于这个地址是无效的或越界的,MMU可能无法将其转换为有效的物理地址。此时,硬件可能会触发一个异常或中断,通知操作系统发生了错误。
从软件角度看
在软件层面,操作系统通过页表来管理内存。每个进程都有自己的页表,它记录了逻辑页与物理页帧的对应关系。当进程尝试访问一个内存地址时,操作系统会查找页表,找到对应的物理地址,然后完成访问。
对于野指针或越界访问,操作系统在检查页表时会发现请求的地址不在页表的映射范围内。此时,操作系统会触发一个信号(如SIGSEGV,即段错误信号),通知进程发生了错误。这个信号可以被进程捕获并处理,或者如果进程没有设置相应的信号处理函数,操作系统会默认终止进程的执行。
信号与进程通信
信号是操作系统中用于通知进程发生了某个事件或条件的一种机制。当野指针或越界访问发生时,操作系统发送信号给进程,进程可以根据信号的类型和内容来决定如何处理。这种机制允许进程之间进行通信和错误处理。
进程通信与错误处理
进程可以通过信号机制来处理野指针或越界访问的错误。例如,进程可以设置信号处理函数来捕获SIGSEGV信号,并在函数中进行错误处理,如打印错误信息、清理资源或尝试恢复执行。这样,进程可以在遇到错误时采取适当的行动,而不是直接崩溃。
总结
野指针或越界访问是编程中的严重错误,它们会导致硬件层面的内存访问异常和软件层面的信号处理。通过页表、MMU和信号等机制,操作系统能够识别并处理这些错误,保护系统的稳定性和安全性。因此,在编程过程中,我们应该遵循良好的编程规范,避免野指针和越界访问的发生,确保程序的正确性和可靠性。
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者