在 Linux 操作系统中,信号是一种进程间通信的机制,用于通知进程发生了某个事件。信号可以由内核、其他进程,或者进程自身发送。每个信号都对应一个特定的事件或异常,例如进程终止、Ctrl+C 中断等。
本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。非常不建议使用信号进行进程间通信。
通过 kill -l 命令可以察看系统定义的信号列表:
# 执行shell命令查看信号
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
常见的 Linux 信号:
abort
函数,用于异常终止进程。通过Linux提供的 man 文档可以查询所有信号的详细信息:
# 查看man文档的信号描述
$ man 7 signal
在信号描述中介绍了对产生的信号的五种默认处理动作,分别是:
Term:信号将进程终止
Ign:信号产生之后默认被忽略了
Core:信号将进程终止, 并且生成一个core文件(一般用于gdb调试)
Stop:信号会暂停进程的运行
Cont:信号会让暂停的进程继续运行
关于对信号的介绍有一句非常重要的描述:
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
9号信号和19号信号不能被 捕捉, 阻塞, 和 忽略
- 9号信号: 无条件杀死进程
- 19号信号: 无条件暂停进程
在 Linux 中,信号可以有三种基本状态:未决(Pending)、阻塞(Blocked)、以及默认状态。这些状态描述了信号的当前处理情况。
kill
函数:
用于向指定进程或进程组发送信号。
#include <signal.h>
int kill(pid_t pid, int sig);
参数 pid
表示进程或进程组的 ID,sig
表示要发送的信号编号。
raise
函数:
用于向当前进程自身发送信号。
#include <signal.h>
int raise(int sig);
参数 sig
表示要发送的信号编号。
abort
函数:
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include <stdlib.h>
void abort(void);
alarm
函数alarm
函数是一个定时器函数,用于在指定时间后产生 SIGALRM
信号。它允许程序员设置一个定时器,当定时器超时时,内核将向进程发送 SIGALRM
信号。通常,SIGALRM
的默认操作是终止进程。
以下是 alarm
函数的基本形式:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds
参数表示设置的定时器时间,以秒为单位。使用 alarm
函数的一个常见用途是在程序中设置一个超时时间,以便在等待某个事件发生时避免无限期地阻塞。
下面是一个简单的例子,演示了如何使用 alarm
函数:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// SIGALRM 信号处理函数
void alarm_handler(int signum) {
printf("Received SIGALRM\n");
}
int main() {
// 设置 SIGALRM 的处理函数
// 当程序接收到 SIGALRM 信号时,将调用 alarm_handler 函数来处理这个信号。
signal(SIGALRM, alarm_handler);
// 设置定时器,5 秒后发送 SIGALRM 信号
unsigned int remaining_time = alarm(5);
printf("Alarm set. Remaining time: %u seconds\n", remaining_time);
// 模拟一些工作,等待超时
sleep(7);
// 如果在超时前收到了 SIGALRM,处理函数将被调用
printf("Work done\n");
return 0;
}
setitimer
函数setitimer () 函数可以进行周期性定时,每触发一次定时器就会发射出一个信号。
// 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>
struct itimerval {
struct timeval it_interval; /* 时间间隔 */
struct timeval it_value; /* 第一次触发定时器的时长 */
};
// 举例: luffy有一个闹钟, 并且使用这个闹钟定时:
// 早晨7点中起床, 第一次闹钟响起时可能起不来, 之后每隔5分钟再响一次
// - it_value: 当前设置闹钟的时间点 到 明天早晨7点 对应的总秒数
// - it_interval: 闹钟第一次响过之后, 每隔5分钟响一次
// 这个结构体表示的是一个时间段: tv_sec + tv_usec
struct timeval {
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* 微妙 */
};
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
参数:
例子:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
void timer_handler(int signum) {
printf("Timer expired!\n");
}
int main() {
// 设置 SIGALRM 信号处理函数
signal(SIGALRM, timer_handler);
// 设置定时器初始值和溢出后的处理方式
struct itimerval timer;
timer.it_value.tv_sec = 2; // 初始值为2秒, 2s后第一次触发
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 1; // 间隔为1秒,即每秒触发一次
timer.it_interval.tv_usec = 0;
// 设置定时器
if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
perror("setitimer");
exit(EXIT_FAILURE);
}
// 让程序持续运行,观察定时器的触发
while (1) {
sleep(5);
}
return 0;
}
信号集(Signal Set)是用于表示一组信号的数据结构。在 Unix/Linux 操作系统中,信号集被广泛用于进程管理中,主要用于设置、查询和修改进程的信号状态。信号集通常是一个二进制位图,每个位表示一个信号,位的状态表示信号的状态(屏蔽或未屏蔽)。
信号集的主要目的是允许进程选择性地阻塞或解除阻塞一组特定的信号,以实现对信号的灵活控制。以下是在 C 语言中使用信号集的相关函数和数据结构:
sigset_t
数据类型:
sigset_t
是用于存储信号集的数据类型,通常是一个整数或类似的位图结构。<signal.h>
中定义,具体实现可能因系统而异。在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集体现在内核中就是两张表。
在阻塞信号集中,描述这个信号有没有被阻塞
在未决信号集中, 描述信号是否处于未决状态
阻塞信号集可以通过系统函数进行读写操作,未决信号集只能对其进行读操作。
读/写阻塞信号集的函数:
#include <signal.h>
// 使用这个函数修改内核中的阻塞信号集
// sigset_t 被封装之后得到的数据类型, 原型:int[32], 里边一共有1024给标志位, 每一个信号对应一个标志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:
oldset: 通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为NULL
返回值:函数调用成功返回0,调用失败返回-1
sigprocmask() 函数有一个 sigset_t 类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数:
#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节
// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);
读未决信号集的操作函数:
#include <signal.h>
// 这个函数的参数是传出参数, 传出的内核未决信号集的拷贝
// 读一下这个集合就指定哪个信号是未决状态
int sigpending(sigset_t *set);
下面举一个简单的例子,演示一下信号集操作函数的使用:
需求:
在阻塞信号集中设置某些信号阻塞, 通过一些操作产生这些信号, 然后读未决信号集, 最后再解除这些信号的阻塞
假设阻塞这些信号:
- 2号信号: SIGINT: ctrl+c
- 3号信号: SIGQUIT: ctrl+\
- 9号信号: SIGKILL: 通过shell命令给进程发送这个信号 kill -9 PID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
int main()
{
// 1. 初始化信号集
sigset_t myset;
sigemptyset(&myset);
// 设置阻塞的信号
sigaddset(&myset, SIGINT); // 2
sigaddset(&myset, SIGQUIT); // 3
sigaddset(&myset, SIGKILL); // 9 测试不能被阻塞
// 2. 将初始化的信号集中的数据设置给内核
sigset_t old;
sigprocmask(SIG_BLOCK, &myset, &old);
// 3. 让进程一直运行, 在当前进程中产生对应的信号
int i = 0;
while(1)
{
// 4. 读内核的未决信号集
sigset_t curset;
sigpending(&curset);
// 遍历这个信号集
for(int i=1; i<32; ++i)
{
int ret = sigismember(&curset, i);
printf("%d", ret);
}
printf("\n");
sleep(1);
i++;
if(i==10)
{
// 解除阻塞, 重新设置阻塞信号集
//sigprocmask(SIG_UNBLOCK, &myset, NULL);
sigprocmask(SIG_SETMASK, &old, NULL);
}
}
return 0;
}