信号概念

信号是OS发送给进程的异步机制!所谓异步指的是,发送信息这个动作,并不会干扰进程本身!

对于信号的基本认识:

1.什么样的信号应该如何处理,是在信号产生之前早就得知了的

2.信号的处理并不是立即处理,而是等待合适的时间去处理

3.对于进程来说,其内部是以及内置了对于信号的识别以及处理方式

4.产生信号的方式很多,也就是说信号源非常多

信号的产生

信号的产生有很多方式

1.键盘产生信号

之前我们常见的:Ctrl + c就是信号,用于终止进程!

信号都有那些:

其中,我们只需要关注信号1~31(普通信号),信号的名字本身是,其真正的值就是前面的编号。

处理信号

进程收到信号之后,进程会在合适的时候,进程处理!其中处理的方式有三种

1.执行默认的处理动作!(相当一部分的信号默认动作都是终止进程)

2.执行自定义动作!

3.忽略信号,继续做自己的事!

自定义处理

代码语言:javascript

AI代码解释

 #include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

代码语言:javascript

AI代码解释

#include <signal.h>
#include <iostream>
using namespace std;

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    while (1)
    {
    }
}

代码语言:javascript

AI代码解释

hyc@hyc-alicloud:~/linux/进程信号$ ./test
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2

可以看到,我们发送了多次的Ctrl + c信号,可见Ctrl + c信号发送的就是2号信号:SIGINT

当然并不是说有的信号都可以被自定义,那不然进程就无法停止了!

前后台

当我们运行可执行程序时,我们发现Linux指令不起作用了?!这就是前后台的问题了

在OS中,进程分为:前台进程、后台进程

前台进程:有且仅有一个!并且只有前台进程才能接收输入的数据!

后台进程:可以有多个!

虽然输入的数据只有前台进程可以接收,但是输出的数据可以由前后台共同进行的!

所以,当我们运行我们的程序时,当前这个程序就处于前台了!那么负责接收解析指令的shell程序就会退出前台!而后台程序是不能接收输入进来的数据的,所以这才导致我们输入的指令没有反应!

发送信号的本质

信号发送给进程后,进程需要在合适的时间再进行处理!那么这就意味着进程需要先将信号保存下来!后续再读取执行。

那么保存在哪里呢?答案是保存在task_struct的sigs变量中!其中sigs采用的是位图结构比特位的位置表示信号的编号比特位的内容(1表示收到、0表示没有收到)表示是否收到

所以,发送信号的本质就是,向目标进程写信号 -> 修改位图!

但是task_struct中的数据属于OS内核数据!所以想要修改其数据,就只能让OS自己来修改!所以信号只能让OS来发送!

2.系统调用产生信号
kill接口

代码语言:javascript

AI代码解释

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

作用:向指定的进程发送信号!

pid 参数:
pid > 0:向指定进程 ID 的进程发送信号
pid = 0:向与调用进程同进程组的所有进程发送信号

sig 参数:
代表信号编号

看看效果:

代码语言:javascript

AI代码解释

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    kill(getpid(), 2);
}

也可以通过kill来验证一下,上面说的“并不是所有信号都可以被自定义!”

代码语言:javascript

AI代码解释

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    for (int i = 1; i < 32; i++)
    {
        kill(getpid(), i);
    }
}

可见,信号9并不能被“自定义”!当然不仅仅编号9,还有其他信号也不能被自定义。

abort接口

代码语言:javascript

AI代码解释

#include <stdlib.h>
void abort(void);

作用:强制终止当前的进程!

看看效果:

代码语言:javascript

AI代码解释

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    abort();

    for (int i = 1; i < 32; i++)
    {
        kill(getpid(), i);
    }
}

显然,进程并没有执行for循环!这说明:即使自定义了处理函数,abort 最终仍会强制终止进程!

alarm接口

代码语言:javascript

AI代码解释

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

作用:向当前进程发送SIGLRM信号

参数 seconds:指定定时器的超时时间(单位:秒)
若 seconds > 0:内核会在 seconds 秒后向当前进程发送 SIGALRM 信号
若 seconds = 0:取消当前进程中已设置的所有 alarm 定时器(如果存在)

返回值:
若之前已设置过 alarm 定时器且未超时:返回剩余的秒数(即距离上次设置的超时时间还剩多久)
若之前未设置过 alarm 或已超时:返回 0

看看效果:

代码语言:javascript

AI代码解释

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    alarm(5);
    sleep(5);
}

可见,确实发送了信号!

3.命令产生信号

代码语言:javascript

AI代码解释

killall -9 chrome  # 发送SIGKILL(9),强制终止所有chrome进程

很简单就不过多说明了

4.异常产生信号

程序异常:出现“除0错误”“野指针” 等错误!

代码语言:javascript

AI代码解释

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    int a = 1;
    a /= 0;
}

除零错误,发送信号8!

代码语言:javascript

AI代码解释

void sighandler(int i)
{
    cout << "收到一个信号:" << i << endl;
}

int main()
{
    // 将1~31的信号全部自定义
    for (int i = 0; i < 32; i++)
    {
        signal(i, sighandler);
    }

    int *p = nullptr;
    *p = 10;
}

同样的,野指针也发送了信号!

上面我们说到所有的信号都是由OS来进行发送的!那OS是如何得知程序出现错误的呢?

因为,OS是所有软硬资源的管理者!通过硬件协作自身监控机制,实时捕获程序运行中的异常状态

信号的保存

信号的产生我们知道了,下面我们来看信号是如何保存的

核心概念

信号从产生到递达之间的状态,称作信号未决(Pending)

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

被阻塞的信号会处于未决状态,直到进程解除对该信号的阻塞,才会执行递达动作

信号递达后分别有3个动作:默认动作、自定义动作、忽略!

注:忽略是递达后的动作!而阻塞是未递达的动作!

保存

task_sturct中存在三张表,信号由三张表负责保存。

handler表,保存信号的处理方法,其本质的函数指针数组。SIG_DFL表示默认方法,SIG_IGN表示忽略,使用接口sighandler(int sigon)表示自定义方法!(SIG_DFL本质是宏,其内容是被强转的整数0:(_sighandler_t) 0。)

pending表,保存信号是否被接收,其本质是位图。0表示没有接收到,1表示接收到了。

block表,保存信号是否被阻塞,其本质是位图。0表示没有被阻塞,1表示被阻塞了。

一行信息才是一个信号的完整信息!从上往下,依次表示信号1~31!

sigset_t

sigset_t是一个数据类型,表示信号集!用于记录每个信号的“有效”“无效”状态。

从上图来看,我们发现每个信号的block、pending都只使用一个bit位来表示!而并不记录这个信号产生了多少次!所以这两个信号都用sigset_t来存储,分别叫做未决信号集阻塞信号集(屏蔽信号集)。

信号集操作函数

sigprocmask

代码语言:javascript

AI代码解释

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

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

参数:

sigprocmask的行为由how决定,set和oldset分别用于指定新的信号集和保存旧的信号集

how

控制信号屏蔽字的修改方式,仅支持 3 个预定义值(核心参数)。

set(输入型参数)

指向新的信号集: 若 how 非 0,此参数指定要操作的信号集; 若为 NULL,表示不修改屏蔽字(仅用于获取旧屏蔽字)。

oldset(输出型参数)

用于保存修改前的旧信号屏蔽字: 若为 NULL,表示不保存旧屏蔽字。

how的取值:

sigpending

代码语言:javascript

AI代码解释

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

作用:获取当前进程未决信号集。

参数:set是指向sigset_t类型的指针,用于存储未决信号集合。
返回值:成功时返回0;失败时返回-1,并设置errno。可能的错误包括EFAULT(set指向非法地址)。

演示:

代码语言:javascript

AI代码解释

#include <signal.h>
#include <iostream>
using namespace std;

void Print(sigset_t pending)
{
    cout << "当前进程:" << getpid() << "pending:" << endl;
    for (int i = 31; i >= 1; i--)
    {
        // 检查信号编号i是否在pending中
        if (sigismember(&pending, i))
        {
            cout << 1;
        }
        else
        {
            cout << 0;
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "信号递达!" << endl;
    sigset_t pending;
    sigpending(&pending);
    Print(pending);
}

int main()
{
    // 捕捉2号信号
    signal(2, handler);

    // 屏蔽2号信号
    sigset_t set, oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);    // 将信号集初始化为空!
    sigaddset(&set, SIGINT); // 向指定信号集,添加信号

    // 向进程阻塞信号集添加信息,让SIGINT信号被阻塞
    sigprocmask(SIG_BLOCK, &set, &oldset);

    int cnt = 10;
    while (true)
    {
        // 获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);

        // 打印pending信号集
        Print(pending);
        cnt--;

        // 解除对2号信号的阻塞
        if (cnt == 0)
        {
            cout << "解除对2号信号的阻塞\n";
            sigprocmask(SIG_SETMASK, &oldset, &set);
        }

        sleep(1);
    }
}

我们可以看到:在还没有解除2号信号阻塞时,信号确实接收到了!但没有递达。当信号阻塞解除时,信号立马递达了!

注意:

当信号准备抵达时,会先将pending表中信号对应的1修改为0!避免同一个信号被反复递达。

补充:

在Linux中信号中止的方式有两种:Core、Term

其唯一区别就在于是否会“核心转储”! Core:在进程异常退出时,会在当前路径下形成一个文件,将进程的核心数据拷贝至文件中,然后将进程退出!

而Term则会直接进行进程退出!核心转储的目的是为了实现debug!开启core dump,程序运行崩溃时,gdb core-file core,可以直接帮我们定位到错误的地方!

当然也可以通过 ulimit -a 查看,ulimit -c打开core dump功能。

信号处理

信号的保存我们知道了是如何进行的,下面我们来讲一讲信号的如何处理的

上面我们讲到了信号的处理,进程收到了信号不是立即处理!而是在合适的时间进行处理。

先直接给出结论:

适合的时间:进程从内核态,返回至用户态的时候。此时会进行信号检查(检查spending若发现接收到了信号,则再去检查block,若block没有显示阻塞,则去执行信号对应的方法!反之不满足任何一点)

我们在执行自定义方法时,OS也必须进行用户身份的转化!:用户态与内核态的转化。(因为用户身份是无法访问操作系统的内核数据的)。当然仅执行默认动作(完全由内核态完成,这是系统预定义好的)或忽略动作是不需要的用户身份转化的!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐