-梅头像
linux(11)信号封面图

linux(11)信号

前言:

最勤劳的一集,早上雷霆大睡到十点,起来写上这么一篇,感觉整个人都越来越通透了🥵

我们今天要来看的是信号,那么我今天要写些什么?

如下:

① 首先,要讲信号,我们肯定要先有信号,谁能产生信号?所以第一点就是信号的产生

③ 其次,我们有了信号,就要对信号进行处理,我们叫做信号捕捉

② 但是,我们收到信号,会立刻对信号进行处理么?(你老冯叫你吃饭你也不一定会直接去吧?)所以我们是在合适的时候处理信号,那么期间,我们就要对信号进行保存🥵

④.但是,合适的时候是什么时候?我们要理解这个,就要引入计算机组成原理中的中断了

⑤ 引入中断后,我们会发现,还需要再引入内核态和用户态这两个概念,我们才能彻底理解信号捕捉的过程

主线如图:

正文:

先来简单理解一下信号:

比如说,我在教室听课,老登突然说,这个同学,你来回答一下这个问题,然后我立刻站起来,回答问题;(立即响应:默认处理)

亦或是,我在家里打游戏,我点的外卖到了,外卖小哥跟我打电话说,您的外卖放在了***,你早点去取。但是呢,我还在打游戏,我挂了电话以后,继续打游戏,打完游戏我才去取外卖;(先存着晚点处理:阻塞(pending 未决))

又或者说,我们去听讲座,讲座上的老头在那一直讲,我觉得无聊,我就左耳朵进右耳多出,总之啥也没记住;(直接丢掉:忽略 SIG_IGN)

再或者,我正在玩奥拉星,我给自己设了个闹钟:我当时这么想,我听到闹钟响了,我就去写作业;闹钟响了之后,我听到了,然后我关闭了电脑,开始写作业;(自己写逻辑:自定义捕获 handler)

也就是说,我对于信号,有这么多不同的处理方式呀,那我们接下来先来看看,信号是如何产生的?

信号的产生:

信号产生,共分为五种情况:

①. 通过终端按键产生信号:

那最常见的,不就是我们敲键盘么,我们执行一个程序,我们可以通过ctrl+c终结掉,那么这其实是给进程发信号啦,那我们如何证明?

此处,我们就要给大家引入一个函数,

signal:
 signal的作用:

提前向内核注册,当收到指定信号时,不去执行系统默认动作,转而执行我们自己写的 handler 自定义处理函数。

(人话:收到信号时,按我提供的方法对信号进行响应)

如何使用?
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signum:信号编号(其实就是宏,可以传下面这些,也可以传其对应的数字)

(编号34开始的是实时信号,我们就不讨论了,我们讨论1-31,注意,不是从0开始)

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法
回归主题:

所以呢,我给大家写了一个测试代码:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("收到一个信号:%d ,你不许玩了\n",sig);
}
int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        sleep(1);
        printf("我正在玩奥拉星\n");
    }
}

我把二号信号捕捉了

所以它在收到二号信号的时候,就会按我们的进行执行咯:

那么我们跑起来看看:

wuchun@VM-0-10-ubuntu:~/10_11$ ./sig1
我正在玩奥拉星
我正在玩奥拉星
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
^C收到一个信号:2 ,你不许玩了
我正在玩奥拉星
我正在玩奥拉星
我正在玩奥拉星
我正在玩奥拉星

我在这里狂按ctrl+c,它也只会打出来一个,收到信号:2,不许玩了。那我们如何杀了它呢?

我们当然可以用另一个终端kill -9杀了它,不过嘛:

我们也可以(ctrl+\)这个也是一个终止信号,可以让我们的进程终止,按下后我们看看

^\Quit                       (core dumped) ./sig1

core dumped是什么意思?

当进程收到SIGQUIT 信号(Ctrl+\)这类信号异常终止时,操作系统会把进程当前内存里的全部数据、寄存器状态,打包保存成一个core核心转储文件。 后续我们可以用 gdb 加载这个 core 文件,事后调试,定位程序崩溃的原因。

但是:咦?我们是不是在哪见过这东西:

我们当年在讲父进程等待子进程的时候,不是有一个输出型参数status么,它里面的第八位,不正是core标志位么:如果这一位为 1,就代表进程退出时产生了 core dump

来个小总结:

  1. 正常退出:低 7 位 = 0,core 位 = 0
  2. 被信号杀死:低 7 位存放信号号;如果 core 位 = 1 → 崩溃并生成 core 文件

那么,以上的就是第一种产生信号的方法

②调⽤系统命令向进程发信号(kill)

这种我们熟呀,我们当年杀进程,不就是kill-9么给大家演示一下:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("收到一个信号:%d ,不许通透了\n",sig);
}
int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        sleep(1);
        printf("我是进程:%d,我感觉好通透呀......\n",getpid());
    }
}
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......
我是进程:1746267,我感觉好通透呀......

我在另一个终端发的信号

所以,我们也可以调用系统命令向进程发信号

③. 调用函数产生信号

我们这里讲三个:

 ①.kill

   函数原型:int kill(pid_t pid, int sig); 作用:向指定 PID 的进程,发送指定信号

  • pid:目标进程 ID
  • sig:要发送的信号编号 / 宏(SIGINT、SIGQUIT 等)

   pid 分 4 种情况:

  1. pid > 0:向 pid 等于该值的进程发信号
  2. pid = 0:向当前进程所在进程组所有进程发送信号
  3. pid = -1:向所有有权限的进程群发信号
  4. pid < -1:向-pid对应的进程组全部进程发送信号

我们来个小例子:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("收到一个信号:%d ,不许通透了\n",sig);
}
int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        sleep(1);
        pid_t id=getpid();
        kill(id,SIGINT);
        printf("我是进程:%d,我感觉好通透呀......\n",getpid());
    }
}

那么它就自己给自己发信号了

wuchun@VM-0-10-ubuntu:~/10_11$ make
gcc -o sig2 sig2.c
wuchun@VM-0-10-ubuntu:~/10_11$ ./sig2
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1749772,我感觉好通透呀......
②.raise

函数原型:int raise(int sig);

作用:给当前自己这个进程发送信号。

等价于:kill(getpid(), sig);

演示一下:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("收到一个信号:%d ,不许通透了\n",sig);
}
int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        sleep(1);
        raise(SIGINT);
        printf("我是进程:%d,我感觉好通透呀......\n",getpid());
    }
}
wuchun@VM-0-10-ubuntu:~/10_11$ ./sig2
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
收到一个信号:2 ,不许通透了
我是进程:1750395,我感觉好通透呀......
③ abort

函数原型:void abort(void); 作用:给自己发送 SIGABRT 信号,终止进程,默认一定会产生 core dump

特点:

  1. 本质底层调用raise(SIGABRT)
  2. 就算你用 signal 捕获 SIGABRT,handler 执行完之后,进程依然会终止,无法阻止退出
  3. 专门用于程序遇到严重错误,主动崩溃、生成 core 文件,方便调试。

这个例子就不写了,总之也是和上面一样的效果。

④软件条件产生信号

我们这里主要讲讲alarm:

函数原型:unsigned int alarm(unsigned int seconds);

作用:设置一个闹钟计时器。当 seconds 秒时间到了,内核会向当前进程发送 SIGALRM 信号。

注意:

①. 如果我们while循环中放了个闹钟,如果在下一次循环到来之前,闹钟没有结束,那么会重置闹钟的时间,那么闹钟就永远不响了,同时闹钟的返回值是,上一个闹钟剩余的时间。

②.闹钟的信号的默认动作是终止进程(其实很多信号的默认动作都是终止进程)

③. alarm(0),取消当前已经设置的闹钟

例子:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("闹钟响起啦!\n");
}
int main()
{
    alarm(5);
    while(1)
    {
        sleep(1);
        printf("闹钟还没响,我直接一个雷霆大睡!\n");
    }
    return 0;
}

当我们到第五秒的时候,闹钟直接响,我们的进程终止

wuchun@VM-0-10-ubuntu:~/10_11$ ./sig3
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
Alarm clock       

下面我们捕捉一下闹钟:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("闹钟响起啦!\n");
}
int main()
{
    signal(SIGALRM,handler);
    alarm(5);
    while(1)
    {
        sleep(1);
        printf("闹钟还没响,我直接一个雷霆大睡!\n");
    }
    return 0;
}
wuchun@VM-0-10-ubuntu:~/10_11$ ./sig3
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟响起啦!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡!
闹钟还没响,我直接一个雷霆大睡

此时我们就会打印一行闹钟响了,而不会终止进程

⑤.硬件异常产生信号

很典型的就是我们的除零错误,当我们把代码放到CPU里执行的时候,我CPU里面会有各种各样的标志位,来表示我这个运算是否正确,而我除零之后,就会有标志位记录下错误,从而进行中断,操作系统知道以后来进行处理

例子如下:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("捕捉到信号:%d,你除零了!!!!\n",sig);
}
int main()
{
    int num=1;
    num/=0;
    return 0;
}
wuchun@VM-0-10-ubuntu:~/10_11$ gcc -o sig4 sig4.c
sig4.c: In function ‘main’:
sig4.c:11:8: warning: division by zero [-Wdiv-by-zero]
   11 |     num/=0;
      |        ^~
wuchun@VM-0-10-ubuntu:~/10_11$ ./sig4
Floating point exception   (core dumped) ./sig4

信号的保存:

在我们的task_struct里面,有三张表,第一张表是block表,第二张是pending表,第三张是handler表:

分别代表什么呢?

1. block(阻塞位图,信号屏蔽字 sigmask)

block:哪些信号是被当前进程屏蔽(阻塞)的

  • 对应 bit=1:代表屏蔽该信号;信号来了,暂时不处理,先放着
  • 对应 bit=0:不屏蔽,信号可以正常处理

2. pending(未决 / 待处理位图)

pending:已经收到、等待被处理的信号

  • bit=1:收到了这个信号,等待处理
  • bit=0:没有收到该信号

当内核给进程发信号,就会把 pending 对应位置 1; 只有当该信号没有被 block 阻塞,并且进程从内核态切回用户态的瞬间,才会去检查 pending 位图,执行 handler。

3. handler(信号处理函数表)

handler:每个信号对应的处理方法 里面存的是函数指针,决定信号来了之后怎么做:

  1. 默认动作(SIG_DFL):比如 SIGINT 默认终止进程
  2. 忽略(SIG_IGN):收到信号直接丢掉
  3. 自定义 handler 函数:我们用signal/sigaction注册的回调函数

那么我们如何操作?

前面我们图里的block、pending本质都是sigset_t信号集(位图)。下面 5 个函数,只在用户态操作信号集变量,不会直接修改内核里进程的屏蔽字

#include <signal.h>
//1. 清空信号集,全部bit置0
int sigemptyset(sigset_t *set);
//2. 填满信号集,全部bit置1
int sigfillset(sigset_t *set);
//3. 向信号集添加一个信号(对应bit置1)
int sigaddset(sigset_t *set, int signo);
//4. 从信号集删除一个信号(对应bit置0)
int sigdelset(sigset_t *set, int signo);
//5. 判断某个信号是否在信号集中,在返回1,不在返回0
int sigismember(const sigset_t *set, int signo);

但是呢,如果真要对内核里面的pending表和block表动刀,它们分别有一个函数:

我们先来看block表:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

作用:修改内核中当前进程的信号屏蔽字(就是我们图里的 block 位图)

  • how:三种选项
    1. SIG_BLOCK:在原有屏蔽集新增set 里的信号(或运算)
    2. SIG_UNBLOCK:在原有屏蔽集解除set 里的信号(取消屏蔽)
    3. SIG_SETMASK:直接把 block 替换成 set 的值,覆盖
  • set:你准备好的信号集;传 NULL 代表不修改
  • oldset:输出参数,保存修改前旧的屏蔽字,方便恢复,传 NULL 不保存
  • 通俗理解:前面 sigaddset 等函数只是在用户态拼好集合,sigprocmask 才是真正把这个集合写入内核 task_struct 的 block 位图。
接下来是pending表:
int sigpending(sigset_t *set);

作用:读取内核中当前进程的 pending 未决信号位图,拷贝到用户态 set 里

  • 把内核 pending 位图复制到你传入的 sigset_t 变量
  • 可以用来查看:当前有哪些信号已经到达、被阻塞,还没处理
具体的使用:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main()
{
    sigset_t block,pend;
    sigemptyset(&block);
    sigaddset(&block,SIGINT);
    sigprocmask(SIG_BLOCK, &block, NULL);
    printf("我已经把二号信号堵塞,按下ctrl+c,即可看到二号信号\n");
    sleep(6);
    sigpending(&pend);
    for(int i=31;i>=1;i--)
    {
        if(sigismember(&pend,i))
        {
            printf("1");
        }
        else
        printf("0");
    }
    printf("\n");
    return 0;
}
wuchun@VM-0-10-ubuntu:~/10_11$ ./rpending
我已经把二号信号堵塞,按下ctrl+c,即可看到二号信号
^C0000000000000000000000000000010

好啦,那么信号的保存我们就讲完啦;

只是,我们接下来按理说该讲信号处理了,只是我们在讲信号处理前,会先给大家引入中断的概念

中断:

硬件中断:

为了方便理解,我给大家画了一张图:

①:我们外设如果有什么动作,需要告诉CPU给我解决,我就需要向中断控制器发起中断;

②:中断控制器在收到硬件中断后,把它的中断号记录下来,然后通知CPU,CPU获取其中断号

③:CPU获取中断号后,把寄存器中的上下文数据保存,去内存中的操作系统中,通过其中断号,索引解决方法。

④:在进行完以上流程后,操作系统会恢复寄存器中的数据,继续还未执行完的代码

那如果,我以一个指定的频率,向我的CPU发起中断,会怎么样呢.....

那么我的操作系统,在我两次固定频率的中断期间,剩下的时间就是给我继续执行代码的:

我们曾经在CPU调度的时候说过,我们会给每一个进程分配一个时间片,而在我们每一个固定中断的过程中,我们会进行时间片的检查,如果时间片没到,那我时间片减减,然后让该进程继续运行,那如果我时间片到了,我就去调度别的进程;所以每一次固定频率的中断,我们都要进行时间片的检查,操作系统正是由于这个机制得以运转,这个中断叫做时钟中断

所以:时钟中断就是操作系统的 “心跳”,周期性打断程序,让 OS 有机会抢占 CPU,实现多进程分时运行

没有时钟中断,操作系统就没有机会主动抢走 CPU,进程一旦拿到 CPU 就会一直运行,无法实现多进程并发

软中断:

为了让操作系统支持系统调用,我们的CPU设计了对应的汇编指令(int或syscall),可以让CPU内部触发中断逻辑,我们叫软中断

过程如下:

我们在用户态时,把系统调用号记录到eax(寄存器)当中,然后把参数也放入到寄存器之中;

之后我们执行int 0x80,触发软中断,我们从用户态陷入内核态(CPU的动作,把cpl由3改为0),然后我们去读取系统调用表,并且通过系统调用号来找到我们所需要的函数,执行完毕后把返回值放到eax,我们再从内核态切回用户态,恢复现场。

这个过程呢,我们叫做陷阱;

而:

缺⻚中断?内存碎⽚处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,
然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有是⽤来
处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
CPU内部的软中断,⽐如除零/野指针等,我们叫做 异常。

用户态和内核态:

当然,我们上面很多次提到用户态和内核态,自然要讲一讲:

简而言之,用户态和内核态,其实就是CPU中寄存器里的两个标志位(低两位);

其只用两种状态11 或者00;

11为3,代表用户态;

00为0,代表内核态;

我们在虚拟地址空间中讲过,0到3G是我们的用户空间,而3到4G是我们的内核空间,在页表的映射下,3到4G映射的是我们的操作系统,通过页表里面的权限位进行区分时用户空间还是内核空间。(为了防止用户乱访问)

其中,由于3到4G都是固定的映射操作系统,所以每一个进程,它3到4G的页表一样:

但是:每个进程都有自己独立的一套页表(独立 PGD 根),但是所有进程页表里,3G~4G 内核虚拟地址对应的那部分页表项,映射关系完全一样,指向内核同一块物理内存。

信号捕捉:

一张图,说明所有问题

尾:

一周双更!!!!!!

写完已经晚上六点了,整个人感觉好通透呀~

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2601_96394870/article/details/167573445

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--