Linux学习之信号

目录

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

相关推荐
西岸行者7 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意7 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码7 天前
嵌入式学习路线
学习
毛小茛7 天前
计算机系统概论——校验码
学习
babe小鑫7 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms7 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下7 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。7 天前
2026.2.25监控学习
学习
im_AMBER7 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J7 天前
从“Hello World“ 开始 C++
c语言·c++·学习