Linux——进程信号(一)

1.信号入门

1.1生活中的信号

什么是信号?

结合实际红绿灯、闹钟、游戏中的"!"等等这些都是信号。

以红绿灯为例子:

一看到红绿灯我们就知道:红灯停、绿灯行;我们不仅知道它是一个红绿灯而且知道当其出现不同的状况时我们应该做出怎么样的行为去应对。

识别=认识+行为产生

对于红绿灯:

我们之前受过的教育,让我们能够识别这个红绿灯(即使它还未出现在我们眼前),也就是说你能识别"红绿灯"

当绿灯量了后我们不一定要直接走,也可以等一会再走(也就是说接收信号后我们不一定要立刻产生对应的行为)

当红灯亮了之后,我们可以玩手机,要记得这个时候还是红灯

当红或绿灯亮时我们可以做出其他行为,也可以忽略红绿灯

对于信号来说(进程看待信号的方式)

  1. 在没有发生的时候,进程已经知道发生的时候该怎么处理了
  2. 信号进程能够认识,在遇见信号之前有人在"大脑"中设置了识别特定信号的方式(进程能够识别一个信号并处理)
  3. 信号到来的时候,进程正在处理更重要的事情,这时进程暂时不能立即处理到来的信号,进程必须暂时将到来的信号进行临时保存
  4. 信号到了进程可以不立即处理,等在合适的时候处理
  5. 信号的产生是随时产生的,进程无法准确预料,所以信号是异步发送的(信号的产生是由别人(用户、进程)产生的,在进程收到信号之前,进程一直在忙自己的事情,并发在跑的)

为什么?

停止、删除...系统要求进程要有随时响应外部信号的能力,随后做出反应

我们学习信号是学习它的整个生命周期,在进程运行期间信号的生命周期分为以下几个阶段:

准备、信号的产生、信号的保存、信号的处理

1.2Linux中的信号

用户输入命令,在Shell下启动一个前台进程
用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
前台进程因为收到信号,进而引起进程退出

cpp 复制代码
#include<iostream>
#include<unistd.h>

using namespace std;

int main()
{
    while(1)
    {
        cout<<"i am main process..."<<endl;
        sleep(1);
    }


    return 0;
}
  1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

1.3信号的概念

信号是进程之间事件异步通知的一种方式,属于软中断

在Linux操作系统中命令:kill -l可以查看系统定义的信号列表

上面的数字和名字都可以标识信号,名字其实就是宏;总共62个信号(没有0、32、33信号)

其中:1~31号是普通信号 34~64是实时信号

我们这里只学普通信号

上面我们说到:每个信号都有一个编号和一个宏定义名称,这些宏定义名称可以在signal.h中找到,这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

根据我们对Linux的了解,信号存放在哪呢?既然信号是给进程的,而进程又是通过内核数据结构来管理的,那么我们可以推断出,信号是放在进程的task_struct结构体中

既然他是在PCB中,如果创建31变量把信号全存进去,那就太浪费了;进程中信号的状态分为有或者没有,那么我们可以大胆的推断:把31个信号存放在一个只有32位整型变量中,每一个比特位都代表一个信号。

比特位的位置,代表信号的编号

比特位的内容:代表进程是否收到信号,1表收到,0表没收到

那么问题来了,内核数据结构的修改,这是由谁来完成的?

毫无疑问,是操作系统,毕竟task_struct就是由它来维护的,只有OS才有权利去修改它

所以说,无论哪个信号,最后都是经由OS之手发送给进程的(发送指的是修改进程PCB中存放信号那个变量的比特位)

信号发送的本质就是:在修改进程PCB中信号位图

信号处理常见方式概览:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

2.信号的产生

kill系统调用接口

cpp 复制代码
kill -9 pid //杀死进程
kill -2 pid //终止进程

2.1通过按键产生信号

通过键盘上按一些热键,来给进程发送相应的信号,比如上面的ctrl c,它产生的是2号信号;ctrl \产生的则是3号信号

如何进行自定义处理信号呢?

信号捕捉

signal

sighandler_t 返回值 signum几号信号

handler自定义方法 typedef void (*sighandler_t)(int)

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;

void handler(int signo)
{
    cout<<"get a sig,number is:"<<signo<<endl;
}

int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        cout<<"I am activing...,pid:"<<getpid()<<endl;
        sleep(1);
    }


    return 0;
}

设置信号捕捉只需要一次

signal调用完了handler方法不会被立即执行,这里只是设置对应的信号的处理方法

这时候我们发现Ctrl C杀不了这个进程了,这是因为我们捕捉信号后(信号的处理方式是自定义处理信号)对于2号信号的处理动作变成执行handler方法

2.2调用系统函数向进程发信号

系统调用和命令的名字一样,man 2 kill 查看

pid_t pid :要给发信号的pid

int sig:要发送的信号编号

返回值:发送成功返回0,失败返回-1

该系统调用是一个进程给另一个进程发送指定信号,可以向任意进程发送任意信号

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include <string.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        cout << "Usage: " << argv[0] << " -signumber pid" << endl;
        return 1;
    }

    int signumber = stoi(argv[1]+1);
    int pid = stoi(argv[2]);

    int n = kill(pid, signumber);
    if(n < 0)
    {
        cerr << "kill error, " << strerror(errno) << endl;
    }

    return 0;
}

raise给自己发送任意信号

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>

using namespace std;
int main()
{
    cout<<"开始运行..."<<endl;
    sleep(1);
    int n = raise(2);
    cout<<"运行结束"<<endl;
    return 0;
}

abort发信号终止自己(指定信号 6号:SIGABRT)

2.3软件条件产生信号

比如管道那时候,把读端关闭(没有了写的条件),那么写端也会收到信号关闭(SIGPIPE)

再举个闹钟的例子:

alarm

unsigned int 返回值 :上一个闹钟剩余秒数

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include <string.h>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int g_cnt = 0;
void handler(int sig)
{
    std::cout << "get a sig: " << sig << " g_cnt: " << g_cnt << std::endl;
    unsigned int n = alarm(5);

    cout << "还剩多少时间: " << n << endl;
    exit(0);
}

int mian()
{
    //设定一个闹钟
    signal(SIGALRM,handler);
    alarm(5);
    while(true)
    {
        g_cnt++;
    }
    int cnt = 0;
    while(true)
    {
        sleep(1);
        cout << "cnt : " << cnt++ << ", pid is : "<< getpid() << endl; //IO其实很慢
        if(cnt == 2)
        {
            int n = alarm(0); // alarm(0): 取消闹钟
            cout << " alarm(0) ret : " << n << endl;
        }
    }

    return 0;
}

2.4硬件异常产生信号

最后一种产生信号的方式:异常

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

除零错误引发异常 8)SIGFPE

cpp 复制代码
void handler(int sig)
{
    cout<<"get a sig:"<<sig<<endl;
    exit(1);
}

int main()
{
    signal(SIGFPE,handler);
    int a = 10;
    a /= 0;
    while(1)
    {
        sleep(1);
    }
    return 0;
}

野指针 11)SIGSEGV

cpp 复制代码
void handler(int sig)
{
    cout<<"get a sig:"<<sig<<endl;
    exit(1);
}
int main()
{
    signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while(1)
    {
        sleep(1);
    }
    return 0;
}

2.5关于信号产生的各种情况的理解

2.5.1键盘产生信号------硬件中断

键盘产生信号:

a.按键按下了 b.哪些按键按下了 c.字符输入(字符设备),组合键输入

字符输入:abcd 组合键输入:CTRL C 键盘驱动和OS联合解释的

OS怎么知道键盘输入了什么数据呢? 硬件中断的技术

键盘 将电信号传到CPU的针脚上 通过硬件中断转化位软信号电流,CPU再传给OS,OS通过中断向量表中以中断号去检索解释 判定是字符就放到缓冲区里面,是命令(ctrl+c)解释为信号

2.5.2异常(硬件)产生信号------除零、解引用空指针

除零

cpp 复制代码
void handler(int sig)
{
    cout<<"get a sig:"<<sig<<endl;
}

int main()
{
    signal(SIGFPE,handler);
    int a = 10;
    a /= 0;
    while(1)
    {
        sleep(1);
    }
    return 0;
}

如果捕捉除零产生的信号不用exit退出进程的话,随着CPU时间片的轮转就会再次被调到

CPU中只有一份寄存器,但是寄存器的内容属于当前进程的硬件上下文

当进程被切换的时候,就有无数次的状态寄存器被保存和恢复的过程

而除0操作导致溢出位置一的数据还会被恢复到CPU中

所以每次恢复的时候,操作系统就会识别到,并且给对应的进程发送SIGFPE信号,这就导致了上面不停调用自定义处理函数,不停地打印

如何理解除0?

进行计算的是CPU这个硬件

CPU内部有寄存器,状态寄存器(位图)有对应的状态标记位、溢出标记位,OS会自动进行计算完后的检测,如果溢出标记位是1,CPU会告诉OS有溢出的问题,OS就会来查看问题并且找到出问题的那个进程,并且将所识别到的硬件错误包装成信号发送给目标进程,本质就是操作系统去直接找到这个进程的task_struct并向该进程写入8信号

野指针异常

对于野指针异常来说,实际上也是CPU、OS共同配合的结果

我们所说的野指针指的是虚拟地址,虚拟地址和物理地址的转化如果是成功的,那么就不会抛出野指针异常,如果是失败的,那么CPU就会告诉OS出问题了,OS就会找到那个进程并发出11号信号,让他终止

虚拟地址与物理地址之间的映射是由一个叫MMU的硬件完成的,他是一种负责处理CPU的内存访问请求的计算机硬件

页表实际上是页表和MMU的结合,而MMU位于CPU中。在讨论中一般会简化称为页表。

当对空指针解引用的时候,MMU会拒绝这种操作,从而产生异常标志

操作系统拿到MMU产生的异常以后就会给对应的进程发送SIGSEGV信号

3.信号的保存

上面我们提到:如果当前进行更重要的工作,那么它会将信号进行保存,等到合适的时候再做处理,那么信号的保存机制是怎么实现的呢?

3.1信号常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择**阻塞 (Block )**某个信号
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

注意: 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

进程PCB中有一张位图:用来表示有无收到指定信号

还有一张位图:比特位的位置依然是信号编号,比特位的内容表示是否阻塞该信号

如:1号位图4号信号位置为1,2号位图4号信号位置为0,4号信号无阻塞通畅递达

1号位图6号信号位置为1,2号位图6号信号位置为1,6号信号阻塞,不递达(除非未来解除阻塞)

3.2在内核中的表示

记住三张表

block表:位图 pending表:位图 handler表:函数指针数组

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

3.3三张表匹配的操作和系统调用

在此之前,了解一下sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的**"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态**。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

3.3.1信号集操作函数

man sigemptyset:

sigset_t set:信号集变量

int signum:信号编号

返回值:成功返回0,失败返回-1。
函数sigemptyset:

初始化 set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号
函数sigfillset:

初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。

初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号

3.3.2sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

man sigprocmask

int how: 修改方式,有三个选项

set:

我们设置号的sigset_t变量

oldset:

输出型参数,将修改之前的信号屏蔽字保存到oldset

返回值:sigprocmask函数调用成功返回0,出错返回-1

3.3.3pending的系统调用sigpending

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

man sigpending

4.代码

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
using namespace std;


void PrintSig(sigset_t &pending)
{
    cout<<"Pending bitmap:"<<endl;
    for(int i=31;i>0;i--)//i为比特位,也是信号编号
    {
        if(sigismember(&pending,i))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}
void handler(int sig)
{
    sigset_t pending;
    sigemptyset(&pending);
    int n = sigpending(&pending);//正在处理2号信号
    assert(n==0);
    cout<<"递达中..."<<endl;
    PrintSig(pending);
    cout<<sig<<"号信号被递达处理"<<endl;

}

int main()
{
    //对2号信号进行自定义捕捉
    signal(2,handler);
    sigset_t block,o_block;
    //屏蔽2号信号
    sigemptyset(&block);
    sigemptyset(&o_block);
    sigaddset(&block,2);
    //进入内核
    int n = sigprocmask(SIG_SETMASK,&block,&o_block);
    assert(n==0);
    cout<<"block 2 signal success"<<"pid:"<<getpid()<<endl;
    int cnt = 0;
    while(1)
    {
        sigset_t pending;
        sigemptyset(&pending);
        n = sigpending(&pending);
        assert(n == 0);
        //打印pending位图中收到的信号
        PrintSig(pending);
        cnt++;
        //解除对2号信号的屏蔽
        if(cnt==10)
        {
            n = sigprocmask(SIG_UNBLOCK,&block,&o_block);
            assert(n==0);
        }
        sleep(1);
    }

    return 0;
}
相关推荐
不知 不知14 分钟前
最新-CentOS 7 基于1 Panel面板安装 JumpServer 堡垒机
linux·运维·服务器·centos
BUG 40422 分钟前
Linux--运维
linux·运维·服务器
千航@abc28 分钟前
vim在末行模式下的删除功能
linux·编辑器·vim
MXsoft6181 小时前
华为E9000刀箱服务器监控指标解读
大数据·运维
贾贾20231 小时前
配电网的自动化和智能化水平介绍
运维·笔记·科技·自动化·能源·制造·智能硬件
九月十九2 小时前
AviatorScript用法
java·服务器·前端
发光小北2 小时前
关于六通道串口服务器详细讲解
运维·硬件工程
jcrose25802 小时前
Ubuntu二进制部署K8S 1.29.2
linux·ubuntu·kubernetes
爱辉弟啦2 小时前
Windows FileZila Server共享电脑文件夹 映射21端口外网连接
linux·windows·mac·共享电脑文件夹
ICT系统集成阿祥2 小时前
科普篇 | “机架、塔式、刀片”三类服务器对比
运维·服务器