欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

操作系统 — 浅析信号

程序员文章站 2022-07-12 12:08:37
...

浅析信号







首先每个信号都有一个名字,这些名字都以3个字符SIG开头.例如,SIGABRT是夭折信号,当进程调用abort函数时产生这种信号.SIGALRM是闹钟信

号,由alarm函数设置的定时器超时后将产生这种信号. 信号的种类是非常庞大的. 在头文件<signal.h>中,信号名都被定义为正整数常量(信号编

号).实际上,实现将各信号定义在另一个头文件中,但是该头文件又包括在<signal.h>中.内核包括对用户级应用程序有意义的头文件,这被认为

一种不好的形式,so如若应用程序和内核两者都需使用同一定义,那么就将有关信息放置在内核头文件当中,然后用户级文件再包括该内核头件.



实际上,不包括编号为0的信号,kill函数对编号0有特殊的应用. 当然在生活中产生信号的条件非常之多:1.比如当用户按某些终端键时,引发终

的信号. 2.硬件异常产生信号  3.进程调用Kill 用户可用kill(1)命令将信号发送给其他进程. 4.当检测到某种软件条件已经发生,并应将

其通知有关程时也产生信号.
.


信号分为可靠信号和不可靠信号



不可靠信号:又叫非实时信号,linux的信号继承unix,所以会有一点问题. 1.在unix信号中,一旦信号抵达了,信号执行了自己的信号处理函数之

后,信号在内核中将自己的信号处理方式就会恢复至默认,不过我们的Linux对这种情况进行了优化已经解决了问题. 2.在unix系统下会导致信号丢

,也就是瞬间来到了许许多多个信号,最多只能处理一个信号(信号丢失),这种情况下linux没有进行优化,其实是拥有解决方法:比如对信号进

行排队等等但是呢! 因为linux现在已经用了这么多年了,很多基于避免信号丢失的代码已经写成了,所以对于Linux来说,就算你现在有更好的解

决方法,但是你不能够更改1-31号信号的结构. 通俗的来说,就是你在盖楼,当你盖到50层的时候,发现第一层有一点小小的问题. 而这49层又基于

第一层,你不可能因为这个小问题而让49层楼重新盖吧?


可靠信号:又叫实时信号,也就是说1.信号在执行自己的信号处理函数之后,信号在内核中的处理方式不会恢复回默认! 2.不会出现信号的丢失.

其实我们64个信号当中,34-64号信号全都是可靠的信号,但是这些函数用户不能使用. 那些都是系统用来处理线程当中的问题的.


处理信号



在某个信号出现时,可以告诉内核按下列3种方式之一进行处理,我们称之为信号的处理或与信号相关的动作.

1.忽略此信号. 大多数信号都可使用这种方式处理,但是有两种信号绝对不会被忽略. 它们是SIGKILL和SIGSTOP. 这两种信号不能被忽略的原因是:

们向内核和超级用户提供了使进程终止或者停止的可靠方法. 另外,如果忽略某些由硬件异常产生的信号,则此进程的运行行为是未定义的.

2.捕捉信号. 为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数. 在用户函数中,可执行用户希望对这种事件进行的处理. 例如

,若正在编写一个命令解释器,它将用户的输入解释为命令并执行之,当用户用键盘产生中断信号时,很可能希望该命令解释器返回到主循环,终

止正在为该用户执行的命令. 如果捕捉到SIGCHLD信号,则表示一个子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进

程ID以及它的终止状态. 所以我们总结一点! 不能捕捉SIGKILL和SIGSTOP这两个信号

3.执行系统的默认动作. 下图就是一些信号的说明和系统默认动作! 我们看到大多数信号的系统默认动作是终止该进程.

操作系统 — 浅析信号

绝大部分信号的默认动作无外乎就是五种!

core dumped:将你的内存相关退出信息,存储到core文件中,以便于你以后查看.

IGN:告诉父进程,子进程已经执行结束等待回收

term:什么也不做直接死亡

cont:程序进行执行

stop:让你的程序暂停.

在系统默认动作列,"终止+core"表示在进程当前工作目录的core文件中复制了该进程的内存映像(该文件名为core,由此可以看出这种功能很久之

前就是UNIX的一部分). 大多数UNIX系统调试程序都使用core文件检查进程终止时的状态. 在下列条件下不产生core文件:

 (a)进程是设置用户ID的,而且当前用户并非程序文件的所有者;

 (b)进程是设置组ID的,而且当前用户并非该程序文件的组所有者;

 (c)文件太大.  core文件的权限通常是用户读写.


我们的信号其实也叫做"软中断". 何为中断? 硬件的中断:正在运行的硬件,突然被打断去做另外一个事情,然后完成后又回来继续执行刚刚没

做完的事情. 所以呢信号就是在软件上的对中断的模拟. 提到信号大家绝壁会想到一个指令! kill指令. 我们平时使用Kill来发送信号,所以

对他就很眼熟,其实呢kill指令的底层是使用kill函数构成的! 所以呢我们来认识一下这个函数:


kill函数


kill命令就是通过调用kill函数生成的,kill函数的作用可大了. kill函数将信号发送给内核,然后呢内核根据信号的对应操作对指定的进程进行

执行.比如你向一个进程发送SIGKILL信号,那么内核就会终止掉这个进程! 我们继续来了解它的情况:

 #include <sys/types.h>
 #include <signal.h>

 int kill(pid_t pid, int sig);
pid参数就是你信号发送至的进程ID,后面的sig就是发送那个信号. 对于pid参数有好几种情况分别为:

pid > 0  : 给指定的pid发送信号                                 pid = 0  :本进程组的任何一个进程发送消息.

pid = -1 : 给任何有权发送信号的进程发送信号                    pid < -1 :|pid|的进程组发送信号.


信号的阻塞



实际执行信号的处理动作称为信号递达,信号从产生到递达之间的状态称之为信号未决,进程可以选择性的阻塞某个信号,被阻塞的信号产生时将

保持在未决状态,知道进程解除对此信号的阻塞,才执行实际执行信号的处理动作称为信号递达,信号从产生到递达之间的状态,称为信号未决.进

程可以选择阻塞某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作. 阻塞和忽略是不相同的,

只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作.达的动作,阻塞和忽略是不相同的,只要信号被阻塞就不会递达,而忽略

是在递达之后可以选择的一种处理动作!

操作系统 — 浅析信号
在PCB当中,分别使用了pending,block,handler三个表项来分别记录 信号是否递达,应该让那些信号阻塞,每个信号对应的处理函数.

每个信号都有两个标志位分为表示阻塞和未决,还有一个函数指针表处理动作.信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号

递达才消除该标志.

如果在进程在阻塞某信号时,该信号产生过多次,Linux是这样实现的:常规信号在抵达之前产生多次只计一次,而实时信号在抵达之前产生多个信

号可以依次放到一个队列中,每个信号只能有一个bit的未决标志,非0既1,不记录该信号产生了多少次,阻塞标志也是这样表示的. 因此呢,未决

和阻塞标志可以用想用的数据类型sigset_t来存储,sigset_t为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有

效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中类似. 阻塞信号集也叫做当前进程的信号屏蔽字,屏蔽这样理解"是阻塞 不是忽略".

常见信号处理函数


//sigset_t类型对于每种信号用一个bit表示 "有效"或者"无效" 接下来我们来认识一下信号集操作函数
#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 sigismemeber(const sigset_t *set,int signo);
//是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

int sigprocmask(int how,const sigset_t *set,sigset_t *oset);
//读取或更改进程的信号屏蔽字(阻塞信号集)如果成功返回0 失败返回-1

int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出,调用成功则返回0,出错则返回-1.
在这里我们着重介绍一下sigset_t的结构,我们来看看他究竟是一个什么东西:

typedef struct
{
    unsigned long int __val[_SISSET_NWORDS];
}__sigset_t;
当你定义了一个sigset_t变量的时候,一定要要记住!! 使用sigemptyset()函数对变量初始化,否则信号集可能会出现随机值!!

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how: 
SIG_BLOCK: mask = mask = mask | set;
SIG_UNBLOCK: mask = mask & ~set;
SIG_SETMASK: mask = set;
//往内核当中设置,存储阻塞信号的状态.
sigprocmask的返回值如果失败返回-1 成功返回为0. 接下来我实现一个关于信号阻塞的实例,用来打印出block表当中的内容. 也就是说打印出被
阻塞的信号. 那么话不多说我们开始写代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void printsigset(sigset_t *set)
{
	int i = 0;
	for(;i<32;i++){
		if(sigismember(set,i))
			putchar('1');
		else
			putchar('0');
	}
	puts("");
}

int main()
{
	sigset_t s,p;
	sigemptyset(&s);
	sigaddset(&s,SIGINT);
	sigprocmask(SIG_BLOCK,&s,NULL);
	while(1)
	{
		sigpending(&p);
		printsigset(&p);
		sleep(1);
	}
	return 0;
}
接下来带上代码注释:

操作系统 — 浅析信号
这个程序的大概意思就是 我们阻塞一个信号集,让它一直处于未决状态,并把它里面的信号编号显示出来,比如中途我们加入了一个ctrl+c. 后面
信号集里面就会出现这个信号,然后他们还是一直处于未决状态. 我们来看看结果:
操作系统 — 浅析信号

特别提醒:如果一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待理信号的阻塞时,待处理信号就会立刻被处理. 如果掌握了这些之后,我觉得我们就可以继续学习信号! 信号的捕捉. <-----博客传送门