进程信号

信号量(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. 计数器+1操作完毕之后,计数器当中的值小于等于0。这时需要通知PCB等待队列当中的进程(因为PCB等待队列不为空)。
  2. 计数器+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:从信号的角度来看,应该如何解决僵尸进程的问题?

  1. 使用signal(SIGCHLD,SIG_IGN),在这种方式下,子进程状态信息会被丢弃,也就是自动回收了,所以不会产生僵尸进程。(爸爸不管儿子了,儿子自己释放掉自己吧)
  2. 从其他角度看,我们可以fork两次,让第一次fork的子进程在fork完成后使用(exit)直接退出,这样第二次fork得到的子进程就没有父进程了,就变成了孤儿进程,它会被自动过继给老祖宗init进程,init会负责释放它的资源,这样就不会由"僵尸"产生了(把原本的父子关系变成爷孙关系,然后直接杀掉中间的爸爸,爷爷也不管孙子了,孙子就成了孤儿,自己释放自己)
  3. 因为父子进程之间的抢占式执行,所以说父子进程之间无法确定哪一个进程先执行,为了防止子进程在退出的时候,父进程陷入循环无法得到子进程的退出状态,可以在子进程进行的时候,让父进程进行进程等待, 以便子进程可以正常退出。(这种方法,爸爸会照顾儿子一生,直至儿子死亡,并妥善处理完儿子的后事)
#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函数的这种使用方式是向系统注册了一个函数,当发生某种特定的事件的时候,会回调之前注册的函数,即回调函数。这里的程序在运行的过程中其实可以说是并行运行的。
在这里插入图片描述

自定义信号的处理流程

前提

  1. 在task_struct结构体中,有一个指向sighand_struct的结构体指针,在该结构体当中有一个action结构体数组,这个数组每一个元素的类型都是struct k_sigation结构体类型,数组中的每一个元素都对应一个信号的处理逻辑。
  2. 在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的循环

  1. 当程序收到ctrl + c,也就是2号信号的时候
  2. 当程序执行sleep函数时,从用户态切换到内核态,然后执行内核代码
  3. 执行完sleep的逻辑之后,需要调用do_signal函数去处理接受到的信号信息。此时如果程序没有接收到2号信号,会直接调用sysreturn函数从内核态切换到用户态;要是接收到2号信号,则会切换到用户态去执行自定义的sigcallback函数。
  4. 执行完毕之后,调用sigreturn 函数切换回内核态,然后再次调用do_signal 函数,重复第3步的逻辑,直到当前程序接受到的信号被处理完毕。
  5. 确保当前程序没有信号的中断的时候,会调用sysreturn 函数返回用户态继续执行代码。

在这里插入图片描述
程序进入内核态的情况

  1. 调用系统功能调用函数
  2. 调用库函数的时候,库函数底层要是调用了系统功能调用函数,就会进入内核态
  3. 程序访问异常,就像空指针的访问(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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章