zoukankan      html  css  js  c++  java
  • 信号

    信号是软件中断,它提供了一种处理异步事件的方法。很多比较重要的应用程序都需要处理信号。

    每个信号都有一个名字,以SIG开头,比如SIGTTIN,当后台进程试图读取控制终端会收到此信号。这些信号都定义在<signal.h>中,都是正整数,不存在值为0的信号,kill函数对编号为0的信号有特俗用途,此种信号称为空信号。

    可以产生信号的条件:

    • 用户按下某些终端键,比如CRTL+\ 产生退出信号,DELETE或者CTRL+C 中断信号,CRTL+Z 停止信号
    • 硬件产生信号,比如除以0,无效内存引用等
    • 进程调用kill函数将信号发给指定的进程或进程组
    • 用户调用kill命令发给其他进程
    • 硬件检测到某种软件条件已经发生,并通知有关进程也会产生信号

    对信号的处理方式有:

    • 忽略此信号,大多数信号都可以使用这种方式处理,但是SIGSTOP 和SIGKILL不可用忽略。
    • 捕捉信号,应用程序提供一个函数,当信号发生时内核调用此函数处理信号。
    • 系统执行默认动作,对大多数信号而言系统默认操作是终止进程。

    当程序开始执行时,所有信号的处理方式都是系统默认的或者忽略,子进程继承父进程的信号处理方式。

    signal函数

    #include <signal.h>
    
    typedef void (*sighandler_t)(int);
    
    sighandler_t signal(int signum, sighandler_t handler);
    // signal()  returns  the previous value of the signal handler, or SIG_ERR on error.

    信号函数接受一个int参数,无返回值,signal函数,第一个参数为信号名,第二个为信号处理函数地址,或者SIG_IGN,或者SIG_DFL,返回信号以前的处理方式。此函数在不同UNIX以及历史版本系统中行为不一致,可能提供了不可靠信号语义,所以应该使用sigaction替代它。

    下面是使用信号的小程序:

    #include <signal.h>
    #include <stdio.h>
    #include <unistd.h>
    static void sig_usr(int);
    
    int main()
    {
        if(signal(SIGUSR1,sig_usr)==SIG_ERR)
        {
            printf("signal call error\n");
        }
    
        if(signal(SIGUSR2,sig_usr)==SIG_ERR)
        {
            printf("signal call error\n");
        }
    
        for(;;)
        {
            pause();
        }
    }
    
    static void sig_usr(int signo)
    {
        if(signo == SIGUSR1)
        {
            printf("SIGUSR1 called\n");
        }
        else if(signo == SIGUSR2)
        {
             printf("SIGUSR2 called\n");
        }else
        {
            printf("receiverd signal %d\n",signo);
        }
    }

    程序执行结果:

    hero@powerPC:~/source$ ./sigusr &
    [1] 2834
    hero@powerPC:~/source$ kill -USR1 2834
    hero@powerPC:~/source$ SIGUSR1 called
    kill -USR2 2834
    hero@powerPC:~/source$ SIGUSR2 called
    kill 2834
    [1]+  Terminated              ./sigusr

    早期不可靠信号缺点:

    • 进程不可用阻塞某些信号,只能忽略它
    • 信号处理动作可能被改变。

    上面两点说白了就是没有解决好时间窗口的问题。

    中断的系统调用

    系统调用分为:低速系统调用和其他系统调用,低速系统调用分为:

    • 在读某些类型的文件,比如FIFO,SOCK ,终端设备
    • 写某些类型的文件,如果不能够立即接受这些数据,也可能是调用者永远阻塞
    • 打开某些类型的文件,在某些条件发生之前也可能使调用者阻塞
    • pause和wait函数
    • 某些ioctl操作
    • 某些进程间通信函数

    signal函数对低速系统调用的处理方式是信号处理完后默认重新启动它

    sigaction则是可选是否启动被信号处理中断的系统调用。

    可出入函数

    可重入函数指那些被多个进程或者线程同时执行但是行为正常的函数。以不可重入方式使用了全局或者静态数据结构的函数,标准IO函数,或者调用malloc或者free的函数是不可重如的。在信号处理函数中使用不可重如函数,进程的行为是不可预知的。

    SIGCLD语义

    对于SIGCLD的早期处理方式是:

    如果进程特地设置该信号的处理方式为SIG_IGN,则调用进程的子进程将不产生僵死进程。在子进程终止时,将其状态丢弃,如果调用进程随后调用wai函数,那么它将阻塞到最后一个子进程终止,然后返回-1,并将errno设置为ECHILD。

    如果将SIGCLD配置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。

    现在的平台上SIGCLD等同SIGCHLD,但是在编写在老系统上运行的程序时仍然要考虑它的语义。

    可靠信号术语和语义

    当引发信号的事件产生时,为进程产生一个信号,通常在进程表中设置某种形式标志。当对信号采取了这种动作时,我们说向进程递送了信号。在信号产生和递送之间的时间间隔内,称信号的状态是pending.

    进程可以选用信号递送阻塞。如果为进程产生了一个设置为阻塞的信号,而且对该信号的处理动作是默认或者捕捉,则为此进程将此信号保持为pending状态,直到该进程对信号去除阻塞或者对该信号的动作改为忽略。内核在递送一个原来被阻塞的信号给进程时,才决定对它的处理方式。于是进程在信号递送给它之前仍可以改变对信号的处理方式。

    如果在进程解除对某个信号的阻塞之前,此信号发生多次,那么解除之后信号可能被递送一次或者多次。如果有多个信号要发送给进程,优先递送与进程当前状态有关的信号。

    每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。,对于每种可能的信号,该屏蔽字中有一位与之对应,如果对应位被设置,它当前是被阻塞的。

    kill和raise函数

    kill函数允许将信号发送给进程或者进程组,raise函数允许进程向自己发送信号。

    #include <sys/types.h>
    #include <signal.h>
    
    int kill(pid_t pid, int sig);
    int raise(int signo);
    // On success (at least one signal was sent), zero is returned.  On error, -1 is returned, and errno  is set appropriately.

    调用raise(signo)相当于调用kill(getpid(),signo)

    kill的pid参数有4中情况:

    pid > 0 将信号发送给进程ID为pid的进程

    pid == 0 将该信号发送给与进程属于同一进程组的所有进程,而且发送进程具有向这些进程发送信号的权限。所有进程不包括系统进程

    pid < 0 将该信号发送给进程组ID等于pid绝对值,而且发送进程具有向这些进程发送信号的权限

    pid == –1 将信号发送给有权发送的所有进程

    alarm和pause函数

    alarm函数可以设置一个计时器,超时后将会产生SIGALM信号,如果不捕获此信号,其默认动作是终止调用alarm函数的进程。注意每个进程只有一个闹钟。

    #include <unistd.h>
    
     unsigned int alarm(unsigned int seconds);
    //如果以前设置的闹钟没有超时,返回它的余留值,如果本次调用的参数为0,那么取消以前的闹钟,并返回剩余时间值

    如果我们需要捕捉SIGALM信号,必须在调用alarm函数之前设置SIGALM的处理函数,否则可能会导致进程终止

    pause函数是调用进程挂起直到捕捉到一个信号

    #include <unistd.h>
    
      int pause(void);
    // pause() returns -1, and errno is set to EINTR.

    信号集处理和信号屏蔽字的检查与更改

    对于信号集sigset_t 有下面的处理函数

    #include <signal.h>
    
    int sigemptyset(sigset_t *set);
    
     int sigfillset(sigset_t *set);
    
     int sigaddset(sigset_t *set, int signum);
    
    int sigdelset(sigset_t *set, int signum);
    //return 0 on success and -1 on error.
    
     int sigismember(const sigset_t *set, int signum);
    //returns  1 if signum is a member of set, 0 if signum is not a member, and -1 on error.

    调用sigprocmask函数可以检查或者更改信号屏蔽字,或者在一个操作中同时执行这两个操作。

    #include <signal.h>
     int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    // returns 0 on success and -1 on error

    如果哦oldset不为空,那么当前信号信号屏蔽字由oldset返回

    当set不为空,how指示了如何修改信号屏蔽字:

    how == SIG_BLOCK 进程新的信号屏蔽字为set和当前值的并集

    how == SIG_UNBLOCK 从现有的信号屏蔽字中移除set指示的内容

    how == SIG_SETMASK 将现有信号屏蔽字设置为set

    如果set 为NULL,how没有意义

    在调用sigprocmask后如果有任何pending,不在阻塞的信号,那么在函数返回之前,至少会将其中一个递送给该进程。

    获取pending状态信号屏蔽字

    #include <signal.h>
    
     int sigpending(sigset_t *set);
    //sigpending() returns 0 on success and -1 on error.

    下面小程序使用这两个函数:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    
    static void sig_quit(int);
    
    int main(void)
    {
        sigset_t newmask,oldmask,pendmask;
    
        if(signal(SIGQUIT,sig_quit) == SIG_ERR)
        {
            printf("signal error\n");
            return -1;
        }
    
        sigemptyset(&newmask);
        sigaddset(&newmask,SIGQUIT);
        if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        {
            printf("sigprocmask failed\n");
            return -1;
        }
    
        sleep(5);
    
        if(sigpending(&pendmask)<0)
        {
            printf("sigpending failed\n");
            return -1;
        }
    
        if(sigismember(&pendmask,SIGQUIT))
        {
            printf("sigut pening\n");
        }
    
        if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        {
            printf("reset sigmask failed\n");
            return -1;
        }
        printf("sigquit unblock\n");
        sleep(5);
        exit(0);
    }
    
    static void sig_quit(int signo)
    {
        printf("sigquit caught\n");
    
        if(signal(SIGQUIT,SIG_DFL)==SIG_ERR)
        {
            printf("can not reset SIGQUIT");
        }
    }

    执行结果:

    hero@powerPC:~/source$ ./sigpend
    ^\sigut pening
    sigquit caught
    sigquit unblock
    ^\Quit (core dumped)

    sigaction函数

    sigaction函数的功能是检查和修改与指定信号相关联的处理动作。此函数取代了早期使用的signal函数。

    #include <signal.h>
    
     int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
    //sigaction() returns 0 on success; on error, -1 is returned,

    参数signum是要检测或者修改的信号编号,若act指针非空,则要修改其动作。如果oldact非空,则该信号当前处理动作由oldact返回。sigaction结构定义如下:

    struct sigaction {
                   void     (*sa_handler)(int); //addr of signal handler or SIG_DFL or SIG_IGN
                   void     (*sa_sigaction)(int, siginfo_t *, void *); //alternate handler
                   sigset_t   sa_mask; //addtional signals to block
                   int        sa_flags; //signal options
               };

    如果sa_handler包含一个信号处理函数地址,则sa_mask说明了一个信号集,在调用信号处理函数之前,该信号集会加入到进程的信号屏蔽字中,在信号处理函数返回时再复位为原先值。在信号处理函数被调用时,操作系统会将正被递送的信号加入信号屏蔽字中,因此保证了在处理一个给定的信号时,这种信号再次发生,那么它会被阻塞到对前一个信号处理结束为止。如果某信号在被阻塞期间发生多次,那么解除阻塞后,信号处理函数只被调用一次。一旦给定的信号设置了一个动作,那么在调用sigaction显式改变它之前该设置就一直有效。这就提供了可靠信号机制。

    sa_flags取值请查看手册,在sa_flags为SA_SIGINFO时,按照

    void (*sa_sigaction)(int, siginfo_t *, void *); //alternate handler 

    调用信号处理程序。

    sigaction的应用

    实现signal函数

    #include <signal.h>
    
    typedef void (*Sigfunc)(int);
    
    Sigfunc* signal(int signo,Sigfunc* func)
    {
        struct sigaction act,oldact;
        act.sa_handler = func;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        if(signo ==SIGALRM)
        {
    #ifdef SA_INTERRUPT
            act.sa_flags |= SA_INTERRUPT;
    #endif
        }else
        {
    #ifdef SA_RESTART
            act.sa_flags |= SA_RESTART;
    #endif
        }
        if(sigaction(signo,&act,&oldact)<0)
            return SIG_ERR;
        return oldact.sigaction;
    
    }

    实现abort函数

    POSIX规范规定abort函数不理会进程对此函数的阻塞和忽略,如果abort调用终止进程,那么它对所有打开的标准IO流的处理方式应当和进程终止时对每个流调用fclose相同。ISO C要求在捕捉到此信号,而且相应信号处理函数返回后,abort仍然不会返回到其调用者,如果捕获此信号,信号处理程序不能返回的唯一条件是它调用exit,_exit,_Exit ,longjmp,siglongjmp.

    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    void abort(void)
    {
        sigset_t mask;
        struct sigaction action;
    
        sigaction(SIGABRT,NULL,&action);
        if(action.sa_handler == SIG_IGN)
        {
            action.sa_handler = SIG_DFL;
            sigaction(SIGABRT,&action,NULL);
        }
        if(action.sa_handler == SIG_DFL)
            fflush(NULL);
    
        sigfillset(&mask);
        sigdelset(&mask,SIGABRT);
        sigprocmask(SIG_SETMASK,&mask,NULL);
        kill(getpid(),SIGABRT);
    
        fflush(NULL);
        action.sa_handler = SIG_DFL;
        sigaction(SIGABRT,&action,NULL);
        sigprocmask(SIG_SETMASK,&mask,NULL);
        kill(getpid(),SIGABRT);
        exit(1)

    system函数

    POSIX要求system忽略SIGINT和SIGQUIT,阻塞SIGCHLD。

    如果不阻塞SIGCHLD,当执行的子进程终止时,会导致system的调用者误认为它的子进程终止而调用wait函数,这样就阻止了system函数获取子进程的终止状态。

    所以system函数要阻塞SIGCHLD

    忽略SIGINT和SIGQUIT的原因是system执行的命令可能是交互式命令,以及因为system的调用者在程序执行时放弃了控制,等待该执行程序的借宿,所以system的调用者不应该接收这两个信号。

    #include <sys/wait.h>
    #include <errno.h>
    #include <signal.h>
    #include <unistd.h>
    
    int system(const char* cmdstring)
    {
        pid_t pdi;
        int status;
        struct sigaction ignore,saveintr,savequit;
        sigset_t chldmask,savemask;
    
        if(cmdstring == NULL)
        {
            return 1;
        }
    
        ignore.sa_hanler = SIG_IGN;
        sigemptyset(&ignore.sa_mask);
        ignore.sa_flags = 0;
        if(sigaction(SIGQUIT,&ignore,&savequit)<0)
        {
            status = -1;
        }
        if(sigaction(SIGINT,&ignore,&saveintr)< 0)
        {
            status = -1;
        }
    
        sigempty(&chldmask);
        sigaddset(&chldmask,SIGCHLD);
        if(sigprocmask(SIG_BLOCK,&chldmask,&savemask) < 0)
        {
            status = -1;
        }
    
        if((pid = fork() )< 0)
        {
            status = -1
        }else if(pid == 0)
        {
            sigaction(SIGINT,&saveintr,NULL);
            sigaction(SIGQUIT,&savequit,NULL);
            sigprocmask(SIG_SETMASK,&savemask,NULL);
            execl("/bin/sh","sh","-c",cmdstring,(char*)0);
            _exit(127);
        }else
        {
            while(wait(pid,&status,0)<0)
            {
                if(errno != EINTR)
                {
                    status = -1;
                    break;
                }
            }
        }
    
        if(sigaction(SIGINT,&saveintr,NULL)<0)
        {
            return -1;
        }
        if(sigaction(SIGQUIT,&savequit,NULL)<0)
        {
            return -1;
        }
        if(sigprocmaks(SIG_SETMASK,&savemask,NULL)<0)
        {
            return -1;
        }
    
        return status;
    }

    sigsetjmp和siglongjmp函数

    当捕捉到一个信号时,进入信号处理函数,此时当前信号被自动地加入到进程的信号屏蔽字当中。这阻止了后来产生的这种信号中断信号处理函数,如果使用longjmp跳出信号处理程序,那么此信号的信号屏蔽字会发生什么呢?POSIX并没有说明会怎样,它定义了两个新函数sigsetjmp和sigsetlongjmp。在信号处理程序中进行非局部跳转应当使用这两个函数。

    #include <setjmp.h>
    void siglongjmp(sigjmp_buf env, int val);
     int sigsetjmp(sigjmp_buf env, int savesigs);

    如果savesigs非0,则sigsetjmp在env中保存了进程的当前信号屏蔽字。调用siglongjmp时,如果带非0 savesigs的sigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

    下面例子说明了系统所设置的信号屏蔽字如何自动地包括刚捕捉到的信号,也说明了这两个函数的使用。

    #include <stdio.h>
    #include <signal.h>
    #include <setjmp.h>
    #include <time.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include "common.h"
    static void sig_usr1(int);
    static void sig_alrm(int);
    
    static sigjmp_buf jmpbuf;
    static volatile sig_atomic_t canjump;
    
    int main()
    {
        if(signal(SIGUSR1,sig_usr1)==SIG_ERR)
        {
            printf("signal SIGUSR1 error\n");
            return -1;
        }
    
        if(signal(SIGALRM,sig_alrm)==SIG_ERR)
        {
            printf("siganl SIGALRM error\n");
            return -1;
        }
    
        pr_mask("starting main:");
    
        if(sigsetjmp(jmpbuf,1))
        {
            pr_mask("ending main:");
            exit(0);
        }
    
        canjump = 1;
    
        for(;;)
            pause();
    }
    
    static void sig_usr1(int signo)
    {
        time_t starttime;
    
        if(canjump == 0)
            return;
    
        pr_mask("staring sig_usr1: ");
        alarm(3);
        starttime = time(NULL);
    
        for(;;)
        {
            if(time(NULL)>starttime +5)
                break;
        }
        pr_mask("finishing sig_usr1 ");
        canjump = 0;
        siglongjmp(jmpbuf,1);
    }
    
    static void sig_alrm(int signo)
    {
        pr_mask("in sig_alrm ");
    }
    
    void pr_mask(const char* str)
    {
        sigset_t sigset;
        int errno_save;
    
        errno_save = errno;
        if(sigprocmask(0,NULL,&sigset)<0)
        {
            printf("sigprocmask error\n");
            _exit(0);
        }
    
        printf("%s ",str);
    
        if(sigismember(&sigset,SIGINT)) printf("SIGINT ");
        if(sigismember(&sigset,SIGQUIT)) printf("SIGQUIT ");
        if(sigismember(&sigset,SIGUSR1)) printf("SIGUSR1 ");
        if(sigismember(&sigset,SIGALRM)) printf("SIGALRM ");
    
        printf("\n");
        errno = errno_save;

    结果如下:

    hero@powerPC:~/source$ ./siglongjmp &
    [1] 3240
    hero@powerPC:~/source$ starting main:
    kill -USR1 3240
    hero@powerPC:~/source$ staring sig_usr1:  SIGUSR1
    in sig_alrm  SIGUSR1 SIGALRM
    finishing sig_usr1  SIGUSR1
    ending main:
    
    [1]+  Done                    ./siglongjmp

    更改信号屏蔽字可以阻塞所选择的信号,或解除他们的阻塞。使用这种技术可以保护不希望被信号中断的临界区代码,如果希望对一个信号解除阻塞,并等待以前被阻塞的信号发生。这时候需要一个原子操作先恢复信号屏蔽字,然后使进程休眠。这个功能由sigsuspend函数提供。

    #include <signal.h>
    
           int sigsuspend(const sigset_t *mask);
    // sigsuspend() always returns -1, with errno set to inndicate the error (normally, EINTR).

    将进程的信号屏蔽字设置为mask指定的值,在捕捉到一个信号或者发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号并从信号处理程序返回,则sigsuspend返回,并且将该进程的信号屏蔽字设置为调用sigsuspend之前的值。

    函数的应用:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include "common.h"
    
    static void sig_int(int signo)
    {
        pr_mask("\n in sig_int: " );
    }
    
    int main()
    {
        sigset_t newmask,oldmask,waitmask;
    
        pr_mask("program start: ");
    
        if(signal(SIGINT,sig_int)== SIG_ERR)
        {
            printf("signal error\n");
            return -1;
        }
    
        sigemptyset(&newmask);
        sigaddset(&newmask,SIGINT);
        sigemptyset(&waitmask);
        sigaddset(&waitmask,SIGUSR1);
    
        if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)<0)
        {
            printf("sigprocmask failed\n");
            return -1;
        }
    
        pr_mask("in critical region: ");
    
    
        if(sigsuspend(&waitmask)!= -1)
        {
            printf("sigsuspend error\n");
            return -1;
        }
    
        pr_mask("after return from sigsuspend ");
    
        if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        {
            printf("sigprocmask faild\n");
            return -1;
        }
    
        pr_mask("programme exit: ");
    
        exit(0);
    }

    等待全局变量被设置:

    #include <stdio.h>
    #include <signal.h>
    #include "common.h"
    
    volatile sig_atomic_t quitflag;
    
    static void sig_int(int signo)
    {
        pr_mask("in sig_int ");
        if(signo == SIGINT)
            printf("\n interputer\n");
        else if(signo == SIGQUIT)
            quitflag = 1;
    }
    
    int main()
    {
        sigset_t newmask,oldmask,zeromask;
        if(signal(SIGINT,sig_int) == SIG_ERR)
        {
            printf("signal interputer error\n");
            return -1;
        }
    
        if(signal(SIGQUIT,sig_int)== SIG_ERR)
        {
            printf("signal quit error \n");
            return -1;
        }
    
        sigemptyset(&newmask);
        sigemptyset(&zeromask);
        sigaddset(&newmask,SIGQUIT);
    
        if(sigprocmask(SIG_BLOCK,&newmask,&oldmask)< 0)
        {
            printf("sigprocmask error\n");
            return -1;
        }
    
        while(quitflag == 0)
        {
            pr_mask("befor sigsuspend: ");
            sigsuspend(&zeromask);
            pr_mask("after sigsuspend: ");
        }
    
        quitflag = 0;
    
        if(sigprocmask(SIG_SETMASK,&oldmask,NULL)<0)
        {
            printf("reset mask failed\n");
        }
    
        return 0;
    
    }

    实现sleep:

    #include <unistd.h>
    #include <signal.h>
    
    static void sig_alrm(int signo)
    {
    }
    
    unsigned int sleep(unsigned int nsecs)
    {
        struct sigaction newact,oldact;
        sigset_t newmask,oldmask,suspmask;
    
        unsigned int unslept;
    
        newact.sa_handler = sig_alrm;
        newact.sa_flags = 0;
        sigemptyset(&newact.sa_mask);
        sigaction(SIGALRM,&newact,&oldact);
    
        sigemptyset(&newmask);
        sigaddset(&newmask,SIGALRM);
        sigprocmask(SIG_BLOCK,&newmask,&oldmask);
    
        alarm(nsecs);
    
        suspmask = oldact;
        sigdelset(&suspmask,SIGALRM);
        sigsuspend(&suspmask);
    
        unslept = alarm(0);
        sigaction(SIGALRM,&oldact,NULL);
    
        sigprocmask(SIG_SETMASK,&oldmask,NULL);
    
        return unslept;
    }

    信号出错信息:

    #include <signal.h>
    #include<string.h>
    void psignal(int sig, const char *s);//s为自定义字符串
     char *strsignal(int sig);//error 返回NULL,成功返回信号对应的字符串
  • 相关阅读:
    IE11开发人员工具 js脚本debugger调试
    Dynamics CRM OData方式进行增删改查时报错的问题
    Get Form type using javascript in CRM 2011
    Dynamics CRM 同一实体多个Form显示不同的Ribbon按钮
    Dynamics CRM 通过OData查询数据URI中包含中文的情况
    打印控件
    spark
    zookeeper集群配置与启动——实战
    javascript学习
    etcd
  • 原文地址:https://www.cnblogs.com/xiaofeifei/p/4122119.html
Copyright © 2011-2022 走看看