💬之前学习的进程间通信的管道、共享内存等机制,它们主要用于进程间的数据传输。但有没有一种更轻量级的通信方式,可以让一个进程"通知"另一个进程发生了某个事件呢?答案是信号!信号是Linux中最古老、最经典的进程间通信方式,也是理解操作系统运行机制的重要窗口。
文章目录
- 信号的概念
- 信号产生的方式:
- 1. 键盘产生信号
- 2. 系统调用(也可以向目标进程发送信号)
- 3. 通过异常产生信号(硬件异常)
- 4. 软件条件
- 简单快速理解系统闹钟
信号的概念
首先,我们要明确:信号和信号量毫无关系。
接下来的顺序:信号的产生(4个方式)、保存(3张表)、处理
信号产生的方式:
1. 键盘产生信号
信号编号确实是用宏定义的,比如#define SIGINT 2
进程收到信号之后,在合适的时候会处理,有3种处理方式:
1.默认的处理动作;
2.自定义信号处理动作;又叫做信号捕捉
3.忽略处理大部分进程收到信号都会默认终止自身(即相当一部分进程都会采用默认处理的动作:让自己终止,执行信号),只有少数信号默认动作是忽略、暂停、继续、转储核心等。
可以使用该函数,更改默认处理动作
这一个过程又叫做:自定义捕捉
- 前台进程&后台进程
查看所有的后台进程:
jobs(可以看到后台进程的编号)把(后台特定进程)提到(前台):
fg 任务号一个进程一旦被
ctrl + z,就被暂停了,提到后台让暂停的后台进程恢复运行:
bg 任务号1~31信号产生之后,并不是立即处理,所以进程需要把信号记录下来。但是记录到哪里?如何记录?发送信号的本质又是什么?
该信号是发给进程的,所以会记录到PCB描述结构体(
struct task_struct{})中,用整数unsigned int sigs的位图结构来记录信号,毕竟信号不止一个
struct task_struct{}是操作系统内的数据结构对象,只能由操作系统来修改位图(本质是修改内核的数据),所以发送信号(修改位图),只能由OS发送!!
为了能够支撑我们向目标进程发送信号,OS需要提供发送信号的系统调用(kill就是C语言写的程序,底层调用了系统调用的接口,来完成发送信号的任务)
发送信号 = 内核修改目标进程的信号位图(发送信号的本质:其实是让OS修改位图)
流程:
1.内核找到目标进程
2.把进程信号位图里对应比特位 置为1
3.进程下次从内核态切回用户态时,扫描位图、执行信号处理函数
- 信号 vs 通信IPC
通信是进程间的通信,传递数据:在进程和进程之间,通知信息:从一个用户到另一个用户
信号机制本质是OS和进程之间的关系,真正的写数据交互是OS给目标进程写的,写数据交给写到数据结构里(不是缓冲区)
信号是人通过OS给进程发信号。IPC是进程和进程之间。
广义来说,信号也可以理解为通信范畴,信号也是某种事件的通知机制(不是以传递数据为目的)
- 有一个信号,禁止被自定义捕捉:signal kill,9号和19号信号,无法被自定义捕捉
- 如果除了kill,剩下都被自定义捕捉,杀掉进程,首先知道进程的id(ps ajx | grep 可执行程序名字),然后再kill -9 pid
2. 系统调用(也可以向目标进程发送信号)
用户进程通过系统调用请求内核代为发送信号
给任意进程发送任意信号:
kill)
接口:int kill(pid_t pid, int sig);给指定 PID 的进程发送信号系统调用(自己给自己发信号:
raise
接口:int raise(int sig);raise(sig);等价于kill(getpid(), sig);
调用内核接口,修改当前进程自身的信号位图,对应信号位直接置 1。abort()是强制终止当前进程的函数,它会发送 6 号信号:SIGABRT给当前进程。执行时会先撤销该信号所有自定义捕捉函数、恢复为默认处理方式,要求进程必须处理这个信号,SIGABRT 不能被忽略!不能被阻塞!必须处理!
3. 通过异常产生信号(硬件异常)
- 异常 ->崩掉的常见情况:除零,野指针
- 一旦程序(运行就是进程)产生异常,发生错误,进程就会收到信号,进而崩掉
- 一个进程收到信号的本质:是OS修改进程PCB中的位图结构,所以发信号永远是OS(代码犯错,OS发)
信号,都是由操作系统发送的。(程序出错误了—>OS发现进程出错了—>出错的类型—>根据类型发送信号)
还有一个问题,OS如何发现进程出错?
以除零为例,除零操作在CPU上进行,CPU上有各种寄存器,有一种状态寄存器(标志寄存器EFLAGS),是32/64个比特位,其有一个比特位是用来记录,CPU在当前计算时,数据是否溢出那OS怎么知道计算出问题了?
CPU是硬件,OS是软硬件资源的管理者,它能识别到硬件出错,然后发现计算是溢出的。而寄存器保存的是进程上下文以及struct_task,然后识别到哪一个进程出错,然后发现你是数据溢出,转而给进程发送8号信号那OS如何发现野指针这个错误的呢?
拿着野指针访问0号地址,但是在页表中并没有对它的映射关系,无法映射到内存当中,CPU拿到的都是虚拟地址,CPU中存在一个CR3寄存器,保存当前进程对应的页表的物理地址,在CPU中集成了一种硬件单元MMU。
寻址:将虚拟地址交给MMU,讲CR3里的内容也交给MMU,接着做地址转换。如果转化失败,MMU硬件报错,OS作为软硬件资源的管理者,给当前进程发送11号信号
4. 软件条件
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号(默认处理动作是终止当前进程,如果显式设置了处理函数,内核才会转而执行你写的回调函数;不设置就直接忽略)
unsignedintalarm(unsignedintseconds);- 有一种软件条件是alarm系统调用(闹钟超时之后,向目标进程发送信号的方式,成为软件条件)
闹钟
alarm()设定秒数,时间到内核主动发送SIGALRM信号,这种靠软件计时触发发信号的形式,就叫做软件条件产生信号。
- 返回值:是0或者是以前设定的闹钟时间还余下的秒数
- 如果seconds值为0,表示取消以前设定的闹钟,
函数的返回值仍然是以前设定的闹钟时间还余下的秒数
两个进程基于管道进行通信,写端在写,但读端关闭,再写进程就被OS终止了。管道是文件,是缓冲区,它们是软件概念,当软件条件不具备,OS会发送SIGPIPE信号
简单快速理解系统闹钟
- 进程是被OS调度的,那谁调度OS呢?
外部刺激让OS不断去运行,拿掉外部刺激OS就会一直pause暂停
进程由操作系统调度运行,操作系统本身是陷入暂停停滞状态(pause),依靠硬件时钟中断这个外部刺激持续运转,触发时钟中断才会去执行任务。没有任务,则pause暂停
系统中每个进程都能调用alarm设置专属闹钟,操作系统遵循先描述、后组织的内核管理思想,用专属内核数据结构来描述闹钟信息,再统一排队组织管理,创建闹钟本质就是在内核中新建对应的闹钟结构体对象。
- 时钟带来的中断刺激节奏固定,进程闹钟设定的时长、进程调度使用的时间片,底层本质都是内核维护的软件计数器,依靠数值递减完成计时。(刺激固定,时间固定,时间片本质是一个计数器)
系统闹钟从计时管理到超时判定,全程都由内核软件逻辑实现,属于纯软件层面的计时条件;当计时计数器归零、闹钟达到设定时长触发超时条件后,操作系统主动向对应进程发送SIGALRM闹钟信号。
这种依靠软件计时条件满足而产生、触发发送的信号,就定义为软件条件产生信号。