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

简介: 【Linux】进程信号“疑问?坤叫算信号吗?“(下)

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函数:


2913e454ccc540cf889801f5d69759bb.png


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


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


63583f03b81d4cf080f3742ab90ecfb7.png

8ed357a9cb1a4cb4aa14d2cd56d85042.png


#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程序杀死这个进程:


20a1e29b72b048e3b357eac25800cf70.png


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

308b6aa5cbbf4e9a95949f8f4e2befbc.png


下面我们将程序运行起来:


2a9bb7783aa4404e87467b965add2d6d.png50ec32fc89694eff83f5b1d457ef2b0f.png


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


ff3dc4678ba3456aa0e90a4d02ec49f6.png


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


fb752496f55e44e1a989120d688bdcd6.png

73c8fea2f8074157a84747300c4d149f.png


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


下面我们再看一下abort函数:

40dcec6d0459442b8ae9b0bdb947ce81.png


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


bc3a8d3c3d5e43808121ebf67c9617d5.png

e564b6baef7d43288b71a125dd4b4b74.png


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


2.由软件条件产生信号


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


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

下面我们写代码来使用一下这个函数:

11cc28ee13a74f93a1e7f9f2c70fa4f5.png

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

c28da9097c9d4c17b8a730e32eb95def.png


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


aa8fcad3f51c4ed592483c5c252587e9.png


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


1ca2b6a1af4e497c896d6f80df2c79b3.png


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


3.硬件异常产生信号


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


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


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

接下来我们直接编译一下:


d51e12cf03d042af89786297d2b7a5af.png


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


d590143067d1424886dda9cc239631a6.png


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


61d7822d51f445aa9be644a817466a72.png


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

9e380826093643178f590fa01344ed99.png

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


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


ada5db1b0f1f4b219838e27db77fb081.png


我们以前用的九号信号作用就是终止进程,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;
}


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


e56747be602349b89905e37b24960583.png


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


645e68e958644a3691375c1ca3644420.png

7077ffa9c9d740fe8c8bc87fe594a442.png


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


9f4424730c1047789a459e216a30a6e7.png109e17b94907448c856449f8fbe21a64.png


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


2be18391098f4341abf0c823ba05dc9c.png


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


总结



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


上面所说的所有信号产生,最终都要有 OS 来进行执行,为什么? OS是进程的管理者。


信号的处理是否是立即处理的? 不是。是在合适的时候。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?


需要被记录下来,记录在进程PCB中


一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢? 能知道,因为程序员教了。


目录
相关文章
|
4天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
26 4
linux进程管理万字详解!!!
|
4天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
24 4
|
5天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
6天前
|
消息中间件 存储 Linux
|
13天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
12 1
|
24天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
18 1
|
29天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
26 0
Linux c/c++之IPC进程间通信
|
29天前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
20 0
Linux c/c++进程间通信(1)
|
29天前
|
Linux C++
Linux c/c++之进程的创建
这篇文章介绍了在Linux环境下使用C/C++创建进程的三种方式:system函数、fork函数以及exec族函数,并展示了它们的代码示例和运行结果。
30 0
Linux c/c++之进程的创建
|
29天前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
17 0