4、信号并发(李慧琴课程笔记)
4.1信号
异步事件的处理:查询法,通知法
信号的是软件中断,信号的响应依赖于中断。
4.1.1 signal函数
一般用下面这种形式
void (*signal(int signum, void (*func)(int)))(int);
/*signal返回值是在给一个信号定义新的行为时,顺便来保存旧的行为。*/
static void int_handler(int s)
{
write(1, "!", 1);
}
int main()
{
int i;
//signal(SIGINT,SIG_IGN);/*忽略掉SIGINT这个中断信号,其实也就是ctl+c*/
signal(SIGINT,int_handler);/*在程序没结束的时候,信号来了才会去执行int_handler函数里面的内容,信号会打断阻塞的系统调用*/
for(i=0; i < 10; i++)
{
write(1, "*", 1);
sleep(1);/*一秒钟打印一次*号*/
}
}
补充:信号不可靠
信号会打断阻塞的系统调用。
当阻塞等待打开或者完成一个设备的过程中,有可能会被signal handler打断,就会报错。
如下图所示,if(errno!=EINTR)会判断是真错还是假错,假错可能就是被信号打断了,那么重新执行一次OPEN函数。
4.1.2 可重入函数
**概念:**第一次调用还未结束,第二次调用已经开始,而且不会出错
所有的系统调用都是可重入的,一部分库函数也是可重入的,比如说memcpy。
还有就是rand和rand_r函数,rand_r函数就是可重入的,但是rand不行。
在看函数手册过程中,如果函数有_r的选项,那么原函数一定不可重入。
4.1.3 信号的相应过程
信号从收到到响应有一个不可避免的延迟。是因为有中断把我当前执行过程打断,我内核态往用户态走的时候才能响应这个信号
思考:1.如何忽略掉一个信号?
内核中有一个全为1的mask列表和一个全为0的pending列表,当当前进程被信号打断时,相应的pending位会被置1,这时候与上mask就知道是哪个中断类型,然后mask和pending位置0,从而进入内核态。这时候不是发生了中断么,那么肯定会保存当前进程的执行代码的地址,在执行中断完毕后回到这个位置,于此同时我们还要执行信号处理函数,因此这个地址会被替换为信号处理函数的地址,然后回到用户态,在执行完以后再替换回最开始的地址回到用户态继续执行,同时mask和pending位也要回复到原来的状态。
我们不能阻止信号的到来,但是可以决定信号是否被响应,也可以决定什么时候被响应。比如signal函数有一个选项ignore,就可以将mask永远置0,则永远匹配不了这个pending信号。
2.标准信号为什么要丢失?
是用位图来保存的,因此来了一万个可能也就一共置1次1。
3.信号是从kernel态回到user态的路上响应的。
4.标准信号的相应没有严格的顺序:
在有中断优先级的情况下还是有顺序的,但是如果是平级的话就没有严格的顺序了。
4.1.4 常用函数(kill、raise、pause)
sleep函数尽量不要使用,在有的环境下sleep是由alarm和pause封装的,而我们的一般是nanosleep封装的,所以考虑到移植一般不用sleep函数,因为如果是用alarm和pause封装的,那么alarm时间有可能会不准确,可能会出现另外一个alarm函数替换掉时间
alarm函数以秒为单位将SIGALRM信号传递给调用进程,SIGALRM的默认机制是杀死当前进程
Linux使用单一计时器,构造一组函数,实现任意数量的计时器_仅使用一个定时器,构造一组函数-优快云博客-----------------alarm相关
令牌桶实现-优快云博客----------------------------------alarm和signal相关
signal返回值是在给一个信号定义新的行为时,顺便来保存旧的行为。
1、int kill(pid_t pid, int sig);/*pid>0,给指定进程发送信号;给进程发送信号,pid为0,则给本组所有进程发送信号;pid为-1则给所有进程发一次信号除了一号初始进程;如果pid<-1则发送给进程组为-pid(也就是绝对值)的所有进程信号。sig为0则用于检查进程或者进程组是否存在,若返回值为0则存在,返回值为-1则需要判断错误类型,若为EINVAL,指定了一个无效信号,若为EPERM则为这个进程存在但不允许被发送信号,ESRCH则为这个信号不存在*/
2、raise(int sig);/*给当前进程或者线程发送信号,相当于kill(getpid(), sig);;在多线程情况下相当于pthread_kill(pthread_self(),sig);*/
3、alarm(unsigned int seconds);/*alram无法实现多函数的计时器,比如说我alarm(10)然后紧跟一个alarm(1)只会计数一秒然后退出程序,*/
4、pause();/*等待一个信号打断他,类似于阻塞*/
pause()函数:调用该函数(系统调用)的进程将处于阻塞状态(主动放弃cpu),直到有信号递达将其唤醒。
/*比如说*/
int main()
{
alarm(10);
alarm(1);
alarm(5);
while(1)
pause();
}
/*时间函数time与信号alarm的区别在与alarm更加精确,尤其是在变量优化之后,volatile优化要判断的变量*/
/*在alarm与signal信号之间一定要有顺序要求,即设置时钟信号的行为一定会在发出第一个alarm信号之前*/
/*必须如下所示
signal(SIGALRM,alarm_handler);
alarm(5);
*/
5、setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
/*
which有三个选项:1、ITIMER_REAL--------实时时间
2、ITIMER_VIRTUAL------虚拟时间,仅在进程进行时递减,然后到期时返回SIGVTALRM
3、ITIMER_PROF-------通常用于应用程序在用户和内核空间中花费的时间
setitimer可以实现周期时钟(因为itimerval结构体)
new_value:参数是一个指向itimerval结构的指针,用于设置新的定时器值;
old_value:参数则是一个指向itimerval结构的指针,用于获取旧的定时器值;
当it_value使用完以后,it_interval会自动填充到it_value,因此可以构成周期性时钟
struct itimerval{
struct timeval it_interval; -----next value
struct timeval it_value; ------current value
}
struct timeval{
long tv_sec;------seconds
long tv_usec;------microseconds
}
*/
6、abort();
/*调用abort函数会异常的终止进程,并且,会刷新缓冲区,因为abort函数内部调用了fflush(NULL),刷新缓冲区不代表调用了fclose。*/
7、system();
/*在有信号参与的程序当中,要阻塞住一个信号,然后要忽略掉SIGINT、SIGQUIT两个信号*/
8、sleep();
/* 尽量不要使用
可以用nanosleep来替代
可以用usleep来替代
*/
4.1.5 信号集
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
4.1.6 信号屏蔽字
信号中,以下两个函数不能轻易的从信号中跳出去
#include <setjmp.h>
int setjmp(jmp_buf env);/*有歧义,因此不能轻易跳出去*/
int sigsetjmp(sigjmp_buf env, int savesigs);/*savesigs为真保存掩码集,为假不保存掩码集,同样不能轻易跳出*/
1、sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*
how---三种方式:1、SIG_BLOCK 阻塞,相当于屏蔽这个信号
2、SIG_UNBLOCK 非阻塞,解除对这个信号的屏蔽
3、SIG_SETMASK 如果这项被选定 则是恢复set的内容并且保存恢复之前oldset的信号情况
例子:sigprocmask(SIG_BLOCK, &set, &oldset);
sigprocmask(SIG_SETMASK,&oldset,NULL);
例子:一开始设置为SIG_BLOCK 之后连续发送很多次信号过来,在SIG_UNBLOCK之后,该信号只会响应一次
set------针对信号集的所有信号,比如我set放入三个信号,然后选择how里面三种方式中的一种
oldset------保留对mask执行动作之前mask的状态,有的情况需要用到oldset这个变量来保存之前的状态,在一系列操作以后返回之前的这个状态
*/
2、int sigpending(sigset_t *set);
/*
涉及到pending集的处理
*/
4.1.7 扩展
#include <signal.h>
int sigsuspend(const sigset_t *mask);/*
sigsuspend的整个操作都是原子的,也就不允许被打断的。sigsuspend的整个原子操作过程为:
(1) 设置新的mask阻塞当前进程;
(2) 收到信号,恢复原先mask;
(3) 调用该进程设置的信号处理函数;
(4) 待信号处理函数返回后,sigsuspend返回。
类似于原子化的pause()函数
*/
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*
struct sigaction{
void (*sa_handler)(int); 左侧两个void函数选其一用
void (*sa_sigaction)(int ,siginfo_t *, void *); 较为重要,参数int可以是多个信号集合,第二个参数可以决定响应的信号源,具体可以查看man手册,其中si_code用的比较多
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
如果sa_flags为SA_SIGINFO则表示三参的信号处理函数。
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
*/
4.1.8 实时信号
实时信号不会像标准信号一样丢失。