可靠信号
可靠信号的处理有两个过程如下:
- 信号产生:当这些事件(硬件异常(如除以0)、软件条件(如alarm定时器超时)、终端产生的信号或调用
kill
函数)发生时,内核会为进程产生一个信号,同时在进程表中设置一个标志。 - 信号递送:内核使目标进程对该信号作出反应称为信号递送:或是改变目标进程的执行状态(默认动作),或是开始执行信号处理程序,或两者都是。
可靠信号
信号产生到信号递送的时间间隔内,信号的状态是未决的(pending)。
如果进程对信号设置阻塞递送,而且对该信号的动作是系统默认动作或捕捉该信号,那么内核会为该进程将此信号保持为未决状态,直到进程解除阻塞或忽略该信号。
进程在信号递送给其之前可以改变对信号的处理动作。sigpending
函数可以判定哪些信号是阻塞并处于未决状态。
在对信号阻塞期间,该信号产生了多次,POSIX.1允许系统递送该信号一次或多次。如果递送多次,会对该信号进行排队。
事实上,只有POSIX.1的实时扩展信号才会递送多次并且排队,其他信号并不会排队且只递送一次。
每个进程都有一个信号屏蔽字,规定了当前要阻塞递送到该进程的信号集。进程可以调用sigpromask
函数来检测和更新其当前信号屏蔽字。
POSIX.1定义了一个新数据类型sigset_t
,来表示信号集。信号屏蔽字也使用信号集来表示。
产生信号的函数
kill
函数
kill
函数将信号发送进程或进程组:
#include <signal.h>
int kill(pid_t pid, int signo);
pid>0
时,将信号发送给进程ID为pid
的进程。
pid==0
时,将信号发送给与调用进程属于统一进程组的所有进程(调用进程需要具有相应权限),系统进程除外。
pid<0
时,将信号发送给进程组ID与pid
绝对值相等的进程组中所有进程(调用进程需要具有相应权限),系统进程除外。
pid==-1
时,将信号发送给调用进程有权限向其发送信号的所有进程,系统进程除外。
权限说明如下:
- 超级用户可将信号发送给任一进程。
- 非超级用户,发送进程的实际用户ID或有效用户ID与接收进程的实际用户ID或有效用户ID相等。
- 如果实现支持_POSIX_SAVED_IDS,则检查接收者的保存设置用户ID(而非有效用户ID)。保存设置用户ID通常是进程的有效用户ID,只有当提升权限或者降低权限时,才会将有效用户ID改为其他值。
- 如果发送的信号是SIGCONT,则可将其发送给同会话的任一进程。
POSIX.1将信号编号0定义为空信号,如果kill
函数的signo
参数为0, 则执行正常的错误检查但不发送信号。
如果信号不阻塞,那么kill
返回之前信号就已经递送给目标进程。
raise
函数
raise
函数允许进程向自身发送信号。
等价于kill(getpid(), signo)
。
alarm
函数
alarm
函数可以设置一个定时器,在定时器超时时,会产生SIGALRM
信号。如果忽略或不捕捉此信号,默认动作是终止调用进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 返回值:0或以前设置的闹钟时间的余留秒数
seconds
指产生信号SIGALRM
需要经过的秒数。SIGALRM
信号由内核产生,到调度进程执行信号处理函数还需要一个时间间隔。
每个进程只能有一个闹钟时间。
如果调用alarm
函数时,之前调用alarm
设置的闹钟时间还未超时,则会将其余留值作为本次调用的返回值。 同时如果这次调用alarm
函数的参数为0,则仅取消之前的闹钟时间,不会产生新的。
pause
函数
pause
函数会使调用进程挂起,直到捕捉到一个信号。
#include <unistd.h>
int pause(void);
只有执行了一个信号处理程序并从其返回时,pause
函数才会返回。
这种情况下,pause
函数返回-1,errno
被设置为EINTR
。
abort
函数
abort
函数的作用是使进程异常终止。
#include <stdlib.h>
[[noreturn]] void abort(void);
// 无返回值
ISO C要求该函数即使捕捉到信号而且从响应信号处理程序返回,abort
函数也不能返回到其调用者。
POSIX对其的进一步要求是:
abort
函数并不理会进程对SIGABRT
信号的忽略或阻塞(仅剩两种动作即默认或者自定义)。- 当捕捉到
SIGABRT
信号并从其信号处理程序返回时(信号处理程序中没有终止进程),abort
函数本身也要终止该进程(执行SIGABRT
信号默认动作或者直接退出进程)。 abort
函数终止进程时,应该在终止前执行与fclose
相同的操作(即刷洗I/O流)。
下面是依据POSIX标准实现的abort
函数示例:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void abort1()
{
struct sigaction action;
// 如果进程对SIGABRT信号的动作是忽略,则重新设置为默认
sigaction(SIGABRT, NULL, &action);
if (action.sa_handler == SIG_IGN)
{
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
}
// 如果是默认则刷洗所有I/O流,否则由用户自定义的信号处理程序来决定
if (action.sa_handler == SIG_DFL)
// 为什么要在这儿刷洗I/O流呢,因为后面接收到SIGABRT信号会导致进程直接退出
fflush(NULL);
// 如果进程阻塞SIGABR信号,则解除阻塞
sigset_t mask;
sigfillset(&mask);
sigdelset(&mask, SIGABRT);
sigprocmask(SIG_SETMASK, &mask, NULL);
// 向进程自己发送SIGABRT信号
// 此时在kill返回前产生SIGABRT信号,处理方式是默认行为或者用户自定义动作, 默认方式会终止进程
kill(getpid(), SIGABRT);
// 如果执行到这里,说明SIGABRT是用户自定义信号处理程序,表明已经从信号处理程序返回
// 同时也说明在其信号处理程序中没有退出进程,因此这里就要刷洗I/O流并退出进程(根据POSIX要求)
// 刷洗所有流
fflush(NULL);
// 设置SIGABRT信号处理程序为默认,并发送SIGABRT信号,让进程退出,如果不退出则执行exit强行退出
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
sigprocmask(SIG_SETMASK, &mask, NULL);
// 发送SIGABRT信号,调用系统默认行为
kill(getpid(), SIGABRT);
// 退出进程
exit(1);
}
事实上,对于SIGABRT
信号只有两种处理方式,分别是 SIG_DFL
或者自定义处理函数,abort
函数则需要在SIG_DFL
时刷洗流,或者在用户自定义程序不退出的时候进行善后(刷洗流以及退出进程)。
system
函数
#include <stdlib.h>
int system(const char *command);
system
函数的执行流程是:
system
函数的调用进程fork
一个子进程,然后调用exec
函数执行一个shell
程序。- 该
shell
进程再fork
一个子进程,通过exec
函数来执行传入system
函数的命令字符串。 system
函数返回的是shell
进程的终止状态,仅当shell
进程异常终止时,system
函数的返回值才会报告一个异常终止。shell
进程会以执行命令字符串进程的终止状态作为其返回值,但如果执行命令字符串带有符号&表示后台执行,则shell进程会立即返回,并且不会将执行命令字符串进程的终止状态作为其返回值。
POSIX.1规定system
函数忽略SIGINT
与SIGQUIT
信号,阻塞SIGCHLD
信号(这里指的是system
函数的调用进程,而非创建的子进程)。
阻塞SIGCHLD
信号的原因在于如果不阻塞SIGCHLD
信号,那么当system
创建的子进程结束时,system
函数的调用进程就可能会认为自己的子进程结束了(如果该调用进程之前有过创建子进程的话),然后调用wait
函数获取子进程的终止状态,导致system
函数无法获得其子进程终止状态作为它的返回值。
忽略SIGINT
与SIGQUIT
信号的原因在于使用system
函数执行命令时,如果不忽略这两个信号,system
函数的调用进程就可能被这两个信号中断或这导致进程退出,造成命令的不完整执行。
下面是依据POSIX.1 标准实现的system
函数
#include <asm-generic/errno-base.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
int system2(const char* cmdstring)
{
pid_t pid;
int status;
if (cmdstring == NULL)
return 1;
// 忽略SIGINT与SIGQUIT,阻塞SIGCHLD
struct sigaction ignore, saveintr, savequit;
sigset_t chldmask, savemask;
ignore.sa_handler = SIG_IGN;
sigemptyset(&ignore.sa_mask);
ignore.sa_flags = 0;
if (sigaction(SIGINT, &ignore, &saveintr) != 0)
return -1;
if (sigaction(SIGQUIT, &ignore, &savequit) != 0)
return -1;
// 阻塞SIGCHLD信号,并保存之前的信号屏蔽字到savemask中,用于之后恢复
sigemptyset(&chldmask);
sigaddset(&chldmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) != 0)
return -1;
// 创建子进程执行命令
if ((pid = fork()) < 0)
status = -1;
else if (pid == 0)
{
// 子进程
// 恢复SIGINT与SIGQUIT的默认动作,并且取消阻塞SIGCHLD
sigaction(SIGINT, &saveintr, NULL);
sigaction(SIGQUIT, &savequit, NULL);
sigprocmask(SIG_SETMASK, &savemask, NULL);
// exec函数执行成功之后不会返回
// shell程序替换子进程的执行内容,至于shell进程如何执行命令字符串,那就由shell进程来决定了
// 正常情况下,shell进程会将执行命令字符串进程的终止状态作为其终止状态
// 但如果命令字符串是后台执行,那么shell进程就会直接返回。
execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
// 失败之后会执行下面
_exit(127);
}
else
{
// 父进程不恢复SIGINT与SIGQUIT,是为了防止接收到这两个命令导致进程退出,造成子进程执行命令不完整。
// waitpid函数获取子进程的终止状态,即shell进程的终止状态
while (waitpid(pid, &status, 0) < 0)
{
if (errno != EINTR)
{
status = -1; // 其他异常错误,如果被其他信号中断,则会返回继续等待
break;
}
}
}
// 恢复SIGINT与SIGQUIT的默认动作和解除阻塞SIGCHLD
if (sigaction(SIGINT, &saveintr, NULL) != 0)
return -1;
if (sigaction(SIGQUIT, &savequit, NULL) != 0)
return -1;
// 解除对SIGCHLD信号的阻塞,在waitpid获取其子进程状态之后,可以防止调用进程获取system函数的子进程的终止状态
if (sigprocmask(SIG_SETMASK, &savemask, NULL) != 0)
return -1;
return status;
}
int main()
{
system2("date");
}
执行结果如下:
$ ./system2
Mon Jan 22 00:04:09 CST 2024
sleep
函数
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
// 返回值: 如果请求休眠的时间达到,返回0;如果被信号处理程序中断,则返回剩余的时间
sleep
函数是进程挂起直到满足下面两个条件:
- 经过了参数
seconds
指定的时钟时间。 - 调用进程捕捉到一个信号并从信号处理程序返回(被信号中断)。
可以使用alarm
函数来实现sleep
函数,但是存在问题是无法处理这两个函数之间的影响,以及SIGALRM
信号产生的影响。
下面是使用alarm
函数实现的sleep
函数:
unsigned int sleep3(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
newact.sa_handler = sig_alrm1;
sigemptyset(&newact.sa_mask);
// 1. 设置SIGALRM的信号处理程序
if (sigaction(SIGALRM, &newact, &oldact) != 0)
return seconds;
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
// 2. 阻塞SIGALRM信号
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
return seconds;
// 3. 设置时钟,seconds秒后产生SIGALRM信号
alarm(seconds);
// 4. 从原有的信号屏蔽字中去除SIGALRM,解除SIGALRM阻塞,并并暂停进程等待接收信号
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
sigsuspend(&suspmask); // 原子操作,解除阻塞到进程休眠是原子操作,不会导致信号丢失
// 5. 检测休眠剩余描述
unslept = alarm(0);
// 6. 恢复之前的信号处理程序
sigaction(SIGALRM, &oldact, NULL);
// 7. 恢复之前的信号屏蔽字
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
问题1:如果调用进程在调用该sleep
函数之前,调用过alarm
函数,并且恰好在步骤1和2之间产生了SIGALRM
信号,这就会导致无法执行用户设定的信号处理函数。
问题2:如果在步骤2和3之间,调用进程之前调用alarm
函数而产生的SIGALRM
刚好被阻塞,那么步骤4中的sigsuspend
函数就会立即返回,而不是等待步骤3中的alarm
函数超时。
基于这些问题,现在sleep
函数一般都由nanosleep
函数来实现。
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec * rem);
nanosleep
函数直到参数req
指定的时间过去或者被信号中断,才会返回。如果被信号处理程序中断则将剩余时间写入参数rem
中,前提是rem!=NULL
。
nanosleep
函数不会产生任何信号,因此没有上面sleep3
函数的问题。
nanosleep
函数采用的时钟是CLOCK_REALTIME
,该时钟时间可能不会被修改,但是系统会进行实时性保证(比如休眠期间时间被NTP修改,系统会忽略这种修改直到休眠指定时长)。
函数clock_nanosleep
函数可以指定采用哪种时钟(不同时钟的精度不同),同时也可以指定相对时间休眠或绝对时间休眠。
#include <time.h>
int clock_nanosleep(clockid_t clockid, int flags,
const struct timespec *req,
struct timespec * rem);
参数flag
取值为0表示相对时间,取值为TIMER_ABSTIME
表示绝对时间。req
与rem
参数与nanosleep
函数保持一致。
参数clockid
的取值如下所示:
时钟类型 | 说明 | 用途 |
---|---|---|
CLOCK_REALTIME | 表示系统的实时时间,即当前的日历时间,会受到系统时间调整的影响。 | 用于获取当前的绝对时间 |
CLOCK_MONOTONIC | 表示一个单调递增的时钟,不受系统时间调整的影响。 | 用于测量时间间隔和计时 |
CLOCK_PROCESS_CPUTIME_ID | 表示当前进程的CPU时间。 | 测量进程使用的CPU时间 |
CLOCK_THREAD_CPUTIME_ID | 表示当前线程的CPU时间。 | 测量线程使用的CPU时间 |
下面的例子说明了这些时钟的用法:
// 示例1,使用CLOCK_REALTIME获取系统当前时间
#include <stdio.h>
#include <time.h>
int main()
{
struct timespec currentTime;
// 获取当前绝对时间
clock_gettime(CLOCK_REALTIME, ¤tTime);
// 将秒和纳秒打印出来
printf("当前绝对时间:秒:%ld,纳秒:%ld\n", currentTime.tv_sec, currentTime.tv_nsec);
return 0;
}
// $ ./get_current_time_by_CLOCK_REALTIME
// 当前绝对时间:秒:1705936900,纳秒:59752278
// 示例2展示了进程占用的CPU时间、线程占用的CPU时间、以及测量的时间间隔
#include <pthread.h>
#include <stdio.h>
// 线程函数
void* threadFunc(void* arg)
{
struct timespec startTime, endTime;
long elapsedTime;
// 获取起始时间
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &startTime);
// 执行一些操作
for (int i = 0; i < 50000; i++)
{
for (int j = 0; j < 4000; j++)
{
}
}
// 获取结束时间
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &endTime);
// 计算CPU时间(秒)
elapsedTime = (endTime.tv_sec - startTime.tv_sec) * 1000000000 + (endTime.tv_nsec - startTime.tv_nsec);
printf("线程使用CPU时间:%ld 纳秒\n", elapsedTime);
return NULL;
}
int main()
{
struct timespec startTime, endTime;
long elapsedTime;
// 获取起始时间
clock_gettime(CLOCK_MONOTONIC, &startTime);
// 执行一些操作
{
struct timespec startTime1, endTime1;
long elaspedTime1;
// 获取起始时间
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &startTime1);
// 执行一些操作
{
pthread_t thread;
// 创建线程
pthread_create(&thread, NULL, threadFunc, NULL);
// 等待线程结束
pthread_join(thread, NULL);
}
// 获取结束时间
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &endTime1);
// 计算CPU时间(秒)
elaspedTime1 = (endTime1.tv_sec - startTime1.tv_sec) * 1000000000 + (endTime1.tv_nsec - startTime1.tv_nsec);
printf("进程使用CPU时间:%ld 纳秒\n", elaspedTime1);
}
// 获取结束时间
clock_gettime(CLOCK_MONOTONIC, &endTime);
// 计算时间间隔(毫秒)
elapsedTime = (endTime.tv_sec - startTime.tv_sec) * 1000000000 + (endTime.tv_nsec - startTime.tv_nsec);
printf("时间间隔:%ld 纳秒\n", elapsedTime);
return 0;
}
// 执行结果如下:
// $ ./get_thread_cpu_time
// 线程使用CPU时间:281239559 纳秒
// 进程使用CPU时间:281451290 纳秒
// 时间间隔:281492002 纳秒
// 可以看到这些时间之间存在微小的变化
示例
使用alarm
与pause
,进程使自己休眠一段指定的时间
#include <signal.h>
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>
static void sig_alrm1(int signo)
{
struct timeval tv;
gettimeofday(&tv, NULL);
printf("date:%ld\n", tv.tv_sec);
printf("sig alrm1\n");
}
static void sig_alrm2(int signo)
{
struct timeval tv;
gettimeofday(&tv, NULL);
printf("date:%ld\n", tv.tv_sec);
printf("sig alrm2\n");
}
unsigned int sleep1(unsigned int seconds)
{
// 1. 根据alarm返回检查之前是否有设置alarm
unsigned int leftseconds = alarm(seconds);
printf("leftseconds=%u\n", leftseconds);
if (leftseconds > 0)
{
// 之前的alarm还有leftseconds 秒超时
unsigned int diff = 0;
if (leftseconds < seconds)
{
// 使用两段休眠,先使之前alarm超时, 在重新设置剩余时间
alarm(leftseconds);
pause();
// 计算第二段需要休眠的时间并重新设置信号处理程序
diff = seconds - leftseconds;
printf("diff=%u\n", diff);
if (signal(SIGALRM, sig_alrm2) == SIG_ERR)
return (diff);
alarm(diff);
pause();
return (alarm(0));
}
else
{
// 之前设置的alarm的剩余时间大于现在设置,采用两段休眠
// 备份之前的信号处理函数,退出之前恢复
void* prev = signal(SIGALRM, sig_alrm2);
if (prev == SIG_ERR)
return (seconds);
alarm(seconds);
pause();
// 恢复之前信号处理配置以及剩余时间
diff = leftseconds - seconds;
printf("diff=%u\n", diff);
if (signal(SIGALRM, prev) == SIG_ERR)
return 0; // 恢复之前的信号处理程序失败
return (alarm(diff));
// 这里不用pause,是用来恢复之前设置的alarm
}
}
else
{
// alarm返回0,之前没有设置alarm
void* prev = signal(SIGALRM, sig_alrm2);
if (prev == SIG_ERR)
return (seconds);
alarm(seconds);
pause();
return (alarm(0));
}
}
int main()
{
if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
{
perror("signal");
return -1;
}
struct timeval tv;
gettimeofday(&tv, NULL);
printf("date:%ld\n", tv.tv_sec);
// 第一次设置alarm
alarm(20);
sleep(5);
printf("before sleep\n");
// 里面会是第二次设置alarm,看看退出时会不会恢复
sleep1(10);
printf("after sleep\n");
gettimeofday(&tv, NULL);
printf("date:%ld\n", tv.tv_sec);
while (1)
{
sleep(1);
}
}
执行结果如下:
$ ./sleep1
date:1705091044
before sleep
leftseconds=15
date:1705091059
sig alrm2
diff=5
after sleep
date:1705091059
date:1705091064
sig alrm1
^C
在sleep1
函数中会检查第一次调用alarm
的返回值:
- 如果其值大于0并且小于本次调用
alarm
的值,则恢复之前的闹钟并等其超时,然后设置差值作为本次alarm
参数值; - 如果其值大于本次设置值,则会使用本次设置值先调用
alarm
,再在sleep1
函数之前使用差值来重置之前的闹钟(包括信号处理程序)。
存在问题:在alarm
与pause
函数之间存在竞争条件,如果alarm
函数在调用pause
之前超时,并调用了信号处理程序,则pause
会使进程永久阻塞。
使用setjmp
与longjmp
避免竞争条件
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
static jmp_buf env_alrm;
static void sig_alrm1(int signo)
{
printf("sig alrm1\n");
longjmp(env_alrm, 1); // 设置setjmp的返回值为1
}
unsigned int sleep2(unsigned int seconds)
{
if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
return (seconds);
if (setjmp(env_alrm) == 0)
{
alarm(seconds);
pause();
}
return (alarm(0));
}
static void sig_int(int signo)
{
int i, j;
volatile int k;
printf("\nsig_int starting\n");
for (i = 0; i < 300000; i++)
{
for (j = 0; j < 300000; j++)
k = i + j;
}
printf("sig_int finished\n");
}
int main()
{
unsigned int unslept;
if (signal(SIGINT, sig_int) == SIG_ERR)
perror("signal");
unslept = sleep2(5);
printf("sleep2 returned: %u\n", unslept);
exit(0);
}
执行结果如下:
$ ./sleep2
^C
sig_int starting # sig_int信号处理程序中断
sig alrm1 # longjmp执行完之后不会恢复之前中断的sig_int信号处理程序
sleep2 returned: 0
在这个例子中,使用setjmp
和longjmp
函数解决上个例子中的竞争问题。
但是如果SIGALRM
中断了其他信号处理函数,longjmp
执行之后,就会提前终止该信号处理程序,因为longjmp
函数会清理栈空间。
这两个例子说明在涉及信号处理时,需要考虑周到、不影响其他代码段才行。
使用alarm
对可能阻塞的操作设置上限值
下面的例子是从标准输入读一行(低速系统调用,可能阻塞),然后写入到标准输出中。
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
static jmp_buf env_alrm;
static void sig_alrm1(int signo)
{
printf("sig alrm1\n");
longjmp(env_alrm, 1); // 设置setjmp的返回值为1
}
unsigned int sleep2(unsigned int seconds)
{
if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
return (seconds);
if (setjmp(env_alrm) == 0)
{
alarm(seconds);
pause();
}
return (alarm(0));
}
int main()
{
int n;
char line[1024];
if (signal(SIGALRM, sig_alrm1) == SIG_ERR)
{
perror("signal");
exit(00);
}
if (setjmp(env_alrm) != 0)
{
printf("read timeout");
exit(-1);
}
alarm(10);
if ((n = read(STDIN_FILENO, line, sizeof(line))) < 0)
perror("read");
alarm(0);
write(STDOUT_FILENO, line, n);
exit(0);
}
执行结果如下:
$ ./read_timeout_longjmp
12
12
$ ./read_timeout_longjmp
sig alrm1
read timeout
该实例与上个示例存在相同的问题,与其他信号处理程序交互会导致其他信号处理程序不正常退出。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E5%8F%AF%E9%9D%A0%E4%BF%A1%E5%8F%B7/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。