1. 信号概念

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

Unix早期版本提供的是不可靠信号,信号可能丢失。之后增加了可靠信号机制

每个信号都有一个名字,以3个字符SIG开头。如SIGABRT是终止信号,进程调用abort函数产生这种信号。

  • SIGABRT信号:

    void abort(void);
    

    abort()首先解除了对SIGABRT(6)信号的阻止和忽略,然后为调用进程发送该信号(就像调用了 raise(SIGABRT)一样)。 导致进程的非正常终止,除非SIGABRT 信号被捕获,并且信号处理函数没有返回(调用exit、_exit、 _Exit、longjmp或siglongjmp使信号处理函数没有返回)。

    如果abort()函数导致进程终止,则关闭和刷新所有打开的文件流。

Linux支持63种信号,不存在编号为0的信号(即为空信号),kill函数对信号编号0有特殊应用。1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

1.1 产生信号条件

  • 用户按某些终端键时,引发终端产生的信号。如在终端上按delete或Ctrl+C键,通常产生中断信号SIGINT。
  • 硬件产生异常信号:除0、无效的内存引用等。因为这些条件通常由硬件检测到,并且通知内核。然后内核为该进程产生适当信号。
  • 进程调用kill(2)函数将任意信号发送给另一个进程或进程组:接收信号进程和发送信号进程的所有者必须相同,或发送方必须是超级用户root
  • 用kill(1)命令将信号(默认是SIGTERM)发送给进程
  • 当检测到某种软件条件发生时将产生信号通知相关进程。如SIGURG、SIGPIPE、SIGALRM(进程设置的定时器已经超时)等

1.2 对信号的处理方式

  • 忽略此信号

    除了两种信号SIGKILL和SIGSTOP绝对不能忽略以外,大多数信号都可使用这种方式处理。

    SIGKILL和SIGSTOP不能被忽略的原因是,它们向内核和超级用户提供了使进程终止或停止的可靠方法(可以理解为终止、停止进程的终极方法)。

    并且如果忽略某些由硬件异常(如除0、非法内存引用)产生的信号,会发生未定义行为

  • 捕捉该信号

    通知内核在某种信号发生时,调用一个用户函数。同样SIGKILL和SIGSTOP不能被捕捉

  • 执行系统默认动作

    对大多数信号的系统默认动作是终止该进程

    Linux对各种信号的默认动作:

    注意“终止+core”表示在进程当前工作目录的core文件中复制了该进程内存映像(默认文件名为core,这种行为即coredump),大多数Unix系统调试程序(如gdb)都使用core文件检查进程终止时的状态。

    注意"硬件故障"对应于具体定义的硬件故障,需要通过对应操作系统手册查看这些信号对应于哪些错误

Core Dump

当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成“核心转储”)。我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景。

如果没有进行core dump 的相关设置,默认是不开启的。可以通过ulimit -c查看是否开启。如果输出为0,则没有开启,需要执行ulimit -c unlimited开启core dump功能。

不产生core文件的条件:

  • ulimit -c查看core文件有没有限制大小。如果是0则说明禁止了core文件产生
  • 程序设置了用户id(即调用setuid),但当前用户并非该程序文件的所有者
  • 程序设置了组id(即调用setgid),但当前用户并非该程序文件的组所有者
  • 用户没有当前目录或指定core文件产生目录的写权限
  • 文件已存在,用户对该文件没有写权限
  • core文件太大,磁盘空间不足

2. 各种信号详细信息

  • SIGABRT

    调用abort函数产生此信号,进程异常终止。

  • SIGALRM

    调用alarm、setitimer函数设置的定时器、间隔时间超时产生此信号。

  • SIGBUS

    表示发生了一个具体定义的硬件故障。当出现某种类型内存故障时,常产生此种信号

  • SIGCHLD

    子进程终止或停止时,SIGCHLD信号发送给父进程。系统默认会忽略此信号。如果父进程希望得知子进程的这种状态改变,那么通常在信号捕捉函数中调用wait等函数获取子进程pid及终止状态。

  • SIGCONT

    用于作业控制。如果收到此信号的进程处于停止状态,则系统默认动作是该进程继续运行;否则忽略此信号

  • SIGEMT

    表示发生了一个具体定义的硬件故障。

  • SIGFPE

    算术运算异常,如除0、浮点溢出等

  • SIGHUP:挂断信号

    • 终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。

    • session首进程退出时,该信号被发送到该session中的前台进程组和后台进程组中的每一个进程

    • 若进程的退出,导致一个进程组变成了孤儿进程组,新的孤儿进程组中处于停止(stopped)状态的每一个进程都会收到挂断(SIGHUP)信号,接着又收到继续(SIGCONT)信号。

    系统对SIGHUP信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。

  • SIGILL

    进程执行一条非法硬件指令

  • SIGINT:中断信号

    用户按中断键(delete或Ctrl+C),产生此信号发送至前台进程组中的每一个进程。当一个进程失控或者在终端产生大量不需要输出时,常通过此命令终止它。

  • SIGIO

    指示发生一个异步I/O事件。Linux中定义SIGIO与SIGPOLL有相同的值,默认行为是终止该进程。

    #define SIGIO       SIGPOLL
    
  • SIGIOT

    表示发生了一个具体定义的硬件故障。Linux中定义SIGIOT与SIGABRT有相同值

    #define    SIGIOT      SIGABRT
    
  • SIGKILL:

    这是两个不能被阻塞、捕捉或忽略的信号中的一个,向内核和超级用户提供了使进程终止的可靠方法

  • SIGPIPE

    管道读进程已经终止后,写进程写入管道产生此信号

    当类型为SOCK_STREAM的socket已不再连接时,进程写入该套接字也产生此信号。

  • SIGPOLL

    当在一个可轮询设备上发生一个特定事件时产生此信号(如当系统发现有东西需要你读的时候,就发个SIGPOLL信号来通知)。在未来可能将此信号移除。

  • SIGPROF

    setitimer函数设置的梗概统计间隔定时器超时产生此信号。在未来可能将此信号移除

  • SIGPWR

    电源故障信号。主要用于具有不间断电源的系统。如果电源失效,系统依靠蓄电池继续运行。但是如果蓄电池也将不能工作,此时发送SIGPWR信号。通常是接到蓄电池电压过低信息的进程将SIGPWR发送给init进程,然后由init处理停机操作。

    Linux对SIGPWR的默认动作是终止相关进程。

  • SIGQUIT:退出信号

    用户在终端上按退出键(Ctrl+\)产生此信号,并发送给前台进程组的所有进程。磁信号不仅终止前台进程组,还产生一个core文件。

  • SIGSEGV

    进程进行了一次无效的内存引用(比如访问了一个未经初始化的指针),或发生段错误

  • SIGSTOP

    作业控制信号,它停止一个进程。同SIGKILL,该信号不能被阻塞、忽略和捕获

  • SIGSYS

    指示一个无效的系统调用

  • SIGTERM

    是kill命令发出的默认信号(系统默认终止信号)。相比于SIGKILL,SIGTERM可以被捕获或忽略,因此允许让程序有机会在退出之前做好清理工作,从而优雅地终止。

  • SIGTRAP

    表示发生了一个具体定义的硬件故障。通常使用此信号将控制转移至调试程序

  • SIGTSTP

    交互停止信号。当用户在终端按挂起键(Ctrl+Z)时,将该信号发送到前台进程组中的所有进程。 SIGTSTP与SIGSTOP都是使进程暂停(都使用SIGCONT让进程重新激活)。唯一的区别是SIGSTOP不可以捕获和忽略,而SIGTSTP可以。

  • SIGTTIN

    当一个后台进程组中的进程试图读控制终端时收到此信号,并使该进程暂停。注意,如果读进程属于孤儿进程组,那么read控制终端操作返回出错,不产生此信号,errno设置为EIO。

  • SIGTTOU

    如果禁止后台作业向控制终端写,此时当一个后台进程组进程试图写控制终端时收到此信号,并使该进程暂停。注意,如果写进程属于孤儿进程组,则写操作返回出错,不产生此信号,errno设置为EIO。

    除此之外,tcsetattr、tcsendbreak、tcdrain、tcflush、tcflow以及tcsetpgrp也能产生SIGTTOU信号。

  • SIGURG

    通知进程发生一个紧急情况(用于socket编程)。在网络连接上接到带外的数据时,可选择地产生此信号。

    带外数据:
    带外数据用于迅速告知对方本端发生的重要的事件。它比普通的数据(带内数据)拥有更高的优先级,不论发送缓冲区中是否有排队等待发送的数据,它总是被立即发送。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。

  • SIGUSR1

    用户定义的信号

  • SIGUSR2

    另一个用户定义的信号,与SIGUSR类似

  • SIGVTALRM

    当一个由setitimer函数设置的虚拟间隔时间超时产生此信号

  • SIGWINCH

    内核维护与每个终端或伪终端相关联窗口的大小。进程可以用ioctl得到或设置窗口大小。如果用ioctl设置窗口大小命令更改了窗口大小,则内核将该信号发送至前台进程组

  • SIGXCPU

    如果进程超过其软CPU时间限制,产生此信号。

  • SIGXFSZ

    如果进程超过其软文件长度限制,产生此信号。

3. signal函数

设置调用进程收到指定信号时的动作(忽略、使用系统默认动作或捕获)

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • signum参数:信号名,如SIGABRT

  • handler参数:

    • SIG_IGN:忽略此信号(不能用于SIGKILL和SIGSTOP)

      #define   SIG_IGN  ((__sighandler_t)  1)  /* Ignore signal.  */
      
    • SIG_DFL:系统默认动作

      #define SIG_DFL  ((__sighandler_t)  0)  /* Default action.  */
      
    • 指定函数地址(不能用于SIGKILL和SIGSTOP):在信号发生时,调用该函数(该函数有一个int形参,即为该信号编号)。称这个操作是捕捉该信号,称此函数为信号处理程序或信号捕捉函数。

  • 返回值:返回之前的信号处理程序的地址,当发生错误时返回 SIG_ERR

    #define   SIG_ERR  ((__sighandler_t) -1)  /* Error return.  */
    

signal函数的一些特点:

  • 注意,exec一个程序后,通常所有信号的处理都是忽略或者使用系统默认操作。如果调用exec前对某个信号忽略,则exec后仍为忽略;但是如果调用exec前对某个信号捕获,则exec后对该信号更改为使用默认操作(因为信号捕捉函数的地址在exec的新程序中毫无意义)。

  • 当fork时,子进程继承父进程的信号处理方式,因为子进程复刻父进程内存映像,因此信号捕捉函数地址在子进程中有效

  • signal函数的一个缺陷

    不改变信号的处理方式就不能确定信号之前的处理方式(根据signal返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction函数确定一个信号的处理方式,而无需改变它

4. 不可靠信号

https://blog.csdn.net/u013074465/article/details/45978755

在早期UNIX中,信号是不可靠的。意为信号可能会丢失,一个信号发生了但是进程可能一直不知道。同时不具备阻塞信号的能力(不要忽略该信号,在其发生时记住它,然后在进程做好准备时再通知它)。

不可靠信号的主要问题:

  • 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
  • 信号可能丢失,如果在进程对某个信号进行处理时,这个信号发生多次,对后到来的这类信号不排队,那么仅传送该信号一次,即发生了信号丢失。
    因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。

Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,Linux下的不可靠信号问题主要指的是信号可能丢失

5. 中断的系统调用

系统调用分类:

将系统调用分为两类 -> 低速系统调用/其他系统调用

低速系统调用是可能会使进程永远阻塞的一类系统调用:

  • 如果某些类型文件(如读管道、终端设备、网络设备)数据不存在,则读操作可能会使调用者永远阻塞
  • 如果这些数据不能被相同类型文件立即接受,则写操作可能会使调用者永远阻塞。
  • 在某种条件发生之前打开某些类型文件,可能发生阻塞
  • pause函数(它使进程挂起直到收到一个信号)和wait函数
  • 某些ioctl操作
  • 某些进程间通信函数

在早期UNIX中,如果进程正在执行低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再执行。该系统调用返回出错,其errno设置为EINTR(系统调用被中断)。可以理解为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。

对于处理已经read/write部分数据量的相应系统调用,此时被信号中断,有两种方式(例如read系统调用已经接收并传送数据至应用程序缓冲区,但尚未接收到应用程序请求的全部数据):

  • 认为系统调用失败,errno设置为EINTR
  • (现在使用这一种):允许该系统调用成功返回,返回值是已读写的数据量。

当被信号中断时,下列系统调用支持自动重启动

ioctl、read、readv、write、writev、wait和waitpid

有些应用程序不希望这些函数被中断后自动重启动,因此进程可以设置对每个信号禁用此功能。(sigaction函数中只有中断信号的SA_RESTART标志有效,才自动重启动系统调用)在Linux中,当信号处理程序是用signal函数注册时,被中断的系统调用会自动重启动

注意,除了低速系统调用,信号也可以中断类似sleep这样的函数:调用sleep函数的线程休眠seconds秒如果中间有一个未被忽略的信号到达则终止休眠

6. 可重入函数

  • 如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc会发生什么?

    可能会对进程造成破坏。因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时进程可能正在更改此链表。

  • **如果进程正在执行getpwnam这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数(如信号处理函数内部又调用getpwnam)**会发生什么?

    返回给正常调用者的信息可能会被返回给信号处理程序的信息覆盖

可重入函数:

在信号处理程序中保证调用安全的函数,这些函数是可重入的并被称为是异步信号安全的(即为在函数A执行期间中断执行信号处理程序,在信号处理程序中可能再次调用函数A,但是不会造成问题)。这种函数除了可重入外(信号处理程序中再次调用被信号中断的函数),在执行信号处理操作期间会阻塞任何会引起不一致的信号发送

下列函数都是异步信号安全即可重入的:

没有列入上表的函数大多是不可重入的,不可重入函数通常有以下特点

  • 它们使用静态数据结构,重入可能导致结果被覆盖
  • 它们调用malloc或free
  • 它们是标准I/O函数。标准I/O库的很多实现都不以可重入方式而是使用全局数据结构

并且要注意,对于errno,因为信号处理程序可能会修改errno原先值,因此应当在调用前保存errno,在调用后恢复errno

如果应用程序要做更新全局数据结构这样的事情,而同时要捕捉某些信号,而这些信号的处理程序又会引起执行siglongjmp,则在更新这种数据结构时要阻塞此类信号。(因为可能导致这些全局数据结构是部分更新的)

综上所述,在信号处理函数中调用一个非可重入函数,其结果不可预知。因此在信号处理函数中不能调用非可重入函数

7. 多线程中的信号处理

使用了多线程后,便有些疑问:

  1. 信号发生时,哪个线程会收到
  2. 是不是每个线程都有自己的mask及action
  3. 每个线程能按自己的方式处理信号么

对于以上问题,结论是:

  • 如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理
  • 如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理
  • 如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到
  • 其次,每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的

8. 可靠信号

8.1 三种信号状态

**信号产生:**generation

当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。

这些造成信号的事件可以是硬件异常(如除0)、软件条件(如alarm定时器超时)、终端产生的信号或调用kill函数。

当一个信号产生时,内核通常在进程中以某种形式设置一个标志。

**信号递送:**delivery

指信号发送给进程之后,对该信号进行了处理(无论是忽略、捕获还是使用系统默认操作)

**信号未决:**pending

在信号产生和递送的时间间隔内称为未决

8.2 阻塞信号递送:

进程可以选择对指定信号“阻塞信号递送”。如果为进程产生了一个阻塞的信号,并且对该信号的动作是系统默认或捕捉,则此信号保持为未决的。直到该进程解除对此信号的阻塞,或者将对此信号的动作更改为忽略,内核才会递送一个原来被阻塞的信号给进程(而不是解除阻塞后再产生的信号)。

如果在进程解除对某个信号的阻塞之前这种信号发生了多次,允许系统能够递送该信号一次或多次。如果递送该信号多次,则称这些信号进行了排队

8.3 信号屏蔽字:

每个进程都有信号屏蔽字(signal mask),它规定当前要阻塞递送到该进程的信号集。对于每一种可能的信号,该屏蔽字中都有一位与之对应。如果该位已设置,则它对应的信号是被阻塞的。

注意,在信号处理函数被调用时,操作系统建立的新信号屏蔽字包含正被递送的信号(即触发本次捕获的信号),信号处理函数返回时再恢复信号屏蔽字。因此保证在处理一个给定信号时,如果该信号再次发生,那么它将被阻塞到前一个信号的处理结束为止

9. kill和raise函数

kill函数将信号发送给进程或进程组,raise函数向进程自身发送信号

int kill(pid_t pid, int sig);
int raise(int sig);

kill的pid参数:

  • pid > 0 :将信号发送给指定进程
  • pid == 0 : 将信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限向这些进程发送信号
  • pid < 0 : 将信号发送给进程组ID等于pid绝对值的所有进程,并且发送进程具有权限向这些进程发送信号
  • pid == -1 : 将信号发送给有权限向它们发送的所有进程。除了进程1(init)

如果sig==0,则说明是空信号,kill仍然执行正常的错误检查但是不发送信号。常被用来确定一个特定进程是否存在。如果向一个不存在的进程发送空信号,kill函数返回-1。

发送信号的权限问题:

  • 超级用户可以把信号发送给任一进程
  • 非超级用户,则要求发送的实际用户ID或有效用户ID等于接受者的实际用户ID或有效用户ID。
  • 但是有一个特例:对于SIGCONT信号,则进程可以将它发送给属于同一会话的任一进程

如果调用kill为调用进程产生信号,并且如果该信号是不被阻塞的,那么在kill函数返回之前,该信号或者其他某个未决、未阻塞信号就被递送给了该进程

10. alarm和pause函数

alarm函数设置一个定时器(秒数),将来某个时刻定时器超时产生SIGALRM信号。如果忽略或不捕捉该信号,默认动作是终止该进程

 unsigned int alarm(unsigned int seconds);
  • 注意,每个进程只能有一个闹钟时间,因此调用alarm会覆盖之前的alarm。即如果在调用alarm时上一次为该进程注册的alarm还没有超时,则该闹钟时间的余留值用作本次调用的返回值,并且以前注册的闹钟时间被新值替代

  • 如果要捕获SIGALRM,必须在alarm调用前安装信号捕获程序。

pause函数是一个慢速系统调用,使调用进程挂起直到捕捉到一个信号

int pause(void);

只有执行了信号处理程序并从其返回时,pause才返回。此时pause返回-1,errno设置为EINTR

  • 注意,在信号处理函数中使用longjmp函数一定要小心,因为如果该信号中断了其他信号处理函数,那么longjmp将会提早终止这些信号处理函数。

11. 信号集

使用sigset_t以包含一个信号集,该数据类型能够表示多个信号的集合,该数据类型会被sigprocmask等函数使用

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
  • sigemptyset:

    初始化set指向的信号集,清除其中所有信号

  • sigfillset:

    初始化set指向的信号集,使其置位所有信号

  • 注意,所有应用程序在使用信号集前,都要对该信号集数据结构调用sigemptyset或者sigfillset

  • sigaddset:

    在set指向的信号集中添加指定信号signum

  • sigdelset:

    在set指向的信号集中删除指定信号signum

  • sigismember:

    判断set信号集中是否有信号signum

12. sigprocmask函数

信号屏蔽字:

阻塞而不能递送给该进程的信号集。可以通过sigprocmask函数检测、更改进程的信号屏蔽字

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 若oldset非空,那么进程之前的信号屏蔽字通过oldset返回。

  • 若set非空,则how指示如何根据set修改当前信号屏蔽字

  • how参数:

    • SIG_BLOCK:阻塞信号,即之前的信号集和set做按位或操作,即并集。set包含了希望被阻塞的信号
    • SIG_UNBLOCK:解除信号阻塞,即和set的补集求交集。set包含了希望被解除阻塞的信号
    • SIG_SETMASK:赋值信号屏蔽字
  • 在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程

13. sigpending函数(未决的信号)

sigpending函数返回一个信号集,以指示当前处于未决状态的信号(即已经产生但是由于被阻塞而不能递送的信号)

int sigpending(sigset_t *set);

14. sigaction函数(信号action动作)

检查、修改指定信号的处理动作。此函数用于取代UNIX早期版本使用的signal函数

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

若act指针非空,则表示要修改其动作。如果oldact非空,则通过该参数返回指定信号的上一个动作

struct sigaction {void     (*sa_handler)(int);void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;int        sa_flags;void     (*sa_restorer)(void);
};
  • sa_handler:信号捕捉函数地址,可以是 SIG_DFL 表示默认动作,SIG_IGN 表示忽略此信号

  • sa_mask:

    一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原来值

    注意,在信号处理函数被调用时,操作系统建立的新信号屏蔽字包含正被递送的信号(即触发本次捕获的信号),信号处理函数返回时再恢复信号屏蔽字。因此保证在处理一个给定信号时,如果该信号再次发生,那么它将被阻塞到前一个信号的处理结束为止

  • sa_flags:

    对指定信号进行处理的各个选项

    • SA_INTERRUPT:由此信号中断的系统调用不自动重启动
    • SA_NOCLDSTOP:如果 signo 是 SIGCHLD,则在子进程停止时不产生此信号;在子进程终止时仍产生此信号
    • SA_NOCLDWAIT:如果 signo 是 SIGCHLD,则调用进程的子进程终止时不会变成僵尸进程(不会发出SIGCHLD信号)
    • SA_NODEFER:当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask中包含此信号)。
    • SA_RESETHAND:在进入信号处理程序时,将该信号处理方式设置为SIG_DFL,并清除SA_SIGINFO标志
    • SA_RESTART:由此信号中断的系统调用自动重启动
    • SA_SIGINFO:此选项对信号处理程序提供了附加信息,即使用sa_sigaction信号处理程序而不是sa_handler
  • sa_sigaction:

    一个替代的信号处理程序。当该信号使用了SA_SIGINFO标志时,使用该信号处理程序。由于sa_handler和sa_sigaction的实现可能共用同一存储区,因此这两个字段只能有一个。

    之前信号处理函数是以下形式:

    void handler(int signo);
    

    若设置了SA_SIGINFO标志,则信号处理函数是以下形式

    void handler(int signo, siginfo_t *info, void *context);
    
    • 其中第二个参数siginfo_t结构体包含了信号产生原因的有关信息,这样在信号处理函数中我们就可以通过第二个参数知道更多具体与该信号相关的信息。其应该至少包含以下字段

       siginfo_t {int      si_signo;     /* Signal number */int      si_errno;     /* An errno value */int      si_code;      /* 发出信号具体原因 */pid_t    si_pid;       /* Sending process ID */uid_t    si_uid;       /* Real user ID of sending process */int      si_status;    /* Exit value or signal */sigval_t si_value;     /* Signal value */void    *si_addr;      /* Memory location which caused fault */...
      }
      

      其中si_code即为信号发生具体原因:

  • 第三个参数context可以被强制类型转换为ucontext_t类型,该结构体标识信号传递时进程上下文,该ucontext_t结构至少包含以下字段

    typedef struct ucontext_t{struct ucontext_t *uc_link;stack_t uc_stack;mcontext_t uc_mcontext;sigset_t uc_sigmask;...} ucontext_t;
    
    • uc_link:为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。
    • uc_sigmask : 执行当前上下文过程中需要阻塞的信号列表,即信号屏蔽字
    • uc_stack : 为当前context运行的栈信息。
    • uc_mcontext : 保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明

注意,对于支持实时信号扩展的系统,用SA_SIGINFO标志建立的信号处理程序将造成信号可靠排队

  • 注意,相较于signal(被指定信号中断的系统调用默认自动重启动),如果信号处理程序使用sigaction指定,那么被该信号中断的系统调用默认不重新启动(可以通过SA_RESTART标志重启动被中断的系统调用)

15. sigsetjmp和siglongjmp函数

系统在进入信号处理程序时,会将该信号自动加入到信号屏蔽字中,这阻止了后来产生的这种信号中断该信号处理程序,然后再信号处理程序返回时恢复信号屏蔽字。

但是如果在信号处理函数中使用longjmp非局部转移到setjmp处,会导致信号屏蔽字无法恢复。解决方案是调用sigsetjmp和siglongjmp而不是使用setjmp和longjmp

int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

这两个函数和setjmp和longjmp的唯一区别是sigsetjmp增加了参数savesigs。如果该参数非0,则在env参数中保存进程的当前信号屏蔽字,此时在信号处理函数中调用siglongjmp进行非局部跳转到sigsetjmp,会导致恢复保存的信号屏蔽字

sig_atomic_t类型:

当把变量声明为该类型会保证该变量在使用或赋值时, 无论是在32位还是64位的机器上都能保证操作是原子的, 它会根据机器的类型自动适应
在处理信号(signal)的时候,有时对于一些变量的访问希望不会被中断,无论是硬件中断还是软件中断,这就要求访问或改变这些变量需要在计算机的一条指令内完成。通常情况下,int类型的变量通常是原子访问的,也可以认为 sig_atomic_t就是int类型的数据,因为对这些变量要求一条指令完成,所以sig_atomic_t不可能是结构体,只会是数字类型。

sig_atomic_t类型总是用volatile修饰,因为该变量总是由两个不同的控制线程-main函数和异步执行的信号处理程序访问,因此必须保证每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,导致将出现不一致的现象

如这样使用:

volatile sig_atomic_t    flag;

16. sigsuspend函数

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

那么如果针对下面代码则会出现问题:

sigprocmask(SIG_SETMASK,&oldmask,NULL);
pause();

如果sigprocmask解除了某个信号阻塞,而在此期间的确该信号被阻塞了,由上所述,那么就好像该信号发生在sigprocmask和pause函数之间。(或者在sigprocmask和pause函数之间的确有某个未阻塞的信号被递送了),那么将会导致pause函数一直阻塞下去,即sigprocmask和pause函数之间的这个时间窗口中的信号丢失了。

针对此问题,需要在一个原子操作中解除信号阻塞并使进程休眠。

因此可以使用sigsuspend函数,该函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠

int sigsuspend(const sigset_t *mask);

进程的信号屏蔽字设置为mask,在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号并且从该信号处理程序返回,则sigsuspend返回,并且恢复信号屏蔽字为sigsuspend之前的值(并且返回-1,errno设为EINTR)。

17. abort函数

使进程异常终止

void abort(void);

此函数将SIGABRT信号发送给调用进程raise(SIGABRT),不应忽略此信号。

  • 注意,若捕获此信号并且由相应信号处理函数返回,abort仍不会返回到其调用者。如果捕捉到此信号,那么信号处理程序不能返回的唯一方法是调用exit _exit _Exit longjmp或siglongjmp。即abort导致进程的非正常终止,除非SIGABRT 信号被捕获,并且信号处理函数没有返回(使用了longjmp等函数使信号处理函数没有返回)。

  • 并且abort不理会进程对此信号的阻塞或忽略

  • 进程捕获abort调用信号处理程序的意图:

    在进程终止之前执行所需的清理操作。如果进程并不在信号处理程序中终止自己,则当信号处理函数返回时,abort终止该进程

  • 如果abort要终止进程,则它对所有打开标准I/O流的效果应当与进程终止前对每个流调用fclose相同。

18. system函数

  • system函数阻塞SIGCHLD:

    正在执行system函数时,应当阻塞对父进程递送SIGCHLD信号。否则,当system创建的子进程结束时,system的调用者可能错误的认为它自己的一个子进程结束了,然后在SIGCHLD信号处理程序中通过wait函数获取子进程终止状态。由于该子进程终止状态已被获取过了,因此就阻止了system函数获取子进程的终止状态并将其作为返回值

  • system函数忽略SIGINT和SIGQUIT

    由之前的知识可知,当在终端键入Ctrl+C会将SIGINT发送给前台进程组、在终端键入Ctrl+\会将SIGQUIT发送给前台进程组。但是在system期间这两个信号应该只发送给正在运行的程序:即system函数中创建的子进程因为由system执行的命令可能是交互式命令(如ed编辑器),以及system函数的调用者在system执行期间放弃了控制,等待该命令程序执行结束,所以system调用者就不应该接收这两个终端产生的信号。这也是为何规定system的调用者在等待命令完成时应当忽略这两个信号。

19. sleep、nanosleep、clock_nanosleep函数

19.1 sleep

sleep函数使调用进程被挂起直到满足以下条件:

  • 已经超过参数指定的秒数
  • 进程捕捉到一个信号并从信号处理程序返回

该函数返回未休眠剩余的秒数

unsigned int sleep(unsigned int seconds);

注意,sleep可以由alarm函数实现,但是可能造成sleep和alarm函数互相影响。比如先alarm(10),然后再sleep(3),那么对SIGALRM信号的产生情况造成影响。

因此Linux使用nanosleep实现sleep。因为nanosleep不涉及产生任何信号,即与闹钟定时器相互独立,所以该实现的sleep函数不会与其他时间相关函数如alarm产生交互影响

19.2 nanosleep

int nanosleep(const struct timespec *req, struct timespec *rem);struct timespec {time_t tv_sec;        /* seconds */long   tv_nsec;       /* nanoseconds [0 .. 999999999] */};

该函数与sleep类似,但是提供纳秒级别精度。

该函数挂起进程,直到要求的时间超时或者某个信号中断该函数。req参数指定进程休眠时间,rem函数返回未休眠的剩余时间

nanosleep函数并不涉及产生任何信号,所以不用担心与其他函数的交互。

19.3 clock_nanosleep

该函数制定了基于特定时钟的延迟时间来挂起线程

int clock_nanosleep(clockid_t clock_id, int flags,nconst struct timespec *request, struct timespec *remain);
  • clock_id:延迟时间基于的时钟

    • CLOCK_REALTIME:系统实时时间,即从1970年开始的时间
    • CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
    • CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码的CPU时间
    • CLOCK_THREAD_CPUTIME_ID:本线程到当前代码的CPU时间
  • flags:控制时间是绝对还是相对的

    0:相对的,即希望休眠的时间长度

    TIMER_ABSTIME:绝对的,即希望休眠到时钟到达某个特定的时间

  • request和remain:和nanosleep一致。使用绝对时间时,remain参数无用

调用clock_nanosleep(CLOCK_REALTIME,0,req,rem)相当于nanosleep(req,rem)

20. sigqueue函数:信号排队

如果支持信号实时扩展,那么就支持信号排队。如果要使用排队信号,则必须遵循以下条件

  • 使用sigaction函数安装信号处理程序时指定SA_SIGINFO标志。如果没有此标志,信号会阻塞延迟,但是是否进入队列取决于具体实现
  • 在sigaction结构的sa_sigaction成员中(不是sa_handler)提供信号处理程序。实现可能允许用户使用sa_handler字段,但是不能获取sigqueue函数发出的额外信息
  • 使用sigqueue发出信号
int sigqueue(pid_t pid, int sig, const union sigval value);

该函数只把信号发给单个进程。可以通过value参数向信号处理程序传递整数和指针值。除此之外,sigqueue和kill类似

注意,信号不能被无限次排队。到达相应限制后,sigqueue会失败,errno设置为EAGAIN。

在支持实时信号扩展的Linux之中,sigqueue只能用于实时信号(SIGRTMIN~SIGRTMAN之间的信号,包括这两个限制值),这些实时信号默认操作是终止进程。对于非实时信号(1-31信号),不支持信号排队

21. 作业控制信号

以下六个信号与作业控制有关:

  • SIGCHLD:子进程停止或终止
  • SIGCONT:如果进程已停止,则使其继续运行
  • SIGSTOP:停止信号(不能被捕捉或忽略)
  • SIGTSTP:交互式停止信号
  • SIGTTIN:后台进程组成员读控制终端
  • SIGTTOU:后台进程组成员写控制终端

当一个进程产生4种停止信号(SIGTSTP、SIGSTOP、SIGTTIN、SIGTTOU),对进程的任一未决SIGCONT信号丢弃

当对一个进程产生SIGCONT信号时,对同一进程的任一未决停止信号被丢弃

22. 信号名和编号

通过数组sys_siglist获取信号编号和信号名间的映射

extern const char *const sys_siglist[_NSIG];//其中数组下标即为信号编号

使用psignal函数打印与信号编号对应的字符串

void psignal(int sig, const char *msg);

该函数类似于perror,通常是在stderr打印出msg参数,后面跟一个冒号一个空格,然后打印出该信号的说明。

如果在sigaction信号处理程序中有siginfo_t结构,也可以通过siginfo_t结构,使用psiginfo函数打印出信号编号更多的信息

void psiginfo(const siginfo_t *pinfo, const char *msg);

使用strsignal获取与信号相关字符串(而不是写入到文件中),类似于strerror

char *strsignal(int sig);

示例:

int main(int argc, char* argv[]) {cout << "sys_list数组 : " << sys_siglist[SIGCHLD] << endl;psignal(SIGCHLD,"psignal函数 ");cout << "strsignal函数 : " << strsignal(SIGCHLD) << endl;
}
/*打印:
sys_list数组 : Child exited
psignal函数 : Child exited
strsignal函数 : Child exited
*/

ist``获取信号编号和信号名间的映射

extern const char *const sys_siglist[_NSIG];//其中数组下标即为信号编号

使用psignal函数打印与信号编号对应的字符串

void psignal(int sig, const char *msg);

该函数类似于perror,通常是在stderr打印出msg参数,后面跟一个冒号一个空格,然后打印出该信号的说明。

如果在sigaction信号处理程序中有siginfo_t结构,也可以通过siginfo_t结构,使用psiginfo函数打印出信号编号更多的信息

void psiginfo(const siginfo_t *pinfo, const char *msg);

使用strsignal获取与信号相关字符串(而不是写入到文件中),类似于strerror

char *strsignal(int sig);

示例:

int main(int argc, char* argv[]) {cout << "sys_list数组 : " << sys_siglist[SIGCHLD] << endl;psignal(SIGCHLD,"psignal函数 ");cout << "strsignal函数 : " << strsignal(SIGCHLD) << endl;
}
/*打印:
sys_list数组 : Child exited
psignal函数 : Child exited
strsignal函数 : Child exited
*/

《UNIX环境高级编程》笔记 第十章-信号相关推荐

  1. Unix环境高级编程 笔记

    Unix环境高级编程(第二版)学习笔记 这是一次较长时间的整理,然而跳跃了一些章节和很多知识点,仍然是很不完善很不全面的. 前言 操作系统某些问题 严格意义上,可将操作系统定义为一种软件,它控制计算机 ...

  2. UNIX环境高级编程笔记

    1.setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);   SO_REUSEADDR套接口选项允许为以下四个不同的目的提供服务:   ...

  3. UNIX环境高级编程笔记之文件I/O

    一.总结 在写之前,先唠几句,<UNIX环境高级编程>,简称APUE,这本书简直是本神书,像我这种小白,基本上每看完一章都是"哇"这种很吃惊的表情.其实大概三年前,那会 ...

  4. UNIX环境高级编程笔记(2)- STDIN_FILENO、STDOUT_FILENO和stdin、stdout的区别

    目录 前言 一.STDIN_FILENO.STDOUT_FILENO介绍 二.stdin.stdout介绍 三.代码例程 1.文件描述符的使用 2.流的使用 3.代码标记 总结 前言 本章主要通过UN ...

  5. UNIX环境高级编程笔记(14)- 函数sigsuspend 实现父进程子进程同步

    前言 本章主要介绍sigsuspend函数以及实现父进程子进程通过信号的同步. 一.函数sigsuspend #include<signal.h> int sigsuspend(const ...

  6. 函数sleep、nanosleep和clock_nanosleep(UNIX环境高级编程笔记)

    #include <unistd.h> unsigned int sleep(unsigned int seconds);               返回值:0或未休眠完的秒数   此函 ...

  7. UNIX环境高级编程笔记之进程控制

    本章重点介绍了进程控制的几个函数:fork.exec族._exit.wait和waitpid等,主要需要掌握的是父进程和子进程之间的运行机制,怎么处理进程的正常和异常终止.以及怎么让进程执行不同的程序 ...

  8. Unix环境高级编程笔记:12、高级IO

    2019独角兽企业重金招聘Python工程师标准>>> 1.非阻塞IO 系统调用分成"低速"系统调用和其他系统调用.低速系统调用是可能会使进程永远阻塞的一类系统调 ...

  9. 文件io(二)--unix环境高级编程笔记

    在linux中,打开的文件组织结构如下: 与打开的文件相关的有三个数据结构,就是上图中的三部分. 在linux中,有一个进程表,每一个进程在进程表中有一个表项.每一个进程表项中都维护着一张打开文件的描 ...

  10. 《Unix环境高级编程》学习笔记:从点到面

    以前在课堂上学习过<Unix初级教程(第四版)>,对于Unix有了一点了解.由于以后使用的需要,要对它进行比较深入的学习,为此需要阅读不少的书籍,这本<Unix环境高级编程>便 ...

最新文章

  1. python的二维数组操作
  2. 线程函数的设计以及MsgWaitForMultipleObjects函数的使用要点
  3. dedecms mysql 支持_安装dedecms MySQL 支持 不支持无法使用本系统 GD 支持Off解决办法...
  4. Swift之深入解析“泛型”的底层原理
  5. Unity3D——C#编译到运行的过程分析
  6. 用函数实现simulink_VCU/BMS基于模型的开发---Simulink 代码集成
  7. android静默卸载,Android实践 -- Android静默安装和卸载
  8. 用python实现远程复制 (scp + expect )
  9. 微信小游戏出台最严健康游戏管理 未成年游戏时间金额受限制
  10. 95-38-025-Buffer-Buffer1
  11. Shell基础(一):Shell基础应用、简单Shell脚本的设计、使用Shell变量、变量的扩展应用...
  12. 2021辽宁省大学生程序设计竞赛题解
  13. html2canvas提升像素,jspdf + html2canvas 实现html转pdf (提高分辨率版本)
  14. SIFT算法学习总结
  15. 【可视化】数据仓库与数据挖掘大作业
  16. 新闻发布系统3.0(javaBean封装)
  17. 使用pdfFactory Pro虚拟打印机便笺功能为文件添加批注
  18. 机器学习6:单层感知器
  19. Elasticsearch 如何自定义扩展词库?
  20. Android The emulator process for AVD XXX has terminated.

热门文章

  1. 基于matlab的可见光成像通信OOK解调
  2. Android--手机一键Root原理分析
  3. PDF文件中的文字怎么修改?来试试这种修改方法
  4. android 小球移动,android studio滑动小球移动
  5. Python-Pandas-Excel/CSV 数据处理大全整理 (二)
  6. 黄仁勋口述:英伟达的发展之道和星辰大海
  7. Linux命令之设置普通用户具有超级管理员权限sudo
  8. C++游戏编程-走迷宫详解
  9. 后台运行shell命令eog,并用pkill关闭
  10. FieldTrip toolbox教程系列(1)-预处理-读取连续的EEG和MEG数据