输入 | 含义 |
---|---|
ctrl+c | 向进程发送2号信号 SIGINT ,默认是终止进程 |
ctrl+ | 向进程发送3号信号 SIGQUIT,默认是终止进程并进行核心转储 Core Dump |
ctrl+z | 向进程发送20号信号 SIGSTP ,默认是停止进程 |
验证:我们通过以下代码,将1~31号信号全部进行捕捉,将收到信号后的默认处理动作改为打印收到信号的编号
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handle(int signal)
{printf("收到%d号信号n",signal);
}
int main()
{int i = 0;for(i = 1;i<31;i++){signal(i,handle); //对1~31号信号进行捕捉}while(1) ;return 0;
}
我们可以发现:如果我们向该进程发送9号信号,并不会执行我们自定义的捕捉方法,而是执行收到9号信号后的默认处理动作 ->即:终止进程
注意:
1)键盘产生的信号,只能作用于前台进程,后台进程由于不影响输入指令,无法使用键盘发送信号
2)有些信号是不能被捕捉的,比如9号信号,因为如果所有信号都能被捕捉的话,那么进程就可以将所有信号全部进行捕捉并将动作设置为忽略,此时该进程将无法被杀死,即便是操作系统
我们可以发现按Ctrl+C终止进程和按Ctrl+都可以终止进程,但是不同的是:这两个信号的Action是不一样的
Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作: 那就是核心转储
问:什么是核心转储
核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为 core.pid
在云服务器中,核心转储是默认被关掉的,我们可以通过使用ulimit -a
命令查看当前资源限制的设定
如何打开核心转储功能
我们可以通过 ulimit -c size
命令来设置core文件的大小,core文件的大小设置完毕后,就相当于将核心转储功能打开了
演示:进程运行过程中输入ctrl+,对进程进行终止
此时我们发现:终止进程后会显示 core dumped,并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID
核心存储有什么作用呢?
当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的, 如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的
当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因,这样子做会非常的慢!而核心转储的目的就是为了在调试时,方便问题的定位!
#include <stdio.h>
#include <unistd.h>
int main()
{printf("Hello I am runningn");sleep(2);int a = 1/0; //会发生除0错误return 0;
}
该程序运行2秒后便会崩溃
此时我们便可以在当前目录下看到核心转储时生成的core文件,我们可以使用gdb对当前可执行程序进行调试,然后直接使用 core-file core.PID
文件 命令加载core文件
我们可以看到该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码
说明: 事后用调试器检查core文件以查清错误原因的这种方式叫做事后调试
core dump标志实际上就是用于表示程序崩溃的时候是否进行了核心转储
我们回忆一下进程等待函数waitpid函数:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
其中第二个参数status是一个输出型参数,用于获取子进程的退出状态, status的不同比特位所代表的信息不同
正常终止的情况下:status的次低8位就表示进程的退出状态(退出码)
若进程是被信号所杀: status的低7位表示终止信号,第8位比特位是core dump标志 -> 即进程终止时是否进行了核心转储
我们在打开核心转储功能的情况下进行如下测试:
下述代码存在野指针的问题,当子进程执行到 *p = 100 时,必然会被操作系统识别,发送信号所终止并在终止时进行核心转储 此时父进程使用waitpid函数便可获取到子进程退出时的状态 可以根据status的第7个比特位便可得知子进程在被终止时是否进行了核心转储
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{if (fork() == 0){//childprintf("I n");int *p = NULL;*p = 100;//对空指针解引用!exit(0);}//fatherint status = 0;waitpid(-1, &status, 0);printf("exitCode:%d, coreDump:%d, signal:%dn",(status >> 8) & 0xff, (status >> 7) & 1, status & 0x7f);return 0;
}
coreDump为1 -> 说明子进程在被终止时进行了核心转储
2)进程异常:进程运行中出现错误导致软硬件出现异常,被操作系统甄别并处理
程序当中出现类似于除0、野指针、越界之类的错误时, 为什么C/C++程序会出现崩溃的情况呢?
本质上是因为进程在运行过程中收到了操作系统发来的信号进而被终止
操作系统是如何识别到一个进程触发了某种问题的呢
所谓的信号是由OS向进程PCB中发的,所以一旦硬件出问题,一定是代码出问题,导致软件/硬件出故障,然后被操作系统识别,操作系统也一定会在硬件故障时将进程的各种信息保存下来,然后在合适的时候向目标进程发送信号
CPU当中有一堆的寄存器,当我们需要对两个数进行算术运算时,我们是先将这两个操作数分别放到两个寄存器当中,然后进行算术运算并把结果写回寄存器当中 此外,CPU当中还有一组寄存器叫做状态寄存器,它可以用来标记当前指令执行结果的各种状态信息,如有无进位、有无溢出等等
操作系统是软硬件资源的管理者 ,在程序运行过程中,若操作系统发现CPU内的某个状态标志位被置位,而这次置位就是因为出现了某种除0错误而导致的,那么此时操作系统就会马上识别到当前是哪个进程导致的该错误,并将所识别到的硬件错误包装成信号发送给目标进程,本质就是操作系统去直接找到这个进程的task_struct,并向该进程的位图中写入8信号,写入8号信号后这个进程就会在合适的时候被终止
#include<stdlib.h>
#include<stdio.h>
#include<signal.h>
void handle(int signal)
{printf("get a signal: %dn", signal);exit(0);
}
int main()
{signal(8,handle);int a = 1/0;return 0;
}
#include<stdio.h>
#include<unistd.h>
int main()
{printf("I am a process t I am runningn");sleep(2);int* p = NULL;*p = 100;return 0;
}
操作系统又是如何识别到我们操作野指针||程序越界的呢
1)当我们要访问一个变量时,一定要先经过页表的映射,虚拟地址转换成物理地址,然后才能进行相应的访问操作
页表属于一种软件映射关系, 实际上在从虚拟地址到物理地址映射的时候还有一个硬件叫做MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,因此映射工作不是由CPU做的,而是由MMU做的
2)当我们要访问不属于我们的虚拟地址时,MMU在进行虚拟地址到物理地址的转换时就会出现错误,然后将对应的错误写入到自己的状态信息当中,这时硬件上面的信息也会立马被操作系统识别到,进而将对应进程发送SIGSEGV信号(段错误的信号)
C/C++程序会崩溃,是因为程序当中出现的各种错误最终一定会在硬件层面上有所表现,进而会被操作系统识别到,然后操作系统就会发送相应的信号将当前的进程终止
3)系统调用: 系统提供多种发送信号的接口, 方便我们在代码中设置信号
当我们要使用kill命令向一个进程发送信号时,我们可以以 kill -信号名 进程PID
|| kill -信号编号 进程PID
的形式进行发送
例子:
int main()
{while(1){printf("I am a process,My pid:%dn",getpid());sleep(2);}return 0;
}
实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号
#include <signal.h>
int kill(pid_t pid, int sig);
作用:向进程PID为 pid 的进程发送 sig 号信号 返回值: 如果信号发送成功,则返回0,否则返回-1
小例子:用kill函数模拟实现一个kill命令
这里利用到的是命名行参数, 我们知道kill是这样使用的: kill -对应的信号 要kill的进程PID, 所以命名行参数的个数需要是3个!如果不是3个就提示用户输入错误! 我们定义使用规则是:mykill(可执行程序名字) 进程PID 信号编号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>//提示用户怎么使用
void Usage(char* proc)
{printf("Usage: %s pid signon", proc);
}
int main(int argc, char* argv[])
{//如果命令行参数不是3个,就错误!if (argc != 3){Usage(argv[0]);return 1;}//我们定义使用规则是:mykill(可执行程序名字) 进程PID 信号编号//argv[1]:进程PID//argv[2]:信号编号pid_t pid = atoi(argv[1]);int signo = atoi(argv[2]);kill(pid, signo);return 0;
}
为了方便使用:让生成的可执行程序在执行时不用带上路径,我们可以将当前路径导入环境变量PATH当中
注意:只在本次登录有效
mykill 进程PID 信号编号
raise函数可以给当前进程发送指定信号,即自己给自己发送信号
#include <signal.h>
int raise(int sig);
参数:给当前进程发送 sig 号信号 返回值:如果信号发送成功,则返回0,否则返回一个非零值
例子:用raise函数每隔1秒向自己发送一个2号信号
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void handle(int signal)
{printf("收到%d号信号n",signal);
}
int main()
{signal(2,handle);//捕捉(注册)2号信号while(1){//每隔1s给当前进程发生2号信号sleep(1);raise(2);}return 0;
}
abort函数可以给当前进程发送SIGABRT信号(6号信号),使得当前进程异常终止
#include <stdlib.h>
void abort(void);
小例子:每隔1秒向当前进程发送一个SIGABRT信号 (6号信号)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void handle(int signal)
{printf("收到%d号信号n",signal);
}
int main()
{signal(6,handle);//捕捉(注册)6号信号-SIGABRTwhile(1){//每隔1s给当前进程发生6号信号-SIGABRTsleep(1);abort();}return 0;
}
我们可以看到:虽然我们对SIGABRT信号进行了捕捉,并且在收到SIGABRT信号后执行了我们给出的自定义方法,但是当前进程依然是会异常终止了,并不一直执行我们的自定义方法
abort和exit函数有什么区别
abort函数的作用是异常终止进程,abort函数的本质是通过向当前进程发送SIGABRT信号而终止进程的 exit函数的作用是正常终止进程
区别: 使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的
4)软件条件 : 某些软件条件不具备触发信号发送的条件, 操作系统向进程发送信号
注意: 虽然产生信号的方式繁多, 但其底层都是操作系统在向进程发送信号数据
SIGPIPE信号实际上就是一种由软件条件产生的信号
回忆我们之前的进程间通信:当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,此时相当于是在浪费OS的资源,此时写端进程就会收到OS发的SIGPIPE信号被终止
代码展示:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0) //创建匿名管道{ perror("pipe");return 1;}pid_t id = fork(); //创建子进程if (id == 0){//childclose(fd[0]); //子进程关闭读端//子进程向管道写入数据const char* msg = "hello father, I ";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子进程写入完毕,关闭写端exit(0);}//fatherclose(fd[1]); //父进程关闭写端close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%dn", status & 0x7F); //打印子进程收到的信号return 0;
}
我们可以发现子进程在退出时收到的是13号信号, 即SIGPIPE信号
调用alarm函数可以设定一个闹钟:告诉OS在若干时间后发送SIGALRM信号给当前进程
#include <unistd.h>
unsigned alarm(unsigned int seconds);
作用:
让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程
返回值:
如果调用alarm函数前:进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置, 如果调用alarm函数前:进程没有设置闹钟,则返回值为0
代码实测:测试Linux下 1s时间可以将一个变量累加到多少
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main()
{int count = 0;alarm(1); //定时1s后发送SIGALRM信号,默认处理动作是终止进程while (1){count++;printf("count: %dn", count);}return 0;
}
我们可以发现,在我们的云服务器下,1s内可以将一个变量累加到7~8万左右
但是实际上CPU的执行速度并没有这么慢,当前的云服务器在一秒内可以执行的累加次数远大于7~8万
为什么上述代码运行结果比实际结果要小呢
1)我们每进行一次累加就进行了一次打印操作,与外设之间的IO操作所需的时间要比累加操作的时间更长
2)当前使用的是云服务器,在累加操作后还需要将累加结果通过网络传输将服务器上的数据发送过来
因此最终显示的结果要比实际一秒内可累加的次数小得多
为了检测上述的描述是否正确:可以先让count变量一直执行累加操作,直到一秒后进程收到SIGALRM信号后再打印累加后的数据
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int count = 0;
void handler(int signo)
{printf("get a signal: %dn", signo);printf("count: %dn", count);exit(1);
}
int main()
{signal(SIGALRM, handler);//捕捉SIGALRM信号alarm(1);while (1){count++;}return 0;
}
此时1s内对count的累加次数高达5亿次 由此也证明了:与计算机单纯的计算相比较,计算机与外设进行IO时的速度是非常慢的
本文发布于:2024-01-28 14:00:04,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/17064216107919.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |