Linux:深入了解进程信号(上)

目录

[1. 什么是信号](#1. 什么是信号)

[1.1 引入](#1.1 引入)

[1.2 概念](#1.2 概念)

[1.3 特性](#1.3 特性)

[1.4 信号的三个方面](#1.4 信号的三个方面)

[2. 信号的产生](#2. 信号的产生)

[2.1 键盘按键产生](#2.1 键盘按键产生)

[2.2 signal捕捉信号函数](#2.2 signal捕捉信号函数)

[2.3 发送信号原理](#2.3 发送信号原理)

[2.4 硬件中断](#2.4 硬件中断)

[2.5 指令和函数接口](#2.5 指令和函数接口)

[2.5.1 kill指令](#2.5.1 kill指令)

[2.5.2 kill函数](#2.5.2 kill函数)

[2.5.3 raise与abort函数](#2.5.3 raise与abort函数)

[2.6 软件条件](#2.6 软件条件)

[2.7 异常错误产生信号](#2.7 异常错误产生信号)


1. 什么是信号

1.1 引入

在我们的日常生活中,会遇到许多信号,比如红绿灯、铃声和乌云。看到红灯,我们知道需要停下来等待;绿灯亮起,我们可以安全通行。教室里的铃声响起,我们明白这是下课的信号。当看到天空中布满乌云,我们就知道应该把晾晒的衣服收回家了。这些信号帮助我们按照既定的规则和自然的指示来安排我们的行动。

1.2 概念

信号是用户、操作系统、其他进程,向目标进程发送异步事件的一种方式。

什么是异步事件?异步事件(Asynchronous Event)是指在一个系统或程序中,事件的发生和它的处理不是在同一个时间序列或流程中进行的。

比如,当你正在玩游戏时,突然快递到你家楼下,你需要暂时中断游戏去取快递。这两个活动各自独立,展现了异步事件的特点。

1.3 特性

为什么我们能识别信号呢?当我们看到红绿灯,知道这是交通信号。因为有人告诉我们,所以识别信号是内置的。

  • 进程也是如此,识别信号的功能,是程序员内置的特性。

我们遇到红灯知道停下来,绿灯响才能过马路。这些都是别人规定的规则。

  • 所以信号产生后,进程知道怎么处理,是程序员设定的。

当你在玩游戏时,突然快递到楼下了。你可能处在游戏的关键时刻,不能立即处理,需要过一会。

  • 进程收到信号时,不一定要立即处理该信号,需要在合适的时候处理。

设定起床闹钟,是为了在特定时间提醒我们。当闹钟响时,有的人起床按掉闹钟,开始做计划之内的事情;有的人可能会按掉闹钟,继续睡觉;有的人可能会赖一会床,再起来。

  • 同样的,进程在接受信号后也有不同的处理方式,分别是采取默认行为,忽略信号,或执行程序员设定的自定义动作。

1.4 信号的三个方面

信号的产生与处理是信号机制中不可或缺的两个环节。然而,还有一个重要的方面尚未提及,那就是信号的保存。

当进程在收到信号时恰好忙于处理其他任务,无法立即对信号作出响应,此时信号的保存机制便发挥了作用。它确保了信号能够被临时记录在案,以便进程在合适的时机对其进行后续处理。

因此,进程信号会从信号的产生,信号的保存和信号的处理这三方面着手讲起。

2. 信号的产生

2.1 键盘按键产生

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main() 
{    
    while(true)
    {
        std::cout << "hello, world!" << std::endl;
        sleep(1);
    }
    return 0;
}

我们执行一个无限循环打印helloworld的代码。可以按键盘上Ctrl+C,来终止该进程。其中这种直接运行的进程时前台进程。

如果在可执行程序后面加上"&"符号,表示将该进程转为后台进程。

前台进程会占用了前台终端,导致shell进程无法接收输入的命令行来执行命令。所以Ctrl+C按键是发送给前台进程,作用是终止前台进程。

而后台进程允许shell进程执行命令行输入的指令。如下图,启动循环打印的程序,中间输入ll指令,显示该目录的文件内容。

如果想杀掉后台进程,可以打开一个新会话,使用kill指令配合-9选项,终止后台进程。

还可以使用nohup指令,将启动程序输出结果重定向到一个默认文件,叫做nohup.out。nohup.out文件不断记录进程输出内容。

此时,可以使用fg文件加上第一次启动进程中括号的数字1,可以将后台进程转为前台进程,再按Ctrl+C按键终止进程。

2.2 signal捕捉信号函数

其中sighandler是函数指针类型,signal函数用于捕捉信号,并使用传进来的函数指针对象对应的方法处理信号。

使用"kill -l"指令可以查看Linux系统提供的常见信号。1号到31号中的信号,是普通信号,之后的信号统称为实时信号。我们重点关注普通信号。其中每个信号名称其实都是一个宏,表示其编号。

其中Ctrl+C按键会发送一个信号给进程,该信号被操作系统解释为2号SIGINT信号。我们自定义一个返回值类型为void,函数参数只有一个整型变量的函数,然后使用signal函数捕捉2号信号。捕捉到2号信号,进程会按照Handler方法打印一条语句。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void Handler(int signo)
{
    std::cout << "Received signal, signal number: " << signo << std::endl;
}

int main() 
{    
    signal(SIGINT, Handler);
    
    while(true)
    {
        std::cout << "hello, world!" << std::endl;
        sleep(1);
    }
    return 0;
}

启动该程序,按Ctrl+C按键,执行Handler函数,打印一条语句。这就证明了Ctrl+C按键,实质上是2号信号。

通过man 7 signal指令,可以查询到普通信号的默认处理动作,其中Term和Core都是终止进程。

2.3 发送信号原理

当按下键盘中Ctrl+C按键,操作系统会识别该操作,发送2号信号给进程。使用kill -l指令,会显示所有信号,你会发现信号编号是从1开始的。

因为进程PCB中有许多字段,其中使用位图上的比特位0和1两个状态,来表示该位数信号是否有接受到。还有个字段是sighandler_t类型的数组,sighandler_t是函数指针类型,也就是说该字段是函数指针数组。

当操作系统发送2号信号给进程时,其实向进程PCB位图中的第二位比特位写入1,表示该进程接受2号信号,并通过函数指针数组中下标为2的函数指针,执行该函数。如果使用signal函数,则会将该数组对应下标的内容修改为Handler函数的地址。

2.4 硬件中断

当用户按下Ctrl+C组合键时,键盘硬件检测到这一动作,并将按键编码转换为相应的信号。此时,键盘准备就绪,将数据通过中断请求线(IRQ)发送给CPU的特定引脚,以此发起一个中断请求。这个中断信号即刻通知操作系统,有来自键盘的外部事件需要被关注和处理。

操作系统接收到中断信号后,随即响应,它会暂停当前正在执行的任务,并将控制权转交给专门处理键盘输入的中断服务例程(ISR)。该例程负责将键盘缓冲区中的数据读取出来,并将其安全地拷贝到系统的内存中,以便CPU可以进一步处理这些输入。

所以,操作系统不需要对外部设备进行轮询检测,当执行完当前指令,只要等外部设备发送中断请求。操作系统如公司老板一般,指挥手底下的员工干活,自己只需要等待员工汇报工作进展,从而达到外设与操作系统并行。

2.5 指令和函数接口

2.5.1 kill指令

kill指令可以向目标进程发送指定信号。我们写一份代码,使用signal函数捕捉1到31号信号,然后打个死循环,使用kill指令发送信号。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

void Handler(int signo)
{
    std::cout << "Received signal, signal number: " << signo << std::endl;
}

int main() 
{    
    for(int signo = 1; signo <= 31; signo++)
    {
        signal(signo, Handler);
    }

    while(true)
    {
        sleep(1);
    }
    return 0;
}

我们使用kill指令发送1号、2号和6号信号,都被sig进程处理为打印一条语句。有人会说把所有信号捕捉,采用自己自定义方法,那进程岂不是无法终止。其实操作系统设计者也想到了这个问题,所以有些信号是无法被捕捉的,只会使用默认方式处理,如9号信号,可以终止所有你可以终止的进程。

2.5.2 kill函数

kill也是系统调用函数,向指定进程发送信号。我们可以使用这个函数写一个自己的kill指令。

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <signal.h>

void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << "signumber processid" << std::endl;
}

// ./mykill 9 12345
int main(int argc, char* argv[]) 
{    
    if(argc < 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int signumber = std::stoi(argv[1]);
    pid_t pid = std::stoi(argv[2]);

    int n = kill(pid, signumber);
    if(n < 0)
    {
        perror("kill");
        exit(2);
    }
    exit(0);
}

如下图,执行mykill进程需要输入三个参数,可以杀掉普通进程。

2.5.3 raise与abort函数

raise函数是发送信号给调用该函数的进程。写一个五秒后发送9号信号给自己的代码。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

int main(int argc, char* argv[]) 
{    
    int cnt = 0;
    while(true)
    {
        std::cout << "I am alive!" << std::endl;
        cnt++;
        if(cnt > 4)
            raise(SIGKILL);
        sleep(1);
    }

    return 0;
}

abort函数作用是通过发送SIGABRT信号给调用该函数的进程,以此终止该进程。这算作异常退出。

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void Handler(int signo)
{
    std::cout << "Received signal, signal number: " << signo << std::endl;
}

int main(int argc, char* argv[]) 
{    
    signal(SIGABRT, Handler);
    int cnt = 0;
    while(true)
    {
        std::cout << "I am alive!" << std::endl;
        cnt++;
        if(cnt > 4)
            abort();
        sleep(1);
    }
}

2.6 软件条件

软件没准备好,软件条件不具备引起信号的产生。常见的是管道的读端关闭,而某个进程向管道写入,操作系统会发送SIGPIPE信号给该进程,并终止进程。

alarm函数用来设置发送信号的闹钟,该信号是SIGALRM信号。

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main() 
{    
    alarm(1); //1s后,会收到SIGALRM信号
    int number = 0;
    while(true)
    {
        printf("count: %d\n", number++);
    }

    return 0;
}

上面的代码实现了测试服务器1s能完成多少次IO的功能,差不多稳定在七万次左右。但是对于CPU每秒几亿执行次数来说,已经很慢了。

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>

int number = 0;

void Die(int signumber)
{
    printf("get a signal %d, count: %d\n", signumber, number);
    exit(0);
}

int main(int argc, char* argv[]) 
{    
    alarm(1);
    signal(SIGALRM, Die);

    while(true)
    {
        number++;
    }
}

上面的代码,没有进行调用printf进行IO。运行结果如下,结果接近6亿次,相比于上面的结果,快了将近一万倍。说明IO操作十分耗时。

2.7 异常错误产生信号

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

int main()
{   
    int *p = nullptr;
    *p = 10; 

    while(true);
}

上面是野指针问题,程序启动后会,操作系统会发送11号SIGSEGV信号给进程,终止该进程。

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

int main()
{   
    int a = 10;
    a /= 0;

    while(true);
}

上面代码是除零错误,会导致栈溢出,操作系统会发送8号SIGFPE信号给进程。

当CPU执行该进程"a /= 0"代码时,实际上被解释为"a = a / 0",CPU会用三个寄存器存储三个值。

  • eax存储一开始a变量的值,ebx存储0,再通过计算把结果存储到ecx寄存器中。CPU还有一个状态寄存器Eflags,其中有个溢出标记位,专门记录结果是否正常。
  • a变量除0后,结果会溢出,Eflags中的标记位会写入为1。
  • CPU因溢出标记位为1,得知计算结果有问题,会触发硬件中断。操作系统就会知道CPU内部出错,会向执行此代码的进程发送终止信号。
  • 即使使用signal函数捕捉SIGSEGV信号,不终止该进程。当进程被CPU执行时,该进程的上下文数据重新加载到CPU时,Eflags上的溢出标记位还是1,照样会引起硬件中断,操作系统就会不断发送信号来终止进程。

创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!

相关推荐
李狗蛋儿啊4 小时前
zero自动化框架搭建---Git安装详解
运维·git·自动化
小金的学习笔记5 小时前
如何在本地和服务器新建mysql用户和密码
运维·服务器·mysql
s_fox_5 小时前
nginx ngx_http_module(7) 指令详解
运维·nginx·http
EasyNVR5 小时前
EasyRTC智能硬件:实时畅联、沉浸互动、消音护航
运维·服务器·网络·安全·音视频·webrtc·p2p
CarryBest5 小时前
Jenkins 环境搭建---基于 Docker
运维·jenkins
若云止水6 小时前
Ubuntu 下 nginx-1.24.0 源码分析 - ngx_process_options
运维·nginx
s_fox_6 小时前
nginx ngx_http_module(9) 指令详解
运维·nginx·http
风口上的猪20156 小时前
thingboard告警信息格式美化
java·服务器·前端
xing.yu.CTF8 小时前
Web入侵实战分析-常见web攻击类应急处置实验2
运维·服务器·windows·web安全·apache·php漏洞·phpstudy后门漏洞
小池先生8 小时前
阿里云子账号管理ECS权限配置全指南
服务器·阿里云·云计算