从生活角度来说: 信号是一种条件反射,不管事件有没有发生,但是你对带这件事情的处理方式是固定的。这件事情的发生对你来说就是一种信号。
操作系统也存在信号,实际上os中的信号:是操作系统向进程传达指令的一种操作。操作系统向进程发出信号,进程接受到信号执行相应的动作。
输入指令kill -l
就可以查看所有的信号(注意这里面只有62个信号,分为前31个普通信号和后31个实时信号),如果想要杀死一个特定的进程只需要:kill -signum 进程pid
signum 为信号编号
SIGSTOP/SIGKILL信号无法被阻塞,无法被自定义,无法被忽
这里每个信号都有一个字字母标识该信号
下面就让我们从这三个方面来对信号深入了解
首先举一个最常用的例子,在使用shell的时候如果一个前台进程卡住了,我们只要在键盘上按下ctrl C
就可以终止这个进程(注意这里一定要是前台进程),这里就是信号的一种体现,查阅资料之后发现ctrl C
实际上向操作系统发送的是2号信号,如何证明?
在所有的介绍之前首先要先学习一下关于信号的一个最基本的函数:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个函数的功能是对进程收到特定信号之后的行为进行自定义处理,自定义处理的方式是sighandler_t函数定义,这个函数唯一的参数就是自定义行为信号的编号
参数
void (*sighandler_t)(int);
,该函数的参数为自定义信号的编号我们在进程中将2号信号的处理自定义,运行起来并向该进程发送二号信号(按下ctrl C)
#include
#include
#include
#include
void sighandler(int signo)
{
printf("收到%d号信号\n",signo);
}
int main()
{
signal(2,sighandler);
sleep(100);
return 0;
}
kill -2 进程pid
,也得到了如上结果所以这里就证明了,我们在键盘上按下的ctrl C实际上是操作系统向进程发送了2号信号
前面说过可以通过指令将特定进程发送信号,如kill
命令
这里实际上还有一些系统调用函数:
kill函数
#include
#include
int kill(pid_t pid, int sig);
参数:向指定进程pid发送指定信号sig
返回值:
raise函数
#include
int raise(int sig);
向自己的进程发送sig信号,相当于kill(geypid(),sig)
abort函数
#include
void abort(void);
实际上这个函数是向自己发送6号SIGABRT
信号,如果执行现象如下:
SIGPIPE
信号是由管道读写时,如果在管道IO时写端关闭,读端不变,这时就会触发系统向进程发送SIGPIPE信号
alarm函数
#include
unsigned int alarm(unsigned int seconds);
参数:为seconds秒之后向当前进程发送SIGALARM
信号(14号信号),当信号触发之后的结果为:
非法访问内存产生的信号
非法访问内存之后,系统中的mmu会产生异常,并把这个情况返回给操作系统,操作系统就会向进程发送11号信号:SIGSEGV
int main()
{
int *p;
*p=1;
return 0;
}
CPU非法运算产生的信号
例如我们进行一个除0操作,这时候负责运算的CPU最先检测到信号,将其报告给操作系统,最后由操作系统向进程发送信号
int main()
{
int a=1/0;
return 0;
}
运行结果如下:
硬件产生的信号的特殊之处
我们来看下面一段代码:
#include
#include
#include
using namespace std;
void sighandler(int signum)
{
cout << "接收到" << signum << "号信号" << endl;
sleep(1);
}
int main()
{
signal(8, sighandler);
cout << "running" << endl;
sleep(2);
int p = 1 / 0; // 触发硬件信号
return 0;
}
结果如下:
出现了一个很奇怪的现象,为什么os一直向进程发送八号信号?这是我们自定义函数收到信号之后并没有退出进程,CPU的中当前进程的上下文会被保存起来,在此调用该进程的时候,CPU检测硬件标志位,发现异常,继续通过OS向进程发送信号。
所以 由硬件产生的信号如果不退出进程的话,会一直向进程发送信号
有些人在运行上述所有信号造成的进程终止时候有可能会出现如下结果(这咯使用的是除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文件名
这时候就会显示文件究竟在哪崩溃的,这里我们没有安装相应的库,所以看不到
首先我们需要搞明白一点:信号是一收到就会执行信号吗?
实际上是不是的,信号收到之后不会立即执行,而是等待进程陷入内核态,再从内核态返回用户态的时候才会检查是否收到信号,这期间信号是不会被执行,于是这里就产生了两个问题:
在此之前补充一些概念:
信号在未决期间的状态是由三张位图(存在进程的PCB中)决定的
上面信号2和信号3 ,都被阻塞了,不管进程有没有收到信号2或3,都不会执行对应的操作。所以阻塞应该理解成一种状态。
信号集sigset_t
有时候我们想认为的控制一个进程的pending位图或者block位图,所以这时候我们就要提供一个接口将两个位图暴露出来,这时候就定义了一种类型sigset_t
——信号集,来标识这两个位图。用户可以通过修改这个数据类型的值来改变进程的两个位图
阻塞信号集又称为当前进程的信号屏蔽字
信号集操作函数
#include
int sigemptyset(sigset_t *set); //将信号集里面所有位 置0
int sigfillset(sigset_t *set); //将信号集里面所有位 置1
int sigaddset(sigset_t *set, int signum); //将指定信号signum的位 置成1
int sigdelset(sigset_t *set, int signum); //将指定信号signum的位 置成0
int sigismember(const sigset_t *set, int signum); //判断signum位是否被设置
获取block位图
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数
获取pending位图
#include
int sigpending(sigset_t *set);
参数
将pending位图通过set参数传出来
例如:我们先将2号信号block掉,然后发送二号信号打印pending位图,八秒之后解除对2号信号的阻塞,这时2号信号被抵达,执行我们自定义的信号操作:
#include
#include
#include
#include
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); //将2号信号屏蔽
int count=8;
while(count--)
{
func();
sleep(1);
}
signal(2,sighandler);
cout<<"解除信号屏蔽"<<endl;
sigprocmask(SIG_SETMASK,&old_sig,NULL);
return 0;
}
前面学进程地址空间的时候低3G的地址空间为用户空间,而高1G的空间是内核空间。
什么是内核态,什么是用户态?
在计算机的体系结构中,一个程序不仅只是计算,还要和硬件设施进行交互,例如:我们的进程需要开辟一块空间需要和内存交互、进程需要从文件中读取信息也需要和磁盘交互…。这些直接与硬件交互的动作(系统调用)实际上是由操作系统代替进程执行的,由于要确保硬件的安全性,这些动作都被设置了很高的权限(防止恶意进程破坏硬件),所以进程遇到这些硬件方面的需求就会向操作系统请求代替自己执行,操作系统执行指令的过程就叫做进程陷入了内核态。如果只是执行普通的代码,那么就叫做用户态。
用户级页表&&内核级页表
用户级页表是每个进程所私有的,存储在进程的PCB中的上下文中。而内核级页表是所有进程所共享的,也就是说所有进程共用一个内核级页表!!!
操作系统是如何区分用户态和内核态
CPU的寄存器储存着进程的状态信息
内核态和用户态最大的区别是什么?
权限,内核态拥有更高的权限,能看到和操作的资源比用户态要多得多
所以整个过程应该是:
前面我们了解到信号是在从内核态切换成用户态的时候递达的,接下来看一下信号被递达的整体你过程
但是如果信号的递达方式是用户自定义的话,整个过程就会大有所不同:
信号处理分为三种处理方式:1.默认 2.忽略信号 3.自定义方式
这个很好理解,就是一个进程受到信号按照该信号的处理方式处理,绝大部分信号的处理方式都是终止进程
这个更好理解了,就是收到信号之后,啥也不干就把信号给忽略了
进程收到信号之后执行用户自己定义的信号处理方式,也就是我们提到的函数typedef void (*sighandler_t)(int);
,在这个函数里面用户可以修改进程收到信号之后的操作,我们把这种情况叫做信号的捕捉
这里我们就要介绍一下一个比较重要的函数:
#include
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; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
}
sigset_t sa_mask;
这里定义的是信号屏蔽字,例如我们在执行信号的自定义操作时,这时候又来了一些信号,这时我们可以用信号屏蔽字将特定信号给屏蔽了
信号在递达之后会将pending位图的对应位置0,并且在执行自定义处理方法的时候系统会自动屏蔽当前信号。同时我们还可以是用sigaction
函数中的sa_mask
来屏蔽其他信号的递达
这里这个结构体和上面的一样,这里不过是一个输出型参数,将修改前的sigaction
结构体传出来
学完了信号之后,让我们用一个例子重新认识一下这个关键字:
我们设置一个全局变量,并修改了二号信号的执行操作——将全局变量的值修改,理论上发送信号2之后,循环就会推出
#include
#include
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 ##这里-O3是编译优化选项
执行程序你会发现即使你发送了2号信号,循环还在继续。
原因就在于这里的优化选项,将flag放到了寄存器中,这样运算效率更快,但是信号修改时内存里面flag的值被修改,但是CPU还是拿寄存器中的值进行比较,所以导致了这个情况。
这时对flag变量加上volatile
关键字就可以避免这种错误,volatile关键字就是优化器在用到这个变量时必须每次都小心地重新读取这个变量的值(From Memory),而不是使用保存在寄存器里的备份。
加上violate运行程序结果:
在进程等待那篇博客,我详细描述了父进程等待子进程的函数wait
和 waitpid
函数,其实在进程等待也是通过信号传递信息的,在子进程终止的时会给父进程发送17号信号SIGCHLD信号,该信号的默认处理动作是忽略,父进程也可以自定义该信号的处理方式
如下代码自定义SIFCHLD
信号的默认处理动作来回收信号
#include
#include
#include
#include
#include
#include
#include
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);
}
}
}