什么是信号
从生活角度来说: 信号是一种条件反射,不管事件有没有发生,但是你对带这件事情的处理方式是固定的。这件事情的发生对你来说就是一种信号。
操作系统也存在信号,实际上os中的信号:是操作系统向进程传达指令的一种操作。操作系统向进程发出信号,进程接受到信号执行相应的动作。
输入指令kill -l 就可以查看所有的信号(注意这里面只有62个信号,分为前31个普通信号和后31个实时信号),如果想要杀死一个特定的进程只需要:kill -signum 进程pid signum 为信号编号 这里每个信号都有一个字字母标识该信号
信号从操作系统发出到信号被执行一共要经理三个过程:
下面就让我们从这三个方面来对信号深入了解
信号产生
通过键盘产生信号
首先举一个最常用的例子,在使用shell的时候如果一个前台进程卡住了,我们只要在键盘上按下ctrl C 就可以终止这个进程(注意这里一定要是前台进程),这里就是信号的一种体现,查阅资料之后发现ctrl C 实际上向操作系统发送的是2号信号,如何证明?
在所有的介绍之前首先要先学习一下关于信号的一个最基本的函数:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数的功能是对进程收到特定信号之后的行为进行自定义处理,自定义处理的方式是sighandler_t函数定义,这个函数唯一的参数就是自定义行为信号的编号 参数
- signum:信号的编号,也就是我们在kill -l指令里面看到的信号的编号
- sighandler_t handler:自定义信号处理方式的函数的指针 该函数的类型是
void (*sighandler_t)(int); ,该函数的参数为自定义信号的编号
我们在进程中将2号信号的处理自定义,运行起来并向该进程发送二号信号(按下ctrl C)
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
void sighandler(int signo)
{
printf("收到%d号信号\n",signo);
}
int main()
{
signal(2,sighandler);
sleep(100);
return 0;
}
- 我们执行进程之后直接按下ctrl C,得到如上结果
- 我们执行进程之后,在另一个终端对该进程执行指令
kill -2 进程pid ,也得到了如上结果
所以这里就证明了,我们在键盘上按下的ctrl C实际上是操作系统向进程发送了2号信号
调用系统函数向进程发送信号
前面说过可以通过指令将特定进程发送信号,如kill 命令 这里实际上还有一些系统调用函数:
kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数:向指定进程pid发送指定信号sig
返回值:
raise函数
#include <signal.h>
int raise(int sig);
向自己的进程发送sig信号,相当于kill(geypid(),sig)
abort函数
#include <stdlib.h>
void abort(void);
实际上这个函数是向自己发送6号SIGABRT 信号,如果执行现象如下:
软件条件产生信号
-
SIGPIPE 信号是由管道读写时,如果在管道IO时写端关闭,读端不变,这时就会触发系统向进程发送SIGPIPE信号 -
alarm函数 #include <unistd.h>
unsigned int alarm(unsigned int seconds);
参数:为seconds秒之后向当前进程发送SIGALARM 信号(14号信号),当信号触发之后的结果为:
由硬件产生的信号
core dump核心转储
有些人在运行上述所有信号造成的进程终止时候有可能会出现如下结果(这咯使用的是除0的代码):
在原有的结果中我们发现了多出了一个core dump。
core dump是一个进程异常终止之时,将进程的所有用户空间内存数据全部保存到磁盘上,文件名通常是core,用来给用户进程调试使用的。
例如上面异常崩溃之后就会发现多处了一个core 文件
如何打开或关闭core dump
使用指令ulimit -a 查看资源
这里我们已经将core file size设置成了1024,如果你没有这个值为0,程序异常就不会出现core dump选项
如何使用core dump
例如上面报错了之后,会生成一个core文件,我们用gdb对崩溃的程序开始调试:
gdb 程序名
然后执行指令core-file 生成的core文件名
这时候就会显示文件究竟在哪崩溃的,这里我们没有安装相应的库,所以看不到
信号识别
首先我们需要搞明白一点:信号是一收到就会执行信号吗?
实际上是不是的,信号收到之后不会立即执行,而是等待进程陷入内核态,再从内核态返回用户态的时候才会检查是否收到信号,这期间信号是不会被执行,于是这里就产生了两个问题:
- 信号收到到执行这段期间存在哪里?
- 什么是内核态,什么是用户态?
信号的屏蔽
在此之前补充一些概念:
- 信号递达:实际执行信号的处理动作
- 信号未决(pending):信号从产生到递达之间的状态
- 信号阻塞(block):将信号一直处于未决状态就叫做阻塞
信号在未决期间的状态是由三张位图(存在进程的PCB中)决定的
- pending位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否接受到信号。OS发送信号实际上就是修改进程PCB中的pending位图
- block位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否阻塞信号
- handler位图:用信号的编号作为数组的索引,找到该信号对应的信号的处理方式,然后指向对应的方法
,从实现的角度来说handler是一个函数指针数组
上面信号2和信号3 ,都被阻塞了,不管进程有没有收到信号2或3,都不会执行对应的操作。所以阻塞应该理解成一种状态。
信号集sigset_t 有时候我们想认为的控制一个进程的pending位图或者block位图,所以这时候我们就要提供一个接口将两个位图暴露出来,这时候就定义了一种类型sigset_t ——信号集,来标识这两个位图。用户可以通过修改这个数据类型的值来改变进程的两个位图 阻塞信号集又称为当前进程的信号屏蔽字
信号集操作函数
#include <signal.h>
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);
获取block位图
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数
-
how -
set:要设置的信号集 -
oldset:将原来的信号集返回出来
获取pending位图
#include <signal.h>
int sigpending(sigset_t *set);
参数 将pending位图通过set参数传出来
例如:我们先将2号信号block掉,然后发送二号信号打印pending位图,八秒之后解除对2号信号的阻塞,这时2号信号被抵达,执行我们自定义的信号操作:
#include<iostream>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
using namespace std;
void sighandler(int signo)
{
cout<<"收到"<<signo<<"号信号"<<endl;
}
void func()
{
sigset_t m;
sigpending(&m);
for(int i=1;i<=32;i++)
{
if(sigismember(&m,i))
cout<<"1";
else
cout<<"0";
}
cout<<endl;
}
int main()
{
sigset_t sig;
sigemptyset(&sig);
sigaddset(&sig,2);
sigset_t old_sig;
sigprocmask(SIG_BLOCK,&sig,&old_sig);
int count=8;
while(count--)
{
func();
sleep(1);
}
signal(2,sighandler);
cout<<"解除信号屏蔽"<<endl;
sigprocmask(SIG_SETMASK,&old_sig,NULL);
return 0;
}
内核态 && 用户态
前面学进程地址空间的时候低3G的地址空间为用户空间,而高1G的空间是内核空间。
-
什么是内核态,什么是用户态? 在计算机的体系结构中,一个程序不仅只是计算,还要和硬件设施进行交互,例如:我们的进程需要开辟一块空间需要和内存交互、进程需要从文件中读取信息也需要和磁盘交互…。这些直接与硬件交互的动作(系统调用)实际上是由操作系统代替进程执行的,由于要确保硬件的安全性,这些动作都被设置了很高的权限(防止恶意进程破坏硬件),所以进程遇到这些硬件方面的需求就会向操作系统请求代替自己执行,操作系统执行指令的过程就叫做进程陷入了内核态。如果只是执行普通的代码,那么就叫做用户态。 -
操作系统是如何区分用户态和内核态 CPU的寄存器储存着进程的状态信息 -
内核态和用户态最大的区别是什么? 权限,内核态拥有更高的权限,能看到和操作的资源比用户态要多得多
所以整个过程应该是:
信号执行的过程
前面我们了解到信号是在从内核态切换成用户态的时候递达的,接下来看一下信号被递达的整体你过程
但是如果信号的递达方式是用户自定义的话,整个过程就会大有所不同:
信号处理
信号处理分为三种处理方式:1.默认 2.忽略信号 3.自定义方式
默认处理方式
这个很好理解,就是一个进程受到信号按照该信号的处理方式处理,绝大部分信号的处理方式都是终止进程
忽略信号
这个更好理解了,就是收到信号之后,啥也不干就把信号给忽略了
自定义方式
进程收到信号之后执行用户自己定义的信号处理方式,也就是我们提到的函数typedef void (*sighandler_t)(int); ,在这个函数里面用户可以修改进程收到信号之后的操作,我们把这种情况叫做信号的捕捉
信号处理函数
这里我们就要介绍一下一个比较重要的函数:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
参数:
-
signum:信号的编号 -
这是一个sigaction结构体,我们这里传入的是我们需要修改的结构体指针,里面包含如下内容: The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
void (*sa_handler)(int); 这个就是信号自定义函数,与signal中的一样 -
void (*sa_sigaction)(int, siginfo_t *, void *); 如果struct sigaciton 结构体中的sa_flag 被定义成SA_SIGINFO ,这时候自定义函数就会调用这个,其他情况调用上面的。参数方面:第一个参数为信号的编号,第三个参数一般不会使用,第二个参数是一个结构体指针siginfo_t 该结构体包含如下信息: siginfo_t {
int si_signo;
int si_errno;
int si_code;
int si_trapno;
pid_t si_pid;
uid_t si_uid;
int si_status;
clock_t si_utime;
clock_t si_stime;
sigval_t si_value;
int si_int;
void *si_ptr;
int si_overrun;
int si_timerid;
void *si_addr;
long si_band;
int si_fd;
short si_addr_lsb;
}
-
sigset_t sa_mask; 这里定义的是信号屏蔽字,例如我们在执行信号的自定义操作时,这时候又来了一些信号,这时我们可以用信号屏蔽字将特定信号给屏蔽了 -
这里这个结构体和上面的一样,这里不过是一个输出型参数,将修改前的sigaction 结构体传出来
volatile关键字
学完了信号之后,让我们用一个例子重新认识一下这个关键字:
我们设置一个全局变量,并修改了二号信号的执行操作——将全局变量的值修改,理论上发送信号2之后,循环就会推出
#include<iostream>
#include<signal.h>
using namespace std;
int flag=1;
void sighandler(int sig)
{
flag=0;
return ;
}
int main()
{
signal(2,sighandler);
while(flag);
cout<<"process quit"<<endl;
}
g++ test.cpp -O3
执行程序你会发现即使你发送了2号信号,循环还在继续。
原因就在于这里的优化选项,将flag放到了寄存器中,这样运算效率更快,但是信号修改时内存里面flag的值被修改,但是CPU还是拿寄存器中的值进行比较,所以导致了这个情况。
这时对flag变量加上volatile 关键字就可以避免这种错误,volatile关键字就是优化器在用到这个变量时必须每次都小心地重新读取这个变量的值(From Memory),而不是使用保存在寄存器里的备份。
加上violate运行程序结果:
SIGCHLD
在进程等待那篇博客,我详细描述了父进程等待子进程的函数wait 和 waitpid 函数,其实在进程等待也是通过信号传递信息的,在子进程终止的时会给父进程发送17号信号SIGCHLD信号,该信号的默认处理动作是忽略,父进程也可以自定义该信号的处理方式
如下代码自定义SIFCHLD 信号的默认处理动作来回收信号
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<signal.h>
#include<stdlib.h>
using namespace std;
void sighandler(int signum)
{
pid_t id;
while((id=waitpid(-1,NULL,WNOHANG))>0)
{
printf("child is quit! %d\n",getpid());
}
printf("child quit\n");
}
int main()
{
int ret= fork();
signal(17,sighandler);
if(ret==0)
{
cout<<"child process start "<<getpid()<<endl;
sleep(4);
exit(10);
}
else
{
while(1)
{
cout<<"father is do sth"<<endl;
sleep(1);
}
}
}
|