进程信号
信号量(system v信号量)
- 作用:实现进程控制,也就是可以实现同步和互斥功能
- 本质:计数器 + PCB等待队列。计数器是指对资源的计数,也就是对所占的资源进行+1,-1操作
实现互斥的方法
- 前提条件:
信号量当中的资源计数器只有两个取值,也就是0或者1。其中0表示当前资源不可用,1表示当前资源可用
- 访问:
当一个进程需要访问一个临界资源的时候,会先获取信号量,预算信号量当中计数器的值。
- 预算:对当前信号量中的计数器进行-1操作,然后判断信号量当中的计数器是否小于0。
如果信号量当中的计数器值小于0,则表示信号量之前的值为0,表示当前的临界资源不可以被访问,然后会将当前获取信号量的进程放到PCB等待队列中去,进行阻塞等待。
如果信号量当中的计数器值等于0,则表示信号量之前的值为1,表示当前的临界资源可以被访问,然后对信号量当中的计数器进行-1操作,再访问临界资源。
- 释放:
如果临界资源访问完成,需要结束对临界资源的访问,然后对信号量当中的计数器进行+1操作,然后唤醒PCB等待队列中的进程(先进先出)。
减一操作 --> p操作
加一操作 --> v操作
- 实现同步的方式
同步即保证进程对临界资源访问的合理性,在实现同步的过程中,计数器的取值不再限制为0或者1,而是任意整数。
初始化信号量:将信号量当中的资源计数器设置为资源的数量。
- 访问临界资源:
先访问信号量:对资源计数器进行-1操作,判断资源计数器的值是否大于0。如果小于0,则将该进程放在PCB等待队列中;如果大于等于0,则访问临界资源。
以停车场为例,资源计数器就是统计停车场还有多少空位置,PCB等待队列就是表示车辆在停车场外排队。
当车位占满的时候,表示临界资源数量为0,也就是资源计数器的值是0;这时如果还有车子想进入停车场,就会在车库门前排队,每排一辆车,就会对信号量中的资源计数器-1操作,如图有3辆车在等待,资源计数器的值为-3。
- 释放资源,通知PCB等待队列的做法
当临界资源被进程所释放,就会对资源计数器进行+1操作,通知PCB等待队列当中的的进程访问临界资源
- 计数器+1操作完毕之后,计数器当中的值小于等于0。这时需要通知PCB等待队列当中的进程(因为PCB等待队列不为空)。
- 计数器+1操作完毕之后,计数器当中的值大于0。就不需要通知PCB等待队列(因为PCB等待队列本身就是空的)。
信号的基本概念
信号是一个软件中断,可以打断当前正在运行的进程,让该进程去处理信号的事件,当前的进程需要处理信号所带来的的操作。就像我么一看到红灯就会下意识的进行等待,看到绿灯就会想到可以通行。
- 信号的种类
在linux操作系统中,总共有62种信号。前31种信号(1 - 31)被称为不可靠信号,信号有可能会丢失,非实时信号;后31种信号(34-64)被称为可靠信号,信号是不可能被丢失的,实时信号。
信号的产生
硬件产生
- ctrl + c:给前台进程发送一个SIGINT信号(2号信号),中断当前的进程。
- ctrl + z:给前台进程发送一个SIGTSTP信号(20号信号),暂停当前进程。
- ctrl + |:给前台进程发送一个SIGQUIT信号(3号信号),使进程崩溃,产生coredump文件
软件产生
- kill [pid]:给前台进程发送一个SIGTERM信号(15号信号)
- kill -[signalno] [pid]:给指定进程发送指定的信号
就像kill -9 [pid]表示给进程发送SIGKILL信号(9号信号),强杀信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main()
{
printf("i am finfshed\n");
kill(getpid(),2);//给当前进程发送一个中断信号
while(1)
{
printf("如果打印到这里,就说明有问题!\n");
sleep(1);
}
return 0;
}
调用函数产生
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
#include <stdlib.h>
void abort(void);//封装kill函数,谁调用就给谁发送SIGABRY信号(6号信号)
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,
//该信号的默认处理动作是终止当前进程。
还有当我们出现访问空指针或者内存越界访问的时候,就会出现11号信号,SIGSEGV段错误信号
man 7 signal //查看信号的作用
信号的注册
- 查看信号的源码
因为在linux中默认是不安装kernel的内核源码,所以说我们在查看的时候,需要先将内核源码安装好。
sudo yum install kernel -devel -y
然后在root用户下进行查找sched.h文件
find /usr/src/ -name sched.h
因为信号的不同头文件下存在着一些引用的关系,所以说一些结构体的定义不在sched.h头文件下,我们可以在查找之前,在/usr/src/kernels/3.10.0-1062.18.1.el7.x86_64/include/目录下建立tags索引文件,然后可以使用tags的查找方式进行跨文件查找。
ctags -R //建立tags索引文件
在文件中可以使用ctrl + ] 获取当前光标下的单词作为tag名字进行跳转
ctrl + T //跳转到前一次的tag处
然后用vim 打开这个目录下的sched.h文件,使用/[关键字],然后就可以看到task_struct结构体了。下面是注册码的一些与信号有关的成员变量之间的关系。
两种注册信号的情况
sigqueue队列是内核当中维护的一个队列,其队列当中的每一个元素对应信号的一个处理节点。
- 非可靠信号:(1 - 31)
会更改sig位图中对应的字节为1,在sigqueue队列中增加对应信号所对应的节点
对于非可靠信号,当sigqueue队列多次收到同样一个信号的时候,只添加一个节点,也就是相当于第二次收到的同样的信号被丢弃掉。
- 可靠信号:(34 - 64)
当sigqueue队列第一次收到一个可靠信号的时候,会更改sig位图中对应的字节为1,然后在队列中增加对应信号所对应的节点。
而当第二次收到同样一个信号的时候,先判断sig位图中的对应字节位置是否为1,并且在sigqueue队列中增加信号所对应的节点
两种信号注销的情况
- 非可靠信号:(1 - 31)
会将sig位图中对应的字节位置置0,并将在sigqueue队列中的节点去除掉(操作系统拿着节点去行使该信号的功能)
- 可靠信号:(34 - 64)
将待处理的信号在sigqueue队列中的对应节点进行盘算,判断当前处理的对应信号的节点是否在sigqueue队列中还有相同类型的节点,
有:则不改变sig位图的对应字节上的1,即不把1置为0
没有:则直接将sig位图对应位置的1置为0
信号捕捉
- 信号的处理方式
默认处理方式:SIG_DEL (执行一个动作或者执行一个函数)
忽略时的处理:SIG_IGN (不会做任何事)
自定义处理方式:我们自定义的处理方式
问题1:为什么会产生僵尸进程?为什么父进程来不及回收子进程的退出信息,导致子进程为僵尸进程?
僵尸进程的产生是因为当子进程退出时,父进程没有回收子进程的退出状态,这个时候子进程退出的消息父进程没有接收,子进程就成为一个僵尸进程。至于为什么父进程来不及处理,是因为子进程在退出的时候,会给父进程发松一个SIGCHID信号,而操作系统对SIGCHID信号的处理方式恰好为忽略状态(SIG_IGN),这时父进程就是不会做任何事的状态。
问题2:从信号的角度来看,应该如何解决僵尸进程的问题?
- 使用signal(SIGCHLD,SIG_IGN),在这种方式下,子进程状态信息会被丢弃,也就是自动回收了,所以不会产生僵尸进程。(爸爸不管儿子了,儿子自己释放掉自己吧)
- 从其他角度看,我们可以fork两次,让第一次fork的子进程在fork完成后使用(exit)直接退出,这样第二次fork得到的子进程就没有父进程了,就变成了孤儿进程,它会被自动过继给老祖宗init进程,init会负责释放它的资源,这样就不会由"僵尸"产生了(把原本的父子关系变成爷孙关系,然后直接杀掉中间的爸爸,爷爷也不管孙子了,孙子就成了孤儿,自己释放自己)
- 因为父子进程之间的抢占式执行,所以说父子进程之间无法确定哪一个进程先执行,为了防止子进程在退出的时候,父进程陷入循环无法得到子进程的退出状态,可以在子进程进行的时候,让父进程进行进程等待, 以便子进程可以正常退出。(这种方法,爸爸会照顾儿子一生,直至儿子死亡,并妥善处理完儿子的后事)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- signum:需要我们自己定义的一个信号值
- handler:一个函数指针类型的参数 --> typedef void (*sighandler_t)(int);
- void:是返回值的类型
- int:参数类型,即是哪一个信号触发操作系统调用该函数
上面的代码,我们在运行的时候,通过pstack [pid]查看的时候显示是程序是在sleep()函数中
但是当我们采取硬件中断ctrl+c 和 ctrl+\ 时,却发现程序调用了signal函数。signal函数的这种使用方式是向系统注册了一个函数,当发生某种特定的事件的时候,会回调之前注册的函数,即回调函数。这里的程序在运行的过程中其实可以说是并行运行的。
自定义信号的处理流程
前提
- 在task_struct结构体中,有一个指向sighand_struct的结构体指针,在该结构体当中有一个action结构体数组,这个数组每一个元素的类型都是struct k_sigation结构体类型,数组中的每一个元素都对应一个信号的处理逻辑。
- 在struct_sigaction结构体中有一个元素是struct sigaction sa,在struct sigaction结构体中有一个sighandler_t类型的元素,这个sighander_t是一个函数指针类型,typedef void(*sighandler)(int),保存信号默认执行的函数。
操作系统对信号的处理
- 操作系统对默认信号的处理
当sig位图中收到一个信号的时候,意味着sig位图当中的某一个bite位置会被置为1,操作系统处理该信号的时候,就会从PCB中找sighang_struct这个结构体的指针,从而找到sa_handler,进而操作系统内核回去调用sa_handler保存的函数地址,然后完成该信号的功能。
- 自定义信号处理函数
signal:相当于改掉了sa_handler保存的函数地址,也就是当收到自定义信号的时候,操作系统内核会去调用sa_handler保存的新的函数地址,而这个函数是我们自己定义的,调用之后就可以达到我们所期望的执行效果。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
//相当于更改掉了action数组当中的元素,相当于直接改掉了结构体,
//从而达到修改信号处理函数地址的目的
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
-
void (*sa_handler)(int); : 操作系统为每一个信号自定义的默认调用函数
-
void (*sa_sigaction)(int, siginfo_t *, void *); : 一个函数指针,但是这个函数指针是预留的,需要配合sa_flags使用,当sa_flags为SA_SIGINFO的时候,操作系统就会调用该函数指针当中保存的函数地址
-
sigset_t sa_mask; : 一般当前进程在处理信号的时候,可能还会收到新的信号,为了有效地保存新的信号,就把新的信号放在sa_mask中。
-
int sa_flags; : 配合sa_sigaction使用
-
void (*sa_restorer)(void); : 预留信息
-
act:表示把signum信号修改为act处理方式
-
oldact:表示是操作系统之前被signum所定义的处理方式
-
struct sigaction 相当于action[]数组中的元素类型(k_sigaction),signal函数就是调用sigaction函数实现的
signal(2,sigcallback);
while(1)
{
sleep(1);
}
对于上面的代码,程序正常情况下会一直执行sleep的循环
- 当程序收到ctrl + c,也就是2号信号的时候
- 当程序执行sleep函数时,从用户态切换到内核态,然后执行内核代码
- 执行完sleep的逻辑之后,需要调用do_signal函数去处理接受到的信号信息。此时如果程序没有接收到2号信号,会直接调用sysreturn函数从内核态切换到用户态;要是接收到2号信号,则会切换到用户态去执行自定义的sigcallback函数。
- 执行完毕之后,调用sigreturn 函数切换回内核态,然后再次调用do_signal 函数,重复第3步的逻辑,直到当前程序接受到的信号被处理完毕。
- 确保当前程序没有信号的中断的时候,会调用sysreturn 函数返回用户态继续执行代码。
程序进入内核态的情况
- 调用系统功能调用函数
- 调用库函数的时候,库函数底层要是调用了系统功能调用函数,就会进入内核态
- 程序访问异常,就像空指针的访问(11号信号),内存访问越界(11号信号),double free(6号信号)。
是否可以使用free函数去释放NULL指针呢?
free(NULL)并不会产生程序崩溃,也不会收到信号
信号阻塞
- 前提
信号想要发生一个阻塞,得要在task_struct结构体当中保存了一个blocked位图
信号的阻塞并不是说信号不可以被注册,而是当收到一个阻塞的信号的时候,如果通过blocked位图发现该信号被阻塞,就不会处理这个信号,但是他的阻塞不会影响信号更改pending位图和增加sigqueue节点
- 操作系统处理信号的逻辑
当程序从用户态切换到内核态之后,处理do_signal函数的时候,发现收到了某个信号,想要处理这个信号之前,得先判读block位图当中对应信号的bite位是否为1
blocked当中对应的bite位置为1,则不处理该信号,sigqueue当中对应的信号的节点还是存在
blocked当中对应的bite位置为0,则处理该信号
更改sigset_t位图中某个bite位的值
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- how:
SIG_BLOCK --> 设置某个信号为阻塞状态,用修改位图来达到目的
SIG_UNBLOCK --> 设置某个信号为非阻塞状态
SIG_SETMASK --> 设置新的阻塞的sigset_t位图
- set:要设置的新的阻塞位图
- oldset:之前程序当中阻塞的位图,出参
可以验证一下我们之前关于可靠信号与非可靠信号在sigqueue队列中的结论,非可靠信号收到多个相同的非可靠信号的时候,只会添加一次sigqueue节点,也就是只处理一次;而可靠信号收到多个相同的信号的时候,会添加多个sigqueue节点,也就是每一个可靠信号都会被处理。
9,19号信号不可以被阻塞
源码地址:
https://github.com/duchenlong/linux-text/blob/master/sigblock.c