当在网上买了快递,而手机提示今天快递会到,在快递要来临之前,你知道应该怎么处理快递,即你能事先识别“快递”。
此时你在打游戏,当快递到楼下了,你收到了消息说快递到了,即你接收到了信号。
此时你会有三种反应,一是马上起身去拿快递并打开使用,这叫默认动作;二是拿完快递回来给老妈(因为是给老妈买的),这叫自定义动作;三是忽略快递,继续打游戏。这三种都是信号被捕捉的表现。
从收到通知到你拿到快递这段时间,叫做时间窗口。即从接收到信号到对信号做出反应这段时间叫做时间窗口。
当消息通知你快递到了,你立刻去拿快递,按照默认动作去做反应,这叫同步;当收到消息时,游戏正打的火热,并不是立刻去拿快递而是过了一段时间再去拿,这叫异步。
在时间窗口里进程不能把信号忘掉,进程必然要具有保存信号的能力。实际上进程把信号保存在PCB里。PCB里有个unsigned int结构,该结构可以看作信号位图,该位图有32位。
信号表里一共有62种信号。从1号到64号,其中没有32号和33号。现在我们只了解1号到31号信号,[1,31]号信号叫做普通信号,[34,64]号信号叫实时信号。
kill -l查看信号名称和编号
man 7 signal查看信号信息
处理信号的方式有三种:
我们可以通过signal函数自定义相应信号的执行动作
函数原型:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
实际上,当进程收到signum时,会通过sighandler_t函数指针回调handler函数
我们知道Ctrl-C可以杀死前台进程
mytest.cc
#include
#include
#include
using namespace std;
void handler(int signo)
{
cout<<"进程捕捉到了一个信号,信号编号是: "<<signo<<endl;
}
int main()
{
signal(2,handler);
while(1)
{
cout<<"我是一个进程,mypid: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
通过man 7 signal查询2号信号
注意:
除了Ctrl-C可以杀死前台进程外,Ctrl-\也可以杀死前台进程
函数原型:
#include
#include
int kill(pid_t pid, int sig);
向指定pid的进程发送sig信号
mysign.cc
#include
#include
#include
#include
#include
using namespace std;
void Usage(const string& s)
{
std::cout << "\nUsage: " << s << " pid signo\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(-1);
}
pid_t pid=atoi(argv[1]);
int signo=atoi(argv[2]);
int n=kill(pid,signo);
if(n!=0)
{
perror("kill error");
exit(1);
}
return 0;
}
mytest.cc
#include
#include
#include
using namespace std;
int main()
{
//signal(2,handler);
while(1)
{
cout<<"我是一个进程,mypid: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
函数原型:
#include
int raise(int sig);
sig为发送的信号
调用成功返回0
mysign.cc
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int cnt=0;
while(cnt<10)
{
cout<<"cnt: "<<cnt<<endl;
cnt++;
sleep(1);
if(cnt==5)
{
int tmp=raise(3);
assert(tmp==0);
}
}
return 0;
}
函数原型:
#include
void abort(void);
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int cnt=0;
while(cnt<10)
{
cout<<"cnt: "<<cnt<<endl;
cnt++;
sleep(1);
if(cnt==5)
{
abort();
}
}
return 0;
}
信号的意义
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int cnt=0;
while(cnt<10)
{
cout<<"cnt: "<<cnt<<endl;
cnt++;
sleep(1);
if(cnt==5)
{
int a=10;
a/=0;
}
}
return 0;
}
可以看到当进程内除0操作时会造成进程直接退出
通过查表可以得知实际上进程有除0操作时,OS会向进程发送8号信号SIGFPE
mysign.cc
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void catchsig(int signo)
{
cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
sleep(1);
}
int main(int argc,char *argv[])
{
signal(SIGFPE,catchsig);
int a=10;
a/=0;
while(1)
{
cout<<"我是一个进程,正在运行中....."<<endl;
sleep(1);
}
return 0;
}
当程序运行到a/=0时,会导致OS给该进程发送8号信号,其原因如下:
至于程序运行到a/=0,之后的代码都不运行其原因如下:
mysign.cc
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void catchsig(int signo)
{
cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
sleep(1);
}
int main(int argc,char *argv[])
{
signal(11,catchsig);
while(1)
{
cout<<"我是一个进程,正在运行中....."<<endl;
int *ptr;
ptr=nullptr;
*ptr=10;
sleep(1);
}
return 0;
}
其发送11号信号原因如下:
信号的意义其二
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数和SIGALRM信号。
函数原型:
#include
unsigned int alarm(unsigned int seconds);
通过查表得知,与alarm相对应的信号是14号SIGALRM
mysign.cc
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void catchsig(int signo)
{
cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
exit(0);
}
int main(int argc,char *argv[])
{
int cnt=0;
signal(SIGALRM,catchsig);
alarm(1);
while(1)
{
cnt++;
cout<<"cnt: "<<cnt<<endl;
}
return 0;
}
可以看到cnt自增并且打印了7w多次,期间一秒后闹钟响了,进程终止。
因为IO很慢,大大拖慢了CPU的处理速度,若是让cnt自增,最后闹钟响后再打印cnt,就能得知该云服务器CPU处理数据的速度了。
#include
#include
#include
#include
#include
#include
using namespace std;
int cnt=0;
void catchsig(int signo)
{
cout<<"the alarm is ringing now,cnt: "<<cnt<<endl;
exit(0);
}
int main(int argc,char *argv[])
{
signal(SIGALRM,catchsig);
alarm(1);
while(1)
{
cnt++;
}
return 0;
}
若在处理SIGALRM信号的自定义函数体中,我不让该函数退出
但我们仍可以在自定义函数体内定义一次alarm函数,当闹钟响后,新的闹钟又被设置了。呈现出来的效果是cnt在1秒内不断自增,到期1秒打印一次,然后再不断自增,再打印,其效果与sleep函数相似
alarm 内核数据结构伪代码
struct alarm
{
uint64_t when;//未来的超时时间
int type;//闹钟类型,是一次性的还是周期性的
task_struct *p;//指向设置该闹钟的进程pcb
struct alarm* next;//指向下一个闹钟
}
在之前通过man 7 signal
中可以看到,各个信号的Action有几种,如:Term,Core,Ign,Cont,Stop;在其中,Action为Core的信号支持核心转储
在云服务器中,核心转储是默认被关掉的,我们可以通过使用
ulimit -a
命令查看当前资源限制的设定。
我们可以通过
ulimit -c size
命令设置core文件大小
由于要通过core对该进程进行调试,所以在makefile文件中标识mysign文件是可调试的(带-g)
输入后就能看到该进程是接收到了8号信号即浮点溢出的错误,并且代码位于第61行
我们可以通过signal函数捕捉相应信号,让其回调我们自定义的函数,那能否捕捉到所有的信号呢?
mysign.cc
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void catchsig(int signo)
{
cout<<"我是一个进程,此时我接收到了信号: "<<signo<<endl;
alarm(1);
}
int main(int argc,char* argv[])
{
for(int signo=1;signo<=31;signo++)
{
signal(signo,catchsig);
}
while(1)
{
cout<<"我在运行着:"<<getpid()<<endl;
sleep(1);
}
注意一下:
- 在block位图和pending位图中,比特位的位置都代表着某一个信号。其中,block位图上0代表未阻塞该信号,1代表阻塞该信号;pending位图上0代表未接收到该信号,1代表接收到该信号
- handler本质上是一个指针数组,数组内存放处理对应信号的方法(函数)的地址。其中方法包括三种:默认,自定义,忽略。
- block,pending和handler这三个数据结构上的位置是一一对应的
- 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储。
- sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
- 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
前提须知:
之前提到,在32位系统下的内存中,0-3G是用户空间,3-4G是内核空间。相应的,内存要通过页表与磁盘上的数据进行映射。由于进程的task_struct是通过mm_struct找到用户级页表,再与磁盘进行映射,进程在OS中有很多份,每一个进程都要与用户级页表一一对应,所以用户级页表在OS中有很多份;相同的,3-4G(内核空间)也需要通过内核级页表与磁盘上的数据进行映射。由于磁盘上内核数据只有一份,所以只需要将一份内核数据加载到内存中,因此只需要一份内核级页表。
由于每一个进程都有自己的地址空间(mm_struct),用户空间是每个进程独占的,而内核空间是每个进程都能访问到的,或者说是内核空间是每个进程共有的。所以进程访问内核空间只需要在进程地址空间上跳转到内核空间即可。这意味着,当进程切换时,3-4G的内核空间不会被更改。
实际上,进程切换时,OS会将进程的上下文加载到CPU中,然后执行对应用户空间上的代码。当需要访问资源时调用系统调用,OS会将去到进程对应的CR3寄存器,将用户态改为内核态,然后从用户空间跳转到内核空间进行资源访问,访问完后再将内核态改为用户态,回到用户空间继续执行相应的上下文。
当有相应信号递达,且信号对应的处理方法是默认动作或者忽略时,执行完后就直接返回用户态继续从上次中断的地方执行上下文。
当有相应信号递达,且信号对应的处理方法是自定义时,会通过系统调用从内核态切换到用户态,然后去用户内存执行相应的处理函数,执行完后通过特定的系统调用sigreturn返回内核态。在清除对应的 pending标志位后,如果没有新的信号递达,最后返回用户态继续从上次中断的地方执行上下文。
注意sighandler
和main
函数使用不同的堆栈空间,它们之间没有相互调用的关系,是两个独立的控制流程!
控制流程大致可以抽象成以下:
调用函数sigpromask可以读取或者更改进程的信号屏蔽字(阻塞信号集)
函数原型:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
SIG_BLOCK | set包含了我们希望添加到当前进程的信号屏蔽字的信号,相当于mask=mask|set |
---|---|
SIG_UNBLOCK | set包含了我们希望从当前进程的信号屏蔽字中去掉的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前进程的信号屏蔽字为set指向的值,相当于mask=set |
如果参数set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。即保存先前的信号屏蔽字
综上,如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。
返回值:若成功则为0,若出错则为-1
获取当前进程的未决信号集
函数原型:
#include
int sigpending(sigset_t *set);
set是输出型参数,将进程的pending位图通过set传出
返回值:调用成功返回0,失败返回-1
函数原型:
#include
int sigemptyset(sigset_t *set);
函数原型:
#include
int sigaddset (sigset_t *set, int signo);
sigaddset函数在set信号集中添加某种有效信号signo
返回值:调用成功返回0,失败返回-1
#include
int sigdelset(sigset_t *set, int signo);
函数原型:
#include
int sigismember(const sigset_t *set, int signo);
函数原型:
#include
int sigfillset(sigset_t *set);
#include
#include
#include
#include
#include
using namespace std;
#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
static void show_pending(sigset_t &pending)
{
for(int signo=MAX_SIGNUM;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}else
cout<<"0";
}
cout<<"\n";
}
int main()
{
//先初始化
sigset_t block,oblock,pending;
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
sigaddset(&block,BLOCK_SIGNAL);//把二号信号添加到block位图中
//将定义的位图设置进进程内核中
sigprocmask(SIG_SETMASK,&block,&oblock);
//打印pending位图
while(true)
{
sigemptyset(&pending);
sigpending(&pending);//获取进程的pending位图并置进pending位图中
show_pending(pending);//打印pending位图
sleep(1);//间隔打印
}
return 0;
}
#include
#include
#include
#include
#include
using namespace std;
#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31
static vector<int> sigarr{2,3};//将2,3号信号添加到进程的信号屏蔽字中
static void show_pending(sigset_t &pending)
{
for(int signo=MAX_SIGNUM;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}else
cout<<"0";
}
cout<<"\n";
}
void myhandler(int signo)
{
cout<<signo<<"信号已经被抵达\n"<<endl;
}
int main()
{
signal(2,myhandler);
//先初始化
sigset_t block,oblock,pending;
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
sigaddset(&block,BLOCK_SIGNAL);//把二号信号添加到block位图中
//for(const auto&sig:sigarr) sigaddset(&block,sig);
//把vector内指定的信号添加进block位图中
//将定义的位图设置进进程内核中
sigprocmask(SIG_SETMASK,&block,&oblock);
//打印pending位图
int cnt=10;
while(true)
{
sigemptyset(&pending);
sigpending(&pending);//获取进程的pending位图并置进pending位图中
show_pending(pending);//打印pending位图
sleep(1);//间隔打印
if(cnt--==0)
{
cout<<"恢复对信号的屏蔽,此时不屏蔽任何信号\n"<<endl;
sigprocmask(SIG_SETMASK,&oblock,&block);
}
}
return 0;
}
sigaction函数可以读取和修改与指定信号相关联的处理动作
函数原型:
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
act和oact指向sigaction结构体
#include
#include
#include
#include
#include
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt: %2d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
cout<<"\n";
}
void handler(int signo)
{
cout<<"get a signo: "<<signo<<endl;
Count(10);//在handler方法里待十秒
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
act.sa_flags=0;
sigaction(SIGINT,&act,&oact);
while(true) sleep(1);
return 0;
}
可以看到,对该进程发送了多次二号信号,实际上只递达了两次二号信号
其发送了多次2号信号只递达了两次二号信号根本原因在于,pending位图最多可以容纳一次未决信号,后来的再无意义
现在有一个场景,这里有三个结点,head指向的结点,node1和node2。在main函数中,先让node1头插到链表里,然后异常陷入内核,调用处理函数时将node2结点头插到链表里。在理论层面上看是没有问题的,但实际上:
- main函数执行流和handler函数执行流是两个执行流。像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。
- insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。相应的若一个函数只访问自己的局部变量或参数,则称为可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
- volatile修饰的变量保持内存的可见性
#include
#include
#include
#include
int quit=0;
void handler(int signo)
{
printf("pid: %d, %d 号信号正在被捕获!\n",getpid(),signo);
printf("quit: %d",quit);
quit=1;
printf("-> %d\n",quit);
}
int main()
{
signal(2,handler);
while(!quit);
printf("注意,我是正常退出的!\n");
return 0;
}
在gcc编译中,可以调整优化级别,这里我调整为O3级别
关键字volatile修饰的变量使其保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
当子进程图退出或者被中止时子进程会发送17号信号SIGCHILD给父进程
#include
#include
#include
#include
#include
void Count(int cnt)
{
while(cnt)
{
//cout<<"cnt:"<
printf("cnt: %d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
printf("我是父进程,pid:%d ,我接收到了子进程的%d 号信号\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);
printf("我是父进程id: %d, ppid: %d\n",getpid(),getppid());
pid_t id=fork();
if(id==0)
{
printf("我是子进程id: %d, ppid: %d\n",getpid(),getppid());
Count(5);
exit(1);
}
while(1) sleep(1);
return 0;
}
让父进程不阻塞等待回收子进程的方法
waitpid函数原型
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
处理函数handler
void handler(int signo)
{
pid_t id=0;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
signal(SIGCHLD,SIG_IGN);