首页 > 系统相关 >从0开始linux(32)——进程信号(1)信号发生

从0开始linux(32)——进程信号(1)信号发生

时间:2024-11-20 16:47:31浏览次数:3  
标签:core int 32 SIGINT 信号 linux 进程 行为

欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪

文章目录

信号

在我们的日常生活中存在各种各样的信号,比如:红绿灯,上课铃声,闹铃等。当人捕捉到这些信号时,会执行各种各样的操作。比如红灯停,绿灯行,上课铃声响了就要去上课。

这里我们得出有关信号的第一个结论:信号传递到进程时,进程会执行对应的操作。

那么第一个问题就来了,人们是对这些信号天生就有反应吗?答案肯定是否,因为在我们成长的过程中,别人一直在说红灯停、绿灯行,人们才会对红绿灯这个信号做出反应。因此对于人来说,关于信号的反应,是要提前约定好的,正是人对信号有了相关的概念,才能做出正确的操作

这里得出第二个结论:系统要在信号发生之前,与进程约定好操作

不同的情况下,对于信号的执行也是不同的,比如正常车辆,在道路交通时,要遵守红灯停、绿灯行。但是警车和救护车在工作时,就不用根据红绿灯的信号来执行操作。因为特殊身份的人,面对信号可以做出特殊处理

这里得出第三个结论:并非所有的进程,都要对信号做出相同的反应。

那么根据上面所讲述的结论,我们展开讲解进程信号。

信号发生

在ubuntu发行版7.1中,linux系统设计的信号有62个。通过kill -l指令可以看到所有的信号,以及对应的信号值。
在这里插入图片描述
其中,信号值(1)~(31)的信号,称为普通信号,(34)~(64)的信号,我们称为实时信号。由于实时信号的使用场景博主还尚未讲解,因此本篇的着重点在信号值(1)~(31)的信号。

接下来我们要讲解的第一点就是,信号是如何产生的?以我们的现实为例,红绿灯信号发生需要让红绿灯通电,因此。我们的进程信号发生,一定要满足某种条件,才会导致信号发生。

键盘发生的信号

如果我们现在有一个常驻进程正在使用,我们可以通过ctrl+c的组合键,将该进程终止,这是博主在很早期讲解linux系统时就提到过的操作,实际上这个说法只提到了表面,我们对于ctrl+c的原理,还是不够了解。

ctrl+c,会向前台进程发送一个SIGINT的信号,其信号值为(2)。在这里插入图片描述
我们可以通过man 7 signal的方式,查看到linux信号与其信号所执行的操作。
在这里插入图片描述
(在后面的文章中,除非必要,不然博主不再将查阅man的结果展示出来,避免篇幅过长。这里介绍是为了让读者可以自行查看)

在这里插入图片描述
ctrl+c的组合键会向前台进程发送SIGINT信号。而SIGINT信号的行为为Term,全称为Termination,即终止的意思,也就是说当进程接收到SIGINT信号时,会终止。注意ctrl+c只会对前台进程发生SIGINT信号,而后台进程则对此毫无反应。那么什么是前台进程,什么是后台进程呢?这里博主做一个简单的介绍。

每一个用户终端,有且只有一个前台进程,这个前台进程就是用户当前使用的进程。但是linux是一个支持多进程的操作系统,因此除了前台终端外,还有许多的进程,这些进程则是后台进程。前台进程负责与用户直接交互。比如我们使用windows系统时,打开了qq和微信聊天的窗口。如果点击qq的聊天窗口,那么qq就会变成前台进程,我们键盘输入的信息,都会出现在qq的聊天窗口,而不是微信的聊天窗口,因此若想使用微信的聊天窗口,就要点击微信,使微信成为新的前台窗口。

通常来说,每个用户在登录终端后,都是将bash进程作为前台进程。因为我们需要通过bash进程进行命令行输入。我们通过./[process]运行的进程,会替代bash成为前台进程。如果想要让进程作为后台进程来执行,则需要输入命令./[process] &

现在我们打开一个进程,使其分别以前台进程或后台进程的方式运行,在运行的过程中我们尝试按下ctrl+c。可以发现前台进程会被终止,而后台进程不会被终止。这个很简单,博主就不展示了。

这里可能有人就会发现一个问题了,那就是根据我们前面所说的,bash也是前台进程,但是我们在bash中按下ctrl+c,bash进程却不会退出。
在这里插入图片描述
这是因为bash进程,对接收到SIGINT信号产生的行为,重新自定义了。因为bash是一个特殊的前台进程,其是用户与内核进行交互的重要一环,如果因为用户误触组合键而导致bash进程终止,那么肯定不是一个好的设计。

那么如何让进程对信号产生的行为进行重定义呢?这里就要用上系统调用了。

 #include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

系统调用signal,可以将signum信号值对应的信号,产生的进程行为发生改变,这个行为需要用户自定义,即创建一个函数,然后通过handler将该函数以函数指针的方式传递给signal函数。这样进程在接收到signum信号值时,产生的行为就不再是原来的默认行为,而是由用户定义的handler行为,而handler行为的函数类型为void (int)。

我们就用这个系统调用来试试改变一个进程,对于SIGINT信号产生的行为从终止变为打印。

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

void SigintHandle(int signum)
{
    std::cout<<"signum:"<<signum<<std::endl;//signum可以接收到信号值
}

int main()
{
    ::signal(SIGINT,SigintHandle);//将SIGINT的行为重定义
    //当进程接收到SIGINT信号值时,就会调用siginthandle函数
    while(1)
    {
        std::cout<<"hello world\n"<<std::endl;
        sleep(1);
    }
    return 0;
}

现在我们已经将该进程接收到SIGINT的行为从终止,变为了打印signum的值。因此在该进程收到SIGINT信号时(即ctrl+c),会将SIGINT的信号值打印出来,即2。
在这里插入图片描述
但是并非所有信号值的行为都能重定义,如果可以这样做的话,如果一个进程中将对所有信号值的行为从停止改为其他行为,那这个进程就永远无法关闭了,除了将计算机重启。那么设计师显然是考虑到了这一点的,因此有一些信号值即使重定义了,行为依然不会改变,比如(9)SIGKILL
在这里插入图片描述

信号是如何发送给进程的?

在上一个例子中,即使进程在执行I/O操作,但是一旦信号发生,进程会立即执行信号对应的行为。这与我们对于进程的理解大相径庭,因为在之前如果进程在运行时,无论我们如何操作,进程是只会对代码顺序执行的,也就是说执行到什么代码,就会产生代码所具有的操作,但是在上例中,我们一旦发送了SIGINT信号,进程立马就执行了SIGINT对应的行为。那么为什么进程会对信号这么特殊呢?

这是因为,信号并不是发送给进程的,而是发送给操作系统的。在前面的章节中博主提到过,OS是通过PCB(task_struct)来管理进程的,在task_struct中,会存在一个位图,该位图的名字为signalbits。
在这里插入图片描述
发送信号的本质是,给OS发送一个信息,即32位的位图,可以表现出32个普通信号的发生情况,比如当SIGINT(2)发生时,将signalbits的第二位变为1,这样就能表示SIGINT信号发生在了该进程中。
在这里插入图片描述
那么现在OS知道该进程的信号SIGINT发生了,就要对该进程执行对应的操作,而这些对应的操作也会保存在进程的PCB中。在PCB中存在一个专门保存信号发生时行为的数组,该数组保存的是函数方法,比如当SIGINT被置入时,就执行arr[SIGINT]的方法。这样不就相当于进程一收到信号,就执行信号对应的行为了吗?

在这里插入图片描述
在默认行为下,该进程执行SIGINT的行为是终止,而通过signal重定义行为后,就变成了执行SigintHandle。因此signal函数的作用是:将pcb中关于信号方法数组的对应行为,换成用户自定义的函数。

从这里我们也可以得出结论,任何进程信号,不是发送给进程的,而是发送给操作系统的。然后操作系统接收信号后,然进程执行信号对应的行为。

信号是如何发送给系统的?

通过前面的讲解,我们知道了信号并不是发送给进程的,因此向进程发送信号是一个伪命题。信号是发送给操作系统的,更具体的说,是发送给操作系统,让操作系统根据信号来管理进程的,和进程无关。那么此时一个新的问题又产生了。信号又是如何发送给操作系统的呢?

实际上信号发生的情况有很多,我们目前只讲了一个键盘发生,但是没关系,后面我们还会继续补充,因此我们还是拿键盘发送信号为例。

那么一个键盘作为一个硬件,而操作系统是一个软件,一个硬件是如何向软件发送信号呢?这个问题的答案,我们得从硬件层面进行解答。

在这里插入图片描述
这是每个计算机都遵循的结构,即冯诺依曼体系结构。我们的键盘,显示器,网卡一类的硬件设备,称为外部设备。而控制器和运算器,在现在都集成为了CPU。虽然CPU不会与外设进行数据交换,但是外设的信号(注意是硬件信号,即电路产生的电信号),是可以传递给CPU的。当外设有数据要发送时,比如我们在键盘输入了数据,又比如我们的网卡接受了数据。此时外设会向CPU发送中断信号。提醒cpu,我们外设已经准备好数据了,快来读我们的数据吧。然后cpu就会让操作系统去从设备当中读取数据。

那么这里就可以解释了,为什么我们按下ctrl+c,操作系统就能收到SIGINT信号。这是因为键盘向CPU发送了中断信号,接着操作系统就会读取键盘的数据,自然而然的读取到了ctrl+c。然后向前台进程的pcb写入SIGINT信号,再让前台进程执行SIGINT信号对应的操作。这样就完成一次信号的发生,接收,与执行了。

其他的信号发生

由系统指令发生的信号

我们可以通过kill -[signum] [pid]指令,向pid对应的进程,发送[signum]对应的信号。比如kill -9 11111。即向pid为11111的进程,发送9信号。9信号对应的是SIGKILL。实际上是让操作系统向pid11111的进程PCB的signal位图的第9位置为1,然后再让pid11111的进程执行SIGKILL对应的行为,由于SIGKILL不支持重定义行为。因此pid为11111进程就会被终止。

由系统调用发生的信号

系统调用kill可以向pid进程发送sig信号,其函数原型如下:

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

当进程调用kill函数时,会向pid进程,发送sig信号,其实这与kill指令很像对不对,实际上kill指令本身就是进程,而进程又有代码,因此,kill指令当中的代码使用的就是系统调用kill!!!!。我们可以尝试写一个仿kill指令的程序。

void Useage()
{
    std::cout<<"Useage: ./mykill [signum] [pid]"<<std::endl;
}
//kill 9 pid
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Useage();
        exit(1);
    }
    int signal=std::stoi(argv[1]);
    pid_t pid=std::stoi(argv[2]);
    ::kill(pid,signal);
    return 0;
}

我们随便写一个死循环打印“hello world”的进程,然后启动该进程,接着打开另外一个终端,使用mykill进程关闭该进程。
在这里插入图片描述

由软件条件引发的信号

这个条件我们可以视为类似于if语句的东西。以管道读写端异常引发信号SIGPIPE。就属于这种类型的信号。

在管道章节中,博主就提到过,当管道读端关闭,而写端依旧向管道写入数据时,就会产生SIGPIPE信号,导致写端的进程被终止。关于SIGPIPE的引发条件,在手册当中也能查看到。
在这里插入图片描述
相当于是触发了条件(进程向没有读端的管道写入),就引发了SIGPIPE信号。

除了SIGPIPE以外,还有一个SIGALRM也属于是软件条件引发的信号。其发送信号需要使用系统调用alarm。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

alarm需要传入参数seconds。在调用alarm(seconds)后,经过seconds秒,就会向进程发送信号SIGALRM。SIGALRM的默认行为是终止进程。

我们可以写一个简单的代码来看看alarm函数的作用。

int main()
{
    unsigned int second=5;
    alarm(second);//进程运行5s后,发送SIGALRM信号
    while(true)
    {
        std::cout<<"last time:"<<second<<'s'<<std::endl;
        second--;
        ::sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述
如果我们使用signal函数,将SIGALRM信号的行为重定义,那么就可以诞生了一个计时程序,比如我们可以写一个程序,每个三秒就刷新数据(这个行为我们通过打印来表示),无论程序运行到什么步骤,都会每个3s执行我们想要进程执行的行为。那么一个定时执行的程序就诞生了。

int cnt=0;
void Handle(int signum)
{
    cnt++;
    std::cout<<"更新数据:"<<cnt<<std::endl;//表示该行为执行了多少次
    alarm(3);
}

int main()
{
    signal(SIGALRM,Handle);
    unsigned int second=3;
    alarm(second);//进程运行5s后,发送SIGALRM信号
    while(true)
    {
        std::cout<<"hello world"<<std::endl;
        ::sleep(1);
    }
    return 0;
}

在这里插入图片描述

由于进程异常引发的信号

在c/c++编程的过程中,有几种错误引发的程序崩溃相信各位c/c++程序员都屡见不鲜了吧。首先是野指针问题,如果我们错误的使用了野指针,就会引发程序崩溃,但是实际上这个崩溃是因为当进程出现野指针错误时,进程会发送SIGSEGV信号,该信号的默认行为就将该进程终止,并且生成core文件。我们在系统手册当中可以查到。
在这里插入图片描述
这里我们发现,SIGSEMV信号的默认行为是Core,而SIGINT的默认行为是Term。但是从我们的视角来看,core和term的行为都是终止进程,很难发现它两的区别。这点我们后面再说。

还有一种是由于除0操作引发的进程错误,即1/0的算术操作,发生了错误,当进程发生除0错误时,操作系统会向进程发送SIGFPE信号。该信号的默认行为也是Core

Core与Term默认行为的差别

那么Core和Term的行为的差别是什么?Term是termination的缩写,即终止进程。而Core除了会终止进程外,还会产生core文件。

那么有人可能就会产生疑问了,引用野指针也不是没做过,但是我从来没见过core文件啊?这是因为,如果使用云服务器来作为linux环境的话。会默认关闭生成core文件这个操作。使用ulimit -a指令,可以看到core文件的相关信息。
在这里插入图片描述
那么如果我们允许生成最大core文件的大小改为10240个块(一个块4kb)。那么就能看到core文件了。指令为ulimit -c 10240。(不一定要10240,1024也行)
在这里插入图片描述
core文件的作用,叫做核心转储,因为当程序由于野指针发生错误时,如果程序是大型程序的话,那么排查工作其实是很困难的,如果我们手动的使用gdb来排查野指针,无疑是在大海捞针,而core文件是在进程发生与Core行为相关的信号时,生成的文件,这个文件会记录发生错误的程序段在哪,以及相关的内存信息。但是要注意,只有debug版本的进程,core文件的信息才有意义,因此我们首先随便写一个野指针的程序。并且将其的编译模式加上-g选项,-g选项表示该编译生成的程序将会是debug版本的。

int main()
{
    int * ptr=nullptr;
    *ptr=100;
    return 0;
}

接着执行该程序,可以发现屏幕上会打印出错误信息,并且生成一个core文件。
在这里插入图片描述

那么这个core文件该怎么用呢?我们打开gdb,调试出现core错误的程序test。接着输入core [corefilename]。就能查看到core文件中包含的信息了。
在这里插入图片描述

标签:core,int,32,SIGINT,信号,linux,进程,行为
From: https://blog.csdn.net/2301_77239666/article/details/143819609

相关文章

  • esp32超声波检测
    为声波在空气中的传播速度是固定的,发射的超声波遇到障碍物会反射回来,我们记录下发射波到接受反射波之间的时间差,就可以计算出模块距离障碍物的距离。我们可以把这个用于测距、避障等领域。HC-SR04模块的测量距离为2-400cm,测量角度为30°,当测量距离大于范围时传感器接收不到......
  • ESP32-CAM模块介绍
    一、引言    在当今的物联网时代,各种智能设备层出不穷。ESP32-CAM模块以其强大的功能、小巧的尺寸和高性价比,成为了众多开发者和爱好者的热门选择。本文将详细介绍ESP32-CAM模块的特点、功能、应用场景以及使用方法。二、ESP32-CAM模块概述    ESP32-CAM是一......
  • 【Linux Ops】如何替换 libstdc++ 提升 GLIBCXX 版本
    【环境】kos5.8sp2,kernel5.10还是上一篇提到的那个软件环境,其依赖的GLIBCXX版本较高,因此在安装时给出了以下错误:xxx:/lib64/libstdc++.so.6:version`GLIBCXX_3.4.29'notfound(requiredbyxxx)执行strings/usr/lib64/libstdc++.so.6|grepGLIBCXX,查看当前环境中......
  • Linux常用命令之info命令详解
    info命令详解info命令是GNU项目提供的一个命令行工具,用于查看详细的文档信息。与man命令相比,info文档通常更加详尽和结构化,更适合深入学习和参考。info文档系统是GNU项目的一部分,旨在提供全面的、结构化的文档,帮助用户更好地理解命令、函数、库和其他GNU软件组......
  • linux基础笔试练习题笔记(3)
    Linux文件系统的文件都按其作用分门别类地放在相关的目录中,对于外部设备文件,一般应将其放在哪个目录中()A./binB./etcC./devD./lib答案解析:/bin,bin是Binaries(二进制文件)的缩写,这个目录存放着最经常使用的命令。/etc,etc是Etcetera(等等)的缩写,这个目录用来存放所有的系......
  • Linux – menuconfig讲解
     原文:https://blog.csdn.net/qq_42837317/article/details/139754748menuconfig1.简介        menuconfig是一套图像化配置工具,由ncurses库提供软件支持。ncurses库提供了一系列的函数以便使用者调用它们去生成基于文本的用户界面。        menuconfig本......
  • [linux] 根据日志文件ban掉ip地址
    搬家之后就没有公网ip地址了,所以在阿里云买了一个服务器,带上优惠160买了三年,流量计费的服务器。并且配置nps用来实现内网穿透。某天发现阿里云的流量计费每隔一段时间扣0.01元,很小的费用,但是我想追究一下为什么会有流量费于是发现,Nps的日志中不停的有新的tcp连接,去看了内网的ssh......
  • 【Linux Ops】如何无痛升级 glibc
    【环境】kos5.8sp2,kernel5.10最近工作中需要搭建一个软件环境,其依赖的glibc版本较高,因此在安装时给出了以下错误:xxx:/lib64/libc.so.6:version'GLIBC_2.33'notfound(requiredbyxxx)去查看当前机器的libc.so支持的GLIBC版本,发现确实太低了:strings/usr/lib64/......
  • linux学习day03_linux文件与目录管理
    1、相对路径和绝对路径的区别绝对路径:路径的写法“一定由根目录/写起”,例如:/usr/share/doc这个目录。相对路径:路径的写法“不是由/写起”,例如由/usr/share/doc要到/usr/share/man下面时,可以写成:“cd../man”这就是相对路径的写法啦!相对路径意指“相对于目前工作目......
  • stm32f4 使用FreeRTOS例程
    文章目录引言开发环境搭建配置STM32CubeMX编写FreeRTOS任务代码编译与调试结论stm32f4使用FreeRTOS例程引言随着物联网(IoT)和嵌入式系统的发展,实时操作系统(RTOS)在资源受限的嵌入式设备上得到了广泛应用。FreeRTOS作为一种开源的、可裁剪的RTOS,因其轻量级、高可靠......