Linux__进程信号

文章目录

    • 1.信号的引入
    • 2.信号概念
    • 3.信号列表
    • 4.信号的生命周期
      • 4.1信号处理常见方式概览
    • 5.信号的产生
      • 5.1通过终端按键产生信号(键盘组合键)
      • 5.2调用系统函数向进程发信号
      • 5.3由软件条件产生的信号
      • 5.4 硬件异常产生信号
    • 6.阻塞信号
      • 6.1信号其他相关常见概念
      • 6.2信号在内核中的表示示意图
      • 6.3 sigset_t
      • 6.4 信号集操作函数
      • 6.5 sigprocmask函数
      • 6.6 sigpending函数
      • 6.7用上述函数写一个获取信号小程序:**
    • 7.信号的捕捉
      • 7.1内核如何实现信号的捕捉
    • 8. 可重入函数
    • 9.volatile
    • 10.SIGCHILD信号

1.信号的引入

当我们在shell上写出一个死循环退不出来的时候,只需要一个组合键,ctrl+c,就可以解决了,这就是一个信号。

用户按下Ctrl+C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。前台进程因为收到信号,进而引起进程退出。

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

2.信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

3.信号列表

用kill -l命令可以察看系统定义的信号列表
Linux__进程信号_第1张图片
一个62个信号。其中前31个时普通信号,后31个时实时信号。

4.信号的生命周期

Linux__进程信号_第2张图片

4.1信号处理常见方式概览

处理动作有以下三种:

  1. 忽略此信号
  2. 执行该信号的默认处理动作
  3. 提供一个信号处理函数,用户自定义函数,这种方式称为捕捉

5.信号的产生

5.1通过终端按键产生信号(键盘组合键)

  • Ctrl + c :给前台发送一个SIGINT信号,中断当前的前台进程
  • Ctrl + z:给前台发送一个SIGTSTP信号,使当前进程暂停
  • Ctrl + \:给前台发送一个SIGQUIT信号,退出进程同时产生一个core文件

5.2调用系统函数向进程发信号

1.kill函数
可以给一个指定的进程发送指定的信号。
Linux__进程信号_第3张图片
参数:

  1. 第一个参数进程id
  2. 第二个参数信号标号

返回值:成功返回0失败返回-1

2.raise函数
**
返回值:成功返回0失败返回-1

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

3.abort函数
Linux__进程信号_第4张图片
使当前进程接收到信号而异常终止。给自己发送异常终止信号6(SIGABRT)

5.3由软件条件产生的信号

SIGPIPE是一种由软件条件产生的信号。

#include 
unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送14号信号(SIGALRM信号),该信号的默认处理动作是终止该进程。

5.4 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为8号信号(SIGFPE)发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为11信号(SIGSEGV)发送给进程。

6.阻塞信号

6.1信号其他相关常见概念

  1. 实际执行信号的处理动作称为信号递达
  2. 信号从产生到递达之间的状态称为信号未决
  3. 进程可以选择阻塞某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意:阻塞和忽略时不同的,信号阻塞状态是信号未决,而信号忽略状态是信号递达。

6.2信号在内核中的表示示意图

Linux__进程信号_第5张图片

  1. 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  2. 在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行系统默认处理动作。
  3. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  4. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

  • POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

6.3 sigset_t

  • 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
  • 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

6.4 信号集操作函数

#include 

int sigemptyset(sigset_t *set);
//sigemptyset初始化set所指向的信号集,使其中所有bit位清零

int sigfillset(sigset_t *set);
//sigfillset初始化set所指向的信号集,使其中所有bit位设为有效

int sigaddset (sigset_t *set, int signo);
//sigaddset将信号集中的某个信号bit位设为有效

int sigdelset(sigset_t *set, int signo);
sigdelset将信号集中的某个信号bit位设为无效
对于上面4个函数的返回值:成功返回0,出错则为-1

int sigismember(const sigset_t *set, int signum);
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号
若包含则返回1,不包含则返回0,出错返回-1

注意: 在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

6.5 sigprocmask函数

调用sigprocmask函数可以读取或更改进程的阻塞信号集(Block位图表)

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
	如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改
	oset若是非空指针,读取当前进程的阻塞信号集通过oset传出,输出型参数。
返回值:若成功则为0,若出错则为-1

下表说明了how参数的可选值:
Linux__进程信号_第6张图片

6.6 sigpending函数

在这里插入图片描述
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

6.7用上述函数写一个获取信号小程序:**

获取未决信号2号信号,然后
Linux__进程信号_第7张图片
Linux__进程信号_第8张图片

7.信号的捕捉

Linux__进程信号_第9张图片

7.1内核如何实现信号的捕捉

  1. 首先在用户正常执行主控制流程由于中断,异常或系统调用而直接进入内核态进行处理处理这种异常。
  2. 内核处理完异常就准备返回用户态了,在这之前会看当前进程有没有可以递达的信号,如果有就对可递达的信号进行处理。
  3. 如果信号的处理函数是用户自定义的就返回用户态去执行用户自定义的信号处理函数。
  4. 信号处理函数执行完之后,会调用一个特殊的系统调用函数sigreturn而再一次进入内核态,执行这个系统调用。
  5. 这个系统调用完成之后,就会返回主控制流程被中断的地方继续执行下面的代码

8. 可重入函数

如果一个函数只访问自己的局部变量或参数,则称为可重入函数。这种函数不会影响运行结果。
常见不可重入函数:
1.调用malloc/free的函数,因为malloc也是用全局链表来管理堆的
2.调用了I/O函数库,标准I/O库中很多实现都以不可重入的方式使用全局数据结构

9.volatile

volatile修饰的变量,是不可被"覆盖"的,读取变量必须读取变量真实的存储位置(不可读取缓存、寄存器等位置的值)。

volatile 作用:保持内存的可见性,告知编译器,被volatile关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

10.SIGCHILD信号

我们知道清除僵尸进程的方法就是使用wait和waitpid函数父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程需要被清理(轮询),第一种方式父进程阻塞就不能做其他事情了,第二种,父进程不断去询问,代码实现比较复杂。

其实子进程在终止时会给父进程发一个SIGCHILD信号,默认处理动作是忽略,用户可以自定义SIGCHILD的处理函数,这样父进程就可以专心处理自己的事情,不用关心子进程了,子进程退出时会通知父进程,父进程在信号处理函数中调用wait来处理子进程就可以了

想不产生僵尸进程还有另外一种方法:父进程调用sigaction将SIGCHILD处理动作置为SIG_IGN这样fork出的子进程在终止时会自动清理掉也不会通知父进程系统默认的忽略和用户自定义的忽略一般是没有区别的,但这是一个特例,对于Linux可以用,在其他unix系统上不一定能用。

你可能感兴趣的:(Linux,linux,内核)