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++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的

相关文章
|
6天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
25 3
|
6天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
19 2
|
7天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
43 8
|
6天前
|
安全 网络协议 Linux
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。通过掌握 ping 命令,读者可以轻松测试网络连通性、诊断网络问题并提升网络管理能力。
24 3
|
9天前
|
安全 Linux 数据安全/隐私保护
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。本文介绍了使用 `ls -l` 和 `stat` 命令查找文件所有者的基本方法,以及通过文件路径、通配符和结合其他命令的高级技巧。还提供了实际案例分析和注意事项,帮助读者更好地掌握这一操作。
26 6
|
9天前
|
Linux
在 Linux 系统中,`find` 命令是一个强大的文件查找工具
在 Linux 系统中,`find` 命令是一个强大的文件查找工具。本文详细介绍了 `find` 命令的基本语法、常用选项和具体应用示例,帮助用户快速掌握如何根据文件名、类型、大小、修改时间等条件查找文件,并展示了如何结合逻辑运算符、正则表达式和排除特定目录等高级用法。
36 6
|
10天前
|
监控 网络协议 算法
Linux内核优化:提升系统性能与稳定性的策略####
本文深入探讨了Linux操作系统内核的优化策略,旨在通过一系列技术手段和最佳实践,显著提升系统的性能、响应速度及稳定性。文章首先概述了Linux内核的核心组件及其在系统中的作用,随后详细阐述了内存管理、进程调度、文件系统优化、网络栈调整及并发控制等关键领域的优化方法。通过实际案例分析,展示了这些优化措施如何有效减少延迟、提高吞吐量,并增强系统的整体健壮性。最终,文章强调了持续监控、定期更新及合理配置对于维持Linux系统长期高效运行的重要性。 ####
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
167 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
3月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。