目录
1.信号的概念
在Linux中,信号是一种用于进程之间通信的基本机制。它是一种异步事件通知,用于通知进程发生了某些事件。如下是一些常见的Linux信号类型:
SIGINT (2):中断进程,通常由终端产生,例如用户按下Ctrl+C。
SIGKILL (9):立即终止进程,无法被捕获或忽略。
SIGTERM (15):请求终止进程,可以被捕获或忽略。
SIGQUIT (3):请求进程退出并生成核心转储文件,可以被捕获或忽略。
SIGSTOP (17):暂停进程的执行,无法被捕获或忽略。
SIGCONT (19):恢复进程的执行,无法被捕获或忽略
这些信号在进程控制、异常处理和进程间通信中扮演着重要角色。请注意,信号只是通知进程发生了什么事件,并不传递任何数据。进程对不同信号有不同的处理方式,可以指定处理函数、忽略或保留系统的默认值。信号机制在Linux编程中非常重要,帮助实现进程之间的协作和控制。
2.信号的产生
先举两个样例:
eg1:
首先我们编写一个死循环代码,编译运行后,我们的命令行就不再有用了,现在是前台程序,只运行当前的程序,当我们编译时加上&,使他成为后台程序,此时的命令行也可以继续使用,
程序在运行的时候,前台程序只能有一个,后台程序可以有多个。后台程序在运行时,我们的键盘可以输入数据,指令可以运行。
一般操作系统会自动根据情况把shell程序提到前台或者后台。下面的指令对shell无效。
前后台程序切换
./可执行 & 把程序放到后台
jobs 查看后台任务
fg number(任务编号) 把任务放到前台
ctrl+z 再加 bg number 把后台任务转到前台
ctrl+\ 默认终止
ctrl + z 暂停程序,先放到后台
而这就是信号的产生,除此之外操作系统知晓键盘的输入也是一种信号:
eg2:当键盘的某个按钮被按下的时候,就会产生高电平信号间接给cpu,cpu得知了之后某个按钮的高电平,发生中断,就产生对应的数据。
而信号的产生就是用软件来模拟中断行为。我们的指令都是发出信号,
例如接口signal
可以发出我们需要的信号。
如下一段代码:
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>
void handler(int signo)
{
std::cout<<"获得一个"<<signo<<"信号"<<std::endl;
exit(1);
}
int main()
{
signal(2,handler);
while(true)
{
std::cout<<"pid:"<<getpid()<<",i am running......"<<std::endl;
sleep(1);
}
return 0;
}
再运行的时候,我们ctrl+z,此时退出进程就会获得一个为2的信号。
因此信号的产生可以通过键盘发出,对于我们的linux也是有许多信号的(kill -l):
其中,没有0号信号,从1-31的信号我们把它叫做普通信号,没有32,33信号,从34到64的信号,我们把它叫做实时信号。这些信号的本质就是一些函数指针数组,对应的下标就与他们的编号有关。
对于普通信号,进程是否收到了普通信号,操作系统(pcb中)会用一张位图来表示,利用位图中的第几个比特位表示编号,0表示没收到,1表示收到。
无论信号有多少种,都是只能让os来写(写)信号,因为os是进程的管理者。
了解到了信号的接收,因此我们在编写程序时就可以直接发送信号,之后自动运行对应handler方法,例如之前我们使用kill -9杀进程,现在我们发送一个为9的信号,此时自定义它的处理方法,例如只是打印一句话,那么我们kill -9的指令就不会再杀掉我们的进程,而是打印一句话。
但实际上并不可以,操作系统对于某些信号是不可以被自定义捕捉的。
除此以外,Linux提供了三种接口供我们产生信号。
方式一:通过键盘组合键发送产生信号。
方式二:通过函数接口
接口 raise 可以自己给自己发送任意信号
接口 abort 收到信号后终止运行
方式三,通过异常:
以我们熟知的除零错误为例,首先除零错误并不是语言错误,而是进程错误,再cpu中通过各个寄存器来计算除零,此时cpu中还有表示状态的寄存器,当发生除零问题后,状态寄存器就会产生溢出标记位,从而转化为信号,就是信号8 SIGFPE 也就是flaot point exception。
当然发出信号也不仅仅可能是因为异常而导致的,也有可能是闹钟响了:
方式四:由软件条件产生信号:
alarm接口可以设置闹钟
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>
int cnt=0;
void handler(int signo)
{
std::cout<<"获得一个"<<signo<<"信号"<<"alarm is:"<<cnt<<std::endl;
exit(1);
}
int main()
{
std::cout<<"pid:"<<getpid()<<std::endl;
//本质上就是修改函数指针数组的位置
signal(14,handler);
//设置1s闹钟,到点了终止进程
alarm(1);
while(true)
{
//cout<<cnt++<<endl; 可以看出外设是很慢的
cnt++;
}
}
操作系统的时间:
当我们电脑关机了,程序结束了,再次重新启动,我们会发现,时间永远是跟着走的,实际上,即使关机了,在电脑里也会有一个纽扣电池一直给硬件供电,固定时间间隔计数,再将计数器转换为时间戳给我们的电脑。CMOS周期性的高频的发送时间中断。
3.信号的保存
. 信号其他相关常见概念
实际执行信号的处理动作称为信号递达 (Delivery)
信号从产生到递达之间的状态 , 称为信号未决 (Pending) 。
进程可以选择阻塞 (Block ) 某个信号。
被阻塞的信号产生时将保持在未决状态 , 直到进程解除对此信号的阻塞 , 才执行递达的动作 .
注意 , 阻塞和忽略是不同的 , 只要信号被阻塞就不会递达 , 而忽略是在递达之后可选的一种处理动作
递达就是开始处理信号,当信号被记录再为途中时就是信号未决状态,阻塞:被阻塞的信号一直处在未决状态,只有当阻塞取消时,才进入递达状态。
阻塞与忽略是有区别的,忽略本身没有阻塞而是递达,处理了信号,效果为忽略,而阻塞是没有抵达,且没处理。
了解了以上概念,因此再管理信号的状态时,os就需要维护这三张位图表,用来表示阻塞,未决,递达这三个状态的信号。
比特位的位置:代表信号的编号
比特位的内容:对特定信号进行阻塞还是屏蔽。
每个信号都有两个标志位分别表示block(阻塞)和pending(未决),其次还有一个函数指针表示要处理的方法。
void handler(int signo)
{
cout<<"signo is "<<signo<<endl;
exit(1);
}
int main()
{
//发送2信号
signal(2,signo);
//把信号的粗粒设置为原来默认的
signal(2,SIG_DFL);
//当然还可以把信号忽略
signal(2,SIG_IGN);
std::cout<<"my pid id:"<<getpid()<<endl;
while(true)
{
cout<<"i am running....."<<endl;
sleep(1);
}
}
由于有这么多信号集,操作系统还提供了许多信号及操作接口:
sigset_t 类型对于每种信号用一个 bit 表示 " 有效 " 或 " 无效 " 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统 实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做 任何解释, 比如用 printf 直接打印 sigset_t 变量是没有意义的。
#include <signal.h>
int sigemptyset(sigset_t *set); //对指定的位图进行清零
int sigfillset(sigset_t *set); //对指定的位图进行置1
int sigaddset (sigset_t *set, int signo); //对指定信号添加到指定的位图中
int sigdelset(sigset_t *set, int signo);
int sigismember ( const sigset_t *set, int signo); //判定一个信号是否在为位图中
对于block表的修改:
sigprocmask 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
如下代码:
int main()
{
//例如对2号信号屏蔽
cout<<"my pid is"<<getpid()<<endl;
//先定义两个信号集位图
sigset_t block,oblock;
//先对信号集清空
sigemptyset(&block);
sigemptyset(&oblock);
//其次对2号信号添加到信号集
sigaddset(&block,2); //当前并没有让操作系统2信号屏蔽,只是语言层面的定义
sigaddset(&oblock,2);
sigprocmask(SIG_BLOCK,&block,&oblock); //真正让操作系统屏蔽、更改信号
while(true)
{
sleep(1);
}
return 0;
}
此时我们再发2号信号就没有作用了,ctrl+c也无法中断程序。
既然如此,那么我们是否可以将一个程序的所有信号屏蔽,这样他就有金刚不坏之身,谁也干不掉他,实际上并是不是所有的信号你都能屏蔽,就跟不是所有的信号的处理可以自定义是一样的。
比如说9号信号就无法被屏蔽。
那么pending表的修改:接口 sigpending
重要的是获取pending表.
接下来我们用一个整体的实例来认识这些接口:
void printpending(const sigset_t &pending)
{
for(int signo=31;signo>0;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}else{
cout<<"0";
}
}
cout<<"\n";
}
//自定义捕捉
void handler(int signo)
{
cout<<"已接受到信号"<<signo<<endl;
//exit(1);
}
int main()
{
//例如对2号信号屏蔽
cout<<"my pid is"<<getpid()<<endl;
signal(2,handler);
//先定义两个信号集位图
sigset_t block,oblock;
//先对信号集清空
sigemptyset(&block);
sigemptyset(&oblock);
//其次对2号信号添加到信号集
sigaddset(&block,2); //当前并没有让操作系统2信号屏蔽,只是语言层面的定义
sigaddset(&oblock,2);
sigprocmask(SIG_BLOCK,&block,&oblock); //真正让操作系统屏蔽、更改信号
//下打印pending表
int cnt=0;
sigset_t pending;
while(true)
{
sigpending(&pending);
printpending(pending);
sleep(1);
cnt++;
if(cnt==5)
{
//直到5S,解除2信号的屏蔽
cout<<"解除对2号信号的屏蔽,2号准备抵达"<<endl;
sigprocmask(SIG_SETMASK,&oblock,nullptr); //设置为旧的信号
}
}
return 0;
}
运行结果如图:
4.信号的捕捉
信号在什么时候去被捕捉处理呢,在合适的时候---从内核态返回到用户态的时候,进行信号的检测和信号的处理。
内核态:内核态是操作系统的一种状态,能够大量访问资源
用户态:用户态是一种受控的转台,能够访问的资源是有限
用户想要访问操作系统只能通过系统调用的方式访问。
首先无论进程如何调度,cpu都会找到os,我们的进程的所有代码的执行,都可以在地址空间中通过跳转的方式进行调用和返回。
那么对于系统的信号的捕捉,首先介绍第一个接口sigaction
第三个参数表示把旧的handler表返回给我,达尔戈参数就是新的handler的设置,第一个参数为信号编号,接口的作用是检测和修改信号动作。
返回类型是sigaction的结构体类型,其中有五个字段。其中我们比较重点关注的是sa_mask字段,
如果在调用信号处理函数时,除了当前信号被屏蔽外,还希望屏蔽些别的信号,此时sa_mask就是需要被额外屏蔽的信号。
以该代码为例:
#include<signal.h>
#include<unistd.h>
#include<iostream>
using namespace std;
void print(sigset_t &pending);
void handler(int signo)
{
cout<<"接收到信号"<<signo<<"......"<<endl;
while(true)
{
//获取当前pending列表
sigset_t pending;
sigpending(&pending);
print(pending);
sleep(1);
}
}
void print(sigset_t &pending)
{
for(int signo=31;signo>0;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}else{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
cout<<"my pid is "<<getpid()<<endl;
//定义新的与旧的act
struct sigaction act,oact;
//设置handler为当前自定义的处理方法
act.sa_handler=handler;
sigaction(2,&act,&oact);
while(true) sleep(1);
return 0;
}
用改接口接受2号信号时,和之前一样,运行程序,第一次我们ctrl+c,发出2信号时接收到2好信号,但自此之后的2好信号都被屏蔽掉了,再次crtl+c时,信号无法被接受处于未决状态。
例如:当我们要修改信号2时,这里默认会自动屏蔽信号2,如下图
。
信号的其它内容:
可重入函数
数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数 , 这称 为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 , 如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc 或 free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据
在这里我们就这样理解,住执行流与信号捕捉流是两种不同的流。
关键字volatile
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作。
那么对于信号有什么作用呢?
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
优化情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag = 1 ,但是 while 条件依旧满足 , 进 程继续运行!但是很明显flag 肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flag , 并不是内存中最新的flag ,这就存在了数据二异性的问题。 while 检测的 flag 其实已经因为优化,被放在了 CPU寄存器当中。如何解决呢?很明显需要 volatile。
实际中在gcc中,也是有自带优化的选项。
SIGCHLD****信号
我们 早已经了解到子进程在退出的时候,是要给父进程发送退出信息的,不然父进程还要维护一份没必要的资源,而子进程是给父进程发送什么样的信号呢?---SIGCHLD
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
//收到退出信号 等待子进程
while( (id = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){
//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is running\n");
sleep(1);
}
return 0;
}
可以看到子进程退出时,时回给父进程发信号的。
在Linux中支持手动忽略信号SIGCHDL,可以不用wait子进程。退出自动回收。