Linux学习之信号

时间:2024-04-17 15:29:23

目录

1.信号的概念

2.信号的产生

3.信号的保存

4.信号的捕捉

信号的其它内容:

SIGCHLD信号


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子进程。退出自动回收。