文章目录
- 一、Linux信号
- 1. 信号的概念
- 2. 信号的定义
- 3. 系统定义的信号
- 二、信号产生的方式
- 1.通过键盘产生
- 2. 通过系统调用
- 3. 软件条件
- 4. 硬件异常
- 三、信号处理函数
- 1. OS发送信号的实质
- 2. 指令发送信号
- 3. signal()
- 4. sigaction()
- 四、信号屏蔽机制
- 1. 信号处理方式
- 2.信号集操作函数
- 五、临界资源和临界区
Linux 信号是操作系统中的重要组成部分,可以用于进程间通信、处理异常等多种场景。本文将深入介绍 Linux 信号的相关知识,包括信号的定义、类型、发送和接收、处理等内容,帮助读者更好地理解和使用 Linux 信号
一、Linux信号
1. 信号的概念
生活角度的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种
-
- 执行默认动作(幸福的打开快递,使用商品)
-
- 执行自定义动作(快递是零食,你要送给你你的女朋友)
-
- 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
-
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
技术应用角度的信号
- 用户输入命令,在Shell下启动一个前台进程。 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
- 前台进程因为收到信号,进而引起进程退出
2. 信号的定义
信号是 Linux 操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。
信号的作用:
-
进程间通信:进程可以通过向其他进程发送信号的方式进行通信,例如某个进程在完成了某项工作之后,可以向另一个进程发送 SIGUSR1 信号,通知其进行下一步的操作。
-
处理异常:信号可以被用来处理程序中的异常情况,例如当一个进程尝试访问未分配的内存或者除以 0 时,系统会向该进程发送 SIGSEGV 或 SIGFPE 信号,用于处理这些异常情况。
-
系统调试:信号可以用于程序的调试,例如在程序运行时,可以向该进程发送 SIGUSR2 信号,用于打印程序的状态信息等。
3. 系统定义的信号
Linux 中,信号分为标准信号和实时信号,每个信号都有一个唯一的编号。
- 标准信号:最基本的信号类型,由整数编号表示,编号范围是 1 到 31。
- 实时信号:Linux 中的扩展信号类型,由整数编号表示,编号范围是 32 到 64。
可以用kill -l命令可以察看系统定义的信号列表
kill -l
查询结果如下:
常见信号编号以及对应信号的名称:
注意:不同的操作系统可能对信号的编号有所不同,因此在跨平台开发时应当注意信号编号的兼容性。
- term 表示终止进程
- core 表示生成核心转储文件,核心转储文件可用于调试
- ignore 表示忽略信号
- cont 表示继续运行进程
- stop 表示停止进程(注意停止不等于终止,而是暂停)
相关信号的解读:
2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
4) SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
5) SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。
6) SIGABRT
调用abort函数生成的信号。
7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
10) SIGUSR1
留给用户使用
11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
12) SIGUSR2
留给用户使用
13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
14) SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
17) SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
18) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
19) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
20) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
21) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
22) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
23) SIGURG
有"紧急"数据或out-of-band数据到达socket时产生.
24) SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
25) SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。
26) SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
27) SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
28) SIGWINCH
窗口大小改变时发出.
29) SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.
30) SIGPWR
Power failure
31) SIGSYS
非法的系统调用。
- 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
- 不能恢复至默认动作的信号有:SIGILL,SIGTRAP
- 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
- 默认会导致进程退出的信号有:
SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM - 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
- 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
- 此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。
二、信号产生的方式
1.通过键盘产生
Ctrl + C
通过键盘敲入 Ctrl + C ,可以向进程发送 2 号信号使其终止。
2. 通过系统调用
① kill
#include<signal.h>
#include<sys/types.h>
int kill(int pid,int signal); //通过函数向指定进程发送信号
② raise
raise接口可以向当前调用进程发送任意信号
可以给当前进程发送指定的信号,即自己给自己发信号
#include <stdio.h>
#include <signal.h>
raise(11); //向当前进程发送 11 号信号
③ abort
使当前进程接收到信号而异常终止
#include <stdio.h>
#include <stdlib.h>
abort();
3. 软件条件
alarm
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数
打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
#include <stdio.h>
#include <unistd.h>
int main()
{
int ret = alarm(20);
while (1) {
printf("I am a process, ret = %d\n", ret);
sleep(5);
int res = alarm(0); //取消闹钟
printf("res = %d\n", res);
}
return 0;
}
4. 硬件异常
#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);
return 0;
}
这里出现野指针异常。系统本该直接清除掉该进程,但因为修改了野指针异常所对应的操作函数,导致系统只打印了一句话,而不正常的对该进程进行清除。但系统不断地检测到有野指针异常。但操作系统做的工作仅仅是打印一句话。那么这里就造成了循环打印catch a sig : xxx
三、信号处理函数
1. OS发送信号的实质
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过OS向目标进程发送信号。产生信号的方式,其实都是OS发送信号数据给task_struct
struct task_struct 中有进程的各种属性,那么其中也一定有对应的数据变量,来保存是否收到了对应的信号,而信号的编号也是有规律的1~31
进程中采用 uint32_t sigs ;——位图结构来标识该进程是否收到信号
0000 0000 0000 0000 0000 0000 0000 0000
比特位的位置(第几个)代表的就是哪一个信号,比特位的内容(0或1),代表的就是是否收到了信号
故本质是OS向指定进程的task_struct中的信号位图写入比特1,即完成信号的发送,也可以说是信号的写入
struct task_struct
{
//信号位图
0000 0000 10...
uint32_t sigmap;
}
2. 指令发送信号
-
kill 命令
kill 命令是 Linux 中最常用的发送信号的命令,语法如下:
kill [-signal] PID
其中,-signal 可选参数表示要发送的信号类型,如果省略该参数,则默认发送 SIGTERM 信号。PID 表示接收信号的进程 ID。
例如,要向进程 ID 101080 发送 SIGINT 信号,可以执行以下命令:
kill -SIGINT 101080 kill -2 101080
当然可以发送对应的信号编号
-
kill 函数
除了使用 kill 命令,程序中也可以通过 kill 函数来发送信号。kill 函数的原型如下:
int kill(pid_t pid, int sig);
其中,pid 表示接收信号的进程 ID,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回 -1 并设置 errno。
例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下代码:
#include <signal.h> #include <unistd.h> int main() { pid_t pid = 123; int sig = SIGINT; if (kill(pid, sig) == -1) { perror("kill"); return 1; } return 0; }
3. signal()
signal 函数可以用来自定义信号捕捉函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//示例
void handler(int signum)
{
cout<<"get a signum: "<<signum<<endl;
}
int main()
{
signal(14,handler); //此时进程收到14号信号的默认处理动作就是执行handler函数
}
signal 函数的两个宏:
signal(2,SIG_DFL); //default 使对应的2号信号恢复默认动作
signal(2,SIG_IGN); //ignore 忽略二号信号
-
参数:
- signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
- handler:sighandler_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设 置为 SIG_IGN 或 SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。
- sighandler_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数 上,此时就可通过此参数来判断当前触发的是哪个信号。
-
返回值:此函数的返回值也是一个 sig_t 类型的函数指针
- 成功:返回值则是指向在此之前的信号处理函数;
- 出错:则返回 SIG_ERR,并会设置 errno。
由此可知,signal()函数可以根据第二个参数 handler 的不同设置情况,可对信号进行不同的处理。
4. sigaction()
在Linux中,sigaction函数是用于设置和检索信号处理器的函数。
sigaction函数有以下语法:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
其中,signum 表示要注册的信号编号,act 是一个指向 struct sigaction
结构体的指针,表示新的信号处理函数和信号处理选项,oldact 是一个指向 struct sigaction
结构体的指针,用于获取之前注册的信号处理函数和信号处理选项。
struct sigaction 结构体的定义如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
sa_handler:指定信号处理函数的地址。如果设置为SIG_IGN,则表示忽略该信号。如果设置为SIG_DFL,则表示使用默认处理器,也可以自己设置需处理的函数逻辑。
-
sa_sigaction:指定一个信号处理器函数,这个函数包含三个参数:一个整数表示信号编号,一个指向siginfo_t结构体的指针,和一个指向void类型的指针。
-
sa_mask:指定了在执行信号处理函数期间要阻塞哪些信号。
-
sa_flags:是一个标志位,可以包括以下值:
- SA_NOCLDSTOP:如果设置了该标志,则当子进程停止或恢复时不会生成SIGCHLD信号。
- SA_RESTART:如果设置了该标志,则系统调用在接收到信号后将被自动重启。
- SA_SIGINFO:如果设置了该标志,则使用sa_sigaction字段中指定的信号处理器。
-
sa_restorer:是一个指向恢复函数的指针,用于恢复某些机器状态。
返回值:如果成功,则返回0,否则返回-1,并设置errno错误号。可以使用以下代码来检查errno
注意:一般情况下,sa_handler 和 sa_mask 使用较多,这里再次详细说明:
-
sa_handler:
-
赋值为常数SIG_IGN表示忽略信号
-
赋值为常数SIG_DFL表示执行系统默认动作
-
赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数
该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
-
-
sa_mask:
- 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
- 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号, 则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
注意:信号在被处理前,pendding位图就修改了对应的比特位
四、信号屏蔽机制
1. 信号处理方式
-
信号的处理方式有三种,称为信号的递达
- 信号的忽略( SIG_IGN ),即不采取任何动作
- 信号的默认,使用系统默认的处理方法
- 信号的自定义捕捉,采用用户定义的方法处理
-
信号从产生到递达之间的状态,称为信号的未决( Pending )
- 信号在位图中,未被处理(未决)
-
进程选择阻塞( Block )某个信号
- 未决之后,暂时不递达,直到解除对信号的阻塞,期间对该信号为屏蔽
信号在内核中的表示示意图:
-
block 信号阻塞表
在该表中标记为 1 的信号将被阻塞
-
pending 信号未决表
信号被进程接收,但还未被处理的信号,就存放在该表中
- 普通信号:只在pending表记录一次,如果产生多次,也只记录一次,之后系统对该信号的处理动作为 1 次
- 实施信号:采用队列的形式,记录历史中产生的所有信号,后系统对该信号的处理动作为 1历史中产生的次数
注意:pendding 表存在多个信号时,会全部处理完,才返回用户端
-
bandler 信号对应处理方法表
该表中记录了信号对应操作方法的函数指针。
2.信号集操作函数
头文件:
#include <signal.h> //头文件
-
sigemptyset
int sigemptyset(sigset_t *set);
初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
-
sigemptyset
int sigemptyset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
-
sigfillset
int sigfillset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
-
sigaddset
int sigaddset (sigset_t *set, int signo);
添加屏蔽信号
-
sigdelset
int sigdelset(sigset_t *set, int signo);
删除屏蔽信号
-
sigismember
int sigismember(const sigset_t *set, int signo); //查询是否有该屏蔽信号 示例: sigset_t set oset; sigismember(pending , int signo)
查询是否有该屏蔽信号
-
sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
示例:
sigset_t set, oset; sigprocmask(SIG_BLOCK, &set, &oset);
- 参数:
-
how:
SIG_BLOCK //添加屏蔽字 SIG_UNBLOCK //删除 SIG_SETMASK //直接用新的替换
-
const sigset_t :
当前需要修改的 sigset_t
-
oset:
传出之前的pending码
使用示例:
int main() { sigset_t set, oset; // 初始化,全置空 // sigfillset 全置1 sigemptyset(&set); sigemptyset(&oset); // 设置屏蔽字 // sigdelset 清除 sigaddset(&set, 2); // 对于set位图,对2号信号设置屏蔽字 // 将 set 信号设置进入当前进程 sigprocmask(SIG_BLOCK, &set, &oset); // oset为之前的屏蔽字 }
-
sigpending
int sigpending(sigset_t *set); 示例: sigset_t pending; sigemptyset(&pending);
- 功能:读取当前进程的未决信号集,通过set参数传出。调用成功则返回 0 ,出错则返回 -1
示例:
//通过此中方式读出 void Printsignal (const sigset_t &pending) { for(int signo = 31; signo > 0; signo--) { if(sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << "\n"; }
五、临界资源和临界区
-
临界资源
- 概念:被保护起来的公共资源,一次仅允许一个进程使用的共享资源。其他都是非临界资源
-
临界区
-
概念:每个进程中访问临界资源的那段程序(代码)称之为临界区。
-
临界区不是内核对象,而是系统提供的一种数据结构,程序中可以声明一个该类型的变量,之后用它来实现对资源的互斥访问。
当欲访问某一临界资源时,先将该临界区加锁(若临界区不空闲则等待),用完该资源后,将临界区释放。
-
补充(待定):分类:临界区也是代码的称呼,所以一个进程可能有多个临界区,分别用来访问不同的临界资源。
-
内核程序临界资源:系统时钟
-
普通临界资源:普通I/O设备,如打印机(进程访问这些资源的时候,很慢,会自动阻塞,等待资源使用完成)
-
-
-
信号量
表示资源数目的计数器。每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量。资源其实就是对信号量计数器进行自减操作,本质上只要自减成功。就完成了对资源的预定机制。如果申请不成功,执行流将被挂起阻塞。
-
进程进入临界区的调度原则
- 如果有若干进程请求进入空闲的临界区(空闲即0进程访问),一次仅允许 一个进程进入。
- 任何时候,处于临界区内的进程不可多于一个(0 或 1),若已有进程进入自己的临界区,则其它想进入自己临界区的进程必须等待。
- 进行临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
- 如果其它进程不能进入自己的临界区,则应让出 CPU,避免进程出现 “忙等” 现象。
- 访问临界资源时先访问信号量资源(可以理解为信号量引用计数自减1,如果自减成功,说明还能访问,未达到上限,允许访问,访问结束后信号量自增1)
- 临界资源的访问是原子的
-
细节问题
-
每个进程都得先看到同一个信号量资源,就只能由OS提供IPC体系。
-
信号量本身也是公共资源。
-
单个信号量
struct sem { int count; //引用计数 task_struct *wait_queue; //进程等待队列 } //如果当前引用计数不为零,那么新的进程将会直接被运行。 //如果当前引用计数为零,那么新的进程将会加入等待队列。当别的进程结束时,引用计数自增,同时等待队列中的进程将会被执行,同时引用计数自减
-