【Linux】进程信号“疑问?坤叫算信号吗?“

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

鸡叫当然也算信号啦~

文章目录

  • 前言
  • 一、认识信号量
  • 二、信号的产生
    • 1.调用系统函数向进程发信号
    • 2.由软件条件产生信号
    • 3.硬件异常产生信号
  • 总结


前言

信号在我们生活中很常见下面我们举一举生活中信号的例子

你在网上买了很多件商品再等待不同商品快递的到来。但即便快递没有到来你也知道快递来临时你该怎么处理快递。也就是你能“ 识别快递
当快递员到了你楼下你也收到快递到来的通知但是你正在打游戏需 5min 之后才能去取快递。那么在在这5min 之内你并没有下去去取快递但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行可以理解成“ 在合适的时候去取
在收到通知再到你拿到快递期间是有一个时间窗口的在这段时间你并没有拿到快递但是你知道有一个快递已经来了。本质上是你“ 记住了有一个快递要去取
当你时间合适顺利拿到快递之后就要开始处理快递了。而处理快递一般方式有三种 1. 执行默认动作幸福的打开快递使用商品2. 执行自定义动作快递是零食你要送给你的朋友 3. 忽略快递快递拿上来之后扔掉床头继续睡觉
快递到来的整个过程对你来讲是异步的你不能准确断定快递员什么时候给你打电话
在讲进程信号之前我们先引入四个重要的概念
1.互斥任何一个时刻都只允许一个执行流在进行共享资源的访问这样的操作可以通过加锁来实现
2.我们把任何一个时刻都只允许一个执行流在进行访问的共享资源叫做临界资源。
3.临界资源是要通过代码访问的凡是访问临界资源的代码叫做临界区。
4.原子性 只有两种确定状态的属性  就比如1和0,能存在中间值0.5

一、认识信号量

感性的认识

信号量也被称为信号灯本质上就是一个描述资源数量计数器下面我们举个生活中的例子来理解信号量

在生活中我们会去电影院看电影但是在看电影之前我们必须先买票而买票的本质功能有两个第一个是对座位资源的预订机制第二个是确保不会因为多放出去特定的座位资源而导致座位冲突。而信号量其实对应的就是买票因为任何一个执行流想访问临界资源中的任何一个子资源的时候是不能直接访问的必须得先申请信号量资源也就是买票而我们前面说过信号量的本质就是个计数器所以我们在申请信号量资源的时候只需要让这个计数器加加或减减即可如果申请成功那么计数器需要--因为我们的信号量资源少了一个。如果申请成功后不想用了那么就让计数器++代表有人将我们的信号量资源归还了。也就是说只要我们申请信号量成功我就一定能在未来拿到一个子资源。同样的例子如果我们的电影院只有一个座位仅供专属VIP座那么这个情况就叫做互斥因为在这期间只有一个VIP能使用这个座位没有其他的人来抢座位。刚刚我们说了信号量本质是个计数器既然是计数器就必须让所有的进程都看到否则无法保证自己的操作是原子的。可以理解为让不同的进程看到同一份资源这个资源就是信号量。

下面我们来认识一下信号量的接口

首先第一个接口是获取信号量semget:

 如果看了我们上一篇共享内存的文章的话一定可以认识semget这个接口的参数因为和获取共享内存接口shmget一模一样。第二个参数nsems的含义是代表信号量的个数也就是说我们一次可以申请多个信号量。要查看我们的信号量的命令是ipcs -s:

同样和共享内存一样删除某个信号量的指令是ipcrm -s +semid。下面我们看看删除信号量的系统调用接口不出意外的话就是semctl这个函数了

 这个函数与共享内存的删除接口不一样的地方是多了一个可变参数列表第二个参数semnum是代表对哪一个信号量做操作因为刚刚我们说过了可以同时申请多个信号量。

semop这个函数可以完成对信号量的计数器-1+1操作

 这个函数的第二个参数结构体就是完成我们对信号量的-1+1操作的下面我们看看这个结构体

 比如说我们要对一个信号量做减操作那么就可以在num这个下标填0num是一个数组sem_op填-1因为要减减flag默认即可。

对于信号量的接口我们差不多已经看完了下面我们来理解一下IPC

 我们可以发现不管是共享内存还是信号量系统用来描述他们的结构体都是XXXid_ds:

那么操作系统是分开管理这些IPC资源的还是一起管理的呢

 我们以左边三个结构体为例在操作系统中有一个这个结构体类型的指针数组这个数组按下标依次存放右边三个不同的ipc结构体的地址对于这个指针数组来讲要保存其他类型的ipc结构体只需要将这个结构体类型强转为系统用于管理的这个结构体指针类型这样就完成了将内核中的所有ipc资源统一以数组的方式进行管理。以上就是操作系统管理这个IPC资源的原理上面的操作不知道有没有看出是什么原理呢其实这就是多态

二、信号的产生

红绿灯闹钟下课铃都是信号而这些信号被看懂前是需要我们被培养过比如说有人告诉我们红灯停所以我们知道红灯要停下我们可以把进程比作自己信号就是一个数字进程在没有收到信号的时候其实进程早就知道该如何处理信号了因为这是程序员教的程序员写代码让进程认识信号而由于信号可能会随时产生所以在信号产生前进程可能在做优先级更高的事情这个时候进程是可以不用立马处理这个信号的但是要在后续合适的时间处理刚刚没有处理的信号由于这样的原因所以我们必须将信号保存起来这样即使当时没有处理信号也能在后续的时间处理这个信号。总结进程收到信号的时候如果没有立马处理这个信号需要进程具有记录信号的能力。

首先我们要知道查看信号的命令  kill -l

 在这些信号中只有1-31是我们要学的因为1-31叫做基本信号34-64叫做实时信号而我们现在的操作系统都是分时的所以我们只学习基本信号。因为信号的产生对于进程来说是异步的那么进程该如何记录对应产生的信号呢答案是先描述再组织。怎么描述呢简单的说0 1就能描述一个信号用位图来管理这个信号。如下图

下面我们用代码来对信号进行简单的测试

#include <iostream>
#include <unistd.h>


int main()
{
    while (true)
    {
        std::cout<<"我是一个进程我正在运行 ...,pid"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

 下面我们将程序运行起来试一试信号

 首先我们看到的现象是我们成功用9号信号杀死了一个进程这就是通过指令的方式发信号。

当然对于前台进程而言我们可以从键盘上输入ctrl +c 终止前台进程

 而如何将一个进程变为后台进程我们也说过了就是在后面加上&符号

 后台进程是无法被ctrl+c这样的命令杀死的所以最后我们用kill-9杀死了这个进程。其实ctrl+c也是操作系统像进程发信号只不过我们看不到下面我们通过signal函数的方式查看操作系统给进程发的信号

 signal这个函数的第一个参数为信号编号第二个参数为如果操作系统像这个进程发了一个信号这个函数会将这个信号拿走用于自定义的功能而不是再像以前一样听取操作系统的指令。下面我们演示一下

#include <iostream>
#include <unistd.h>
#include <signal.h>

void handler(int sig)
{
    std::cout<<"get a signal: "<<sig<<std::endl;
}


int main()
{
    signal(2,handler);
    while (true)
    {
        std::cout<<"我是一个进程我正在运行 ...,pid"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

 这段代码的意思是当操作系统向我们发送2号信号的时候ctrl + c 就发送的2号信号我们不在执行原来的终止程序而是去打印出来signal信号

 也就是说我们通过signal函数成功捕获了操作系统向我们发送的2号信号这个2号信号就是我们按下ctrl+c的时候操作系统转化为信号发送给进程的当然如果上面的图片还是没看懂那么我们也可以这样

 下面这两张图就清楚的证明了我们发送的ctrl+c信号就是2号信号因为我们发送2号信号不会中断程序ctrl+c也不会中断程序。下面我们要说一下在我们用回调函数的时候就像我们上面的代码在调用signal函数的时候是不会调用handler函数的这里只是更改了2号信号的处理动作并没有调用handler方法。比如下面这样

 在我们调用show方法的时候是不会调用print的函数的下面我们将代码运行起来

 我们可以看到只打印了hello show,那么如何在调用show的时候还调用print函数呢其实很简单在show中调用函数指针即可

 这也就证明了我们调用signal函数的时候是不会调用handler函数的。

下面我们将所有信号都自定义捕捉这样是不是这个进程就无敌了没有指令可以杀掉这个进程了呢

int main()
{
    //signal(2,handler);
    for (int i = 1;i<=31;i++)
    {
        signal(i,handler);
    }
    while (true)
    {
        std::cout<<"我是一个进程我正在运行 ...,pid"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

 我们可以看到其他信号确实都被捕捉了但是kill -9还是会杀掉进程因为操作系统不允许有进程不被杀死。

下面我们讲一下信号的产生原理

我们平时在输入的时候计算机怎么知道我从键盘输入了数据呢键盘是通过硬件中断的方式通知系统我们的键盘已经被按下了。

 上图中的圆圈代表CPU边上的毛代表CPU的针脚而键盘会通过中断控制器找到对应与CPU的针脚

 当我们从键盘输入指令后cpu的寄存器会存储键盘的中断号然后CPU通过中断号去中断向量表中查找与之中断号相对应的函数方法这样就完成了我们从键盘输入ctrl + c然后转化为2号信号并且杀死进程的操作。

下面我们将上面所讲的知识先小小的总结一下

1. 用户输入命令,在Shell下启动一个前台进程。
.
用户按下Ctrl-C ,这个键盘输入产生一个硬件中断被OS获取解释成信号发送给目标前台进程
.
前台进程因为收到信号进而引起进程退出。
2. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
3. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
4. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步
(Asynchronous)的。

当然除了上面我们用数字当信号也可以用宏来使用

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

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <sys/types.h>


void Usage(std::string proc)
{
    std::cout<<"Usage: \n\t";
    std::cout<<proc<<"信号编号 目标进程\n"<<std::endl;
}
int main(int argc,char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    return 0;
}

我们要完成的工作是写一个和kill-9命令一样的函数所以我们在main函数中判断如果用户使用我们的kill命令用的参数不对的话就给用户发一个使用手册然后退出程序这个使用手册就是教用户如何使用这个kill函数

 当我运行程序参数用的不对就会给我发一个使用手册下一步我们完善代码

在使用kill接口前我们先看看kill接口需要的参数和返回值

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <cerrno>
#include <cstring>
void Usage(std::string proc)
{
    std::cout<<"Usage: \n\t";
    std::cout<<proc<<"信号编号 目标进程\n"<<std::endl;
}
int main(int argc,char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);
    int n = kill(target_id,signo);
    if (n!=0)
    {
        std::cerr<<errno<<" : "<<strerror(errno)<<std::endl;
        exit(2);
    }
    return 0;
}

首先我们要用的参数都在参数列表中所以我们需要拿到用户提供的参数但是由于参数列表为char*类型所以我们需要将字符串式的信号转为整数所以我们用了atoi函数这个函数可以将字符串转为整数拿到了信号和进程编号后我们就可以使用kill函数了由于函数成功后返回0失败返回-1所以我们用了if条件判断然后我们再写一个死循环的程序等会让这个程序挂着然后用我们的kill程序杀死这个进程

 同时因为要生成两个可执行程序所以我们将makefile修改一下

 下面我们将程序运行起来

 程序运行起来后我们可以看到我们写的程序成功杀死了一个进程。下面我们看一下raise函数

 raise函数是谁调用我我就给谁传几号命令这里的命令是参数下面我们演示一下

 我们可以看到raise函数的作用确实是谁调用了我我就给谁发信号。

下面我们再看一下abort函数

 abort这个函数的含义是给自己发送指定的结束信号

 我们可以看到本来应该打印的end由于abort被迫停止所以abort是给自己发送指定的结束信号。

2.由软件条件产生信号

软件条件产生信号其实我们在学管道的时候就学过了我们在学管道的时候讲过如果管道的读端关闭了写端一直在写这个时候操作系统就会给管道发送13号信号关闭管道这就是由软件条件产生信号。下面我们主要讲解alarm函数这个函数的意思是

这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
下面我们写代码来使用一下这个函数

 下面我们用这个代码测试1秒钟CPU可以记多少数

 当我们计数了7万多后程序被alarm函数叫停而这个数据是真实的吗CPU1秒才能记这么多数吗其实不是因为我们计算要打印出来包含了IO包含了网络有很多影响的因素所以这样的测试并不准确如果测最准确的呢如下代码

 首先我们定义一个全局的count计数器然后用signal捕捉alarm函数在这期间计数器会一直加加当1秒达到后alarm发送命令然后被myhandler方法捕捉打印出信号和计数器的数值

 这次的计数才是正常的五亿多。

3.硬件异常产生信号

不知道大家有没有发现当我们写代码有对空指针进行解引用或者数组越界或者右除0操作时编译的时候都会提示我们出现错误那么错误是如何被发现的呢下面用代码来验证

首先我们重新创建一个文件然后写一段简单的代码

#include <iostream>
using namespace std;

int main()
{
    int a = 10;
    a/=0;    //进行除0操作引发异常
    cout<<"div zero ..... here"<<endl;
    return 0;
}

接下来我们直接编译一下

 可以看到在编译的时候直接给我们发出警告了当然我们还是可以继续编译的

 运行后我们发现输出了一行Floating point exception也就是浮点数异常下面我们讲一下原理

 如上图所示代码是在内存中的某个位置存储假设a/=0这个代码存储在内存的某个位置而在CPU中有各种各样的寄存器我们的代码在运行的时候会被加载到CPU当中然后CPU会将刚刚那个代码加载到寄存器里比如上图将a加载到一个寄存器将0加载到另一个寄存器而在CPU做计算时是有一个状态寄存器的这个状态寄存器会报错我们本次计算是否会有溢出问题一旦溢出了那么状态寄存器中的溢出标志位就被置为1了只要被置为1就说明计算有问题CPU就立马告知操作系统一旦操作系统发现确实状态寄存器中的标志位被置为1了那么操作系统就会向目标进程发送信号这个信号就是Floating point exception浮点数异常我们可以在信号中查看这个这个是几号信号

经过查询我们发现8号信号就是浮点数异常因为信号的后三位字母是刚刚报错信号的每个单词的首元素。当然我们也可以验证一下直接捕捉信号即可

当然我们也可以先看看这个信号的作用

 我们以前用的九号信号作用就是终止进程term就是terminal的缩写终止的意思core是什么呢我们等会再讲

#include <iostream>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout<<"我们的进程确实收到了"<<signo<<"号信号"<<endl;
}

int main()
{
    signal(8,handler);
    int a = 10;
    a/=0;    //进行除0操作引发异常
    cout<<"div zero ..... here"<<endl;
    return 0;
}

 当我们将异常信号捕捉后程序就不会停止了

 运行后一直死循环打印下面我们在捕捉的时候让这个进程退出

 运行后不再死循环了并且打印了我们要求的返回值。下面我们再试试其他异常

 我们发现程序正常编译但是同样直接结束了下面我们讲一下关于地址的问题

 首先有一个0000~FFFF的进程地址空间进程地址空间上的红色方框是指针的虚拟地址实际上是在最右边的物理内存上开辟空间的当我们对空指针解引用的时候其实访问的是进程地址空间的0号地址比如向0号地址写100要经过页表转化到物理内存中但是页表实际上是做KV关系的做转化的动作不是由软件完成的而是由硬件完成的这个硬件叫MMUMMU被称为内存管理单元所以从虚拟地址转化到物理地址采用软硬件结合的方式以上方式是正常情况而我们对空指针进行解引用首先指针与页表没有对应的映射关系对0号地址是没有写权限的所以我们对空指针写入是非法的。*p = 100这句代码第一步并不是写入而是首先进行虚拟到物理地址的转换在转换的时候要进程地址空间是否和页表有映射关系如果没有映射则MMU会硬件报错如果有映射还需要看是否有对应的权限如果没有权限也会报错如果MMU报错也就是硬件报错操作系统就会识别到然后操作系统向当前进程的PCB发送信号以上就是对空指针解引用的报错原理。


总结

以上就是linux信号产生的所有知识下一篇我们将详细讲解linux信号是如何保存和处理的。

上面所说的所有信号产生最终都要有 OS 来进行执行为什么 OS是进程的管理者。
信号的处理是否是立即处理的 不是。是在合适的时候。
信号如果不是被立即处理那么信号是否需要暂时被进程记录下来记录在哪里最合适呢
需要被记录下来记录在进程PCB中
一个进程在没有收到信号的时候能否能知道自己应该对合法信号作何处理呢 能知道因为程序员教了。
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux