【Linux】进程信号

【Linux】进程信号

1、基本概念

在Linux和类Unix操作系统中,信号是一种轻量级的通信机制,用于通知进程发生某种事件或异常情况。信号是异步发生的,这意味着进程可以在任何时候接收到信号,而不需要显式地请求或等待它们的到来。当某个事件发生时,内核会向目标进程发送一个信号,目标进程可以选择忽略、采取默认动作或者指定自定义的处理动作。

以下是Linux信号的基本概念:

  1. 信号编号:每个信号都由一个唯一的整数编号来标识,以SIG开头,如SIGINT、SIGTERM、SIGKILL等。信号编号可以通过头文件<signal.h>中的宏定义获得。

  2. 信号产生:信号可以由多种事件或原因产生,例如用户按下CTRL+C键产生SIGINT信号、进程发生错误产生SIGSEGV信号等。

  3. 信号处理:当进程接收到信号时,可以采取不同的处理方式。处理方式包括:

    • 忽略信号:某些信号是可以被进程忽略的,但并不是所有信号都可以被忽略。
    • 执行默认动作:对于大多数信号,系统有预定义的默认动作,如终止进程、退出进程等。
    • 自定义处理:进程可以注册一个信号处理函数,当信号发生时,执行该函数中指定的操作。
  4. 信号传递:信号通常由内核向目标进程发送,进程在任何时候都可能接收到信号。如果进程当前正在执行某些关键代码,可以阻塞某些信号,暂时不处理它们,直到进程准备好处理信号时再解除阻塞。

  5. 常见信号:Linux系统支持多种信号,其中一些比较常见的信号包括:

    • SIGINT:终止进程(通常由CTRL+C触发)。
    • SIGTERM:请求终止进程。
    • SIGKILL:强制终止进程。
    • SIGSEGV:无效的内存访问。
    • SIGCHLD:子进程终止或停止时通知父进程。

信号是进程间通信(IPC)的一种简单方式,但也需要谨慎使用,特别是在涉及信号处理函数时,需要确保处理函数的安全性和可靠性,避免竞态条件和资源冲突。

2、信号产生

重点介绍以下四种常见方式:

  1. 通过终端按键产生信号:
    用户可以通过在终端(控制台)中按下特定的按键组合来产生信号。最常见的例子是按下CTRL+C键组合,它会产生SIGINT信号,通常用于终止正在运行的程序。类似地,CTRL+Z会产生SIGTSTP信号,用于将进程挂起(暂停)。这些按键产生的信号通常称为终端信号。

  2. 调用系统函数向进程发信号:
    进程可以通过调用系统函数kill()raise()主动向其他进程或自身发送信号。kill(pid, signal)函数可以向指定进程ID的进程发送特定信号,raise(signal)函数用于向自身发送信号。这种方式是进程间通信的一种简单形式。

  3. 由软件条件产生信号:
    在程序执行过程中,特定的软件条件可能导致信号的产生。例如,除以零的操作会产生SIGFPE信号,表示浮点异常。在某些计算需要避免异常情况时,可以利用信号来捕捉并处理这些条件。

  4. 硬件异常产生信号:
    硬件故障或异常情况也可能导致信号的产生。例如,当程序尝试访问无效的内存地址或执行非法操作时,硬件会产生异常,内核会将对应的信号发送给进程。最常见的是SIGSEGV信号,表示无效的内存访问。

这四种方式是产生Linux信号的主要途径。信号的产生是异步的,也就是说进程无法预测何时会收到信号,因此在处理信号时需要小心设计,确保程序能够正确地应对不同信号的情况,并且避免因信号处理不当导致的问题。

3、信号状态

3.1 信号在内核表示

在内核中,信号被表示为一组标志位,通常使用位掩码的方式来表示。这些标志位用于表示各种不同的信号,每个信号对应一个唯一的位。内核使用这些标志位来追踪进程收到的信号以及其状态。

通常情况下,使用32位整数作为信号掩码,每个位代表一个特定的信号。在Linux中,可以使用sigset_t数据类型表示信号掩码,它是一个位图类型。不同的信号会在sigset_t中占据相应的位位置,收到一个信号时,相应的位会被置位。

例如,假设第1位代表SIGINT信号(中断信号),第2位代表SIGTERM信号(终止信号),那么如果进程收到SIGINT信号,则sigset_t会被设置为 00000001,如果进程同时收到SIGINT和SIGTERM信号,则sigset_t会被设置为 00000011

内核使用信号掩码来管理进程的信号状态,包括以下几个方面:

  • 未决信号(Pending Signals):位于信号掩码中的相应信号位表示该信号处于未决状态,即信号已经产生但还未递达给进程。
  • 阻塞信号(Blocked Signals):位于信号掩码中的相应信号位表示该信号被阻塞,即进程暂时不接收这些信号。
  • 已处理信号(Handled Signals):进程可以为每个信号设置自定义的信号处理函数,内核通过信号掩码来记录是否已为特定信号设置了处理函数。

使用位掩码的方式可以高效地管理信号状态,允许内核在一组信号中进行位操作,从而更快地判断信号状态和处理相应的信号。

3.2 递达阻塞

  1. 信号递达(Signal Delivery):
    信号递达是指一旦信号产生后,进程接收到信号并且执行对应的处理动作的过程。在信号递达时,进程可以采取不同的处理方式,包括执行默认动作、执行自定义信号处理函数或者忽略信号。

  2. 信号未决(Pending):
    信号从产生到实际递达之间的状态称为信号未决。当信号产生后,它可能会暂时处于未决状态,这意味着信号已经生成,但还没有递达给目标进程。如果该信号在进程中处于未决状态,那么它不会再次产生。

  3. 信号阻塞(Signal Blocking):
    进程可以选择阻塞(屏蔽)某个信号,这样当被阻塞的信号产生时,它们不会立即被处理,而是被放置在未决状态,直到进程解除对该信号的阻塞。阻塞信号可以防止信号的递达,直到进程解除阻塞后,才执行信号的处理。

  4. 阻塞和忽略的区别:
    阻塞信号和忽略信号是两种不同的处理方式。当信号被阻塞时,它不会递达给进程,而是保持在未决状态。进程可以通过解除对信号的阻塞来允许信号递达和处理。而忽略信号是在信号递达后对信号的处理方式。如果进程选择忽略某个信号,那么当该信号递达时,进程会忽略它,不执行任何处理动作。

阻塞和忽略是进程对信号处理的主动选择,进程可以根据需要对不同的信号采取不同的处理策略,以确保程序的正确性和可靠性。

4、信号集 & 操作函数

在Linux系统中,信号集(Signal Set)是一种用于管理多个信号的数据结构。它是一个位图,每个位对应一个特定的信号,用于表示信号的集合。信号集允许对多个信号进行操作,如添加信号、删除信号、检查信号是否在集合中等。在C语言中,信号集通常通过sigset_t类型表示。

信号集操作函数用于创建、设置、修改和查询信号集,这些函数允许程序员以编程方式管理信号的集合。以下是一些常见的信号集操作函数:

  1. sigemptyset(sigset_t *set)
    该函数用于初始化一个空的信号集,即将信号集中的所有位都清零,表示没有任何信号被包含在集合中。

  2. sigfillset(sigset_t *set)
    该函数用于将信号集设置为包含所有可能的信号,即将信号集中的所有位都置为1,表示集合包含所有信号。

  3. sigaddset(sigset_t *set, int signum)
    该函数用于向信号集中添加特定的信号。参数signum指定要添加的信号编号,将该信号的相应位设置为1,表示该信号被添加到信号集中。

  4. sigdelset(sigset_t *set, int signum)
    该函数用于从信号集中删除特定的信号。参数signum指定要删除的信号编号,将该信号的相应位设置为0,表示该信号被从信号集中删除。

  5. sigismember(const sigset_t *set, int signum)
    该函数用于检查特定的信号是否在信号集中。参数signum指定要检查的信号编号,如果该信号在信号集中,返回非0值,否则返回0。

这些信号集操作函数允许程序员方便地管理多个信号的集合,以便进行信号的阻塞、解除阻塞、处理等操作。在多线程和进程间通信的场景中,信号集也是重要的数据结构,用于控制各个线程或进程对信号的处理方式。

4.1 sigprocmask & sigpending

sigprocmasksigpending是两个与信号集相关的系统调用,用于在Unix/Linux系统中管理进程的信号阻塞状态和未决信号。

  1. sigprocmask系统调用:
    sigprocmask用于更改进程的信号屏蔽集(Signal Mask Set)。信号屏蔽集决定了哪些信号在进程中被阻塞,即信号集中相应的位被置为1的信号将被阻塞,不会递达给进程。该系统调用的原型如下:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how参数指定了如何更改信号屏蔽集:

    • SIG_BLOCK:将set中的信号添加到当前进程的信号屏蔽集中。
    • SIG_UNBLOCK:将set中的信号从当前进程的信号屏蔽集中解除阻塞。
    • SIG_SETMASK:将当前进程的信号屏蔽集替换为set中的信号。
  • set参数指向一个新的信号集,用于指定要更改的信号。

  • oldset参数用于存储之前的信号屏蔽集,如果不为NULL,则旧的信号屏蔽集将被存储在oldset中。

使用sigprocmask可以控制进程对特定信号的阻塞状态,防止信号的递达或解除对信号的阻塞,以满足程序的需要。

  1. sigpending系统调用:
    sigpending用于获取进程的未决信号集(Pending Signal Set)。未决信号集包含了当前已经产生但由于进程阻塞而暂时未能递达给进程的信号。该系统调用的原型如下:
int sigpending(sigset_t *set);
  • set参数用于存储进程的未决信号集,获取的未决信号将被存储在set中。

通过sigpending可以获取进程当前处于未决状态的信号,这些信号在进程解除阻塞后将递达给进程,可以在适当的时候进行处理。

总结:sigprocmask用于更改进程的信号屏蔽集,控制信号的阻塞和解除阻塞;sigpending用于获取进程的未决信号集,查看当前处于未决状态的信号。这两个系统调用是信号处理中非常重要的工具,帮助程序员对信号的处理进行精确控制。

5、信号捕捉

5.1 内核实现捕捉

在Unix/Linux系统中,内核是如何实现信号的捕捉和处理的,可以总结为以下几个步骤:

  1. 注册信号处理函数:
    用户程序在需要捕捉特定信号时,通过调用signal()sigaction()等函数来注册信号处理函数。这样,当相应的信号递达时,内核会调用用户定义的信号处理函数。

  2. 中断或异常发生:
    当进程执行用户态代码(如main()函数)时,如果发生中断或异常(如硬件异常或其他进程向当前进程发送信号),系统会将控制权从用户态切换到内核态,进入信号处理过程。

  3. 保存用户态上下文:
    在切换到内核态之前,内核会保存用户态下的上下文信息,包括寄存器、堆栈指针等。这是为了在信号处理函数执行完毕后,能够正确恢复用户态的执行。

  4. 选择合适的处理函数:
    在内核态,处理信号的过程会检查进程的信号屏蔽集和未决信号集,确定是否有信号需要递送给当前进程。如果有,根据信号的处理方式,选择相应的处理函数。

  5. 执行信号处理函数:
    如果信号的处理方式是调用用户定义的信号处理函数,内核会将控制权转移到相应的信号处理函数,并在用户态执行该函数。信号处理函数运行在独立的栈空间中,与原来的用户态函数没有调用关系,因此它们可以独立执行。

  6. 特殊系统调用sigreturn:
    在信号处理函数执行完毕后,内核会让信号处理函数返回,并执行一个特殊的系统调用sigreturn。这个系统调用会重新切换到内核态,并根据保存的上下文信息将控制权还给用户态。

  7. 恢复用户态上下文:
    在重新回到用户态之前,内核会恢复之前保存的用户态上下文信息,包括寄存器、堆栈指针等,使得程序能够继续执行之前被中断的地方。

  8. 继续执行用户态代码:
    最后,内核将控制权交还给用户态,程序继续在用户态执行,从信号处理函数返回后继续执行main函数或其他用户态代码。

通过这样的流程,内核实现了信号的捕捉和处理机制,允许进程对特定信号采取自定义的处理方式,从而实现了进程间的通信和事件处理。信号是一种轻量级的通信机制,在Unix/Linux系统中被广泛使用。

5.2 sigaction

sigaction函数是用于设置信号处理方式的系统调用,它提供了更强大和灵活的信号处理机制,相对于signal函数而言更加推荐使用。sigaction函数在Unix/Linux系统中提供,用于注册对特定信号的自定义信号处理函数。

以下是sigaction函数的原型:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:指定要设置的信号编号,如SIGINTSIGTERM等。
  • act:指向一个struct sigaction结构体,用于指定新的信号处理方式。可以设置信号的处理函数、信号屏蔽集、以及其他一些标志位。
  • oldact:指向一个struct sigaction结构体的指针,用于存储之前注册的信号处理方式。如果不需要保存之前的处理方式,可以将该参数设为NULL

struct sigaction结构体定义如下:

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_sigaction)(int, siginfo_t *, void *);
};
  • sa_handler:指定信号的处理函数。可以设置为一个函数指针,当信号递达时会调用该函数来处理信号。

  • sa_mask:设置在信号处理函数执行期间要屏蔽的信号集。这样做可以避免在信号处理函数执行时又收到其他信号的干扰。

  • sa_flags:用于设置一些标志位,控制信号处理的行为。常见的标志位有:

    • SA_RESTART:当信号处理函数执行时,如果系统调用被中断,允许自动重启被中断的系统调用。
    • SA_SIGINFO:指定信号处理函数为sa_sigaction,而不是sa_handler,用于支持更复杂的信号处理。
  • sa_sigaction:指定一个信号处理函数,如果设置了SA_SIGINFO标志位,则该函数会被调用。

使用sigaction函数可以注册更为灵活的信号处理方式,它可以替代旧的signal函数,提供更多的信号处理选项,同时也更加可靠和可移植。因此,在编写新的代码时,建议使用sigaction函数来处理信号。

6、可重入函数

可重入函数(Reentrant Function)是指一个函数可以被安全地同时在多个线程中调用,而不会产生竞态条件或出现不正确的结果。多角度解释可重入函数可以从以下几个方面进行:

  1. 线程安全性:
    可重入函数是线程安全的,意味着多个线程可以同时调用这个函数,而不需要额外的同步措施。在函数的执行过程中,它不使用全局变量或静态变量,也不修改任何共享状态,从而避免了线程之间的竞态条件和数据竞争。

  2. 不依赖于全局数据:
    可重入函数不依赖于全局数据或静态数据,它的执行结果只依赖于输入参数和内部局部变量。这样的设计确保了在任何上下文中调用该函数都会得到正确的结果,不会受到其他调用或全局状态的影响。

  3. 不使用非本地资源:
    可重入函数不会使用非本地资源,例如不会调用不可重入的函数、不会进行文件IO操作或访问共享设备。这样做是为了避免多个线程之间对非本地资源的竞争,从而保证函数的可重入性。

  4. 堆栈内分配:
    可重入函数在需要使用临时变量时,会使用堆栈内的局部变量进行分配,而不使用静态或全局变量。这样做可以确保不同线程之间的临时变量不会相互干扰,从而保持函数的可重入性。

  5. 信号安全性:
    可重入函数是信号安全的,即在信号处理函数中可以安全地调用它,而不会引发不确定的行为。这是因为信号处理函数通常在异步上下文中执行,它可能打断正在执行的代码,而可重入函数在被信号处理函数调用时仍然可以正确执行,不会出现问题。

总结:
可重入函数是一种在多线程和异步上下文中安全使用的函数,它不依赖于全局状态、非本地资源或静态变量,而且可以同时在多个线程中调用而不会导致问题。可重入函数是编写并发程序和信号处理函数的基础,因为它们的设计能够避免许多与多线程并发相关的问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值