生活中的信号:
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能 “识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你在忙学习,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。
而处理快递一般方式有三种:
快递到来的整个过程,对你来讲是异步的,快递员可能在送快递的时候你在做别的事情
计算机中的信号:
kill -l查看,【1-31】普通信号 【34,64】实时信号
进程是如何识别信号的?认识+动作 进程本身是被程序员编写的属性和逻辑的集合——程序员编码完成
当进程收到信号的时候,进程可能在执行更重要的代码,所以信号不一定会被立即处理 进程本身必须要有对信号的保存能力
进程在处理信号的时候,一般有三个动作(默认、自定义、忽略)—进程处理的专业名词【信号被捕捉】
如果一个信号发送给进程,信号要被进程保存在pcb里面
struct task_struct
{
......
unsigned int signal;
......
}
发送信号的本质:修改PCB中的信号的位图
PCB的管理者是OS,任何一种发送信号的方式,本质都是通过OS向目标进程发送的信号,例如:kill命令——底层一定调用了对应的系统调用,所以OS必须要提供发送信号/处理信号的相关系统调用
以前我们所使用的ctrl+c终止进程本质是向进程发送了2号信号
1.signal——对一个信号注册特定的处理动作(注册一个对信号的捕捉方式)
不过9号进程是无法被修改的!
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum: 要注册的信号
handler: 处理动作,有三种:SIG_DFL(默认) 、SIG_IGN(忽略) 和 自定义(函数指针)
其中函数指针指向的函数有一个int类型的参数,无返回值,这个函数指针就是用户给信号自定义的处理动作,通过函数实现
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "进程捕捉到了信号,编号是" << signo << endl;
}
int main()
{
//这里是signal的调用,不是handler的调用
//仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
//一般这个方法不会被执行,除非收到对应的信号
signal(2,handler);
while(1)
{
cout << "我是一个进程:" << getpid() << endl;
sleep(1);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ohi6imlS-1680794945950)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfc3be46-85d7-4c64-9650-dec174722350/Untitled.png)]
在正式进入话题前,我们先谈一谈整个信号的生命周期:预备工作(例如:信号产生的时候,进程早已知道该如何处理,信号在产生之时,并不能立即处理)→信号发送(通过键盘产生,通过软件产生等)→信号保存→信号递达处理
除了之前的ctrl+c,还有ctrl+\,发送3号进程
我们使用man 7 signal可以查看到信号的详细信息
我们发现2号信号和3号信号都是退出,但是2号信号是Term,3号信号是Core,这有什么区别呢?
Core Dump(核心转储)
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常
core,这叫做Core Dump。我们可以通过使用gdb调试查看core文件查看进程退出的原因,这也叫
后调试。
不过在云服务器上,默认进程如果是core退出的,我们暂时看不到现象,我们可以使用ulimit -a查看OS给用户设置的各种资源的上限:
我们可以使用ulimit -c [大小]去更改core file的大小
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int arr[10];
arr[10000]=1;
return 0;
}
这样一串代码运行起来
文本编译器打开是乱码,在gdb中可以调式,带上**-g选项才可以调式 gcc -o test test.cpp -g**
1.kill——给任意进程发送任意信号
疑问:写成如下这样为什么对别人的进程发送信号没有自定义操作
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
printf("catch a signal : %d\n", signo);
}
int main(int argc,char* argv[])
{
signal(2, handler);
while(1)
{
kill(atoi(argv[1]),2);
}
return 0;
}
#include
int kill(pid_t pid, int sig);
参数:
pid:进程pid
sig:要发送的信号
返回值:
成功返回0,失败返回-1
#include
#include
#include
#include
#include
using namespace std;
int main(int argc,char* argv[])
{
if(argc==3)
{
kill(atoi(argv[1]),atoi(argv[2]));
}
return 0;
}
#include
int raise(int sig);
参数:
sig:要发送的信号
返回值:
成功返回0,失败返回-1
和kill比较:
raise函数相当于kill(getpid(), sig)
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
printf("catch a signal : %d\n", signo);
}
int main()
{
signal(2, handler);
while(1){
raise(2);
sleep(1);
}
return 0;
}
3.abort——给自己发送指定的信号(发送6号信号)
#include
void abort(void);
#include
#include
#include
#include
#include
using namespace std;
int main()
{
while(1){
abort();
}
return 0;
}
管道如果读端不读了,存储系统会发生SIGPIPE 信号给写端进程,终止进程。这个信号就是由一种软件条件产生的,这里再介绍一种由软件条件产生的信号SIGALRM(时钟信号)
1.alarm——设定一个闹钟,操作系统会在闹钟到了时送SIGALRM信号给进程,默认处理动作是终止进程
#include
unsigned alarm(unsigned seconds);
参数:
second:设置时间,单位是s
返回值:
0或者此前设定的闹钟时间还余下的秒数
#include
#include
#include
#include
#include
using namespace std;
int main()
{
alarm(1);
int cnt=0;
while(1)
{
cnt++;
cout << cnt << endl;
}
return 0;
}
“闹钟“就是一个软件实现的,任意一个进程都可以通过alarm系统调用在内核中设置闹钟,OS内可能存在很多的闹钟,操作系统需要管理这些闹钟,即先描述,再组织
这里介绍CPU异常和MMU异常
#include
#include
#include
#include
int main()
{
// 由软件条件产生信号 alarm函数和SIGPIPE
// CPU运算单元产生异常,内核将这个异常处理为SIGFPE信号发送给进程
int a = 10;
int b = 0;
printf("%d", a/b);
return 0;
}
CPU产生异常:发生除零错误,CPU运行单元会产生异常,内核将这个异常解释为信号,最后OS发送SIGFPE信号给进程
#include
#include
#include
#include
int main()
{
// MMU硬件产生异常,内核将这个异常处理为SIGSEGV信号发送给进程
int* p = NULL;
printf("%d\n", *p);
return 0;
}
**MMU产生异常:**当进程访问非法地址时,mmu想通过页表映射来将虚拟转换为物理地址,此时发现页表中不存在该虚拟地址,此时会产生异常,然后OS将异常解释为SIGSEGV信号,然后发送给进程
**注意:**阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
OS发生信号给一个进程,此信号不是立即被处理的,那么这个时间窗口中,信号就需要被记录保存下来,那么信号是如何在内核中保存和表示的呢?
**说明:**上面有三种表,分别是信号阻塞位图block表,信号未决位图pending表,信号处理动作handler表
图片分析:
总结:
POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
几个疑问:
1.所有信号的产生都要由OS来进行执行,这是为什么?
信号的产生涉及到软硬件,且OS是软硬件资源的管理者,还是进程的管理者。
2.进程在没有收到信号的时候,能否知道自己应该如何对合法信号进行处理呢?
答案是能知道的。每个进程都可以通过task_struct找到表示信号的三张表。此时该进程的pending表中哪些信号对应的那一位比特位是为0的,且进程能够查看block表知道如果收到该信号是否需要阻塞,可以查看handler表知道对该信号的处理动作。
3.OS如何发生信号?
OS给某一个进程发送了某一个信号后,OS会找到信号在进程中pending表对应的那一位比特位,然后把那一位比特位由0置1,这样OS就完成了信号发送的过程。
sigset_t: 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,也被定义
一种数据类型。这个类型可以表示每个信号状态处于何种状态(是否被阻塞,是否处于未决状态)
阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内
如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操
sigset_ t变量,而不应该对它的内部数据做任何解释
注意: 对应sigset类型的变量,我们不可以直接使用位操作来进行操作,而是一个严格实现系统给
们提供的库函数来对这个类型的变量进行操作
信号集操作函数的原型
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
注意:在实现这些函数之前,需要使用sigemptyset或sigfillset对信号集进行初始化。前四个函数的返回值是成功返回0,失败返回-1。最后一个函数的返回值是真返回0,假返回-1
1.sigprocmask——阻塞信号集操作函数
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
功能:
读取或更改进程的信号屏蔽字
参数:
how:三个选项
SIG_BLOCK:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|set
SIG_UNBLOCK:把set中的信号屏蔽字在进程信号屏蔽字的那些去掉,mask = mask&~set
SIG_SETMASK:设置当前进程的信号屏蔽字为set,mask = set
set:如果为非空指针,则根据how参数更改进程的信号屏蔽字
oset:如果为非空指针,将进程原来的信号屏蔽字备份六种oset中
返回值:
成功返回0,失败返回-1
2.sigpending——未决信号集操作函数
#include
int sigpending(sigset_t *set);
功能:
读取进程的未决信号集
参数:
set:读取当前进程的信号屏蔽字到set指向的信号屏蔽中
返回值:
成功返回0,失败返回-1
实例演示:
1.把进程中信号屏蔽字2号信号进行阻塞,然后隔1s对未决信号集进行打印,观察现象
#include
#include
#include
#include
using namespace std;
static void show_pending(const sigset_t& pending)
{
for(int signo=31;signo>=1;--signo)
{
//判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)
if(sigismember(&pending,signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
// 1. 先尝试屏蔽指定的信号
sigset_t set,oset;
sigset_t pending;
//1.1使用系统函数对信号集进行初始化
sigemptyset(&set);
sigemptyset(&oset);
sigemptyset(&pending);
//阻塞2号信号
//1.2添加要屏蔽的信号
sigaddset(&set,2);
//1.3开始屏蔽,设置进内核, 前面的代码没有影响当前进程,从这段开始影响
//oset保存原来的信号
sigprocmask(SIG_BLOCK,&set,&oset);
//2.遍历打印pending信号集
while(1)
{
//2.1初始化
sigemptyset(&pending);
//2.2获取它
sigpending(&pending);
//2.3打印
show_pending(pending);
sleep(1);
}
}
**代码运行结果如下:**可以看到,进程收到2号信号时,且该信号被阻塞,处于未决状态,没有被递达,未决信号集中2号信号对应的比特位由0置1,所以代码一直运行
2.进行运行10s后,我们将信号屏蔽字中2号信号解除屏蔽
#include
#include
#include
#include
using namespace std;
static void show_pending(const sigset_t& pending)
{
for(int signo=31;signo>=1;--signo)
{
//判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)
if(sigismember(&pending,signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
int main()
{
// 1. 先尝试屏蔽指定的信号
sigset_t set,oset;
sigset_t pending;
//1.1使用系统函数对信号集进行初始化
sigemptyset(&set);
sigemptyset(&oset);
sigemptyset(&pending);
//阻塞2号信号
//1.2添加要屏蔽的信号
sigaddset(&set,2);
//1.3开始屏蔽,设置进内核, 前面的代码没有影响当前进程,从这段开始影响
//oset保存原来的信号
sigprocmask(SIG_BLOCK,&set,&oset);
//2.遍历打印pending信号集
int cnt=10;
while(1)
{
//2.1初始化
sigemptyset(&pending);
//2.2获取它
sigpending(&pending);
//2.3打印
show_pending(pending);
sleep(1);
cnt--;
if(cnt==0)
{
cout << "屏蔽信号解除" << endl;
sigprocmask(SIG_UNBLOCK,&set,&oset);
}
}
}
**代码运行结果如下:**2号信号解除阻塞后,信号被递达了,进程终止
问题:信号什么时候被处理的?
首先,不是立即被处理的。而是在合适的时候,这个合适的时候,具体指的是进程从用户态切换回内核态时进行处理
何为用户态?内核态?
操作系统怎么知道你处于什么状态?
进程空间分为用户空间和内核空间。此前我们介绍的页表都是用户级页表,其实还有内核级页表。
程的用户空间是通过用户级页表映射到物理内存上,内核空间是通过内核级页表映射到物理内存上
如下面简图所示:
进程有不同的用户空间,但是只有一个内核空间,不同进程的用户空间的代码和数据是不一样的,但是内核空间的代码和数据是一样的。
上面的图主要说明:进程处于用户态访问的是用户空间的代码和数据,进程处于内核态,访问的是内核空间的代码和数据。
下面给演示信号捕捉的整个过程:
从上面的图可以看出,进程是在返回用户态之前对信号进行检测,检测pending位图,根据信号处
动作,来对信号进行处理。这个处理动作是在内核态返回用户态后进行执行的。
如果信号的处理动作是用户的自定义函数,可以画成如下的图,便于理解:
其中4个绿点是4次状态切换,4个红色的点对应上图的4个执行步骤
之前我们介绍过信号捕捉函数signal,这里在介绍另一个
1.sigaction——一个对指定信号的执行动作进行特殊处理的函数
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
可以读取和修改与指定信号相关联的处理动作
参数:
signum: 要操作的信号
act:一个结构体
sa_handler:SIG_DFT、SIG_IGN和handler(用户自定义处理函数)
sa_sigaction:实时信号处理的函数,我们不关心
sa_mask:一个信号屏蔽字,里面有需要额外屏蔽的的信号
sa_flags:包含一下选项,这里我们给0
sa_restorer:我们这里不使用
act:如果不为空,根据act修改信号处理动作
oact: 如果不为空,备份原来的信号处理动作给oact
返回值:
成功返回0,失败返回-1
act结构体如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
实例演示:
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
act.sa_flags=0;
sigaction(2,&act,&oact);
while(1) sleep(1);
return 0;
}
代码运行结果如下:
再做一个实验,自定义动作睡眠20秒,会有什么现象呢?
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
sleep(20);
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(2,&act,&oact);
while(1) sleep(1);
return 0;
}
运行结果如下:
原因:当我们进行正在递达某一个信号期间,同类型的信号无法递达!当当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字(block),当信号完成捕捉,系统又会自动解除对该信号的屏蔽,一般一个信号被解除屏蔽的时候,会自动进行递达当前屏蔽信号,如果该信号已经被pending的话,没有就不会做任何动作!
再做一个实验:在sa_mask中在添加一个信号
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
sleep(20);
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
sigemptyset(&act.sa_mask);
// 这个sa_mask只是屏蔽!不会加入到handler动作!
sigaddset(&act.sa_mask,2);
act.sa_flags=0;
sigaction(3,&act,&oact);
while(1) sleep(1);
return 0;
}
运行结果如下:
如果一个函数符合以下条件之一则是不可重入的:
先看一段代码:
#include
#include
#include
#include
#include
using namespace std;
int quit=0;
void handler(int signo)
{
cout << signo << "号信号,正在被捕捉!" << endl;
cout << "quit:" << quit ;
quit=1;
cout << "->" << quit << endl ;
}
int main()
{
signal(2,handler);
while(!quit) ;
cout << "注意,我是正常退出的" << endl;
return 0;
}
运行结果如下:我们输入ctrl+c,会正常退出
但如果编译的时候带上O3级别的优化呢?g++ -o test test.cpp -O3
运行结果如下:
改变了也不会退出,这是为什么呢?quit放在内存中
使用volatile可以保存内存的可见性,加上volatile就正常执行了
子进程在死亡的时候,会向父进程发送SIGCHILD信号,不过父进程默认是忽略的,使用man 7 signal查看
用以上知识检查看看是不是17号信号
#include
#include
#include
#include
#include
using namespace std;
void handler(int signo)
{
printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
}
void Count(int cnt)
{
while (cnt)
{
printf("cnt: %2d\r", cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
int main()
{
signal(17,handler);
printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());
pid_t id=fork();
if(id==0)
{
printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
sleep(20);
exit(1);
}
//保持父进程在运行
while (1)
sleep(1);
return 0;
}
运行结果如下:
这样的意义在于,以前父进程被动式的等待,例如阻塞等待子进程,或者主动去“问问子进程”,即非阻塞式等待,现在我们可以让子进程叫我们了!
所以我们可以把handler写成如下形式
void handler(int signo)
{
// 1. 我有非常多的子进程,在同一个时刻退出了 【只需要循环处理】
// 2. 我有非常多的子进程,在同一个时刻只有一部分退出了 【必须非阻塞式等待,因为操作系统不知道你有多少个子进程要退出
//如果你没退出,在这里就会造成死循环】
//waitpid第一个参数是pid,这里是多个子进程,所以设置-1,意思是会等待任意一个子进程
while(1)
{
pid_t ret = waitpid(-1, NULL, WNOHANG);
if(ret == 0) break;
}
}
由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程
signal(17,SIG_IGN);
这里的手动设置的IGN和之前默认的IGN是不一样的