在这里插入图片描述

一、信号概念

(一)理解进程信号

  • 在Linux系统中,信号是一种用于通知进程发生特定事件的机制。信号可以由硬件、其他进程或操作系统本身发送给目标进程。每个信号都有一个唯一的编号,例如SIGKILL表示终止进程,SIGINT表示中断进程等。

  • 信号的产生流程包括信号的产生,信号的保存,信号的处理

  • 相比于进程间通信,信号更偏向于操作系统和进程间的通信。

(二)信号的产生

  • 1、键盘发送信号
    键盘可以给进程发送信号给进程,但是只能发送给前台进程。
  • 2、命令行发送进程
    使用指令kill -信号编码 进程pid可以发送特定的信号给目标进程。
  • 3、系统调用发送型号
    使用下面两个系统调用可以给目标进程发信号,abort函数会将捕捉方式恢复到默认,直接退出进程。
    在这里插入图片描述
    在这里插入图片描述
  • 4、硬件异常发送信号
    程序如果出现除0或者段错误,操作系统会向进程发送8号/11号信号。

cpu包含状态寄存器(current->task_struct),标志寄存器:EFLAGS 位图,其中一个比特位判断当前运送是否溢出。操作系统是软硬件资源的管理者,通过检查硬件CPU就能判断是否溢出。

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

void hander(int sig)
{
    std::cout << "我是" << sig << "号信号!" << std::endl;
}

int main()
{
    for(int i = 1; i < 32; i++)
    {
        signal(i, hander);
    }
    raise(6);
    abort();
    return 0;
}

在这里插入图片描述

  • 5、软件条件产生信号
    在这里插入图片描述
#include <iostream>
#include <signal.h>


void handerSig(int sig)
{
    std::cout << "获得了一个信号:" << sig << std::endl;
    int n = alarm(1);
}



int main()
{
    signal(SIGALRM, handerSig);
    alarm(1);
    while(true)
    {
        pause();
        std::cout << "完成一次操作" << std::endl;
    }
}

(三)信号的保存

1、信号三张表

信号产生后,并不是立刻处理,进程必须先把信号记录下来。操作系统内部PCB中有一个位图sigs,修改位图就能判断是否有对应位置的对应信号产生,这个修改工作得操作系统自己完成。

  • 信号递达:实际信号的处理过程
  • 信号未决:信号产生到信号递达的过程。进程可以选择阻塞某个信号,让信号始终保持到未决状态。
struct tast_struct
{
	unsigned int block;
	unsigned int pending;
	unsigned int hander;
}

其中block就是屏蔽的信号,能够抵达的信号就是通过运算pending & (~block),结果为一就能递达。
在进行信号递达之前,首先清空pending信号中的信号对应的位图。
在这里插入图片描述

2、信号集

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 修改block表
    调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1 

在这里插入图片描述

  • 查看pending表
    函数中使用的是输出型参数。
#include <signal.h>
int sigpending(sigset_t *set)
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

3、综合实践

通过下面的实践,我们可以阻塞信号从而让信号不能被递达,那如果我们把信号全部中断了,如果这个进程是个病毒,岂不是无法无天了吗,实际上9号信号不能被捕捉,不能被屏蔽。

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

void PrintPending(sigset_t &pending)
{
    std::cout << "我是一个进程,我的pid是" << getpid() << std::endl;

    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block, SIGINT);
    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)n;
	int cnt = 0;
    while (true)
    {
        // 2、获取pending信号集合
        sigset_t pending;
        int m = sigpending(&pending);

        // 3、打印
        PrintPending(pending);
		if(cnt == 10)
		{
			sigprocmask(SIG_SETMASK, &oblock, nullptr);
			std::cout << "解除对2号的屏蔽" << std::endl;
		}
        sleep(1);
        cnt++;
    }

    return 0;
}

二、信号的处理

(一)信号的处理方式

1、自定义捕捉

在对信号进行处理时,系统中有一个函数指针数组,其中数组下标对应的就是信号的值,因此我们可以传入不同的函数来改变对应信号的处理动作。
在这里插入图片描述

void handerSig(int sig)
{
    std::cout << "获得了一个信号:" << sig << std:: endl;
    // exit(13);
}

int main()
{
    for(int i = 1; i < 32; i++)
    {
        signal(i, handerSig);
    }
    for(int i = 1; i < 32; i++)
    {
        sleep(1);
        if(i == 9 || i == 19)
            continue;
            raise(i);
    }
    return 0;
}

2、系统默认

signal(i, SIGDFL);

3、忽略

signal(i, SIGIGN);

(二)信号处理动作、前台进程、后台进程

1、信号处理方式

在这里插入图片描述
使用man 7 signal可以查看系统中存在的信号以及对应的行为,其中core终止进程,Term终止进程,Stop暂停进程,Cont继续进程。

  • core终止进程:当一个进程接收到SIGABRT信号时(通常由程序员使用abort()函数发送),进程会立即被终止,并在当前工作目录下生成一个core文件,其中包含了进程终止时的内存映像信息,用于诊断程序错误。

云服务器上,core dump功能是被禁止掉的,因为core文件通常内存比较大,当一个公司程序挂掉时,就会进行重启,最后导致了大量内存垃圾。
使用指令ulimit -a可以查看当前是否打开`core``功能。在这里插入图片描述

通过上图的core file size的大小可以看出是关闭了,此时使用ulimit -c 40960可以开启这项功能。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(2);
        int a = 10;
        a /= 0;

        exit(1);
    }
    int status = 0;
    waitpid(id, &status, 0);
    printf("signal: %d, exit code: %d, core dump: %d\n",
           (status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}

在这里插入图片描述

  • Term终止进程:进程接收
    SIGTERM信号时,表示请求该进程正常终止。进程会收到这个信号后,应该以最合适的方式来处理善后工作并退出。
  • Stop暂停进程:当一个进程接收到SIGSTOP信号时,进程会暂停执行,直到收到SIGCONT信号时才会继续执行。
  • Cont继续进程:当一个进程处于暂停状态(通过SIGSTOP信号暂停)时,可以通过SIGCONT信号来继续执行该进程。

2、前台进程后台进程的转化

前台进程:./XXX执行的程序可以从标准输入中获取内容,只能有一个
后台进程:./XXX &执行的程序不能从键盘获取内容,父进程在退出后,如果子进程没退出,那么子进程会被一号进程领养,同时提到后台运行,`ctrl + c’杀不掉了。

目标进程:目标进程就是前台进程。

  • 后台进程转化成前台
    在这里插入图片描述
    输入指令jobs可以查看当前的后台进程。
    在这里插入图片描述
    输入指令 fg 进程编号可以将后台进程转化成前台进程,在使用ctrl + c就能发送 2 号信号杀死进程。由于混合输出原因,所以会出现
    在这里插入图片描述
  • 前台进程转化成后台
    前台进程在运行后,使用ctrl + z可以将进程暂停并且放到后台,使用bg + 进程编号使得进程到后台恢复运行。

三、信号处理时间

当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的main函数之前检查是否有信号递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
在这里插入图片描述

四、中断(操作系统运行)

(一)硬件中断

操作系统本质上就是一个基于中断进行工作的软件。
操作系统就是一个死循环,系统中会设置时钟中断,当触发始终中断时,操作系统就会去执行中断向量表中的方法进而完成相关的方法,进行进程调度等。

在这里插入图片描述

(二)陷阱和异常

  • 陷阱

陷阱通常与系统调用、硬件故障或程序错误相关。当发生这些事件时,处理器会中断当前执行的程序,跳转到预定义的陷阱处理程序,处理完后再返回原程序继续执行。
硬件故障:如除零错误、内存访问越界等。
调试断点:用于调试程序时暂停执行。

  • 异常

异常是编程语言提供的一种机制,用于处理程序运行时的错误或特殊情况。异常通常由程序代码显式抛出,并由异常处理机制捕获和处理。异常处理机制允许程序在发生错误时进行优雅的恢复或报告错误信息。

(三)软件中断

软件中断是通过特定的指令(如int 0x80或syscall)触发的,这些指令会将控制权从用户空间转移到内核空间。内核会根据中断号执行相应的中断处理程序。
我们在调用统调用时,首先这个系统调用会封装一个数组下标,其次再跳转到下标对应的函数去执行相关的方法。
在这里插入图片描述
在这里插入图片描述

五、扩展

(一)用户态,内核态

虚拟地址空间大概是4G的空间,其中用户态是我们以用户的身份,只能访问0~3G的地址空间,而内核态是我们以内核的身份访问3-4G的地址空间。

CS(Code Segment)段寄存器是CPU中的一个重要寄存器,用于存储当前正在执行的代码段的段选择子。它指向内存中代码段的起始地址,帮助CPU定位和执行指令。
CS段寄存器的权限位
CS段寄存器的最后两位(即最低两位)被称为RPL(Requested Privilege Level),用于指示当前代码的执行权限级别。RPL的值决定了当前代码是运行在内核态还是用户态。

RPL = 00:表示内核态(Ring 0),CPU具有最高权限,可以执行所有指令和访问所有资源。
RPL = 11:表示用户态(Ring 3),CPU权限受限,只能执行部分指令和访问部分资源。

(二)可重入函数

可重入函数是指在多线程或中断环境下,能够被多个执行流同时调用而不会产生错误或数据不一致的函数。这类函数通常不依赖于全局变量或静态变量,也不调用不可重入的函数。

  • 可重入函数通常具有以下特性:
    不使用全局变量或静态变量。
    不调用不可重入的函数。
    不依赖于外部状态,所有数据都通过参数传递。
    在函数内部使用局部变量,确保每个调用都有自己的独立数据空间。

(三)volatiile

代码在进行到while循环判断时,通常情况下,CPU都会将物理内存中的flag传到CPU中,再进行逻辑运算。但是有些编译器会发现while 循环中不存在flag就会产生优化,不是每次执行都去物理内存中取flag,这样在程序发送二号信号就会产生bug,解决方法就是给flag加上volatile关闭编译器的优化。

#include <signal>
#include <iostream>

//int flag = 0;
volatile int flag = 0;

void handler(int signu)
{
    std::cout << "更改全局变量, " << flag << "-> 1" << std::endl;
    flag = 1;
}

int main()
{
    signal(2, handler);

    while(!flag);
    std::cout << "process quit normal!" << std::endl;
    return 0;
}

(四)优雅的回收子进程SIGCHLD

子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理函数是SIG_DFL,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

版本一

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

void WaitAll(int num)
{
    while (true)
    {
        pid_t n = waitpid(-1, nullptr, WNOHANG); // 阻塞了!
        if (n == 0)
        {
            break;
        }
        else if (n < 0)
        {
            std::cout << "waitpid error " << std::endl;
            break;
        }
        else if(n > 0)
        {
            std::cout << "等待到了进程: " << n << std::endl;
        }
    }
    std::cout << "father get a signal: " << num << std::endl;
}

int main()
{
    // 父进程
    signal(SIGCHLD, WaitAll); // 父进程
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork(); // 如果我们有10个子进程呢??6退出了,4个没退
        if (id == 0)
        {
            sleep(3);
            std::cout << "I am child, exit" << std::endl;
            exit(3);
            // if(i <= 6) exit(3);
            // else pause();
        }
    }

    while (true)
    {
        std::cout << "I am fater, exit" << std::endl;
        sleep(1);
    }

    return 0;
}

版本二

下面的方式更为优雅,直接将回收方式设置为忽略函数

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>

int main()
{
    // 父进程
    signal(SIGCHLD, SIGIGN); // 父进程
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork(); // 如果我们有10个子进程呢??6退出了,4个没退
        if (id == 0)
        {
            sleep(3);
            std::cout << "I am child, exit" << std::endl;
            exit(3);
            // if(i <= 6) exit(3);
            // else pause();
        }
    }

    while (true)
    {
        std::cout << "I am fater, exit" << std::endl;
        sleep(1);
    }

    return 0;
}
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐