一、signal函数
我们前文信号入门了解了人在接收信号后会有三类不同的动作,其实进程本质上也是如此:
- 默认执行(红灯停绿灯行、马上取快递、马上拿外卖)
- 进行自定义动作(边唱歌边等红绿灯、边打电话边取件)
- 选择忽略(忽略红绿灯直接闯、忽略外卖继续打游戏
所以我们可以先学习一下signal函数来对信号进行动作的自定义,方便日后的学习。
signal函数:通过signum方法设置回调函数,设置某一信号的对应动作
#include <signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t signal(int signum, sighandler_t handler);
直接通过代码来理解signal这个接口:
void handler(int signal)
{
cout<<"进程捕捉到了一个信号,信号编号是:"<<signal<<endl;
}
int main()
{
//signal函数的调用,并不是handler的调用
//这仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
//一般这个方法不会执行,除非收到对应的信号
signal(2,handler);
while(true)
{
cout<<"我是一个进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

发送2号信号的时候并没有终止进程,原因是我们把默认动作设置成自定义动作,想让其终止直接kill -9 +pid。
二、信号产生的五种方式

1.按键产生
CTRL+C, 2号信号,ctrl+c是组合键,OS会将ctrl+c解释成2号信号,所以我们ctrl+c的时候该进程直接进入结束状态
int main()
{
while(true)
{
cout<<"我是一个进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

CTRL+\ , 三号信号,ctrl+\也是组合键,OS会将ctrl+\解释成3号信号,所以我们ctrl+\的时候该进程也会终止
int main()
{
while(true)
{
cout<<"我是一个进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

2.kill命令
kill -signal pid
KILL(1)
NAME
kill - terminate a process
SYNOPSIS
kill [-s signal|-p] [-q sigval] [-a] [--] pid...
kill -l [signal]
这是简单直接、也常用的命令。
int main()
{
while(true)
{
cout<<"我是一个进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

3.几个传输信号的函数接口
3.1系统调用kill函数&&模拟实现
KILL(2)
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//pid:目标进程的pid。sig:几号信号
成功时(至少发送了一个信号),返回零。出现错误时,返回 -1,并设置errno
kill函数可以向任意进程发送任意信号,
kill命令底层实际上就是kill系统调用,信号的发送由用户发起,而执行者是OS。
发送信号的能力是OS的,但是有这个能力并不一定有使用这个能力的权力,一般情况下用户决定OS向目标进程发信号。所以OS有这个能力,那么对外提供能力只能通过系统调用的接口的方式来让用户向目标进程发送信号。
kill函数模拟实现如下:
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<string>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
int main(int argc, char *argv[])
{
//usage: ./mykill signum pid
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int ret = kill(pid, signum);
if (ret == -1)
{
perror("kill");
exit(2);
}
return 0;
}
//myproc.cc
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "hello, i am a process, pid is " << getpid() << endl;
sleep(1);
}
return 0;
}
实现效果:

3.2raise函数
raise函数可以让进程给自己发送任意信号。
RAISE(3)
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
using namespace std;
int main()
{
int cnt = 10;
while (cnt--)
{
cout << "i am a process!!!" << endl;
sleep(1);
if (cnt == 5)
{
raise(3);//kill(getpid(), 3);
}
}
return 0;
}
实现效果:

3.3abort函数
abort终止进程的方式,给自己发送指定的信号 6) SIGABRT 。
ABORT(3)
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
RETURN VALUE
The abort() function never returns.
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<cstdlib>
#include<string>
using namespace std;
int main()
{
int cnt = 10;
while (cnt--)
{
cout << "i am a process!!!" << endl;
sleep(1);
if (cnt == 5)
{
abort();
//kill(getpid(), 6);
}
}
return 0;
}

4.硬件异常产生信号
1.除零发送8号信号 —> 8) SIGFPE
信号产生,不一定非得用户显示的发送,有些情况下信号会在OS内部自动产生。
int main(int argc,char*argv[])
{
//4.产生信号的方式:硬件异常产生信号
//信号产生,不一定非得用户显示的发送
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int a = 10;
a/=0;
}
}

为什么/0会终止进程:除0当前进程会受到来自OS系统的信号SIGFPE
证明:通过signal接口,把SIGFPE信号自定义捕捉:
void handler(int signal)
{
cout<<"进程捕捉到了一个信号,信号编号是:"<<signal<<endl;
sleep(1);
}
int main(int argc,char*argv[])
{
signal(8, handler);
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int a = 10;
a/=0;
}
}

OS 系统如何得知应该给当前进程发送8号信号的?CPU异常。
除0的理解:
CPU内有很多寄存器eax,edx等,执行int a=10,a/=0;CPU内除了数据保存,还得保证运算有没有问题,所以还有状态寄存器,状态寄存器衡量这次的运算结果,10/0相当于10乘以无穷大,结果无穷大,引起状态寄存器溢出标记位由0变成1,CPU发生了运算异常,OS得知CPU发生运算异常,就要识别异常:状态寄存器的标记位置为1,由当前进程导致的,在向目标进程发送信号,最后就终止进程了。
我们可以看到上面的结果:收到信号不一会引起进程的退出
进程没有退出,则还有可能还会被调度,CPU内部的寄存器只有一份,但是寄存器中的内容属于当前进程的上下文,一旦出现异常我们没有能力去修正这个问题,所以当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1。
2.野指针发送11号信号 —> 11) SIGSEGV
int main(int argc,char*argv[])
{
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int*p = nullptr;
*p=10;
}
}

野指针崩溃:收到了11号信号
OS如何识别到野指针:访问野指针的时候,会引起虚拟地址到物理内存之间转化时对应的MMU报错,进而OS识别到报错,会给当前进程发送11号信号,11号信号代表非法的内存引用(man 7 signal)。


首先我们必须知道的是,当我们要访问一个变量时,一定要先经过页表的映射,将虚拟地址转换成物理地址,然后才能进行相应的访问操作。
其中页表属于一种软件映射关系,而实际上在从虚拟地址到物理地址映射的时候还有一个硬件叫做MMU,它是一种负责处理CPU的内存访问请求的计算机硬件,因此映射工作不是由CPU做的,而是由MMU做的,但现在MMU已经集成到CPU当中了。
当需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,我们再通过这个物理地址进行相应的访问。
而MMU既然是硬件单元,那么它当然也有相应的状态信息,当我们要访问不属于我们的虚拟地址时,MMU在进行虚拟地址到物理地址的转换时就会出现错误,然后将对应的错误写入到自己的状态信息当中,这时硬件上面的信息也会立马被操作系统识别到,进而将对应进程发送SIGSEGV信号。
总结一下:
C/C++程序会崩溃,是因为程序当中出现的各种错误最终一定会在硬件层面上有所表现,进而会被操作系统识别到,然后操作系统就会发送相应的信号将当前的进程终止。
发送信号不是为了处理问题,而是为了便于操作者统一处理异常,进行后续处理工作。
5.软件条件产生信号
1.管道 —> 13) SIGPIPE
我们之前所说的管道,如果父端的读端关闭,子端的写端一直在写,写的数据就没有读的意义了,OS不允许这样子,会向子进程发送13号信号SIGPIPE终止子进程。管道跟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){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
运行代码后,即可发现子进程在退出时收到的是13号信号,即SIGPIPE信号。如下图所示,由于子进程是被信号所杀,所以读取status的后七位。


2.定时器 —> 4) SIGALRM
调用alarm函数可以设定一个闹钟,也就是告诉操作系统在若干时间后发送SIGALRM信号给当前进程,alarm函数的函数原型如下:
ALARM(2)
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or
zero if there was no previously scheduled alarm.
alarm函数的作用:让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。
alarm函数的返回值:
- 若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。
- 如果调用alarm函数前,进程没有设置闹钟,则返回值为0。
例如,我们可以用下面的代码,测试自己的云服务器一秒时间内可以将一个变量累加到多大。
int main(int argc,char*argv[])
{
//软件条件
alarm(1);
int cnt = 0;
while(true)
{
cout<<"cnt:"<<cnt++<<endl;
}
}

这份代码的意义在于可以统计1S左右,我们的计算机能够将数据累计多少次。实际上这种方法是比较慢的,为什么?打印时是要进行输出的,输出是外设,外设IO较慢。如果没有打印:
int cnt = 0;
void handler(int signal)
{
cout<<"进程捕捉到了一个信号,信号编号是:"<<signal<<endl;
cout << "cnt = " << cnt << endl;
exit(1);
}
int main(int argc,char*argv[])
{
//软件条件
signal(SIGALRM, handler);
alarm(1);
while(true)
{
cnt++;
}
}

此时可以看到,count变量在一秒内被累加的次数变成了五亿多,由此也证明了,与计算机单纯的计算相比较,计算机与外设进行IO时的速度是非常慢的。

2103

被折叠的 条评论
为什么被折叠?



