- 信号的旧识引入
- 信号引入
- signal调用
- 系统调用向目标进程发送信号
- 模拟实现一个kill命令
- raise给自己发送任意信号
- abort给自己发送指定信号(6)SIGABRT
- 硬件异常产生信号
- 除0异常
- 野指针访问异常
- 软件条件产生信号
- 拓展
- 总结思考
- 进程退出时核心转储问题
- 小实验
信号的旧识引入
kill -l
是一个在Linux
和Unix
系统中使用的命令,用于列出可用的信号列表。
在Linux和Unix系统中,进程可以通过发送信号来与其他进程或操作系统交互。kill 命令可以向指定的进程发送一个特定的信号,以便对其进行控制,例如终止进程或重新启动进程等。
kill -l 命令会列出可用的信号列表,每个信号都有一个唯一的数字编号和一个名称。输出结果通常是一个由数字和名称组成的列表,例如:
[AMY@VM-12-15-centos ~]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) 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
我们可以发现里面数字没有0、32 、33:
1~31
普通信号
34~64
实时信号
进程信号的认识:信号是给进程发的,比如kill -9 (pid)
信号相关总结:
- 进程是如何识别信号的?认识+动作
- 进程本身是被程序员编写的属性和逻辑的集合—程序员编码完成的
- 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理
- 进程本身必须要有对于信号的保存能力
- 进程在处理信号(信号被捕捉)的时候,一般有三种动作(默认,自定义,忽略)
如果一个信号是发给进程的,而进程要保存,那么应该保存在哪里? task struct(PCB)
如何保存呢?是否收到了指定的信号1~31
struct task_struct
{
...
unsigned int signal;
...
}
32位:
比特位的位置,代表信号编号。
比特位的内容,代表是否收到该信号,0没有
,1有
发送信号的本质:修改PCB中的信号位图
PCB
是内核维护的数据结构,PCB的管理者是OS
,那么谁有权力修改PCB的内容呢?是OS,所以无论我们学习多少种发送信号的方法,本质上都是通过OS向目标进程发送信号。那么OS一定需要提供发送信号的相关系统调用,我们也可以推测出kill
命令,底层肯定是调用了对应的系统调用
信号引入
我们常常使用CTRL+c
来终止一个正在运行的进程,本质上,这个CTRL+c
是一个组合键,OS将其解释成为2
号信号2) SIGINT
我们可以使用:
man 7 signal
原始POSIX.1-1990标准中描述的信号:
signal调用
man 2 signal
函数原型:
sighandler_t signal(int signum, sighandler_t handler);
案例使用:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "进程捕捉到了一个信号,信号编号是: " << signo << std::endl;
}
int main()
{
// 这里是signal函数的调用,并不是handler的调用
/// 仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
// 一般这个方法不会执行,除非收到对应的信号!
signal(2, handler);
while(true)
{
std::cout << "我是一个进程: " << getpid() << std::endl;
sleep(1);
}
}
直接执行我们并没有执行handler()
,因为没有传递这个信号
我们在执行的时候按CTRL+c
就会执行handler()
为什么我们现在的CTRL+c
不能够终止进程呢?是因为我们将默认动作改为自定义动作而去执行handler()
了,所以我们不能终止,需要终止我们在handler()
函数最后加上exit
就行。当然我们还可以使用kill -9 (pid)
去杀掉这个进程
系统调用向目标进程发送信号
模拟实现一个kill命令
先写了一个将来会一直运行的程序mytest.cpp
,用来进行后续的命令测试
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(true)
{
std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
sleep(1);
}
}
我们使用man手册查看kill的系统调用
然后我们实现读取键盘部分代码mysignal.cpp
:
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
static void Usage(const string &proc)
{
std::cout << "\nUsage: " << proc << " pid signo\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int n = kill(pid,signo);
if(n != 0)
{
perror("kill");
}
return 0;
}
从上述过程我们可得:我们使用自己的调用实现了kill命令
,我们平时使用的kill命令其实也就是封装的系统调用kill
kill()
可以向任意进程发送任意信号
raise给自己发送任意信号
int main(int argc, char* argv[])
{
int cnt=0;
while(true)
{
cnt++;
cout<<"cnt:"<<cnt<<endl;
if(cnt>=3)
{
raise(9);
}
}
return 0;
}
函数原型:
int raise(int sig);
只需要传入信号值,就可以直接发送该信号
abort给自己发送指定信号(6)SIGABRT
int cnt=0;
while(true)
{
cnt++;
cout<<"cnt:"<<cnt<<endl;
if(cnt>=3)
{
abort();
//raise(9);
}
}
前三个调用总结:
kill()
可以想任意进程发送任意信号raise()
给自己 发送 任意信号 等同:kill(getpid(), 任意信号)
abort()
给自己 发送 指定的信号SIGABRT 等同:kill(getpid(), SIGABRT)
关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程
信号的意义:信号的不同,代表不同的事件,但是对事件发生之后的处理动作可以一样!
硬件异常产生信号
除0异常
我们对一个常数进行除0运算:
我们捕捉这个信号:
void catchSig(int signo)
{
std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char* argv[])
{
signal(SIGFPE, catchSig);
while (true)
{
std::cout << "我在运行中...." << std::endl;
sleep(1);
int a = 10;
a /= 0;
}
return 0;
}
虽然我们将signal(SIGFPE, catchSig);
是写在循环之前,但是当我们进行除0以后,它还是会运行,这是因为:signal()
在这里是注册一种未来的方法,当某种条件成立后就自动调用这个注册好的方法
我们还发现这个捕捉到信号SIGFPE后,它就一直运行catchSig()
打印
除0异常解释:
由上述输出结果我们可以知道:收到信号不一定会引起进程退出,没有退出有可能还会被调度。而CPU内部的寄存器只有一份,但是寄存器里面的内容,属于当前进程上下文。一旦出现异常,我们有没有能力或者动作去修正这个问题呢?答案是没有,比如溢出标记位被置1了,我们有没有能力去更改为0呢?我们肯定不能,因为状态寄存器是由CPU自己维护的,用户没有办法也没有权力去更改。
当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1
野指针访问异常
while(true)
{
std::cout << "我在运行中...." << std::endl;
sleep(1);
int *p = nullptr;
*p=10;
}
void catchSig(int signo)
{
std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char *argv[])
{
signal(11, catchSig);
int *p = nullptr;
*p = 10;
while (true)
{
std::cout << "我在运行中...." << std::endl;
sleep(1);
}
return 0;
}
由上述的操作结果我们可知:野指针访问属于是(11) SIGSEGV
,且跟除0的捕捉结果是一样的,也是一直调用catchSig()
。
MMU位于CPU内部,在ARM32中,MMU主要完成虚拟地址到物理地址的映射,并且能够控制内存的访问权限,而页表是实现上述功能的主要手段。页表又分为一级页表、二级页表,在ARM64中甚至还有三级页表。上图这样画只是为了更形象。
while循环
的代码不会被使用因为:访问野指针并触发SIGSEGV
信号时,程序已经进入了异常状态。当catchSig函数
被调用时,异常状态并没有被完全处理,因此程序进入了一个不稳定的状态。一旦catchSig函数返回,程序会尝试继续执行之前的指令,但由于进程仍处于异常状态,又会再次触发SIGSEGV信号。这将导致catchSig函数被不断地调用。
软件条件产生信号
管道通信举例:
当读端关闭,写端却一直在写的情况时候,OS会发送(13) SIGPIPE
信号来让写端关闭,这中情况就是由软件条件触发产生信号。
定时器软件条件:
alarm()
:设定闹钟
int main(int argc, char *argv[])
{
alarm(1);
int cnt = 0;
while (true)
{
cout << "cnt:" << cnt << endl;
cnt++;
}
return 0;
}
输出结果:
我们更改一下代码:
int cnt = 0;
void catchSig(int signo)
{
std::cout << "获取到一个信号,信号编号是: " << signo << " cnt:" << cnt << std::endl;
}
int main(int argc, char *argv[])
{
signal(SIGALRM, catchSig);
alarm(1);
while (true)
{
cnt++;
}
我们首先可以的个结论:IO确实很慢,都是1秒钟,一直打印cnt的那个明显累加次数很少(我使用的是云服务器,这些运行的结果还要通过网络传递给我,所以会更慢,一般使用虚拟机会快很多)
我们这里跟之前的输出结果不一样,这里只打印了一次,说明alarm只发送一次信号
拓展
为什么你说设置“闹钟”是软件条件呢?
任意一个进程,都可以通过alarm
系统调用在内核中设置闹钟,OS内可能会存在着很多的闹钟,那么操作系统要不要管理这些闹钟呢?要→先描述再组织
举例:操作系统中有很多管理闹钟的方法比如堆等
描述:
struct alarm
{
uint64_t when; // 未来的超时时间int type
// 闹钟类型,一次性的,还是周期性
task_struct *p;
struct alarm *next;
}
组织:
OS会周期性的检测这些闹钟,如果curr_timestamp > alarm.when
,超时了OS发送SIGALARM -> alarm.p;
总结思考
关于信号发送的问题:
-
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者,也只有OS有权利去操作 -
信号的处理是否是立即处理的?
在合适的时候 -
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
是的,保存在PCB -
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
我们应该知道,比如红灯快要亮起,但还没有,此时我们知不知道红灯亮起的时候该怎么办?答案是知道。信号还没有产生但是我们应该知道信号产生该做什么处理,这个工作由程序员系统中代码默认体现。 -
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
就是OS直接修改目标进程中PCB存放信号的位图
进程退出时核心转储问题
同样都是越界,为什么右边的越界报错了?因为数组是只分配了10的空间,但是不代表整个函数的栈帧结构只有那么多,所以你即使越界了,但是你还是在有效栈区里所以就没报错,除非你访问了一个完全不属于你的空间,比如系统的某些空间,这时候OS就会识别出来。(不同编译器检查越界方式也可能不同,比如vs2019就是采用抽检)
Term正常结束,Core除了终止还要做其他工作
在云服务器上,默认如果进程是core退出的,我们暂时看不到明显的现象,如果想看到:
[AMY@VM-12-15-centos lesson_18]$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 7260
max locked memory (kbytes, -l) unlimited
max memory size (kbytes, -m) unlimited
open files (-n) 100001
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 7260
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
云服务器默认关闭了core file size选项:
如何打开这个选项呢?
我们现在还多了一个文件core.15199
(core dumped)核心转储
︰当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据,转储到磁盘中–核心转储!而core.15199
的15199
是引起core
问题的进程的pid
进程在运行时出异常,以前的终止就是直接结束,而现在打开了core选项
进程在终止时会多做一个工作,就是把进程中一些有效的二进制数据给dumped转储
到磁盘当中,这个就叫核心转储,形成的这个临时文件以core
命名,后缀为该进程的pid
为什么要有这个呢?
正常我们程序崩溃了,我们最想知道的是为什么崩溃?在哪里崩溃?而核心转储可方便我们调试(Linux调试我们需要带上 -g
选项)
在gdb
上下文之中,输入core-file core.(pid值)
Term不可以核心转储,Core可以核心转储
小实验
如果我们将所有的信号全部捕捉,那么还能不能杀死这个进程呢?
void catchSig(int signo)
{
std::cout << "获取到一个信号,信号编号是: " << signo << std::endl;
}
int main(int argc, char *argv[])
{
for(int signo = 1; signo <= 31; signo++)
{
signal(signo, catchSig);
}
while(true)
{
cout << "我在运行: " << getpid() <<endl;
sleep(3);
}
return 0;
}
我们将所有信号全部捕捉,别的确实无法终止进程,但是kill -9
可以,这也是OS的设置,OS禁止捕捉9号进程,即使你捕捉了也无法生效,它是管理员进程。否则如果真出现恶意程序捕捉所有信号,那岂不是真不能终止。
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀