1、信号概念和系统调用函数signal
例子:
#include <iostream> #include <unistd.h>int main() {while(1){std::cout << "hello" << std::endl;sleep(1);}return 0; }
上面的例子是一个死循环打印的简单例子,用户在Shell下启动这个前台进程,就会一直运行;如果用户在进程运行期间按下 ctrl+c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到这个信号而导致进程退出。
系统调用:signal
signal
是一个用于设置信号处理方式的系统调用,它允许进程指定当特定信号到达时的处理行为。signal函数注册信号处理函数时,实际上是在操作系统内核里设置了一个全局性的信号处理回调,一旦设置完成,每当进程接收到已注册的信号,系统就会自动调用对应的处理函数,无需反复注册。NAMEsignal - ANSI C signal handling SYNOPSIS#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);参数说明: signum:要处理的信号编号(如 SIGINT、SIGTERM 等),实际上是一个整数,一个整数分别代表一个信号 handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法返回值成功:返回之前的信号处理函数失败:返回 SIG_ERR 并设置 errno
其实Ctrl+c的本质就是向前台进程发送 SIGINT 即2号信号,在上面死循环的代码引入signal函数来捕获2号信号:
#include <iostream> #include <unistd.h> #include <signal.h>void handler(int signumber) {std::cout << "我是" << signumber << "信号" << std::endl; }int main() {//signal(2,handler); //SIGINT本质就是一个整数2,表示2号信号signal(SIGINT,handler);while(1){std::cout << "hello" << std::endl;sleep(1);}return 0; }
通过结果可以看出,点击ctrl+c的时候,进程不再退出而是执行handler函数;前面没有使用signal时,发送2号SIGINT信号进程退出是因为这个信号默认信号处理方式是退出进程;使用signal本质就是设置了特定信号的捕捉行为处理方式(并不是直接调用处理动作),当后续特定信号产生,设置的捕捉信号就会被调用(例如上面SIGINT的默认处理方式退出进程被替换成handler方式)。
- signal函数仅仅设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作;如果后续特定信号没有产生,设置的捕捉函数永远不会被调用。
- 一个Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像ctrl+c这样控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下Ctrl+c而产生个信号,也就是说该进程的用
户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控
制流程来说是异步的。
补充:取消前后台进程的方式
- 命令:kill -9 PID
- 命令:fg 作业号,将指定作业号的后台进程拉到前台,然后ctrl+c结束进程
- ctrl + c只能终止前台进程,不能终止后台进程。ctrl+c是键盘组合键,转化成信号发给进程(ctrl+c向前台进程发送信号)。
- 默认给前台进程发送3号信号,默认也是终止的。
补充:创建文件nohup.out,执行命令:nohup ./sig &,将后台进程本该输出到屏幕的数据写入nohup.out而不输出到屏幕
信号概念:信号是进程之间事件异步通知的一种方式,属于软中断。
查看信号:kill -l
每一个信号都有一个编号和一个宏定义名称,这些宏定义可以在对应文件中找到:
1~31号信号是普通信号,其他是实时信号,本章只讨论普通信号。
命令:man 7 signal,查看信号的详细信息
Action:表示信号默认处理动作,例如Term和Core两个都表示退出,但是存在区别(后面介绍)
2、信号处理
可选的信号处理动作有以下三种:
- 忽略此信号,在signal函数的处理动作中设置为SIG_IGN,设置忽略信号的宏。
例如:signal(SIGINT, SIG_IGN); //忽略2号信号的默认处理动作
- 执行该信号的默认处理动作,在signal函数的处理动作中设置为SIG_DFL,设置执行默认动作的宏。
例如:signal(SIGINT,SIG_DFL); //使用2号信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理信号是切换到用户态执行这个处理函数,这种方式称之为自定义捕捉(Catch)一个信号。
例如:上面的代码例子,signal(SIGINT, handler); //使用自定义的信号处理动作。
3、信号产生、信号保存、信号处理
下图是信号的整个历程:信号产生 -> 信号保存 -> 信号处理
3.1 产生信号
产生信号的方式:键盘输入、kill命令、函数(kill,raise,abort)、软件条件满足产生信号、硬件异常产生信号。
通过键盘输入来产生信号
- ctrl+c:产生SIGINT信号(2号信号),默认处理动作为终止进程。
- ctrl+\:产生SIGQUIT信号(3号信号),默认处理动作为终止进程,并生成core dump文件,用于事后调式(后面介绍)。
- ctrl+z:产生SIGSTP信号(20号信号),发送停止信号,默认处理动作为将当前前台进程暂停挂起到后台,并将其放入后台作业列表;进程被暂停挂起后,用户可以通过 fg 号 或者 bg 命令来恢复运行。
$ sleep 100 # 启动一个长时间运行的前台进程 ^Z # 按下 Ctrl+Z [1]+ Stopped sleep 100 $ fg # 恢复进程到前台
理解OS如何得知键盘有数据:
通过调用系统命令kill向进程发信号
使用kill命令,向指定进程发送信号,例如:kill -9 1000(或者是kill -SIGKILL 1000),向PID为1000的进程发送9号信号(SIGKILL信号)。
通过函数kill产生信号
kill命令本质上是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号。
NAMEkill - send signal to a processSYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig); //向指定pid的进程发送指定sig信号RETURN VALUEOn success (at least one signal was sent), zero is returned. On error,-1 is returned, and errno is set appropriately.
例子:使用kill函数来实现自己的kill命令
#include <iostream> #include <sys/types.h> #include <unistd.h> #include <signal.h> #include <string> #include <cctype>int main(int argc, char *argv[]) {if(argc != 3){std::cout << "Usage: " << argv[0] << " -signumber pid" << std::endl;std::cout << "Example: " << argv[0] << " -9 1234" << std::endl;std::cout << "Or: " << argv[0] << " -SIGKILL 1234" << std::endl;return 1;}// 获取pidpid_t pid;try {pid = std::stoi(argv[2]);} catch (...) {std::cerr << "Invalid PID: " << argv[2] << std::endl;return 1;}// 检查信号参数格式是否正确(必须以-开头)if(argv[1][0] != '-'){std::cerr << "Signal must start with '-'" << std::endl;return 1;}// 获取信号名称/数字部分(去掉-后的部分)std::string sig_str = argv[1] + 1;// 判断是数字信号还是名称信号int signum;if(isdigit(sig_str[0])){// 数字信号(如-9)try {signum = std::stoi(sig_str);} catch (...) {std::cerr << "Invalid signal number: " << sig_str << std::endl;return 1;}}else{// 名称信号(如-SIGKILL)// 转换为大写,因为信号名称通常是大写的for(char &c : sig_str) {c = toupper(c);}// 检查是否是"SIG"开头,如果不是则加上if(sig_str.compare(0, 3, "SIG") != 0) {sig_str = "SIG" + sig_str;}// 将信号名称转换为信号编号if(sig_str == "SIGHUP") signum = SIGHUP;else if(sig_str == "SIGINT") signum = SIGINT;else if(sig_str == "SIGQUIT") signum = SIGQUIT;else if(sig_str == "SIGILL") signum = SIGILL;else if(sig_str == "SIGABRT") signum = SIGABRT;else if(sig_str == "SIGFPE") signum = SIGFPE;else if(sig_str == "SIGKILL") signum = SIGKILL;else if(sig_str == "SIGSEGV") signum = SIGSEGV;else if(sig_str == "SIGPIPE") signum = SIGPIPE;else if(sig_str == "SIGALRM") signum = SIGALRM;else if(sig_str == "SIGTERM") signum = SIGTERM;else if(sig_str == "SIGUSR1") signum = SIGUSR1;else if(sig_str == "SIGUSR2") signum = SIGUSR2;else if(sig_str == "SIGCHLD") signum = SIGCHLD;else if(sig_str == "SIGCONT") signum = SIGCONT;else if(sig_str == "SIGSTOP") signum = SIGSTOP;else if(sig_str == "SIGTSTP") signum = SIGTSTP;else if(sig_str == "SIGTTIN") signum = SIGTTIN;else if(sig_str == "SIGTTOU") signum = SIGTTOU;else {std::cerr << "Unknown signal name: " << sig_str << std::endl;return 1;}}// 发送信号int result = kill(pid, signum);if(result == -1) {perror("kill failed");return 1;}std::cout << "Signal " << signum << " sent to process " << pid << std::endl;return 0; }
通过函数raise产生信号
raise函数可以给当前进程发送指定的信号(自己给自己发信号)
NAMEraise - send a signal to the callerSYNOPSIS#include <signal.h>int raise(int sig);RETURN VALUEraise() returns 0 on success, and nonzero for failure
例子:
#include <iostream> #include <unistd.h> #include <signal.h>void handler(int signal) {std::cout << "signal:" << signal << std::endl; }int main() {signal(SIGINT,handler);while(true){//每隔一秒,自己给自己发送2号信号sleep(1);raise(SIGINT);}return 0; }
通过函数abort产生信号
abort函数使当前进程接收到SIGABRT信号而异常终止。
NAMEabort - cause abnormal process termination SYNOPSIS#include <stdlib.h>void abort(void); RETURN VALUEThe abort() function never returns.// 就像exit函数⼀样,abort函数总是会成功的,所以没有返回值
例子:
#include <iostream> #include <unistd.h> #include <signal.h>void handler(int signal) {std::cout << "signal:" << signal << std::endl; }int main() {signal(SIGABRT,handler);while(true){sleep(1);abort();}return 0; }
通过结果可以发现,并没有循环捕捉信号并执行自定义行为,而是执行一次后继续退出,这是因为abort函数默认行为的规定:
发送
SIGABRT
信号 给当前进程。如果信号未被捕获,则终止进程并生成核心转储(core dump)。
如果信号被捕获(如您的代码),则会执行信号处理函数
handler
,但abort()
仍然会终止程序(即使信号被捕获)。这是 POSIX 标准规定的行为,
abort()
不允许程序继续运行,即使信号被捕获。
通过软件条件产生信号
什么是软件条件?
在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。1、SIGPIPE是一种由软件条件产生的信号,在之前关于管道的文章介绍过了,管道写端正常&&读端关闭,OS会直接发送13号信号SIGPIPE杀掉写入的进程。
2、SIGALRM信号和alarm函数:调用 alarm 函数可以设定⼀个闹钟,也就是告诉内核seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程;SIGALRM也是一种通过alarm触发的软件条件产生的信号。
- 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
- 如果设置seconds值为0,表示取消以前设定的闹钟,函数的返回值是以前设定的闹钟时间还余下的秒数。
- 如果设置seconds值非0,表示重新设定的闹钟,函数的返回值是以前设定的闹钟时间还余下的秒数。
NAMEalarm - set an alarm clockSYNOPSIS#include <unistd.h>unsigned int alarm(unsigned int seconds);RETURN VALUEalarm() returns the number of seconds remaining until any previouslyscheduled alarm was due to be delivered, or zero if there was no previ‐ously scheduled alarm.
例子:
#include <iostream> #include <unistd.h> #include <signal.h>void handler(int signal) {std::cout << "signal:" << signal << std::endl; }int main() {signal(SIGALRM,handler);alarm(2);auto n = sleep(1000000);std::cout << "sleep剩余时间:"<< n << std::endl;return 0; }
结果可以看出,alarm产生了信号SIGALRM并被捕捉执行了handler方法,但发现sleep并不是继续休眠,而是继续执行程序然后退出,这是因为:当进程收到信号(如
SIGALRM
)时,sleep
会被中断,并立即返回剩余的睡眠时间(如果被信号中断)。
通过硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
1、模拟除0产生硬件异常:#include <iostream> #include <signal.h>void handler(int signal) {std::cout << "signal:" << signal << std::endl; }int main() {//signal(SIGFPE,handler);int a = 10;a/=0;while(1);return 0; }
如果把注释放开,通过signal捕捉硬件异常发出的SIGFPE信号,可以发现一直执行handler方法:
2、模拟访问野指针异常产生信号:#include <iostream> #include <signal.h>void handler(int signal) {std::cout << "signal:" << signal << std::endl; }int main() {//signal(SIGSEGV,handler);int *p = nullptr;*p = 100;while(1);return 0; }
如果把注释放开,通过signal捕捉硬件异常发出的SIGSEGV信号,可以发现一直执行handler方法:
为什么上述代码中的除0和非法访问内存会导致反复执行handler方法?
上面我们只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也是如此。
同时,也可以发现在C/C++中除零、内存越界等异常,在系统层面上,是被当成信号处理的。
进程终止Term、Core和核心存储
通过前面信号的默认处理方式,进程终止的action有Term和Core两种,两个都表示进程退出,但Core在表示进程退出的同时,还会额外多做一些事情。
Core Dump核心存储:在当前目录下形成文件core(或者core.pid,pid是当前进程的PID,例如core.1241),在进程奔溃的时候,将进程在内存中的部分信息保存在磁盘,方便后续调试(这就是事后调试)。
注意:云服务器一般是关闭core功能的。是否能core取决于两个因素:退出信号是否终止动作是core && 服务器是否开启core功能。
通过命令:ulimit -a,查看core file siz,可以查看core功能是否开启,支持的大小;
通过命令:ulimit -c 数字,可以开启并设置core的数字大小。较新内核都是形成core(多次形成都是同一个core),老的内核是core.pid(多次形成都是不同的core.pid),新内核解决了core可能占用大量空间的问题
#include <iostream> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <string> #include <sys/wait.h>int main() {if(fork() == 0){sleep(1);int a = 10;a /= 0;exit(0);}int status = 0;waitpid(-1,&status,0);printf("exit signal: %d, core dump: %d\n",status&0x7F,(status>>7) & 1);return 0; }
开启core功能后,执行上述代码后,会生成core文件:
通过这个core文件,利用gdb工具进行事后调试:
在gdb中,直接core-file core,直接定位到错误信息;就不需要自己一步一步调试;可以先执行出错,进行核心转储形成core,再进入gdb,使用core-file core定位错误,这就是事后调试。
3.2 信号保存
关于信号的常见概念:
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,作直到进程解除对此信号的阻塞,才执行递达的动作
- 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
信号在内核中的表示示意图
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
- 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
在task_struct源码中关于block、pending、handler的字段:
// 内核结构 2.6.18 struct task_struct {//.../* signal handlers */struct sighand_struct *sighand;sigset_t blocked;struct sigpending pending;//... };struct sighand_struct {atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_t siglock; };struct __new_sigaction {__sighandler_t sa_handler;unsigned long sa_flags;void (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t sa_mask; };struct k_sigaction {struct __new_sigaction sa;void __user *ka_restorer; };/* Type of a signal handler. */ typedef void (*__sighandler_t)(int);struct sigpending {struct list_head list;sigset_t signal; };
通过源码看出,在block和pending的字段中,都有sigset_t的字段。
sigset_t
从上图的信号在内核中的表示来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字=(SignalMask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
信号集操作函数是 POSIX 提供的用于管理信号集合(
sigset_t
)的接口,主要用于信号的阻塞、检查和处理。以下是常用的信号集操作函数及其用法说明:
信号集初始化
int sigemptyset(sigset_t *set)
将信号集set
初始化为空,不包含任何信号。
int sigfillset(sigset_t *set)
将信号集set
初始化为包含所有支持的信号。#include <signal.h>sigset_t set; sigemptyset(&set); // 初始化空信号集 sigfillset(&set); // 初始化包含所有信号的信号集
信号集添加与删除
int sigaddset(sigset_t *set, int signo)
将信号signo
添加到信号集set
中。
int sigdelset(sigset_t *set, int signo)
从信号集set
中删除信号signo
。sigaddset(&set, SIGINT); // 添加 SIGINT 到信号集 sigdelset(&set, SIGTERM); // 从信号集删除 SIGTERM
信号集查询
int sigismember(const sigset_t *set, int signo)
检查信号signo
是否在信号集set
中,返回 1 表示存在,0 表示不存在。if (sigismember(&set, SIGINT)) {printf("SIGINT is in the set\n"); }
信号集应用--读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
修改或检查进程的信号屏蔽字(阻塞信号集)。
how
参数可选:
SIG_BLOCK
:将set
中的信号添加到当前屏蔽字。SIG_UNBLOCK
:从当前屏蔽字移除set
中的信号。SIG_SETMASK
:直接将set
设置为新的屏蔽字。oldset
用于保存旧的屏蔽字(可为NULL
)。sigset_t new_set, old_set; sigemptyset(&new_set); sigaddset(&new_set, SIGINT);// 阻塞 SIGINT sigprocmask(SIG_BLOCK, &new_set, &old_set);// 恢复旧屏蔽字 sigprocmask(SIG_SETMASK, &old_set, NULL);
信号集应用--读取当前进程的未决信号集
int sigpending(sigset_t *set);
sigpending
仅用于检查哪些信号处于未决状态,但不会改变信号的状态。
- 参数:
set
是一个指向sigset_t
类型的指针,用于存储当前待决的信号集。- 返回值:成功时返回
0
,失败时返回-1
并设置errno
。#include <stdio.h> #include <signal.h> #include <unistd.h>int main() {sigset_t pending_signals;// 阻塞 SIGINT 和 SIGQUIT 信号sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGINT);sigaddset(&mask, SIGQUIT);sigprocmask(SIG_BLOCK, &mask, NULL);// 模拟发送信号raise(SIGINT);raise(SIGQUIT);// 检查待决信号sigpending(&pending_signals);if (sigismember(&pending_signals, SIGINT)) {printf("SIGINT is pending\n");}if (sigismember(&pending_signals, SIGQUIT)) {printf("SIGQUIT is pending\n");}return 0; }
等待信号到来
int sigsuspend(const sigset_t *mask)
临时将信号屏蔽字设置为mask
,并挂起进程直到信号到来,此后这个临时的信号屏蔽字mask失效。sigset_t wait_set; sigemptyset(&wait_set); sigsuspend(&wait_set); // 等待信号
信号集注意事项
- 信号集操作函数通常返回 0 表示成功,-1 表示失败。
- 信号屏蔽字仅影响当前线程的信号处理。
- 某些信号(如
SIGKILL
和SIGSTOP
)无法被阻塞或忽略。
3.3 信号捕捉
信号捕捉的流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
例子:
- 用户程序注册了SIGQUIT信号的处理函数sighandler。
- 当前正在执行main函数,这时发生中断或异常切换到内核态。
- 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
- 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
- sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态(为什么不是直接进入main函数?因为第四点说的,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程,这就导致无法return到main函数中)。
- 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
signal和sigaction
signal 的基本用法:
signal
是 POSIX 标准中用于设置信号处理函数的接口,其函数原型如下:#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
signum
是需要捕获的信号编号(如SIGINT
、SIGTERM
),handler
是信号处理函数。以下是一个简单的示例:#include <stdio.h> #include <signal.h> #include <unistd.h>void handler(int signum) {printf("Received signal %d\n", signum); }int main() {signal(SIGINT, handler); // 捕获 Ctrl+C 信号while (1) {sleep(1);}return 0; }
sigaction 的基本用法:
sigaction
提供了更强大的信号处理功能,包括信号掩码和标志控制。其函数原型如下:#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
包含以下关键字段:
sa_handler
:信号处理函数(类似signal
的handler
)。sa_mask
:在信号处理期间阻塞的信号集。sa_flags
:控制信号行为的标志(如SA_RESTART
重启被中断的系统调用)。- 示例代码:
#include <stdio.h> #include <signal.h> #include <unistd.h>void handler(int signum) {printf("Received signal %d\n", signum); }int main() {struct sigaction sa;sa.sa_handler = handler;sigemptyset(&sa.sa_mask); // 初始化信号掩码为空sa.sa_flags = 0; // 无特殊标志sigaction(SIGINT, &sa, NULL);while (1) {sleep(1);}return 0; }
signal 和 sigaction 的区别:
信号掩码控制:
signal
无法在信号处理期间阻塞其他信号。sigaction
可以通过sa_mask
设置信号掩码,避免嵌套信号干扰。行为标志:
signal
的行为依赖实现(如某些系统会重置信号处理为默认行为)。sigaction
通过sa_flags
提供明确的行为控制(如SA_RESTART
或SA_NODEFER
)。可靠性:
signal
在多线程环境中可能不安全。sigaction
是线程安全的,适合现代多线程程序。兼容性:
signal
是传统接口,不同系统表现可能不一致。sigaction
是 POSIX 标准推荐用法,行为一致。
4、操作系统是怎么运行的
4.1 硬件中断
硬件中断流程:
硬件设备 → 发送中断请求(IRQ) → CPU检测并响应 → 保护现场 → 查找ISR → 执行ISR → 恢复现场 → 返回原程序
- 中断向量表就是操作系统的一部分,启动就加载到内存中了
- 通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询
- 由外部设备触发的,中断系统运行流程,叫做硬件中断
4.2 时钟中断
问题:
进程可以在操作系统的指挥下,被调度、执行,那么操作系统自己被谁指挥?被谁推动执行?外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
答:操作系统本质就是一个死循环,时钟源持续触发时钟中断,使操作系统在硬件的推动下,自动进行调度进程。
时间片:本质就是PCB内部的计数器,时钟中断触发中断向量表的 中断服务:进程调度 不一定就是进程切换,而是通过CPU固定的主频 ,使时间片减少时间,判断时间片是否为0再决定是否进行进程切换
4.3 软中断
上述外部硬件中断,需要硬件设备触发。同样,也可以通过软件原因,触发上面的逻辑。
为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 0x80或者syscall),可以让CPU内部触发中断逻辑。
系统调用的过程,其实就是先int 0x80/syscall陷入内核,通过寄存器(如EAX)把系统调用号给操作系统,然后触发中断服务处理软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。系统调用号的本质就是数组下标。
我们用户层调用系统调用,不需要自己走上面的调用流程,Linux的GUN C标准库,把几乎所有的系统调用全部封装了:
缺页中断、内存碎片处理、除零野指针错误,这些问题,全部都会被转换成为CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。
CPU内部的软中断,比如int 0x80或者syscall,叫做陷阱;除零/野指针等,叫做异常。
4.4 理解内核态和用户态
操作系统无论怎么切换进程,都能找到同一个操作系统,换句话说操作系统系统调用方法的执行,是在进程的地址空间中执行的。
5、可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。
- 一个函数,被两个以上的执行流同时进入 -- 重入
- 如果有影响 -- 该函数是不可重入函数
- 如果没有影响 -- 该函数是可重入函数
如果一个函数符合以下条件之一则是不可重入函数:
- 调用了malloc 或者 free,因为malloc也是用全局链表来管理堆的,过程没有原子性
- 调用了标准I/O库函数;标准I/O库的很多实现都是以不可重入的方式使用全局数据结构实现的
6、volatile易变关键字
volatile关键字的作用
volatile
是Linux内核和C语言中用于修饰变量的关键字,主要作用是防止编译器对变量进行优化。它告诉编译器该变量可能被意外修改,因此每次访问时必须从内存中读取,而不是使用寄存器中的缓存值。volatile int counter;
典型应用场景
设备寄存器访问:硬件设备的寄存器值可能随时被硬件改变,使用
volatile
确保每次读取都是真实的寄存器值。volatile unsigned *reg = (unsigned *)0x12345678;
多线程共享变量:在多个线程间共享的变量可能被其他线程修改,需要用
volatile
防止编译器优化。
中断服务程序:中断处理程序修改的变量需要用volatile
修饰,确保主程序能看到最新值。与内存屏障的区别
volatile
仅保证每次访问都从内存读取/写入,但不保证执行顺序。内存屏障(如smp_mb()
)则保证指令的执行顺序。volatile int flag; int data;// 写操作 data = 42; smp_wmb(); flag = 1;// 读操作 while (!flag) ; smp_rmb(); use_data(data);
过度使用
volatile
会影响性能,因为它阻止了编译器优化。在Linux内核中,正确的同步通常需要结合内存屏障和原子操作。
7、SIGCHLD信号
SIGCHLD信号概述:
SIGCHLD是Unix/Linux系统中的一种信号,通常由内核发送给父进程,表示其子进程的状态发生了变化(如终止、停止或继续执行)。该信号默认行为为忽略,但父进程可通过捕获此信号实现异步回收子进程资源(避免僵尸进程)。
主要触发场景:
- 子进程终止(正常或异常退出)。
- 子进程被信号暂停(如SIGSTOP)。
- 子进程因信号恢复执行(如SIGCONT)。
信号处理示例:
以下是一个使用C语言捕获SIGCHLD并回收子进程的示例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/wait.h>void sigchld_handler(int sig) {int status;pid_t pid;while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {if (WIFEXITED(status)) {printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));}} }int main() {signal(SIGCHLD, sigchld_handler);pid_t pid = fork();if (pid == 0) {// 子进程逻辑sleep(2);exit(42);} else {// 父进程等待信号pause(); // 等待信号触发}return 0; }
关键注意事项:
- 避免僵尸进程:通过
waitpid
或wait
系统调用回收子进程资源。- 非阻塞处理:使用
WNOHANG
标志防止父进程阻塞在信号处理函数中。- 多个子进程:信号可能因多个子进程状态变化而合并,需循环调用
waitpid
。
8、pause函数
在 Linux 系统中,
pause
是一个系统调用,其原型定义在<unistd.h>
头文件中。pause
系统调用的主要功能是让调用进程暂停执行,进入睡眠状态,直到该进程接收到一个信号(signal),并且这个信号对应的信号处理函数返回。当接收到信号后,pause
调用会被中断,并且根据信号处理情况可能会执行相应的操作。函数原型
#include <unistd.h> int pause(void);返回值
-1:
pause
调用总是返回 -1,并将errno
设置为EINTR
(表示被信号中断)。因为pause
只有在接收到信号时才会返回,而信号的到来会中断pause
的执行,所以返回 -1 表示调用被信号中断。例子:
#include <stdio.h> #include <unistd.h> #include <signal.h> // 信号处理函数 void signal_handler(int signum) {printf("Received signal %d\n", signum); } int main() {// 注册信号处理函数,当接收到 SIGINT(Ctrl+C)信号时调用 signal_handler 函数signal(SIGINT, signal_handler); printf("Process is pausing, press Ctrl+C to interrupt...\n");// 调用 pause 函数,使进程进入暂停状态int result = pause();if (result == -1) {perror("pause");} printf("Process resumed.\n");return 0; }