赞
踩
在Linux系统中,信号(Signal)是一种软件中断,它提供了一种机制,允许进程之间、进程和内核之间传递异步事件通知。这些事件可能是用户从终端输入的一个命令(如Ctrl+C),也可能是系统检测到的一个硬件错误(如内存访问违规)。信号机制为Linux系统提供了丰富的进程间通信手段,并且是实现多任务并发执行和进程管理的重要基础。
很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法:终端用户键入中断键,则会通过信号机构停止一个程序。
信号在Linux系统中的作用和重要性不言而喻。它们为系统提供了一种强大的控制机制,允许管理员和用户通过发送信号来影响进程的执行。例如,通过发送SIGINT
信号,用户可以中断一个正在运行的程序;通过发送SIGTERM
信号,管理员可以优雅地关闭一个服务进程。此外,信号还用于实现进程间的同步和通信,以及处理系统错误和异常情况。
Linux信号是操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。
每个信号都有一个名字。这些名字都以三个字符SIG
开头。在头文件<signal.h>
中,这些信号都被定义为正整数(信号编号)。没有一个信号其编号为 0。
Linux 支持两种类型的信号:
SIGINT
(通常由 Ctrl+C 产生)、SIGTERM
(请求终止)、SIGKILL
(强制终止)等。SIGRTMIN
到 SIGRTMAX
。(即,信号34到64都是实时信号。)SIGABRT
调用abort()
函数时,产生此信号。进程异常终止。
SIGALRM
超过用alarm()
函数设置的时间时产生此信号。
SIGBUS
指示一个实现定义的硬件故障。
SIGCHLD
在一个进程终止或停止时,SIGCHLD
信号被送给其父进程。按系统默认,将忽略此信号。如果父进程希望了解其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用wait()
函数以取得子进程ID和其终止状态。
SIGFPE
此信号表示一个算术运算异常,例如除以0,浮点溢出等。
SIGKILL
这是两个不能被捕捉或忽略信号中的一个。它向系统管理员提供了一种可以杀死任一进程的可靠方法。
SIGQUIT
当用户在终端上按退出键(一般采用 ctrl + \)时,产生此信号,并送至前台进程组中的所有进程 。
在Linux系统中,信号的产生主要有以下几种方式:
SIGINT
信号(2号信号),用于中断前台进程的执行。终端中按下Ctrl+\
,会产生SIGQUIT
信号(3号信号)。kill()
函数:这是一个常用的系统调用,允许用户或进程向另一个进程发送指定的信号。通过kill()
函数,你可以发送几乎所有的信号到指定进程。raise()
函数:这个函数允许进程向自己发送信号。abort()
函数:当程序遇到严重错误时,可以调用此函数来发送SIGABRT
信号,并生成一个核心转储文件(core dump)。alarm()
函数可以设置定时器,当定时器超时时,会向进程发送SIGALRM
信号。pipe
)或套接字(socket
)的写操作,如果接收端已经关闭,则写操作会失败并发送SIGPIPE
信号给进程。alarm
函数允许设置一个定时器,当定时器到期时,会向进程发送一个SIGALRM
信号。这个信号是软件条件产生的,因为它不是由硬件异常或终端按键直接触发的,而是由进程内部的计时器到期触发的。SIGSEGV
)、除零错误(SIGFPE
)等,会向进程发送相应的信号。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。kill
命令将信号发送给其他进程。此程序是kill
函数的界面。常用此命令终止一个失控的后台进程。当某个事件发生时,系统会为该事件生成一个信号。事件可以是上面提到的任意一种。在产生信号时,内核通常在进程的PCB中设置某种形式的一种标志。
但是,无论信号的产生方式有多少种,最终都是操作系统向进程的PCB中写入信号的。
在操作系统中,每个进程都有一个与之关联的PCB,用于存储进程的状态信息,如程序计数器、内存指针、进程状态、信号状态等。当信号产生时,操作系统会根据信号的类型和进程的状态来决定如何处理这个信号。
如果信号没有被阻塞或忽略,并且进程没有为该信号设置特定的处理函数,那么操作系统通常会采取默认的行为,比如终止进程或停止进程的执行。如果进程为该信号设置了处理函数,那么操作系统会在合适的时机调用这个函数来处理信号。但在所有这些情况下,操作系统都需要向进程的PCB中写入或更新信号的状态信息,以确保信号能够被正确地处理。这包括设置信号的未决状态,以便在进程恢复执行时能够递达信号;以及更新信号的阻塞状态,以反映进程对特定信号的阻塞情况。
一旦信号产生,操作系统内核并不会立即处理它,而是会将其状态保存在某个地方,直到合适的时候再进行处理。实际执行信号的处理动作被称为信号递达(delivery)。这种从信号产生到递达之间的状态被称为信号未决(Pending)状态,也就是信号的保存状态。 进程可以选择阻塞某些信号的递达。 当进程阻塞了一个信号时,即使该信号被生成,内核也不会立即将该信号递达给进程,而是将该信号保持在未决状态。直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。
信号的存储在内核中的示意图如下:
我们用描述Linux中信号处理流程的相关表或组件之间的关系。
三张表的每一行是一一对应的。 总之,这张图描述了一个典型的Linux进程在信号处理时如何与这些表或组件进行交互。进程PCB是核心,它包含了进程的状态信息;阻塞表决定了哪些信号应该被忽略;未决表存储了待处理的信号;而处理方式表则定义了如何响应这些信号。阻塞表和未决表是一种位图,某一为置1表示该位所对应的信号被阻塞或待处理。
当递达一个原来被阻塞的信号给进程时,而不是在产生该信号时,内核才决定对它的处理方式。因此,进程在信号递达给它之前仍可改变对它的动作。进程可以调用特定的函数,将指定的信号设置为阻塞和未决。
如果在进程解除对某个信号的阻塞之前,这种信号发送了多次,那么会发生什么呢?
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,这些信号会保持在未决状态,直到进程解除对该信号的阻塞。这些信号会在阻塞解除后递达给进程,但递达的顺序可能会受到操作系统调度策略的影响,并不一定是按照它们被发送的顺序。
也就是说,当一个信号被发送到进程时,如果该信号在进程的信号阻塞集中,这个信号仍会被加入到进程的未决信号集中。如果进程在后续的时间里又收到了相同的信号(在阻塞解除之前),那么我们的未决信号集不会变化。
当进程解除对某个信号的阻塞时,内核会检查该信号的未决状态。如果未决信号集中有该信号,那么内核会按照它的调度策略依次将该信号递达给进程,但具体的处理方式(是执行默认动作、忽略还是调用信号处理函数)取决于进程为该信号注册的处理方式。
需要注意的是,虽然信号在递达之前会保持在未决状态,但某些信号(如
SIGKILL
和SIGSTOP
)是不可阻塞、不可忽略、不可捕获的,它们会立即中断进程的执行。
每个进程都有一个信号屏蔽字,它决定了哪些信号当前被阻塞,不会被内核递达给进程。这个屏蔽字是一个位图,就是阻塞信号集。其中的每一位对应于一个可能的信号,如果某信号的对应位被设置(通常是 1),那么该信号就被阻塞了。进程可以调用sigprocmask
来检测和更改其当前信号屏蔽字。这个函数允许进程指定一个新的信号屏蔽字,并(可选地)获取旧的信号屏蔽字。
总的来说,当信号产生后,它需要通过操作系统内核的干预来传递给目标进程。
在信号处理过程中,进程可能会进行一系列操作,如清理资源、保存状态、记录日志等。然后,根据处理函数的返回值或执行结果,进程可能会继续执行原来的代码、退出或执行其他操作。
需要注意的是,信号的传递和处理是异步的,进程可以在任何时间点接收到信号,不论它当时正在做什么。
在Linux系统中,信号处理是操作系统内核与进程交互的一部分。当一个信号(如SIGHUP
、SIGINT
、SIGQUIT
等)被发送给进程时:
现在,我们来分析信号被阻塞时的情况:
总结起来,Linux信号在进程中的处理流程涉及到信号的接收、屏蔽检查、处理等多个步骤。当信号被阻塞时,它会被放入未决表等待,直到进程检查并处理这些信号。
Linux中有一个能表示多个信号的数据类型,sigset_t
。我们称之为信号集,类似位图。未决表和阻塞表均是这个数据结构。处理方式表定义了进程对各种信号的处理方式。本质上是一个函数指针数组。数组的下标代表了特定的信号。数组的内容是指向一个处理函数的指针。这个处理函数定义了当信号递达时进程应该执行的动作。
阻塞表与未决表相同,中每个比特位代表一个特定的信号。例如,第0位可能代表SIGHUP
信号,第1位代表SIGINT
信号,以此类推。
总结:操作系统内部使用了几种数据结构来保存信号的相关信息,其中最重要的是
pending
表(未决信号集)和block
表(阻塞信号集)。
pending
表:用于0保存当前进程已经接收到但尚未处理的信号。当信号产生时,内核会在进程控制块(PCB)中的pending
表中设置相应的位,表示该信号已经产生但尚未处理。这样,即使信号的产生和处理之间存在一定的时间差,操作系统也能确保不会丢失任何信号。block
表:这个表用于保存当前进程所阻塞的信号。被阻塞的信号在产生时会保持在未决状态,直到进程解除对此信号的阻塞,才会执行递达的动作。阻塞和未决是两种状态,阻塞表示进程选择不处理某个信号,而未决表示信号已经产生但尚未处理。
在 POSIX.1 中,还定义了一组函数来操作 sigset_t
类型的信号集,包括:
函数sigemptyset
初始化由set
指向的信号集,使排除其中所有信号。函数sigfillset
初始化由set
指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用 sigemptyset
或sigfillset
一次。这是因为C编译程序将不赋初值的外部和静态度量都初始化为 0, 而这是否与给定系统上信号集的实现相对应并不清楚。 一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函数 sigaddset
将一个信号添加到现存集中,sigdelset
则从信号集中删除一个信号。对所有以信号集作为参数的函数,都向其传送信号集地址。
信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能只是测试一个变量(例如errno
)来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。可以要求系统在某个信号出现时按照下面三种方式中的一种进行操作:
忽略此信号:
SIGKILL
和SIGSTOP
。这两种信号不能被忽略的原因是:它们向超级用户提供一种使 进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的。signal()
函数或更现代的sigaction()
函数,并指定一个信号处理程序为SIG_IGN
。捕捉信号:
SIGINT
(通常由用户按下Ctrl+C产生)时,进程可以选择优雅地清理并退出,而不是立即终止。为了捕捉一个信号,进程使用signal()
或sigaction()
函数,并指定一个信号处理程序函数。当信号被触发时,操作系统会暂停进程的正常执行,调用相应的处理程序,然后恢复进程的执行。执行系统默认动作:
如果进程没有为某个信号指定处理程序,并且也没有忽略它,那么当该信号被发送时,操作系统会执行与该信号关联的默认动作。这些默认动作通常是终止进程(如SIGTERM
)、停止进程(如SIGSTOP
)、或者产生核心转储文件(如SIGSEGV
)。
对大多数信号的系统默认动作是终止该进程。我们可以使用man 7 signal
命令,去查询信号的默认操作:
在系统的默认行为中,Core
表示在进程当前工作目录的core
文件中复制了该进程的存储图像(该文件名为core)。gdb调试程序可以使用core
文件以检查进程在终止时的状态。
那么内核是如何对收到的一个信号进行处理的呢?
我们做出如下解释:
首先当处理信号的方式是方式或忽略时:
main()
函数)执行过程中遇到中断、异常或系统调用时,进程会进入内核态进行处理(步骤1)。do_signal()
函数来处理信号。do_signal()
函数来表示这一过程)。这些信号可能是之前被阻塞(阻塞在pending位图中)的,或者是在内核处理异常期间产生的。do_signal()
函数执行完毕,进程会再次返回到用户态,继续执行之前被中断的指令(步骤3)。do_signal()
函数完成的。无论是在哪种情况下,一旦返回到用户态,进程都会从其主控制流程中上次被中断的地方继续执行。do_signal()
后会直接返回用户态,并从主控制流程中上次被中断的地方继续执行。那么如果处理方式是用户自定义的:
main()
函数)执行过程中遇到中断、异常或系统调用时,进程会进入内核态进行处理(步骤1)。do_signal()
函数来处理信号(步骤3)。do_signal()
函数完成的。但是如果上面两图在信号处理函数执行完后,又有信号需要递达,我们通常会根据该信号的处理方式(当前处于内核态),或切换到用户态去执行用户的自定义函数,或在内核态执行系统设定的处理方式。
那么我们进行思考:如果一个信号没有被递达,处于未决状态,且该信号没有被阻塞。此时,该信号又被发送给进程会出现什么情况?
我们先来描述关于信号递达和信号处理过程中信号屏蔽字(类似位图的数据结构,sigset_t
)的变化:
当一个信号被发送给进程时,它首先被放置在进程的未决信号集中。这个未决信号集是一个内部的数据结构,用于跟踪进程尚未处理的信号。此时,即使信号已经被发送到进程,但如果进程正在执行一个不允许中断的操作(如某些系统调用),或者进程选择忽略或阻塞了该信号,那么这个信号就不会立即被递达给进程。
当进程处于一个可以接收信号的状态,并且该信号没有被阻塞或忽略时,操作系统就会将信号从未决信号集中取出,并递达给进程。在递达信号之前,操作系统会临时修改进程的信号屏蔽字,将当前被递达的信号的位置设置为阻塞状态(即置1),而且未决信号集中会把该信号的位置设为0。如若此时又接收到该信号,那么该信号在未决信号集的该位置又被设为1。这是为了确保在信号处理函数执行期间,相同类型的信号不会被再次递达给进程,从而防止信号处理函数被递归调用。
在信号处理函数执行完毕,要回到程序原执行处时,操作系统会恢复进程原来的信号屏蔽字,即将之前置为阻塞状态的信号位置重新设置为非阻塞状态(即置0)。这样,进程就可以继续接收和处理其他信号了。
简单来说,就是当一个信号被递达给进程时,操作系统会临时修改进程的信号屏蔽字,将当前被递达的信号的位置设置为阻塞状态。在信号处理函数执行完毕,要回到程序原执行处时,操作系统会恢复进程原来的信号屏蔽字。这样可以确保在信号处理函数执行期间,相同类型的信号不会被再次递达给进程。
操作系统中信号处理是一个常见过程,特别是当信号的处理动作是用户自定义函数(即信号捕捉)时。这个过程涉及用户态和内核态之间的切换,以及信号处理函数和主程序之间的独立控制流程。
信号注册:用户程序使用系统调用(如
signal
或sigaction
)注册了某个信号(如SIGQUIT
)的处理函数sighandler
。主程序执行:用户程序在
main
函数中执行正常的代码逻辑。中断或异常:当程序运行时,如果发生中断或异常(如用户按下
Ctrl+\
组合键产生SIGQUIT
信号),程序会切换到内核态进行中断或异常处理。查信号递达:在中断或异常处理完毕后,内核会检查是否有待处理的信号。如果检测到有
SIGQUIT
信号递达,并且该信号已经被用户程序注册了处理函数sighandler
,内核会决定不立即恢复main
函数的执行。执行信号处理函数:内核通过某种机制(通常是系统调用)将控制权转移给
sighandler
函数。注意,sighandler
函数是在用户态下执行的,并且它使用与main
函数不同的堆栈空间。这两个函数之间没有直接的调用关系,它们是两个独立的控制流程。信号处理函数执行:
sighandler
函数执行用户定义的信号处理逻辑。这个处理过程完全在用户态下进行,并且可以访问用户态的资源。返回内核态:当
sighandler
函数执行完毕后,它会执行一个特殊的系统调用(如sigreturn
)来返回内核态。这个系统调用通知内核信号处理已经完成。复主程序执行:内核检查是否还有其他待处理的信号。如果没有,内核将恢复
main
函数的上下文,并将控制权返回给用户程序,使main
函数从之前被中断的地方继续执行。整个过程体现了操作系统在处理用户程序信号时的灵活性和安全性。通过允许用户注册信号处理函数,操作系统提供了更多的控制权给用户程序,使得用户程序能够根据需要对信号进行定制处理。同时,由于信号处理函数和主程序使用不同的堆栈空间,并且没有直接的调用关系,这保证了它们之间的独立性,防止了可能的堆栈溢出等安全问题。
信号的默认处理行为取决于信号的类型和操作系统的具体实现。当进程接收到一个信号而没有特别指定如何处理这个信号时,系统会采取默认的信号处理行为。这些默认行为通常是由操作系统内核定义的,并且取决于信号的类型。
以下是一些常见信号的默认处理行为:
SIGSTOP
信号用于停止进程的执行,而 SIGCONT
信号则用于恢复被 SIGSTOP
停止的进程。SIGSTOP
的默认行为是立即停止进程的执行,而 SIGCONT
的默认行为是恢复进程的执行。SIGPIPE
),系统默认的行为是忽略它们。当进程接收到这些信号时,它不会执行任何操作,而是继续执行其正常的工作流程。再次再次提到,有些信号的默认行为是进程无法更改的,如 SIGSTOP
和 SIGKILL
。SIGSTOP
总是用于停止进程的执行,而SIGKILL
则用于立即终止进程,这两个信号的默认行为是固定的,进程无法捕获或忽略它们。
核心转储(core dump)并非特指某一种信号,而是在操作系统中,当进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件的过程。这种信息通常用于调试。我们可以通过核心转储文件定位到进程为什么退出,以及执行到哪行代码退出。
具体来说,常将“主内存”(main memory) 称为核心(core),因为在使用半导体作为内存材料之前,人们便是使用“核心”作为内存。核心映像(core image) 就是 “进程”(process)执行当时的内存内容。当进程发生错误或收到“信号”(signal) 而终止执行时,系统会将核心映像写入一个文件,以作为调试之用,这就是所谓的核心转储。
然而,需要注意的是,如果进程没有写入核心文件的权限,那么即使收到上述信号,也不会产生核心转储文件。
我们可以使用ulimit -a
查看核心转储功能是否被打开。若未打开,可以用ulimit -c 10240
。数字是自定义大小。也可以使用ulimit -c 0
关闭。
zyb@myserver:~/study_code/singal_study/demo4$ ulimit -c 10240
zyb@myserver:~/study_code/singal_study/demo4$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 10240
使用 gdb
加载程序的可执行文件。例如,如果程序名为 myprogram
,可以运行 gdb myprogram
来启动 gdb
。然后使用 core-file
命令加载 core
文件。例如,如果核心转储文件的文件名为 core
(这通常是默认的文件名),可以运行 core-file core
。
在 Linux 系统中,当父进程使用 waitpid
或wait
函数等待子进程结束时,子进程的终止状态的信息会被存储在 status
参数所指向的整数变量中。这个 status
变量包含了子进程退出时的各种信息,比如是正常退出还是因为某个信号而终止,以及具体的退出码或信号编号。
我们可以从status
中知道当前进程是否允许核心转储。下图中core dump位为 1 意为允许核心转储。 进程的核心转储标志位标志着该进程是否进行了核心转储。
另外,进程可以通过使用 signal()
或 sigaction()
函数来改变对特定信号的处理方式。这些函数允许进程指定一个自定义的信号处理函数来替代默认的处理行为,或者在接收到信号时忽略它们。这对于实现复杂的进程间通信和同步机制非常有用。
我们可以通过man 7 signal
命令查看。以下是一些常见信号的默认处理行为:
不同的操作系统和不同的进程可能具有不同的默认处理行为。此外,进程可以使用signal()
或sigaction()
函数来改变特定信号的默认处理行为。
信号有默认处理,也可以自定义处理信号(信号的捕捉),也可以忽略信号。这都是对信号的处理,我们对信号的处理方式都可以进行更改。包括signal()
和sigaction()
函数
signo
参数是信号名。handler
是一个指向信号处理程序函数的指针。signal()
调用本身并不会立即执行 handler
函数;它仅仅是设置了当 signum
信号递达时应该调用的函数。handler
的值也可以是常数:
如果指定 SIG_IGN
,则向内核表示忽略此信号。(再次注意:有两个信号SIGKILL
和SIGSTOP
不能忽略。)如果指定SIG_DFL
,则表示接到此信号后的动作是系统默认动作。
当指定函数地址时,我们称此为捕捉此信号。我们称此函数为信号捕捉函数。
#include <stdio.h> #include <signal.h> #include <stdlib.h> // 自定义信号处理函数 void handler(int signum) { printf("Received SIGINT signal, exiting...\n"); exit(signum); // 或者进行其他清理工作 } int main() { // 设置 SIGINT 信号的处理函数为 handler if (signal(SIGINT, handler) == SIG_ERR){ perror("signal"); return 1; } // 进入一个无限循环,等待信号 while (1) { // ... } return 0; }
在这个例子中,当程序接收到 SIGINT
信号时(通常由用户按下 Ctrl+C 触发或使用kill
命令),它会调用 handler
函数,该函数会打印一条消息并退出程序。
当一个进程调用
fork()
时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制 了父进程存储图像,所以信号捕捉函数的地址在子进程中是有意义的。
sigaction()
这个函数提供了比早期 signal()
函数更多的控制和可移植性。
signum
:要检查或修改的信号的编号。act
:如果非空,指向一个包含新信号处理器信息的 sigaction
结构的指针。oldact
:如果非空,指向一个用于存储先前信号处理器信息的 sigaction
结构的指针。// The sigaction structure is defined as something like:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数,或 SIG_IGN 或 SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *); // 替代信号处理函数
sigset_t sa_mask; // 在处理信号时要阻塞的信号集
int sa_flags; // 信号处理的标志
void (*sa_restorer)(void); // 已过时,不使用
};
其中,sa_handler
是一个指向信号处理函数的指针,可以是自定义的函数,也可以是 SIG_IGN
(忽略信号)或 SIG_DFL
(恢复默认处理行为)。sa_sigaction
是一个替代的信号处理函数,它允许接收更详细的信号信息(如 siginfo_t
结构中的信息)。
sa_mask
是一个信号集,指定了在处理信号时要阻塞的信号。这可以确保在处理一个信号时不会被其他信号中断。sa_flags
是一组标志,用于修改信号的行为。例如,SA_RESTART
标志用于指示被中断的系统调用在信号处理后应自动重启。
该已不常用,我们了解即可。
忽略信号也可以使用signal()
函数。例如,如果我们想让进程忽略signo
号信号,可以这样做:
signal(signo, SIG_IGN );
sigpending
返回对于调用进程被阻塞不能递送和当前未决的信号集。该信号集通过set
参数返回。
该函数调用成功返回0,出错返回-1。
sigprocmask
函数用于检查和更改当前进程的信号屏蔽字(signal mask),也就是当前阻塞而不能递达给该进程的信号集。信号屏蔽字是一个位图,其中的每一位对应一个信号,如果某一位被设置(即值为1),则对应的信号就被阻塞,即不会被立即递送到该进程。
sigprocmask
函数的第三个参数(oset
)是一个指向sigset_t
的指针,是输出型参数,用于保存调用sigprocmask
之前的信号屏蔽字的状态。如果oset
不为NULL
,则当前的信号屏蔽字会被保存到*oset
中。这在需要暂时更改信号屏蔽字,然后稍后恢复其原始状态的场景中非常有用。
如果set
是个空指针,则不改变该进程的信号屏蔽字, how
的值也无意义。 下面是针对每种how
参数的详细解释:
SIG_BLOCK
当how
设置为SIG_BLOCK
时,sigprocmask
函数会将当前阻塞的信号集(即当前进程的信号屏蔽字)与set
参数所指向的信号集进行并集操作。换句话说,set
参数中指定的所有信号都会被添加到当前阻塞的信号集中,即使这些信号已经存在于当前的信号屏蔽字中。
SIG_UNBLOCK
当how
设置为SIG_UNBLOCK
时,sigprocmask
函数会从当前阻塞的信号集中移除set
参数所指向的信号集中的所有信号。即使某个信号并不在当前的信号屏蔽字中(即它没有被阻塞),尝试解除对它的阻塞也是允许的,但这样的操作实际上不会产生任何效果,因为该信号本来就没有被阻塞。
SIG_SETMASK
当how
设置为SIG_SETMASK
时,sigprocmask
函数会将当前阻塞的信号集(即当前进程的信号屏蔽字)完全替换为set
参数所指向的信号集。这意味着,无论之前的信号屏蔽字中包含了哪些信号,它们都将被set
参数中的信号集所替代。只有set
参数中指定的信号会被阻塞,其他信号则不会被阻塞。
下面我们试着屏蔽2号信号:
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void handler(int signumber) { std::cout << " signal number is : " << signumber << std::endl; } void PrintSig(sigset_t &pending) { std::cout << "Pending bitmap: "; for (int signo = 31; signo > 0; signo--) { if (sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << std::endl; } int main() { // 1、屏蔽2号信号 sigset_t block; sigemptyset(&block); sigaddset(&block, 2); sigset_t oldblock; // 输出型参数 int n = sigprocmask(SIG_SETMASK, &block, &oldblock); assert(n == 0); cout << "block 2 signal success" << endl; while (1) { // 2、获取pending位图 sigset_t pending; // 数据类型 sigemptyset(&pending); n = sigpending(&pending); assert(n == 0); // 3、打印位图中收到的信号 PrintSig(pending); sleep(1); } }
我们得到下面结果,可以发现我们不能用ctrl+c退出进程了。
zyb@myserver:~/study_code/singal_study/demo8$ make
g++ -o testSig handlerSignal.cc -std=c++11
zyb@myserver:~/study_code/singal_study/demo8$ ./testSig
block 2 signal success
Pending bitmap: 0000000000000000000000000000000
Pending bitmap: 0000000000000000000000000000000
^CPending bitmap: 0000000000000000000000000000010
Pending bitmap: 0000000000000000000000000000010
Pending bitmap: 0000000000000000000000000000010
^CPending bitmap: 0000000000000000000000000000010
^CPending bitmap: 0000000000000000000000000000010
下面我们试着屏蔽所有信号:
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void handler(int signumber) { std::cout << " get a signal, number is : " << signumber << std::endl; } void PrintSig(sigset_t &pending) { std::cout << "Pending bitmap: "; for (int signo = 31; signo > 0; signo--) { if (sigismember(&pending, signo)) { std::cout << "1"; } else { std::cout << "0"; } } std::cout << std::endl; } int main() { // 0、若屏蔽全部信号呢? // 1、屏蔽2号信号 sigset_t block; // 数据类型 sigemptyset(&block); for (int signo = 1; signo <= 31; signo++) sigaddset(&block, signo); sigset_t oldblock; // 输出型参数 int n = sigprocmask(SIG_SETMASK, &block, &oldblock); assert(n == 0); cout << "block 2 signal success" << endl; while (1) { // 2、获取pending位图 sigset_t pending; // 数据类型 sigemptyset(&pending); n = sigpending(&pending); assert(n == 0); // 3、打印位图中收到的信号 PrintSig(pending); sleep(1); } }
观察发现 1~8 10~18可以被屏蔽 9, 19不可以被屏蔽, 18会特殊处理
c=1; while [ $c -le 31 ]; do kill -$c 13963; echo "send $c to process"; let c++; sleep 1; done
信号递达的时候pending位图所对应的位一定会被置0;因此信号在递达前pending位图所对应的位已经被置0。
使用alarm()
函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当 所设置的时间值被超过后,产生 SIGALRM
信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。
alarm函数,系统调用,设定闹钟其实是OS设定的。
返回值是0或以前设置的闹钟时间的余留秒数。
其中,参数seconds
的值是秒数,经过了指定的seconds
秒后会产生信号SIGALRM
。要了解的是, 经过了指定秒后,信号由内核产生,由于进程调度的延迟,进程得到控制能够处理该信号还需一段时间。
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <signal.h> using namespace std; int g_cnt = 0; int ret = 0; void handler(int sig) { std::cout << "get a sig: " << sig << " g_cnt: " << g_cnt << std::endl; unsigned int n = alarm(5); cout << "还剩多少时间: " << n << endl; exit(0); } int main() { // 设定一个闹钟 signal(SIGALRM, handler); alarm(5); // 响一次 int cnt = 0; while (true) { sleep(1); cout << "cnt : " << cnt++ << ", pid is : " << getpid() << endl; if (cnt == 2) { int n = alarm(0); // alarm(0): 取消闹钟 cout << " alarm(0) ret : " << n << endl; } } }
每个进程只能有一个闹钟时间。如果在调用alarm
函数时,以前已为该进程设置过闹钟时间, 而且它还没有超时,则该闹钟时间的余留值作为本次alarm
函数调用的值返回。以前登记的闹 钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds
值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。虽然SIGALARM
的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清除操作。
kill
系统调用将信号发送给进程或进程组。raise
函数则允许进程向自身发送信号。
将编号为sig
的信号发送给运行该代码的进程。
void handler(int signumber) { std::cout << "get signal :" << signumber << std::endl; } int main() { signal(2, handler); int cnt = 0; while (true) { cout << "cnt: " << cnt++ << endl; sleep(1); if (cnt % 5 == 0) { cout << "send 2 to caller" << endl; raise(2); } } }
我们可以使用上述代码来观察现象。
将编号为sig
的信号发送给进程ID为pid
的进程。下面我利用kill()
系统调用函数实现一个类似kill
命令的程序:
// mykill -9 pid int main(int argc, char *argv[]) { if (argc != 3) { cout << "Usage: " << argv[0] << " -signumber pid" << endl; return 1; } int signumber = stoi(argv[1] + 1); int pid = stoi(argv[2]); int n = kill(pid, signumber); if (n < 0) { cerr << "kill error, " << strerror(errno) << endl; } return 0; }
abort()
函数可用于终止进程的给自己发送指定 信号 6) SIGABORT
。
当 abort()
被调用时,程序会立即停止执行,不进行任何正常的清理操作(如关闭打开的文件或释放内存),并返回一个非零值给操作系统,表示程序异常终止。该函数通常用于处理无法恢复的错误情况,或者当程序检测到某些严重问题时需要立即停止。
在调用 abort()
之后,程序会发送一个 SIGABRT
信号给自身。如果已为该信号安装了信号处理器,并且处理器没有调用 abort()
或 _exit()
来终止程序,那么程序将继续执行。但是,在大多数情况下,SIGABRT
信号的默认行为是终止程序。
信号屏蔽(Unblocking):
abort()
函数首先会确保 SIGABRT
信号没有被阻塞(blocked)。如果 SIGABRT
信号已经被阻塞,abort()
会取消对它的阻塞,这样该信号就可以被接收和处理了。
发送信号(Raising Signal):
接下来,abort()
会发送一个 SIGABRT
信号给调用它的进程。这通常是通过调用 raise(SIGABRT)
来实现的。
信号处理(Signal Handling):
如果 SIGABRT
信号被捕获并且处理函数返回了(即没有使用 longjmp(3)
或其他机制直接跳转到其他代码位置),那么进程会继续执行。但是,请注意,这通常不是一个好的做法,因为 abort()
的初衷是终止进程。
默认处理(Default Disposition):
如果 SIGABRT
信号被忽略了(ignored),或者它的处理函数返回了,abort()
会恢复 SIGABRT
信号的默认处理行为(通常是终止进程)。然后,它会再次发送 SIGABRT
信号给进程,确保进程被终止。
因此,无论 SIGABRT
信号是否被捕获或忽略,abort()
函数都会确保进程被异常终止。这是通过首先尝试发送信号,并在必要时恢复信号的默认处理行为来实现的。
当创建一个新的子进程时,它会继承其父进程的信号行为和任何已设置的信号处理程序。但是,一些信号(如 SIGSTOP
和 SIGKILL
)是不能被捕获、忽略或修改的,所以它们的行为在子进程中总是相同的。
当子进程终止时,它会向父进程发送一个SIGCHLD
信号。在默认情况下,这个信号是被忽略的,但父进程可以选择注册一个自定义的信号处理函数来捕获这个信号,并在其中调用wait()
或waitpid()
函数来清理子进程的资源。
以下是关于SIGCHLD
信号在父进程与子进程间信号传递的详细说明:
信号的发送:当子进程的状态改变(如终止、停止或继续)时,内核会自动向该子进程的父进程发送一个SIGCHLD
信号。
信号的接收与处理:
SIGCHLD
信号,但这通常不是一个好的做法,因为忽略该信号可能会导致僵尸进程的产生。僵尸进程是已经终止但其父进程尚未回收其资源的进程。SIGCHLD
信号,并在信号处理函数中调用wait()
或waitpid()
函数来回收子进程的资源。这样做可以确保父进程能够正确地处理子进程的终止,并避免僵尸进程的产生。处理该信号的示例:
子进程退出发送信号给父进程,通知父进程去等待:
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> using namespace std; int cnt = 0; void CleanupChild(int signo) { if (signo == SIGCHLD) { while (true) { pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 : 回收任意一个子进程 if (rid > 0) { std::cout << "wait child success: " << rid << " , cnt: " << ++cnt << std::endl; } else if (rid <= 0) break; } } std::cout << "wait sub process done " << std::endl; } int main() { signal(SIGCHLD, CleanupChild); for (int i = 0; i < 100; i++) { pid_t id = fork(); if (id == 0) { int cnt = 3; while (cnt--) { std::cout << "I am child process: " << getpid() << ", i :" << i << std::endl; sleep(1); } std::cout << "child process died" << ", i :" << i << std::endl; exit(0); } } while (true) sleep(1); return 0; }
设置了一个SIGCHLD
信号的处理函数CleanupChild
,这个函数会在父进程中任何一个子进程终止时被调用。在CleanupChild
函数中,使用了一个while
循环和非阻塞的waitpid
(WNOHANG
选项)来回收已经终止的子进程的PID。
如果将SIGCHLD
的信号处理函数设置为SIG_DFL
,则不会释放子进程资源,子进程退出后会进入僵尸状态;只有将SIGCHLD
处理函数设置为SIG_IGN
,才表示当前父进程不关心子进程状态,子进程退出由系统自动释放其资源,父进程不需要调用wait/waitpid
来获取子进程退出信息。但如果父进程关心子进程的退出信息,建议使用信号捕捉函数来获取子进程退出信息。
x 1signal(SIGCHLD, SIG_IGN );
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。