Linux进程信号

目录

信号的认识

技术应用角度的信号

信号处理函数

信号概念

信号处理

忽略此信号

执行默认处理动作

产生信号

基本操作

调用系统命令向进程发送信号

闲聊

使用函数产生信号

raise函数

abort

由软件条件产生信号

puase函数

测试

如何理解软件条件

硬件异常产生信号

[子进程退出core dump​编辑](#子进程退出core dump编辑)

[Core Dump](#Core Dump)

保存信号

信号其他相关常见概念

在内核中的表示

sigset_t

信号集操作函数

sigprocmask

sigpending

测试

捕捉信号

信号处理流程

流程

sigaction

sa_handler

sa_mask

硬件中断

时钟中断

软中断

缺页中断?内存碎片处理?除零野指针错误?

理解内核态与用户态

volatile

例子


信号的认识

举个例子,在生活中,信号可以理解为"通知",你收到了某个"通知",所以你要在合适的时候去做某件事

但是这个信号是什么时候到的你并不知道,因此信号异步

要怎么处理你也需要定好,处理信号也可以分为:默认、自定义、忽略 三种

技术应用角度的信号

复制代码
#include <iostream>
#include <unistd.h>
int main()
{
    while(true)
    {
        std::cout << "I am a process, I am waiting signal!" << std::endl;
        sleep(1);
    }
}

上面这段代码,在程序上是每隔一秒钟在控制台打印消息

但是你也可以选择按下ctrl+c将进程关闭掉,实际上就是ctrl+c这个键盘输入产生一个硬件中断被操作系统获取,并被解释为信号,发送给目标前台进程

目标前台进程收到消息之后引起进程退出

信号处理函数

复制代码
#include <signal.h>
//定义一个参数为int类型,返回值为空的函数指针类型为sighandler_t
typedef void (*sighandler_t)(int);

//我们可以将写好的的信号处理函数交给对应的信号,当收到对应的信号时就执行对应的回调方法
sighandler_t signal(int signum, sighandler_t handler);

//signum为对应的信号编号,handler为自定义的信号处理函数

同样以ctrl+c为例,ctrl+c实际上是向前台进程发送2号信号SIGINT,这里,我们可以做一个实验

复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handle(int signum)
{
    std::cout<<"我是进程:"<<getpid()<<",我收到了"<<signum<<"号信号,执行回调方法"<<std::endl;
}
int main()
{
    signal(SIGINT,handle);
    while(true)
    {
        std::cout<<"我是进程:"<<getpid()<<",我在等待"<<std::endl;
        sleep(1);
    }
    return 0;
}

此时,我们就将2号信号SIGINT(即ctrl+c发送的信号)修改为了我们的自定义方法

此时我们发现进程关不掉了,我们可以通过

kill -9 进程pid

这个命令来直接杀死掉对应的进程即可

不过要注意下,我们写的回调方法只是定义了收到某个信号时对应的处理方法,也就是说,如果我们收不到对应信号,那么我们就永远也不会执行对应的回调方法

信号概念

信号是进程之间事件异步通知的⼀种方式,属于软中断。

我们可以通过 kill -l 命令查看信号

我们也可以直接去signal.h头文件中去查看信号的宏定义

因此我们可以发现SIGINT实际上就是2

信号处理

我们上面已经写过了信号处理函数,我们再来解释下其他的信号处理

忽略此信号

我们只要设置对应信号的处理函数为SIG_IGN那么我们就可以忽略掉对应的信号了

即 signal ignore

复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
    signal(SIGINT,SIG_IGN);
    while(true)
    {
        std::cout<<"我是进程:"<<getpid()<<",我在等待"<<std::endl;
        sleep(1);
    }
    return 0;
}

执行默认处理动作

设置信号处理函数为SIG_DFL

即 signal default

复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handle(int signum)
{
    std::cout<<"我是进程:"<<getpid()<<",我收到了"<<signum<<"号信号,执行回调方法"<<std::endl;
    signal(SIGINT,SIG_DFL);
    std::cout<<"已经将回调方法设置为默认方法"<<std::endl;
}
int main()
{
    signal(SIGINT,handle);
    while(true)
    {
        std::cout<<"我是进程:"<<getpid()<<",我在等待"<<std::endl;
        sleep(1);
    }
    return 0;
}

提供⼀个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为自

定义捕捉(Catch)⼀个信号。

我们从源码可以看到,SIG_DFL,SIG_IGN实际上就是将0,1强转为函数指针类型

产生信号

基本操作

Ctrl+\(SIGQUIT)可以发送终止信号并生成core dump文件(3号信号)

Ctrl+Z(SIGTSTP)可以发送停止信号,将当前前台进程挂起到后台等。(20号信号)

调用系统命令向进程发送信号

其实就是上文的kill命令

ctrl+c发送的是2号信号,所以我们可以输入

kill -2 进程pid

我们也可以写出

kill -SIGINT 进程pid(因为SIGINT本来就宏定义为2

所以信号可以使用数字,也可以使用宏

闲聊

然后我们在这里随便说一下为什么信号的指令是kill

其实很简单,kill一开始就是作为终止信号来结束进程而诞生的,但是后来发现单单的结束进程还不够,可能还需要暂停,恢复进程等其他功能,但是吧,到那会,kill命令已经使用很久了,也不可能一下子让所有用户都知道kill命令换了个名字用来代表发送信号吧,因此就继续使用kill命令来发送信号

使用函数产生信号

复制代码
int kill(pid_t pid, int sig);
//On success (at least one signal was sent), zero is returned. On error,
//-1 is returned, and errno is set appropriately.

没错,kill命令的函数名也是kill,我们通过这个函数向指定进程发送指定信号

raise函数

让进程自己给自己发送信号

复制代码
int raise(int sig);

#include<iostream>
#include<unistd.h>
#include<signal.h>
void handle(int signum)
{
    std::cout<<"我是进程:"<<getpid()<<",我自己给自己发送了"<<signum<<"号信号"<<std::endl;
    sleep(1);
}
int main()
{
    signal(3,handle);
    while(true)
    {
        std::cout<<"我是进程:"<<getpid()<<",我在等待"<<std::endl;
        sleep(1);
        raise(3);
    }
    return 0;
}

abort

发送6号信号给自己来结束进程(就算自定义了6号信号也一样会退出)

复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handle(int signum)
{
    std::cout<<"我是进程:"<<getpid()<<",我收到了"<<signum<<"号信号"<<std::endl;
}
int main()
{
    signal(6,handle);//或者写SIGABRT(abort发送的6号信号的宏定义)
    while(true)
    {
        std::cout<<"我是进程:"<<getpid()<<",我在等待"<<std::endl;
        sleep(1);
        abort();
    }
    return 0;
}

由软件条件产生信号

主要介绍alarm函数与SIGALRM信号

复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值为0或者之前设定的闹钟还剩下的时间
//没有设定过闹钟就返回0

//设定过闹钟有两种返回情况
//如果没有修改默认动作,那么就说明这里一定不是由信号产生的,返回之前设定的闹钟剩下的时间
//修改过默认动作的话,如果是在SIG_ALRM信号里再次调用,那么之前设定的闹钟剩下的时间就肯定为0了,返回值为0

也就只有一个作用:定时

设定一下多少秒后发送SIGALRM信号,默认动作为结束当前进程

使用方法有两种:

1.设定seconds不为0,那么就设定一个闹钟

2.设定second为0,那么就取消以前设定的闹钟

puase函数

复制代码
int pause()
//将进程挂起(暂停)直到接收到一个信号才重新唤醒进程
//返回值总是-1,并且会设置错误码,表示进程被信号中断

我们在这里用pause和alarm函数来测试一下

测试

复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handle(int signum)
{
    std::cout<<"进入alarm自定义动作,打断挂起"<<std::endl;
}
int main()
{
    alarm(5);
    signal(SIGALRM,handle);
    std::cout<<"即将进入pause被挂起"<<std::endl;
    pause();
    std::cout<<"被alarm信号解除pause的挂起状态"<<std::endl;
    return 0;
}

如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产生机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据 产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生

硬件异常产生信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产⽣异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的

子进程退出core dump

复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
int main()
{
    if (fork() == 0)
    {
        sleep(1);
        int* now=nullptr;
        *now=1;
        exit(0);
    }
    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);
    return 0;
}

正常退出,后8为表示退出码

非正常退出(被信号杀死),前7为表示信号编号,第8位表示core dump标志

Core Dump

SIGINT的默认动作为终止进程并且core dump

解释什么是Core Dump。当⼀个进程要异常终止时,可以选择把进程的用户空间内存数据全部

保存到磁盘上,文件名通常是core,这叫做Core Dump。

进程异常终止通常是因为有Bug,⽐如非法内存访问导致段错误,事后可以用调试器检查core文件以

查清错误原因,这叫做 Post-mortem Debug (事后调试)

⼀个进程允许产生多大的 core 文件取决于进程的 Resource Limit (这个信息保存在PCB

中)。默认是不允许产⽣ core 文件的,因为 core ⽂件中可能包含用户密码等敏感信息,不安全。

在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产生 core 文件。⾸先⽤ ulimit 命令

改变 Shell 进程的 Resource Limit ,如允许 core 文件最大为 1024K: $ ulimit -c

1024

保存信号

信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)

从信号产生到抵达之前的状态称为信号未决(Pending)

进程可以选择阻塞(Block)某个信号

信号被阻塞就不会被递达,保持在未决状态,除非进程解除阻塞,才能执行递达工作

注意:阻塞与忽略不同,阻塞是收到了信号但不执行动作,忽略是收到了信号并执行了动作,只不过动作本身为忽略

在内核中的表示

如上图,每个信号都有两个标志位block与pending,还有一个信号处理函数handler

当信号产生时就会设置pending标志位,决定执行信号时再清空pending标志位

清空pending的具体步骤为:

某信号没有被阻塞并且轮到了该信号->取出信号->清空pending->执行动作

我们以图中的三种情况为例:

1.SIGHUP信号没有产生也没有阻塞,它递达时将会执行SIG_DFL(即默认处理方法)

2.SIGINT信号产生了,并且被阻塞,那么该信号就会保持在未决状态,直到信号阻塞被进程解除,处理方法为SIG_IGN(即信号忽略)

3.SIGINT信号没有产生,但是被阻塞了,当信号产生时,信号就会一直被阻塞,保持在未决状态。处理方法为自定义方法sighandler

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信

号⼀次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计⼀次,而实时信号在递达之

前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。

sigset_t

从上图来看,每个信号只有⼀个bit的未决标志,非0即1,不记录该信号产⽣了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"⽆效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。下⼀节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字,这⾥的"屏蔽"应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些

bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调⽤以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

复制代码
#include <signal.h>

//初始化信号集的所有数据,使其中所有信号的bit位置0,该信号集不会包含任何有效信号
int sigemptyset(sigset_t *set);

//初始化信号集的所有数据,使其中所有信号的bit位置1,表示该信号集启用所有系统支持的信号
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);

上面的四个函数都是成功返回0,失败返回-1。

sigismember是一个bool函数,存在返回1,不存在返回0,出错返回-1

sigprocmask

复制代码
#include <signal.h>

//读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

//返回值:若成功则为0,若出错则为-1

简单说下参数,how指示应该如何处理信号屏蔽字

如果set是非空指针,那么就和how一起进行修改进程的信号屏蔽字

如果oset是非空指针,那么就把修改前的信号屏蔽字带出来

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中⼀

个信号递达。

sigpending

复制代码
#include <signal.h>
int sigpending(sigset_t *set);

//读取当前进程的未决信号集,通过set参数传出。
//调⽤成功则返回0,出错则返回-1

测试

我们使用上面学习的几个函数进行一点测试

复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
void PrintPending(sigset_t set)
{
    std::cout << "curr process[" << getpid() << "]pending: ";
    for (int i = 31; i >= 1; --i)
        std::cout << sigismember(&set, i);
    std::cout << std::endl;
}
void handler(int signum)
{
    std::cout << signum << "号信号被递达!" << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "-------------------------------" << std::endl;
}
int main()
{
    signal(2, handler);
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, 2);
    sigprocmask(SIG_BLOCK, &block_set, &old_set);
    int cnt = 5;
    while (true)
    {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        cnt--;
        if (cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
        sleep(1);
    }
}

从这里我们也可以看出,pending标志位是在执行动作之前就已经被清空了

捕捉信号

信号处理流程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

上文已经解释的很清楚了,我们在这里再次解释下信号处理的全部过程

流程

1.首先,由于中断、异常、系统调用等原因从用户态进入内核态

2.内核态处理完异常之后处理可以执行动作的信号(没有信号需要处理那么就单纯恢复上下文继续向下执行)

3.如果是自定义信号处理函数,那么就由内核态转为用户态执行信号处理函数(如果不是自定义信号那么就视信号类型而定,有些信号的默认动作可以在内核态执行,而有些还是需要返回用户态执行,因为默认动作保存在内核里而不是在用户写的代码里)

4.执行完自定义函数之后,信号处理函数返回时会调用特殊系统调用再次进入内核(因为要恢复上下文,继续执行之前还没执行完的程序)

5.回到内核后,判断信号是否全部处理完,没有处理完,则继续按照上面的流程进入用户态执行信号处理函数。全部处理完之后,恢复上下文,进入用户态执行接下来的程序

总的来说,执行自定义信号处理函数就是:

1.进入内核获取信号

2.返回用户态执行函数

3.执行完后返回内核恢复上下文

4.返回用户态继续执行

sigaction

复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

这个函数可以看作是"加强版"的signal函数,因为他的功能更强大

复制代码
struct sigaction {
    void (*sa_handler)(int); // 信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *); // 用于支持信号的附加信息
    sigset_t sa_mask; // 在信号处理函数执行期间需要屏蔽的信号集合
    int sa_flags; // 信号处理的标志
    void (*sa_restorer)(void); // 已废弃,不要使用
};

oact,即old action,将旧的信号处理动作带出来,为空则不带回

act,即action,放入一个新的信号处理动作,为空则不修改

我们主要介绍下sa_handler与sa_mask

sa_handler

信号处理动作,与signal函数相同,是一个返回值为空,参数为int的函数指针

sa_mask

执行信号处理函数期间需要屏蔽的信号集

原理就是

1.在收到信号,执行信号处理函数之前,保存原先的信号屏蔽字

2.将当前进程信号屏蔽字与我们传入的sa_mask进行'或'操作,也就是增加一些信号屏蔽字

3.执行信号处理函数

4.信号处理函数结束后,使用事先保存好的信号屏蔽字来进行恢复

硬件中断

由外部设备触发的,中断系统运行流程,叫做硬件中断

中断向量表是操作系统的一部分,一启动就加载到了内核,保存了各种中断的处理方法

总结这张图:

1.外部设备发起终端

2.中断控制器收到中断,通知cpu

3.cpu知道有中断,向中断控制器获取中断号,并保护现场

4.cpu根据中断号查中断向量表,执行对应的方法

5.执行完中断之后,恢复现场,继续之前的工作

时钟中断

定时触发的设备,定期产生时钟中断

位于操作系统最底下的中断,操作系统所有的操作都要按照时钟中断来执行

包括轮询、调度、定时

操作系统在时钟中断的推动下进行定时调度

复制代码
//以下为操作系统调度的模拟

void handler(int signum)
{
    调度
}

int main()
{
    //时钟中断定期产生,操作系统是个死循环,定期产生的时钟中断推着操作系统进行调度
    while(1)
    {
    
    }
}

操作系统本质就是个死循环

我们需要什么中断,需要什么方法,就向中断向量表里面加就可以了

最终都会交给cpu来进行执行

软中断

软件也有自己的软中断

为了让操作系统⽀持进行系统调用,CPU也设计了对应的汇编指令(int或者syscall),可以让CPU内

部触发中断逻辑

不同的指令代表着不同的软中断处理方法,只不过这些指令产生的中断统称为软中断

就像上面的键盘,显示等等也可以统称为硬件中断

我们在使用的时候最终都是通过中断来让cpu执行我们的方法,只不过linux的C标准库已经帮我们把这些软中断指令封装好了

缺页中断?内存碎片处理?除零野指针错误?

最后全都会被转化为cpu的软中断进行处理,走中断处理流程

理解内核态与用户态

用户区在[0,3]GB

内核区在[3,4]GB

上面总结一下就是级别的问题

内核权限高于用户,内核可以访问用户,用户却不能直接访问内核

用户可以采用软中断指令触发系统调用,cpu保存用户态的上下文数据,进入权限更高的内核态

内核可以访问用户的数据,实际上就是内核态可以访问进程的地址空间拿到数据,然后在内核态执行系统调用方法,将运行结果交给进程

这样子就不是多个进程主动查找操作系统,而是操作系统主动去找进程,这样就可以让多个进程找到同一个操作系统了

volatile

这个关键字的目的主要是为了保证数据必须严格读取内存中最新的值

主要是为了应对优化的情况,不优化的话数据默认就是从内存中读取最新的值

但是一旦开启优化,编译器就会出于减少访问内存的次数提升效率的目的导致某些情况下获取到的数据可能不是最新的

例如:

1.多线程情况下,相当于多个程序对同一个数据进行修改,那么我们开启优化就会导致数据不一致的情况

2.信号直接修改数据的值,这样的话优化后程序又不读取内存最新的值就会导致同样的数据不一致情况

例子

我们不加volatile关键字

复制代码
#include <stdio.h>
#include <signal.h>
int flag = 0;
//volatile flag=0;
void handler(int sig)
{
    printf("chage flag 0 to 1\n");
    flag = 1;
}
int main()
{
    signal(2, handler);
    while (!flag);
    printf("process quit normal\n");
    return 0;
}

不开优化

开启优化

复制代码
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1

加上关键字

复制代码
^Cchage flag 0 to 1
process quit normal

volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该

变量的任何操作,都必须在真实的内存中进行操作

相关推荐
HHBon27 分钟前
判断用户输入昵称是否存在(Python)
linux·开发语言·python
苇柠2 小时前
Java补充(Java8新特性)(和IO都很重要)
java·开发语言·windows
编程绿豆侠2 小时前
力扣HOT100之多维动态规划:62. 不同路径
算法·leetcode·动态规划
鑫鑫向栄2 小时前
[蓝桥杯]剪格子
数据结构·c++·算法·职场和发展·蓝桥杯
白总Server2 小时前
C++语法架构解说
java·网络·c++·网络协议·架构·golang·scala
羊儿~2 小时前
P12592题解
数据结构·c++·算法
Wendy_robot2 小时前
池中锦鲤的自我修养,聊聊蓄水池算法
程序人生·算法·面试
.Vcoistnt2 小时前
Codeforces Round 1028 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
白熊1883 小时前
【机器学习基础】机器学习入门核心算法:层次聚类算法(AGNES算法和 DIANA算法)
算法·机器学习·聚类
156082072193 小时前
在QT中,利用charts库绘制FFT图形
开发语言·qt