1.信号产生的生命周期
- 进程目前没收到信号,但是进程知道收到信号之后该怎么做。进程内部一定能够识别信号,程序员在设计进程的时候,已经内置了处理方案。信号属于进程内部特有的特征
- 当信号来的时候,进程可能在做优先级更高的事情,信号可能不会立刻被处理,等合适的时候再进行处理。至于什么事合适的时候我们之后再谈。在信号来了,处理信号前,信号必须暂时被进程保存下来
- 进程开始处理信号,有如下三种方式:
- 默认行为(如终止进程,暂停等)
- 自定义行为
- 忽略信号
2.信号是如何发送以及记录的
信号共62个,其中前31个是普通信号,34-64为实时信号。现在我们只关注前31个普通信号
进程的信号是记录在进程的task_struct(PCB)当中,本质上更多的是为了记录信号是否产生。信号是使用位图记录的
进程收到信号的本质是进程PCB内的信号位图被修改了。只有OS有资格修改进程内的数据。信号发送只有OS有资格,但是信号发送方式有多种。
我们知道,当我们写一段死循环 的代码,代码跑起来之后,我们可以使用组合键Ctrl+C终止掉程序。其实Ctrl+C是2号信号,终止程序是默认处理方式,它等价于kill -2 【进程pid】。当然我们也可以通过以下代码自定义收到2号信号的处理方式
先介绍一个函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
其中handler是函数指针,简单理解这个函数就是,我们收到了几号信号,对该信号的处理方式就是handler。handler是我们自定义的一个函数。他的参数int就是几号信号。下面我们看这样一段代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main()
{
void handler(int signo)
{
printf("get a signal: %d\n", signo); //遇到信号我们就打印一句
}
signal(2, handler);//遇到2号信号就执行handler
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
此时,我们再在键盘上使用Ctrl+C或者kill -2,进程就不会终止了,如图
此时我们想终止掉进程需要使用kill -3,退出进程
注意
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的。
3.信号产生的方式
3.1Core Dump
首先我们先解释一下什么叫Core Dump。翻译为中文是“核心转储”。当一个进程要异常退出时,可以选择把进程的用户空间内存数据保存到磁盘上,文件名通常是core。这就叫核心转储。
还记得我们之前学进程控制的时候,在学waitpid的时候,我们知道status是一个整数,我们关注该整数的低16位。低7位,表示的是进程退出时的退出信号,次第八位表示的是进程的退出码,而我们没有提到的第8位,就是代表进程异常退出时是否是否核心转储。默认情况下是不允许产生core文件的,我们可以使用ulimit命令运行产生core文件。
现在我们举几个例子,让进程异常退出,观察一下他的coredump以及core文件。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
if(fork() == 0)
{
//child
printf("I am a child, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(3);
int a = 1 / 0; //除0错误
exit(0);
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %d, coredump: %d, signal: %d\n", (status>>8)&0xFF, (status>>7)&1, status&0x7F);
return 0;
}
由此可见,进程异常退出产生了core文件,coredump为1,core文件的命名方式是core.pid。
我们再来测试一下野指针
3.2为什么c/c++进程会崩溃
本质上就是收到了信号。像除0,野指针这种错误。只要出现错误,最终一定会在硬件上有所体现,进而被OS识别到(OS是软硬件资源的管理者)。
3.3信号产生方式
-
通过终端按键产生信号。如Ctrl+C,Ctrl+\。
-
异常产生信号。
-
调用系统接口产生信号,如调用kill函数
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
下面我们写一段代码模拟一下kill命令
mykill.c
#include <stdio.h> #include <sys/types.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(int argc, char* argv[]) { void use_method(char* proc) { printf("use method: %s pid signo\n", proc);//mykill 2458 9 } if(argc != 3) {//命令行参数说明命令不正确 printf("error method\n"); use_method(argv[0]); } pid_t pid = atoi(argv[1]); int signo = atoi(argv[2]); kill(pid, signo); return 0; }
显然我们可以通过这种方式发送信号给进程。当然如果想不加./,只需要将当前路径添加到环境变量PATH中即可,有兴趣的小伙伴可以 尝试一下
-
软件条件如SIGPIPE, ALARM
其中SIGPIPE在管道中已经介绍过来,今天主要结束alarm函数和SIGALRM信号
#include <unistd.h> unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程.函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <signal.h> void handler(int signo) { printf("got SIGALRM\n"); } int main() { signal(SIGALRM, handler);//为了更好地观察到收到了SIGALRM信号,我们自定义handler alarm(1); while(1) {} return 0; }
4.阻塞信号
4.1信号其他相关概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
4.2信号 在内核中的表示
block和pending是两个位图。任何一个信号都有两个标志位分别表示阻塞(block)和未决(pending)。信号产生时,内核在进程控制块中设置该信号的未决标志,即将该信号的pending为置为1,直到信号递达才清除该标志(置0)。以上图为例,我们分析一下这三种情况
- SIGHUP信号的block和pending位都是0,说明该信号既没有阻塞,也没有产生过,当它递达时执行默认动作
- SIGINT信号的block和pending位都是1,说明该信号产生过,但是正在被阻塞,所以暂时不能递达。虽然他的处理动作是忽略,但没有接触阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再接触阻塞。
- SIGQUIT信号block位是1,pending位是0,说明该信号没有产生过,一旦产生就会被阻塞。他的默认处理动作是自定义的
我们知道,进程在收到信号的时候,不一定是在立即执行的,而是要等到“合适的时间”再去处理。合适的时间指的是进程由内核态->用户态的时候。
所谓内核态和用户态,指的是当前系统所处的状态
信号从发送到递达的过程是这样的:发送信号->修改pending->时间合适->检查block->对应信号没有被block->开始递达。
4.3sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
4.4信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
int sigfillset(sigset_t *set);
//初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号
int sigaddset (sigset_t *set, int signo);//添加某信号
int sigdelset(sigset_t *set, int signo);//删除某信号
int sigismember(const sigset_t *set, int signo);//判断一个信号集是否有某个信号
注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。前四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
使用sigprocmask函数可以读取或更改进程的信号屏蔽字(即阻塞信号集)。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。how参数有以下几种选择
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
下面我们通过 刚刚学习的几个函数,来做这样一个实验
- 先屏蔽2号信号
- 键盘发送2号信号(Ctrl+C),可以预见,2号将会一直被阻塞,一定一直在pending中
- 使用sigpending获取当前进程的pending信号集
- 恢复阻塞,再次查看当前进程pending信号集
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo)
{
printf("get a signal: %d", signo);
}
void printPending(sigset_t* pending)
{
int i = 0;
for(i = 1;i <= 31; i++)
{
if(sigismember(pending, i))
{
printf("1 ");
}
else
{
printf("0 ");
}
}
printf("\n");
}
int main()
{
signal(2, handler);
sigset_t set, oset;
//先置为空
sigemptyset(&set);
sigemptyset(&oset);
//1.先屏蔽2号信号
sigaddset(&set, 2);
sigprocmask(SIG_SETMASK, &set, &oset);
int count = 0;
sigset_t pending;
while(1)
{
sigemptyset(&pending);
sigpending(&pending);//不停获取当前pending信号集,获取到的信号集放进p ending
printPending(&pending);//打印pending信号集
sleep(1);
count++;
if(count == 10)
{
//在count==10的时候恢复阻塞
sigprocmask(SIG_SETMASK, &oset, NULL);
printf("取消阻塞\n");
}
}
return 0;
}
``