第九章 信号及信号处理
引言:信号是一种软件中断,他提供了一种处理异步事件的方法,也是进程间唯一的异步通信方式,在Linux系统内,根据posix标准扩展以后的信号机制,不仅可以用来通知某进程发生了什么事情,还可以给进程传递数据。
ps:有的知识点嵌入在注释里面
信号:64种,对于信号内核有写好的标准处理函数。
kill -l命令查看所有的信号,现在信号已经增加到65个了,但是在这里我要提一下,从33-64这些信号一般不会采用,这是为了区分可靠信号和不可靠信号而新增加的32个信号
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX
对于上面部分重要的信号的简单解释
(2) SIGINT ctrl +c 终止信号
(3) ctrl +\ 暂停信号,放入后台
(4) 非法指令
(5) abort 进程异常终止
(7) SIGBUS (虚实关系建立) 总线错误(从写的位置到物理内存,操作系统没有将磁盘的开始位置到物理内存之间建立 联系 mmap(把虚拟内存和磁盘文件的关系映射起来,如果磁盘大小大于0,就建立这种关系
(9) SIGKILL kill - 9 pid 杀死进程
(11) SIGSEGV 段错误
(13) 管道破裂
(14) 闹钟
(15) 缺省终止某个进程,终止掉
(17) 子进程死的时候会给父进程发送这个信号
(19) 进程暂停
(23) SIGURG 紧急数据
(29) 异步IO
可靠信号和不可靠信号
不可靠信号
Linux的信号继承自早期的Unix信号,Unix信号的缺陷
1.信号处理函数执行完毕,信号恢复成默认处理方式(Linux已经改进)
2.会出现信号丢失,信号不排队。
1-31 都是不可靠的,会出现信号丢失现象。
可靠信号
34-64重新设计的一套信号集合
不会出现信号丢失,支持排队,信号处理函数执行完毕,不会恢复成缺省处理方式
实时信号 : 就是可靠信号
非实时信号:不可靠信号
不可靠信号和可靠信号的区别在于前者不支持排队,只递送一次,可能会造成信号丢失,而后者不会。
信号有优先级,信号的优先级比较高,但是在所有信号中,编号越小的先递送,而且如果可靠信号和不可靠信号同时来,大多数情况先递送不可靠信号。
信号捕获处理
进程能够通过系统调用signal告诉内核, 它要如何处理信号, 进程有3个选择。
(1)接收默认处理(通常是消亡)
SIGINT的默认处理是消亡, 进程并不一定要使用signal接收默认处理,但是进程能够通过以下调用来恢复默认处理。
signal(SIGINT, SIG_DFL);
(2)忽略信号
程序可以通过以下调用来告诉内核, 它需要忽略SIGINT。
signal(SIGINT, SIG_IGN);
(3)信号处理函数
作用:程序能够告诉内核,当程序到来时应该调用哪个函数。
主要有两个函数可以实现
1.siganl函数
第二个参数是一个函数指针,传进去一个回调函数。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void myfunc(int no){
printf("catch you signal:%d\n",no);
}
int main(){
//第一个参数是屏蔽的信号,第二个参数是函数指针,siganl去调用
//信号产生就调用
//捕捉ctrl+c
//注册捕捉函数
//那个函数指针是typedef void(*sighandler_t)(int)
//sighandler_t signal (int signum,sighandler_t handler);
signal(SIGINT,myfunc);
/* signal(SIGINT,SIG_DFL);//默认处理 */
/* signal(SIGINT,SIG_IGN);//忽略 */
while(1){};
return 0;
}
2.sigaction函数
属于signal的加强版本
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myfunc(int no){
printf("%d\n",no);
sleep(3);
printf("week up\n");
}
int main(){
//函数原型个第一个捕捉的参数,第二个是一个结构体,第三个是上一次函数的处理,一般传null
//第二个参数是一个结构体,在第二个参数的结构体里
//需要在结构体里面做设置
//结构体里面第一个参数就是signal的第二个参数,第二个函数指针用的不多,两个函数指针二选一
//一般选择第一个
//在结构体里第二个参数中给可以添加信号,使该信号在处理函数执行过程中,临时屏蔽指定信号,处理结束时解除屏蔽
struct sigaction act;
act.sa_flags = 0;//只有在用第二个回调函数的时候用赋值
sigemptyset(&act.sa_mask);//清零mask
//可以在这里添加回调函数执行时的临时屏蔽函数
sigaddset(&act.sa_mask,SIGQUIT); //添加屏蔽信号
act.sa_handler = myfunc;
sigaction(2,&act,NULL);
while(1){};
return 0;
}
信号屏蔽
信号屏蔽又称为信号阻塞,实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。
另外进程可以忽略大多数信号(也可以被阻塞),少数不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。这里又牵扯到了信号集的概念了。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <wait.h>
int main(){
//可以手动屏蔽内核信号
//自定义一个内核信号集
//
sigset_t myset;
//集合清空
sigemptyset(&myset);
//添加要阻塞的信号
sigaddset(&myset,SIGINT); //crtl+c
sigaddset(&myset,SIGQUIT); //crtl+/
sigaddset(&myset,SIGKILL); //kill,不允许阻塞
//sig_block 相当于mask|set,set表示需要屏蔽的信号,常用
//sig_unblock 相当于mask&~set,set表示需要解除屏蔽的信号
//sig_setmask 当how设置为此,set表示用于替代原始屏蔽集的新屏蔽集,相当于覆盖mask = set
//只是设置到自定义信号集里面,通过自定义信号集和数据设置给内核的阻塞信号集
sigprocmask(SIG_BLOCK,&myset,NULL);
//每隔一秒读取一次内存中的未决信号集
//当恩了以后就存在未决信号集里面了
while(1)
{
sigset_t pendset;
sigpending(&pendset);//可以读取内核中的信号集
//1-31
for(int i=1;i<32;i++){
//对每一个信号进行一次判断,是否处于未决信号集
if(sigismember(&pendset,i)){
printf("1");
}
else{
printf("0");
}
}
putchar('\n');
sleep(1);
}
return 0;
}
这里有几篇帖子还不错
signal函数、sigaction函数及信号集操作函数
另外,关于sigaction()函数的使用
发送信号:
kill -信号值 pid
int kill(int pid,int signum)
pid值 | 操作 |
---|---|
pid > 0 | 发送给pid进程 |
pid = 0 | 调用者所在进程组的任一进程 |
pid = -1 | 有权发送的任何一个进程,除了1 |
pid < -1 | pid进程组所有的进程 |
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
int main(){
pid_t pid;
pid = fork();
if(pid == 0){
sleep(2);
kill(getppid(),9);
//所有进程pid都大于0
//当第一个参数为0时,杀死同一进程组的所有进程
//当第一个参数大与0时,杀死了该进程,不能杀死你权限不够的进程
//当小于-1时,杀死绝对值里面的组里面进程
//当为-1时,杀死所有能杀的进程
//子进程无条件杀死父进程
}
else{
while(1){
printf("ffffffpid = %d\n",getpid());
usleep(100000);
};
}
return 0;
}
进程组:进程组中有若干个进程
用管道连接的进程, fork创建的父子进程都属于同一个进程组
sleep 返回值 > 0 表示还剩多少秒没睡就被信号打断
给自己发信号
raise(int signum)//自己给自己发参数信号
kill(getpid() ,signum)//自己给自己发后面的信号
abort();自己异常退出
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
int main(){
pid_t pid;
pid = fork();
if(pid == 0){
sleep(1);
/* raise(9); */
/*kill(getpid(),9)*/
//子进程给自己发信号
//调用abort()函数,abort函数所在进程就死了
//
while(1){
abort();
//6 异常终止信号,直接死
}
}
else{
//父进程,回收子进程资源
//raise,给自己发信号
//kill给谁都能发
//父进程可以回收子进程的pcb
//当子进程死了就能查看子进程当时状态
int s;
pid_t wpid = wait(&s);
printf("child is died %d\n",wpid);
if(WIFSIGNALED(s)){//是否被信号弄死的
printf("died by signal %d\n",WTERMSIG(s));
}
}
return 0;
}
定时函数:
在程序开发过程中,我们时不时要用到一些定时器,通常如果时间精度要求不高,可以使用sleep,usleep函数让进程睡眠一段时间来实现定时,前者单位为秒(s),后者为微妙(us);但有时候我们又不想让进程睡眠阻塞在哪儿,我们需要进程正常执行,当到达规定的时间时再去执行相应的操作,在linux下面我们一般使用alarm函数跟setitimer函数来实现定时功能;
下面对这两个函数进行详细分析:
(1)alarm()函数
函数原型:
unsigned int alarm(unsigned int seconds); seconds 为指定的秒数
alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号;
如果sec是0,表示清除信号
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <wait.h>
int main(){
//一个程序运行一秒,实际上在用户态上不是一秒,
//在进行系统调用时在内核态用的时间也很长
//time的一秒是用户+内核
//当printf时,在用户区和内核区进行多次切换,进行文件io操作,优化代码的重点,是优化的瓶颈,内核区占得时间长
/* pid_t pid; */
/* pid = fork(); */
//alarm设置定时器(每个进程只有一个定时器)
//使用了自然定时法
//不受进程状态影响
//参数为秒,当时间到达发出一个信号,slgalrm,终止进程
//可以设置信号捕捉函数,进行其他事情
int ret = alarm(5);
printf("%d\n",ret);
sleep(2);
//重新设置定时器,在等两秒打印一下
ret = alarm(1);
printf("%d\n",ret);
int i = 0;
while(1){
printf("%d\n",i++);
}
return 0;
}
(2)setitimer()函数
函数原型:
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
time结构体
struct itimerval {
struct timeval it_interval; /* next value /
struct timeval it_value; / current value /
};
struct timeval {
time_t tv_sec; / seconds /
suseconds_t tv_usec; / microseconds */
};
在linux下如果对定时要求不太精确的话,使用alarm()和signal()就行了,但是如果想要实现精度较高的定时功能的话,就要使用setitimer函数。
setitimer()为Linux的API,并非C语言的Standard Library,setitimer()有两个功能,一是指定一段时间后,才执行某个function,二是每间格一段时间就执行某个function;
样例:
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>
//setitimer--定时器,并实现周期性定时
int main(){
//设计定时器
//三个参数,第一个哪一种定时法则,第二个设置定时器,第三个上一次定时器的信息
struct itimerval new_val;
//第一次触发的时间
new_val.it_value.tv_sec = 2; //秒
new_val.it_value.tv_usec = 0;//微秒,两个都得赋值
//周期性定时
new_val.it_interval.tv_sec = 1;
new_val.it_interval.tv_usec = 0;
//倒计时两秒
setitimer(ITIMER_REAL,&new_val,NULL);
while(1){
printf("hello world\n");
sleep(1);
//要进行捕捉,进程才可以不死,进行周期性操作
}
return 0;
}
其他几个发送信号的函数
给进程组发信号
int killpg(int gid,int signum);
暂停进程,直到被信号打断
int pause( void) 把当前正在运行的进程变成就绪态,让出CPU calling process
第十章 进程间通信
基本定义
管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据,两个进程在管道两边,而且管道不能广播数据,不能确定便捷,无法判断哪一个发数据,哪一个收数据。而且管道只能在具有亲缘关系的之间的进程使用,进程的亲缘关系通常是指父子进程关系。
匿名管道的特点:
1.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信
2.管道提供的是半双工的服务,是单向的
3.管道是面向字节流服务的(连续的,无边界)
4.文件生命周期随进程,进程结束了,管道也就释放了。
有名管道[FIFO文件]:有名管道和管道差不多,但是他允许无亲缘的关系的进程进行通信。
管道知识点
消息队列:消息队列就是消息的一个链表,它允许一个或多个进程向它写消息,一个或多个进程从中读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中所有的消息队列。
互斥:同一时间段内,只能容许一个线程进入临界区,对临界资源进行操作。
临界区:每个进程中访问临界资源的那段代码。
临界资源:为多个线程,进程所共享的资源,同一时间段只能容许一个线程对其进行操作的资源。
共享内存:共享内存就是映射一段能被其他进程访问的内存,这段共享内存由一个进程创建但是多个进程都可以访问,共享内存是最快的ipc方式,他是针对其他进程间通信方式运行效率低而专门设计的,他往往与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信。
共享内存解析
套接字:网络编程使用,可以用来在不同机器之间的进程之间的通信。
信号量:一种特殊的变量,是一个计数器,用来控制多个进程对共享资源的访问,他常作为一种锁机制,防止某些进程正在访问共享资源时,其他进程也在访问该资源,因此主要作为进程间以及同一进程内不同线程之间的同步手段。
pv操作:唯一一个可以改变信号量大小的操作,和信号量配合使用
用信号量和pv操作简单实现生产者消费者模型
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
sem_t blank,xfull;
#define _SEM_CNT_ 5
int queue[_SEM_CNT_]; //模拟饼框
int beginnum = 100;
void *thr_producter(void *arg){
int i = 0;
while(1){
sem_wait(&blank); //申请资源 blank++
printf("=====%s====self==%lu===num===%d\n",__FUNCTION__,pthread_self(),beginnum);
queue[(i++)%_SEM_CNT_] = beginnum++;
sem_post(&xfull); //xfull++
sleep(rand()%3);
}
return NULL;
}
void *thr_customer(void *arg){
int num= 0;
int i = 0;
while(1){
sem_wait(&xfull);
num = queue[(i++)%_SEM_CNT_];
printf("----%s----self--%lu----num----%d\n",__FUNCTION__,pthread_self(),num);
sem_wait(&blank);
sleep(rand()%3);
}
return NULL;
}
int main(){
sem_init(&blank,0,_SEM_CNT_);
sem_init(&xfull,0,0);//消费者默认没有产品
pthread_t tid[2];
pthread_create(&tid[0],NULL,thr_producter,NULL);
pthread_create(&tid[1],NULL,thr_customer,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
sem_destroy(&blank);
sem_destroy(&xfull);
return 0;
}