【linux】信号的理论概述和实操

东洛的克莱斯韦克 2024-07-19 08:37:01 阅读 65

目录

 

理论篇

信号概述

信号的分类

信号机制

理解硬件中断

异步

信号对应的三种动作

信号产生的条件

终端按键

系统调用

软件条件

硬件异常

除0错误

野指针

OS对于错误的态度

信号在进程中的内核数据结构

信号的处理

CPU的内核态和用户态概述

进程处理信号的时机

​编辑

在递达时该信号被屏蔽

实操篇

sigset_t

信号集操作函数

sigprocmask

参数

返回值

sigpending

参数

返回值

sigaction

参数

返回值

struct sigaction 结构

闹钟

参数

返回值

注意事项

signal 


个人主页东洛的克莱斯韦克

理论篇

信号概述

在Linux系统中,信号是一种软件中断,用于通知进程发生了某个事件

信号是由操作系统发送给进程的,用于通知进程某个条件已经发生或者需要执行某个动作,进程处理信号的默认动作可用如下命令查看

<code>man 7 signal

信号提供了一种进程间通信的机制,虽然它主要是用来通知异常或中断情况,但也可以被用来实现进程间的同步或控制

信号的分类

信号分为普通信号  和  实时信号

用如下指令查看信号

kill -l

在Linux系统中1号信号到31号信号为普通信号,34号到64号信号为实时信号。每一种信号都有自己的宏定义。

实时信号一般用于特定的系统中,如嵌入式系统,能够在特定的硬件上运行并对外部事件做出迅速响应的系统。一个很贴近生活的例子是车载系统,如果用户层踩了刹车,车载系统即使再忙也要马上做出响应。实时信号会打破进程占用CPU资源的公平性。

而普通信号不会破坏进程占用CPU的资源的公平性,本文重点探讨普通信号。

信号机制

信号的机制属于一种软件中断,它模拟的是硬件中断

理解硬件中断

硬件中断是指当硬件设备(如网卡、硬盘、键盘等)有数据或事件需要处理时,会自动向CPU发送一个中断请求(IRQ),CPU在收到中断请求后,会暂停当前正在执行的任务,转而执行处理该中断请求的程序,这一过程称为硬件中断处理。

也就是说OS不会耗费资源去检查外设的数据情况,而是检测CPU是否收到中断请求。

以键盘为例,OS不可能知道用户什么时候敲键盘,OS也不会检测键盘是否有数据,只有用户敲键盘了,键盘会给CPU发送中断请求,OS检查到CPU的中断请求,才会把数据从键盘文件刷到内核缓冲区。

硬件中断是硬件行为,信号是在软件层模拟硬件中断。操作系统给进程发送相应的信号,进程收到信号完成某些任务。

异步

与硬件中断类似,进程不知道操作系统什么时候给自己发送信号。由于信号可以在进程执行的任何时间点到来,且进程通常不会预先知道何时会收到信号,因此信号的接收是异步的。

信号对应的三种动作

默认动作    忽略    自定义

默认动作:每一个信号对应一个动作。就比如红绿灯的 红灯 ,绿灯, 黄灯 分别对应停止, 前进, 等一等。

信号编号 信号名称 默认动作 备注
1 SIGHUP 终止进程 当用户退出shell时,由该shell启动的所有进程将接收此信号
2 SIGINT 终止进程 相当于Ctrl+C,通常用于终止前台进程
3 SIGQUIT 终止进程并生成core文件 相当于Ctrl+\,除了终止进程外,还会生成core文件用于调试
4 SIGILL 终止进程并生成core文件 非法指令,例如执行了未知或不支持的指令
5 SIGTRAP 终止进程并生成core文件 跟踪/断点陷阱,通常与调试器相关
6 SIGABRT 终止进程并生成core文件 调用abort()函数产生的信号,用于异常终止进程
7 SIGBUS 终止进程并生成core文件 总线错误,非法访问内存地址(如对齐错误)
8 SIGFPE 终止进程并生成core文件 浮点异常,如除以0、溢出等
9 SIGKILL 终止进程 不能被捕捉、阻塞或忽略,无条件终止进程
10 SIGUSR1 终止进程 用户自定义信号1,程序员可以在程序中定义并使用该信号
11 SIGSEGV 终止进程并生成core文件 无效的内存引用,如解引用空指针
12 SIGUSR2 终止进程 用户自定义信号2,程序员可以在程序中定义并使用该信号
13 SIGPIPE 终止进程 写入没有读端的管道,常见于socket编程中
14 SIGALRM 终止进程 由alarm()函数设置的定时器超时产生
15 SIGTERM 终止进程 请求程序终止,与SIGKILL不同,SIGTERM可以被捕捉、阻塞或忽略
16-17 SIGSTKFLT 终止进程 协处理器栈错误(已废弃,在某些系统上可能不存在)
SIGCHLD 忽略 子进程停止或终止时,通知父进程,但父进程通常选择忽略此信号
18 SIGCONT 继续执行被停止的进程 使一个停止的进程继续执行(如果它被停止的话)
19 SIGSTOP 停止进程 不能被捕捉、阻塞或忽略,无条件停止进程
20 SIGTSTP 停止进程 相当于Ctrl+Z,用于暂停前台进程
21-22 SIGTTIN 停止进程 后台进程尝试从控制终端读取时产生(如后台进程尝试读取输入)
SIGTTOU 停止进程 后台进程尝试向控制终端写入时产生(如后台进程尝试输出)
23-31 SIGURG, ... 依赖于具体实现 这些信号的默认动作可能因系统而异,且一些信号可能不在所有系统上都有定义

忽略:进程屏蔽了该信号

自定义:进程捕捉了该信号

9号信号和19号信号不能被捕捉也不能被屏蔽

9号信号是用来杀进程的且一定能杀掉,用来处理有问题的进程,恶意攻击系统的进程等等

19号信号用来停掉进程而且一定能停掉,如果进程正在做一些很重要的事情但确实出了一些问题,可以用19号信号停掉该进程。

信号产生的条件

终端按键

常用的组合键如下

Ctrl+C信号:SIGINT(Interrupt,中断信号)

作用:当用户按下Ctrl+C时,终端驱动程序会接收到这个输入,并调用信号系统向当前的前台进程发送SIGINT信号。默认动作是终止进程。

补充:前台进程指的是你收到键盘输入的进程,linux中只允许有一个前台进程,可以有多个后台进程。

Ctrl+Z信号:SIGTSTP(Stop typed at terminal,终端停止信号)

作用:当用户按下Ctrl+Z时,终端驱动程序会向当前的前台进程发送SIGTSTP信号。这个信号会停止(挂起)进程的执行,但进程并没有被终止。用户可以使用<code>fg命令将进程恢复到前台继续执行,或者使用bg命令将其放到后台执行。

Ctrl+\信号:SIGQUIT(Quit,退出信号)

作用:在某些系统中,当用户按下Ctrl+\时,会向当前的前台进程发送SIGQUIT信号。这个信号的默认处理方式是终止进程,并且会生成一个核心转储文件(core dump),该文件包含了进程终止时的内存、寄存器状态等信息,有助于开发者进行调试。

补充:服务器默认是关掉核心转储的,在线上生产环境中,会有大量收到SIGQUIT信号的进程,如果核心转储没有关掉,会有大量的核心转储文件挤压磁盘空间,使服务器无法正常运行。

系统调用

#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);

这两个函数都是成功返回

0

,

错误返回

-1

kill命令

是调用

kill函数

实现的。

kill

函数可以给一个指定的进程发送指定的信号。

raise

函数可以给当前进程发送指定 的信号(

自己给自己发信号

)

#include <stdlib.h>

void abort(void);

abort

函数使当前进程接收到信号而异常终止。

就像

exit

函数一样

,

abort

函数总是会成功的

,

所以没有返回值。

软件条件

【linux】进程间通信

对于管道而言,如果管道的读端全部被关闭,写端就没有意义了。操作系统会给写端进程发送SIGPIPE信号,其信号编号为13。

SIGPIPE信号是Linux中定义的一个标准信号,用于指示一个写操作尝试写入一个没有读端的管道。SIGPIPE信号的默认动作是终止进程,但可以通过设置信号处理函数来捕获或忽略该信号。大多数服务器程序为了避免因SIGPIPE信号而异常终止,会选择忽略该信号。

上述例子就属于进程触发了一些软件条件,从而让操作系统发送信号来通知进程。

硬件异常

硬件异常是指由硬件设备引起的程序执行过程中的错误或异常情况。下面分析除0错误和野指针问题的硬件异常

除0错误

除0错误是很常见的硬件异常,它会使CPU的运算溢出。当进程执行了除以0的指令时,CPU的运算单元会产生异常,并通知内核。内核会解析这个异常为SIGFPE信号,并发送给当前进程。

野指针

当进程访问了非法或未分配的内存地址时,MMU会产生异常,并通知内核。内核会解析这个异常为SIGSEGV信号,并发送给当前进程。

本质上讲,野指针问题都是虚拟地址在页表中转化物理地址失败。【Linux】进程地址空间

OS对于错误的态度

对于一些异常或错误系统往往不会发送9或19号信号,上文中4个条件触发的信号都是可以被捕捉的和屏蔽的。

也就是说即使触发了一些异常或错误,系统做的也只是通知,而不是强硬的杀掉或停掉进程。比如上述的除 0 错误,进程只要捕捉了SIGFPE信号,那么该进程只要被调度到CPU上就会触发除 0 异常,OS就会继续给该进程发送SIGFPE信号。

OS为什么要用信号的方式通知,而不是直接杀进程?

可以搭个场景来理解下,进程A正在向磁盘写入10万条转账记录,但进程A触发了某些异常,此时OS的做法是通知进程A,还是杀掉或停掉进程A呢。如果做的比较极端导致这10万条转账记录丢失了,是进程的问题,还是OS的问题呢。

那么我们也就不难理解,OS为什么要给进程足够多的宽容度。因为应用层可能正在做很重要的事情,OS太极端对应用层的体验会大打折扣。

所以OS会以信号的方式通知进程,而对异常的处理就看上层是怎么设计的,出了问题也和OS没关系。

信号在进程中的内核数据结构

屏蔽:进程不在接收次信号(block)

递达:实际执行信号的处理动作称为信号递达(Delivery)

未决 :信号从产生到递达之间的状态,称为信号未决(Pending)。

每个进程的内核数据都有三张表。

block是位图0表示该信号未被屏蔽,1表示该信号被屏蔽了。

Pending也是位图0表示没有收到该信号,1表示收到了该信号但没有执行。

bandler是一张函数指针数组表,如果该函数要被递达了就会执行对应的方法,可以这些默认方法,也可以执行自定义方法。

需要注意的是Pending位图中没有计数的概念。也就是说,在一段时间内一种信号如果被发送了多次,那么也只会被递达一次。

信号的处理

CPU的内核态和用户态概述

CPU的内核态和用户态是操作系统中的两种重要状态,它们分别代表了CPU在执行不同类型程序时的权限和访问范围。

内核态是CPU的一种特权状态,也称为系统态或管态。在此状态下,CPU可以执行任何机器指令,包括访问和修改所有硬件设备的寄存器,执行内存管理等特权操作。

用户态是CPU的一种非特权状态,也称为目态。在此状态下,CPU只能执行非特权指令,不能直接访问硬件设备或执行特权操作。

内核态(Kernel Mode) 用户态(User Mode)
定义 CPU的特权状态,可执行任何机器指令 CPU的非特权状态,只能执行非特权指令
权限 最高特权级别(通常为Ring 0) 较低特权级别(通常为Ring 3)
运行程序 操作系统内核代码 用户编写的应用程序和操作系统提供的用户接口程序
访问范围 可访问和修改系统的任何部分 只能访问和操作分配给它的资源
安全性 必须确保安全性和稳定性 权限较低,即使出现错误也不会对系统造成严重影响
切换方式 通过系统调用、异常和中断等方式实现 通过系统调用等方式请求操作系统服务时切换

cpu陷入内核后怎么找到内核的数据和代码呢?

进程地址空间中0到3G是用户空间,3到4G是内核空间,所以进程的内核地址空间都会映射到同一块物理内存(这块物理内中是内核数据),只要CPU陷入内核就有权力访问地址空间中的内核空间。

地址空间示意图

需要注意的是,在执行用户代码是,CPU必须是用户态,因为用户代码中可能会有窃取数据,恶意攻击等非法操作。

进程处理信号的时机

红线以上是用户态,红线以下是内核态。蓝点是CPU切换用户态和内核态的时机,绿点是处理信号的时机。

在递达时该信号被屏蔽

一个信号在被处理时,该信号会被屏蔽。这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

实操篇

sigset_t

sigset_t 是一种数据类型,它涵括了上文的三张表。用于表示信号集,即一个或多个信号的集合。

<code>sigset_t 是处理信号时的一个重要数据类型,它允许程序灵活地指定和操作信号集合。通过与相关的函数结合使用,sigset_t 提供了强大的信号处理能力,使得程序能够更精确地控制信号的行为。

信号集操作函数

#include <signal.h>

int sigemptyset(sigset_t *set);

此函数用于初始化信号集,将其设置为空集,即不包含任何信号。如果操作成功,则返回0;如果发生错误,则返回-1。

int sigfillset(sigset_t *set);

此函数用于将信号集初始化为包含所有信号。这意味着,在调用此函数后,信号集将包含所有可能由系统发送的信号。如果操作成功,则返回0;如果发生错误,则返回-1。

int sigaddset(sigset_t *set, int signo);

此函数用于向信号集中添加一个信号。signo参数指定了要添加的信号编号。如果操作成功,则返回0;如果发生错误(例如,signo不是有效信号),则返回-1。

int sigdelset(sigset_t *set, int signo);

sigaddset相反,sigdelset函数用于从信号集中删除一个信号。signo参数指定了要删除的信号编号。如果操作成功,则返回0;如果发生错误(例如,signo不是信号集中的成员),则返回-1。

int sigismember(const sigset_t *set, int signo);

此函数用于检查指定的信号编号(signo)是否属于信号集(set)。如果signo是信号集的成员,则返回1;如果不是,则返回0;如果发生错误(例如,set不是有效的信号集指针),则返回-1。

这些函数在信号处理中非常有用,特别是当需要阻塞或捕捉特定信号时。例如,可以在进行关键操作之前使用sigprocmask函数和sigemptyset/sigaddset来阻塞某些信号,以防止它们干扰这些操作。操作完成后,可以解除对这些信号的阻塞。

sigprocmask

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值:若成功则为0,若出错则为-1

调用函数

sigprocmask

可以读取或更改进程的信号屏蔽字

(

阻塞信号集

)

参数

how:指定如何更改信号屏蔽。这个参数可以是以下三个常量之一:

SIG_BLOCK:新的信号屏蔽是当前屏蔽与set指向的集合的并集。即,set中指定的信号将被添加到当前进程的屏蔽中。SIG_UNBLOCK:新的信号屏蔽是当前屏蔽与set指向的集合的差集。即,set中指定的信号将从当前进程的屏蔽中移除。SIG_SETMASK:新的信号屏蔽直接设置为set指向的集合。即,忽略当前进程的屏蔽,并使用set中指定的新屏蔽。

set:指向sigset_t类型的指针,表示要更改的信号集。如果howSIG_SETMASK,则这个集合将直接成为新的信号屏蔽。如果howSIG_BLOCKSIG_UNBLOCK,则这个集合中的信号将被相应地添加到或从当前屏蔽中移除。

oset:如果此参数非空,则指向的sigset_t变量将被设置为函数调用前的信号屏蔽。这允许调用者保存旧的屏蔽并在以后恢复它。

返回值

成功时,sigprocmask返回0。失败时,返回-1,并设置errno以指示错误。

#include <stdio.h>

#include <signal.h>

#include <string.h>

#include <unistd.h>

void handler(int sig) {

printf("Caught signal %d\n", sig);

}

int main() {

sigset_t set, oldset;

// 设置SIGINT的处理程序

signal(SIGINT, handler);

// 初始化信号集

sigemptyset(&set);

sigaddset(&set, SIGINT);

// 阻塞SIGINT

if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {

perror("sigprocmask");

return 1;

}

// 现在SIGINT被阻塞了,发送SIGINT信号不会调用handler

printf("SIGINT is blocked. Trying to send SIGINT...\n");

raise(SIGINT); // 尝试发送SIGINT信号,但不会被处理

// 恢复旧的信号屏蔽

if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {

perror("sigprocmask");

return 1;

}

// 现在SIGINT不再被阻塞,发送SIGINT信号将调用handler

printf("SIGINT is unblocked. Trying to send SIGINT...\n");

raise(SIGINT); // 发送SIGINT信号,将调用handler

return 0;

}

sigpending

sigpending 函数是 POSIX 系统中用于查询当前进程阻塞(pending)的信号集合的接口。这些信号已经发送给进程,但由于某些原因(如信号被阻塞)而尚未被处理。通过调用 sigpending 函数,程序可以获取这些待处理的信号,并据此做出相应的处理。

#include <signal.h>

int sigpending(sigset_t *set);

参数

set:指向 sigset_t 类型的指针,用于存储查询到的待处理信号集合。

返回值

成功时,sigpending 返回 0。失败时,返回 -1,并设置 errno 以指示错误原因。

#include <stdio.h>

#include <signal.h>

#include <string.h>

void print_sigset(const sigset_t *set) {

printf("Pending signals: ");

for (int i = 1; i < _NSIG; ++i) {

if (sigismember(set, i)) {

printf("%d ", i);

}

}

printf("\n");

}

int main() {

sigset_t pending;

// 查询待处理信号集合

if (sigpending(&pending) == -1) {

perror("sigpending");

return 1;

}

// 打印待处理信号集合

print_sigset(&pending);

return 0;

}

sigaction

sigaction 函数是 POSIX 系统中用于检查和更改与指定信号相关联的处理动作的函数。这个函数比早期用于相同目的的 signal 函数提供了更多的功能和灵活性。通过 sigaction,程序可以精确地指定信号的处理函数、设置信号处理的选项,并查询信号处理的当前状态。

#include <signal.h>

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

参数

signo:指定要操作的信号的编号。act:指向 struct sigaction 的指针,包含了新的信号处理动作。如果此参数为 NULL,则不会更改信号的处理动作,但可以用来查询当前的处理动作(如果 oact 非空)。oact:如果非空,指向一个 struct sigaction 变量,该变量用于存储调用 sigaction 之前信号的处理动作。这允许程序在更改信号处理动作之前保存它,以便将来恢复。

返回值

成功时,sigaction 返回 0。失败时,返回 -1,并设置 errno 以指示错误原因。

struct sigaction 结构

struct sigaction 结构体通常包含以下字段(但具体实现可能有所不同):

sa_handler:指向信号处理函数的指针。如果此字段非 NULL,则它是一个普通的信号处理函数。如果为 SIG_IGN,则忽略该信号。如果为 SIG_DFL,则使用信号的默认处理动作。sa_sigaction:一个指向函数的指针,该函数用于处理信号,并提供了对信号发生时的额外信息的访问(如 siginfo_t 结构)。这通常与 SA_SIGINFO 标志一起使用。sa_mask:一个信号集,指定了在执行信号处理函数期间应该被阻塞的信号。这允许信号处理函数在执行时不受其他信号的干扰。sa_flags:一个标志位集合,用于修改信号处理的行为。常见的标志包括 SA_RESTART(如果信号中断了一个系统调用,则自动重启它),SA_NODEFER(在执行信号处理函数时不自动阻塞该信号),和 SA_SIGINFO(使用 sa_sigaction 而不是 sa_handler)。sa_restorer:这个字段在现代系统中很少使用,通常被忽略。它最初用于指定一个函数,该函数将恢复被信号处理函数中断的系统调用的执行环境。

#include <stdio.h>

#include <signal.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

void signal_handler(int signum) {

printf("Caught signal %d\n", signum);

// 清理资源、关闭文件等操作...

exit(signum);

}

int main() {

struct sigaction act;

// 初始化信号处理结构体

memset(&act, 0, sizeof(act));

act.sa_handler = signal_handler;

// 忽略 SIGPIPE 信号(可选)

signal(SIGPIPE, SIG_IGN);

// 设置 SIGINT 信号的处理函数

if (sigaction(SIGINT, &act, NULL) == -1) {

perror("sigaction");

exit(EXIT_FAILURE);

}

// 主循环,等待信号

while (1) {

pause(); // 暂停执行,等待信号

}

return 0; // 实际上永远不会执行到这里

}

闹钟

alarm 函数是 UNIX 和类 UNIX 系统(包括 Linux)中用于设置定时器的一个函数,它定义在 <unistd.h> 头文件中。当你调用 alarm 函数并传递给它一个参数 seconds 时,该函数会设置一个定时器,该定时器在指定的秒数后到期。当定时器到期时,如果进程没有捕获 SIGALRM 信号(通过调用 signal 或 sigaction 函数设置信号处理函数),则进程会收到 SIGALRM 信号。如果进程捕获了该信号,那么可以执行一些特定的操作,比如清理资源、记录日志等。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

参数

seconds:定时器到期前的秒数。如果 seconds 是 0,则任何当前设置的定时器都会被取消,但不会发送 SIGALRM 信号。

返回值

如果之前已经调用了 alarm 并且设置了定时器,alarm 函数会返回之前设置的剩余时间(秒),直到那个定时器到期。如果之前没有设置定时器,则返回 0。

注意事项

定时器重置:如果在一个已经设置了定时器的进程中再次调用 alarm,那么之前的定时器会被新的定时器替换。返回值是之前定时器的剩余时间(如果有的话)。

精度和限制alarm 函数的精度和限制取决于系统的实现。在一些系统上,alarm 的精度可能较低,因为它基于系统的定时器中断。此外,一些系统可能对 alarm 可以设置的最大时间有限制。

与 sleep/pause 的交互:如果进程在调用 alarm 后调用了 sleep 或 pause,并且 alarm 定时器在 sleep 或 pause 期间到期,那么 sleep 或 pause 会被中断,并且进程会收到 SIGALRM 信号(如果未捕获)。

信号处理:为了处理 SIGALRM 信号,你需要使用 signal 或 sigaction 函数来设置信号处理函数。

#include <stdio.h>

#include <unistd.h>

#include <signal.h>

void sigalrm_handler(int sig_num) {

printf("Alarm signal received\n");

// 在这里执行清理或其他操作

}

int main() {

// 设置 SIGALRM 信号的处理函数

signal(SIGALRM, sigalrm_handler);

// 设置 5 秒后的定时器

alarm(5);

// 暂停执行,等待定时器到期或信号

pause();

return 0;

}

signal 

在C语言中,signal 函数的原型可能因系统而异,但通常它遵循 POSIX 标准或类似的标准。不过,标准的 C 库(如 glibc)通常会在 <signal.h> 头文件中提供一个符合大多数系统需求的 signal 函数声明。

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

参数

int sig:要处理的信号编号。void (*func)(int):指向信号处理函数的指针,该函数接受一个整型参数(信号编号)并返回 void返回值

返回一个指向之前为该信号设置的信号处理函数的指针(也接受一个整型参数并返回 void),或者返回 SIG_ERR 以表示错误,并设置 errno 以指示错误原因。

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

// 信号处理函数

void handler(int sig_num) {

printf("Caught signal %d\n", sig_num);

// 可以在这里执行清理操作或退出程序

exit(0); // 或者使用其他逻辑来响应信号

}

int main() {

// 设置 SIGINT 信号的处理函数

if (signal(SIGINT, handler) == SIG_ERR) {

// 如果 signal 调用失败,则打印错误信息并退出

perror("signal");

exit(EXIT_FAILURE);

}

// 程序的主循环或其他逻辑

while (1) {

printf("Waiting for signal...\n");

sleep(1); // 暂停一秒,以便可以看到信号何时被捕获

}

// 注意:实际上,由于我们在信号处理函数中调用了 exit,

// 所以这行代码永远不会被执行。

return 0;

}


【linux】进程控制——进程创建,进程退出,进程等待-CSDN博客

【linux】进程间通信(IPC)——匿名管道,命名管道与System V内核方案的共享内存,以及消息队列和信号量的原理概述-CSDN博客



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。