#include <signal.h>
信号signal:Linux或Unix系统支持的经典的消息机制,用于处置进程,挂起进程或杀死进程
kill -l #查看系统支持的信号
1~31 Unix经典信号(软件开发工程师)
32、33信号被系统隐藏,不对用户开放,供NPTL线程库使用
34~64 自定义信号/实时信号(驱动工程师)
Ctrl+C 系统帮你发送了 2)SIGINT信号。终端组合按键产生的信号,杀死唯一的前台终端进程。
一、系统中触发信号的几种方式:
1、终端组合按键触发信号
Ctrl+C SIGINT/2 终止进程 Ctrl+\ SIGQUIT/3 退出进程 Ctrl+Z SIGTSTP/20 挂起终端进程
jobs #查看挂起的作业编号
fg 作业编号 #唤醒到前台
tty 终端 pts 虚拟终端
D (TASK_UNINTERRUPTIBLE) 不可中断的睡眠状态
R (TASK_RUNNING) 正在运行,或在队列中的进程
S (TASK_INTERRUPTIBLE) 可中断的睡眠状态
T (TASK_STOPPED) 停止状态
t (TASK_TRACED) 被跟踪状态
Z (TASK_DEAD - EXIT_ZOMBIE) 退出状态,但没被父进程收尸,成为僵尸状态
W 进入内存交换(从内核2.6开始无效)
X (TASK_DEAD - EXIT_DEAD) 退出状态,进程即将被销毁
< 高优先级
N 低优先级
L 有些页被锁进内存
s 包含子进程
+ 位于前台的进程组;
l 多线程,克隆线程 multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
2、命令触发信号
kill -signo信号 pid进程号 #向任意进程发送任意信号
3、函数触发信号
kill(pid_t pid,int signo); //向任意进程发送任意信号
raise(int signo); //向当前进程发送任意信号
abort(); //向当前进程发送固定的SIGABRT/6信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main(int argc,char* argv[]){
//支持命令,KILL signo pid
if(argc<3){
printf("pram error\n");
exit(0);
}
kill(atoi(argv[2]),atoi(argv[1]));
return 0;
}
4、硬件异常产生信号
违规访问使用硬件,导致信号杀死进程
1)如果进程非法操作内存,系统向其发送SIGSEGV/11杀死进程,错误信号为段错误
char* str="hello";
str[0]='H'; //只读内存的写操作
2)如果进程出现cpu运算异常,系统向其发送SIGFPE/8杀死进程,错误为浮点数例外
int b=0;
int a=8/b; //除0
3)如果进程出现内存访问越界,系统向其发送SIGBUS/7杀死进程,错误为总线错误
int* ptr=NULL;
int fd=open("MapFile",O_RDWR);
int size=lseek(fd,0,SEEK_END);
ptr=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//在使用mmap内存映射时,若"MapFile"是内容为空的新文件大小为0,没有与之对应的合法的物理页,mmap不能扩展,发生了越界访问
//添加错误检测来防止总线错误
if((ptr=mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED){//内存映射
perror("mmap failed");
exit(0);
}
5、软件异常产生信号
可以使用某个组件,但是如果使用时触发软条件,系统会杀死进程(信号)
1)定时器alarm(10),定时到时,系统向定时进程发送SIGALRM/14信号,默认杀死进程
alarm(1); //定时1s后杀死进程,返回“闹钟”
2)匿名管道读端关闭,写端向管道写数据,系统向写端发送SIGPIPE/13信号,杀死进程
二、信号的三大行为与处理动作:
信号的行为可以被修改,默认情况下为默认行为,但是可以改为忽略或捕捉(三选一)
1、默认行为SIG_DFL:默认五种处理动作(五选一)
(1)TERM 直接杀死进程(2)CORE 杀死进程并且转储核心(3)STOP 挂起进程(4)CONT 忽略/继续进程(5)IGN 忽略
每个信号都有自己的默认动作来处置进程,进程不是被挂起就是被杀死。只有动作为IGN的信号,不会处置进程。
可以通过信号结束进程的输出,分析其动作
2、忽略行为SIG_IGN:忽略行为没有处理动作,无法处置进程
3、捕捉行为SIG_ACTION:可以手写捕捉函数来自定义行为
void sig_do(int n/*信号编号*/);
信号捕捉可以让自定义的捕捉函数与信号绑定,以后触发此信号,系统就会去执行此函数
可以用来设计实现条件触发的工作和任务,信号触发则执行,否则不执行。
修改信号行为
每个信号都有一个自己的信号行为结构体,修改信号行为要保留其原结构体,便于复位
struct sigaction nact,oact; //信号行为结构体
act.sa_handler=SIG_DFL|SIG_IGN|函数指针;
act.sa_flags=0; //默认选项
act.sa_mask; //临时屏蔽字
sigaciton(signo,&nact,&oact); //替换信号行为结构体,act为新的,oact传出进程原有的
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sig_do(int n){
printf("已经成功捕捉SIGINT,signo=%d\n",n);
}
int main(){
//void(*sa_handle)(int)
struct sigaction act,oact;
act.sa_handler=sig_do;
act.sa_flags=0;
sigemptyset(&act.sa_mask);//初始化
sigaction(SIGINT,&act,&oact);//替换信号行为
while(1)
sleep(1);
return 0;
}
三、三种让信号生效的方法:
1、信号忽略,将信号设置为忽略行为(递达但是没有处理动作)
2、信号捕捉,对信号进行捕捉设定,绑定捕捉函数
3、信号屏蔽:阻塞信号的传递(没有递达)
如果信号通过未决信号集,系统会将未决中对应的位设置为1,标记未决同时避免相同的信号同时处理。
如果未决为1,对应的信号直接丢弃,不处理
信号的处理不支持排队(经典信号不支持排队队列,但是自定义信号可以),最大处理1个
驱动开发软件需要支持排队,因为每个信号与功能绑定,功能需要排队处理。经典信号目的是为了杀死进程,处理一次即可,无需排队。
信号通过屏蔽字,从未决态切换为递达态,系统将未决的1设置回0。系统会对正在处理的信号的屏蔽字临时设为1,等待信号处理完将屏蔽字设回0(避免如果捕捉函数中使用全局资源,信号多次执行出现异常)相同信号同时触发,最大可以处理两个,一个正在处理,一个被屏蔽,其他被丢弃。
如果将某个信号对应的屏蔽字位设置为1,该信号被阻塞,不允许递达。
用户可以自行设置信号屏蔽字,实现阻塞信号的效果。
屏蔽方式与忽略与捕捉有很大的不同。忽略与捕捉吸纳后已经递达了并处理完成了,但是屏蔽属于延迟处理,信号没有消失。
信号屏蔽设置:
sigset_t set; //信号集类型
sigemptyset(&set); //初始化0
sigfillset(&set); //初始化1
sigaddset(&set,signo); //将某个特定信号位置设置为1
sigdelset(&set,signo); //将某个信号位设置0
int sigismember(&set,signo); //返回特定信号位的位码
sigprocmask(SIG_SETMASK,&newset,&oldset); //设置替换进程的信号屏蔽字
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int main(){
sigset_t set,oset;
sigemptyset(&set);
sigaddset(&set,SIGQUIT);
sigprocmask(SIG_SETMASK,&set,&oset);
while(1)
sleep(1);
return 0;
}
四、高级信号与普通信号
高级信号:只要发出必然递达,无法屏蔽捕捉忽略
SIGKILL/9 必然杀死
SIGSTOP/19 必然挂起
硬件异常的信号SIGSEGV SIGFPE SIGBUS,用户发出触发可以屏蔽,但是系统触发不可屏蔽
查看当前进程实时的信号情况,应该查看未决信号集
sigpending(&pset); //调用此函数,系统会传出当前进程的未决信号集
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
int main(){
sigset_t set,oset,pset;
sigemptyset(&set);
sigaddset(&set,SIGINT);//2
sigaddset(&set,SIGQUIT);//3
sigaddset(&set,SIGSEGV);//11
sigaddset(&set,SIGALRM);//14
sigprocmask(SIG_SETMASK,&set,&oset);
while(1){
sigpending(&pset);//传出进程未决
for(int signo=1;signo<32;signo++)
printf("%d",sigismember(&pset,signo));
printf("\n");
sleep(1);
}
return 0;
}
信号的处理不是实时的
系统发出的信号在内核层,而程序运行在用户层
1.进程执行于用户层,串行执行主函数代码
2.信号发送到内核层,等待处理
3.满足切换条件,切换到内核层
触发上下文切换(cpu权限切换)的三种事件:系统调用、软件中断(时间片完)、异常
4.完成调用、处置中断、处置异常
5.在返回用户层前,检查是否有未递达信号,如果有则处理
6.发现捕获函数在用户层使用,系统使用指令直接执行捕捉函数,避免切换(执行捕捉函数,系统并没有降低权限,避免了开销)
7.执行完毕通过SIG_RETURN指令返回内核层
8.执行上下文切换,返回用户空间
9.主函数从暂停的位置继续执行
一般情况下,主函数率先执行,但是执行过程中触发信号,系统调用捕捉函数,捕捉函数永远比主函数先执行完
主函数运行时使用当前进程的时间片,捕捉函数调用时主函数暂停使用当前进程的时间片(一个进程的时间片资源只有一份)
五、信号的全局变量异步IO
主函数和捕捉函数相对独立,一个全局变量被主函数和捕捉函数一起使用,一定会发生冲突
如果捕捉函数里面访问使用全局数据,会导致数据异常或结果异常
谨慎在捕捉函数中使用全局资源(全局变量、磁盘文件等共享数据)
常见错误:编译错误 运行错误 不定时报错 结果异常
六、针对信号的可重入不可重入
不可重入函数:使用全局数据或静态数据,导致异常(信号开发中不建议使用不可重入函数)
可以将不可重入函数改造为可重入函数,例如:使用局部数据进行insert,避免垃圾链表
readdir()返回文件夹指针,指向文件夹记录项,记录项指针PTR是静态参数
当readdir()被main和捕捉函数一起调用,可能导致可以读取部分数据名单时无法完成读取所有记录项
近乎所有的不可重入函数都可以改造为可重入的,系统开发时通常会提供可重入和不可重入的两个版本。
首先确定函数中是否使用了全局或静态资源,要使用对应的-r版本函数
信号不支持调试(捕捉函数是系统进行调用的)
七、信号的实际使用
信号实现睡眠函数
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sig_alrm(int n){
return;
}
unsigned int mysleep(unsigned int seconds){
unsigned int reval;
//捕捉设定
struct sigaction act,oact;
act.sa_handler=sig_alrm;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM,&act,&oact);
reval=alarm(seconds);
pause();//立即挂起,察觉信号唤醒
return reval;
}
int main(){
while(1){
int s=1;
printf("sleep %d\n",s);
mysleep(s);
}
}
不安全:
1、pause()可以查觉任意信号唤醒,这导致如果定时未结束,进程察觉到其他信号,被提前唤醒
2、alram()和pause()不是原子操作,信号可能在挂起前被处理
sigsuspend(); //挂起的同时临时解除某个信号屏蔽,完成后再复位
时序静态
关键数据被别的进程截胡。可以采用原子方法避免时许问题,让触发关键数据的进程得到关键数据
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sig_alrm(int n){
return;
}
unsigned int mysleep(unsigned int seconds){
unsigned int reval;
//信号屏蔽 SIGALRM
sigset_t set,oset;
sigemptyset(&set);
sigaddset(&set,SIGALRM);
sigprocmask(SIG_SETMASK,&set,&oset);
//捕捉设定
struct sigaction act,oact;
act.sa_handler=sig_alrm;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM,&act,&oact);
reval=alarm(seconds);
sleep(2);
//解除屏蔽,使用不会被修改并一直为0的信号集
sigsuspend(&act.sa_mask);//临时解除屏蔽字,原来为1,临时修改为0,解除成功后复位1
//pause();//立即挂起,察觉信号唤醒
return reval;
}
int main(){
while(1){
int s=1;
printf("sleep %d\n",s);
mysleep(s);
}
}
八、信号完成进程间通信
union sigval{
int sival_int; //整型
void* sival_ptr; //地址
}val;
sigqueue(pid_t pid,signo,union sigval val); //发送信号并携带消息
只能选择系统未绑定特定事件的信号(用户信号)SIGUSR1/10 SIGUSER2/12,默认动作是杀死进程
信号传数据时,void (*sa_handler)(int n)无法用函数参数接收数据,使用新版void (*sa_sigaction)(int n,siginfo_t * info,void * arg)支持接收通信数据
void (*sa_sigaction)(int n,siginfo_t * info,void * arg);
info->si_int; //接收整型数据
info->si+ptr; //接收地址数据
通过继承屏蔽字,防止若父进程发送信号时,子进程未捕捉成功,导致子进程被杀死,变为僵尸进程
例:父子进程交叉报数,利用信号传递数据
九、相同信号的临时屏蔽
Linux操作系统信号的处理不支持排队,最大只能多处理一个,是因为当某个信号递达处理时,系统会临时屏蔽此信号,阻塞一个相同的信号,避免多个信号同时递达引发捕捉函数执行的冲突
十、僵尸进程回收
主动回收:阻塞回收,非阻塞轮询回收。父进程影响自身代码任务,有无意义的等待或轮询开销
被动回收:子进程死后系统给父进程发送SIGCHLD信号,通过捕捉函数进行回收
一个信号只回收一次,会造成遗漏
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void sig_wait(int n){
pid_t zpid;
//检测到一个信号回收一次
//优化:利用一个信号回收多次,避免回收遗漏
while((zpid=waitpid(-1,NULL,WNOHANG))>0){
printf("parent wait child success, zpid %d\n",zpid);
}
}
int main(){
pid_t pid;
int i=0;
//捕捉sigchld
struct sigaction act,oact;
act.sa_handler=sig_wait;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,&oact);
for(i;i<10;i++){
pid=fork();
if(pid==0)
break;
}
if(pid>0){
while(1){
printf("parent %d running...\n",getpid());
sleep(1);
}
}
else if(pid==0){
printf("child %d exit..\n",getpid());
exit(1);
}
else{
perror("fork failed");
exit(1);
}
}
阻塞函数也是需要时间片的