文章目录
- 1. 信号的概念
- 2. 信号的分类
- 1) 标准信号 (Traditional/Standard Signals)
- 2) 实时信号 (Real-time Signals)
- 3. 信号的处理
- 4. 理解信号处理
- 5. 信号产生
- 1) 键盘输入
- 2) kill命令
- 3) kill函数及系统调用
- 4) 软件条件
- 5) 硬件异常
- 6. 信号保存
- 6.1 信号其他相关常见概念
- 6.2 信号在内核中的表示
- 6.3 sigset_t
- 6.4 信号集操作函数
- 1) 初始化sigset_t信号集函数
- 2) 读取或更改未决信号集(pending)
- 3) 读取或更改阻塞信号集函数(block)
- 7. 信号捕捉
- 7.1 信号捕捉流程
- 7.2 sigaction函数
- 8. 结语
Linux进程信号是一种进程间通信的机制,它允许一个进程通知另一个进程某个事件已经发生。以下是关于Linux进程信号的详细介绍:
1. 信号的概念
信号就像是一个突然的电话铃声,它会打断正在进行的程序并引起其注意。在Linux系统中,信号是一种软件中断,它通常是异步发生的,用来通知进程某个事件已经发生。每个信号都有一个唯一的编号和一个宏定义名称,这些宏定义可以在signal.h中找到。
- 使用
kill -l
命令查看信号编号:
- 查看信号宏定义:
2. 信号的分类
在Linux中,信号被分为标准信号(也称为传统或不可靠信号)和实时信号。它们的主要区别在于编号范围、处理方式以及特性。
1) 标准信号 (Traditional/Standard Signals)
这些信号是早期Unix系统定义的,编号通常从1到31(尽管某些系统可能会有所不同)。以下是一些常见的标准信号:
SIGHUP
(1): 终端挂起或控制进程结束。
1号信号,当用户退出终端时,由该终端开启的所有进程都会接收到这个信号,默认动作为终止进程。但也可以捕获这个信号,比如wget能捕获SIGHUP信号并忽略它,以便在退出登录后继续下载。
SIGINT
(2): 中断信号,通常是Ctrl+C产生的。
2号信号,程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。
SIGQUIT
(3): 退出信号,产生核心转储。
3号信号,和SIGINT类似,但由QUIT字符(通常是Ctrl+\)来控制。进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。
SIGILL
(4): 非法指令。SIGTRAP
(5): 跟踪陷阱(由调试器使用)。SIGABRT
(6): 调用abort()函数生成的信号。SIGBUS
(7): 总线错误。SIGFPE
(8): 浮点异常。SIGKILL
(9): 强制终止信号(不可被捕获、阻塞或忽略)。
9号信号,用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。
SIGSEGV
(11): 段违例。SIGPIPE
(13): 管道破裂。SIGALRM
(14): 定时器到期。SIGTERM
(15): 终止请求。
15号信号,程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,允许进程做一些必要的清理工作后退出。
2) 实时信号 (Real-time Signals)
实时信号是在POSIX.1b标准中引入的,用于提供更可靠的信号机制。它们的编号范围从SIGRTMIN
到SIGRTMAX
,具体数值取决于操作系统实现。一般情况下,这个范围是从34开始直到系统的最大信号数。例如,在许多Linux系统上,SIGRTMIN
对应的是34,而SIGRTMAX
可以达到64或者更高。
实时信号的特点包括但不限于:
- 不会丢失:如果多个相同的实时信号发送给同一个进程,所有信号都会被接收。
- 支持排队:每个类型的实时信号可以有一个队列来存储未处理的信号实例。
- 有序性:实时信号按照发送顺序处理。
- 可携带数据:可以通过
sigqueue()
发送附加的数据(一个整数或指针)。
请注意,实际的信号编号可能根据不同的系统架构和版本有所变化。此外,对于实时信号,应当使用SIGRTMIN + n
和SIGRTMAX - n
这样的形式来引用,而不是直接使用具体的数字值,以确保兼容性和正确性。
本章只讨论编号31以下的信号,不讨论实时信号。
3. 信号的处理
在Linux中,信号处理是进程对特定事件响应的一种机制。信号处理有三种方式:
✨方式一:执⾏该信号的默认处理动作
- 使用命令
man 7 signal
查看信号在什么条件下产⽣,默认的处理动作是什么:
✨方式二:忽略此信号
可以通过设置信号处理器(也就是信号处理函数来实现):
- 信号处理器(Signal Handler)
信号处理器是一个函数,它在进程接收到指定信号时被调用。你可以为每个信号设置一个自定义的处理器,函数如下:
#include <signal.h>void (*signal(int signum, void (*handler)(int)))(int);
参数signum表示要设置的信号编号,参数handler表示要设置的信号处理函数。signal函数会返回上一个信号处理函数的指针,如果出错则返回SIG_ERR。
- 忽略信号
可以将信号处理器设置为 SIG_IGN 来忽略某些信号。但是,不能忽略像 SIGKILL 和 SIGSTOP 这样的不可捕获信号。代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{signal(SIGINT,SIG_IGN);//将2号信号忽略while(true){std::cout<<"PID:"<<getpid()<<" I am waiting a signal."<<std::endl;sleep(1);}return 0;
}
结果如下:
2号信号的默认处理动作是程序终止(interrupt),但是由于我们使用信号处理器忽略了该信号,所以输入Ctrl+c没有终止程序。
✨情况三:设置自定义处理方式
和忽略信号相同我们也是使用信号处理器来实现,实例代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
void Handler(int signo)
{std::cout<<"PID:"<<getpid()<<" Get a signal:"<<signo<<std::endl;//当接受到SIGINT也就是2号信号会执行打印动作,而非终止进程
}
int main()
{signal(SIGINT,Handler);//对2号信号设置自定义处理动作while(true){std::cout<<"PID:"<<getpid()<<" I am waiting a signal."<<std::endl;sleep(1);}return 0;
}
结果如下:
对于信号默认处理动作,我们也可以使用信号处理器来实现:signal(SIGINT/*2*/, SIG_DFL);
使用SIG_DFL即可选择默认处理动作,结果如下:
注意:signal方法只需要设置一次,那么在这个进程中,只要接受到被设置的信号就会执行Handler方法;此外如果没有产生被设置的信号,Handler方法就永远不会被执行。也就是说signal方法类似的设置了一种机制,当有对应的信号产生时就触发,没有就不触发。
4. 理解信号处理
对于信号的处理我们可以分别通过软件和硬件这两个视角来理解,由于硬件比较麻烦,设计操作系统运行原理、时钟中断、死循环等,所以这里不做解释。
- 软件
我们以发送2号信号——从键盘输入Ctrl+c为例,当键盘将输入的Ctrl+c信息交给操作系统后,操作系统就会将信号发送给对应的进程,然后进程就会在合适的时候处理信号。
注意,进程处理信号不是立即处理,而是在合适的时机,因为当前进程可能在处理自己的事。
当进程接收到信号时,是如何记录是哪个信号从而执行相应的信号处理任务呢?
- 我们发现本次学习的信号是1 ~ 31,连续的数字,那么只需选择位图即可使用最小的空间记录下完整的1 ~ 31个数字;
- 只需在进程PCB中定义一个无符号整型,使用32个比特位记录31个信号,比特位的位置代表信号的编号,比特位设为1代表收到该位置的信号,为0代表没有。
也就是说操作系统给进程发送信号就是将进程PCB中记录信号的位图对应位置的信号比特位由0置1,然后进程在合适的时候发现自己收到了信号,执行对应处理动作。
接下来为了保证条理,将采⽤如下思路来进⾏阐述:
5. 信号产生
在Linux系统中,信号(signal)是一种异步通知机制,用于通知进程发生了某些事件。进程可以接收到多种类型的信号,并且可以对这些信号进行处理或者忽略。以下是几种产生信号的常见方法:
1) 键盘输入
Ctrl+C
:发送SIGINT给前台进程,通常用于终止一个进程。Ctrl+\
:发送SIGQUIT给前台进程,类似于SIGINT但会生成核心转储文件。Ctrl+Z
:发送SIGTSTP给前台进程,暂停进程的执行。
2) kill命令
- 使用
kill
命令可以向指定的进程发送信号,默认情况下是发送SIGTERM信号请求进程正常终止。可以通过-l
选项列出所有可用的信号,通过-s
或直接跟信号编号来指定发送的信号类型。kill -9 <pid> # 发送SIGKILL信号,强制终止进程 kill -15 <pid> # 发送SIGTERM信号,请求进程正常终止
3) kill函数及系统调用
kill()
函数: 可以直接从程序内部发送信号给其他进程。
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
参数说明:
- pid:要发送信号的进程ID
- sig:要发送的信号编号
该函数的返回值为0表示成功,返回-1表示失败。
所以我们可以根据kill系统调用来封装一个自己的kill命令,代码如下:
#include<iostream>
#include <sys/types.h>
#include <signal.h>int main(int argc,char* argv[])
{if(argc!=3){std::cout<<"you should print: ./mykill -signo -pid"<<std::endl;//提示输入格式return 1;}int signo = std::stoi(argv[1]);//字符串转整型pid_t pid = std::stoi(argv[2]);int n = ::kill(pid,signo);if(n < 0){perror("kill");return 2;}return 0;
}
结果如下:
所以其实kill命令本质也是通过系统调用来实现的。
raise
函数:用于向调用它的进程自身发送信号。
int raise(int sig);
参数sig为要生成的信号编号。常见的信号编号包括SIGINT(中断信号,通常由终端键盘输入产生)、SIGABRT(终止信号,由abort函数产生)等。
abort
函数:abort函数用于异常终止程序。当调用该函数时,程序会立即退出,并生成SIGABRT信号。
void abort(void);
4) 软件条件
- 当特定的软件条件发生时,比如子进程结束(SIGCHLD),文件描述符准备就绪(SIGIO),可能会触发信号的产生。
- 例如
alarm
定时器:用于设置一个定时器,可以在指定的时间后产生一个SIGALRM信号。
#include <unistd.h>unsigned int alarm(unsigned int seconds);
- 参数seconds指定了定时器的时间间隔,单位为秒。
- 该函数返回当前的闹钟定时器的剩余秒数。如果没有闹钟定时器正在运行,则返回0。
使用代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{::alarm(1);//设置1s的定时器int count = 0;while(true){count++;//统计服务器1s可以将计数器累加到多少printf("count:%d\n",count);}return 0;
}
结果如下:
我们发现服务器1s也就计算4万多次,有点慢,这其实是因为打印count数值时,进行了IO交互,会影响速度;所以我们可以通过signal信号处理器来优化,代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>int gcount = 0;void Handler(int signo)
{printf("count:%d\n",gcount);//只在1s后打印总的计算次数exit(1);//直接退出
}
int main()
{signal(SIGALRM,Handler);::alarm(1);while(true){gcount++;}return 0;
}
结果如下:
可以看到服务器1s大概计算了6亿多次
- 闹钟返回值:返回当前的闹钟定时器的剩余秒数。如果没有闹钟定时器正在运行,则返回0。测试代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>int main()
{int n = ::alarm(4);//在此之前没有闹钟在运行,返回0std::cout<<n<<std::endl;sleep(1);int m = ::alarm(0);//0:表示取消闹钟,m表示剩余秒数std::cout<<m<<std::endl;return 0;
}
结果如下:
- 闹钟原理:
其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。操作系统需要对定时器进行管理,使用结构体对它先描述再组织,其结构体大概包含:
struct timer
{int who;task_struct *t;//表示哪个进程设置的闹钟uint64_t expired;//过期时间struct timer *next;//定时器可以有多个,通过链表连接起来func_t f;//到期执行的函数//...
}
当定时器到期后,操作系统就会根据它的func_t f
函数,执行对应的方法,比如给目标进程发送SIGALRM
信号。此外我们可以理解定时器在操作系统中连接的数据结构为小堆,每次都是堆顶元素先到期,这样操作系统就不需要每次都遍历定时器查找是否有过期的定时器。
也就是说定时器到期时可以理解为软件条件就绪,操作系统就会给对应进程发送信号,所以软件条件当作信号产生的方式之一。
- 使用闹钟完成定时器功能:
根据闹钟定时作用,我们可以使用信号处理器使得闹钟响起时进程自动去完成某些任务,代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<vector>
#include<functional>using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;//使用vector管理相关任务void Handler(int signo)
{for(auto& f : gfuncs)//遍历任务执行f();std::cout<<"gcount:"<<++gcount<<std::endl;//记录执行次数alarm(1);
}
int main()
{gfuncs.push_back([](){std::cout<<"我是一个内核刷新操作..."<<std::endl;});gfuncs.push_back([](){std::cout<<"我是一个检测进程时间片的操作..."<<std::endl;});gfuncs.push_back([](){std::cout<<"我是一个检测进程时间片的操作..."<<std::endl;});signal(SIGALRM,Handler);//信号处理器自定义设置闹钟响起时执行的任务::alarm(1);//设置1s后执行任务while(true)//防止进程退出{pause();//暂停等待信号到来}return 0;
}
结果如下:
注意闹钟设置是一次性的,所以在Handler执行方法中要再设置闹钟,不然就会暂停在那。
5) 硬件异常
硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。
- 除0异常:
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a /= 0;while (1);return 0;
}
- 野指针异常:
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}
int main()
{signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while (1);
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。
但是我们发现⼀直有8/11号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信号的现象。访问⾮法内存其实也是如此,⼤家可以⾃⾏实验。
以上就是信号产生的五种方法,包括键盘输入、命令行产生、kill函数及系统调用、软件条件及硬件异常;但是无论产生信号是何种方式,发送信号的永远是操作系统!!!这是因为操作系统是进程的管理者。
6. 信号保存
6.1 信号其他相关常见概念
- 信号递达(Delivery):实际执⾏信号的处理动作称为信号递达
- 信号未决(Pending):信号从产⽣到递达之间的状态,称为信号未决
- 阻塞:进程可以选择阻塞 (Block )某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。
6.2 信号在内核中的表示
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动作。如图所示:
信号产⽣时,内核在进程控制块中设置该信号的未决标志,表示接收到信号,直到信号递达(也就是说信号处理完成)才清除该标志。
在上图的例⼦中,SIGHUP
信号未产生(pending
表SIGHUP
信号标志位为0),也未阻塞(block
表SIGHUP
信号标志位为0)。SIGINT
信号产⽣过,但正在被阻塞,所以暂时不能递达。SIGQUIT
信号未产⽣过,⼀旦产⽣SIGQUIT
信号将被阻塞,无法执行它的处理动作(也就是无法递达)。
如果SIGHUP
信号产生时,操作系统会将SIGHUP
信号发送给进程,进程pending
表SIGHUP
标志位就会由0置为1,进程在将自己的事情处理完后查看信号表,发现收到了SIGHUP
信号而且没被阻塞,进而执行SIGHUP
的handler
方法——SIG_DEF表示默认处理方法。
如果SIGINT
解除阻塞,那么进程在查看信号时就会执行SIGINT
的处理方法——SIG_IGN
(忽略)。
如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?
- POSIX.1允许系统递送该信号⼀次或多次。
- Linux是这样实现的:常规信号在递达之前产⽣多次
只计⼀次
,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。
6.3 sigset_t
从上图来看,pending
表中每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产⽣了多少次;阻塞标志(block
表)也是这样表示的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t
来存储, sigset_t
称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态。
在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞, ⽽在未决信号集中“有 效”和“⽆效”的含义是该信号是否处于未决状态。
sigset_t的底层实现是一个整数类型,使用位操作来设置和获取各个信号的状态。阻塞信号集也叫做当前进程的信号屏蔽字这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。
6.4 信号集操作函数
有了信号的操作类型——信号集sigset_t
之后,我们就可以通过它对pending
表和block
表进行操作,函数如下:
1) 初始化sigset_t信号集函数
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数
sigemptyset
初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。 - 函数
sigfillset
初始化set
所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系 统⽀持的所有信号。 - 注意,在使⽤
sigset_ t
类型的变量之前,⼀定要调⽤sigemptyset
或sigfillset
做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset
和sigdelset
在该信号集中添加或删除某种有效信号。 - 这四个函数都是成功返回0,出错返回-1。
sigismember
是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
有了上述函数,我们就可以定义完一个信号集之后进行添加或删除等初始化操作。
2) 读取或更改未决信号集(pending)
#include <signal.h>
int sigpending(sigset_t *set);
- 读取当前进程的未决信号集(pending),通过set参数传出。
- 调用成功则返回0,出错则返回-1
3) 读取或更改阻塞信号集函数(block)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:若成功则为0,若出错则为-1
- 如果oset是非空指针,则读取进程的当前信号屏蔽字(阻塞信号集)通过oset参数传出
- 如果set是非空指针,则 更改进程的信号屏蔽字
- 参数how指⽰如何更改。
- 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
- 假设当前的信号屏蔽字为mask,下表说明了how可选参数及其含义:
how参数 | 参数使用说明 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀
个信号递达。
下⾯是使用上述函数的示例代码:
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
//打印pending表
void PrintPending(sigset_t &pending)
{std::cout << "cur process[" << getpid() << "]pending: ";for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo))//如果pending表中有signo信号返回1std::cout << 1;elsestd::cout << 0;}std::cout << "\n";
}void handler(int signo)
{std::cout << signo << " 号信号被递达!!!" << std::endl;std::cout << "-------------------------------" << std::endl;sigset_t pending;sigpending(&pending);//获取当前pending表PrintPending(pending);//打印pending表std::cout << "-------------------------------" << std::endl;
}
int main()
{ signal(2, handler); // 0.⾃定义捕捉2号信号// 1. 屏蔽2号信号sigset_t block_set, old_set;sigemptyset(&block_set);//将自定义block_set设为0sigemptyset(&old_set);//将自定义old_set设为0sigaddset(&block_set, SIGINT); // 我们有没有修改当前进⾏的内核block表呢???1 0// 1.1 设置进⼊进程的Block表中sigprocmask(SIG_BLOCK, &block_set, &old_set); //SIG_BLOCK->block_set = mask|oldset 真正的修改当前进⾏的内核block表,完成了对2号信号的屏蔽!int cnt = 10;while (true){// 2. 获取当前进程的pending信号集sigset_t pending;sigpending(&pending);// 3. 打印pending信号集PrintPending(pending);cnt--;// 4. 解除对2号信号的屏蔽if (cnt == 0){std::cout << "解除对2号信号的屏蔽!!!" << std::endl;sigprocmask(SIG_SETMASK, &old_set, &block_set);}sleep(1);}
}
结果如下:
没有看懂的同学,可以回顾一下信号未决与阻塞的概念以及相关操作函数。
7. 信号捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
7.1 信号捕捉流程
由于信号处理函数的代码是在用户空间的,处理过程⽐较复杂,举例如下:
- 用户程序注册了
SIGQUIT
信号的处理函数sighandler
。 - 当前正在执行
main
函数,这时发生中断或异常切换到内核态。 - 在中断处理完毕后要返回用户态的
main
函数之前检查到有信号SIGQUIT
递达。 - 内核决定返回用户态后不是恢复
main
函数的上下⽂继续执行,而是执行sighandler
函数,sighandler
和main
函数使用不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。 sighandler
函数返回后自动执行特殊的系统调用sigreturn
再次进⼊内核态。- 如果没有新的信号要递达,这次再返回用户态就是恢复
main
函数的上下文继续执行了。
我们之前说过信号处理不是立即的,而是选择合适的时间,所以进程在进行信号处理的时间段如下图:
也就是说当接收到信号会被存储在pending表中,此时不会立即处理,等到进程从用户态与内核态切换时再检查pending表看是否有信号要处理。
我们可以简单理解:
- 用户态:执行我自己写的代码
- 内核态:执行操作系统的代码
7.2 sigaction函数
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
-
sigaction
函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回0,出错则返回- 1。 -
signo
:指定要操作的信号编号,例如 SIGINT、SIGTERM 等。 -
act
:指向sigaction
结构体的指针,根据act修改该信号的处理动作。如果不需要改变当前的信号处理方式,则可以设置为 NULL。 -
oact
:指向sigaction
结构体的指针,通过oact传出该信号原来的处理动作。如果不需要保存当前的信号处理方式,则可以设置为 NULL。 -
sigaction
结构体:
struct sigaction {void (*sa_handler)(int); // 指定信号处理函数或特殊值 SIG_IGN 或 SIG_DFLvoid (*sa_sigaction)(int, siginfo_t *, void *); // 用于替代 sa_handler 的扩展信号处理器sigset_t sa_mask; // 额外的信号屏蔽字,在执行信号处理器期间阻止其他信号int sa_flags; // 特殊标志,影响信号传递行为void (*sa_restorer)(void); // 不再使用,应设为 NULL
};
- 将
sa_handler
赋值为常数SIG_IGN
传给sigaction
表示忽略信号; - 赋值为常数
SIG_DFL
表⽰执⾏系统默认动作; - 赋值为⼀个函数指针表示用自定义函数捕捉信号,或者说向内核注册了⼀个信号处理函数,该函数返回值为
void
,可以带⼀个int参数——通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是⼀个回调函数,不是被main
函数调用,而是被系统所调用。
代码示例如下:
#include<iostream>
#include<signal.h>
void handler(int signo)//自定义处理动作
{std::cout<<"get a signal:"<<signo<<std::endl;
}
int main()
{struct sigaction act,oact;act.sa_handler=handler;//将sa_handler赋值为一个函数指针,自定义2号信号处理动作::sigaction(2,&act,&oact);//oact保存的是2号信号原来的处理动作while(true){::pause();//等待}return 0;
}
结果如下:
其实和信号处理器功能一样,只是sigaction函数提供了对信号处理更精确的控制,相比于 signal 函数来说更为安全和灵活。
8. 结语
我们从信号定义、分类、处理谈到信号产生、信号保存最后到信号捕捉,关键在于信号处理的理解、相关的信号处理函数、信号保存的三张表——pending表、block表和handler表以及信号捕捉的理解与运用。总之,Linux进程信号是一种强大且灵活的进程间通信机制。通过合理地使用信号,可以实现进程间的异步通知、同步和通信等功能。以上就是Linux进程信号有关的内容啦~ 完结撒花~ 🥳🎉🎉