Linux 进程信号

文章目录

  • 信号的概览
  • 信号的产生
  • 信号的处理
  • 信号集操作
  • 信号的捕捉
  • 补充与说明

信号的概览

信号由软件或硬件产生发送给进程,进程对其做相应处理。信号是进程之间事件异步通知的一种方式,属于软中断

Linux下的全部信号由指令kill -l查询
Linux 进程信号_第1张图片
Linux 下指令的说明使用指令 man 7 signal 查询
Linux 进程信号_第2张图片

信号的产生

  1. 中断组合按键产生信号:如果前台有进程正在运行用可通过Ctrl + CCtrl + \ 的按键组合来向再在运行的前台进程发送信号,达到终止这个前台进程的目的。Ctrl+C:对应的信号是SIGINT(中断信号),Ctrl+\:对应的信号是SIGQUIT(退出信号),两者的区别是 SIGQUIT 信号会产生用于调试的核心转储文件(Core Dump),用于调试错误。
    Linux 进程信号_第3张图片

  2. 进程间通信:在中端使用kill指令,可发送信号给指定进程,kill指令的使用方法为:
    kill -s <信号编号或名称> <进程ID>。 另外父子进程的状态通知等情境也会有进程间相互发送信号的情况,在补充说明部分会提到。

  3. 软件条件:在编程中,可以使用特定的函数(如raise())来主动产生信号。说的更简单点,在进程中的代码中,用户可通过使用int kill(pid_t pid, int signo); int raise(int signo); void abort(void);等函数生成信号,这些函数的具体使用在补充说明部分。

  4. 硬件异常:硬件异常,如除零错误、非法内存访问等,会导致操作系统产生相应的信号。
    硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

信号的处理

信号处理的方式:

  1. 执行信号默认的方案
  2. 忽略该信号,即接收到了一个信号但不采取任何行动。需要注意的是有些信号无法被忽略,因为如果可以全部忽略,那将可能会产生一个无法杀死进程。
  3. 捕获,捕获指定是更改信号对应的默认方案。

信号的状态:

  1. 递达: 实际执行信号的处理动作称为信号递达(Delivery) ,可以将递达理解为处理信号中
  2. 未决 : 信号从产生到递达之间的状态,称为信号未决(Pending)。
  3. 阻塞: 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作. 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号产生以后交付的对象是进程,最终执行信号对应方案的是OS(因为OS管理进程)。信号产生不是立即处理的,处理的方式也是要记录的。所以要有一个记录已产生的信号以及记录信号处理方式的地方,如下图所示,这个地方就在tast_struct中:Linux 进程信号_第4张图片
上图中的block、pendin结构,被称为信号集( sigset_t)(类似于位图),图中block就是block信号集,pending就是pending信号集 。所处位置的下标代表是几号信号。 block代表信号是否被设置为阻塞,pending带表信号是否处于未决状态。handler是存储的是指向的是处理对应信号的函数方法,这个方法可以用户自定义。

注意: 不会出现同一个信号block位为0,pending为1的情况。因为peding代表有信号在等待处理,等待的原因是因为被设置为阻塞(block被置为1)。 之所以这样设计,是因为block为在后续有可能因为某些原因被重新设置为0,即取消阻塞。这时候信号就可以到递达状态了。

信号集操作

#include 
int sigemptyset(sigset_t *set); // 初始化信号集中所有标志位为0
int sigfillset(sigset_t *set);  // 初始化信号集中所有标志位为1
int sigaddset (sigset_t *set, int signo);  //向信号集中添加有效信号
int sigdelset(sigset_t *set, int signo);   //从信号集中删除有效信号
int sigismember(const sigset_t *set, int signo);  
//判断一个信号集中signo信号标志位是否为1 。
  1. 使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

sigprocmask

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

功能:设置阻塞信号集
参数:

  1. how 参数
    Linux 进程信号_第5张图片
  2. set 输入参数,要设置的阻塞信号集。
  3. oset 输出参数 接收函数设置前的阻塞信号集。

sigpending

#include 
int sigpending(sigset_t *set);

功能:获取未决定信号集。
参数:

  1. set 输出参数,接收未决信号集。

上述函数使用范例:
范例一: 函数的使用

#include 
#include 

int main() {
    sigset_t set;
    sigemptyset(&set); // 清空信号集

    // 添加要阻塞的信号
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 设置信号屏蔽字
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask");
        return 1;
    }

    printf("信号屏蔽字设置成功,按下Ctrl+C或Ctrl+\\将不会终止程序。\n");

    // 模拟程序运行
    int i;
    for (i = 0; i < 10; i++) {
        printf("程序正在运行,i = %d\n", i);
        sleep(1);
    }

    return 0;
}

范例二:验证信号的未决状态

#include "stdlib.h"
#include "unistd.h"
#include "signal.h"
#include "stdio.h"

void handler(sigset_t *set)
{

    while (true)
    {
        sleep(1) ;

        sigpending(set);
        int i = 0 ;
        for(; i < 32 ; i++)
        {
            if(sigismember(set, i))
            {
                printf("1") ;
            }
            else
            {
                printf("0");
            }
        } 
        printf("\n") ; 
    }
}


int main()
{
    sigset_t test_1 , test_2 ; 
    sigemptyset(&test_1);
    sigaddset(&test_1, SIGINT); 
    sigprocmask(SIG_BLOCK, &test_1, nullptr);

    handler(&test_2) ; 

    return 0 ; 
}

Linux 进程信号_第6张图片

信号的捕捉

Linux 进程信号_第7张图片

  1. 内核和用户态:
    根据对进程地址空间的学习,我们知道操作系统内核的代码在内存地址空间的前段存储,实际上由于虚拟内存机制,所有进程的都使用同一块物理内存上的内核代码,进程地址空间的内核代码地址通过专门的内核级页表找到物理内存上的内核代码。 当用户进行系统调用或者程序出现异常时,程序执行进入内核态,这里所说的进入内核态指的是执行流在出现以上情况时在进程地址空间中从用户空间进入到内核空间的中执行代码。执行流在内核态执行完需要执行的代码后需要返回用户态,即执行流从内核空间到用户空间,这一过程会进行信号的检测和处理。 实际上cpu通常具右两套寄存器,分别负责用户空间代码和内核空间代码的运行,在补充说明部分有更详细的解释。

  2. 信号从产生到捕捉的流程图:
    解释:
    ①如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了
    ② 对图中修改sigset_t的解释:
    当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(将block信号集对应的信号置为1),当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 这样可以防止如果捕捉信号后处理时,自定义的信号处理函数还未执行完,再次捕捉到信号时发生时可以记录这些来不及处理的信号。

Linux 进程信号_第8张图片
信号捕捉到处理完毕,内核态和用户态的切换情况:
Linux 进程信号_第9张图片

补充与说明

子进程父进程信号通信

子进程终止:当子进程终止时,它会向父进程发送一个SIGCHLD信号,父进程可以通过捕获该信号来处理子进程的终止状态。事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。

中断概念

中断的相关概念 (了解即可) 中断的分类: 硬件中断(外部中断,异步中断):可屏蔽中断和不可屏蔽中断。
软件中断(内部中断,同步中断):异常和系统调用。 中断又分为硬中断和软中断。
硬中断:就是由系统硬件产生的中断。系统硬件通常引起外部事件。外部事件具有随机性和突发性,因此硬中断也具有随机性和突发性。
软中断:是执行中断指令时产生的。软中断不用外设施加中断请求信号,因此中断的发生不是随机而是由程序安排好的。 中断又分为外部中断和内部中断。
外部中断一般是指计算机外设发出的中断请求。外部中断是可以通过编程方式屏蔽的。例如键盘中断,打印机中断,定时器中断等。
内部中断是指因硬件出错(突然掉电,奇偶校验错误)或者运算错误(除数为零,运算溢出,单步中断)所引起的中断。内部中断是不可屏蔽的中断。大多数内部中断都是由Linux内核进行处理的,所以驱动程序员往往不需要关系这些问题。
中断又分为同步中断和异步中断。 同步中断是指令执行的过程中有CPU控制的,CPU在执行完一条指令后才发出中断。
异步中断是由硬件设备随机产生的,产生中断时并不考虑与处理器的时钟同步问题,该类型的中断时可以随时产生的。

中断信号线(IRQ):
中断信号线是对中断输入线和中断输出线的统称。中断输入线是指接收中断信号的引脚。中断输出线是指发送中断信号的引脚。每一个能够产生中断的外设都有一条或者多条中断输出线,用来通知处理器产生中断。相应地,处理器也有一组中断输入线,用来接收连接到它的外部设备发出的中断信号。
中断控制器:
中断控制器位于ARM处理器核心和中断源之间。外部中断源将中断发到中断控制器。中断控制器根据优先级进行判断,然后通过引脚将中断请求发送给ARM处理器核心。

CPU寄存器
CPU(中央处理器)通常具有两套寄存器,分别是用户可见寄存器和控制寄存器。

用户可见寄存器:

功能:用户可见寄存器用于存储程序执行过程中的数据和运算结果。
使用:程序员可以直接访问和使用这些寄存器,将数据存储在这些寄存器中,进行运算和操作。
示例:通用寄存器(如EAX、EBX、ECX、EDX等)用于存储临时数据和运算结果;索引寄存器、指针寄存器、标志寄存器等用于支持内存寻址和程序控制。

控制寄存器:

功能:控制寄存器用于控制CPU的工作状态和行为。 使用:控制寄存器通常由操作系统或特权级别较高的程序使用,用于控制CPU的工作模式和特性。
示例:程序计数器(PC)用于存储下一条要执行的指令的地址;状态寄存器(PSW或FLAGS)用于存储程 序运行过程中的状态信息,如条件码、进位标志等;控制寄存器用于控制特权级、中断使能等。

函数使用

signal函数 signal函数用于设置信号处理函数,用于捕获和处理特定信号的发生。它的原型如下:
#include

void (*signal(int signum, void (*handler)(int)))(int);

其中,signum表示要设置处理函数的信号编号,handler表示要设置的信号处理函数。 signal函数有以下几种用法:
1.注册信号处理函数: void handler(int signum) {
// 处理信号的代码 }

signal(SIGINT, handler);

上述代码将SIGINT信号(即键盘中断信号)的处理函数设置为handler函数。当接收到SIGINT信号时,将调用handler函数进行处理。
2.恢复默认信号处理行为: signal(SIGINT, SIG_DFL);

上述代码将SIGINT信号的处理方式恢复为默认行为。即当接收到SIGINT信号时,将执行默认的中断操作。
3.忽略信号:

signal(SIGINT, SIG_IGN);

上述代码将SIGINT信号的处理方式设置为忽略。即当接收到SIGINT信号时,不做任何处理

kill函数 在C/C++编程中,kill函数用于向指定的进程发送信号。它的原型如下:
#include
#include

int kill(pid_t pid, int sig);

其中,pid表示要发送信号的进程ID,sig表示要发送的信号编号。

raise函数 在C/C++编程中,raise函数用于向当前进程自身发送信号。它的原型如下: #include

int raise(int sig);

其中,sig表示要发送的信号编号。

abort函数

abort函数用于异常终止程序的执行。它的原型如下:
#include

void abort(void);

abort函数会导致程序立即终止,并生成一个终止信号(SIGABRT)来通知操作系统。操作系统会接收到该信号后,会进行相应的处理,例如生成core文件以供调试分析。

abort函数通常用于以下情况:
1.致命错误:当程序遇到无法恢复的错误或异常情况时,可以使用 abort函数终止程序的执行。这可以帮助开发人员在发生错误时及时发现问题并进行调试。
2.断言失败:在调试过程中,可以使用断言( assert)来检查程序中的条件是否满足。当断言失败时,abort函数可以用于终止程序的执行,并在控制台上显示相关的错误信息。
需要注意的是,abort函数会直接终止程序的执行,不会执行任何清理操作,例如关闭文件、释放内存等。因此,在调用abort函数之前,应该确保已经处理了必要的清理工作。
另外,abort函数不会捕获信号,而是生成一个终止信号。如果需要在程序中捕获和处理信号,可以使用signal函数或sigaction函数来注册信号处理函数。

alarm函数 alarm函数用于设置定时器,当定时器到达指定的时间后,会触发一个SIGALRM信号。它的原型如下:
c复制代码#include

unsigned int alarm(unsigned int seconds);

其中,seconds表示要设置的定时器时间,单位为秒。alarm函数返回之前设置的定时器剩余时间。

需要注意的是,alarm函数只能设置一个全局的定时器,如果之前已经设置了定时器,调用alarm函数会取消之前的定时器并设置新的定时器。如果将seconds参数设置为0,则会取消当前的定时器。
另外,alarm函数在多线程程序中使用时需要格外小心,因为它是一个全局的定时器,可能会对其他线程产生干扰。在多线程程序中,可以考虑使用线程专用的定时器函数,如timer_create和timer_settime。

alarm 构成循环

alarm 构成循环 :

> #include <stdio.h>
> #include <unistd.h>
> #include <signal.h>
> 
> void handler(int signum) {
>     printf("Received SIGALRM signal\n");
>     alarm(3); }
> 
> int main() {
>     signal(SIGALRM, handler);
>     unsigned int remaining = alarm(3);
> 
>    while(1)
>      cout << 1 ; 
> 
>    return 0;
>    }

你可能感兴趣的:(个人学习,Linux,操作系统)