一、信号的概念
信号在我们的生活中随处可见,如:古代战争中挠杯为号;现代战争中的信号弹;体育比赛中使用的信号机枪…他们都有共性:1.简单2.不能携带大量信息 3.满足某个特设条件才发送
1、信号的特质
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。与硬件中断类似—―异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”"。
所有信号的产生及处理全部都是由内核完成的。
2、与信号相关的事件和状态
1)产生信号
- 按键产生,如:Ctrltc-Ctrltz、Ctrl+\w
- 系统调用产生,如: kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(毁错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如: kill命令
2)递达
递送并且到达进程
3)未决
产生和递归之间的状态。主要是由于阻塞(屏蔽)导致该状态。
4)信号的处理方式:
- 执行默认动作
- 忽略(丢弃), 也是一种处理
- 捕捉, 调用户处理函数.
Linux内核的进程控制块 PCB是一个结构体,task_struct,除了包含进程 id,状态,工作目录,用户 id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
5)阻塞信号集(信号屏蔽字):
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后) kill -9无法屏蔽
6)未决信号集
1.信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
2.信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
但如果加到信号屏蔽字(详细的图往下翻)
总结
概念:
未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略、捕捉(自定义)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集:本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。
3、信号4要素
与变量三要素类似的,每个信号也有其必备4要素,分别是:
- 编号
- 名称
- 事件
- 默认处理动作。
4、kill发送信号函数
kill不仅仅局限于杀死,如上面提到的那些信号
int kill(pid_t pid, int sig);
pid: > 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员(由于大于0只发一个,这里加个符号表示他那个组的所有成员)。
= -1:发送信号给,有权限发送的所有进程。
sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
成功: 0;
失败:-1(ID非法,信号非法,普通用户杀init进程等权级问题),设置 errno
关于pid
5、alarm函数
设置定时器(闹钟)。在指定seconds 后,内核会给当前进程发送14〉SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
谁调用就给谁发,要求多少秒后给自己发信号
返回定时器剩余的秒数或0 返回0是已经过了定时器设置的时间了
常用:取消定时器:alarm(0),返回旧闹钟余下秒数。
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
time 命令 : 查看程序执行时间。 实际时间 = 用户时间 + 内核时间 + 等待时间。 --》 优化瓶颈 IO
6、setitimer函数
设置定时器(闹钟)。可代替alarm函数。精度微秒us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which: ITIMER_REAL: 进行自然计时。 ——> SIGALRM
ITIMER_VIRTUAL: 进行用户空间计时 ---> SIGVTALRM
ITIMER_PROF: 进行内核+用户空间计时 ---> SIGPROF
new_value:定时秒数
类型:struct itimerval {
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}it_interval;---> 周期定时秒数(第一次执行后进行每隔这个时间的周期执行)
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
}it_value; ---> 第一次执行定时秒数(仅执行一次)
};
old_value:传出参数,上次定时剩余时间。
e.g.
struct itimerval new_t;
struct itimerval old_t;
new_t.it_interval.tv_sec = 0;
new_t.it_interval.tv_usec = 0;
new_t.it_value.tv_sec = 1;
new_t.it_value.tv_usec = 0;
int ret = setitimer(&new_t, &old_t); 定时1秒
返回值:
成功: 0
失败: -1 errno
当setitimer()所执行的timer时间到了,会呼叫SIGALRM signal,
itimerval.it_value设定第一次执行function所延迟的秒数,
itimerval.it_interval设定以后每几秒执行function,
若只想延迟一段时间执行function,只要设定 itimerval.it_value即可,
若要设定间格一段时间就执行function,则it_value和it_interval都要设定,否则 funtion的第一次无法执行,就别说以后的间隔执行了。
二、信号操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。
因为不允许直接对mask进行操作,因此必须要有个缓冲,如上图所示,然后再和mask进行运算
1、信号集操作函数
sigset_t set; 自定义信号集。 首先设置一个信号集
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中。 在--》1, 不在--》0
2、sigprocmask函数
读取或修改进程的信号屏蔽字(PCB中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
成功: 0;
失败:-1,设置errno参数:
set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how参数取值:假设当前的信号屏蔽字为mask
1. SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask = mask|set
2. SlG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号。相当于mask = mask & ~set3.
3. SlG_SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set
若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
3、sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);
set传出参数。
返回值:
成功:0;
失败:-1,设置errno
例子,下面运行后执行ctlc+c会打印0100000
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void print_set(sigset_t *set)
{
int i;
for (i = 1; i<32; i++) {
if (sigismember(set, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(int argc, char *argv[])
{
sigset_t set, oldset, pedset;
int ret = 0;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigaddset(&set, SIGBUS);
sigaddset(&set, SIGKILL);
ret = sigprocmask(SIG_BLOCK, &set, &oldset);
if (ret == -1)
sys_err("sigprocmask error");
while (1) {
ret = sigpending(&pedset);
print_set(&pedset);
sleep(1);
}
return 0;
}
三、信号捕捉
1、signal函数
注册一个信号捕捉函数,抓信号还是内核来
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
typedef定义了一个类型,函数指针类型
该函数由ANSI定义,由于历史原因在不同版本的 Unix和不同版本的 Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
2、sigaction函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
返回值:
成功: 0;
失败:-1,设置errno
这里要有信号屏蔽,比如捕捉到一个信号,进入回调函数处理过程,假设回调函数特别长,这时候正好又来了一个信号,由于信号比程序执行优先级高那么前面那个回调函数就没法再执行。sa_flags设置为0将默认屏蔽这个信号。
4、信号捕捉特性
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(依赖于sa_flgs = 0).
3. 捕捉函数执行期间,被屏蔽信号多次发送,如果有多次的话解除屏蔽后只处理其中的一次!
3、内核实现信号捕捉
四、sigchld信号
1、sigchld的产生条件
子进程终止时
子进程接收到SIGSTOP信号停止时
子进程处在停止态,接受到SIGCONT后唤醒时
2、借助sigchld信号回收子进程
阻塞代码
五、中断系统调用