目录
-
信号实际就是软件层面模拟中断的一种方式,也称软中断
-
信号的分类
- 标准信号(1~31)
- 实时信号(34~64)
- 注意如果没有特别说明,我们在课程中所指的信号都是标准信号
-
查看信号
- kill -l
- man 7 signal
-
标准信号的默认行为
- Term 接收到信号的进程终止
- Ign 忽略接收到的信号
- Core 接收到信号的进程终止并产生dump文件
- Stop 接收到信号的进程暂停
- Cont 接收到信号的进程继续运行
-
实时信号与标准信号的区别
- 标准信号有默认行为,实时信号无
- 标准信号可看作位图,实时信号则是队列
-
进程接收到信号可以有三种选择
- 执行默认行为
- 选择屏蔽掉 ---> 忽略
- 响应这个信号的信号处理函数
-
信号和fork
- fork创建的子进程,继承父进程的信号的行为和信号的屏蔽
- 如果子进程进行了替换(exec),信号的行为会重新设置为默认行为
-
信号行为的注册
-
signal(2)【只做练习用】
-
sigaction(2) 【企业中常用】
- 附加功能
- signal(2)函数在不同的linux版本有不同的实现,所以项目中避免使用,用sigaction(2)替代
- sa_flags
- SA_NOCLDWAIT 如果信号是SIGCHLD,那么表示子进程终止的时候不变成僵尸进程
- SA_SIGINFO 表示信号处理函数使用三参的那个,回填信号的属性
- sa_mask
- 设置信号处理函数调用期间的信号屏蔽字
-
// signal 在不同的Linux版本中,可能会出现错误 /* 所以在企业中基本上都用sigaction */ struct sigaction act; act.sa_handler = sig_handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGINT, &act, NULL);
- 附加功能
-
-
产生信号
-
kill(2)
-
控制台终端发送 kill -14 【pid】
-
kill(pid_t pid, int sig)
-
-
alarm(2) --->SIGALRM
-
setitimer(2) 可替换 alarm(2)
-
struct itimerval itv; itv.it_interval.tv_sec = 0; itv.it_interval.tv_usec = 500000; // 0.5 毫秒 周期性的将interval 赋值给it_value itv.it_value.tv_sec = 3; // 3s itv.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &itv, NULL);
-
-
-
等待信号
-
pause(2)
-
sigsuspend(2)
-
// 恢复信号屏蔽 sigprocmask(SIG_SETMASK, &oldset, NULL); pause(); sigprocmask(SIG_BLOCK , &set, &oldset); sigsuspend(&oldset);} // 原子操作 与 上三行同理
-
-
屏蔽信号
-
sigpending(2) 【返回信号集】
-
sigprocmask(2)
- 信号的屏蔽
- sigset_t 信号屏蔽类型
- sigismember(3) 测试信号是否在信号屏蔽字集中
- sigdelset(3)将信号从信号屏蔽字集移除
- sigaddset(3) 将信号加入信号屏蔽字集
- sigfillset(3) 满集
- sigemptyset(3) 空集
- 设置信号屏蔽
-
sigset_t myset,oldset; // 初始化信号集为空 sigemptyset(&myset); // 信号集 为满 sigfillset(&myset); // 向信号集中添加指定信号 sigaddset(&myset, SIGINT); sigaddset(&myset, SIGTERM); // 从信号集中移除指定信号 sigdelset(&myset, SIGTERM); // 检查 SIGINT 信号是否在信号集中 if (sigismember(&myset, SIGINT)) { // 设置信号集中 SIGINT 信号为阻塞状态 sigprocmask(SIG_BLOCK, &myset, NULL); // 执行需要阻塞 SIGINT 信号的操作 // 解除对 SIGINT 信号的阻塞 sigprocmask(SIG_UNBLOCK, &myset, NULL); } // 将信号集设置为阻塞状态 [返回原有的信号集到oldset] sigprocmask(SIG_BLOCK, &myset, &oldset); // 设置新的信号屏蔽集,替换当前进程的阻塞信号集 sigprocmask(SIG_SETMASK, &oldset, NULL);
- 信号的屏蔽
-
-
异常程序终止
-
abort(2)
-
-
标准信号的响应过程
-
异步
- 什么时候产生不确定的,产生后会有什么样的行为也是不确定
- 信号是典型的初步异步事件
-
捕获异步事件
- 通知法 pause(2);【使调用进程挂起,直至捕捉到一个信号】
- 轮询法 【不停的循环等待信号,直到信号到来】
- 事件密集
-
流量控制
-
漏桶
- 流入的速率不固定,流出的速率的恒定的
- 不容易调整容量的限制
- 适合突发性的高并发,例如整点支付、秒杀活动
- 保护自己
- 实例(轮询法与通知法)
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <signal.h> #define BUFSIZE 32 #define CPS 10 /* 读取argv[1]文件 写到stdout */ // 权限 static int token; static void alrm_handler(int s); int main(int argc, char *argv[]/*char **argv*/) { int fd; char buf[BUFSIZE] = {}; int cnt; if (argc < 2) return 1; // 注册SIGALARM的行为 signal(SIGALRM, alrm_handler); // 定义闹钟1s alarm(1); // 打开 if(-1 == (fd = open(argv[1], O_RDONLY))){ perror("open()"); return -1; } // cat while (1) { // 流量控制 ---> 当token为1的时候才有权限读文件 while (token == 0) // 等待信号到来--->轮询 //通知 分号变为 pause(); ; token = 0; // 重复使用buf,建议buf清理干净,防止有脏数据 memset(buf, 0, BUFSIZE); cnt = read(fd, buf, CPS); if (0 == cnt) { // 读完了 break; } else if (-1 == cnt) { // 出错了 goto ERROR; } // 成功了 write(1, buf, cnt); // 注意读多少写多少 } close(fd); return 0; ERROR: close(fd); return 1; } /* SIGALRM注册一个行为 控制是否有权限输出 */ static void alrm_handler(int s) { alarm(1); token = 1; }
-
令牌桶
- 一定的速率积攒令牌,令牌的个数是由上限的
- 请求服务是可以解决突发事件的,请求响应的令牌个数就可以了,当时也有限制,如果令牌数量不能满足,则丢弃
- 处理本身的流量控制的同时还可以保护第三方服务
- 更适合处理突发的事件(注意区别高并发)
-
#ifndef __TBF_H__ #define __TBF_H__ /* 令牌桶仓库 所谓库,就是不止一个,大量 */ #define TBFMAX 1024 /* 创建令牌桶 cps:速率 burst:上限 return: 所在令牌桶库的索引值---》桶描述符 */ int tbf_init(int cps, int burst); /* 取令牌 tb:桶描述符 ntokens:要取得的令牌数 return:取到的令牌数 */ int tbf_fetch_token(int tb, int ntokens); // 销毁指定令牌桶 void tbf_destroy(int tb); // 销毁令牌桶库 void tbf_destroy_all(void); #endif
#include "tbf.h" #include <stdlib.h> #include <signal.h> #include <unistd.h> #include <sys/time.h> #include <signal.h> // 抽象数据类型 typedef struct { int token; // 令牌 int cps; // 速率 int burst; // 上限 }tbf_t; // 令牌桶库结构 static tbf_t *jobs[TBFMAX] = {}; static int inited = 0; static int __get_free_pos(void); static void sig_moduler_load(void); int tbf_init(int cps, int burst) { tbf_t *me; int pos; /* 当第一次调用init启动信号SIGALRM 信号周期性产生,信号处理函数将所有令牌桶库中不空闲的位置对应积攒令牌 [ 仓库管理员 ] */ if (!inited) { sig_moduler_load(); // 注册信号的行为,定闹钟 inited = 1; } me = malloc(sizeof(tbf_t)); if (NULL == me) return -1; me->token = 0; me->cps = cps; me->burst = burst; pos = __get_free_pos(); /* 如果桶满了释放开辟的空间 */ if (pos == -1) { free(me); return -1; } /* 令牌桶库中添加数据 [结构体指针数组存放数据] */ jobs[pos] = me; return pos; } // 找到库中空闲的指针[ 获取空的令牌桶 下标] static int __get_free_pos(void) { int i; for (i = 0; i < TBFMAX; i++) if (jobs[i] == NULL) return i; return -1; } static void alarm_handler(int s) { int i; // 管理令牌桶库 //alarm(1); for (i = 0; i < TBFMAX; i++) { if (jobs[i]) { // 这里有令牌桶 需要积攒令牌 /* 令牌桶不满,数据不抛弃,直到来下一波数据填满 */ jobs[i]->token += jobs[i]->cps; /* 令牌总数大于请求的数量 多的抛弃 */ if (jobs[i]->token > jobs[i]->burst) jobs[i]->token = jobs[i]->burst; } } } static void sig_moduler_load(void) { // 注册信号的行为 //signal(SIGALRM, alarm_handler); #if 1 struct sigaction act, oldact; act.sa_handler = alarm_handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGALRM , &act, &oldact); #endif // 定闹钟 //alarm(1); #if 1 struct itimerval itv; itv.it_interval.tv_sec = 1; itv.it_interval.tv_usec = 0; itv.it_value.tv_sec = 1; itv.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &itv, NULL); #endif } int tbf_fetch_token(int tb, int ntokens) { int ret; if (tb < 0 || tb >= TBFMAX) return -1; while (jobs[tb]->token <= 0) pause(); // 如果没有令牌则等信号 通知法 /* 要取的令牌数 大于 当前桶内的数据 输出桶内的实际数据 */ if (jobs[tb]->token < ntokens) { ret = jobs[tb]->token; } else { /* 取多少拿多少 */ ret = ntokens; } /* 令牌被取出 当前桶内令牌需减少 */ jobs[tb]->token -= ret; return ret; } void tbf_destroy(int tb) { if (tb < 0 || tb >= TBFMAX) return ; free(jobs[tb]); jobs[tb] = NULL; } void tbf_destroy_all(void) { free(jobs); } #if 0 // 时间库与令牌桶的不同 // 遍历时钟库,每秒中将定义的闹钟递减 static void alarm_handler(int s) { int i; //alarm(1); for (i = 0; i < TIMER_MAX; i++) { if (jobs[i]) { // 闹钟 jobs[i]->sec--; if (jobs[i]->sec == 0) { // 闹钟到时间了,执行函数 (jobs[i]->handler)(jobs[i]->arg); anytimer_cancel(i); } } } } #endif
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <signal.h> #include "tbf.h" #define BUFSIZE 32 #define CPS 10 // 测试令牌桶 int main(int argc, char *argv[]/*char **argv*/) { int fd; char buf[BUFSIZE] = {}; int cnt; int tb,n; if (argc < 2) return 1; if(-1 == (tb = tbf_init(CPS, CPS*10))) return 1; // 打开 fd = open(argv[1], O_RDONLY); if (-1 == fd) { return -1; } // cat while (1) { // 令牌桶实现流量控制 n = tbf_fetch_token(tb, CPS); // 重复使用buf,建议buf清理干净,防止有脏数据 memset(buf, 0, BUFSIZE); cnt = read(fd, buf, n); if (0 == cnt) { // 读完了 break; } else if (-1 == cnt) { // 出错了 goto ERROR; } // 成功了 write(1, buf, cnt); // 注意读多少写多少 } tbf_destroy(tb); close(fd); return 0; ERROR: close(fd); return 1; }
-
-
信号响应过程中信号的问题
-
标准信号为什么会丢失
-
标准信号可能会丢失的原因有以下几点:
-
信号屏蔽:进程可以使用信号屏蔽集(signal mask)来阻止对某些信号的传递。如果某个信号被设置为屏蔽状态,那么当该信号产生时,它将被暂时忽略,并不会被进程处理。如果在信号屏蔽期间持续产生相同类型的信号,那么这些信号就有可能被丢失。
-
信号排队:有些信号可以被排队,即允许同一类型的多个信号积累并在适当时处理。但是,某些信号是不可排队的,例如SIGKILL和SIGSTOP。如果这些信号在未处理之前又产生了多次,那么附加的信号就会丢失。
-
信号处理不及时:当信号产生时,操作系统会尝试将信号交付给进程。但如果进程当前正在执行某个长时间的操作(例如系统调用、计算密集型任务等),它可能无法及时响应信号,从而导致信号丢失。
-
信号竞争条件:在多线程编程中,多个线程可能同时处理信号。如果信号处理程序没有正确地进行同步和互斥,多个线程可能会竞争处理信号,导致信号丢失或不按预期的方式处理。
-
-
要避免信号丢失,可以采取以下措施:
- 使用可排队的信号,并确保及时处理所有信号。
- 避免在信号处理程序中进行耗时的操作,应尽量保持简洁和高效。
- 在多线程环境中,确保对信号处理程序的访问进行适当的同步和互斥。
-
需要注意的是,由于系统的调度和各种外部因素的影响,无法完全消除信号丢失的可能性。因此,在设计和编写程序时,应注意处理信号丢失的情况,并确保程序能够正确处理这种情况
-
-
为什么不能在信号处理函数中longjmp跳转
-
longjmp
是一个用于非局部跳转的函数。它可以将程序控制流直接跳转到之前使用setjmp
设置的跳转点,并且会忽略中间的函数调用栈(即栈展开)。使用
longjmp
的问题在于,它会绕过正常的控制流程和资源清理过程,可能引发以下问题:-
状态不一致:当在信号处理函数中使用
longjmp
时,可能会导致程序处于不一致的状态。例如,在信号处理函数中跳过了正在执行的代码块,会导致相关的变量和数据结构处于未被正确更新或恢复的状态。 -
资源泄漏:由于
longjmp
绕过了正常的栈展开和资源清理过程,会导致在跳转位置之后的代码没有机会进行资源释放和清理操作。这可能会引起内存泄漏、文件句柄未关闭等问题。 -
破坏异常处理机制:如果程序中使用了异常处理机制(如C++的异常处理),在信号处理函数中使用
longjmp
会破坏异常处理流程,导致异常无法正确传播和处理。
-
-
由于上述问题的存在,通常建议在信号处理函数中避免使用
longjmp
。相反,应该使用安全的方式处理信号,例如设置标志位,然后在主程序中根据标志位进行相应的处理操作。
-
-
如何屏蔽一个信号
- 创建一个信号集变量
sigset_t
,用于保存要屏蔽的信号集。 - 调用
sigemptyset
函数将信号集清空,以确保其中没有任何信号。 - 调用
sigaddset
函数将要屏蔽的信号添加到信号集中。 - 调用
sigprocmask
函数将修改后的信号集应用到进程的信号屏蔽集上。 - 要注意的是,通过屏蔽信号只能暂时阻止信号的传递,而不能完全丢弃信号。当信号解除屏蔽后,之前被屏蔽的信号可能会被触发并递送给进程。另外,应谨慎使用信号屏蔽,以免因为信号丢失而导致问题
- 创建一个信号集变量
-
不同的信号会嵌套响应吗
-
不同的信号在一般情况下不会嵌套响应。在多个信号同时到达时,操作系统会根据信号的优先级和处理机制来确定如何处理这些信号。
对于非实时信号(例如
SIGINT
、SIGTERM
等),通常操作系统会采用以下方式处理:- 信号排队:如果同一类型的信号在处理期间重复到达,操作系统会将这些信号排队,等待当前正在处理的信号处理完成后再依次处理。
- 信号屏蔽:当一个信号正在处理时,其他同类型的信号可能会被操作系统屏蔽,防止信号处理函数嵌套调用。只有当当前信号处理完成后,才会接收并处理下一个同类型的信号。
-
这种方式确保同一类型的信号只会按顺序逐个处理,即使在处理信号期间有新的同类型信号到达也不会中断当前的处理过程。
然而,对于实时信号(例如
SIGRTMIN
和SIGRTMAX
之间的信号),它们具有更高的优先级和不同的排队机制。实时信号之间具有竞争优先级,并且可以在处理期间中断其他实时信号的处理。综上所述,一般情况下不会出现信号嵌套响应的情况,操作系统会对同一类型的信号进行合适的处理和排队。但是需要注意,如果信号处理函数耗时过长或存在竞争条件,可能会引发信号丢失或不按期望的顺序响应的问题。因此,在处理信号时需要尽量保持简洁、高效,并避免在信号处理函数中使用非可重入的函数和状态。
-
-
同一个信号的信号处理函数会嵌套响应吗
-
对于同一个信号的信号处理函数,在一般情况下并不会嵌套响应。当同一个信号连续多次到达时,操作系统通常会按照以下方式处理:
- 信号排队:如果同一个信号在处理期间重复到达,操作系统会将这些信号排队,等待当前正在处理的信号处理函数返回后再依次处理。
- 信号屏蔽:当一个信号处理函数正在执行时,其他同类型的信号可能会被操作系统屏蔽,防止信号处理函数嵌套调用。只有当当前信号处理函数返回后,才会继续处理队列中的下一个同类型的信号。
-
这种机制确保了同一个信号的信号处理函数在任意给定时间只有一个实例在运行,并按顺序处理所有到达的同类型信号。因此,当一个信号处理函数正在执行时,不会立即中断自身或其他同一类型的信号处理函数。
需要注意的是,在某些特殊情况下,比如使用非标准的信号处理机制或其他编程技术时,可能会出现信号嵌套响应的情况。但在一般的操作系统环境中,同一个信号的信号处理函数不会嵌套响应
-
-
信号会打断阻塞的系统调用吗
-
当一个进程正在执行一个阻塞的系统调用(如
read()
、write()
、sleep()
等)时,如果接收到一个信号,操作系统会采取以下行动之一:- 中断系统调用:有些信号(如
SIGINT
、SIGQUIT
)会立即中断正在进行的系统调用,并将控制权返回给进程的信号处理函数。在这种情况下,系统调用的返回值可能会被设置为一个特殊值(通常是-1),表示系统调用被中断。 - 临时中断系统调用:大多数其他类型的信号会暂时中断正在进行的系统调用,允许进程执行信号处理函数。一旦信号处理函数返回,操作系统将重新启动被中断的系统调用,并继续执行。
- 中断系统调用:有些信号(如
-
需要注意的是,并非所有的系统调用都是可中断的。对于某些系统调用,例如
select()
和poll()
等,可以通过设置相应的标志参数来使其可中断。但对于一些不可中断的系统调用,如open()
和close()
等,即使接收到信号,系统调用也不会被中断。因此,当一个系统调用处于阻塞状态时,接收到信号可能会打断该系统调用的执行,并使进程转而执行信号处理函数。这是操作系统提供的一种机制,可以使进程对特定事件做出及时响应。
-
-