目录
信号的基本概念
通知事件的发生,并且事件比较紧急,会打断当前的操作去处理事件,又因为这是所说的信号
是针对进程,所以又称为软中断
- 从一个简单的场景说起
- 用户输入命令,在shell下启动一个前台进程
- 用户按下这个ctrl + c 这个键盘输入产生一个硬件中断
- 如果CPU当前正在执行这个代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断
- 终端驱动程序将按键解释为一个SIGINT信号,记载该进程的PCB中
- 当某个时刻要从内核态返回到该进程的用户空间代码继续执行之前,首先处理PCB中的信号,发现有一个信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间执行代码
kill -l
//可以查看系统定义的信号列表
kill
//并不是为了杀死一个进程而设计的,而是为了给某一个指定进程发送信号
-
linux下有62个信号,并且分了两类,通过kill -l 查看
-
信号的功能:
实际上就是为了通知进程发生了那些事件,应该怎么处理
信号实际也可以归为一类进程间通信方式
-
1-31是非可靠信号(非实时信号)
1- 31号信号时继承于unix而来的,每一个信号都对应了一个指定的事件
非可靠代表:这个信号可能会丢失,如果有相同的信号已经注册到这个进程没有
被处理,那么接下来的相同型号就会被丢掉(只能被注册一次) -
34-64是可靠信号(实时信号)
信号不会丢失
-
信号的生命周期:
信号的产生——>信号的注册——>( 信号的阻塞\屏蔽)——>信号的注销——>信号的处理
信号的产生方式
- 硬件中断
- 程序异常((段错误)内存访问异常SIGSEG)
- 接口调用发送信号(命令产生),kill函数(如果不明确指定发送信号则发送SIGTERM信号,该信号的默认处理为终止进程)
- 软件条件产生:kill函数,raise函数(给自己发送信号),alarm函数(定时器函数),sigqueue函数
int kill(pid_t pid ,int sig);
向进程发送指定信号,
pid 进程id,用于指定发送信号到那个进程
sig信号编码:用于指定发什么信
int raise(int sig) ;
给调用(自己)的进程或线程发送信号
sig:信号编码,用于发送指定信号
int sigqueue(pid_T pid, int sig ,union sigval value);
给指定的进程发送指定的信号,同时可以携带一个参数过去
pid 进程id,用于指定信号发送给那个进程
sig信号编码,用于指定发送什么信号
value:要携带的数
union:联合体(一个int,一个指针):同一个进程之间可以使用指针,不同进程使用int
unsigned int alarm(unsigned int seconds)
指定在seconds秒后发送一个SIGALRM信号到进程
可以返回上一个定时器的剩余时间(定时器可以被替换)每次调用都会替换上一个
如果值为0,取消以前的定时器
产生信号
SIGINT的默认处理动作是终止进程,SIGQOUIT的默认处理动作是终止进程并且Core Dump
Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,
文件名通常是core,这叫做Core Dump(核心转储)。进程异常终止通常是因为有Bug,比如非法访问
内存导致段错误,事后可以用调试器检查core文件以查清楚错误原因。这个叫Post mortern Debug(事后调试)
一个进程允许产生多大的core文件取决于进程的Resource Limit(保存在PCB中),
默认是不允许产生core文件的,因为这里包含了很多敏感信息,可以使用ulimit命令改变限制
允许产生文件最大为1024k
默认关闭,默认的core文件大小为0,ulimit -c size
信号的注册
-
信号注册就是将信息这个传递给进程,让进程知道有这么一个信号,信号是记录在PCB中的
-
进程记录一个信号的时候是通过这个结构体的pending位图来记录的,位图的位数+1就代表指定的信号储存位置
信号的阻塞与屏蔽
阻塞:将信号添加到blocked位图,向进程备注说明这些信号暂时不处理
在pcb中有一个pending的结构中储存当前接收到的信号,还有一个结构体blocked(位图)用于存储现在都有那些信号要被阻塞(信号的阻塞就是组织信号的递达(信号处理))
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
#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);
信号阻塞屏蔽验证代码
//这是一个验证信号阻塞屏蔽的代码
//sigpromask
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
int main()
{
sigset_t mask;
sigemptyset(&mask);//清空集合数据,防止出现意外
sigaddset(&mask,SIGINT);//向集合添加指定信号
// int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// how:对集合所做的操作
// SIG_BLOCK:对set集合中的信号进行阻塞,oldset保留原有
// SIG_UNBLOCK:对集合中的信号解除阻塞,oldset忽略
// SIG_SETMASK 将set集合中的信号设置到blocked集合中
sigset_t oldmask;
sigemptyset(&oldmask);
sigprocmask(SIG_BLOCK,&mask,&oldmask);
while(1)
{
printf("this is porc mask!\n");
sleep(1);
}
return 0;
}
程序一开始就把第2位置为1,所以第2号信号在block中记录阻塞,运行时发现被阻塞不进行处理
所以ctrl + c 无法退出,这时来了一个ctrl \ (SIGQUIT)信号,block中没有置1,无阻塞所以直接终止
信号的注销
从pending集合中将要处理的信号移除,但是这个移除分情况
- 不管是注册还是注销都区分了可靠信号和非可靠信号
- 非可靠信号
注册:非可靠信号注册的时候,是给sigqueue链表添加一个信号节点,并且将pending集合对应的位图置1,当这个信号注册的时候
如果位图已经置1,代表信号已经被注册过了,因此不做任何操作,不会添加新的信号节点
注销:删除链表中的节点,并将对应的位图置0 - 可靠信号
注册:可靠信号注册的时候,是给sigqueue链表添加一个信号节点(不管这个信号是否已经注册),如果没有注册则添加新节点的同时更改对应的位图置1
注销:删除一个节点,然后查看链表中还有没有相同信号的节点,如果还有,信号对应的位图组依然置1,如果没有相同的节点,代表这个信号已经全部被处理了,因此将对应的位图置0 - 可靠信号因为每次信号到来都会添加新的节点,因此可靠信号不会丢失
信号的处理
每一个信号实际都对应了某个时间,当进程收到了一个信号,那么就意味着现在有一个重要的事件需要处理
因此会打断我们当前的操作,然后去处理这个事件
信号处理还有一个名字:信号的递达
那么进程到底什么时候才会去检测pending集合,看有没有信号需要处理呢?什么时候处理信号?
- 进程是在从内核态切换到用户态的时候回去检测一下是否有信号需要被处理
信号的处理方式
- 忽略处理方式:忽略和阻塞完全不同,一个被忽略的信号来时直接被丢弃
SIG_IGN
- 默认处理方式:操作系统原有定义好的,对于一个信号所对应事件的处理
SIG_DFL
- 自定义处理方式:提供一个信号处理函数,要求内核在处理信号时切换到用户态执行这个处理函数
这种方式称为捕捉一个信号(void (*函数指针)(int signo))
信号的忽略处理代码实现
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//信号的处理方式测试
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
int main()
{
//2信号的忽略处理
//sighandler_t signal(int signum, sighandler_t handler)
//用于修改一个信号的处理方式
//signum:用于指定修改哪个的处理
//handler:用于指定处理方式(函数)
//SIG_IGN忽略处理
//SIG_DFL默认处理
//
//体现阻塞和忽略的区别:
//阻塞一个信号:信号依然会注册在pending集合中
//忽略一个信号:信号来了就直接丢弃,不会被注册
signal(SIGINT,SIG_IGN);//忽略中断处理
getchar();//回车之后变成默认处理方式
signal(SIGINT,SIG_DFL);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
开始中断,也无法退出因为忽略退出信号,回车之后变成默认处理方式,正常运行
信号的自定义处理代码实现(sigcb)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void sigcb(int signo)
{
printf("recv sigo :%d\n",signo);
}
int main()
{
//信号的自定义处理方式
//sigcb是用户自己定义的信号处理方式
//void sigcb(int signo)
signal(SIGINT,sigcb);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
调用自定义的处理方式,打印信号号码不退出
信号的自定义处理代码实现(sigaction推荐)
//信号的处理方式测试
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void sigcb(int signo)
{
printf("recv sigo:%d\n",signo);
sleep(5);
}
int main()
{
// struct sigaction {
// 用于自定义处理方式
// void (*sa_handler)(int);
// 自定义处理方式,可以接受信号携带的参数
// void (*sa_sigaction)(int, siginfo_t *, void *);
//处理信号的时候,希望这个处理过程不被其他信号到来打扰
// sa_mask 就是用于在处理信号时要阻塞的信号
// sigset_t sa_mask;
// 选项标志,决定用哪一个成员函数作为信号处理将接口
// int sa_flags; // 0-sa_handler SA_SIGINFO-sa_sigaction
// void (*sa_restorer)(void)
// };
//
// 信号的自定义处理
//
//int sigaction(int signum, const struct sigaction *act,
// struct sigaction *oldact);
//signum:用于指定修改哪个信号的处理动作
//act:给指定信号要指定的处理动作
//oldact:用于保存这个信号原来的处理动作
struct sigaction n_act, o_act;
sigemptyset(&n_act.sa_mask);
//在处理信号期间,不希望受到SIGQUIT影响
//因此在处理期间将sa_mask中的信号全部(SIGQUIT)阻塞
sigaddset(&n_act.sa_mask,SIGQUIT);
n_act.sa_handler = sigcb;
n_act.sa_flags = 0;//0就是默认使用handler
//痛过sa_flags判断调用那个函数(函数区别:有无携带参数)
sigaction(SIGINT,&n_act,&o_act);
while(1)
{
printf("xixi~~~\n");
sleep(1);
}
return 0;
}
自定义将中断信号放在sa_mask阻塞,程序正常循环,一旦接收到中断信号,执行函数打印信号编号,暂停五秒,继续循环
信号的捕捉流程:
主要是针对自定义信号处理方式
信号不是立即处理的,会选择一个合适的时机去处理
(从内核态转变到用户态)
如何从用户态切换到内核态?
程序异常,系统调用,中断
可重入函数不可重入函数
可重入函数
这个函数如果中间操作被打断,在其他地方多次调用,并不会对运行结果造成影响
不可重入函数:一旦重入就会出问题
这个函数如果中间操作被打断,在其他地方多次调用,会对运行结果造成影响,这类函数称之为不可重入函数
(但凡函数中涉及到全局数据的修改并且这个数据修改没有被保护起来,这个函数就是不可重入函数)
可重入代码演示
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
//可重入函数
//这个函数调用的时候如果中间操作被打断,在其他的地方有多次调用
//但是并不会对运行结果造成影响,那么这种函数叫做可重入函数
//不可重入函数:一旦重入就会出现问题
//这个函数调用的时候如果中间操作被打断,在其他地方有多次调用
//这些多次调用会对运行结果造成影响,那么这类函数叫不可重入函数
//
//特点:操作了一些公共数据
int a = 10;
int b = 20;
int sum( )
{
printf("%d + %d\n",a + 1,b + 1);
a++;
sleep(5);
b++;
return (a + b);
}
void sigcb(int signo)
{
printf("sig sum = %d\n",sum());
return;
}
int main()
{
signal(SIGINT,sigcb);//修改一个信号的处理方式
printf("sum = %d\n", sum());
while(1)
{
sleep(1);
}
return 0;
}
1:主函数中调用sum函数,打印a自加与b自加之和,然后死循环停止。
2:如果按 ctrl + c 发信号,在sleep时打断sum函数(通过signal修改指定的处理方式调用sum函数),只有a已经自加
这时信号返回的求和时b还未自加,然后重入到打断之前的sum函数,b在自加,打印b自加后的求和
可重入函数被打断不会对运行结果造成影响
如果插入节点时:insert函数被不同的控制流调用,发生重会造成错乱,可能只会成功的插入一个节点
这种函数为不可重入函数
如果一个函数满足以下条件则是不可重入
- 调用了malloc或free,因为malloc也是用全局链表管理堆的
- 调用了标准IO库函数,因为很多库函数的实现都是不可重入的方式使用的全局数据结构
volatile保持内存可见性
每次处理这个被volatile修饰的变量时,都会从内存中重新加载变量的值到寄存器,因为程序做优化的时候,如果与个变量使用的频率非常高,那么这个变量有可能就会被优化为只向寄存器加载一次,往后直接使用寄存器中的保留值,而不关心这个变量内存里边的值,因此就有可能造成程序的逻辑错误。
简而言之:指定是cpu每次操作一个被volatile修饰的变量的时候,都需要取内存重新加载变量数据
代码演示
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdint.h>
#include<signal.h>
uint64_t a = 1;
void sigcb(int so)
{
printf("recv :%d\n",so);
a = 0;
}
int main()
{
signal(SIGINT,sigcb);
while(a)
{}
return 0;
}
gcc -O1:编译时优化
优化后会直接调用从内存中加载到寄存器的保留值,如果改变这个值下一次调用则会使用改变后的值。造成问题
如果不优化会一直使用内存中的值,即是寄存器中的值被修改,下一次加载内存的值也不会被影响
竞态条件
如果我们的操作不是原子操作,意味着这个操作有可能被打断,然后去做其他的事情,如果这时候做其他的事情有可能对我们的程序的整个逻辑或者函数的运行产生不良的影响。(当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件)
alarm(n);
pause();收到任何信号都会打断暂停,唤醒等待
虽然alarm紧接着下一行就是pause,但是我们无法保证pause一定会在alarm之后调用(因为可能出现优先级更高的进程),如果我们写的时序考虑不周密,就可能出现时序问题而导致错误
即是这样,信号依然可能在调用到pause之前的间隙递达
所以“解除信号屏蔽”和“挂起等待信号”能合二为一变成原子操作就好了
这个正是sigsuspend函数的功能,函数包含了pause的挂起等待的功能,解决了竞态条件的问题
对时序要求严格的场合都可以调用这个函数
int sigsuspend(const sigset_t *mask);
临时使用mask中的信号替换阻塞结合blocked中的信号,然后进入阻塞等待,唤醒后还原
也就是说,临时替换看一下信号阻塞集合,然后进入休眠,当休眠被唤醒时,再将原来的
阻塞信号替换回去
alarm(5)
pause()收到任何信号都会打断暂停,唤醒等待
SIGCHLD
在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程。按系统默认将忽略此信号。如果父进程希望被告知其子系统的这种状态,则应捕捉此信号。信号的捕捉函数中通常调用wait函数以取得进程ID和其终止状态。