Linux 信号

文章目录

一、基本概念

1、信号和信号量

就是类似有老婆和老婆饼之间的关系。— 毫无关联

2、基本结论

image-20240816164452654

[!IMPORTANT]

  • 信号可以随时产生。
  • 进程能认识信号。
  • 进程知道接收某个信号,怎么处理。
  • 进程可能对信号进行延缓处理。(1、进程需要记录信号。2、进程需要在合适的时间进行处理信号)
  • 信号:Linux系统中提供的一种以指定进程发送特定时间的方式。进程收到信号会做识别和处理
  • 信号的产生是异步的

1~31号:普通信号。

34~64号:实时信号。

二、信号的产生

1、产生方式

[!IMPORTANT]

  1. 通过kill指令,向进程发送信号。— 指令
  2. 键盘可以给进程发送信号。— 外设
  3. 系统调用发送信号。—系统调用
  4. 软件条件。—特殊规则
  5. 异常。—异常

真正发送信号的是os。

2、系统调用

给指定进程发送指定信号

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

给当前进程发送指定信号

#include <signal.h>
int raise(int sig);

给当前进程发送终止信号

[!IMPORTANT]

注意点:

  • abort不是系统调用,而是库函数。
  • 终止进程,如果要是自定义捕捉信号的话,仍然会终止进程。给进程发送一个SIGABRD信号。
#include <stdlib.h>
void abort(void);

3、软件条件

管道

读关闭,写一直在进行 —> 操作系统会发送13号信号SIGPIPE给写进程。

闹钟

设置一个闹钟,当seconds时间后,操作系统会发送一个14号SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

// alarm(0): 取消闹钟。
// 闹钟只触发一下。
// 返回值是上一个闹钟的剩余时间。
闹钟是一个结构体对象。
struct alarm
{
    time_t expired;
    pid_t pid;
    func_t f;
}

4、异常

程序为什么会崩溃?

因为程序进行了非法访问等操作,导致os给进程发送信号,进程接收信号直接终止了程序。

程序崩溃了为什么会退出?

因为进程中,对该信号的默认处理方式就是退出进程。

程序崩溃了可以不退出吗?

可以不退出,前提是需要自己进行自定义捕捉该信号。

为什么自定义捕捉了,死循环?

进程不退出,进程要切换,所以要把cpu寄存器里面的上下文数据要被保存,过了一段时间后,该进程的上下文要恢复到cpu中,然后又报错,陷入死循环。

// 下述的例子中,描述了问题
#include <iostream>
#include <unistd.h>
#include <signal.h>
void excute()
{
    int *a = nullptr;
    *a = 10;
}
void func(int sig)
{
    std::cout << "捕捉成功" << sig << "号信号" << std::endl;
    sleep(1);
}
int main()
{
    signal(SIGSEGV, func);
    excute();
    return 0;
}

为什么推荐终止进程?

释放进程的上下文数据,包括溢出标志数据或者其他异常数据。

cpu如何得知运算时正常的还是异常的?

cpu中eflag有个溢出标记位。

硬件上下文的保护和恢复

寄存器只有一套,但是寄存器里面的数据属于每一个进程。

三、信号的保存

1、概念

[!IMPORTANT]

  • 信号递达:实际执行信号的处理动作。(1、默认,2、忽略,3、自定义捕捉)
  • 信号未决:信号从产生到递达之间的状态。
  • 信号阻塞:阻塞某一个信号。如果该信号产生了,将会一直处于未决状态(永不递达),直到解除阻塞。

一个信号阻塞和它有没有未决有关系吗?

无关。一个信号处于未决状态不能保证这个信号正在处于阻塞状态,也有可能是刚刚解除阻塞,但是还没递达。

阻塞 Vs 忽略

[!IMPORTANT]

阻塞:信号被阻塞就不会被递达。

忽略:信号递达之后的一种处理行为。

2、信号的管理

概念图

信号的管理就是通过这三张表来进行管理的。

image-20240816192433852

系统调用

sigprocmask
#include <signal.h>  
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

// sigset_t是信号集,位图,标识有效、无效两种状态。
// block、pending都是sigset_t类型。
// sigset_t是Linux给用户提供的一个用户级类型。

image-20240816195028659

sigpending
#include <signal.h>
int sigpending(sigset_t *set);

// 获取未决信号集
其余操作

image-20240816202652142

解除屏蔽

[!IMPORTANT]

  • 解除屏蔽,如果是pending(未决),一般会立即处理当前被解除的信号。
解除屏蔽,pending位图对应的信号也要被清零,在递达之前清零还是递达之后?

答:在递达之前进行清零。

3、如何理解信号的发送和保存?(浅)

进程在内核中有task_struct,在里面有很多成员变量,并且用位图来保存收到的信号!

发送1号信号,将第一个比特位由0置1。(由右到左,第1个比特位~第32位比特位)

发送信号:修改进程pcb中的信号的指定位图,0->1,写信号。

四、信号的处理

1、三种方式

[!IMPORTANT]

  1. 默认动作。进程处理信号一般都是默认动作 — 终止进程、暂停进程、忽略进程。
  2. 忽略动作。
  3. 自定义捕捉。
signal(signum, handler);// 自定义捕捉信号
signal(signum, SIG_IGN);// 忽略一个信号
signal(signum, SIG_DFL);// 信号的默认处理动作

下图的action就是每个信号的默认动作。Core、Term都是进行终止进程。

image-20240816204031209

2、默认的处理方式

Core、Term

[!IMPORTANT]

  • Term:异常终止。
  • Core:异常终止,但是会形成一个debug文件。为什么平常的时候没有,因为默认是关闭的。debug文件存储的是进程退出时候的镜像数据(核心转储)
// core--是协助debug的文件。
// 查看core
ulimit -a
// 打开core,使云服务器在程序崩溃时自动生成debug文件
ulimit -c
// 关闭core
ulimit -c 0
// 用gdb来对core生成的debug文件进行解析,得到错误位置
core-file core

云服务器为什么为关闭Core形成debug文件的功能呢?

因为每次异常的时候,都会生成一个debug文件,假如遇到一个问题,电脑一直重启失败,磁盘中就会有大量的磁盘文件。这样就不仅仅会出现重启错误,还会使磁盘爆满。

Core dump标记位

image-20240816221402399

[!IMPORTANT]

  • 当形成Core dump文件(也就是core形成的debug文件)时,为1;没有生成时,为0。
  • Term和这个标记位没有任何的联系,无论是不是Term处理方式,Core dump标记位始终为0。

3、自定义捕捉信号

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

// handler的参数就是signum。
// 含义是捕捉了signum信号,然后执行handler方法。
---------------------------------------------------------------------------------------------------
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act是一个输入型参数,oldact是一个输出型参数
// oldact输出的是之前的信号的sigaction --- 对其进行保存
struct sigaction {
	void       (*sa_handler)(int);
 	void       (*sa_sigaction)(int, siginfo_t *, void *);// 不用管
	sigset_t   sa_mask;
	int        sa_flags;
	void       (*sa_restorer)(void);// 不用管
};
// sa_handler:就是函数指针,对捕捉的函数进行处理。
// sa_mask:屏蔽的信号集。9号信号,禁止屏蔽
// sa_flags:一个标志字段,用于修改 sigaction 的行为。
// 1、SA_NODEFER:防止自动阻塞当前信号(即,即使设置了 sa_mask,当前信号也不会被阻塞)。
// 2、SA_RESETHAND:在信号处理函数返回后,将信号的处理方式重置为默认处理(即,sa_handler 被设置为 SIG_DFL)。
// 3、SA_RESTART:如果信号处理函数被中断的系统调用是自动重启的(如 read、write 等),则尝试重启该调用。
// 4、SA_SIGINFO:使用 sa_sigaction 而不是 sa_handler 作为信号处理函数。

如果我们对2号信号进行捕捉:
当前如果正在对2号信号进行处理,默认2号信号会被自动屏蔽。
对2号信号完成的时候,会自动解除对2号信号的屏蔽。

[!IMPORTANT]

  • 9号信号不允许自定义捕捉。
  • 自定义捕捉信号函数,只要捕捉一次,后续一直有效。
  • 一直不产生signum,handler一直不执行。
  • 可以对更多信号进行捕捉。
  • 2号信号SIGINT:是ctrl + c,给目标进程发送二号进程,默认处理方式是终止进程。

4、信号处理的时间

信号不会被立刻处理,而是在合适的时间处理。

合适的时间是:进程从内核态返回到用户态的时候处理。

image-20240816214039388

5、为什么进程要在内核和用户之间来回进行切换?

如果用户级函数在内核中进行处理的话,用户可能会借着内核执行越权的事情。所以要在内核和用户之间进行切换,防止用户越权操作。

6、信号的捕捉流程

image-20240816214427436

五、内核态 Vs 用户态

1、地址空间

  • 对于32位的机器而言,有2^32字节 = 4GB的地址空间(虚拟地址)。[0, 3]GB是用户空间([0, 3]是区间),(3, 4]GB是内核空间。
  • 不管有多少个进程,内核级页表总是只有一份。(内核级页表是内核的虚拟地址和物理内存之间的映射关系)
  • 我们访问os,其实还是在我们的地址空间中进行的,和我们访问库函数没有区别!
  • os不相信任何用户,用户访问(3, 4]地址空间的时候,要受到一定的约束!不能直接访问操作系统的数据,只能通过系统调用。

2、键盘输入的过程

image-20240816222323544

[!IMPORTANT]

上述的过程感觉和信号很相似!

我们学习的信号就是模拟中断实现的。

信号:纯软件。

中断:软件+硬件。

外部中断的目的?

让cpu内部寄存器形成一个中断号的数字。

3、如何理解系统调用?

在系统中,会有一张系统调用的表(函数指针类型)。 — sys_call_tablep[]。

执行系统调用:1.系统调用号(下标),2.系统调用函数指针数组

系统调用是cpu不产生中断号,而是在一个寄存器内直接生成一个地址(比如是0x80),进行中断并取出系统调用号,然后去中断向量表中寻找0x80,通过系统调用号,去执行系统调用。

4、os是如何运行的?

操作系统的本质就是一个死循环 + 时钟中断不断调度系统任务。

image-20240816222648440

os不是不相信任何用户吗,用户如何直接跳转到(3,4]GB空间?

必须在特定条件下,才能跳转过去。

在cpu中有一个ecs的寄存器 — 代码区的范围。其中有两个比特位,如果是0则是cpu可以执行内核级代码,如果是3则是cpu可以执行用户级代码。所以,当这两个比特位由3–>0,cpu才允许访问外部((3, 4]GB)。

六、可重入函数

对于可重入、不可重入描述是函数的特征,而不是优缺点。

就比如说下图:当主函数正在执行对链表的插入时,进程接收到信号,启动对应的信号处理函数。假如该信号处理函数里面也调用了插入函数,完成了链表的插入。当返回原函数的时候,继续向下执行,这样会导致内存泄漏问题。对于类似于这种函数,叫做不可重入函数。

可重入函数是一种能够在多个任务或线程中安全调用的函数。在并发编程中,尤其是在多线程环境下,可重入性是一个重要的概念。如果一个函数在多线程环境中被同时调用时,能够正确地处理多个线程的输入,而不会导致数据竞争、死锁或其他并发问题,那么这个函数就是可重入的。

我们学过的大部分函数都是不可重入函数

image-20240816230216001

七、volatile关键字

保持内存的可见性。

// 编译器是有优化级别的
// 对于while(!gflag);
// 在底层,没有优化的话,运行逻辑应该是:cpu从内存中加载数据到cpu中,cpu进行逻辑运算,然后再根据运算结果,继续往下执行。
// 但是,如果编译器达到一定的优化程时,编译器发现,main函数中,没有修改gflag的代码,cpu就只会从内存中读取一遍,然后一直判断,这样的话,就算你接收到信号了,也不会退出循环。
// 不信的话,可以试试:g++ -o main.cc -O1

// 为了解决这样的问题,volatile int gflag = 0; 这样的话,无论优化级别是多少,cpu都会从内存中读取数据,不会只进行读取一次。
#include <iostream>
#include <unistd.h>
#include <signal.h>
int gflag = 0;
void changedata(int sig)
{
    std::cout << "get a signo" << sig << ", change gflag 0->1"<<std::endl;
    gflag = 1;
}
int main() // main 函数中没有对gflag进行修改
{
    signal(2, changedata);

    while (!gflag);

    std::cout << "process quit normal" << std::endl;
    return 0;
}

八、SIGCHLD信号

子进程退出时,会给父进程发送信号 — SIGCHLD信号。

1、父进程不用刻意的等待子进程,可以去执行别的任务。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
void func(int sig)
{
    pid_t rid = waitpid(-1, nullptr, 0);
    if (rid > 0)
    {
        std::cout << "接收子进程成功" << std::endl;
    }
}
void DoOtherThing()
{
    std::cout << "父进程执行其他任务" << std::endl;
}
int main()
{
    signal(SIGCHLD, func);

    pid_t id = fork();

    if (id == 0)
    {
        // child
        sleep(3);
        std::cout << "child excute" << std::endl;
        exit(1);
    }
    // father
    while (true)
    {
        DoOtherThing();
        sleep(1);
    }
    return 0;
}

2、上述代码有问题

问题一:如果不止一个子进程的话,就会有问题

// 只需要循环等待即可
void func(int sig)
{
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, 0);
    	if (rid > 0)
    	{
        	std::cout << "接收子进程成功" << std::endl;
    	}
        else if (rid < 0)
        {
            std::cout << "处理完毕" << std::endl;
            break;
        }
    }
}

问题二:如果有多个子进程,部分退出,部分不退出呢?

// waitpid使用非阻塞等待
void func(int sig)
{
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG);
    	if (rid > 0)
    	{
        	std::cout << "接收子进程成功" << std::endl;
    	}
        else if (rid < 0)
        {
            std::cout << "处理完毕" << std::endl;
            break;
        }
        else // 部分退出的已经退出完了,也可以退出了
        {
            //...
            break
        }
    }
}

“处理完毕” << std::endl;
break;
}
}
}


### 问题二:如果有多个子进程,部分退出,部分不退出呢?

```c++
// waitpid使用非阻塞等待
void func(int sig)
{
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG);
    	if (rid > 0)
    	{
        	std::cout << "接收子进程成功" << std::endl;
    	}
        else if (rid < 0)
        {
            std::cout << "处理完毕" << std::endl;
            break;
        }
        else // 部分退出的已经退出完了,也可以退出了
        {
            //...
            break
        }
    }
}
  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仍有未知等待探索

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值