Linux进程信号
前言
信号的产生
键盘产生
我们可以通过signal来验证我们所说的。
测试代码:
#include<iostream>
#include<unistd.h>
#include<signal.h>
void handler(int signo)
{
std::cout<<"我是接收到"<<signo<<"信号,才执行handler函数的..."<<std::endl;
}
int main()
{
//自定义2号信号的处理动作
signal(SIGINT,handler);
//让我们的进程一直处于死循环,然后我们通过ctrl+c的方式来给进程发送信号,看一看ctrl+c是不是给进程发送的2号信号
while(true)
{
std::cout<<"我是一个进程,我的pid是:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
运行结果:
现象:
1、我们按下ctrl+c过后,当前进程确实是收到了2号信号,因为当前进程在接收到ctrl+c过后,执行了handler函数,同时也表明了自己是因为收到什么信号才执行的handler函数的,因此我们的验证成功了。
2、我们确实是验证了ctrl+c实际上就是OS向进程发送2号信号,可是为什么进程在接收到2号信号过后,并没有向以前一样终止进程,反而是继续循环呢?
主要是因为,在我们自定义的handler方法中,就只是做了一个简单的打印,在handller中并没有调用诸如exit之类的退出函数,进程也就不会终止,而是回到上一次运行的地方,继续向下运行,如果,我们想让进程在接收到2号信号过后也终止掉进程,我们可以在handler函数最后加一个exit()函数:
当然ctrl+\
也可以终止掉进程,这个组合键是向进程发送3号信号来终止进程;
通过命令或系统调用来产生信号
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
int main(int argc,char *argv[])
{
//参数个数不够
if(argc!=3)
{
std::cerr<<"参数个数不够"<<std::endl;
exit(1);
}
//检查信号是否合法
int signo=atoi(argv[1]+1);//字符串转换为整数
if(signo<1||signo>64)
{
std::cerr<<"信号不合法"<<std::endl;
exit(2);
}
//合法信号
pid_t id=atoi(argv[2]);
//向指定进程发送指定信号
kill(id,signo);
return 0;
}
运行结果:
测试:在5s过后向当前进程发送一个终止信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<cstring>
#include<wait.h>
int main()
{
int cnt=0;
while(cnt<5)
{
std::cout<<"我是一个进程,我的pid:"<<getpid()<<",编号:"<<cnt++<<std::endl;
sleep(1);
if(cnt==5)
{
//5s过后向自己发送一个3号信号
raise(SIGQUIT);
}
}
return 0;
}
运行结果:
硬件异常产生的信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<cstring>
#include<wait.h>
void handler(int sig)
{
std::cout<<"我接收到"<<sig<<"信号"<<std::endl;
sleep(1);
}
int main()
{
//自定义下SIGFPE信号
signal(SIGFPE,handler);
//如果除0操作,确实发送的SIGFPE信号,那么我们就会看到handler函数被执行
//如果除0操作,不是发送的SIGFPE信号,那么我们就不会看到handler函数被执行
int a=10;
a/=0;
return 0;
}
Core终止信号与Term终止信号的区别
我们可以通过一段测试代码来验证一下,是否有核心转储文件:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<cstring>
#include<wait.h>
int main()
{
int cnt=0;
while(true)
{
std::cout<<"我是一个正常的进程,我的pid:"<<getpid()<<"编号"<<cnt++<<std::endl;
sleep(1);
}
return 0;
}
这是一段正常的程序,大概意思就是每隔1s,输出一下信息;
先我们主动给其发不同类型的终止信号,看看其所引发的现象:
我们先给其,发送一个1号信号吧,通过上面的表可知1号信号时Term类型终止信号,Term类终止信号不会进行核心转储,因此在当前目录下,我们不应该看到core文件才对:
现在,我们在来给它,发送一个3号信号,通过查表可以知道3号信号是Core类型的终止信号,因此我们除了看到进程被终止外,还应在源程序目录下看到一个core.pid文件才对:
我们可以发现,在源文件目录下并没有core.pid文件,这是为什么?难道是我们之前的理论是错误的?
实际上,并不是我们的理论错误了,而是由于我所使用的是云服务器,对于服务器来说核心转储功能一般都是关闭的,通过ulimit -a
命令我们可以查看当前服务器下的一下最高限制:
如果我们硬是想要使用这个核心转储的功能呢?
我们可以用命令ulimit -c size
重新设置core文件的大小单位为kbyte
我们可以将其大小设置为10240,如果最终产生的core文件大小超过了设置的大小,那么最终生成的core文件就是一个不完整的文件,gdb调试时会提示错误;
这次,我们将核心转储功能也打开了,现在我们再来对其发送3号信号,应该就可以生成core文件了吧:
由于,我们gcc/g++编译器默认生成的可执行文件时release版本的,该版本不支持调试,为了方便演示core文件的功能,我们需要将我们的可执行文件以debug版本的方式生成,为此我们需要向gcc/g++命令中添加-g选项,表示我们要以debug模式生成可执行文件;
再我们生成好可执行程序过后,我们在利用gdb调试器,调试我们的程序:
为了,能够演示,core文件能够帮助我们快速定位到错处,我们可以尝试让进程自己产生异常,然后由OS自己主动给进程发送信号,比如:我们在代码中故意加个除0操作,代码中出现除0操作,OS会向进程发送一个8号信号,8号信号也是一个Core类型的终止信号,因此进程在因为收到8号信号而终止过后,也会产生core文件:
测试代码:
运行结果:
确实产生了core文件,接着我们再来调试:
可是如此强悍的功能,服务器为什么要关闭呢?
功能是强悍可是确实有代价的,我们可以看一看所形成的core文件的大小:
很明显,core文件很大,我的可执行文件也就才2w多字节,要是我们的可执行文件在大一点,那么core文件的大小岂不是更大!况且,我们在服务其上部署项目时,都会为我们的服务再部署一个监控服用,这个监控服务就是当我们部署的服务挂掉的时候能够快速的重新启动该服务。
要是我把核心转储的功能打开,那么当我们的服务遇到一个bug时,该bug会导致OS向进程发送一个Core类型信号来终止掉该进程,在终止之前就会生成一个巨大的Core文件。随后监控服务立马重启这个服务,然后由于这个bug被触发的概率比较大,我们一重启,就又收到了Core类型终止信号,就又会生成一个巨大的Core文件,如此循环往复下去,我们一直触发这个bug,就一直生成core文件,当我们第二天起来一看,整个服务器都被core文件给占满了,这时后就不是当前这个服务挂掉的问题,而是这会导致部署在整个服务器上的所有服务都出现问题!
因此,在服务器上核心转储功能一般都是关闭的;
由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了这里主要介绍alarm函数 和SIGALRM信号。
测试代码:
int main()
{
//1s过后OS会向当前进程发送一个SIGALRM信号
alarm(1);
int count=0;
while(true)
std::cout<<count++<<std::endl;
return 0;
}
上面代码的意思是,测试1s内count能被加多少次,可以简单的表示计算机的算力;
我们可以发现,1s内count能够被加11w多次但是这是不准确的,为什么因为我们每++一次count都会进行一次IO输出,IO输出是很耗时间的,这就会导致1s之内大量的时间都浪费在了IO上,因此,我们想要准确测量1s内count能加多少次,我们可以如下改进我们的代码:
int count=0;
void sigcb(int signo)
{
std::cout<<count<<std::endl;
exit(1);
}
int main()
{
//自定义一下SIGAMRM信号的处理动作
signal(SIGALRM,sigcb);
//1s过后OS会向当前进程发送一个SIGALRM信号
alarm(1);
while(true)
count++;
return 0;
}
我们捕捉一下SIGAMRM信号,让在while循环里count只++,那么当1s过后,当前进程接收到SIGAMRM信号,该进程在合适时候会去调用SIGAMRM信号的自定义函数,在自定义函数中,我们在打印一下count:
我们可以发现,没有IO过后,计算机++的能力提升了不少,这也验证了IO操作真的很耗时间;
信号保存的细节
阻塞信号
在内核中表示
sigset_t
信号集函数
做个小实验来使用一下我们上面的函数:
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<cstring>
#include<wait.h>
void ShowTable(const sigset_t &nums)
{
for(int i=1;i<65;i++)
{
if(sigismember(&nums,i))
std::cout<<1;
else
std::cout<<0;
}
std::cout<<std::endl;
}
int main()
{
//前10s阻塞2号和40号信号;
//10s过后,不在阻塞2号信号
//15s过后,不在阻塞40号信号;
sigset_t set;
sigemptyset(&set);
sigaddset(&set,2);
sigaddset(&set,40);
//将需要阻塞的信号添加进block表
sigprocmask(SIG_BLOCK,&set,nullptr);
//再次获取一下新的block表
sigset_t newset;
sigemptyset(&newset);
sigprocmask(SIG_BLOCK,nullptr,&newset);
int cnt=1;
while(true)
{
if(cnt==10)
{
//在第10s的时候,我们取消2号信号的阻塞
sigemptyset(&set);
sigaddset(&set,2);
sigemptyset(&newset);
sigprocmask(SIG_UNBLOCK,&set,nullptr);
//解除更新过后,重新获取block表
sigprocmask(SIG_UNBLOCK,nullptr,&newset);
}
else if(cnt==15)
{
//在第15s的时候,我们取消40号信号的阻塞
sigemptyset(&set);
sigaddset(&set,40);
sigemptyset(&newset);
sigprocmask(SIG_UNBLOCK,&set,nullptr);
//解除更新过后,重新获取block表
sigprocmask(SIG_UNBLOCK,nullptr,&newset);
}
std::cout<<"我的pid:"<<getpid()<<".";
printf("[%3d]->",cnt++);
std::cout<<"block表:";
ShowTable(newset);
sleep(1);
}
return 0;
}
信号的捕捉
内核如何实现信号的捕捉
sigaction
测试代码:
//父进程调用了这个函数,说明收到了SIGCHLD信号,说明有子进程死亡,我们可以开始回收子进程
void handler(int signo)
{
std::cout<<"handler begin..."<<std::endl;
//方便观察父子进程的状态
sleep(5);
/*//等待任意子进程
waitpid(-1,nullptr,0);*/
//上面这种做法是有bug的,如果,父进成创建了很多个子进程呢?然后每个子进程的生命周期不一样长
//如果父进程在第一次接收到SIGCHLD信号到信号递达这个时间片内,又陆续接收到其他子进程发来的SIGCHLD信号
//极端情况下,在这个时间片内,所有子进程都发送了SIGCHLD信号
//那么pending位图会被重复置1,但是无论发多少次SIGCHLD信号,pending位图只会记录一次,当父进开始
//递达该信号的时候,根据我们上面的写法,也就只会回收一个子进程,对于其他死亡的子进程,父进程并不会回收
//,t同时在结束这次回收过后,父进程不会在收到SIGCHLD信号,因为子进程已经都发过信号了,对于其他子进程来说,这会造成内存泄漏!
//改进1:在handler方法里面,我们不要只回收一次,尽管,我们也不知道有多少个子进程死亡
//我们可以根据waitpid的返回值来决定什么时候取消回收
/*while(waitpid(-1,nullptr,0)!=-1);*/
//上面这种做法还是有点小bug,怎么说呢?上面采用的是阻塞的方式回收子进程
//如果有5个子进程,其中4个子进程先结束运行,剩下一个子进程死循环
//那么那4个子进程先结束运行就会向父进程发送信号,父进程接收到信号过后
//在合适的时间调用handler方法,成功回收4个子进程;
//由于采取的阻塞回收的方式,那么在回收递5个子进程的时候,由于该子进程迟迟不终止,
//那么父进程就会阻塞在waitpid函数内部,无法退出handler函数,直到子进程运行结束;
//改进2:父进程采用轮询的方式回收
while(waitpid(-1,nullptr,WNOHANG)>0);
std::cout<<"[子进程被pid为:"<<getpid()<<"的进程成功回收]"<<std::endl;
}
//用于实现父进程的自动回收子进程的资源
int main()
{
//用于自定义SIGCHLD信号的信号处理函数
struct sigaction s;
memset(&s,0,sizeof(s));
s.sa_handler=handler;
s.sa_mask=sigset_t();
s.sa_flags=0;
s.sa_restorer= nullptr;
sigaction(SIGCHLD,&s,nullptr);
//创建多个子进程
for(int i=0;i<5;i++)
{
pid_t id=fork();
if(id==0)
{
if(i==4)
alarm(200);
else
alarm(10);
int cnt=10;
while(true)
{
std::cout<<"我是子进程,我的pid:"<<getpid()<<",我的ppid:"<<getppid()<<",我的生命倒计时:"<<cnt--<<std::endl;
sleep(1);
}
}
}
//father
while(true)
{
sleep(1);
}
return 0;
}
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction 设置SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。请编写程序验证这样做不会产生僵尸进程