Linux系统-进程信号(上)

简介: Linux系统-进程信号(上)

零、前言


本章主要讲解学习Linux中的信号,从信号的产生到识别,再到处理的各个时期的详细学习


一、信号入门


1、生活角度的信号

  • 示例:
  1. 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  2. 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”
  3. 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了
  4. 而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话


2、技术应用角度的信号

  • 示例:

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


  • 示图:

image-20220326201835336.png

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


3、信号及其处理概念

  • 信号的基本概念:
  1. 信号是进程之间事件异步通知的一种方式,属于软中断
  2. 用kill -l命令可以察看系统定义的信号列表

202203241754550.png

3、每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到

202203241802883.png

4、编号1-31的信号是普通信号,在合适的时候进行处理,而编号34-64的信号是实时信号,需要进行立即处理

5、这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

202203241804930.png


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


二、信号产生


1、终端按键产生

SIGINT(ctrl+c)的默认处理动作是终止进程,SIGQUIT(ctrl+\)的默认处理动作是终止进程并且Core Dump,这个键盘输入产生一个硬件中断,被OS获取解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出


  • Core Dump的概念:
  1. 当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core+进程id,这叫做Core Dump
  2. 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做事后调试
  3. 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中),默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息不安全,且产生的core文件内容比较大


注:在开发调试阶段可以用ulimit -c 1024命令限制,允许产生core文件(允许core文件最大为1024K)

202203241813825.png


  • 示例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main()
{
    while(1)
    {
        cout<<"getpid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}


  • 示图:

202203241823594.png

注:使用gdb对当前可执行程序进行调试,然后直接使用core-file core文件命令加载core文件,即可判断出该程序在终止时的信号,并且定位错误代码


  • Core dump标志位:

waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同


  • 示图:

202203241828328.png

  • 注意:
  1. 若进程是正常终止的,那么status的次低8位就表示进程的退出码
  2. 若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储


2、kill命令发信号

首先在后台执行死循环程序,然后用kill命令给它发信号


  • 示例:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
int main()
{
    if(fork()==0)
    {
        //child
        while(1)
        {
            cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
        }
        exit(0);
    }
    //father 
    cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
    int status=0;
    int ret=waitpid(-1,&status,0);
    if(ret>0&&WIFEXITED(status))
    {
        cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl;
    }
    else if(ret>0)
    {
        cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl;
    }
    return 0;
}


  • 结果:

202203241903159.png

  • 注意:
  1. 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGFPE 18425 或 kill -8 4568 , 8是信号SIGFPE的编号
  2. kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号;raise函数可以给当前进程发送指定的信号(自己给自己发信号) ;abort函数使当前进程接收到信号而异常终止


  • 函数原型:
#include <signal.h>
int kill(pid_t pid, int signo);
//第一个参数为对应进程的id,第二个参数为想要发送的信号编号
int raise(int signo);
//这两个函数都是成功返回0,错误返回-1
#include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值


3、软件条件产生信号

  • SIGPIPE信号:

SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止


  • 示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
  int fd[2] = { 0 };
  if (pipe(fd) < 0){ //使用pipe创建匿名管道
    perror("pipe");
    return 1;
  }
  pid_t id = fork(); //使用fork创建子进程
  if (id == 0){
    //child
    close(fd[0]); //子进程关闭读端
    //子进程向管道写入数据
    const char* msg = "hello father, I am child...";
    int count = 10;
    while (count--){
      write(fd[1], msg, strlen(msg));
      sleep(1);
    }
    close(fd[1]); //子进程写入完毕,关闭文件
    exit(0);
  }
  //father
  close(fd[1]); //父进程关闭写端
    char buffer[128]={0};
    ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置
    if(s>0)
    {
        buffer[s]=0;//设置结束符
        printf("msg from child:%s",buffer);
    }
    else if(s==0)
    {
        printf("子进程写端关闭...\n");
    }
  close(fd[0]); //父进程读一次直接关闭读端(导致子进程被操作系统杀掉)
  int status = 0;
  waitpid(id, &status, 0);
  printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
  return 0;
}


  • 结果:

4466e0e248dffa7c936b06dfb46f463f.png

  • alarm函数和SIGALRM信号:

alarm函数原型:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程


  • 解释:
  1. 功能:让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程
  2. 返回值:若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置;如果调用alarm函数前,进程没有设置闹钟,则返回值为0

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

  • 示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt=0;
void  handler(int signo)
{
    cout<<"get a signal:"<<signo<<" cnt:"<<cnt<<endl;
    exit(0);
}
int main()
{
    //对信号SIGALRM进行捕获
    signal(SIGALRM,handler);
    alarm(1);//1秒后唤醒
    while(1)
    {
        cnt++;
    }
    return 0;
}


  • 效果:

1784004f9edb8e49baa6617657660e4c.png

注:这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止


4、硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号


示例:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程;当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程


  • 示例:子进程野指针错误
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
int main()
{
    if(fork()==0)
    {
        //child
        int cnt=0;
        while(cnt<5)
        {
            cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
            sleep(1);
            cnt++;
        }
        int* p=NULL;
        *p=100;
        sleep(1);
        exit(0);
    }
    //father 
    cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
    int status=0;
    int ret=waitpid(-1,&status,0);
    if(ret>0&&WIFEXITED(status))
    {
        cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl;
    }
    else if(ret>0)
    {
        cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl;
    }
    return 0;
}


  • 结果:

745e9589a9eb30fb93a76809b15c7013.png

在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的

目录
打赏
0
0
0
0
2
分享
相关文章
|
24天前
|
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
122 84
|
13天前
|
Linux系统资源管理:多角度查看内存使用情况。
要知道,透过内存管理的窗口,我们可以洞察到Linux系统运行的真实身姿,如同解剖学家透过微观镜,洞察生命的奥秘。记住,不要惧怕那些高深的命令和参数,他们只是你掌握系统"魔法棒"的钥匙,熟练掌握后,你就可以骄傲地说:Linux,我来了!
86 27
|
17天前
|
Linux系统ext4磁盘扩容实践指南
这个过程就像是给你的房子建一个新的储物间。你需要先找到空地(创建新的分区),然后建造储物间(格式化为ext4文件系统),最后将储物间添加到你的房子中(将新的分区添加到文件系统中)。完成这些步骤后,你就有了一个更大的储物空间。
80 10
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
36 4
基于进程热点分析与系统资源优化的智能运维实践
智能服务器管理平台提供直观的可视化界面,助力高效操作系统管理。核心功能包括运维监控、智能助手和扩展插件管理,支持系统健康监控、故障诊断等,确保集群稳定运行。首次使用需激活服务并安装管控组件。平台还提供进程热点追踪、性能观测与优化建议,帮助开发人员快速识别和解决性能瓶颈。定期分析和多维度监控可提前预警潜在问题,保障系统长期稳定运行。
78 17
|
29天前
|
Linux系统中如何查看CPU信息
本文介绍了查看CPU核心信息的方法,包括使用`lscpu`命令和读取`/proc/cpuinfo`文件。`lscpu`能快速提供逻辑CPU数量、物理核心数、插槽数等基本信息;而`/proc/cpuinfo`则包含更详细的配置数据,如核心ID和处理器编号。此外,还介绍了如何通过`lscpu`和`dmidecode`命令获取CPU型号、制造商及序列号,并解释了CPU频率与缓存大小的相关信息。最后,详细解析了`lscpu`命令输出的各项参数含义,帮助用户更好地理解CPU的具体配置。
93 8
深度体验阿里云系统控制台:SysOM 让 Linux 服务器监控变得如此简单
作为一名经历过无数个凌晨三点被服务器报警电话惊醒的运维工程师,我对监控工具有着近乎苛刻的要求。记得去年那次大型活动,我们的主站流量暴增,服务器内存莫名其妙地飙升到90%以上,却找不到原因。如果当时有一款像阿里云 SysOM 这样直观的监控工具,也许我就不用熬通宵排查问题了。今天,我想分享一下我使用 SysOM 的亲身体验,特别是它那令人印象深刻的内存诊断功能。
|
1月前
|
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
58 5
|
19天前
|
微服务2——MongoDB单机部署4——Linux系统中的安装启动和连接
本节主要介绍了在Linux系统中安装、启动和连接MongoDB的详细步骤。首先从官网下载MongoDB压缩包并解压至指定目录,接着创建数据和日志存储目录,并配置`mongod.conf`文件以设定日志路径、数据存储路径及绑定IP等参数。之后通过配置文件启动MongoDB服务,并使用`mongo`命令或Compass工具进行连接测试。此外,还提供了防火墙配置建议以及服务停止的两种方法:快速关闭(直接杀死进程)和标准关闭(通过客户端命令安全关闭)。最后补充了数据损坏时的修复操作,确保数据库的稳定运行。
50 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等