目录
Simple Signal
信号作用
我们执行一个程序,这个程序会出现很多种状态,程序执行完正确,执行完后错误,或者在执行的过程中直接被终止了。那么这个被终止的状态就和信号有关联。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int main()
{
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
这个进程就收到了来外部的信号,也就是键盘上的CTRL+C命令。这个命令被操作系统内存获取到。然后控制这个进程终止~我们可以使用信号的函数来证明,这个进程是收到信号后被终止的~
Signal函数
函数功能:设置一个信号的对应动作
参数解读:Signum 表明需要处理的信号,除了9号信号SIGKILL和19号信号SIGSTOP以外的信号这个函数都可处理。
Handler:对这个信号的处理方法这个方法可以是以下三种状态
关联动作 处理方式 SIG_DFL 执行该信号的默认处理方式 SIG_IGN 忽略这个信号 sighandler_t 信号的捕捉,内核在处理该信号时切换到用户态就调用这个方法,对信号进行处理
1.SIG_DFL默认处理动作
执行CTRL+C系统默认执行2号信号终止该进程
2.SIG_IGN 忽略该信号
#include<stdio.h> #include<unistd.h> #include<signal.h> #include<stdlib.h> int main() { signal(2,SIG_IGN); while(1) { printf("hello world\n"); sleep(1); } return 0; }
此时我们使用2号信号这个信号已经被忽略了,就起不到终止的作用了,我们可以使用CTRL+\(SIG_QUIT)来结束这个进程
3.提供信号处理函数
#include<stdio.h> #include<unistd.h> #include<signal.h> #include<stdlib.h> void handler(int signo)//信号处理函数 { printf("git a signal:%d",signo); } int main() { signal(2,handler); while(1) { printf("hello world\n"); sleep(1); } return 0; }
此时我们再使用2号信号,这个信号就会被捕捉到。
notice:前台进程可以被control+c掉,但是后台进程不可以,要是想让程序在后台运行,在可执行文件后面+&就可以在后台运行。
信号列表
通过kill -l命令行指令来查看信号列表(1~31)为普通信号,(34~64)为实时信号
信号的处理方式
默认的信号处理方式
信号的产生
1.硬件中断
使用键盘命令向内操作系统发送指令,操作系统对进程执行对应的信号处理.
2号信号和3号信号的作用不同一个是进行core Dump对这个进程中的一段进行核心转储,其实就是记录这个进程中的错误处。
core Dump
首先编写一个错误的进程
#include<stdio.h> #include<unistd.h> #include<signal.h> #include<stdlib.h> int main() { while(1) { printf("hello world\n"); sleep(1); int a=2; a/=0; } return 0; }
我们在工程中要是想找到一个很小的错误,core Dump标志进行程序错误的精准错误。
core Dump方法在xshell上默认关闭。我们可以手动打开。使用Ulimit命令可以查看当前系统下的资源使用
默认标志位是0,可以使用ulimit +每个资源-后的字母进行打开关闭
ulimit -c 1024打开core功能
core Dump标志位
进程的退出状态的获取
在Linux中,当一个进程退出的时后,它的退出码和退出信号都会被设置;当一个进程异常退出时,进程的退出信号会被设置,表明当前进程的退出原因;如果有必要,操作系统会设置退出信息中的core dump标志位,方便我们后期进行调试;
进程退出时的退出码和退出信号,我们是利用waitpid函数中的第二个参数status来获取的,所以core dump标志位也同样可以获取:
退出码的获取:(status>>8)&)0xFF,
推出信号的获取:status&0x7F
core 标志位获取:(status>>7)&1;
代码实例获取上面信息:
int main() { if(fork()==0) { while(1) { printf("i am child....\n"); int a=10; a/=0; } } int status=0; waitpid(-1,&status,0); printf("exit code: %d ,exit sig: %d, core dump flag: %d \n",(status>>8)&0xFF,status&0x7F,(status>>7)&1); return 0; }
我们再将core dump打开试一下
因为代码中的错误,这个标志位就起到作用了~
2.系统函数发送信号
1.kill命令 -要发送的信号 进程pid
2.kill函数发送信号
//模拟一个kill函数,kill 谁 怎么执行 static void Usage(const char*proc) { printf("Usage:\n\t %s signo who\n",proc); } int main(int argc,char*argv[]) { if(argc!=3) { Usage(argv[0]);//如果只输入了1个参数,就进入提示, return 1; } int signo=atoi(argv[1]);//把第一个参数转为进程pid int who=atoi(argv[2]);//第二个参数转为记号信号 kill(who,signo); printf("signo: %d,who: %d\n",signo,who); return 0; }
3.rise函数发送信号(自己终止自己)
void handler(int signo) { printf("get a signo:%d\n", signo); exit(1); } int main() { signal(8, handler); int count = 5; while(1){ count--; printf("I am process. I will exit!! %d\n", count); sleep(1); if(count <= 0){ raise(8); } } return 0; }
4.abort函数发送信号
void handler(int signo) { printf("get a signo:%d\n", signo); } int main() { signal(6, handler); int count = 5; while(1){ count--; printf("I am process. I will exit!! %d\n", count); sleep(1); if(count <= 0){ abort(); } } return 0; }
notice:abort向进程发送的是SIGBRT信号,无论信号是捕捉还是忽略,这个函数都能执行成功~
3.软条件产生信号
通过某种软件(OS),来触发信号的发生,系统层面设置定时器,或者某种操作而导致条件不就续等这样的场景下,触发的信号;SIGPIPE 和 SIGALRM 信号都是由软件条件产生的信号。SIGPIPE :当读端关闭,写端一直在写,最终写端会收到该信号,这就是一种典型的软件条件触发的信号发生;接下来以alarm函数 和 SIGALRM 信号为例。
alarm函数:设置一个闹钟来传递信号
//所需头文件 #include <unistd.h> //函数原型 unsigned int alarm(unsigned int seconds);
函数功能:
让操作系统在seconds秒之后给当前进程发送SIGALRM信号,SIGALRM信号的默认处理动作是终止进程。这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。
举个例子:
某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被别人吵醒了,但还想多睡一会儿。于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。
参数及返回值说明:
如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数;
#include <stdio.h> #include <unistd.h> int main() { alarm(3); while(1){ printf("hello linux!\n"); sleep(1); } return 0; }
闹钟可以归零
4.硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
内存管理单元简称MMU,它负责虚拟地址到物理地址的映射,并提供硬件机制的内存访问检查。MMU使得每个用户进程拥有自己独立的地址空间,并通过内存访问权限的检查保护每个进程所用的内存不被其他进程破坏。
信号的阻塞
信号再理解
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
操作系统的信号是怎么设置的。为什么kill + 进程id + 几号信号 操作系统就会对这个进程做处理。
信号在操作系统中的表示
和c++,c语言中的结构体一样也是被放在一个结构中,不过这个结构叫做信号集中,这个信号集长这样
- 信号也被保存在进程控制块中,这个指针就指向位图结构,
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),他们都是位图结构;还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会在改变处理动作之后再接触阻塞。
- SIGQUIT信号未产生过,但一旦产生SIGQUIT信号,该信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前,这种信号产生过多次,POSIX.1允许系统递达该信号一次或多次。Linux是这样实现的:普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里,这里只讨论普通信号。
在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。
在pending位图中,比特位的位置代表某一个信号,比特位的内容代表是否收到该信号。
handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
block、pending和handler这三张表的每一个位置是一一对应的
sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
#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);
- sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
- sigaddset函数:在set所指向的信号集中添加某种有效信号。
- sigdelset函数:在set所指向的信号集中删除某种有效信号。
- sigemptyset、sigfillset、sigaddset和sigdelset函数都是成功返回0,出错返回-1。
- sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1。
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 如果oset是非空指针,则读取进程当前的信号屏蔽字通过oset参数传出。
- 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。
- 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
返回值:若成功则为0,若出错则为-1
注意: 如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask函数返回前,至少将其中一个信号递达
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1
#include <signal.h> int sigpending(sigset_t *set);
信号捕捉
1.用户空间和内核空间
- 每一个进程都有自己的进程地址空间(虚拟地址空间),并且由两部分组成:内核空间和用户空间 :
- 用户所写的代码和数据均存放与用户空间,通过用户级页表与物理内存建立映射关系;
- 内核空间存储的是操作系统的代码和数据,通过内核级页表与物理内存建立映射关系;
- 内核级页表是一个全局的页表(意味着独一份),它是用来维护操作系统的代码与进程的代码之间的关系。每个进程看到的代码和数据数据是不一样的,因为他们是不同的页表,但是对于内核页表是他们共享的,如下图所示:
信号的处理时机
信号的处理时机为:信号是保存在进程控制块中pendling位图中,当进程从用户态到内核态,再返回到用户态的时候就会对信号进行处理~