目录
1.键盘信号
2.显示器回显过程
3.信号产生方式
4.总结
键盘信号
键盘数据是如何输入给内核的,crtl+c是怎么变成信号的?
键盘被按下,肯定是os先知道,os怎么知道键盘上有数据呢?
c让操作系统每隔一段时间去轮询每个硬件有没有数据消耗很大。所以cpu有很多针脚高低电平,每个针脚都对应了硬件的中断号,当硬件有数据时,通过中断单元给cpu发送硬件中断,充电和放电,根据中断号就知道哪个硬件有数据了,然后os将内容拷贝到对应文件的缓冲区,传给用户缓冲区。在内存操作系统部分,有一个中断向量表,里面保存了硬件方法的地址,调用这个方法就可以将键盘的数据读取到内存。拷贝完成后再次给cpu发送中断,继续之前的执行
os在拿到数据后会先判断,如果是控制类,如crtl+c,会转换为2号信号,不会拷贝到缓冲区
信号是模仿硬件中断的一种软件中断的方式
显示器回显的过程
显示器和键盘是两个文件,有自己对应的缓冲区,键盘输入后将内容拷贝到显示器的缓冲区就是回显。所以当后台运行时,输入命令后,虽然显示器显示是乱的,但仍然可以运行命令,就是因为内容是键盘缓冲区里,并没有在显示器缓冲区,只是回显到了显示器
信号产生方式
1.键盘组合键
crtl+c默认终止进程,除过这个快捷键还有crtl+\是3号信号
捕捉3号信号,运行验证:
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void myhandler(int signal)
{
cout << "get a singal" << signal << endl;
}
int main()
{
//signal(SIGINT, myhandler);
signal(3, myhandler);
while (true)
{
//printf("hello world\n");
sleep(1);
}
}
kill3号信号也是同样的结果
crtl+z是19号信号,捕捉19号信号发现不成功,crtl+z仍然会暂停进程,kill发送19号信号也暂停进程
不是所有的信号都可以被signal捕捉的,比如19
可以循环捕捉31个信号,然后kill从1发送到31,看看哪些信号不能捕获
结果9和19号新号不能捕捉
9和19号新号是杀死和暂停进程,为了防止恶意程序和重要工作运行正常不能被捕捉
SIGQUIT的默认处理动作是终止进程并且Core Dump
Core Dump
什么是Core Dump
当一个进程要异步终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫core dump。进程异步终止通常是因为有bug,如非法访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做post-morterm debug(事后调试)。一个进程允许产生多大的core文件取决于进程的resource limit(这个信息保存在pcb中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件,首先ulimit命令改变shell进程的resource limit允许core文件最大为1024k,$ulimit -c 1024
7章man手册
core dump 和term
ign是忽略,cont是继续。term和coredump都是终止,但有区别
这个标记之前在子进程status状态中说过,是第8位。生成子进程父进程等待获取一下coredump
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
void work()
{
cout << "print log" << endl;
}
void myhandler(int signal)
{
work();
//cout << "get a singal" << signal << endl;
int n = alarm(5);
cout << "sencond: " << n << endl;
}
int main()
{
//signal(SIGINT, myhandler);
//signal(3, myhandler);
// signal(SIGALRM, myhandler);
// int n = alarm(50);
// while (true)
// {
// printf("%d\n", getpid());
// sleep(1);
// }
pid_t id = fork();
if (id == 0)
{
while (true)
{
printf("我是子进程: %d\n", getpid());
sleep(1);
}
}
int status;
pid_t ret = waitpid(id, &status, 0);
printf("core dump: %d\n", status >> 7 & 1);
}
云服务器的core功能被关闭的,ulimit -a可以查看所有,-c查看core的开关
ulimit -c 1024,设置大小为1024,开启功能,关闭设置为0
运行一个除0错误的代码,并生成一个core文件
打开系统的core dump功能,一旦出现异常,os会将进程在内存运行信息,转储到进程的当前目录,形成core.pid文件:核心转储(core dump)。这个可以让运行时错误,保存出现问题的在哪一行,先运行,再core-file,就是事后调试
运行gdb,直接输入core文件就可以定位打印错误信息
core-file [core文件]
core dump方便事后调试
为什么云服务器关掉这个功能,因为生成的core文件比较大,在一个比较大型的项目中,如果一出现这些错误就生成core文件,可能会导致生成特别多文件,占用很多资源,甚至导致运行被停止
2. kill
kill -sign pid
3. 系统调用
- kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
自己实现kill命令
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void usage(string proc)
{
cout << "usage:\n\t" << proc << "signum pid\n\n";
}
void myhandler(int signal)
{
cout << "get a singal" << signal << endl;
}
//./kill 3 pid
int main(int args, char* argv[])
{
if (args != 3)
{
usage(argv[0]);
return 1;
}
int sign = stoi(argv[2]);
pid_t pid = stoi(argv[1]);
int n = kill(sign, pid);
if (n < 0)
{
perror("kill");
return 2;
}
}
raise命令给当前进程发送指定信号
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
- abort可以给当前进程发送6号信号
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
这个函数内部做了封装,调用后还会结束进程。如果是kill发送这种形式发6号信号,并不会结束进程
无论信号如何产生,一定是os发送的,因为os是进程的管理者
4. 异常
除0错误和野指针错误,这两个分别是8号和11号信号,发生这个错误会报告崩溃并退出。当对这两个信号捕捉,行为会改为一直循环
os如何知道异常
除0异常
cpu中的内容是进程的上下文数据,当cpu执行到除0错误的时候,会将cpu中的状态寄存器里的溢出标志位改为1,关于cpu的设计在cpu开发手册会有说明。cpu也是硬件,os是硬件的管理者,os发现这个这个标志就知道这个计算错误,然后给进程发送信号
野指针异常
cpu中继承了MMU(内存管理单元),这个可以进行虚拟地址和物理地址的页表转换,野指针就是地址转换失败的情况,cpu会将结果和失败的地址保存起来。os和上面一样发现这个错误就发送信号
为什么重复输出
当出现这个错误的时候没有结束进程,就会继续执行,当cpu再次加载进程的时候,发生的错误没有修复,又会继续发送信号,重复这个动作
硬件异常一方面没有权限去修复,是用户自己的数据。另一方面因为异常了,这个计算结果已经不值得信任。所以进程最好终止。信号和捕捉并不能解决这个异常,只是告诉用户明白出现的情况,可以统一处理
软件条件信号
异常不只可以由硬件产生,软件也可以。如管道中如果读取方关闭,就会发生异常,发送13号SIGPIPE信号并关闭写端。os检测条件不满足就会出现异常,可以这样产生闹钟
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。
这个函数到达设定延迟后会发送SIGALRM信号
返回值是0或者以前设定的闹钟时间还剩下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟后响,20分钟后被吵醒了,还想多睡一会,重新设定闹钟为15分钟后响,这时以前设定的闹钟剩下的时间就是10分钟。如果参数为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还剩下的秒数
捕获闹钟信号验证:
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void myhandler(int signal)
{
cout << "get a singal" << signal << endl;
}
int main()
{
//signal(SIGINT, myhandler);
//signal(3, myhandler);
signal(SIGALRM, myhandler);
int n = alarm(5);
while (true)
{
printf("%d\n", getpid());
sleep(1);
}
}
闹钟不是异常,所以响应一次后就会结束,想让闹钟每隔5s响一次,可以重新设置
void myhandler(int signal)
{
cout << "get a singal" << signal << endl;
alarm(5);
}
闹钟可以设置一个定时任务
void work()
{
cout << "print log" << endl;
}
void myhandler(int signal)
{
work();
cout << "get a singal" << signal << endl;
alarm(5);
}
将闹钟时间设长,运行的时候kill发送信号,打印出返回值
os中一定存在大量的闹钟,将这些结构用组织管理起来,用系统当前时间和这些时间作对比,就能判断是否超时。超时了就发送对应的信号。这种用堆来管理最好,优先处理超时时间最近的,只要堆顶没超时,堆所有都没超时,就不需要遍历所有数据
总结
上面所说的所有信号产生,最终都要由os来执行,因为os是进程的管理者
信号可以是立即处理,也可以在合适的时候
信号如果不是被立即处理,那么信号是否需要暂时被记录下来,记录在哪里?怎么记录
一个进程在没有收到信号的时候,能否知道,自己应该对合法信号如何处理?
如何理解os向进程发送信号?能否描述一下完整的发送处理过程?