10 - 信号

GitHub 地址


1. 信号

信号是 软中断 ,信号提供了一种处理异步事件的方法。

当造成信号的事件发生时,为进程 产生 一个信号(或向进程 发送 一个信号)。事件 可以是硬件异常(如除以 \(0\))、软件条件(如alarm定时器超时)、终端产生的信号或调用 kill 函数。

每个信号都有一个名字,以 \(3\) 个字符 SIG 开头,定义在头文件 <signal.h> 中。信号名都被定义为 正整数常量(信号编号),不存在编号为 \(0\) 的信号(空信号)。

产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核 “在此信号发生时,请执行以下操作” 。当对信号采取了这种操作时,称为向进程 递送 了一个信号。在信号产生和递送的时间间隔内,信号是 未决的

内核进行信号处理 有 \(3\) 种方式:

  1. 忽略此信号。大多数信号都可用这种方式进行处理,但是 SIGKILLSIGSTOP 不能被忽略,因为它们向内核和超级用户提供了使进程终止或停止的可靠方法。
  2. 捕捉信号。通知内核在某种信号发生时,调用一个用户函数,在用户函数中可执行用户希望对这种事件进程的处理。不能捕捉 SIGKILLSIGSTOP 信号。
  3. 执行系统默认动作。对于大多数信号的系统默认动作是终止该进程。终止+core 表示在进程当前工作目录的core文件中复制了该进程的内存镜像,UNIX系统调试程序使用core文件检查进程终止时的状态。

2. 函数 signal

signal 函数用于设置对应信号的处理方式:

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
//返回值:若成功,返回 以前 的信号处理配置;若出错,返回 SIG_ERR

\(signo\) 参数是信号名。

\(func\) 的值:

  • SIG_IGN ,向内核表示忽略此信号
  • SIG_DFL ,表示接收到此信号后的动作是系统默认动作
  • (捕捉该信号)接到此信号后要调用的函数(信号处理程序 或叫 信号捕捉函数)的地址

返回值 是一个函数指针,指向在此之前的信号处理程序的指针。

程序启动 时,所有信号的状态都是系统默认或忽略。若 exec 函数被调用,其将原先设置为要捕捉的信号都更改为默认动作,其他信号状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。

进程创建 时,子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程内存映像。

3. 中断的系统调用

如果进程在执行一个 低速系统调用 而阻塞期间捕捉到一个信号,则该系统调用被中断。

系统调用分成两类:低速系统调用其他系统调用 ,低速系统调用是可能会使进程永远阻塞的一类系统调用。

sigaction 函数使用标志 SA_RESTART 允许应用程序请求重启动被中断的系统调用。

Linux 系统中,当信号处理程序是用 signal 函数时,被中断的系统调用会重启。

自动重启的系统调用包括:ioctlreadreadvwritewritevwaitwaitpid 。前五个函数只有对低俗设备进行操作时才会被信号中断。而 waitwaitpid 在捕捉到信号时总是被中断。

4. 可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处,所以其结果是不可预知的。

信号处理程序中保证调用安全的函数是 可重入的 并被称为是 异步信号安全的 ,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送。

不可重入函数 的原因:

  • 已知它们使用静态数据结构
  • 它们调用 mallocfree
  • 他们是标准 I/O 函数(标准I/O库的很多实现都以不可重入方式使用全局数据结构)

5. SIGCLD 语义

在 Linux 中,SIGCLD 等同于 SIGCHLD ,在一个进程终止或停止时,此信号将被送给其父进程。按系统默认,将忽略此信号。

对此信号的 处理方式 是:

  • 按系统默认( SIG_DFL ),SIGCLD 被忽略,则子进程可能产生僵死进程,需父进程对其等待。
  • SIGCLD 的配置被设置 SIG_IGN ,则调用进程的子进程终止时丢弃状态,将不产生僵死程序。如果调用进程随后调用一个 wait 函数,那么它将阻塞直到所有子进程都终止,然后该 wait 会返回 \(-1\) ,并将其 errno 设置为 ECHILD
  • 如果将 SIGCLD 的配置设置为捕捉,则内核检查是否有子进程准备好被等待,如果有,则调用 SIGCLD 处理程序。(若在进程被安排捕捉 SIGCLD 之前已有子进程准备好被等待,此时系统不调用 SIGCLD 信号的处理程序)

6. 函数 kill 和 raise

kill 函数将信号发送给进程或进程组。raise 函数则允许进程向自身发送信号。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo); //raise(signo) = kill(getpid(), signo)
//两个函数返回值:若成功,返回 0;若出错,返回 -1

\(pid\) 参数取值:

  • \(pid>0\):将该信号发送给进程 ID 为 \(pid\) 的进程
  • \(pid==0\):将信号发送给与发送进程属于同一进程组的所有进程(不包括系统进程集,即内核进程与init),且发送进程具有权限向这些进程发送信号
  • \(pid<0\):将该信号发送给其进程组ID等于 \(pid\) 绝对值的所有进程(不包括系统进程集),且发送进程具有权限向这些进程发送信号
  • \(pid==-1\):将信号发送给发送进程有权限向它们发送信号的所有进程(不包括系统进程集)

权限

  • 超级用户可将信号发送给任一进程
  • 对于非超级用户,基本规则是发送者的实际用户ID或有效用户ID必须等于接受者的实际用户ID或有效用户ID。如果实现 _POSIX_SAVED_IDS ,则检查接收者的保存设置用户ID(而不是有效用户ID)

如果调用 kill 为调用进程产生信号,而且此信号是不被阻塞的,那么在 kill 返回之前,\(signo\) 或者某个其他未决的、非阻塞信号被传送至该进程。

7. 函数 alarm 和 pause

使用 alarm 函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM 信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该 alarm 函数的进程:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0 或以前设置的闹钟时间的余留秒数

参数 \(seconds\) 的值是产生信号 SIGALRM 需要经过的时钟秒数。当这一时刻到达时,信号由内核产生。

每个进程只能有一个闹钟时间。如果在调用 alarm 时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次 alarm 函数调用的值返回。以前注册的闹钟时间则被新值代替。

pause 函数使调用进程挂起直至捕捉到一个信号:

#include <unistd.h>
int pause(void); //返回值:-1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回,pause 才返回。在这种情况下,pause 返回 \(-1\) ,errno 设置为 EINTR

8. 信号集

信号集 能表示多个信号,定义数据类型 sigset_t 以包含一个信号集,并有以下处理信号集的函数:

#include <signal.h>
int sigemptyset(sigset_t *set); //初始化由set指向的信号集,清除其中所有信号
int sigfillset(sigset_t *set); //初始化由set指向的信号集,使其包括所有信号
//所有应用程序使用信号集之前,要对该信号集调用sigemptyset或sigfillset一次
int sigaddset(sigset_t *set, int signo); //将一个信号添加到已有的信号集中
int sigdelset(sigset_t *set, int signo); //从信号集中删除一个信号
//以上四个函数返回值:若成功,返回0;若出错,返回-1
int sigimember(const sigset_t *set, int signo); //判断是否已包含某信号。若真,返回1;若假,返回-1

9. 函数 sigprocmask

一个进程的 信号屏蔽字 ,规定了当前阻塞而不能递送给该进程的信号集。调用函数 sigprocmask 可以检测或更改进程的信号屏蔽字:

#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//返回值:若成功,返回0;若出错,返回-1

若 \(oset\) 是非空指针,那么进程的当前信号屏蔽字通过 \(oset\) 返回。

若 \(set\) 是一个非空指针,则参数 \(how\) 指示如何修改当前信号屏蔽字。

在调用 sigprocmask 后如果有任何未决的、不再阻塞的信号,则在此函数返回前,至少将其中之一递送给该进程。

sigprocmask 是仅为单线程进程定义的。

10. sigpending

sigpending 函数返回一信号集,对于调用进程而言,其中的各信号是 阻塞不能递送 的,因而也一定是当前未决的。该信号通过 \(set\) 参数返回:

#include <signal.h>
int sigpending(sigset_t *set); //返回值:若成功,返回0;若出错,返回-1

11. 函数 sigaction

sigaction 函数的功能是检查或修改与指定信号相关联的处理动作。很多平台都用 sigaction 实现 signal 函数。

#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
//返回值:若成功,返回0;若出错,返回-1

\(signo\) 是要检测或修改其具体动作的信号编号。若 \(act\) 指针非空,则要修改其动作。如果 \(oact\) 指针非空,则系统经由 \(oact\) 指针返回该信号的上一个动作。

关于连续发送同一信号的处理:

若更改后的信号动作是 捕捉函数 而不是 SIG_IGN 或 SIG_DFL ,则调用该信号捕捉函数之前,此信号被添加到进程的信号屏蔽字中,仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。若同一种信号多次发生,通常并不将它们加入队列,所以如果某种信号在被阻塞时多次发生,其信号处理函数只会被调用一次。

若更改后的信号动作是 捕捉函数 而不是 SIG_IGNSIG_DFL ,则调用该信号捕捉函数之前,此信号被添加到进程的信号屏蔽字中,仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。

12. 函数 sigsetjmp 和 siglongjmp

信号处理程序中调用 longjmp 有一个问题:当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加到进程的信号屏蔽字中。这阻止了后来产生的这种信息中断该信号处理程序。如果用 longjmp 跳出信号处理程序,进程的信号屏蔽字可能无法恢复。

信号处理程序使用 sigsetjmpsiglongjmp 进行非局部转移:

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask); //返回值:若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);

这两个函数和 setjmplongjmp 之间的唯一 区别sigsetjmp 增加了一个参数。如果 \(savemask\) 非 \(0\) ,则 sigsetjmp 在 \(env\) 中保存进程的当前信号屏蔽字。调用 siglongjmp 时,如果非 \(0\) \(savemask\) 的 sigsetjmp 调用已经保存了 \(env\) ,则 siglongjmp 从其中恢复保存的信号屏蔽字。

13. 函数 sigsuspend

sigsuspend 函数在一个原子操作中设置信号屏蔽字,然后使进程休眠:

#include <signal.h>
int sigsuspend(const sigset_t *sigmask);

进程的信号屏蔽字设置为由 \(sigmask\) 指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则 sigsuspend 返回,并且该进程的信号屏蔽字设置为调用 sigsuspend 之前的值。

此函数 没有返回值 ,如果它返回到调用者,则总是返回 \(-1\) ,并将 errno 设置为 EINTER (表示一个被中断的系统调用)。

sigsuspend 函数可用于:

  • 保护代码临界区,使其不被特定信号中断(在此功能中起到 pause 作用)
  • 等待一个信号处理程序设置一个全局变量
  • 实现父、子进程之间的同步

14. 函数 abort

abort 函数的功能是使程序异常终止:

#include <stdlib.h>
void abort(void);

abortSIGABRT 信号发送给调用进程(进程不应忽略此信号)。实质上,abort 函数向主机环境递送一个 未成功终止 的通知。

让进程捕捉 SIGABRT 的意图是:在进程终止之前由其执行所需的清理操作。如果进程并不在信号处理程序中终止自己,当(SIGABRT)信号处理程序返回时,abort 终止该级才能拿,且对所有打开标准 I/O 流的效果应当与进程终止前对每个流调用 fclose 相同。

15. 函数 sleep、nanosleep 和 clock_nanosleep

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

此函数调用进程被挂起直到满足下面两个条件之一:

  • 已经过了 \(seconds\) 所指定的墙上时钟时间(返回值为 \(0\) )
  • 调用进程捕捉到一个信号并从信号处理程序返回(返回值为未休眠完的秒数)

nanosleep 函数提供了纳秒级的精度:

#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);

\(reqtp\) 参数用秒和纳秒指定了需要休眠的时间长度。如果某个信号中断了休眠间隔,进程并没有终止,\(remtp\) 参数指向的 timespec 结构就会被设置为未休眠完的时间长度。

clock_nanosleep 函数可使用相对于特定时钟的延迟时间来挂起调用线程:

int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *reqtp, struct timespec *remtp);

\(clock_id\) 参数指定了计算延迟时间基于的时钟。

\(flags\) 参数用于控制延迟是相对的还是绝对的:

  • \(flags\) 为 \(0\) 时表示休眠时间是相对的
  • 若 \(flags\) 值设置为 TIMER_ABSTIME ,表示休眠时间是绝对的(例如,希望休眠时间到达某个特定的时间)

《UNIX环境高级编程》(APUE) 笔记第十章 - 信号的更多相关文章

  1. (十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  2. Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字 . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级 ...

  3. (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  4. (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  5. (三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  6. (四) 一起学 Unix 环境高级编程(APUE) 之 系统数据文件和信息

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  7. (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  8. (六) 一起学 Unix 环境高级编程 (APUE) 之 进程控制

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  9. (七) 一起学 Unix 环境高级编程(APUE) 之 进程关系 和 守护进程

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  10. (八) 一起学 Unix 环境高级编程 (APUE) 之 信号

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

随机推荐

  1. JavaScript (二) js的基本语法 - - 运算符、流程控制

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.运算符 1.一元运算符 ++ -- 都是运算符 ++ 和 -- 可以分为:前+ 和后+ and 前- ...

  2. Linux kernel学习(序)

    伟大的Linux kernel有几大重要模块: 1.文件系统(File System) 2.进程调度(Process Scheduler) 3.内存管理(Memory Management) 4.进程 ...

  3. Java实现 LeetCode 735 行星碰撞(栈)

    735. 行星碰撞 给定一个整数数组 asteroids,表示在同一行的行星. 对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动).每一颗行星以相 ...

  4. Java实现 神犇的悲惨一生

    [问题描述] 传说中有位神犇,因其一贯低调,所以人们连他活了多少岁都不知道. 好在XXXX文献上有段关于他生平细节的文字:神犇一生中, 幼年占了1/6,又过了1/12的青春期,又谈了1/6的恋爱后结婚 ...

  5. Java实现 蓝桥杯 算法提高 道路和航路

    问题描述 农夫约翰正在针对一个新区域的牛奶配送合同进行研究.他打算分发牛奶到T个城镇(标号为1-T),这些城镇通过R条标号为(1-R)的道路和P条标号为(1-P)的航路相连. 每一条公路i或者航路i表 ...

  6. java实现文件管理

    ** 文件管理** 显示"DaSai"目录下以"Ex"开头的文件和目录,写了如下代码,请完善之: import java.io.*; class JavaFil ...

  7. Java实现 蓝桥杯 历届试题 错误票据

    问题描述 某涉密单位下发了某种票据,并要在年终全部收回. 每张票据有唯一的ID号.全年所有票据的ID号是连续的,但ID的开始数码是随机选定的. 因为工作人员疏忽,在录入ID号的时候发生了一处错误,造成 ...

  8. electron内使用vue-slider-component组件报“$attrs is readonly”错误

    解决办法 安装vue-no-ssr插件 https://www.npmjs.com/package/vue-no-ssr npm install vue-no-ssr --save-dev 代码 &l ...

  9. STL常用序列容器

    这里简要的记述一下STL常用容器的实现原理,要点等内容. vector vector是比较常用的stl容器,用法与数组是非类似,其内部实现是连续空间分配,与数组的不同之处在于可弹性增加空间,而arra ...

  10. HashMap解析(主要JDK1.8,附带1.7出现的问题以及区别)

    按问题的形式来吧,这些大多是我自己总结的,如有错误请及时指正谢谢 1.你了解HashMap么,可以说说么? 首先,HashMap是一种数据结构,可以快速的帮我们存取数据.它的底层数据结构在1.7和1. ...