文章目录
- 信号的概念
- 信号的产生
- 硬件产生(按键盘中的按键)
- 软件产生
- kill函数
- kill命令
- abort函数
- raise函数
- 捕捉信号后的处理方式
- 默认处理方式SIG_DFL
- 忽略处理方式SIG_IGN
- 自定义信号处理方式
- signal函数
- sigaciotn函数
- 信号的注册
- 进程中的未决信号集(位图)
- 非实时信号(非可靠信号)的注册
- 实时信号(可靠信号)的注册
- 信号的注销
- 非可靠信号的注销
- 可靠信号的注销
- 信号的捕捉流程
- 信号阻塞
- 信号阻塞的理解
- 设置阻塞位图函数
信号的概念
信号是一个程序中断,且是一个软中断,收到一个信号后,具体怎么处理该信号,什么时候处理是由进程决定的,所以是软中断。
信号的种类:使用kill -l
命令可以查看有多少个信号
- 1~31是非可靠信号
- 34~64是可靠信号
- 非可靠信号:当前信号有可能丢失,丢失就无法执行该信号
- 可靠信号:当前信号不可能会丢失的
可以通过man 7 signal
查看信号的具体含义命令
信号的产生
硬件产生(按键盘中的按键)
-
终止进程组合键 ctrl + c
在程序运行过程中我们按下 ctrl + c 就可以中断进程运行
组合键ctrl + c 本质上是SIGINT 信号(2号信号),是一个终止信号,终止正在进行的这个前台进程,该组合键对后台进程没有任何作用
-
暂停进程组合键 ctrl + z
在程序运行过程中我们按下 ctrl + z 就可以暂停进程运行,此时该进程的进程状态是T,意为暂停状态
组合键 ctrl + z 本质上是SIGTSTP信号(20号信号),是一个暂停信号,让正在运行的前台程序暂停运行
-
产生核心转储文件组合键 ctrl + |
组合键 ctrl + | 本质上是SIGQUIT信号(3号信号),是一个结束进程并产生核心转储文件的信号
核心转储文件:核心转储文件中存储的是程序异常终止进程产生的一个文件,进程终止瞬间将程序的内存映像,包括程序代码、变量、堆栈、寄存器状态等,以及导致程序崩溃的错误信息写到这个磁盘文件,其中信息常用于调试寻找程序错误原因
核心转储:是指在计算机程序发生严重错误或崩溃时,操作系统将程序在崩溃瞬间的内存状态和相关信息保存到一个文件中的过程。这个文件被称为核心转储文件(Core Dump File)。核心转储通常在以下情况产生:
程序崩溃
:当程序遇到无法处理的错误,如访问无效内存地址、除以零等,操作系统会生成核心转储文件;异常信号
:当程序收到操作系统发送的某些异常信号,如段错误(Segmentation Fault)或浮点异常(Floating Point Exception),也可能触发生成核心转储文件。当进程异常退出或收到信号退出时,却没有产生核心转储文件,此时可以通过
ulimit -a
命令查看core file size设置情况,如果它被设置为0,我们需要命令ulimit -c unlimited
修改设置值为unlimited
后,进程异常退出后才能产生核心转储文件我们可以用gdb来加载核心转储文件并检查程序的状态和堆栈,找到错误原因,修复bug
gdb调试核心转储文件寻找错误地方:gdb [可执行文件名] [核心转储文件]
一些非法行为对应的信号量:
非法行为 | 信号量 | 信号名 |
---|---|---|
解引用空指针 | 11号信号并产生核心转储文件 | SIGSEGV |
访问越界 | 11号信号并产生核心转储文件 | SIGSEGV |
动态分配空间free两次 | 6号信号并产生核心转储文件 | SIGABRT |
软件产生
kill函数
int kill(pid_t pid, int sig)
功能:kill 函数在操作系统中用于发送信号给进程,以控制和影响其行为。这些信号可以用于各种目的,包括终止进程、重新加载配置、重新启动等。需要注意的是,kill 函数的名称可能会导致误解,因为它实际上并不是用来强制终止进程的专门函数。
一些常见信号编号包括
- 1:SIGHUP(终端挂起)
- 2:SIGINT(中断信号,通常由Ctrl+C发送)
- 9:SIGKILL(强制终止)
- 15:SIGTERM(正常终止)
- 20:SIGTSTP(挂起进程)
头文件:sys/types.h、signal.h
参数:
- pid : 进程标识符,给哪一个进程发送信号
- sig : 信号值,具体发送哪一个信号
返回值:
- 成功:返回信号值
- 失败:返回 -1
kill命令
kill命令可以指定给具体进程发送具体信号量
kill -signal pid
- signal:信号量,可以是具体数值,也可以是信号名字
- pid : 进程标识符
abort函数
void abort(void);
功能:可以向进程发送SIGABRT信号(6号信号),使进程异常终止,并关闭刷新进程打开的流。哪一个进程调用该函数,便向该进程传送SIGABRT信号(6号信号),其实abort内部封装了kill函数
头文件:stdlib.h
raise函数
#include <signal.h>
int raise(int sig);
功能:sig 是要发送的信号编号。调用 raise 函数会向当前进程发送指定的信号。这个函数实际上是一个库函数的封装,其底层实现会调用系统调用来发送信号。
捕捉信号后的处理方式
#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
#define SIG_ERR ((__sighandler_t)-1) /* error return from signal */
默认处理方式SIG_DFL
#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
//SIG_DFL就是 __sighandler_t结构体类型 的0
默认处理方式:SIG_ DFL:操作系统当中已经定义号信号的处理方式了,例如2号信号用于终止进程;11号信号用户终止进程,并且产生核心转储文件
忽略处理方式SIG_IGN
还记得我们之前说僵尸进程子进程先于父进程退出,子进程在退出的时候,会告知父进程,其实就是子进程向父进程发送了一个SIGCHLD信号,但是父进程接收到信息之后是忽略处理,父进程并没有回收子进程的退出状态信息,就是说父进程对SIGCHLD信号的处理方式是忽略处理,从而导致子进程变成僵尸进程。
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
//SIG_IGN就是 __sighandler_t结构体类型 的1
自定义信号处理方式
自定义处理方式,就是让程序猿自己定义某一个信号的处理方式
signal函数
typedef void (*sighandler_t)(int); // void handler(int)
sighandler_t signal(int signum, sighandler_t handler);
功能:当进程接收到了signum信号时,调用handler函数,执行handler函数中的代码,该信号以前需要执行的任务不再执行
头文件:signal.h
参数:
- signum : 信号量值,要处理的信号
- handler : 信号处理句柄,就是一个函数指针(回调函数),当进程接收到了signum这个信号时,进程需要调用handler函数去做一些事先规定好的事情
注意:因为9号信号是强杀信号,不能被自定义处理
sigaciotn函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:自定义信号处理方式函数:程序员定义一个函数去处理接收到的信号
头文件:signal.h
参数:
- signum : 信号量值
- act : 输入型参数,保存 对signum信号 所采取的措施信息
- oldact : 输出型参数,保存 以前对signum信号 所采取的措施信息
sigaction结构体详解:
struct sigaction {
void (*sa_handler)(int);
//保存了内核对信号的处理方式的函数指针:默认处理方式和忽略处理方式
void (*sa_sigaction)(int, siginfo_t *, void *);
//函数指针,保存自定义处理函数,但是没有使用
//当要使用的时候,配合sa_flags一起使用。
//当sa_flags的值为SA_SIGINFO的时候,信号按照sa_sigaction保存的函数地址进行处理
sigset_t sa_mask;
//信号集位图,保存收到的信号。
//当进程在处理信号的时候,如果还在收到信号,
//则放到该信号位图当中,后续再放到进行的信号位图当中
int sa_flags; //填入宏
void (*sa_restorer)(void); //保留字段
};
宏 | 含义 |
---|---|
SA_SIGINFO | 操作系统在处理信号的时候,调用的就是sa_sigaction函数指针当中保存的函数 |
0 | 操作系统在处理信号的时候,调用的就是sa_handler函数指针当中保存的函数 |
内核源码中的sigaction结构体源码:
struct sigaction {
union {
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int, struct siginfo *, void *);
} _u;
sigset_t sa_mask;
int sa_flags;
};
#define sa_handler _u._sa_handler
#define sa_sigaction _u._sa_sigaction
typedef char* __user __sighandler_t;
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
signal函数与sigaction函数的关系:
- signal函数只是修改了sigaction结构体中的_sa_sigaction
- sigaction函数修改了整个sigaction结构体
- signal函数内部调用了sigaction函数
信号的注册
一个进程收到一个信号,这个过程称之为注册,信号的注册和注销并不是一个过程,是两个独立的过程
内核中信号注册位图以及sigqueue队列的的了解:都是task_ struct结构体内部的内容;每一个进程都有自己独有的注册位图和sigqueue队列
进程中的未决信号集(位图)
位图:进程的task_struct中位图的初始定义:
struct task_struct {
...
struct sigpending pending;
...
}
sigpending:内核源码的include\linux\signal.h中sigpending:
struct sigpending {
struct list_head list;//双向链表
sigset_t signal;
};
sigset_t:内核源码的 include\asm-generic\signal.h中定义了sigset_t:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
_NSIG_WORDS:内核源码的 include\asm-generic\signal.h中定义了_NSIG_WORDS:
#define _NSIG 64
#define _NSIG_BPW __BITS_PER_LONG
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
__BITS_PER_LONG:内核源码的 arch\alpha\include\asm\bitsperlong.h中定义了__BITS_PER_LONG:
#define __BITS_PER_LONG 64
最终我们发现:位图就是一个unsigned long sig[1]
,Linux操作系统中long占8个字节,即64位
,每一个信号,在该位图中存在一个与之对应的比特位,当信号对应的比特位为1时,表示当前进程接收到该信号
非实时信号(非可靠信号)的注册
第一次注册:修改sig位图(0->1),修改sigqueue队列
第二次注册:相同信号值的信号,在前一个信号未被处理的前提下:修改sig位图(1->1),并不会添加sigqueue节点,也就是说再次添加,不会添加sigqueue节点
非实时信号容易信号丢失的原因就是再次注册的时候不会添加sigqueue节点
实时信号(可靠信号)的注册
第一次注册:修改sig位图(0->1),修改sigqueue队列
第二次注册:相同信号值的信号:修改sig位图(1->1),添加sigqueue节点到sigqueue队列中
,也就是说,再次添加,会再次添加siquque节点
sigqueue源码:
struct sigqueue {
struct list_head list;
int flags;
siginfo_t info;
struct user_struct *user;
};
信号的注销
非可靠信号的注销
如果信号已经处理完,则将处理完的信号对应位图中的比特位从1变成0,并将该信号的sigqueue节点从sigqueue队列中出队
可靠信号的注销
如果信号已经处理完了,则将该信号的sigqueue节点从sigqueue队列中出队,同时需要判断sigqueue队列中是否还有与出队的该信号相同的sigqueue节点:如果还有相同的sigqueue节点:则不修改位图中的对应比特位;如果没有:将该信号对应位图中的比特位从1变成0
信号的捕捉流程
如果没有收到信号,执行顺序为1->2->3->4
如果收到了信号,执行顺序为1->2->5->6->7->3->4
main函数调用了一个系统调用函数或者调用库函数(库函数底层也是封装的系统调用函数)后,cpu从用户态切换到内核态,内核态调用系统调用函数后想回到用户态需要调用do_signal函数,do_signal函数的功能是检查进程是否接受到信号:
如果接受到信号
,调用sigcb函数去处理信号(默认处理
方式则不需要切换到用户态,直接在内核态进行信号处理;自定义处理
方式需要切换到用户态进行信号处理),信号处理完毕后调用sig_return函数表明信号处理完,再调用do_signal函数检查是否接受到新的信号;如果未接收到信号
,直接调用sys_return函数,让cpu从内核态切换到用户态
信号阻塞
信号阻塞的理解
信号的注册是信号注册, 信号阻塞是信号阻塞。信号的阻塞并不会干扰信号的注册,而是说进程收到这个信号之后,由于阻塞, 暂时不处理该信号 。
task_struct源码中定义了信号阻塞位图和信号注册位图
struct task_struct{
...
sigset_t blocked, real_blocked; // 信号阻塞位图
struct sigpending pending; // 信号注册位图 位于这个结构体内部
...
}
当信号阻塞位图block中对应信号的位为1,表示当前进程阻塞该信号
当进程进入内核状态,准备返回到用户态时,调用do_signal函数时,接收到了一个信号,如果该信号的阻塞位图中的对应位置为1,则不会立即去处理该信号
等到该信号的阻塞位图上的该信号对应位变成1之后才会去处理该信号,可靠信号发送了几次处理几次,非可靠信号发送大于等于1次都是处理1次
设置阻塞位图函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:设置阻塞位图
头文件:signal.h
参数:
-
how:想让sigprocmask函数做什么事情
- SIG_ BLOCK:设置某个信号为阻塞状态
- SIG_ UNBLOCK:设置某个信号为非阻寒状态
- SIG_ SETMASK:替换阻塞位图,用第二个参数“set”,替换原来的阻寨位图
-
set:新替换入的阻塞位图,可以为NULL
-
alodset:原阻塞位图,可以为NULL
SIG_BLOCK设置阻塞原理:按位或,将新的要阻塞的信号加入了阻塞位图中 (原阻塞位图 | 新阻塞位图)
SIG_UNBLOCK解除阻塞原理:按位与,将要解除阻塞的信号的比特位变成0(原阻塞位图 & 新阻塞位图)