【Linux】进程信号
信号是什么
信号是信息的载体,在Unix和Linux环境下,是一种古老、经典的通讯方式,对于现代Unix和Linux来说,依然是非常重要的IPC(进程间通信)方式。
(1)信号:其实是就是一个软件中断,通知正在运行的进程发生了某个事件,进程收到通知后立刻停止当前所干的事情,然后后去处理这个事件。这种软件中断与硬件中断类似,都是提供了一种处理异步事件的方式,但是信号的软件中断却是在软件层面上所实现的中断。
(2)一种信号也就对应一种事件有多种信号也就能代表多种事件。
(3)每一个进程所收到的所有信号,都是有内核负责发送,并且有内核进行处理。
(1)信号的种类:通过命令 kill -l 查看信号列表
通过从上面查看信号列表,儿们可以看出信号的种类有62种。并且其中:1到31号为非可靠信号、34到64号为可靠信号。
可靠信号:本可能造成事件丢失,也叫实时信号
非可靠信号:可能造成事件丢失,也叫常规信号、普通信号
(2)信号的状态
1、递达状态:信号递送并且送达到目标进程
2、 未决状态:处于信号产生与信号递达之间的状态。主要由于阻塞/屏蔽而产生的状态
3、信号处理方式:执行默认动作、忽略、阻塞
4、阻塞信号集(信号屏蔽字):该集合用于设置信号屏蔽,当将这个信号加入到这个集合中,就可以阻塞/屏蔽该信号。后续收到的该信号,该信号的处理将被延后,知道该信号的屏蔽被解除为止。
5、未决信号集:信号从发出到递达的中间过程,叫做未决状态。内核中有个数组专门用来记录信号的未决状态,叫做未决信号集,当信号处于未决状态时,对应的位置被置为1,否则置0。
解决未决状态的方式有两种:
a.此信号解除阻塞
b.此信号被忽略
信号的产生
(1)硬件产生
ctrl+c(2号信号) 、ctrl+|(3号信号) 、 ctrl+z(20号信号)
以上三种都是 产生了一个硬件信号有操作系统转换为软件信号
(2)软件产生
kill + signum pid ---想一个进程发送一个信号,默认发送15号信号
#incldue<sys/types.h>---系统接口
#include<signal.h>
int kill(pid_t pid,int sig)--发送任意信号给任意进程
参数1:pid---进程ID
参数2:sig---信号编号
pid > 0:发送信号给进程号为pid的进程
pid = 0:发送信号给与调用kill函数进程属于同一个进程组内面的所有进程
返回值:成功返回0,失败-1,并且设置errno
#include<signal.h>---库函数
int raise(int sig)--发送一个信号给调用进程,就是自己给自己发信号
参数1:pid---进程ID
返回值:成功返回0,失败非0
#include<stdlib.h>
void abort(void)---给进程自己发送SIGABRT,引发进程非正常终止,出错时才用并且设置core文件
#include<unistd.h>---系统接口
unsigned int alarm(unsigned int seconds)---等到seconds后,给进程发送一个SIGALRM信号,终止当前进程
参数seconds:指定时间(秒)
返回值:返回0或者闹钟剩余秒数,无失败情况
注意: 上面的abort函数,调用后会设置core文件,其实就是core dumped(核心转储)为的就是:进程非正常退出后,方便程序调试的。但是这个功能是默认关闭的。这些信息都属于限制信息。
默认关闭状态下core文件大小为0,如果想要使用这core文件,那么只需要改变文件大小使其不为0,则打开; 查看进程中限制信息:ulimit -a 设置命令:ulimit -c + 文件大小修改core文件大小
信号在进程中注册(修改信号pending位图)
我们都知道进程在内核中进行调度是通过PCB的,在PCB中有两个结构体一个pending结构体存储当前收到的信号,还有一个结构体blocked用于存储现在都有哪些信号要被阻塞。
(1)pending位图(未决信号集合)在PCB中的结构:
进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending pending:
struct sigpending{
struct sigqueue *head, *tail;//内核当中的双向链表
sigset_t signal;//结构体,只有一个成员是一个数组,用来做位图标记信号
};
存储信号的双向链表结构体:
struct sigqueue{
struct list_head list;
int flags;
siginfo_t info;//存储信所携带的信息
struct user_struct *user;
};
- 信号的注册其实就是在pcb中进行记录
- 信号集合:sigset_t 结构体(保存信号):进程记录一个信号时是通过这个结构体的位图来记录的(1号信号在位图第0位置存储,位图的该位置原本是0,如果有信号,该位置置1),这个位图的位数+1代表的就是指定 的信号存储位置
注册流程: 产生的信号发送给了进程,操作系统进程中的pending位图中信号对应的标记为,,将0置为1,表示该信号的已经存在,并且向pending结构体中sigqueue链表中添加一个注册信号的信号节点。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但是还没有来得及处理,或者该信号被进程阻塞。
(2)非可靠信号和可靠信号在进程注册有什么不同?
非可靠信号: 若当前未决信号集中指定的信号已经注册,则什么都不干,不去修改文位图也不向sigqueue链表中添加信号节点。
可靠信号: 不管当前信号是否已经注册,都回去修改对应位图,向sigqueue链表中添加一个新的信号节点。
注意:
1、位图只是标记有没有这个信号,而sigqueue链表才真正的标记了这个信号有多少个。
2、这也就同时印证了上面非可靠信号容易造成事件丢失的性质了,假如同时来了三个相同的非可靠信号,只有第一个注册并且添加到sigqueue链表当中,后面两个相当于没有注册,这不就丢失了吗!
3、总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)
信号在进程中注销(修改信号pending位图)
(1)注销: 其实就是在此修改PCB中的未决信号集合pending,删除其位图上对用的信号标记,并且删除sigqueue中的节点。
**注销流程:**去PCB中的pending(未决信号集合)中位图去查找相应的信号标志位,将标志位由1置为0,并且同时删除sigqueue中的对应注销信号的信号节点
(2)非可靠信号和可靠信号在进程注销有什么不同?
非可靠信号: 删除当前信号的sigqueue节点,并且修改位图置为0。
可靠信号: 删除节点后,判断是否还有相同的信号节点,若没有,则将位图置0。
信号在进程中处理(信号递达)
前面说过信号的处理方式有三种:执行默认动作、忽略、阻塞,
(1)默认动作: 就是操作系统原本既定义好号的处理方式
(2)忽略: 收到信号后什么也不干,会注册,注销,但是处理方式就是什么都不干
(3)自定义: 修改原有操作系统定义好的处理动作,只执行自定义的动作
(4)信号
当然那么信号的处理方式有三种那也就意味着,信号的处理方式是可以修改的。下面有两个信号安装函数来改变信号处理方式:
#include<signal.h>--存在内核版本差异性
sighandler_t signal(int signum,sighandler_t handler);
signum:信号id
handler:信号的操作处理方式,是一个回调函数
1、SIG_IGN将信号处理方式改为忽略
2、SIG_DFL将信号的处理方式改为默认方式
3、回调函数typedef void(*sighandler_t)(int) 没有返回值但是有一个int参数的函数
最后把signum作为参数传入回调函数作为参数----作为信号捕捉使用
#include<signal.h> sigaction>---实现---->signal
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)
signum:信号值
struct sigaction *act:signum当前要要修改的新的动作
struct sigaction *oldact:用于获取signum信号原有的动作---便于在还原回去要定义两个结构体作为参数;
struct sigaction newact
{
newact.sa_handler=//定义回调函数;
newact.sa_flags=0,//决定使用哪个回调函数;0位默认使用sa_handler回调函数
sigemptyset(&newact.sa_mask);//清空临时要阻塞的信号集合
};
struct sigaction oldact;//接受原有动作
(5)signal()和sigaction()栗子运用:
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
struct sigaction oldact;//接受原有动作
void sidcd(int signum)
{
printf("recv a signal:%d\n",signum);
sigaction(signum,&oldact,NULL);//将信号处理方式有自定义改成默认
}
int main()
{
//都是自定义信号处理方式
//signal(2,sidcd);
//signal(3,sidcd);
struct sigaction newact;
newact.sa_handler = sidcd;//设置自定义回调函数
newact.sa_flags = 0;//=0时默认使用sa_handler回调函数
sigemptyset(&newact.sa_mask);//清空临时要阻塞的信号集合
sigaction(2,&newact,&oldact);
while(1)
{
printf("=====================\n");
sleep(3);
}
return 0;
}
(6)信号处理流程
处理流程: 在PCB有一个pending(未决信号集合),这是有一个2号信号到来,在pending中进行注册后。开始处理信号时,那么在PCB还有一个处理动作数组handler,其中存储的是每一个信号自己的处理动作,而pending中信号的值就是handler数组中的下标。例如:一个2号信号在PCB中已经注册了,那么处理是就去handler数组中2号下标去找该信号的对应处理动作。
自定义处理方式的捕捉流程
注意:一个信号虽然在PCB中进行了注册,但是并不是立即就会处理而是在合适的时机,合适的时机就是从内核态返回到用户态的时候。
信号有三种处理方式,其中忽略、默认都是操作系统内核中所定义好的在内核态中运行。而自定义处理方式由用户自己定义,那就意味着自己写的就运行在用户态当中,那么当我们自定义信号处理方式时,需要先调用自定义函数处理,这个过程叫**信号捕捉*
信号在进程中阻塞(屏蔽)
(1)阻塞: 阻止一个信号被递达(信号依然可以注册,只是暂时不被处理)
(2)如何阻塞: 只需要在PCB中将这个信号在阻塞集合blocked中标记起来就行了
(3)阻塞流程
阻塞流程: 一个信号到来在pending位图中进行注册,在信号由内核态返回用户态时,就要准备处理当前进程递达的信号,但是在处理递达信号之前还有一步操作那就是去blocked集合中查找看该信号有没有被标记,如果有那么则该信号阻塞不处理,返回主控流程,没有则信号正常处理去handler动作数组找相应动作进行处理,完毕后返回主控流程。
信号的阻塞---阻止一个信号被递达(信号依然可以注册,只是暂时不被处理)
阻塞一个信号只需要在PCB中将这个信号在阻塞信号集合中标记起来
#include<signal.h>
int sigprocmask(int how,const, sigset_t *set,sigset_t *oldset)---设置进程的一个信号掩码
参数1:how:对blocked位图要进行什么样的操作
SIG_BLOCK:添加一个阻塞信息 blocked | set
SIG_UNBLOCK:解除信号阻塞 blocked & (~set)
SIG_SETMASK:设置阻塞信号集合 blocked = set
参数2:sigset_t *set:当前阻塞信息集合
参数3:sigset_t *oldset:获取原有的状态,便于还原回去
向集合中添加信号:
int sigfillset(sigset_t *set)---将所有信号添加到集合当中
int sigaddset(sigset_t *set,int signum)---将指定信号添加到集合当中
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sigcb(int signum)
{
printf("recv signal:%d\n",signum);
}
int main()
{
signal(SIGINT,sigcb);
signal(40,sigcb);
sigset_t mask_set,old_set;//设置信号集合
sigemptyset(&mask_set);//清空信号集合
sigfillset(&mask_set);//添加所有信号到集合中
sigprocmask(SIG_BLOCK,&mask_set,&old_set);//阻塞mask_set中所有信号
printf("press enter to continue!\n");
getchar();//不按回车,则流程就卡在这里
sigprocmask(SIG_UNBLOCK,&mask_set,NULL);//解除阻塞
//sigprocmask(SIG_SETMASK,&old_set,NULL);//解除阻塞
return 0;
}
输入:
结果:
结论:在所有信号都被阻塞时,所有的信号都可以注册但是都不能被处理,此时我们自定义了2号和40号信号的处理方式。我们用硬件产生2号信号,用软件产生40信号,刚好这两个被触发了5次。按下解除信号阻塞,信号开始处理,此时输出一次2号信号,五次40信号,明明都是输入5次,为什么?2号是非可靠信号,40号是可靠信号。因为这也正印证的可靠信号和非可靠信号在进程中注册的方式了!
尽管如此还是有一些特殊信号:就是9-SIGKILL、19-SIGSTOP信号,这两个无法被阻塞、无法被自定义、无法被忽略。
竞态条件
(1)竞态条件: 在多个执行流中,对同一个代码竞争执行。
(2)函数的可重入和不可重入
1、函数的重入:在多个执行流当中,重复进入同一个函数执行代码
2、不可重入:如果一个函数重入之后有可能造成数据二义或者程序的逻辑紊乱,这个函数则不可重入
3、可重入:函数重入之后,不会出现任何影响
(3)重入与不可重入判断依据
1、这个函数当中是否对全局数据进行了非原子安全操作
(4)重入与不可重入函数用处
1、当自己设计函数/使用别人函数的时候,根据使用场景就要考虑函数的重入状况
注意:malloc和free是不可重入函数,并且malloc什么的地址空间并没有立刻分配空间,使用的时候在分配
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int a = 0;
int b = 0;
int test()
{
a++;
sleep(10);
b++;
return a+b;
}
void sigcb(int signo)
{
printf("signal:%d\n", test());
}
int main()
{
signal(SIGINT, sigcb);
printf("main:%d\n", test());
return 0;
}
volatile关键字: 用于修饰一个变量保持变量的内存可见性,防止编译器过度优化(每次都从内存中重新获取变量的数据)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
volatile int a = 1;
void sigcb(int signo)
{
a = 0;
}
int main()
{
signal(SIGINT,sigcb);
while(a)
{
}
return 0;
}
情况一:此时全局变量没有关键字volatile,且普通编译。这时的代码编译运行,ctrl+c,
产生SIGINT信号,a = 0,主函数循环退出。
情况二:但是经过编译优化后,gcc -O1/2 ;一二级优化,ctrl+c此时主函数不退出,
因为编译器发现在主函数中main中a变量不断在使用,使用直接将a放入寄存器,
为了提高效率,往后每次直接从寄存器中获取。此时虽然内存中a=0了,
但是寄存器中的还是a=1,所以主函数还在无限循环不能退出。
情况三:此时在全局变量a全面加上volatile关键字,在进过优化编译时。
a虽然被不断使用,但是由于关键字volatile所以编译器没有吧a放入寄存器,
所以ctrl+c产生SIGINT信号将a=0,此时主函数正常退出
编译过程中优化: gcc -O1/2 ;一二级优化
SIGCHLD信号
SIGCHLD信号:
子进程退出时操作系统个父进程发生的信号,因为SIGCHLD信号默认处理方式就是什么都不敢,一次父进程相当于没关注子进程的退出状态,因此导致子进程成为僵尸进程,否则就要进行进场等待(一直阻塞等待)