您现在的位置是:首页 >技术杂谈 >Linux 信号学习网站首页技术杂谈
Linux 信号学习
Linux 信号学习
信号量的基本概念
信号是事件发生时对进程的通知机制,也可以把它称为 软件中断 。 信号与硬件中断的相似之处在于能够打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。
信号产生的条件
一个具有合适权限的进程能够向另一个进程发送信号 ,信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。信号可以由“谁”发出呢?以下列举的很多情况均可以产生信号:
- 硬件发生异常 ,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程。硬件检测到异常的例子包括 执行一条异常的机器语言指令 ,诸如, 除数为0 、 数组访问越界导致引用了无法访问的内存区域 等,这些异常情况都会被硬件检测到,并通知内核、然后内核为该异常情况发生时正在运行的进程发送适当的信号以通知进程。
- 在终端下输入能够产生信号的特殊字符 。譬如在终端上按下 CTRL + C 组合按键可以产生
中断信号(SIGINT)
;按下 CTRL + Z 组合按键可以产生暂停信号(SIGCONT)
。 - 进程调用
kill()
系统调用 可 将任意信号发送给另一个进程或进程组 。当然对此是有所限制的,接收信号的进程和发送信号的进程的所有者必须相同,亦或者发送信号的进程的所有者是root超级用户。 - 用户可以通过
kill
命令 将信号发送给其它进程。kill
命令想必大家都会使用,通常我们会通过kill
命令来“杀死”(终止)一个进程,譬如在终端下执行"kill -9 xxx"
来杀死PID为xxx的进程,这里的-9
其实指的就是发送编号为9的信号,也就是SIGKILL
信号。 - 发生了软件事件 ,即当检测到某种软件条件已经发生。这里指的不是硬件产生的条件(如除数为0、引用无法访问的内存区域等),而是软件的触发条件、触发了某种软件条件(进程所设置的 定时器已经超时 、 进程执行的CPU时间超限 、 进程的某个子进程退出 等等情况)。
信号如何被处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行以下操作之一:
- 忽略信号 。也就是说, 当信号到达进程后,该进程并不会去理会它、直接忽略 ,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但 有两种信号却决不能被忽略 ,它们是
SIGKILL
和SIGSTOP
,这两种信号不能被忽略的原因是: 它们向内核和超级用户提供了使进程终止或停止的可靠方法 。另外,如果忽略某些由硬件异常产生的信号,则进程的运行行为是未定义的。 - 捕获信号 。 当信号到达进程后,执行预先绑定好的信号处理函数 。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了
signal()
系统调用可用于注册信号的处理函数,将会在后面向大家介绍。 - 执行系统默认操作 。 进程不对该信号事件作出处理,而是交由系统进行处理 ,每一种信号都会有其对应的系统默认的处理方式,8.3小节中对此有进行介绍。需要注意的是, 对大多数信号来说,系统默认的处理方式就是终止该进程 。
信号的异步特质
信号是异步事件的经典实例 ,产生信号的事件对进程而言是随机出现的,进程无法预测该事件产生的准确时间,进程不能够通过简单地测试一个变量或使用系统调用来判断是否产生了一个信号, 这就如同硬件中断事件,程序是无法得知中断事件产生的具体时间,只有当产生中断事件时,才会告知程序、然后打断当前程序的正常执行流程、跳转去执行中断服务函数 ,这就是异步处理方式。
信号的分类
可靠信号/不可靠信号
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,**把那些建立在早期机制上的信号叫做"不可靠信号",Linux 中信号值小于 SIGRTMIN(34)
的信号都是 不可靠信号 **。不可靠信号的来源主要是:
- 进程每次处理信号后,就将对信号的响应设置为默认动作 。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
- 信号可能丢失 ,后面将对此详细阐述。 如果在进程对某个信号进行处理时, 这个信号发生多次 ,对后到来的这类信号不排队,那么 仅传送该信号一次 ,即发生了信号丢失。因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此, Linux下的不可靠信号问题主要指的是信号可能丢失 。
随着时间的发展,实践证明,有必要对信号的原始机制加以改进和扩充,所以,后来出现的各种UNIX版本分别在这方面进行了研究,力图实现"可靠信号"。 由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号(SIGRTMIN~SIGRTMAX),并在一开始就把它们定义为可靠信号 ,在Linux系统下使用 kill -l
命令可查看到所有信号,如下所示:
Tips:括号" ) "前面的数字对应该信号的编号,编号131所对应的是不可靠信号,编号3464对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了
SIGRTMIN+N
或SIGRTMAX-N
的方式来表示。
可靠信号支持排队,不会丢失,同时,信号的发送和绑定也出现了新版本,信号发送函数 sigqueue()
及信号绑定函数 sigaction()
。
实时信号/非实时信号
实时信号与非实时信号其实是从时间关系上进行的分类,与可靠信号与不可靠信号是相互对应的, 非实时信号都不支持排队,都是不可靠信号 ; 实时信号都支持排队,都是可靠信号 。实时信号保证了发送的多个信号都能被接收,实时信号是POSIX标准的一部分,可用于应用进程。
一般我们也把非实时信号(不可靠信号)称为标准信号,如果文档中用到了这个词,那么大家要知道,这里指的就是非实时信号(不可靠信号)。
实时信号较之于标准信号,其优势如下:
- 实时信号的信号范围有所扩大 ,可应用于应用程序自定义的目的,而标准信号仅提供了两个信号可用于应用程序自定义使用:
SIGUSR1
和SIGUSR2
。 - 内核对于实时信号所采取的是队列化管理 。如果将某一实时信号多次发送给另一个进程,那么将会多次传递此信号。相反,对于某一标准信号正在等待某一进程,而此时即使再次向该进程发送此信号,信号也只会传递一次。
- 当发送一个实时信号时,可为信号指定伴随数据 (一整形数据或者指针值),供接收信号的进程在它的信号处理函数中获取。
- 不同实时信号的传递顺序得到保障 。如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高,如果是同一类型的多个信号在排队,那么信号(以及伴随数据)的传递顺序与信号发送来时的顺序保持一致。
Linux内核定义了31个不同的实时信号,信号编号范围为34~64 ,使用 SIGRTMIN
表示编号最小的实时信号,使用 SIGRTMAX
表示编号最大的实时信号, 其它信号编号可使用这两个宏加上一个整数或减去一个整数 。
应用程序当中使用实时信号,需要有以下的两点要求:
- 发送进程使用
sigqueue()
系统调用向另一个进程发送实时信号以及伴随数据,见后文。 - 接收实时信号的进程要为该信号建立一个信号处理函数,使用
sigaction
函数为信号建立处理函数,并加入SA_SIGINFO
,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction
指针指向的处理函数,而不是sa_handler
,当然允许应用程序使用sa_handler
,但这样就不能获取到实时信号的伴随数据了。
常见信号与默认行为
信号本质上是 int
类型的数字编号 ,这就好比硬件中断所对应的中断号。 内核针对每个信号,都给其定义了一个唯一的整数编号 ,从数字1开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏),信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样, 所以在程序当中一般都使用信号的符号名 (也就是宏定义)。
这些信号在<signum.h>头文件中定义,每个信号都是以SIGxxx开头。Linux下对标准信号(不可靠信号、非实时信号)的编号为1~31:
信号名称 编号 描述 系统默认操作 SIGINT 2 终端中断符 term SIGQUIT 3 终端退出符 term+core SIGILL 4 非法硬件指令 term+core SIGABRT 6 异常终止 (abort) term+core SIGBUS 7 内存访问错误 term+core SIGFPE 8 算术异常 term+core SIGKILL 9 终极终止信号 term SIGUSR1 10 用户自定义信号 1 term SIGSEGV 11 无效的内存引用 term+core SIGUSR2 12 用户自定义信号 2 term SIGPIPE 13 管道关闭 term SIGALRM 14 定时器超时 (alarm) term SIGTERM 15 终止进程 term SIGCHLD/SIGCLD 17 子进程终止或停止 ignore SIGCONT 18 使停止状态的进程继续运行 cont SIGSTOP 19 停止进程 stop SIGTSTP 20 终端停止符 stop SIGXCPU 24 超过 CPU 限制 term+core SIGVTALRM 26 虚拟定时器超时 term SIGWINCH 28 终端窗口尺寸发生变化 ignore SIGPOLL/SIGIO 29 异步 I/O term/ignore SIGSYS 31 无效系统调用 term+core egin{array}{|c|c|c|c|} hline ext { 信号名称 } & ext { 编号 } & ext { 描述 } & ext { 系统默认操作 } \ hline ext { SIGINT } & 2 & ext { 终端中断符 } & ext { term } \ hline ext { SIGQUIT } & 3 & ext { 终端退出符 } & ext { term+core } \ hline ext { SIGILL } & 4 & ext { 非法硬件指令 } & ext { term+core } \ hline ext { SIGABRT } & 6 & ext { 异常终止 (abort) } & ext { term+core } \ hline ext { SIGBUS } & 7 & ext { 内存访问错误 } & ext { term+core } \ hline ext { SIGFPE } & 8 & ext { 算术异常 } & ext { term+core } \ hline ext { SIGKILL } & 9 & ext { 终极终止信号 } & ext { term } \ hline ext { SIGUSR1 } & 10 & ext { 用户自定义信号 } 1 & ext { term } \ hline ext { SIGSEGV } & 11 & ext { 无效的内存引用 } & ext { term+core } \ hline ext { SIGUSR2 } & 12 & ext { 用户自定义信号 } 2 & ext { term } \ hline ext { SIGPIPE } & 13 & ext { 管道关闭 } & ext { term } \ hline ext { SIGALRM } & 14 & ext { 定时器超时 (alarm) } & ext { term } \ hline ext { SIGTERM } & 15 & ext { 终止进程 } & ext { term } \ hline ext { SIGCHLD/SIGCLD } & 17 & ext { 子进程终止或停止 } & ext { ignore } \ hline ext { SIGCONT } & 18 & ext { 使停止状态的进程继续运行 } & ext { cont } \ hline ext { SIGSTOP } & 19 & ext { 停止进程 } & ext { stop } \ hline ext { SIGTSTP } & 20 & ext { 终端停止符 } & ext { stop } \ hline ext { SIGXCPU } & 24 & ext { 超过 CPU 限制 } & ext { term+core } \ hline ext { SIGVTALRM } & 26 & ext { 虚拟定时器超时 } & ext { term } \ hline ext { SIGWINCH } & 28 & ext { 终端窗口尺寸发生变化 } & ext { ignore } \ hline ext { SIGPOLL/SIGIO } & 29 & ext { 异步 I/O } & ext { term/ignore } \ hline ext { SIGSYS } & 31 & ext { 无效系统调用 } & ext { term+core } \ hline end{array} 信号名称 SIGINT SIGQUIT SIGILL SIGABRT SIGBUS SIGFPE SIGKILL SIGUSR1 SIGSEGV SIGUSR2 SIGPIPE SIGALRM SIGTERM SIGCHLD/SIGCLD SIGCONT SIGSTOP SIGTSTP SIGXCPU SIGVTALRM SIGWINCH SIGPOLL/SIGIO SIGSYS 编号 2346789101112131415171819202426282931 描述 终端中断符 终端退出符 非法硬件指令 异常终止 (abort) 内存访问错误 算术异常 终极终止信号 用户自定义信号 1 无效的内存引用 用户自定义信号 2 管道关闭 定时器超时 (alarm) 终止进程 子进程终止或停止 使停止状态的进程继续运行 停止进程 终端停止符 超过 CPU 限制 虚拟定时器超时 终端窗口尺寸发生变化 异步 I/O 无效系统调用 系统默认操作 term term+core term+core term+core term+core term+core term term term+core term term term term ignore cont stop stop term+core term ignore term/ignore term+core
term
表示终止进程;core
表示生成核心转储文件,核心转储文件可用于调试,这个便不再给介绍了;ignore
表示忽略信号;cont
表示继续运行进程;stop
表示停止进程(注意停止不等于终止,而是暂停)。
具体介绍各信号作用如下:
信号名称 | 信号作用 |
---|---|
SIGINT | 当用户在终端按下中断字符(通常是CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程。 该信号的系统默认操作是终止进程的运行。 所以通常我们都会使用CTRL + C来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。 |
SIGQUIT | 当用户在终端按下退出字符(通常是CTRL + )时,内核将发送 SIGQUIT 信号给前台进程组中的每一个进程。 该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。 进程如果陷入无限循环、或不再响应时,使用SIGQUIT信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符CTRL + C、也可以按下退出字符CTRL + 来终止,当然前提条件是,此进程会将SIGINT信号或SIGQUIT信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。 |
SIGILL | 如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。该信号的系统默认操作是终止进程的运行。 |
SIGABRT | 当进程调用abort()系统调用时(进程异常终止),系统会向该进程发送SIGABRT信号。该信号的系统默认操作是终止进程、并生成核心转储文件。 |
SIGBUS | 产生该信号(总线错误,bus error)表示发生了某种内存访问错误。该信号的系统默认操作是终止进程。 |
SIGFPE | 该信号因特定类型的算术错误而产生,譬如除以0。该信号的系统默认操作是终止进程。 |
SIGKILL | 此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用SIGINT信号和SIGQUIT信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,如果使用SIGINT或SIGQUIT无法终止进程,那就使用“必杀信号”SIGKILL吧。Linux下有一个kill命令,kill命令可用于向进程发送信号, 我们会使用"kill -9 xxx"命令来终止一个进程(xxx表示进程的pid),这里的-9其实指的就是发送编号为9的信号,也就是SIGKILL信号。 |
SIGUSR1 | 该信号和SIGUSR2信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。 |
SIGSEGV | 这一信号非常常见,当应用程序对内存的引用无效时,操作系统就会向该应用程序发送该信号。引起对内存无效引用的原因很多,C语言中引发这些事件往往是解引用的指针里包含了错误地址(譬如,未初始化的指针),或者传递了一个无效参数供函数调用等。该信号的系统默认操作是终止进程。 |
SIGUSR2 | 与SIGUSR1信号相同。 |
SIGPIPE | 涉及到管道和socket,当进程向已经关闭的管道、FIFO或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。 |
SIGALRM | 与系统调用alarm()或setitimer()有关,应用程序中可以调用alarm()或setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送SIGALRM信号给该应用程序,关于alarm()或setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。 |
SIGTERM | 这是用于终止进程的标准信号,也是kill命令所发送的默认信号(kill xxx,xxx表示进程pid),有时我们会直接使用"kill -9 xxx"显式向进程发送SIGKILL信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获SIGTERM信号、并为其绑定一个处理函数,当该进程收到SIGTERM信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用SIGKILL信号终止进程,从而跳过了SIGTERM信号的处理函数,通常SIGKILL终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用SIGTERM,实在不行再使用最后手段SIGKILL。 |
SIGCHLD | 当父进程的某一个 子进程终止时 ,内核会 向父进程发送该信号 。当父进程的某一个 子进程因收到信号而停止或恢复时 ,内核也可能 向父进程发送该信号 。注意这里说的停止并不是终止,你可以理解为暂停。该信号的系统默认操作是忽略此信号,如果父进程希望被告知其子进程的这种状态改变,则应捕获此信号。 |
SIGCLD | 与SIGCHLD信号同义。 |
SIGCONT | 将该信号发送给已停止的进程,进程将会恢复运行。当进程接收到此信号时并不处于停止状态,系统默认操作是忽略该信号,但如果进程处于停止状态,则系统默认操作是使该进程继续运行。 |
SIGSTOP | 这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。 |
SIGTSTP | 这也是一个停止信号,当用户在终端按下停止字符(通常是CTRL + Z),那么系统会将SIGTSTP信号发送给前台进程组中的每一个进程,使其停止运行。 |
SIGXCPU | 当进程的CPU时间超出对应的资源限制时,内核将发送此信号给该进程。 |
SIGVTALRM | 应用程序调用setitimer()函数设置一个虚拟定时器,当定时器定时时间到时,内核将会发送该信号给进程。 |
SIGWINCH | 在窗口环境中,当终端窗口尺寸发生变化时(譬如用户手动调整了大小,应用程序调用ioctl()设置了大小等),系统会向前台进程组中的每一个进程发送该信号。 |
SIGPOLL/SIGIO | 这两个信号同义。这两个信号将会在高级IO章节内容中使用到,用于提示一个异步IO事件的发生,譬如应用程序打开的文件描述符发生了I/O事件时,内核会向应用程序发送SIGIO信号。 |
SIGSYS | 如果进程发起的系统调用有误,那么内核将发送该信号给对应的进程。 |
信号处理
signal()
函数
signal()
函数是Linux系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:
#include <signal.h>
typedef void (*sig_t)(int);
// signum: 此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
// handler: sig_t类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数。
sig_t signal(int signum, sig_t handler);
信号处理参数handler
既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN
或 SIG_DFL
:
-
SIG_IGN
表示此进程需要忽略该信号 -
SIG_DFL
则表示设置为系统默认操作
#include <signal.h>
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
/* Fake signal functions. */
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
sig_t
函数指针的 int
类型参数指的是,当前触发该函数的信号, 可将多个信号绑定到同一个信号处理函数上 ,此时就可通过此参数来判断当前触发的是哪个信号。
返回值 :此函数的返回值也是一个 sig_t
类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR
,并会设置 errno
。
sigaction()
函数
sigaction()
允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制,其函数原型如下所示:
#include <signal.h>
// signum:需要设置的信号,除了SIGKILL信号和SIGSTOP信号之外的任何信号。
// act:act参数是一个struct sigaction类型指针,该数据结构描述了信号的处理方式,稍后介绍该数据结构;如果参数act不为NULL,则表示需要为信号设置新的处理方式;如果参数act为NULL,则表示无需改变信号当前的处理方式。
// oldact:oldact参数也是一个struct sigaction类型指针。如果参数oldact不为NULL,则会将信号之前的处理方式等信息通过参数oldact返回出来;如果无意获取此类信息,那么可将该参数设置为NULL。
int sigaction (int sig, const struct sigaction *restrict act, struct sigaction *restrict oact)
struct sigaction
结构体如下所示:
struct sigaction {
union {
__sighandler_t _sa_handler;
void (*_sa_sigaction)(int, struct siginfo *, void *);
} _u;
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
};
sa_handler
: 指定信号处理函数 ,与signal()函数的handler参数相同。sa_sigaction
:也用于指定信号处理函数, 这是一个替代的信号处理函数,提供了更多的参数 ,可以通过该函数获取到更多信息,这些信号通过siginfo_t
参数获取,稍后介绍该数据结构;sa_handler
和sa_sigaction
是互斥的,不能同时设置 ,对于标准信号来说,使用sa_handler
就可以了,可通过SA_SIGINFO
标志进行选择。sa_mask
:参数sa_mask
定义了一组信号,当 进程在执行由sa_handler
所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再将这组信号从信号掩码字段中删除 。当进程在执行信号处理函数期间,可能又收到了同样的信号或其它信号,从而打断当前信号处理函数的执行,这就好点像中断嵌套;通常我们在执行信号处理函数期间不希望被另一个信号所打断,那么怎么做呢?那么就是通过信号掩码来实现, 如果进程接收到了信号掩码中的这些信号,那么这个信号将会被阻塞暂时不能得到处理,直到这些信号从进程的信号掩码中移除 。 在信号处理函数调用时,进程会自动将当前处理的信号添加到信号掩码字段中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞 。如果用户还需要在阻塞其它的信号,则可以通过设置参数sa_mask
来完成(此参数是sigset_t类型变量,关于该类型的介绍信息下文有详细介绍),信号掩码可以避免一些信号之间的竞争状态(也称为竞态)。sa_flags
:参数sa_flags
指定了一组标志, 这些标志用于控制信号的处理过程 ,可设置为如下这些标志(多个标志使用位或" | "组合):- SA_NOCLDSTOP
如果signum
为SIGCHLD
,则 子进程停止时 (即当它们接收到SIGSTOP
、SIGTSTP
、SIGTTIN
或SIGTTOU
中的一种时)或 恢复时 (即它们接收到SIGCONT
) 不会收到SIGCHLD
信号 。 - SA_NOCLDWAIT
如果signum
是SIGCHLD
,则在 子进程终止时不要将其转变为僵尸进程 。 - SA_NODEFER
不要阻塞从某个信号自身的信号处理函数中接收此信号 。也就是说当进程此时正在执行某个信号的处理函数,默认情况下,进程会自动将该信号添加到进程的信号掩码字段中,从而在执行信号处理函数期间阻塞该信号,默认情况下,我们期望进程在处理一个信号时阻塞同种信号,否则引起一些竞态条件;如果设置了SA_NODEFER
标志,则表示不对它进行阻塞。 - SA_RESETHAND
执行完信号处理函数之后,将信号的处理方式设置为系统默认操作 。 - SA_RESTART
被信号中断的系统调用,在信号处理完成之后将自动重新发起 。 - SA_SIGINFO
如果设置了该标志,则 表示使用sa_sigaction
作为信号处理函数、而不是sa_handler
,关于sa_sigaction
信号处理函数的参数信息。 sa_restorer
:该成员已过时,不要再使用了。
一般而言,将信号处理函数设计越简单越好 ,这就好比中断处理函数,越快越好,不要在处理函数中做大量消耗CPU时间的事情,这一个重要的原因在于,设计的越简单这将降低引发信号竞争条件的风险。
向进程发送信号
与 kill
命令相类似,Linux系统提供了 kill()
系统调用,一个进程可通过 kill()
向另一个进程发送信号;除了 kill()
系统调用之外,Linux系统还提供了系统调用 killpg()
以及库函数 raise()
,也可用于实现发送信号的功能。
kill()
函数
kill()
系统调用可 将信号发送给指定的进程或进程组中的每一个进程 ,其函数原型如下所示:
#include <sys/types.h>
#include <signal.h>
// pid:参数pid为正数的情况下,用于指定接收此信号的进程pid;除此之外,参数pid也可设置为0或-1以及小于-1等不同值,稍后给说明。
// sig:参数sig指定需要发送的信号,也可设置为0,如果参数sig设置为0则表示不发送信号,但仍执行错误检查,这通常可用于检查参数pid指定的进程是否存在。
int kill(pid_t pid, int sig);
返回值:成功返回0;失败将返回-1,并设置 errno
。
参数pid不同取值含义:
- 如果pid为正,则信号sig将发送到
pid
指定的进程 。 - 如果pid等于0,则将sig发送到当前进程的 进程组中的每个进程 。
- 如果pid等于-1,则将sig发送到当前进程 有权发送信号的每个进程 ,但进程1(init)除外。
- 如果pid小于-1,则将sig发送到ID为
-pid
的进程组中的每个进程 。
进程中将信号发送给另一个进程是需要权限的,并不是可以随便给任何一个进程发送信号,超级用户root进程可以将信号发送给任何进程,但对于非超级用户(普通用户)进程来说,其基本规则是 发送者进程的实际用户ID或有效用户ID必须等于接收者进程的实际用户ID或有效用户ID 。
从上面介绍可知,当 sig
为0时 ,仍可进行正常执行的错误检查,但不会发送信号,这通常可 用于确定一个特定的进程是否存在 。如果向一个 不存在的进程 发送信号, kill()
将会返回 -1
, errno
将被设置为 ESRCH
,表示进程不存在。
raise()
函数
有时 进程需要向自身发送信号 , raise()
函数可用于实现这一要求, raise()
函数原型如下所示(此函数为C库函数):
#include <signal.h>
// sig:需要发送的信号
int raise(int sig); // 等价于 kill(getpid(), sig);
alarm()和pause()函数
alarm()
定时函数
使用 alarm()
函数可以设置一个定时器(闹钟) ,当定时器定时时间到时,内核会向进程发送 SIGALRM
信号,其函数原型如下所示:
#include <unistd.h>
// seconds:设置定时时间,以秒为单位;如果参数seconds等于0,则表示取消之前设置的alarm闹钟。
unsigned int alarm(unsigned int seconds);
返回值:如果在调用 alarm()
时,之前已经为该进程设置了闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()
函数调用的返回值,之前设置的闹钟则被新的替代;否则返回0。
每个进程只能设置一个 alarm
闹钟 ;虽然SIGALRM信号的系统默认操作是终止进程,但是如果程序当中设置了 alarm
闹钟,但大多数使用闹钟的进程都会捕获此信号。
需要注意的是 alarm
闹钟并不能循环触发,只能触发一次 ,若想要实现循环触发,可以在 SIGALRM
信号处理函数中再次调用 alarm()
函数设置定时器。
pause()
暂停进程函数
pause()
系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止 , 只有执行了信号处理函数并从其返回时, pause()
才返回 ,在这种情况下, pause()
返回-1,并且将 errno
设置为 EINTR
。其函数原型如下所示:
#include <unistd.h>
int pause(void);
通过 alarm()
和 pause()
模拟sleep功能
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig) {
puts("Alarm timeout");
}
int main(int argc, char *argv[]) {
struct sigaction sig = {0};
int second;
/* 检验传参个数 */
if (2 > argc)
exit(-1);
/* 为SIGALRM信号绑定处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGALRM, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
/* 启动alarm定时器 */
second = atoi(argv[1]);
printf("定时时长: %d秒
", second);
alarm(second);
/* 进入休眠状态 */
pause();
puts("休眠结束");
exit(0);
}
运行测试:
信号集
通常我们需要有 一个能表示多个信号(一组信号)的数据类型——信号集(signal set) ,很多系统调用都使用到了信号集这种数据类型来作为参数传递,譬如 sigaction()
函数、 sigprocmask()
函数、 sigpending()
函数等。本小节向大家介绍信号集这个数据类型。
信号集其实就是 sigset_t
类型数据结构:
typedef struct {
unsigned long int __val[_SIGSET_NWORDS];
} sigset_t;
使用这个结构体可以表示一组信号,将多个信号添加到该数据结构中,当然Linux系统了用于操作 sigset_t
信号集的API,譬如 sigemptyset()
、 sigfillset()
、 sigaddset()
、 sigdelset()
、 sigismember()
,接下来向大家介绍。
sigemptyset()
初始化空信号集
sigemptyset()
初始化信号集,使其不包含任何信号:
#include <signal.h>
// set:指向需要进行初始化的信号集变量
int sigemptyset(sigset_t *set);
返回值:成功返回0;失败将返回-1,并设置errno。
使用示例:
sigset_t sig_set;
sigemptyset(&sig_set);
sigfillset()
初始化满信号集
sigfillset()
函数初始化信号集,使其包含所有信号(包括所有实时信号):
#include <signal.h>
// set:指向需要进行初始化的信号集变量
int sigfillset(sigset_t *set);
返回值:成功返回0;失败将返回-1,并设置errno。
使用示例:
sigset_t sig_set;
sigfillset(&sig_set);
sigaddset()
向信号集添加信号
sigaddset()
向信号集添加信号:
#include <signal.h>
// set:指向需要进行初始化的信号集变量
// signum:需要添加的信号
int sigaddset(sigset_t *set, int signum);
返回值:成功返回0;失败将返回-1,并设置errno。
使用示例:
sigset_t sig_set;
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
sigdelset()
从信号集移除信号
sigaddset()
从信号集移除信号:
#include <signal.h>
// set:指向需要进行初始化的信号集变量
// signum:需要移除的信号
int sigdelset(sigset_t *set, int signum);
返回值:成功返回0;失败将返回-1,并设置errno。
使用示例:
sigset_t sig_set;
sigfillset(&sig_set);
sigdelset(&sig_set, SIGINT);
sigismember()
测试信号是否属于信号集
使用 sigismember()
函数可以测试某一个信号是否在指定的信号集中,函数原型如下所示:
#include <signal.h>
// set:指向需要进行初始化的信号集变量
// signum:需要进行测试的信号
int sigismember(sigset_t *set, int signum);
返回值:如果信号 signum
在信号集 set
中,则返回1,否则返回0;失败则返回-1,并设置errno。
使用示例:
sigset_t sig_set;
......
if (1 == sigismember(&sig_set, SIGINT))
puts("信号集中包含SIGINT信号");
else if (!sigismember(&sig_set, SIGINT))
puts("信号集中不包含SIGINT信号");
信号描述信息
sys_siglist
信号描述信息数组
在Linux下,每个信号都有一串与之相对应的字符串描述信息,用于对该信号进行相应的描述。这些字符串位于 sys_siglist
数组中, sys_siglist
数组是一个 char *
类型的数组,数组中的每一个元素存放的是一个字符串指针,指向一个信号描述信息。譬如, 可以使用 sys_siglist[SIGINT]
来获取对 SIGINT
信号的描述 。我们编写一个简单地程序进行测试:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("SIGINT描述信息: %s
", sys_siglist[SIGINT]);
printf("SIGQUIT描述信息: %s
", sys_siglist[SIGQUIT]);
printf("SIGBUS描述信息: %s
", sys_siglist[SIGBUS]);
exit(0);
}
运行结果:
strsignal()
输出描述信息函数
除了直接使用 sys_siglist
数组获取描述信息之外,还可以使用 strsignal()
函数。较之于直接引用 sys_siglist
数组,更推荐使用 strsignal()
函数,其函数原型如下所示:
#include <string.h>
// sig:指定的信号
// s: 用户添加的输出信息
char *strsignal(int sig);
调用 strsignal()
函数将会获取到参数 sig
指定的信号对应的描述信息,返回该描述信息字符串的指针 ;函数会对参数 sig
进行检查,若传入的 sig
无效,则会返回"Unknown signal"信息。
测试如下:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
printf("SIGINT描述信息: %s
", strsignal(SIGINT));
printf("SIGQUIT描述信息: %s
", strsignal(SIGQUIT));
printf("SIGBUS描述信息: %s
", strsignal(SIGBUS));
printf("编号为1000的描述信息: %s
", strsignal(1000));
exit(0);
}
测试结果:
psignal()
输出描述信息到标准错误函数
psignal()
可以在 标准错误(stderr)
上输出信号描述信息 ,其函数原型如下所示:
#include <signal.h>
// sig:指定的信号
void psignal(int sig, const char *s);
所以整个输出信息由 字符串s
+ 冒号
+ 空格
+ 描述信号编号sig的字符串
+ 尾随的换行符
组成。
测试如下:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
psignal(SIGINT, "SIGINT信号描述信息");
psignal(SIGQUIT, "SIGQUIT信号描述信息");
psignal(SIGBUS, "SIGBUS信号描述信息");
exit(0);
}
运行结果:
信号掩码(阻塞信号传递)
内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。 当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理 ,那么内核会将其阻塞, 直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理 。
向信号掩码中添加一个信号,通常有如下几种方式:
- 当应用程序调用
signal()
或sigaction()
函数为某一个信号设置处理方式时,进程会自动将该信号添加到信号掩码中,这样保证了在处理一个给定的信号时,如果此信号再次发生,那么它将会被阻塞;当然对于sigaction()
而言,是否会如此,需要根据sigaction()
函数是否设置了SA_NODEFER
标志而定;当信号处理函数结束返回后,会自动将该信号从信号掩码中移除。 - 使用
sigaction()
函数为信号设置处理方式时,可以额外指定一组信号,当调用信号处理函数时将该组信号自动添加到信号掩码中,当信号处理函数结束返回后,再将这组信号从信号掩码中移除;通过sa_mask
参数进行设置,参考前面所述。 - 除了以上两种方式之外,还可以使用
sigprocmask()
系统调用,随时可以显式地向信号掩码中添加/移除信号。
sigprocmask()
添加/移除/设置信号集至信号掩码
#include <signal.h>
// how:参数how指定了调用函数时的一些行为。
// set:将参数set指向的信号集内的所有信号添加到信号掩码中或者从信号掩码中移除;如果参数set为NULL,则表示无需对当前信号掩码作出改动。
// oldset:如果参数oldset不为NULL,在向信号掩码中添加新的信号之前,获取到进程当前的信号掩码,存放在oldset所指定的信号集中;如果为NULL则表示不获取当前的信号掩码。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
返回值:成功返回0;失败将返回-1,并设置errno。
参数 how
可以设置为以下宏:
SIG_BLOCK
:将参数 set所指向的信号集内的所有信号添加到进程的信号掩码中 。换言之,将信号掩码设置为当前值与set的并集。SIG_UNBLOCK
:将参数 set指向的信号集内的所有信号从进程信号掩码中移除 。SIG_SETMASK
:将参数 set指向的信号集直接设置为进程信号掩码 。
下面编写一个简单的测试代码,验证信号掩码的作用:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("执行信号处理函数...
");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t sig_set;
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 信号集初始化 */
sigemptyset(&sig_set);
sigaddset(&sig_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &sig_set, NULL))
exit(-1);
/* 向自己发送信号 */
raise(SIGINT);
/* 休眠2秒 */
sleep(2);
printf("休眠结束
");
/* 从信号掩码中移除添加的信号 */
if (-1 == sigprocmask(SIG_UNBLOCK, &sig_set, NULL))
exit(-1);
exit(0);
}
上述代码中,我们为 SIGINT
信号注册了一个处理函数 sig_handler
,当进程接收到该信号之后就会执行它;然后调用 sigprocmask
函数将 SIGINT
信号添加到信号掩码中,然后再调用 raise(SIGINT)
向自己发送一个 SIGINT
信号,如果信号掩码没有生效、也就意味着 SIGINT
信号不会被阻塞,那么调用 raise(SIGINT)
之后应该就会立马执行 sig_handler
函数,从而打印出"执行信号处理函数…"字符串信息;如果设置的信号掩码生效了,则并不会立马执行信号处理函数,而是在2秒后才执行,因为程序中使用 sleep(2)
休眠了2秒钟之后,才将 SIGINT
信号从信号掩码中移除,故而进程才会处理该信号,在移除之前接收到该信号会将其阻塞。流程图如下:
运行的结果如下,证明信号掩码生效:
阻塞等待
sigsuspend()
阻塞等待信号
前面已经说明,更改进程的信号掩码可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的关键代码段。如果希望 对一个信号解除阻塞后,然后调用 pause()
以等待之前被阻塞的信号的传递 ,这将如何?譬如有如下代码段:
sigset_t new_set, old_set;
/* 信号集初始化 */
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_set, &old_set))
exit(-1);
/* 受保护的关键代码段 */
......
/**********************/
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_set, NULL))
exit(-1);
/* 等待信号唤醒 */
pause();
执行受保护的关键代码时不希望被 SIGINT
信号打断,所以在执行关键代码之前将 SIGINT
信号添加到进程的信号掩码中,执行完毕之后再恢复之前的信号掩码。最后调用了 pause()
阻塞等待被信号唤醒,如果此时发生了信号则会被唤醒、从 pause
返回继续执行;
考虑这样一种情况:如果信号的传递恰好发生在第二次调用 sigprocmask()
之后、 pause()
之前,就会产生一个问题:信号传递过来会导致执行信号的处理函数,而从处理函数返回后又回到主程序继续执行,从而进入到 pause()
被阻塞,直到下一次信号发生时才会被唤醒,这有违代码的本意。
虽然信号传递发生在这个时间段的可能性并不大,但并不是完全没有可能,这必然是一个缺陷,要避免这个问题, 需要将恢复信号掩码和 pause()
挂起进程这两个动作封装成一个原子操作 ,这正是 sigsuspend()
系统调用的目的所在, sigsuspend()
函数原型如下所示:
#include <signal.h>
// mask:参数mask指向一个信号集
int sigsuspend(const sigset_t *mask);
返回值: sigsuspend()
始终返回-1,并设置 errno
来指示错误(通常为 EINTR
),表示被信号所中断,如果调用失败,将 errno
设置为 EFAULT
。
sigsuspend()
函数会用参数 mask
所指向的信号集来替换进程的信号掩码, 也就是将进程的信号掩码设置为参数 mask
所指向的信号集,然后挂起进程,直到捕获到信号被唤醒 (如果捕获的信号是 mask
信号集中的成员,将不会唤醒、继续挂起)、并从信号处理函数返回,一旦从信号处理函数返回, sigsuspend()
会将进程的信号掩码恢复成调用前的值。
调用 sigsuspend()
函数相当于以不可中断(原子操作)的方式执行以下操作:
sigprocmask(SIG_SETMASK, &mask, &old_mask);
pause();
sigprocmask(SIG_SETMASK, &old_mask, NULL);
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("执行信号处理函数...
");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t new_mask, old_mask, wait_mask;
/* 信号集初始化 */
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigemptyset(&wait_mask);
/* 注册信号处理函数 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGINT, &sig, NULL))
exit(-1);
/* 向信号掩码中添加信号 */
if (-1 == sigprocmask(SIG_BLOCK, &new_mask, &old_mask))
exit(-1);
/* 执行保护代码段 */
puts("执行保护代码段");
/******************/
/* 挂起、等待信号唤醒 */
if (-1 != sigsuspend(&wait_mask))
exit(-1);
/* 恢复信号掩码 */
if (-1 == sigprocmask(SIG_SETMASK, &old_mask, NULL))
exit(-1);
exit(0);
}
在上述代码中,我们希望执行受保护代码段时不被 SIGINT
中断信号打断,所以在执行保护代码段之前将 SIGINT
信号添加到进程的信号掩码中, 执行完受保护的代码段之后,调用 sigsuspend() 挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态 。
实时信号
如果进程当前正在执行信号处理函数,在处理信号期间接收到了新的信号,如果该信号是信号掩码中的成员,那么内核会将其阻塞,将该信号添加到进程的等待信号集(等待被处理,处于等待状态的信号)中, 为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()
函数获取 。
sigpending()
读取等待信号集
函数原型如下:
#include <signal.h>
// set:处于等待状态的信号会存放在参数set所指向的信号集中
int sigpending(sigset_t *set);
返回值:成功返回0;失败将返回-1,并设置errno。
使用示例,判断 SIGINT
信号当前是否处于等待状态:
/* 定义信号集 */
sigset_t sig_set;
/* 将信号集初始化为空 */
sigemptyset(&sig_set);
/* 获取当前处于等待状态的信号 */
sigpending(&sig_set);
/* 判断SIGINT信号是否处于等待状态 */
if (1 == sigismember(&sig_set, SIGINT))
puts("SIGINT信号处于等待状态");
else if (!sigismember(&sig_set, SIGINT))
puts("SIGINT信号未处于等待状态");
sigqueue()
发送实时信号
等待信号集只是一个掩码,仅表明一个信号是否发生,而不能表示其发生的次数。换言之, 如果一个同一个信号在阻塞状态下产生了多次,那么会将该信号记录在等待信号集中,并在之后仅传递一次(仅当做发生了一次),这是标准信号的缺点之一 。
可以使用 sigqueue()
函数发送实时信号,其函数原型如下所示:
#include <signal.h>
// pid:指定接收信号的进程对应的pid,将信号发送给该进程
// sig:指定需要发送的信号。与kill()函数一样,也可将参数sig设置为0,用于检查参数pid所指定的进程是否存在
// value:参数value指定了信号的伴随数据,union sigval数据类型
int sigqueue(pid_t pid, int sig, const union sigval value);
返回值:成功将返回0;失败将返回-1,并设置 errno
。
union sigval
数据类型(共用体)如下所示:
typedef union sigval
{
int sival_int;
void *sival_ptr;
} sigval_t;
携带的伴随数据,既可以指定一个整形的数据,也可以指定一个指针 。
使用举例:
发送进程使用 sigqueue()
系统调用向另一个进程发送实时信号:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
union sigval sig_val;
int pid;
int sig;
/* 判断传参个数 */
if (3 > argc)
exit(-1);
/* 获取用户传递的参数 */
pid = atoi(argv[1]);
sig = atoi(argv[2]);
printf("pid: %d
signal: %d
", pid, sig);
/* 发送信号 */
sig_val.sival_int = 10; //伴随数据
if (-1 == sigqueue(pid, sig, sig_val)) {
perror("sigqueue error");
exit(-1);
}
puts("信号发送成功!");
exit(0);
}
接收进程使用 sigaction()
函数为信号绑定处理函数:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig, siginfo_t *info, void *context)
{
sigval_t sig_val = info->si_value;
printf("接收到实时信号: %d
", sig);
printf("伴随数据为: %d
", sig_val.sival_int);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
int num;
/* 判断传参个数 */
if (2 > argc)
exit(-1);
/* 获取用户传递的参数 */
num = atoi(argv[1]);
/* 为实时信号绑定处理函数 */
sig.sa_sigaction = sig_handler;
sig.sa_flags = SA_SIGINFO; // 选择 sa_sigaction 作为信号处理函数,而不是 sa_handler
if (-1 == sigaction(num, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
/* 死循环 */
for ( ; ; )
sleep(1);
exit(0);
}
异常退出
abort()
终止进程
不同于 exit()
、 _exit()
或 _Exit()
这些函数用于正常退出应用程序, 对于异常退出程序,则一般使用 abort()
库函数 。使用 abort()
终止进程运行,会生成核心转储文件,可用于判断程序调用 abort()
时的程序状态。
abort()
函数原型如下所示:
#include <stdlib.h>
void abort(void);
函数 abort()
通常产生 SIGABRT
信号来终止调用该函数的进程, SIGABRT
信号的系统默认操作是终止进程运行、并生成核心转储文件。
使用实例如下:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
static void sig_handler(int sig)
{
printf("接收到信号: %d
", sig);
}
int main(int argc, char *argv[])
{
struct sigaction sig = {0};
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGABRT, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
sleep(2);
abort(); // 调用abort
for ( ; ; )
sleep(1);
exit(0);
}
运行结果:
从打印信息可知,即使在我们的程序当中捕获了 SIGABRT
信号,但是程序依然会无情的终止, 无论阻塞或忽略 SIGABRT
信号, abort()
调用均不收到影响,总会成功终止进程 。
参考文献
1:【正点原子】STM32MP1嵌入式Linux C应用编程指南V1.4 - 第八章
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023.05 by Mr.Idleman. All rights reserved.