- 介绍
- 1. 信号的概念
- 2. signal
- 3. 信号的不可靠性
- 4. 可重入函数
- 5. 信号的响应过程:过程图
- 6. 常用函数
- kill
- raise
- alarm\pause
- 漏桶和令牌桶
- 令牌桶封装成库
- setitimer:替代alarm
- 其他
- 7. 信号集:sigemptyset
- 8. 信号屏蔽字/pending集的处理:sigprocmask
- 9. 拓展内容
- sigsuspend
- sigaction:替代signal
- 10. 实时信号
- 11. 总结:信号安全
介绍
1. 信号的概念
信号是软件中断
信号的实现依赖中断
前31个:标准信号; 34~64:实时信号
标准信号会丢失,实时信号不会
ulimit -c
产生core文件
报段错误产生core文件,然后gdb调试core文件
2. signal
##include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);//相当于: (防止命名空间冲突)void **(*** signal(int signum, void (*function) (int)) **)** (int)
注册信号处理函数
- 参数:响应的信号(用宏更好); 处理函数 或者
SIG_IGN[忽略]
,SIG_DFL[默认]
- 返回值:以前定义的旧行为
例子: ctrl+c 发送 SIGINT
信号,注册新的处理函数
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<unistd.h>
##include<signal.h>static void int_handler(int s)
{write(1,"!",1);
}int main()
{int i;signal(SIGINT, int_handler);for(i=0; i<10; i++){write(1,"*",1);sleep(1);}exit(0);
}
运行结果:
问题:
快速按下 ctrl+c
,程序执行时间不足10s
之前学过的所有io都是阻塞io,所以出错时都要考虑是否是信号错,如果是,相当于是假错,重试,否则才报错
open\read\write的errno都有 EINTR ,说明都会被信号打断。可以这么修改:先判断,如果真错就退出,假错就重试。
3. 信号的不可靠性
行为不可靠,处理函数没有手动调用,所以是内核布置现场,因此同一时间内一个信号出现多次,后面响应信息可能会把前面的覆盖。
4. 可重入函数
解决上述问题:
一个函数有 _r
版本,那么前一个版本必然不能用作信号处理函数。
同时说明前一个版本的指针必然是指到了 static 静态区(因为会互相覆盖):
_r
版本 成为可重入的方法是调用时传入了地址空间,避免了数据冲突
下面的函数函数同理:
5. 信号的响应过程:过程图
-
内核为进程维护了一组位图,mask和pending,是32位的,对应32个标准信号。
进程在扎入内核等待调度的时候,会保存执行现场,包括返回地址address
-
当进程被调度,从kernel返回到user的时候,会
mask & pending
,哪一位为1,说明哪一个信号到来 -
知道哪个信号到来后,执行信号处理函数:将 address 修改为信号处理函数的入口地址,将mask和pending的对应位置为0,然后跳转到信号处理函数
-
执行完信号处理函数后,将mask置为1,再次检查
mask & pending
为什么会有不可避免的延迟:信号的实现依赖与中断扎内核,从内核出来的时候才会检测信号,所以又延迟。
为什么会丢失: 因为是位图,所以一个信号来100次,置100次1,还是1
如何忽略一个信号: 设置SIG_IGN[忽略]
把 mask 对应位置为0
不能从信号处理函数随意往外跳:会错过第二次扎内核的过程,导致 mask 没有从0恢复到1,可以用 sigsetjmp
和 siglongjmp
解决。
细化到线程维度: mask & pending
实际进行两次,一次和进程pending,一次和线程pending
6. 常用函数
kill
##include <sys/types.h>
##include <signal.h>
//给某个进程发信号
int kill(pid_t pid, int sig);
pid_t
-
0:指定的进程
- 0:组内广播
- -1:全局广播
- -x:第x组的进程
-
-
返回值:0成功,非0失败。错误分为三种:
raise
##include <signal.h>
//给当前进程发信号
int raise(int sig);
- 等价于:
kill(getpid(), sig)
或pthread_kill(pathread_self(), sig)
alarm\pause
##include <unistd.h>
//等待 secondes 秒,发送SIGALRM 信号(默认行为是终止进程)
unsigned int alarm(unsigned int seconds);
- 无法实现多任务,重复设置谁先生效听谁的。
- 比
time()
函数更精确,ms级别
##include <unistd.h>
//sleep,知道被一个信号唤醒,防止忙等待
int pause(void);
例子:5sec.c
示例代码:
##include<stdlib.h>
##include<stdio.h>
##include<unistd.h>
##include<time.h>int main()
{long long cnt = 0;time_t end;end = time(NULL)+5;while(time(NULL)<end)cnt++;printf("%lld\n", cnt);exit(0);
}
##include<stdlib.h>
##include<stdio.h>
##include<unistd.h>
##include<signal.h>static int loop = 1;
static void alarm_handler(int s)
{loop = 0;
}int main()
{long long cnt = 0;signal(SIGALRM, alarm_handler);alarm(5);while(loop)cnt++;printf("%lld\n", cnt);exit(0);
}
运行结果:
-
其他问题:加上O1优化:死循环无法退出
前
后:死循环
循环里没用到loop,优化认为loop不变,于是直接取值,没有去loop的地址里取值,loop恒为1
volitale作用是声明该变量可能由于其他外部环境所改变
漏桶和令牌桶
例子:修改mycopy(目标设为fd1标准输出)为slowcat。每秒读取10个字符,alarm用作流量控制[sleep移植性不足]
漏桶:娟娟溪流,无法应对突发大数据
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<sys/stat.h>
##include<fcntl.h>
##include<unistd.h>
##include<signal.h>
##include<errno.h>##define CPS 10
##define BUFSIZE 1024static volatile int loop = 0;
static void alarm_handler(int s)
{//链式调用,一环接一环alarm(1);loop = 0;
}int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr, "Usage:...\n");exit(1);}//输出位置为fd1size_t sfd, dfd=1;int len=0, ret, pos;char buf[BUFSIZE];//设置信号处理函数signal(SIGALRM, alarm_handler);alarm(1);do{sfd = open(argv[1], O_RDONLY);if(sfd < 0){//出假错:被信号打断,则重试if(errno != EINTR){perror("open()");exit(1);}}}while(sfd < 0);while(1){//避免忙等待while(loop)pause();loop = 1;//读取10个字节 while((len = read(sfd, buf, CPS)) < 0){if(len < 0){//出假错:被信号打断,则重试if(errno != EINTR){perror("read()");close(sfd);exit(1);}}if(len == 0)break;}pos = 0;while(len > 0){ret = write(dfd, buf+pos, len);if(ret < 0){perror("write()");break;}pos += ret;len -= ret;}}close(sfd);exit(0);
}
运行结果:
没用pause,cpu沾满
用了pause就不会
注意点:
- alarm链式调用和处理函数的书写
while((len = read(sfd, buf, CPS)) < 0)
当没读到内容的时候就重新尝试读。
令牌桶:可以处理突发数据
写法token
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<sys/stat.h>
##include<fcntl.h>
##include<unistd.h>
##include<signal.h>
##include<errno.h>##define CPS 10
##define BUFSIZE 1024
##define BRUST 100static int token = 1;
static void alarm_handler(int s)
{alarm(1);token++;if(token > BRUST)token = BRUST;}int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr, "Usage:...\n");exit(1);}size_t sfd, dfd=1;int len=0, ret, pos;char buf[BUFSIZE];signal(SIGALRM, alarm_handler);alarm(1);do{sfd = open(argv[1], O_RDONLY);if(sfd < 0){//出假错:被信号打断,则重试if(errno != EINTR){perror("open()");exit(1);}}}while(sfd < 0);while(1){while(token < 0)pause();token --;//读取10个字节 while((len = read(sfd, buf, CPS)) < 0){if(len < 0){if(errno != EINTR){perror("read()");close(sfd);exit(1);}}if(len == 0)break;}pos = 0;while(len > 0){ret = write(dfd, buf+pos, len);if(ret < 0){perror("write()");break;}pos += ret;len -= ret;}}close(sfd);exit(0);
}
运行结果:同上
思路:处理突发数据体现在哪里
while((len = read(sfd, buf, CPS)) < 0)
当没读到内容的时候重新尝试读,并且token++,相当于积攒令牌,以后有大数据来的时候,就可以连续读取多次。
问题: token- -
不一定原子【信号原子类型】
令牌桶封装成库
文件
main.c
:模拟应用实例,以slowcat1.c
(令牌桶) 为基础mytbf.c
:库文件,动态连接到应用程序,实现令牌桶mytbf.h
:头文件
基本思路:
令牌桶数据作为一个结构体存放到到数组中,结构体内容包括 cps,burst,token
给用户提供四个库函数
mytbf_t * mytbf_init(int cps, int brust);
初始化令牌桶,成功放回令牌桶结构体指针,失败返回 NULLint mytbf_fetchtoken(mytbf_t *ptr, int size);
:希望从令牌桶中拿到size个令牌(令牌:字符=1:1),返回实际拿到的值 min(token, size);没有令牌则阻塞。int mytbf_returntoken(mytbf_t *ptr, int size);
:归还size个令牌。int mytbf_destroy(mytbf_t *ptr);
:销毁当前令牌桶
示例代码:
##ifndef MYTBF_H__
##define MYTBF_H__##define MYTBF_MAX 1024
//隐藏定义,真正的定义在mytbf.c中
typedef void mytbf_t;mytbf_t * mytbf_init(int cps, int brust);int mytbf_fetchtoken(mytbf_t *, int);int mytbf_returntoken(mytbf_t *, int);int mytbf_destroy(mytbf_t *);##endif
##include<stdio.h>
##include<stdlib.h>
##include<signal.h>
##include<unistd.h>##include<string.h>
##include<errno.h>##include "mytbf.h"struct mytbf_t
{int cps;int burst;int token;int pos;
};typedef void (*sighandler_t)(int);
static struct mytbf_t* job [MYTBF_MAX];
static int inited = 0;
static sighandler_t alarm_handler_save;static void alarm_handler(int s)
{int i;//alarm链式调用alarm(1);for(i = 0; i < MYTBF_MAX; i++){if(job[i] != NULL){job[i]->token += job[i]->cps;if(job[i]->token > job[i]->cps)job[i]->token = job[i]->cps;}}
}//模块卸载:在进程结束的时候调用,钩子函数
static void module_unload(void)
{int i;//恢复信号处理函数,关闭alarm信号signal(SIGALRM, alarm_handler_save);alarm(0);for(i = 0; i < MYTBF_MAX; i++)free(job[i]);
}static void module_load(void)
{alarm_handler_save = signal(SIGALRM, alarm_handler);alarm(1);//挂钩atexit(module_unload);
}static int get_free_pos(void )
{int i;for(i = 0; i < MYTBF_MAX; i++){if(job[i] == NULL)return i;}return -1;
}mytbf_t *mytbf_init(int cps, int burst)
{struct mytbf_t *me;int pos;//alarm:注册和发送只能调用一次if(!inited){module_load();inited = 1;}me = malloc(sizeof(*me));if(me == NULL)return NULL;pos = get_free_pos();if(pos < 0)return NULL;me->token = 0;me->cps = cps;me->burst = burst;me->pos = pos;job[pos] = me;return me;
}static int min(int a, int b)
{if(a < b)return a;else return b;
}int mytbf_fetchtoken(mytbf_t *ptr, int size)
{struct mytbf_t *me = ptr;int n;if(size < 0)return -EINVAL;//阻塞:一直等到有token位置才继续while(me->token <= 0)pause();n = min(me->token, size);me->token -= n;return n;
}int mytbf_returntoken(mytbf_t *ptr, int size)
{struct mytbf_t *me = ptr;if(size <= 0)return -EINVAL;me->token += size;if(me->token > me->burst)me->token = me->burst;return size;
}int mytbf_destroy(mytbf_t *ptr)
{struct mytbf_t *me = ptr;job[me->pos] = NULL;free(ptr);return 0;free(me);
}
##include<stdio.h>
##include<stdlib.h>
##include<sys/types.h>
##include<sys/stat.h>
##include<fcntl.h>
##include<unistd.h>
##include<signal.h>
##include<errno.h>
##include<string.h>
##include"mytbf.h"##define CPS 10
##define BUFSIZE 1024
##define BURST 100/*static int token = 1;
static void alarm_handler(int s)
{alarm(1);token++;if(token > BRUST)token = BRUST;}*/int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr, "Usage:...\n");exit(1);}size_t sfd, dfd=1;int len=0, ret, pos, size;char buf[BUFSIZE];mytbf_t *tbf;/* signal(SIGALRM, alarm_handler);alarm(1);*/tbf = mytbf_init(CPS, BURST);if(tbf == NULL){fprintf(stderr, "mytbf_init() failed!\n");exit(1);}do{sfd = open(argv[1], O_RDONLY);if(sfd < 0){if(errno != EINTR){perror("open()");exit(1);}}}while(sfd < 0);while(1){/*while(token < 0)pause();token --;*///拿取令牌size = mytbf_fetchtoken(tbf, BUFSIZE);if(size < 0){fprintf(stderr,"mytbf_fetchtoken():%s\n", strerror(-size));exit(1);}while((len = read(sfd, buf, size)) < 0){if(len < 0){if(errno != EINTR){perror("read()");close(sfd);exit(1);}}if(len == 0)break;}pos = 0;//归还tokenif(len-size > 0)mytbf_returntoken(tbf, size-len);while(len > 0){ret = write(dfd, buf+pos, len);if(ret < 0){perror("write()");break;}pos += ret;len -= ret;}}close(sfd);mytbf_destroy(tbf);exit(0);
}
all:mytbfmytbf:main.o mytbf.ogcc $^ -o $@clean:rm -rf *.o mytbf
运行结果:
setitimer:替代alarm
同样是定点发送 SIGALRM
:替代alarm,更好用,不需要链式使用,不会累计误差。
##include <sys/time.h>
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
- 参数
itimerval
: 两个成员,都是timeval
类型的结构体,it_interval
会在it_value
到时的时候重新填充it_value
new_value
是我们填入的计时参数,old_value
用来回填原来的计时参数,类似于signal
返回原理的信号处理函数
- 返回值:成功0,失败-1
例子:setitimer替换alarm
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<unistd.h>
##include<sys/time.h>int main()
{//alarm(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;if(setitimer(ITIMER_REAL, &itv, NULL) < 0){perror("setitimer()");exit(1);}while(1)pause();exit(0);
}
运行结果:
其他
7. 信号集:sigemptyset
8. 信号屏蔽字/pending集的处理:sigprocmask
sigprocmask相当于给 pending 数组置位, sigprcmask(SIG_SETMASK, sigset_t oset, xxx) 相当于把 oset 代表的当前 pending 数组赋给当前进程
无法控制信号的到来,但可以控制什么时候响应
9. 拓展内容
sigsuspend
##include <signal.h>
int sigsuspend(const sigset_t *mask);
- 作用:等待信号集里的一个信号
- 流程:
- 设置
mask
- 阻塞,等待信号到来
- 恢复为原来的
mask
- 设置
例子:信号驱动程序【pause实现】
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<signal.h>
##include<unistd.h>static void int_handler(int s)
{write(1,"!",1);
}int main()
{int i,j;sigset_t set, oset, saveset;signal(SIGINT, int_handler);sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_UNBLOCK, &set, &saveset);for(i = 0; i < 1000; i++){sigprocmask(SIG_BLOCK, &set, &oset);for(j = 0; j < 5; j++){write(1,"*",1);sleep(1);}write(1,"\n",1);sigprocmask(SIG_SETMASK, &oset, NULL);pause();}sigprocmask(SIG_SETMASK, &saveset, NULL);exit(0);
}
运行结果:
ctrl + \ 发送 quit 信号退出
思路:
加上 puase
就可以做出信号驱动程序的样子,但是我们信号打断打印 * 的系统调用,所以把信号阻塞了(pending = 1 但由于 mask = 0,所以收到但不响应)。
存在问题:
sigprocmask(SIG_UNBLOCK, &set, NULL); pause();
两个操作不原子: sigprocmask
也是系统调用,导致它扎内核回来后,检查mask&pending,发现有信号来,直接去响应了,相应完后pending=0, pause
没有察觉,即没来得及 pause
就直接把信号响应掉了。
例子:信号驱动程序【suspend】实现
示例代码:
##include<stdio.h>
##include<stdlib.h>
##include<signal.h>
##include<unistd.h>static void int_handler(int s)
{write(1,"!",1);
}int main()
{int i,j;sigset_t set, oset, saveset;signal(SIGINT, int_handler);sigemptyset(&set);sigaddset(&set,SIGINT);sigprocmask(SIG_UNBLOCK, &set, &saveset);//多了这一句话:类似do whilesigprocmask(SIG_BLOCK, &set, &oset);for(i = 0; i < 1000; i++){for(j = 0; j < 5; j++){write(1,"*",1);sleep(1);}write(1,"\n",1);sigsuspend(&oset);// 相当于下面的原子操作:/*sigset_t tmpset;sigprocmask(SIG_SETMASK, &oset, &tmpset);pause();sigprocmask(SIG_SETMASK, &tmpset, &oset);*/}sigprocmask(SIG_SETMASK, &saveset, NULL);exit(0);
}
运行结果:
信号不会再 sigprocmask和pause之间被响应掉,导致pause无法收到信号,以至于阻塞
sigaction:替代signal
信号处理函数的参数是为了区分注册信号
有重入危险,类似于嵌套中断
signal不区分信号来源
使用三参信号处理函数
10. 实时信号
与系统信号的区别
- 不丢失
- 响应顺序未定义
例子:用实时信号重构 suspend.c
,验证实时信号不丢失
示例代码:
运行结果:
kill -l
查看所有信号,前34个是系统信号
kill -x process
向某进程发送 x
信号,默认是 -9
SIGKILL
信号
ulimit -a
查看实时信号的最大排队数量
11. 总结:信号安全
信号是先被使用,后成为事实标准,所以肯定会有边角无法纳入标准,所以使用的时候要比较注意。
如:
因此只能使用一些简单的数值计算和库函数,但是多线程没有这种问题,因为它已经先标准化后进行实现。