基本概念
信号作为最简单的进程间通信手段,具有简单和开销小的优点,但是携带的信息有限。信号相当于软件层面的“中断”,它的实现手段导致了信号有很强的延时性。当一个进程收到另一个进程的信号时,无论程序执行到什么位置。必须立即停止运行,处理信号,处理结束后再继续执行后续指令,注意所有信号的产生及处理全部都是由内核完成的。信号必须满足一定的条件才能发送,不能够随意乱发。
产生信号的方式
1.按键产生,如:Ctrl+c(结束进程)、Ctrl+z(暂停进程)、Ctrl+(结束进程);
2.系统调用产生,如:kill,ralse,abort;
3.软件条件产生,如:定时器alarm;
4.硬件异常产生,如:段错误,内存对齐出错;
5.命令产生:kill。
处理信号的方式
1.执行默认动作;
2.忽略;
3.捕捉,调用用户定义的信号处理函数。
未决信号集和信号屏蔽字
如下图所示,进程的内核空间中的pcb控制块中有未决信号集和信号屏蔽字两个位图(只有0和1两个状态),当进程没有收到信号时,未决信号中的位全部都为0,假如键盘上按下了Ctrl+c产生了一个结束当前进程的信号,则未决信号集里面对应的位会变成1,如果信号处理完毕后对应位又会变0,但是如果信号屏蔽字里面对应位为1,则信号不能递达,信号被阻塞,只有将其改回0后才能处理信号。
信号的分类
信号分为常规信号(131)和实时信号(3464),常规信号都有对应的默认事件发生(除了用户自定义的信号),而实时信号没有对应的默认事件发生,实时信号一般做底层驱动才会用到。
常规信号有以下特点:
1.不排队(当阻塞了很多个同一个信号,解除阻塞后只会处理一个),可嵌套;
2.未响应,将丢弃;
3.产事件,发信号;
4.挂起多,随机响。
实时信号有以下特点:
1.要排队,不嵌套;
2.不忽略,不丢弃;
3.实时信,无对应。(实时信号,无对应的系统事件)
信号的四要素
1.编号 2.名称 3.事件对应信号 4.默认处理动作
注意:
1.信号使用之前应该清楚四要素才能使用,使用命令 man 7 signal 查看每个信号的作用;
2.不同芯片对应的信号编号可能会不同,但是名字是一样的,所以记名字比记编号更加通用。
注意:
1.9号信号和19号信号为特殊信号,只能执行默认动作不可以屏蔽。
信号相关API
kill()
函数功能:给指定进程的pid发送一个信号。
头 文 件:
#include <sys/types.h>
#include <signal.h>
定义函数:
int kill(pid_t pid, int sig);
参数分析:
pid: 小于-1:信号将被发送给组ID等于|-pid| 的进程组里面的所有进程
等于-1:信号将被发送给所有进程(如果当前进程对其有权限)
等于 0: 信号将被发送给与当前进程同一个进程组内的所有进程
大于 0:信号将被发送给 PID 等于 pid 的指定进程
sig:要发送的信号
返 回 值:
成功 : 0
失败 :-1
signal()
函数功能:注册信号捕捉函数。
头 文 件:
#include <signal.h>
定义函数:
void (*signal(int sig, void (*func)(int)))(int);
参数分析:
sig:要捕捉的信号
func:信号处理函数,当捕捉到指定信号后便会进入这个函数。
当第二个参数传递SIG_IGN时,捕捉的信号会被忽略;传递SIG_DFL时,则执行该信号的缺省动作。
返 回 值:
成功 :最近一次调用该函数时第二个参数的值
失败 :SIG_ERR
注意:
1.signal()函数一般与函数()kill配套使用,以此来捕捉某个信号,以此来改变信号的缺省行为,执行用户所需要的行为。
信号实验程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void func(int arg)
{
printf("Currently in signal response function
");
}
int main(int argc, char const *argv[])
{
union sigval val;
signal(SIGUSR2 , func);
kill(getpid(), SIGUSR2);
return 0;
}
alarm()
函数功能:当时间减为0秒时产生一个SIGALRM信号将当前进程杀死,也可以捕捉该信号执行别的操作,相当于一个定时器(自然计时法)。
头 文 件:
#include <unistd.h>
定义函数:
unsigned int alarm(unsigned int seconds);
参数分析:
seconds: 需要增加的定时秒数
返 回 值:
成功 :上次定时剩余时间
失败 :没有失败
注意:
1.每个进程只有一个定时器。
2.实际执行时间 = 系统时间 + 用户空间 + 等待时间,其中等待时间是最长的,因为系统中很多设备是共用的,尤其是使用printf打印一些东西到终端,如果将其打印到文件将大大提高效率,优化程序也是一般从io入手的,查看程序的运行时间在要执行程序的前面加上time就可以了。
alarm实验程序
int main(int argc, char const *argv[])
{
alarm(1);
int i = 0;
while(1)
{
printf("%d
", i);
i++;
}
return 0;
}
setitimer()
函数功能:设置定时器,可代替alarm函数,精度微秒us,可以实现周期定时。
头 文 件:
#include <sys/time.h>
定义函数:
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数分析:
which:定时方式:ITIMER_REAL(系统真实时间)
ITIMER_VIRTUAL(用户态花费时间)
ITIMER_PROF(系统和用户态的总和时间)
struct itimerval {
struct timeval it_interval; //周期定时,间隔时间
struct timeval it_value; //定时时间
};
struct timeval {
time_t tv_sec; //秒
suseconds_t tv_usec; //微秒
};
new_value:需要增加的定时时间
old_value:上次定时剩余时间
返 回 值:
成功 :0
失败 :-1
信号集
由前面可知信号集有阻塞信号集(信号屏蔽字)和未决信号集,用户只可以操作阻塞信号集,通过操作阻塞信号集来影响未决信号集。
信号集设定
因为阻塞信号集在内核的PCB中,用户不可以随意更改内核,所以可以自己定义一个集合并设置需要的位(信号集操作函数),再与阻塞信号集位与 或 位或(设置信号屏蔽字)。信号集操作函数就是做这种操作的。
信号集操作函数
sigset_t set; 自定义set信号集
int sigemptyset(sigset_t *set); 清空信号集set
int sigfillset(sigset_t *set); 将信号集set全部置1
int sigaddset(sigset_t *set, int signum); 将指定的信号置1
int sigdelset(sigset_t *set, int signum); 将指定的信号清0
int sigismember(const sigset_t *set, int signum); 当信号在对应的未决信号集中为1则函数sigismember()返回1
int sigpending(sigset_t set); 查看未决信号集,set为传出的
设置信号屏蔽字
sigprocmask()
函数功能:将自定义信号集set与阻塞信号集位与 或 位或操作
头 文 件:
#include <signal.h>
定义函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数分析:
how:SIG_BLOCK: 设置阻塞
SIG_SETMASK:取消阻塞
SIG_UNBLOCK:设置阻塞,但是会替换掉原有的设置,所以一般不用
set:设定好的信号集
oldset:用以回复阻塞信号集的原有设置
返 回 值:
成功:0
失败:-1
信号集实验程序
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void func(int arg)
{
printf("This is signal response arg: %d
", arg);
}
void print_set(sigset_t *set)
{
int i;
//判断未决信号集中的所有信号
for (i = 1; i < 32; i++)
{
//当信号i在对应的未决信号集中为1则函数sigismember()返回1
if (sigismember(set, i))
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("
");
}
int main(int argc, char const *argv[])
{
sigset_t set, pset;
//注册信号1(关闭终端),2(Ctrl+c),3(Ctrl+)的处理函数
for (int i = 1; i < 4; i++)
{
signal(i , func);
}
//在此进程中产生信号1,2,3
for (int i = 3; i > 0; i--)
{
raise(i);
}
//清空用户自定义屏蔽字
sigemptyset(&set);
// 增加需要屏蔽的信号
for (int i = 1; i < 4; i++)
{
sigaddset(&set, i);
}
//将信号1,2,3屏蔽
int ret = sigprocmask(SIG_BLOCK, &set, NULL);
if (-1 == ret)
{
perror("sigprocmask error");
return -1;
}
printf("sleep
");
sleep(3);
printf("wake up
");
//实时监测未决信号集,当用户按下Ctrl+c,Ctrl+时对应的位将被置1,表示该信号处理未执行
while(1)
{
int ret = sigpending(&pset);
if (-1 == ret)
{
perror("sigpending error");
}
print_set(&pset);
sleep(1);
}
return 0;
}
输出结果:
sigaction实现信号捕捉
上面讲到的函数signal()是ANSI定义的标准,并且在不同的操作系统中的行为也有所不同,这将造成程序的移植性不好,所以应该避免使用它,取而代之的是使用函数sigaction()。
sigaction()
函数功能:注册信号捕捉函数。
头 文 件:
#include <signal.h>
定义函数:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数分析:
signum:需要捕捉的信号
struct sigaction
{
void (*sa_handler)(int); //需要注册的信号响应函数
void (*sa_sigaction)(int, siginfo_t *, void *);//将sa_flags设置为SA_SIGINFO时,使用这个函数类型指针来注册信号响应函数,用以获取信号携带的数据
sigset_t sa_mask; //当在执行信号响应函数的时候,需要屏蔽的信号由这个自定义信号集决定,信号响应函数执行完后将决定权交还给PCB中的信号屏蔽字
int sa_flags; //为0时,在执行信号响应函数的时候,屏蔽产生执行信号响应函数的信号(默认选项)
void (*sa_restorer)(void);
};
act:信号处理参数结构体
oldact:原有的信号处理参数结构体
返 回 值:
成功:0
失败:-1
sigqueue()
函数功能:
发生一个信号,并且携带数据,需要配合函数sigaction()使用。
头 文 件:
#include <signal.h>
定义函数:
int sigqueue(pid_t pid, int sig, const union sigval value);
参数分析:
pid:目标进程 PID
sig:要发送的信号
value:携带的额外数据
union sigval
{
int sigval_int;
void * sigval_prt;
}
sigaction实现信号捕捉实验程序
#include <stdio.h>
#include <signal.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
void func(int sig, siginfo_t *val, void *p)
{
printf("Currently in :%d signal response function
", sig);
printf("The data carried is :%s
", (char *)val->si_ptr);
}
int main(int argc, char const *argv[])
{
//1.定义一个信号处理结构体,并将其初始化
struct sigaction act;
bzero(&act, sizeof(act));
act.sa_sigaction = func;
act.sa_flags |= SA_SIGINFO;//设置信号可以携带数据
//2.设置信号处理结构体
sigaction(SIGINT, &act, NULL);
//3.设置信号携带的联合体
union sigval val;
val.sival_ptr = "Hello 2021";
//4.产生信号,并发送给自己
sigqueue(getpid(), SIGINT, val);
return 0;
}
内核实现信号捕捉流程简析
如下图步骤1所示用户空间进入内核需要一个契机,这个契机就是程序因为中断、异常、系统调用等进入内核。信号属于软中断,当信号产生时系统会从用户空间进入内核空间,检查信号是否产生,如果信号产生了并且设置了捕捉的话则执行信号响应函数,信号响应函数处理完毕之后调用sigreturn再次进入内核,最后再从内核返回用户空间执行后续代码。