苏格网站建设,建设银行网站可以更改个人电话,网站建设添加资料,淘宝一个关键词要刷多久进程信号一、信号概念1.1 信号理解二、产生信号2.1 通过键盘产生信号2.2 捕捉信号自定义signal2.3 系统调用接口产生信号2.3.1 向任意进程发送任意信号kill2.3.2 给自己发送任意信号raise2.3.3 给自己发送指定信号abort2.3.4 理解2.4 硬件异常产生信号2.4.1 除0异常2.4.2 野指针…
进程信号一、信号概念1.1 信号理解二、产生信号2.1 通过键盘产生信号2.2 捕捉信号自定义signal2.3 系统调用接口产生信号2.3.1 向任意进程发送任意信号kill2.3.2 给自己发送任意信号raise2.3.3 给自己发送指定信号abort2.3.4 理解2.4 硬件异常产生信号2.4.1 除0异常2.4.2 野指针异常2.4.3 总结2.5 软件条件产生信号2.5.1 定时器软件条件alarm2.5.2 alarm的深层理解2.6 核心转储Core Dump一、信号概念
首先要知道查看信号的指令kill -l 通过观察发现没有0和32和33号信号只有1 ~ 31 34 ~ 64的信号。我们把 【1 ~ 31】叫做普通信号 【34 ~ 64】叫做实时信号
1.1 信号理解
在日常生活中有很多的信号例如红路灯、裁判哨声、闹钟这些都是给我们人类看的当这些场景触发的时候我们人类立马就知道要做什么并且产生行动。
而我们为什么能识别这些信号呢 我们对特定事件的反应是被教育的结果本质是我们记住了。
还有一种情况当信号传来的时候我们可能正在做更重要的事情所以不一定会立马处理信号此时信号的产生和我们正在做的事情称为异步。 我们把信号传递过来到处理之前的这段时间称为时间窗口。在时间窗口我们必须得记住这个信号。
在我们处理信号的时候我们可以有不同的处理方式比方说我们早上听到闹钟响起会直接起床这里叫做默认动作而听到闹钟后先做十个俯卧撑再起床这叫做自定义动作当然我们也可以不理会闹钟这叫做忽略动作。
把概念迁移到进程中 1️⃣ 进程能认识信号并产生动作是因为程序员编码完成的。 2️⃣ 当进程收到信号进程可能在执行更重要的代码所以信号不一定被立即处理。 所以进程要有对信号的保存能力。 3️⃣ 进程在处理信号的时候一般有三种动作默认、自定义、忽略有个专业名词叫信号被捕捉。 那么信号是怎么被捕捉的呢 信号发给进程而进程需要保存到PCB中那么如何保存呢 是否收到信号具有原子性只有两太而我们知道普通信号是1 ~ 31所以我们可以在PCB中创建一个unsigned int的变量有32个比特位刚好用这些比特位来标记接收的信号。比特位的位置代表信号的编号。0表示没有1表示有。 所以发送信号的本质修改PCB中的信号位图。
而只有操作系统才能修改PCB发信号本质就是给操作系统发信号那么操作系统就必须要提供发送信号、处理信号的相关调用接口我们以前的kill指令就是调用了底层接口。
二、产生信号
2.1 通过键盘产生信号
ctrl c是一个组合键OS将它解释成3号信号SIGQUIT 我们知道每个信号有三种处理动作那么怎么查看信号的默认动作是什么呢 ctrl \是一个组合键OS将它解释成2号信号SIGINT
指令man 7 signal 可以看到二号信号的动作是Term终止描述是Interrupt from keyboard从键盘中断
2.2 捕捉信号自定义signal
#include signal.h
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);RETURN VALUE
signal() returns the previous value of the signal handler, or SIG_ERR on error.
In the event of an error, errno is set to indicate thecause.参数说明 signum指定的信号。 handler设置自定义动作就是一个回调函数函数内我们可以自定义我们想要的动作。
void handler(int sig)
{std::cout 进程捕捉到信号编号是 sig std::endl;
}int main()
{signal(2, handler);while(true){std::cout in service: getpid() std::endl;sleep(1);}return 0;
}这里要注意是signal函数的调用不是handler的调用。这个函数仅仅是对2号信号的捕捉并不代表被调用了。只有收到对应信号才会被调用。 可以看到发送2号信号并不能导致进程被终止了。
这里有个问题如果我们对所有的信号都进行了信号捕捉那我们是不是就写了一个不会被异常终止或者用户杀掉的进程呢我们通过代码来验证一下
void Catchsig(int sig)
{std::cout 捕捉到了一个信号: sig pid: getpid() std::endl;
}int main()
{for(int i 1; i 31; i)signal(i, Catchsig);while(1) sleep(1);return 0;
}操作系统的设计者也考虑到了上述的情况所以就让 9 号信号无法被捕捉9 号信号是管理员信号。
2.3 系统调用接口产生信号
2.3.1 向任意进程发送任意信号kill
#include sys/types.h
#include signal.hint kill(pid_t pid, int sig);RETURN VALUE
On success (at least one signal was sent), zero is returned.
On error, -1 is returned, and errno is set appropriately.参数说明 pid目标进程的pid。 sig向目标进程发送指定信号。
所以我们可以自己写一个kill的进程。
// mykill.cc
void Usage(const std::string proc)
{std::cout \nerror, format: proc pid sig std::endl;
}int main(int argc, char* argv[])
{if(argc ! 3){Usage(argv[0]);exit(1);}pid_t pid atoi(argv[1]);int sig atoi(argv[2]);kill(pid, sig);return 0;
}再写一个永远运行的进程让mykill进程来杀死它。
// myproc.cc
int main()
{while(true){std::cout please kill me, my pid: getpid() std::endl;sleep(1);}return 0;
}2.3.2 给自己发送任意信号raise
#include signal.hint raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.sig就是发送的信号。
int main(int argc, char* argv[])
{int cnt 0;while(cnt 10){std::cout cnt: cnt std::endl;sleep(1);if(cnt 5){raise(9);}}return 0;
}其实这里raise也可以写成kill(getpid(), 9)
2.3.3 给自己发送指定信号abort
#include stdlib.hvoid abort(void);int main(int argc, char* argv[])
{int cnt 0;while(cnt 10){std::cout cnt: cnt std::endl;sleep(1);if(cnt 5){abort();}}return 0;
}而发送的指定信号就是6号SIGABRT。 所以这里abort也可以自己用kill封装kill(getpid(), 6)
2.3.4 理解
我们可以看到进程收到的大部分信号默认处理动作都是终止进程。 信号的不同代表了不同的事件但是它们的处理动作可以一样。
2.4 硬件异常产生信号
2.4.1 除0异常
信号的产生不一定需要用户手动发送。
int main(int argc, char* argv[])
{while(true){std::cout in service std::endl;sleep(1);int a 1;a / 0;}return 0;
}这里为什么/0会导致进程终止呢 因为进程会收到来自操作系统的8号信号SIGFPE。
我们可以用前面学的捕捉信号进行验证
void handler(int sig)
{std::cout 捕获到信号 sig std::endl;sleep(1);
}int main(int argc, char* argv[])
{signal(8, handler);while(true){std::cout in service std::endl;sleep(1);int a 1;a / 0;}return 0;
}这次我们把/0放到循环前 可以看到这里还是循环打印好像一直在调用捕获函数。 这里就要先知道操作系统是如何得知要给进程发送八号信号的呢怎么知道的/0 这里1/0会被放进CPU中的寄存器中0相当于无穷小的数字这样就会导致CPU的状态寄存器中的溢出标记由0变为1。这样就发生了CPU的运算异常操作系统就会知道操作系统是软硬件的管理者然后向目标进程发送8号信号。而收到信号进程不一定退出没有退出说明还会被继续调度。而寄存器的内容属于当前进程上下文信息但是进程没有能力把状态标识符置为0所以进程切换的时候就有无数次的状态寄存器被保存和恢复上下文信息每次恢复就会发送信号。导致捕获函数一直被调度。 2.4.2 野指针异常
int main(int argc, char* argv[])
{while(true){std::cout in service std::endl;sleep(1);int *ptr nullptr;*ptr 2;}return 0;
}这里为什么空指针会导致进程终止呢 因为进程会收到来自操作系统的11号信号SIGSEGV。
利用signal函数证明
void handler(int sig)
{std::cout 捕获到信号 sig std::endl;sleep(1);
}int main(int argc, char* argv[])
{signal(11, handler);while(true){std::cout in service std::endl;sleep(1);int *ptr nullptr;*ptr 2;}return 0;
}那么操作系统是如何知道发生了野指针情况呢 我们知道指针本质上是个虚拟地址而我们知道虚拟地址需要转化成物理地址通过页表MMUMMU是集成在CPU中的硬件通过访问通过页表的内容形成物理地址再访问物理地址。而我们解引用空指针MMU就会发生异常然后被操作系统得知然后发送信号给进程。 2.4.3 总结
大部分信号会导致进程退出我们需要捕获这个异常因为异常的不同代表不同的原因导致的进而让我们能够追溯原因让我们能够反向定位问题。比如说我们收到的信号是段错误我们就会想到可能是野指针收到浮点数溢出报错就会想到可能是除0错误。
2.5 软件条件产生信号
我们以前学过管道【linux】进程间通信——管道通信 当两个进程正在利用管道进行读写此时把读端关闭操作系统就会终止掉写进程发送SIGPIPE信号。这种情况称为软件条件产生信号。
2.5.1 定时器软件条件alarm
#include unistd.hunsigned int alarm(unsigned int seconds);调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号(14), 该信号的默认处理动作是终止当前进程。 int main(int argc, char* argv[])
{alarm(1);int cnt 0;while(true){std::cout cnt std::endl;}return 0;
}这个进程的目的就是统计1s的时间内计算机能将数据叠加多少次。
而如果我们这么写
int cnt 0;void handler(int sig)
{std::cout 捕获到信号 sig std::endl;std::cout cnt: cnt std::endl;
}int main(int argc, char* argv[])
{signal(14, handler);alarm(1);while(true){cnt;}return 0;
}从这里就可以看到IO跟不IO的效率差距相当大。
而只打印了一次说明是收到了一个SIGALRM信号闹钟响过一次就不再响了。 那如果我们想让它一直打印呢 相当于在handler内部又要调用handler。这样就类似于sleep(1)
unsigned int alarm(unsigned int seconds);当然alarm也可能提前响起。比方说有可能手动发送SIGALRM他就会返回剩余多少时间。当我们把seconds设置为0表示取消闹钟。
2.5.2 alarm的深层理解
我们知道每个进程都可能通过alarm接口设置闹钟所以可能会存在很多闹钟那么操作系统一定要管理起来它们。 先用一个结构体描述每个闹钟其中包含各种属性闹钟还有多久结束时间戳、闹钟是一次性的还是周期性的、闹钟跟哪个进程相关、链接下一个闹钟的指针…… 然后我们可以用数据结构把这些数据连接起来。 接下来操作系统会周期性的检查这些闹钟当前时间戳和结构体中的时间戳进行比较如果超过了说明超时了操作系统就会发送SIGALRM给该进程。
为了方便检查是否超时可以利用堆结构来管理。
2.6 核心转储Core Dump
核心转储 当进程出现异常的时候我们可以将该进程在对应时刻的内容数据保存到磁盘上文件名通常是 core。 这里的Term和Core都表示进程退出Trem表示正常结束操作系统不会做额外的工作如果是Core退出我们暂时看不到明显的现象如果想要看到我们可以打开一个选项ulimit -a可以看到操作系统给用户所设置的资源上限 可以看到第一行core file size的大小为0因为云服务器默认关闭了core file这个选项。 如果我们想修改我们就可以用后边的参数进行修改-c。ulimit -c 打开了以后我们继续解引用空指针
可以发现比以前多了一点内容。在查看当前目录 多了一个core文件 我们把core dumped叫做核心转储core文件后面的数字就是问题进程的pid。
那么为什么要有核心转储 我们需要知道程序为什么崩溃在哪崩溃而核心转储就是为了支持我们进行调试。 那么如何调试呢 第一步先编译的时候带上-g选项 第二步使用gdb调试 第三步直接输入core-file core.17633 从结果可以看出代码终止的原因是收到了11号信号引发了段错误。在mykill.cc的17行。 我们把这种处理错误的方法叫做事后调试
总结一下当程序出现异常我们先确定是几号信号然后man 7 signal查看是core还是Trem如果是core直接打开核心转储然后gdb调试直接定位错误。