第四种,软件也可以产生信号:
比如说之前的管道,读端关闭,写端也会关闭,然后导致这个软件触发条件,发生信号。
在Linux下有一个叫定时器的软件,可以设定一个闹钟,如果时间到了,会给当前进程发送编号为14的信号。(闹钟只会响一次)
参数是按照秒为单位设置一个信号。
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <cstdlib> using namespace std; int main(int argc, char *argv[]) { int count = 0; alarm(1); while(true) { count++; cout << count << endl; } return 0; }
这段代码的功能是统计1S左右能让我们的计算机数据累加多少次。
其实正常来说CPU不会这么慢,可以改进一下代码:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <cstdlib> using namespace std; int count = 0; void catchSig(int sig) { cout << count <<endl; exit(1); } int main() { signal(14, catchSig); alarm(1); while(true) { count++; } return 0; }
那么为什么差距这么大呢?
因为打印是一种外设输出,访问外设的时候是很慢的,需要大量的时间,第一段代码一直在通过外设进行打印,所以很慢,第二段之后结束的时候才会通过外设打印。
如果是服务器还要经过网络IO,会更慢。
”闹钟“其实就是用软件实现的:
任何一个进程都可以通过alarm系统调用在内核中设计闹钟,OS内可能会存在很多的闹钟,OS也一定要管理这些闹钟,先描述再组织。
用struct alarm类型的对象去描述各个进程的闹钟数据:
struct alarm { uint64_t when;//未来的超时时间 int type;//闹钟类型,一次性的还是周期性的 tasl_struct *p;//和哪个进程相关 struct alarm *next; }
然后OS用特定堆的数据结构方式管理,struct alarm *head
OS会周期性的检测这些闹钟,如果发现超时了OS就会给对应的进程发SIGALARM信号。
上面所说的所有信号的产生,都是由OS来执行,但是信号不一定立即处理,那么是什么时候被处理的呢?
进程退出时——核心转储
先来看一段代码:
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <cstdlib> using namespace std; int main() { while(true) { int arr[10]; arr[100] = 106;//这里数组是越界的 } return 0; }
这里并没有显示越界的报错。
改成一千也没报错,但是i改成一万就报错了
这里是什么情况呢?因为开辟的栈区是合法的,只有到了为开辟的栈区才会进行报错。
像这种,Term这种是正常退出,而Core是退出之后还要做其他工作。
在云服务器上,默认如果进程是core退出的暂时看不到现象,想看到需要打开一个选项:
第一个core file size是0,这是云服务器默认的。
这里设置一下。
然后再次运行上面的段错误的代码:
并且还多出来了一个文件。
第一个后面多出来的core dumped就是核心转储操作,多出来的文件就是核心转储的内容。
多出来的文件.后缀是引起core问题进程的pid。
核心转储:当进程出现异常是hi后,我们将进程的对应时刻,在内存中的有效数据转储到磁盘中。(二进制临时文件)
作用就是为了更方便调试:
这里直接就帮助我们找到了问题。(这里叫做事后调试)
core-file core.xxx
信号的保存
有一个问题,如果所有信号都被捕捉了,那么这个信号是不是就无法停下来了呢?
#include <iostream> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <cstdlib> using namespace std; void catchSig(int signo) { cout << "信号拦截:" << signo << endl; } int main() { for(int signo = 1; signo <= 31; signo++) { signal(signo,catchSig); } while(true) { cout << "运行中" << getpid() << endl; sleep(1); } return 0; }
最后用了kill -9才将这个进程杀掉。
OS中9号信号是无法进行捕捉的。
信号其它相关概念
实际执行信号处理的动作称为信号递达。
信号从生产到递达之间的状态称为信号未决(Pending)。
进程可以选择阻塞(Block)某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
并且,PCB中还有一个信号的函数指针数组,里面都是处理信号的方法。
我们使用的信号捕捉也只是将该数组中对应信号的方法给替换了,也就是替换了函数地址。
也就是说,如果要给信号产生,不妨碍他可以先被阻塞。
信号如何实现捕捉的
之前说信号只会在合适的时候才会被处理,不然就一直被保存在pending位图中。
从内核态返回用户态的时候,进行信号的处理。
我们平时是用户态,但是难免会去通过OS访问系统自身的资源和硬件资源,这个时候就要去进行系统调用才能完成:
也就是说,系统调用还要进行身份切换,会比调用用户层本身的方法慢。
所以避免频繁的使用系统调用。
并且,CPU中由寄存器会存储以下相关数据。
那么,一个进程怎么跑到OS中执行方法呢?
因为进程的独立性,所以每个进程都有一个用户级页表。
在开机的时候,操作系统要加载到内存中,因为操作系统只有一份,在内存中也只有一份,相对应的内核级页表也只有一份就够了。
CPU中也会有一个寄存器储存内核级页表,每个进程都会通过内核空间访问内核页表,然后去找到物理内存中的操作系统的代码和数据。
也就是说,进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了。
如果想访问内核级数据,CPU的CR3要变成0才有权限。
那么是怎么进行切换的呢?是系统调用接口的起始位置会帮助我们进行切换。
也就会说前半段代码可能是用户态跑的,但是这里突然就变成内核态跑。
在Linux中,有一个叫Int 80 —— 陷入内核。
这个是汇编指令,这个就是修改当前进程在寄存器中CR3的身份状态。