信号的概念:
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或者系统异常。
信号的产生:
1>对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。如输入Ctrl+c向进程发送一个中断信号。
2>系统异常。如浮点数异常和非法的内存段的访问
3>系统状态变化。如alarm定时器到期将一起SIGALRM信号。
4>运行kill命令或者调用kill函数。
服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。
常见的信号对应的含义: 查看系统信号--- kill -l 或者 man 7 signal
1>关闭终端产生
2> ctrl +c
3> ctrl + \
4> 非法指令
5> 从用户空间嵌入内核产生
6> abort函数产生
7> 两个设备信号断掉 地址映射出错 pipe(最快) mmap函数(最高效) socket(最稳定)
8> 浮点数异常
9> 杀死进程
10、12> 由用户定义行为
11> 段错误
13> 管道破裂 <----- 关掉管道读端
14>闹钟、警报
15>进程终止
17> 子进程编程僵尸
18> 继续
19>暂停
23>紧急数据
29> 异步IO信号
信号处理的方式:
1.忽略掉 不能被忽略的信号----1.SIGKILL 2.kill -SIGKILL pid 3.SIGSTOP
2.捕获并处理 SIGKILL SIGSTOP 不能被捕获
3.执行系统的默认动作
信号的分类:
1.不可靠信号 1-31 -----可能丢失
1>Linux的信号继承自UNIX,早期的Unix当信号处理函数执行完毕时,该信号恢复成缺省处理动作,(Linux已经改进)
2>信号不排队,会出现信号的丢失现象
2.可靠信号 34 - 64 ------不可能丢失
3.非实时信号 不可靠信号都是非实时的信号
4.实时信号 可靠信号
信号的注册:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
handler:
1.自己定义函数
2.SIG_IGN //忽略信号
3.SIG_DFL //默认行为
#define SIG_IGN ((sighandler_t)0)
#define SIG_DFL ((sighandler_t)1)
#define SIG_ERR ((sighandler_t)-1)
返回值:若成功则返回信号以前的处理配置,若出错则返回SIG_ERR
补充:父进程注册的信号处理方式,子进程会继承
发送信号:
1. kill -信号值 pid
2..killall -信号值 进程名(name) 给所有叫name的进程发送信号,默认15号信号
3. kill(pid_t pid, int signum);
pid > 0; 给pid进程发送信号
pid = 0; 给本进程组的所有进程发送信号
pid = -1; 给有权发送的所有进程发送信号
pid < -1; 给|pid|进程组的任何一个进程发送信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void handler(int s)
{
printf("recv = %d\n", s);
exit(0);
}
int main()
{
signal(SIGUSR1, handler);
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
return 1;
}
else if(pid == 0)
{
sleep(3);
if(-1 == kill(getppid(), SIGUSR1)) //向父进程发送信号,杀死父进程
{
perror("kill");
return 1;
}
}
else
{
for(;;)
{
printf(".");
fflush(stdout);
sleep(1);
}
}
return 0;
}
int raise(int signum); //给进程自己发送信号
int killpg(int pgid, int signum); //给pgid进程组发送信号
pause() //将当前进程置为可中断睡眠状态,然后调用schedule(),使Linux 进程调度算法找到另外一个进程来执行。(让出CPU来让其他进程运行)
1.pause 调用者进程一直挂起,直到有信号被捕获。
2.pause 要等到信号处理函数执行完毕再返回,这是的pause返回-1,bingjiangerror设置为EINTR
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> //pause函数要等到handler函数执行完毕才返回
void handler(int s)
{
printf("recv = %d\n", s);
sleep(2);
printf("handler finish\n");
}
int main()
{
signal(SIGINT, handler);
while(1)
{
pause();
printf("pause finish\n");
}
}
进程组介绍(有的系统也称为作业):管道连接的多个进程,父子进程同属一个进程组
管道连接的进程组:第一个进程的pid就是这个进程组的组pid。fork产生的进程组: 父进程的pid是组pid
sleep 100 & 以后台的方式启动进程
ctrl+c 只能发给前台进程组
jobs 查看有哪些后台作业
fg %作业号(缺省为1) : 将后台的作业拿到前台
经典的信号处理方式:
SIGALRM
1.设置延时器(闹钟) 每个进程只能有一个闹钟
unsigned int alarm(unsigned int seconds); //时间以秒为单位
一定时间触发SIGALARM信号,alarm函数相当于延时器,一定时间触发一次(可以用来进行超时检测)
如果参数为0,表示清除该信号
返回值为被信号打断后还余留的秒数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h> //程序阻塞三秒后向进程发送SIGALRM信号
void handler(int s)
{
printf("recv= %d\n", s);
}
int main()
{
signal(SIGALRM, handler);
alarm(3);
while(1)
{
pause();
}
return 0;
}
设置定时器:
int setitimer(int which, //ITIMER_REAL----按桌面时钟来定时
const struct itimerval *new_value, //设置的定时器
struct itimerval *old_value); //NULL
时间设置的结构体:
struct itimerval {
struct timeval it_interval; /* next value */ 以后每次间隔多长时间调用定时器struct timeval it_value; /* current value */ 第一次启动定时器的时间
};
struct timeval {
time_t tv_sec; /* seconds */ 秒 //都设置成0表示永不启动
suseconds_t tv_usec; /* microseconds */ 微秒
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
void handler(int s)
{
printf("recv = %d\n", s);
}
int main()
{
signal(SIGALRM, handler);
struct itimerval it;
it.it_value.tv_sec = 0; //启动时间
it.it_value.tv_usec = 1;
it.it_interval.tv_sec = 2; //每隔2秒启动一次
it.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &it, NULL);
while(1)
{
pause();
}
return 0;
}
可重入函数和不可重入函数: 查看系统的可重入函数 man 7 signal
导致函数成为不可重入函数:
1. 函数内部调用了malloc 或 free
2. 调用了标准IO库函数
3. 函数使用了非常量的全局/静态变量
4.函数调用了其他不可重入函数
信号的内核表示: (数据结构为位图)
信号的递达(delivery): 处理信号的处理动作称为信号递达
信号的未决:从信号产生到信号抵达之间的状态称之为信号未决(pending)信号阻塞(blocking):
1.SIGHUP信号未阻塞也未发生过,当它抵达时执行默认的处理动作
2.SIGINT信号产生过,但正在被阻塞,所以暂时不能抵达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再接触阻塞。
3.SIGQUIT信号从未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义的函数handler
说明:如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次。从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
进程的信号屏蔽子的设置或检查:(信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前的进程收到)
int sigprocmask(int how, //SIG_BLOCK : mask = mask | set 在原来内核屏蔽字的基础上添加set//SIG_UNBLOCK : mask = mask & ~set 解除set
//SIG_SETMASK : mask = set 直接替换
const sigset_t *set,
sigset_t *oldset); //返回原来的信号屏蔽信息
返回值: -1 表示失败,并设置errno, 0 表示成功
说明:若oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。
信号集操作函数:信号集---能表示多个信号的数据结构
sigset_t set;
设置信号初值
int sigemptyset(sigset_t* set)
{
memset(set, 0, sizeof(sigset_t));
}
//添加信号
int sigaddset(sigset_t* set, int signum)
{
*set |= 1<<(signum - 1);
}
//在原来的基础上删除信号
int sigdelset(sigset_t* set, int num)
{
*set &= ~(1<<(num-1));
}
//判断信号是否在信号集中
int sigismember(sigset_t* set, int num)
{
return *set & 1<<(num - 1);
}
说明:函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
获得内核的未决信号集:
int sigpending(sigset_t* set);
返回值:成功返回0 ,失败返回-1
说明:设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> //循环的获得搜有的信号信息,验证信号未决前后的变化。
void handler(int s)
{
printf("recv = %d\n", s);
}
void handler_quit(int s)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT); //解除SIGINT的屏蔽
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
int main()
{
signal(SIGINT, handler);
signal(SIGQUIT, handler_quit);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
while(1)
{
sigset_t pset;
int i = 0;
sigemptyset(&pset);
sigpending(&pset);
for(i = 1; i < _NSIG; i++)
{
if(sigismember(&pset, i))
{
printf("1");
}
else{
printf("0");
}
}
printf("\n");
sleep(1);
}
return 0;
}
竞态条件和sigsuspend函数:
先来看一段代码:Mysleep.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int s){}
int mysleep(int sec)
{
signal(SIGALRM, handler);
sigset_t set;
sigprocmask(SIG_BLOCK, &set, NULL);
alarm(sec);
pause();
int r = alarm(0);
return r;
}
int main()
{
mysleep(3);
printf("3 sec\n");
}
这段代码的执行结果是正常的,也是我们想要的结果:三秒之后返回。
现在我们假设有一新的(极端)状态:
1.调用alarm函数的时候,内核调度优先级更高的进程取代了当前进程执行(或者时间片到,CPU去执行其他任务),并且这个进程执行了很长时间.
2.闹钟时间超时,内核发送SIGALRM信号给这个进程,处于未决状态
3.优先级更高的进程执行完了,内核回到当前进程执行,SIGALRM信号递达,执行处理函数handler。
4.返回进程的主控制流程,alarm(sec)返回,调用pause()挂起等待
5.这时的SIGALRM信号已经处理完了,pause将一直阻塞
解释:出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(sec)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(sec)之后的sec秒之内被调用。由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件(Race Condition)。
解决方法:
int sigsuspend(const sigset_t *mask); <===> pause + 信号屏蔽 ----屏蔽mask中规定的信号
函数执行期间 mask的信号屏蔽会覆盖内核的信号屏蔽,函数返回,内核的信号屏蔽恢复。如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
重写上面的Mysleep.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int s){}
int mysleep(int sec)
{
signal(SIGALRM, handler);
sigset_t set, old;
sigemptyset(&set);
sigemptyset(&old);
sigaddset(&set, SIGALRM);
sigprocmask(SIG_BLOCK, &set, NULL);
alarm(sec);
// pause();
sigset_t pset;
sigemptyset(&pset);
sigsuspend(&pset);
sigprocmask(SIG_UNBLOCK, &pset, NULL);
int r = alarm(0);
return r;
}
int main()
{
mysleep(3);
printf("3 sec\n");
}
1. 调用sigprocmask(SIG_BLOCK, &set, NULL);时屏蔽SIGALRM。
2. 调用sigsuspend(&pset);时解除对SIGALRM的屏蔽,然后挂起等待待。
3. SIGALRM递达后suspend返回,自动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM。
4. 调用sigprocmask(SIG_UNBLOCK, &pset, NULL);时再次解除对SIGALRM的屏蔽。