这里写目录标题
- 引言
- 1. 信号的基本概念
- 1.1 信号的分类和编号:
- 1.2 查看信号默认处理动作
- 1.3 信号的作用
- 1.4 信号的产生
- 1.4.1通过终端按键产生
- 1.4.2通过系统函数向进程发信号
- 1.4.3由软件条件产生信号
- 1.4.4硬件异常产生信号
- 2. 常见信号及其作用
- `SIGINT (2) - 中断信号:`
- ` SIGTERM (15) - 终止信号:`
- ` SIGKILL (9) - 强制终止信号:`
- `SIGSEGV (11) - 段错误信号:`
- 示例
- 核心转储
- 什么是核心转储?核心转储文件有什么用?
- 应该怎么用?
- 开启核心转储功能
- 怎么查看
- 3. 信号捕捉和处理
- 3.1 信号捕捉函数
- 3.2 sigaction 函数
- 示例
- 问题总结
- 1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?`OS是进程的管理者`
- 2. 信号的处理是否是立即处理的?在合适的时候
- 3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
- 4. 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
- 5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
- 4. 信号阻塞
- 阻塞信号
引言
Linux操作系统中,信号是一种重要的进程间通信机制,用于通知进程发生了某些事件。信号既可以是来自内核的通知,也可以是由其他进程发送的。在本篇博客中,我们将深入探讨Linux信号的作用、产生机制、捕捉方式以及信号阻塞的概念。
1. 信号的基本概念
1.1 信号的分类和编号:
Linux中的信号被分类为标准信号
和实时信号
。
标准信号
是最基本的信号类型,由整数编号表示,编号范围是1到31。
实时信号
是Linux中的扩展信号类型,由整数编号表示,编号范围是32到64。
为什么31-34中间缺少两个信号无法展示呢?
kill -l
命令用于列出系统支持的所有信号名称,但它并不显示编号为32和33的信号。这是因为Linux中的信号被分类为标准信号和实时信号,而编号为32和33的信号属于实时信号,它们在kill -l
的输出中不会显示。
1.2 查看信号默认处理动作
man 7 signal
1.3 信号的作用
信号在Linux系统中有多种作用,包括通知进程某个事件的发生、中断进程的正常执行、以及在进程间传递简单的消息等。不同的信号有不同的含义和影响,了解这些信号是理解Linux系统行为的关键(常见信号及作用请看第2点
)。
1.4 信号的产生
信号可以由多种来源产生,包括硬件故障、用户输入、操作系统事件等。例如,Ctrl+C组合键产生SIGINT信号
,表示中断信号
,通常用于终止正在运行的程序。此外,操作系统还可以向进程发送信号以通知发生的事件,如进程终止、停止等。
1.4.1通过终端按键产生
当用户在终端上按下某些特定的按键组合时,操作系统会向进程发送相应的信号。例如,当用户按下Ctrl+C时,操作系统会向前台进程发送SIGINT信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void signal_handler(int signal) {
printf("Interrupt signal received.\n");
// 在这里可以添加代码来处理中断信号
}
int main() {
// 注册信号和信号处理函数
signal(SIGINT, signal_handler);
// 进入无限循环,等待信号
while (1) {
printf("Waiting for signal...\n");
sleep(1);
}
return 0;
}
在上述示例中,程序通过无限循环等待信号。当用户按下Ctrl+C时,操作系统会向前台进程发送SIGINT信号,并调用之前注册的信号处理函数signal_handler。此时想要终止只能ctrl+\
;
1.4.2通过系统函数向进程发信号
在程序中,可以使用系统函数如kill()或raise()来向指定的进程发送信号。例如,使用kill()函数向指定进程ID的进程发送SIGTERM信号来终止该进程。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
int n=0;
while(n++<5)
{
printf("i am %d\n",n);
}
pid_t pid = getpid();
int signo = 9;
kill(pid, signo);
while(n++<10)
{
printf("i am %d\n",n);
}
return 0;
}
在上述示例中,使用kill()函数向指定进程ID的进程发送SIGTERM信号。kill()函数的第一个参数是要发送信号的进程ID,第二个参数是要发送的信号类型。在这个例子中,我们使用了SIGTERM信号来请求进程终止。
1.4.3由软件条件产生信号
在某些情况下,软件本身可能会根据特定的条件触发信号。例如,当某个进程遇到未处理的异常或错误时,它可能会向自身发送SIGSEGV信号,表示发生了段错误。这种信号通常是由软件内部的错误检测机制触发的。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <assert.h>
void signal_handler(int signal) {
printf("Segmentation fault occurred.\n");
// 在这里可以添加代码来处理段错误信号
}
int main() {
char *buffer = NULL; // 未初始化的指针,可能导致段错误
*buffer = 'A'; // 试图访问未初始化的指针,触发段错误并发送SIGSEGV信号
return 0;
}
在上述示例中,程序试图访问一个未初始化的指针buffer,这会导致段错误并触发SIGSEGV信号。当发生段错误时,操作系统会向进程发送SIGSEGV信号,并调用之前注册的信号处理函数signal_handler。在处理函数中,可以添加代码来处理段错误。需要注意的是,上述代码只是一个演示示例,实际开发中应该避免出现未初始化的指针。
1.4.4硬件异常产生信号
硬件异常产生信号的示例可以是除零异常。在计算机中,除零异常是一种常见的硬件异常。当一个进程试图除以零时,CPU的运算单元会产生异常,并将这个异常解释为SIGFPE信号发送给进程。
以下是一个示例代码,演示了除零异常的产生和处理:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void signal_handler(int signal) {
printf("Division by zero error occurred.\n");
exit(1); // 退出程序
}
int main() {
// 注册信号和信号处理函数
signal(SIGFPE, signal_handler);
int a = 10;
int b = 0;
int result = a / b; // 试图除以零,触发除零异常
printf("Result: %d\n", result); // 这行代码将不会被执行,因为程序在除零异常后退出
return 0;
}
在上述示例中,程序试图将变量a除以变量b,其中b被初始化为0。这将触发除零异常,导致CPU的运算单元产生异常,并将这个异常解释为SIGFPE信号发送给进程。当进程接收到SIGFPE信号时,它会调用之前注册的信号处理函数signal_handler。在处理函数中,程序打印错误信息并退出。由于除零异常的严重性,操作系统通常会终止进程以防止进一步错误。
2. 常见信号及其作用
SIGINT (2) - 中断信号:
- 作用:用于通知进程中断正在执行的操作,通常由用户通过键入 Ctrl+C 生成。
- 例子:在终端中运行一个长时间执行的程序,用户可以按下 Ctrl+C 来发送 SIGINT 信号,终止程序的执行。
SIGTERM (15) - 终止信号:
- 作用:请求进程正常终止,允许进程清理资源和保存状态。
- 例子:当系统关闭时,操作系统向所有运行的进程发送 SIGTERM 信号,请求它们正常退出。
SIGKILL (9) - 强制终止信号:
- 作用:立即终止进程,不允许进程清理资源或保存状态。
- 例子:在系统管理员需要立即停止一个无响应的进程时,可以使用 kill -9 命令发送SIGKILL信号。
SIGSEGV (11) - 段错误信号:
- 作用:表示进程尝试访问其无法访问的内存区域,通常是由于指针错误或内存越界引起。
- 例子:如果一个程序尝试访问已经释放的内存块,操作系统将发送SIGSEGV信号给该进程,使其崩溃。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
// SIGSEGV信号处理函数
void segv_handler(int signum) {
printf("Segmentation fault (SIGSEGV) caught. Exiting...\n");
exit(EXIT_FAILURE);
}
int main() {
// 设置SIGSEGV信号处理函数
signal(SIGSEGV, segv_handler);
int *ptr = NULL;
// 尝试解引用空指针,导致段错误
*ptr = 10;
// 这里的代码不会执行,因为上面的语句导致了段错误
printf("This line will not be reached.\n");
return 0;
}
我们通过signal函数将
segv_handler
函数与SIGSEGV
信号关联起来。当运行程序并尝试解引用空指针时,会触发段错误,然后程序会捕获SIGSEGV
信号并执行segv_handler
函数。在这个处理函数中,我们输出一条消息并调用exit退出程序。
SIGUSR1 (10) 和 SIGUSR2 (12) - 用户定义信号:
- 作用:这两个信号没有预定义的含义,可以由用户自定义其作用。
- 例子:某个应用程序可以使用这两个信号来触发自定义的操作,比如重新加载配置文件或执行特定的功能。
示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// SIGUSR1信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与SIGUSR1相关的操作
}
// SIGUSR2信号处理函数
void sigusr2_handler(int signum) {
printf("Received SIGUSR2 signal.\n");
// 在这里执行与SIGUSR2相关的操作
}
int main() {
// 设置SIGUSR1和SIGUSR2信号处理函数
signal(SIGUSR1, sigusr1_handler);
signal(SIGUSR2, sigusr2_handler);
printf("My process ID: %d\n", getpid());
printf("I am process[%d], Waiting for SIGUSR1 or SIGUSR2 signals...\n",getpid());
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
这些信号的作用覆盖了从正常终止到异常情况的多个场景,使得进程能够及时响应各种事件。在编写程序时,了解这些信号及其作用是确保程序稳定性和可维护性的关键。
核心转储
大多数信号默认行为都是中断进程,那么这些信号又有什么不同呢?
以下述例子为例,我们中断一个进程可以用ctrl+c
也可以用ctrl+\
,但是这两者有什么不同呢?
可以看到两者的行为(Actrion
并不相同),一个为Trem
,另一个为Core
:
-
term信号是终止进程的信号,当进程接收到term信号时,它可以选择正常退出或执行一些清理操作后退出。
-
core信号则是核心转储信号,当进程接收到core信号时,它会被终止,并且操作系统会生成一个核心转储文件(core dump file)。这个核心转储文件包含了进程在内存中的数据和堆栈信息,对于调试程序和排查问题非常有用。
简单来说,term信号是用来终止进程的,而core信号则是用来生成核心转储文件的。
什么是核心转储?核心转储文件有什么用?
核心转储(coredump)是Unix和类Unix系统中的一种机制,用于在程序发生错误时,将程序当时的内存状况、寄存器状况、系统状态等,以转储文件(coredump文件)的形式保存下来。这个机制对于程序的错误调试、系统故障排查等,是非常有用的。
核心转储的生成通常是由操作系统在检测到程序异常终止(如:段错误)时自动进行的。当程序异常终止时,操作系统会首先尝试生成核心转储文件。如果系统支持核心转储,并且用户有相应的权限,就可以通过ulimit命令来设置生成核心转储文件的开关。
核心转储文件包含的内容通常包括:程序状态(例如程序计数器、堆栈指针等)、内存管理信息(例如内存区域的大小、位置等)、处理器和操作系统的标志和信息等。这些信息对于调试程序、理解程序运行状态以及排查系统故障非常有帮助。
对于核心转储文件的解释,需要使用调试器(例如gdb)或者其他专用工具(例如objdump)来进行。调试器可以查看转储进程的地址空间,包括查看变量的值、源代码等。如果系统中没有可用的符号表,调试器可能只能显示一些地址或者十六进制数值,但仍然可以帮助程序员进行故障排查。
总的来说,核心转储是一种重要的系统机制,可以帮助程序员和系统管理员更好地理解和解决程序和系统中的问题。
简要的说,
核心转储就是一个文件,记录了进程在运行过程中发生错误时的内存状态,以及有助于调试的信息
应该怎么用?
开启核心转储功能
ulimit -a //查看前资源限制的设定。
从图中可以看出默认大小为0,也就是默认关闭状态
。
ulimit -c size //设置core文件的大小
ulimit -c size 命令用于设置核心转储文件的大小限制。其中,size 可以是一个具体的数值,也可以是一个关键字 unlimited。
- 如果将 size 设置为一个
具体的数值
,例如 ulimit -c 10000,则核心转储文件的大小将被限制为 10000 字节。如果进程生成的核心转储文件超过了这个大小,那么只有部分数据会被写入到文件中
。- 如果将 size 设置为
unlimited
,则核心转储文件的大小将不受限制
,可以生成任意大小的核心转储文件。
上述案例可以看出
ulimit只对一个终端有效
,其他终端想要改变必须重新设置- 生成的核心转储文件以
core.pid
命名
怎么查看
我们得到的core.pid文件是一个二进制文件,那么我怎么查看呢?
这里就要搬出GDB来帮助我们进行使用了,在GDB中查看核心转储文件的内容需要使用一些特定的命令。以下是一些常用的命令:
命令名称 | 命令作用 |
---|---|
file: | 指定核心转储文件。例如,如果你知道核心转储文件名为core,你可以使用命令"file core"来指定要查看的文件。 |
info proc mappings: | 显示程序的内存映射情况。这可以帮助你理解程序是如何使用内存的,以及哪些内存区域是可访问的。 |
info proc stat: | 显示程序的状态信息,例如程序计数器、堆栈指针等。 |
info proc symbols: | 显示程序的符号表信息,例如函数名称、变量名称等。 |
print: | 打印变量的值。例如,如果你知道在发生段错误之前某个变量被设置了一个值,你可以使用"print"命令来查看这个变量的值。 |
backtrace: | 显示堆栈跟踪信息,这可以帮助你理解程序执行时的调用栈情况。 |
list: | 显示源代码。你可以使用"list"命令来查看特定函数或代码段的源代码。 |
step和next: | 单步执行程序。你可以使用"step"命令来逐行执行程序,或者使用"next"命令来执行下一行而不进入函数调用。 |
break: | 设置断点。你可以使用"break"命令来在特定位置设置断点,以便在程序执行到该位置时停止并查看相关信息。 |
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main()
{
printf("i am process [%d]\n",getpid());
sleep(2);
int a=10/0; //代码出现除0错误
return 0;
}
2秒后崩溃,产生core.pid文件
gdb调试可知是由8号信号导致——8号信号是SIGFPE
SIGFPE | 它表示浮点数异常(Floating Point Exception)。当进程进行浮点数运算时,如果发生了异常,如除以零、溢出、下溢等,操作系统会向进程发送SIGFPE信号。 |
---|
3. 信号捕捉和处理
Linux允许进程捕捉和处理信号,以执行自定义的操作。信号处理可以通过以下方式实现:
3.1 信号捕捉函数
在C语言中,可以使用 signal 函数来设置信号的处理函数。例如:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signal)
{
printf("i am process[%d],get a signal:%d\n",getpid(),signal);
// 处理信号的代码
}
int main()
{
int signo;
// 设置 1~31信号 的处理函数为 sigint_handler
for (signo = 1; signo <= 31; signo++){
signal(signo, handler);
}
while (1){
sleep(1);
// 进程持续运行
}
return 0;
}
但是为什么不能捕捉
9 号信号
呢?
-
9号信号(SIGKILL)是不能被捕捉的。这是因为SIGKILL信号是Linux系统中的一种特殊信号,它具有最高的优先级,并且无法被进程捕获、阻塞或忽略。当进程收到SIGKILL信号时,它会被立即终止,而无法执行任何处理逻辑。
-
这种设计是为了确保系统的稳定性和安全性。SIGKILL信号通常用于强制结束一些不响应或处于异常状态的系统进程,以防止它们对系统造成损害或影响其他进程的正常运行。因此,为了确保这种强制终止的执行,SIGKILL信号不能被捕获或修改其默认行为。
3.2 sigaction 函数
sigaction 函数是一种更为可靠和灵活的处理信号的方式,相较于 signal 函数,它提供了更多的控制选项。它允许指定处理函数、设置标志和提供可选的信号屏蔽。
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); The sigaction structure is defined as something like: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// SIGUSR1 信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与 SIGUSR1 相关的操作
}
int main() {
struct sigaction sa;
// 设置 sa 结构体
sa.sa_handler = sigusr1_handler;
sa.sa_flags = 0;
// 清空 sa_mask,即不阻塞任何其他信号
sigemptyset(&sa.sa_mask);
// 使用 sigaction 函数关联 SIGUSR1 信号和处理函数
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("Error setting up SIGUSR1 handler");
exit(EXIT_FAILURE);
}
printf("My process ID: %d\n", getpid());
printf("Waiting for SIGUSR1 signal...\n");
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
上述示例中我们使用了 sigaction 函数来设置对 SIGUSR1 信号的处理。struct sigaction 结构体用于指定信号处理函数及其他相关属性。在 main 函数中,我们设置了 sa_handler 为 sigusr1_handler,表示接收到 SIGUSR1 信号时执行这个处理函数。
上述示例中,我们还清空了 sa_mask,即不阻塞任何其他信号。如果你希望在信号处理期间阻塞某些其他信号,可以使用 sigaddset 等函数来设置 sa_mask。
此外,sa_flags 可以用来设置不同的标志,例如 SA_RESTART 用于指示系统调用在接收到信号后是否应该自动重启。
问题总结
1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者
操作系统负责管理系统资源和协调各个进程之间的交互。信号是一种用于进程间通信和处理异步事件的机制。当进程发生某些事件(例如错误、用户输入等)时,会生成相应的信号。操作系统作为进程的管理者,负责接收、传递和执行这些信号,以确保系统的正常运行和进程的协同工作。
2. 信号的处理是否是立即处理的?在合适的时候
信号的处理可以是立即的,也可以是在合适的时候执行。某些信号(如SIGKILL)会立即终止进程,而其他一些信号可以被捕获并延迟处理,直到进程执行适当的信号处理程序。
3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
是的,如果信号不是立即处理,通常会被记录下来。记录的方式可以是将信号挂起,或者将其加入到进程的信号队列中。这样,进程在适当的时候可以检查这些信号并执行相应的处理。
4. 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
进程通常可以事先定义信号处理函数(Signal Handler),这样当信号发生时,操作系统会调用相应的信号处理函数。通过注册信号处理函数,进程可以在没有收到信号时就定义好对合法信号的处理方式。
5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
当发生与进程相关的事件时,操作系统会生成相应的信号并将其发送给目标进程。信号可以通过系统调用(例如kill)或其他机制发送。发送过程包括以下步骤:
操作系统生成信号:
根据事件的性质,操作系统生成相应的信号。
确定目标进程:
确定将信号发送给哪个进程。这可以通过进程标识符(PID)等方式来指定目标进程。
发送信号
: 将信号发送给目标进程。这可以通过向进程发送软中断、修改进程的状态等方式实现。
触发信号处理:
如果进程注册了信号处理函数,操作系统会调用该函数来处理信号。如果没有注册处理函数,操作系统可能会采取默认的处理方式,例如终止进程或忽略信号。
总体而言,操作系统向进程发送信号是一种异步的通信方式,用于通知进程发生了某些事件或需要进行特定的处理
4. 信号阻塞
信号阻塞是指进程暂时屏蔽对某些信号的处理。在某些情况下,我们希望在处理一个信号时,暂时阻塞掉其他同类的信号,以确保处理的完整性。可以使用 sigprocmask 函数来设置信号阻塞。
#include <signal.h>
// 阻塞 SIGINT 信号
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
// 执行一些需要阻塞 SIGINT 的操作
// 恢复原信号屏蔽状态
sigprocmask(SIG_SETMASK, &old_mask, NULL);
示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
// SIGUSR1 信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与 SIGUSR1 相关的操作
}
int main() {
// 设置 SIGUSR1 信号处理函数
signal(SIGUSR1, sigusr1_handler);
printf("My process ID: %d\n", getpid());
printf("Waiting for SIGUSR1 signal (blocked)...\n");
// 创建一个集合来存储被阻塞的信号
sigset_t block_mask;
sigemptyset(&block_mask); // 清空信号集
// 将 SIGUSR1 加入到被阻塞的信号集中
sigaddset(&block_mask, SIGUSR1);
// 使用 sigprocmask 阻塞指定信号
if (sigprocmask(SIG_BLOCK, &block_mask, NULL) == -1) {
perror("Error blocking SIGUSR1");
exit(EXIT_FAILURE);
}
// 在这里进行一些工作,期间 SIGUSR1 信号会被阻塞
// 解除对 SIGUSR1 的阻塞
if (sigprocmask(SIG_UNBLOCK, &block_mask, NULL) == -1) {
perror("Error unblocking SIGUSR1");
exit(EXIT_FAILURE);
}
printf("Waiting for SIGUSR1 signal (unblocked)...\n");
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
上述示例中,首先使用
sigemptyset
清空信号集,然后使用 sigaddset 将SIGUSR1
添加到被阻塞的信号集中。接着,使用sigprocmask
函数将这个信号集应用到当前进程,从而阻塞了 SIGUSR1 信号。
在需要解除对信号的阻塞时,同样使用sigprocmask
函数,但这次使用SIG_UNBLOCK
标志。这样就解除了对
SIGUSR1 信号的阻塞。
这个过程演示了如何在程序运行过程中阻塞和解除阻塞指定的信号。在实际应用中,这种机制通常用于确保某些关键部分的原子性
,防止信号中断影响程序的正常执行。
阻塞信号
信号其他相关常见概念
- 实际执行信号的处理动作,称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(pending)。
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后的一种处理动作。
信号在内核中的表示示意图
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。不讨论实时信号。