🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:it's 6pm but I miss u already.—bbbluelee
0:01━━━━━━️💟──────── 3:18
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
前言
进程信号的产生
1、通过终端按键产生信号。
OS怎么知道终端按键(即:键盘)有数据了呢?
常见的通过按键产生的信号及其作用
Core Dump
2、调用系统函数向进程发信号。
kill
其他
3、由软件条件产生信号。
4、硬件异常产生信号。
前言
本文书接上回Linux进程信号处理:深入理解与应用(1),主要是对于信号的产生中四种主要的产生方式进行详细的解析。哪四种呢?1、通过终端按键产生信号。2、调用系统函数向进程发信号。3、由软件条件产生信号。4、硬件异常产生信号。
进程信号的产生
1、通过终端按键产生信号。
OS怎么知道终端按键(即:键盘)有数据了呢?
这个主要是通过中断的方式让OS知道的!我们都知道CPU是装在主板上的,而CUP有很多针脚,这些针脚通过对应的线路与主板相连接,而实际上我们的外设也会通过线路与主板相连接。(当然,外设那么多,主要是通过一个主板上的一个叫8259(中断控制器)的来级联)当我们通过终端按键按下按钮的时候,透过导线和主板,CPU会将对应传递的电信号透过一定的标记手段将对应的内容储存到寄存器中。这时OS就知道哪个外设已经就绪了。这时就相当于把硬件就绪转换成了寄存器中的某种数据(也就是中断号)。此时,这些数据就可以被程序读取了。也就是说硬件行为被转换成了软件行为。再接着,OS通过相应的驱动就可以读取对应的数据到内存中处理。最后,根据开机就会启动的一张中断向量表,按特定的读取方法读取数据。大致图示:
常见的通过按键产生的信号及其作用
- SIGINT (2):通常由Ctrl + C产生,这个信号会中断当前进程,常常用来终止正在运行的程序。
- SIGTSTP (20):由Ctrl + Z产生,此信号会使进程暂停并转入后台。之后可以通过fg命令将进程重新带回到前台继续运行。
- SIGQUIT (3):通过Ctrl + \产生,该信号不仅会终止前台进程组中的所有进程,还会生成core文件,用于调试。
Core Dump
在signal 7 手册中有这样一张表,其中Action表示该信号的执行方式,其中Term是terminal的意思表示为终止,而Core也是终止的意思,完整叫Core Dump,叫做核心转储,他会在进程崩溃时会形成以进程pid命名的code.pid文件或者pid.code(不同系统不一样),通常在该进程的同一目录下,将进程崩溃时的核心上下文数据转储到该文件中。
如下为对应执行方式详细解释:
在Linux操作系统中,"core"文件是当进程异常终止时由操作系统生成的。
作用和重要性:
- 调试信息源:它包含了进程崩溃时的内存映像和部分相关的调试信息,这对于开发人员来说是非常宝贵的资料,有助于快速定位问题所在。
- 故障分析:通过分析core文件,可以了解导致程序崩溃的具体原因,比如指针错误、内存泄漏等。
- 重现问题:core文件可以帮助开发人员在没有源代码的情况下重现问题,从而进行更有效的问题修复。
如何生成core文件:
- 信号触发:有多种信号可能导致core文件的生成,例如SIGSEGV(段错误)、SIGFPE(浮点异常)等。当这些信号被发送到进程且未被捕获或忽略时,系统会创建core文件。
- ulimit命令:可以使用
ulimit -c unlimited
命令来确保在进程崩溃时生成core文件。- /proc/sys配置:编辑
/proc/sys/kernel/core_pattern
文件可以设置core文件的命名和存储路径。
如何使用core文件:
- gdb工具:使用gdb加载可执行文件和core文件,可以查看崩溃时的调用栈、变量值等信息,帮助定位问题。
- 其他工具:除了gdb,还有其他一些工具如addr2line、libbfd等也可以用于分析core文件。
存储位置和管理:
- 存储位置:默认情况下,core文件通常存储在与崩溃进程相同的目录下,但可以通过修改
core_pattern
来改变存储位置。- 空间问题:由于core文件可能非常大,它们可能会占用大量磁盘空间。因此,对于磁盘空间有限的系统,需要谨慎管理core文件的生成。
- 大小限制:可以通过
ulimit -c
命令设置core文件的大小限制,防止过大的core文件占用过多磁盘空间。- 禁止生成:如果不需要core文件,可以将
ulimit -c
设置为0或者通过echo "/dev/null" > /proc/sys/kernel/core_pattern
来禁止core文件的生成。
如下例子:
通过ulimit -a 查看对应的code file size,如果为0则不会生成code文件,因此我们通过ulimit -a 需要的大小,来设置使他可以生成code文件:
发生Floating point exception错误,生成对应pid的code文件:
如下是通过gdb与code文件的使用:
2、调用系统函数向进程发信号。
kill
在Linux系统中,kill
函数是用于向进程发送信号的核心系统调用。它允许一个进程影响另一个进程的执行流程,包括终止、暂停或继续执行等操作。以下是kill
函数的详细解释:
函数原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数
pid
:要接收信号的进程的进程ID(PID)。可以是一个正数(表示具体进程),0(表示与当前进程相同的进程组),或者是1(表示所有进程,除了init进程)。sig
:要发送的信号的编号。常见的信号包括SIGTERM
(终止信号)、SIGKILL
(强制终止信号)、SIGSTOP
(停止信号)和SIGCONT
(继续运行信号)等。
返回值
如果成功,
kill
函数返回0;如果失败,它将返回-1,并设置errno
以指示错误。错误
ESRCH
:找不到与指定PID相匹配的进程。EPERM
:调用者没有足够的权限发送信号给目标进程。EINVAL
:提供了无效的信号编号。
如下例子:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid == 0) {
// 子进程中
while (1) {
printf("Child process is running...
");
sleep(1);
}
} else if (pid > 0) {
// 父进程中,发送SIGTERM信号给子进程
kill(pid, SIGTERM);
printf("Signal sent to child process.
");
}
return 0;
}
在这个例子中,父进程通过kill
函数向其子进程发送了一个SIGTERM
信号,导致子进程被终止。
其他
raise、abort在Linux进程信号处理:深入理解与应用(1)有详细介绍。
3、由软件条件产生信号。
我们在介绍管道的时候,我们打开一个管道,其中一端在写一端在读,当我们将读端关闭了后我们再向管道去写,会发现出现了SIGPIPE信号,然后会发现这个进程就结束了。这是因为通过操作系统对于管道读描述符是否被打开了等等,检测了内核数据结构的情况某些字段是否有效来管理进程,在发现读端没了后,进程就被杀死了。很明显,这是通过软件来产生的信号。因为操作系统是软硬件的管理者,信号当然可以由软件产生,也可以由硬件产生!当然,并不是所有的信号都是杀死进程,也有诸如:SIGTSTP暂停信号。以下是一些常见的软件条件产生的信号及其产生方式:
- SIGTERM (15):这是默认的信号,当
kill
命令不带任何参数时会发送这个信号。它通常用于请求进程正常终止,允许进程执行清理操作。- SIGKILL (9):这是一个强制终止进程的信号,不允许进程捕获或忽略。通常,
kill -9
命令用于发送此信号。- SIGINT (2):通常由
Ctrl + C
产生,但也可以通过kill
命令发送。它通常用于中断前台运行的进程。- SIGQUIT (3):可以通过
kill -3
命令发送。与SIGKILL不同,SIGQUIT允许进程在终止前执行清理操作,并且会产生core dump。- SIGHUP (1):通常用于通知进程重新加载配置文件。例如,当修改了Web服务器的配置文件后,可以发送SIGHUP信号使其重新加载配置。
- SIGUSR1和SIGUSR2 (10, 12):这两个信号可以被进程自定义使用,通常用于用户定义的信号处理。
- SIGALRM (14):当定时器到期时,内核会向进程发送SIGALRM信号。
- SIGCHLD (20):当子进程停止或终止时,会向父进程发送SIGCHLD信号。
- SIGIO (29):当I/O操作完成时,内核会向进程发送SIGIO信号。
- SIGWINCH (28):当窗口大小发生变化时,会向进程发送SIGWINCH信号。
下面介绍一下其中比较重要的alarm函数和SIGALRM信号:
函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数
- alarm函数接受一个无符号整数seconds作为参数,表示定时器的时间长度。
返回值
- 如果之前已经设置了闹钟,则alarm函数返回上一个闹钟剩余的秒数;如果没有设置过闹钟,则返回0。在错误情况下,alarm函数可能返回-1,但这通常意味着有错误发生。
它接受一个参数seconds
,表示从当前时刻开始计时,经过这段时间后,内核将向当前进程发送SIGALRM
信号。如果进程没有特别设置对SIGALRM
信号的处理函数,那么默认的动作是终止该进程。此外,alarm
函数还有一些其他重要的特性:
覆盖性
- 如果在
seconds
秒内再次调用了alarm
函数并设置了新的闹钟,原来的闹钟将被覆盖。
常见用途
- 定时任务:可以使用
alarm
函数来设置一个定时器,当达到指定时间后执行某个任务或操作。- 超时检测:可以结合
alarm
函数和信号处理机制,实现对某个操作的超时检测。- 资源释放:在某些情况下,可能需要在一定时间后自动释放某些资源,这时可以使用
alarm
函数来实现。
下面看一个例子:假设我们有一个程序需要在一定时间内完成某个任务,如果超过这个时间还没有完成,就发送一个警告消息并终止程序。我们可以使用alarm
函数来实现这个功能。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout_handler(int signum) {
printf("任务超时!\n");
exit(1);
}
int main() {
// 设置超时时间为5秒
alarm(5);
// 注册超时处理函数
signal(SIGALRM, timeout_handler);
// 模拟一个耗时的任务
sleep(6);
return 0;
}
在这个例子中,我们首先使用alarm
函数设置了一个5秒的定时器。然后,我们注册了一个超时处理函数timeout_handler
,当定时器到期时,这个函数会被调用。最后,我们模拟了一个耗时6秒的任务,超过了我们设置的5秒超时时间,所以程序会输出"任务超时!"并终止。
注意事项:
- 与其他信号处理机制的交互影响:
alarm
函数设置的定时器是基于系统时钟的,而系统时钟可能会受到各种因素的影响,如系统负载、硬件性能等。因此,在使用alarm
函数时,需要注意其精度可能不如实时操作系统中的定时器。- 在不同Linux发行版中的实现差异:虽然
alarm
函数在大多数Linux发行版中都有相同的实现,但在某些特定的发行版中可能会有一些差异。因此,在使用alarm
函数时,最好查阅相关文档以确保其正确性和兼容性。
4、硬件异常产生信号。
硬件是怎么产生信号的呢?当我们在运行时,代码或者某些问题导致硬件出现了异常,CPU可以直接向操作系统发出信号,OS会将发出的信号进行解释,会解释类似于kill(pid_t pid, int sig);,然后向目标进程发送特定的信号。此时就由硬件问题转化成了信号问题,可以让进程直接终止。在Linux系统中,当硬件发生异常时,内核会向进程发送信号。以下是一些常见的硬件异常信号及其解释:
- SIGBUS(7):总线错误。通常由于对齐问题或访问不存在的内存地址导致。
- SIGSEGV(11):段错误。通常是由于访问无效内存地址导致,如空指针解引用、访问已释放的内存等。
- SIGILL(4):非法指令。当程序尝试执行非法或不支持的指令时产生。
- SIGTRAP(5):陷阱指令。当程序执行陷阱指令(如断点)时产生。
- SIGFPE(8):浮点异常。当程序执行浮点运算时发生错误,如除以零、溢出等。
- SIGSTKFLT(16):协处理器栈错误。当协处理器栈溢出或发生其他错误时产生。
- SIGSYS(38):系统调用错误。当程序执行系统调用时发生错误,如参数错误、系统调用号错误等。
- SIGEMT(72):EMT指令错误。当程序执行EMT(Intel扩展内存类型)指令时发生错误。
- SIGPWR(39):电源故障。当系统电源发生故障时产生。
- SIGXCPU(24):CPU故障。当CPU发生错误时产生。
- SIGXFSZ(25):文件尺寸错误。当程序尝试读取文件时,文件尺寸发生变化导致错误。
- SIGVTALRM(26):虚拟定时器到期。当程序设置的虚拟定时器到期时产生。
- SIGPROF(27):性能计数器超时。当程序的性能计数器达到设定值时产生。
- SIGWINCH(28):窗口大小改变。当终端窗口大小发生改变时产生。
- SIGURG(16):紧急数据可用。当网络连接上出现紧急数据时产生。
- SIGIO(29):I/O请求完成。当异步I/O请求完成时产生。
- SIGPENDING(30):挂起信号。当有未处理的信号被捕获时产生。
- SIGPOLL(31):流事件。当流设备上有事件发生时产生。
- SIGRTMIN(32):实时信号最小值。用于自定义实时信号的起始值。
- SIGRTMAX(33):实时信号最大值。用于自定义实时信号的结束值。
下面我们以8号信号为例子看一段代码的运行结果:
我们通过让变量a除0导致浮点异常,再使用signal更改了8号信号的处理方法。通过以上的效果,我们发现程序现了死循环的情况,然而我们在程序中并为有死循环的操作,这是为什么呢?因为当一个进程出现了异常后,我们的操作系统默认是终止这个进程的,把进程杀死,默认就是处理问题的方式之一,为的是让操作系统恢复健康。而如今我们改变了处理方法且并没有退出进程的方法,异常就不会退出了,他会一直存在,CPU只要调度这个进程就会出现异常,然而这个进程也没杀死,进程虽然不会继续往后执行,但是异常还是存在,而CPU调度完其他进程后,再次调度这个进程又会出现这样的情况。本质就是异常没有被终止,进程一直被调度!
PS:Linux浩如烟海,信号还没完!下一篇继续!
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~