当前位置: 首页 > news >正文

【视频笔记】408新增知识点信号——里昂视频

原视频来自于B站里昂,总长22min
视频链接

在这里插入图片描述

进程通信方法:管道,共享内存,消息队列,信号和信号量

在这里插入图片描述


2.信号

        对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。信号跟信号量虽然名字相似度 66.66%,但两者用途完全不一样,就好像 Java 和 JavaScript 的区别。

        一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。比如,如果当进程在前台运行时,你键入Ctrl+C(也就是同时按下Ctrl键和 C键),那么内核就会发送一个SIGINT信号给这个前台进程

在这里插入图片描述

在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:

$ kill -l1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP6) 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

每种信号类型都对应于某种系统事件,用不同的整数表示,例如SIGINT信号用号码2表示

1~31号为非实时信号(不可靠信号)处于就绪队列多个相同的非实时信号只会被响应一次。其余的被丢掉。
34~64号信号为实时信号(可靠信号),处于就绪队列多个相同的实时信号全部会被响应。

在这里插入图片描述

比如,如果一个进程试图除以0,那么内核就发送给它一个SIGFPE信号(号码8 Floating Point Exception浮点异常)。
如果一个进程执行一条非法指令,那么内核就发送给它一个SIGILL信号(号码4)。
sigill(ill生病,有病的,不健康的;不良的;不良的;〈美俚〉(因嫌疑而)被捕的,被拘留的;坏的,邪恶的,有害的;不舒服;)

kill -l
#查看系统上支持的不同类型的信号
3.信号的实现

        在操作系统中,每个进程都有一个进程控制块(Process Control Block,PCB),它包含了进程的管理和控制信息.。
【例】Linux下,用一个名为task_struct的结构体类型来描述PCB,包括很多字段,如进程的状态进程的标识、进程的优先级等。每一种信息都用一个字段来实现。

struct task struct
{
...
volatile long state; /进程状态
pid_t pid; //进程ID,每个进程都有一个唯一的PID,用于区分不同进程,相当于身份证号
unsigned long rt_priority;//进程优先级
stuct mm_struct*mm,*active_mm;//与内存管理有关的数据结构,包含进程内存使用的信息
...
}

因此,我们可以将信号记录在进程的task_struct(PCB)结构体中。
例如,我们可以用位图来表示某个信号是否产生,每个比特位代表了某个特定的信号,比特位为0代表没有收到了信号,为1则代表收到了信号。进程收到信号,本质是位图被修改

在这里插入图片描述

4.信号的处理
        信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。

        信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

①忽略信号

        不采取任何操作。但是有两种信号不能被忽略:SIGKILL(9)和SIGSTOP(19),它们用于在任何时候中断或结束某一进程。
        这样做的原因是系统管理员需要能够杀死或停止进程,如果进程能够选择忽略SIGKILL(使进程不能被杀死)或SIGSTOP(使进程不能被停止)将破坏这一权力。

②执行信号的默认操作

        Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。

        例如,进程收到SIGINT信号后会终止,本质上是向进程发送了一个编号为2的SIGINT信号,只不过这个信号是通过键盘输入的,然后经过操作系统处理后再发送给进程。
在这里插入图片描述

③捕获井处理信号

          内核(内核态)会暂停该进程正在执行的代码,并跳转到用户注册的函数(用户态)。

  • 用户态:我自己写的代码
  • 内核态:执行操作系统的代码
  • 两者都属于操作系统运行状态

言归正传,让我们开始讲信号捕捉的具体流程

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

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下;

用户程序注册了 SIGQUIT 信号的处理函数 sighandler,当前正在执行 main 函数

①这时发生中断或异常切换到内核态

②在中断处理完毕后要返回用户态的 main 函数之前检査到有信号 SIGQUIT 递达:

③内核决定返回用户态后不是恢复 main 函数的上下文继续执行而是执行 sighandler(信号处理)函数sighandler 和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态

⑤如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

例题:

由上述信号捕获过程,易选A

实际举例

SIGINT原本是用来结束进程的,但用signal自定义它的功能后就可以使它对进程的操作改变,

在这里插入图片描述

#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){signal(SIGINT,handle);
//将SIGINT号信号的功能改成handlewhile(1){printf("a\n");sleep(1);}
return 0;}

可以看到,每次按下Ctrl+C,都会打印对应内容(实现自定义的功能而不是像原来一样终止进程),而sigNum也证明Ctrl+C对应的信号值确实是2号(SIGINT)。
在这里插入图片描述

经常捕获的两种信号是 SIGINT 和 SIGTERM。

SIGKILL和SIGSTOP 不能被捕获,即无法通过自定义handle函数来修改其信号操作。

exp1

#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){signal(SIGKILL,handle);
//将SIGINT号信号的功能改成handlewhile(1){printf("still alive\n");sleep(1);}
return 0;}

在这里插入图片描述


exp2

#include<singal.h>
#include<stdio.h>
#include<unistd.h>
int count=0;
//handle的函数实现的功能是打印"count=%d, still alive"
void handle(int sigNum){
printf("count=%d, still alive sigNum:%d\n",sigNum);
}
int main(){signal(SIGKILL,handle);signal(SIGTERM,handle);signal(SIGINT,handle);
//将SIGINT号信号的功能改成handlewhile(1){count++;sleep(1);}
return 0;}

在这里插入图片描述
2:sigint
15:sigterm
9:sigkill


几个Linux支持的典型信号:

在这里插入图片描述

SIGCHLD
当进程终止或停止时,内核会给进程的父进程发送此信号。在默认的情况下SIGCHLD是被忽略的,如果进程对它们的子进程是否存在感兴趣,那么进程必须显式地捕获并处理该信号。
SIGFPE
不考虑它的名字,该信号代表所有的算术异常,而不仅仅指浮点数运算相关的异常,异常包括溢出、下溢和除以0.
默认的操作是终止进程并形成内存转储文件,但进程可以捕获并处理该信号。
SIGILL
当进程试图执行一条非法机器指令时,内核会发送该信号。默认操作是终止进程并进行内存转储进程可以选择捕获并处理SIGILL。
SIGINT
当用户输入中断符(通常是Ctrl-C)时,该信号被发送给所有前台进程组中的进程默认的操作是终止进程。进程可以选择捕获并处理该信号,通常是为了在终止前进行清理工作

5.信号的产生

信号通常由以下方式产生:

① 通过终端按键(键盘)产生信号例如,Ctrl+C发送2号信号SIGINT、Ctrl+\发送3号信号SIGQUIT
② 程序异常时操作系统会向程序发送信号来终止进程。
③ 调用函数
- kill系统调用:

kil()调用会从一个进程向另一个进程发送信号:
int kill(pid t pid,int signo);//调用kill给pid代表的进程发送信号signo。
kill命令本质上是通过系统调用实现的。

- raise系统调用

【例子】创建一个raise程序,将其2号信号的处理函数改为自定义函数,该信号每隔一段时间便会给自己发送信号

#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){signal(2,handle);
//将2号信号自定义。while(1){printf("I'm a process, pid = %d\n",getpid());sleep(1);raise(2);//进程自己给自己发送2号信号。}
return 0;}
abort 系统调用

abort 使当前进程接受到信号而异常终止

#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){signal(6,handle);
//将SIGINT号信号的功能改成handlewhile(1){printf("I am a process, pid : %d\n",getpid());sleep(1);abort();}
return 0;}

在这里插入图片描述

④由于软件条件产生信号

例如alarm()函数可以设置定时器,当定时器倒计时结束,就会向进程发送一个SIGALARM信号。

exp1
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>void handle_alarm(int signo){
printf("Alarm clock!\n");
}
int main(){
//设置信号处理函数signal(SIGALRM,handle_alarm);
//做其他事情或者简单地等待while(1){printf("running\n");alarm(2);sleep(2);}
return 0;}
exp2

pause()函数的作用是使当前进程进入睡眠状态,直到接收到一个信号为止。

int pause(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main( ) {pid_t child_pid = fork();//fork一个子进程。 子进程的pid不一样。if(child_pid < 0) {//创建新进程失败。perror("Fork failed");exit(1);}if (child pid == 0){ // 子进程while (1){pause(); // 等待信号}else { // 父进程while (1) {sleep(2);//每2秒发送一次信号kill(child_pid, SIGUSR1);// 向子进程发送信号printf("Sent SlGUSR1 to child process (PID: %d)\n", child_pid);}return 0;
}
⑤ 硬件异常产生信号

发生硬件异常时,它被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除零的指令CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号,并将该信号发送给进程。

总之,使用信号的两个主要目的:
① 让进程知道已经发生了一个特定的事件。
② 强迫进程执行它自己代码中的信号处理程序,
注意,并不是系统中所有进程都可以向其他进程发送信号,只有核心和超级用户可以。

普通进程只可以向拥有相同uid(用户标识号)和gid(组标识号)或者在相同进程组中的进程发送信号。

当信号产生时,内核将进程taskstruct中的信号相应标志位设置为1,表明产生了该信号。

系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号。
信号产生后并不马上送给进程,它必须等待直到进程再一次运行时才交给它。
每当进程从系统调用中退出时,内核会检查它的signal和blocked字段(位图),查看是否有需要发送的非屏蔽信号,若有则立即发送信号。

如果信号的处理被设置为缺省,则系统内核将会处理该信号,否则会执行用户提供的信号处理程序。

【练习1】信号是用户按下Ctrl+C时默认发送给前台进程组的信号?
A. ‘SIGKILL’
B.'SIGSTOP’
C.‘SIGINT’
D.‘SIGTERM’
答案:C

【练习2】哪个信号用于终止进程,并且不能被进程捕获或忽路?
A. 'SIGKILL`
B.“SIGBUS”
C. 'SIGINT
D.'SIGTERM
答案:A

【练习3】在信号的进程通信机制中,以下哪个说法是正确的?
A.对于任意的信号都可以忽略
B.对于任意的信号都可以捕获
C.系统中所有进程都可以向其他进程发送信号
D.系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号
答案:D


http://www.mrgr.cn/news/79700.html

相关文章:

  • 新能源汽车 “能量侠”:移动充电机器人开启便捷补电新征程
  • 基础学习:(5)不同卷积:transposed convolution,deconvolution,dilated convolution
  • python 基于 docx 文件模板生成 docx 或 PDF 文件
  • Spring Boot 进阶指南:深入核心与实战技巧
  • 暴雨首发 Turin平台服务器亮相中国解决方案AMD峰会巡展
  • Python轻松获取抖音视频播放量
  • 手里有病理切片+单细胞测序的数据,如何开展医工交叉的研究?
  • 【CSS in Depth 2 精译_073】第 12 章 CSS 排版与间距概述 + 12.1 间距设置(中):对 CSS 行高的深入思考
  • vue中父组件接收子组件的多个参数的方法:$emit或事件总线
  • Vue框架入门
  • 解决前后端分离跨域产生的session丢失问题
  • 一个直接看央视频道的软件,可直接安装到TV
  • DMA代码部分
  • 计算机网络基础知识
  • 基于python+django+vue的购物商城系统
  • 雪花算法详解
  • 正则表达式的高级方法
  • STL之空间配置器allocator
  • 正则化:机器学习中的泛化利器
  • webrtc-java:引领Java进入实时通信新时代
  • 线上常见问题案例及排查工具
  • DevOps持续集成
  • STM32-C语言基础知识
  • 力扣HOT 100(图)
  • 多人聊天室 NIO模型实现
  • 1.1.Flink的项目初始化和Hello-world