• 进程


    进程标识

    每个进程都有一个非负数的唯一ID,称为进程ID,由于进程ID是唯一的,所以经常用于其他标识的一部分,比如文件名等

    进程除了拥有进程ID外还有其他的ID,通过下面函数可以获取这些ID:

    #include <sys/types.h>
    #include <unistd.h>
    
    pid_t getpid(void);  //return process id of calling process
    pid_t getppid(void);//return the parent  ID of calling process
    uid_t getuid(void); // returns the real user ID of the calling process
    uid_t geteuid(void);// returns the effective user ID of the calling process
    gid_t getgid(void);//returns the real group ID of the calling process
    gid_t getegid(void);// returns the effective group ID of the calling process
    //all these function always sucessful

    创建子进程

    Unix中使用fork函数创建子进程,fork函数调用一次返回两次,在父进程中返回新建子进程的ID,在子进程中返回0,返回-1表示失败。

    #include <unistd.h>
    
     pid_t fork(void);
    //On  success,  the PID of the child process is returned in the parent, and 0 is returned in the child.
    //On failure, -1 is returned in the parent, no child process is created, and  errno  is  set  appropriately

    子进程和父进程在执行fork后继续执行。子进程是父进程的副本,子进程获得父进程的堆,栈,和数据空间,注意这些都是副本,并不共享。下面程序说明子进程对变量的修改并不影响父进程中该变量的值:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <stdlib.h>
    
    int glob = 6;
    char buf[]="a write to stdout\n";
    
    int main(int argc,char* argv[])
    {
        int var;
        pid_t  pid;
        var = 88;
    
        if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!= sizeof(buf)-1)
        {
            printf("write to stdout failed\n");
            return -1;
        }
    
        printf("before fork\n");
    
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
        }else if(pid == 0)
        {
            glob++;
            var++;
        }else
        {
            sleep(3);
        }
    
        printf("pid = %d,glob = %d,val= %d\n",getpid(),glob,var);
    
        exit(0);
    }

    下面是执行结果:

    hero@powerPC:~/source$ ./fork
    a write to stdout
    before fork
    pid = 3048,glob = 7,val= 89
    pid = 3047,glob = 6,val= 88
    hero@powerPC:~/source$ ./fork >> tmp
    hero@powerPC:~/source$ cat tmp
    a write to stdout
    before fork
    pid = 3051,glob = 7,val= 89
    before fork
    pid = 3050,glob = 6,val= 88

    我们看到子进程对变量的修改并没有改变变量在父进程中的值。当我们将输出重定向到文件后,发现befork fork打印了两次,这是因为标准IO面向交互终端是行缓冲的,而重定向到文件时候变成全缓冲的,在执行fork之后,这个缓冲也被拷贝到了子进程中,所以在执行exit(0)的时候,exit函数刷新所有标准IO,所以打印了两次。

    在执行fork之后父子进程共享已经在父进程中打开的文件:

    在fork之后处理描述符有两种常见情况:

    • 父进程等待子进程完成。
    • 父子进程执行不同的程序段。

    除了打开的文件描述符被子进程继承外,父进程还有许多其他属性也被子进程共享:

    • 实际用户ID,实际组ID,有效用户ID,有效组ID,附加组ID
    • 进程组ID,会话ID
    • 控制终端
    • 设置用户ID标志和设置组ID标志
    • 当前工作目录,根目录
    • 文件模式创建屏蔽字(umask值)
    • 信号屏蔽字(sig_t)
    • 针对任意打开的文件的描述符的FD标志
    • 环境
    • 连接的共享存储段
    • 存储映射
    • 资源限制(rlimt_t)

    父子进程之间的区别:

    • fork的返回值
    • 进程ID
    • 进程父ID
    • 子进程的tms_utime,tms_stime,tms_cutime以及tms_ustime均被设置为0
    • 父进程设置的文件锁不会被子进程继承
    • 在子进程中未被处理的alarm被清除
    • 在子进程中未处理信号集被设为空

    vfork函数

    vfork用于创建一个新的进程,该进程随后执行exec运行新的程序。vfork和fork都会创建一个新的进程,但是有以下区别:

    • vfork并不完全复制复制父进程地址空间,在执行exec或exit之前,它在父进程地址空间中运行
    • fork保证子进程先执行
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int glob = 6;
    
    int main(int argc,char* argv[])
    {
        int var;
        pid_t pid;
    
        var = 88;
        printf("before vfork\n");
    
        if((pid = vfork())<0)
        {
            printf("vfork error\n");
            return -1;
        }else if(pid == 0)
        {
            glob++;
            var++;
            _exit(0);
        }
    
        printf("pid = %d,glob=%d,var=%d\n",getpid(),glob,var);
    
        exit(0);
    }

    执行结果:

    hero@powerPC:~/source$ ./vfork
    before vfork
    pid = 3620,glob=7,var=89

    注意在子进程不可用调用exit,或者return 返回,因为标准IO会冲洗IO并关闭打开的流,由于父子进程共享相同的地址空间,这样父进程的标准IO流就被关闭了,进程的行为不确定。

    exit函数

    前面的文章中讲述过进程正常终止和异常终止的方法:

    正常终止:

    • 在main函数中调用return,等于调用exit
    • 调用exit函数,exit函数由ISO定义,操作包括调用清理函数,然后关闭所有标注IO流
    • 调用_Exit或者_exit,其目的是提供了一种无需运行信号处理程序和终止处理程序而终止进程的方式
    • 进程中的最后一个线程返回
    • 进程中的最后一个线程执行pthread_exit,这种情况下进程的终止状态是0,与传递给pthread_exit的参数无关。

    异常终止:

    • 调用abort
    • 信号终止
    • 最后一个线程对取消请求作出响应

    对于上述的任何终止情形,父进程都是可以获取子进程的终止状态的,正常终止时,三个exit函数的退出状态就是终止状态,异常终止时内核会产生一个对应终止状态。如果父进程在子进程之前终止,那么子进程由init进程领养,也就是子进程的父进程变成init进程,init进程被设计成任何子进程终止时,init都会调用wait获取它的中孩子状态。在UNIX中,一个已经终止,但是父进程没有对其做善后处理(获取子进程相关信息,释放它仍然占有的资源)的进程称为僵死进程。下面是一个僵死进程。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    #define PSCMD "ps -ef|grep defunct" 
    
    int main(int argc,char* argv[])
    {
        pid_t pid;
    
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
        }else if (pid == 0)
        {
            exit(0);
        }
    
        sleep(4);
        system(PSCMD);
        exit(0);
    }

    子进程终止状态获取

    在子进程终止后内核会向父进程发送SIGCHLD信号,由于信号是异步事件,父进程要么提供信号处理函数处理信号,要么忽略它,对于这个信号系统模式动作是忽略它,如果在信号处理函数中调用wait函数可能立即返回,但是在任意时候调用可能会阻塞。

    子进程状态变化获取函数:

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t wait(int *status);
    
    pid_t waitpid(pid_t pid, int *status, int options);
    
     int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    
    //成功返回进程ID或者0,失败返回-1

    wait函数一直阻塞到有任意一个子进程状态发生变化(进程状态变化具体为:进程终止,停止,恢复执行)这时候wait函数立即返回,如果status不为空,子进程状态存在status中,如果不关心可以设为NULL

    waitpid函数根据参数不一样行为不一样:

    pid > 0,等待 进程ID等于pid的子进程

    pid == 0 等待进程组ID等于调用进程组ID的任意子进程

    pid == –1 同wait

    pid < –1  等待进程组ID等于pid绝对值的子进程

    option取值如下:

    WNOHANG // return immediately if no child has exited

    WUNTRACED // also  return  if  a  child has stopped,but not traced

    WCONTINUED  // also return if a stopped child has been resumed by delivery of SIGCONT

    waitid行为由idtype控制:

    idtype == P_PID //等待进程ID等于id的子进程

    idtype == P_PGID //等待进程组ID等于id的子进程

    idtype == P_ALL //等待任意子进程,id参数被忽略

    waitid成功返回后,子进程状态相关信息存储在类型为siginfo的infop中,该结构如下:

    siginfo_t {
                   int      si_signo;    /* Signal number */
                   int      si_errno;    /* An errno value */
                   int      si_code;     /* Signal code */
                   int      si_trapno;   /* Trap number that caused
                                            hardware-generated signal
                                            (unused on most architectures) */
                   pid_t    si_pid;      /* Sending process ID */
                   uid_t    si_uid;      /* Real user ID of sending process */
                   int      si_status;   /* Exit value or signal */
                   clock_t  si_utime;    /* User time consumed */
                   clock_t  si_stime;    /* System time consumed */
                   sigval_t si_value;    /* Signal value */
                   int      si_int;      /* POSIX.1b signal */
                   void    *si_ptr;      /* POSIX.1b signal */
                   int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
                   int      si_timerid;  /* Timer ID; POSIX.1b timers */
                   void    *si_addr;     /* Memory location which caused fault */
                   long     si_band;     /* Band event (was int in
                                            glibc 2.3.2 and earlier) */
                   int      si_fd;       /* File descriptor */
                   short    si_addr_lsb; /* Least significant bit of address
                                            (since Linux 2.6.32) */
               }

    该结构中需要比较关心的是si_code,si_pid,si_uid,si_signo(这时候该值总是SIGCHLD),si_status.

    获取进程退出状态:

    #include "common.h"
    #include <stdio.h>
    #include <stdlib.h>
    
    void pr_exit(int status)
    {
        if(WIFEXITED(status))
        {
            printf("normal termination,exit code=%d\n",WEXITSTATUS(status));
        }else if(WIFSIGNALED(status))
        {
            printf("abnormal termination,signal number=%d%s\n",WTERMSIG(status),WCOREDUMP(status)?"core dump generated":" ");
        }else if(WIFSTOPPED(status))
        {
            printf("child stopped,signal number=%d\n",WSTOPSIG(status));
        }
    }
    
    
    
    int main()
    {
        pid_t pid;
        int status;
    
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
        }else if(pid == 0)
        {
            exit(7);
        }
    
        if(wait(&status)!=pid)
        {
            printf("wait error\n");
            return -1;
        }
        pr_exit(status);
        
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
        }else if(pid == 0)
        {
            abort();
        }
    
        if(wait(&status)!=pid)
        {
            printf("wait error\n");
            return -1;
        }
        pr_exit(status);
        return 0;
    }

    避免僵死产生进程的方法是调用两次fork

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
        pid_t pid;
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
        }else if(pid == 0)
        {
            if((pid = fork())< 0)
            {
                printf("fork faild\n");
                return -1;
            }else if(pid > 0)
            {
                exit(0);
            }
            sleep(3);
            printf("second child,parent pid=%d\n",getppid());
            exit(0);
        }
    
        if(pid != waitpid(pid,NULL,0))
        {
            printf("waitpid failed\n");
            return -1;
        }
    
        exit(0);
    }

    进程之间的竞争

    当调用fork之后,到底父进程还是子进程先执行是依赖内核调度的,下面演示这种情况:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    
    static void charatatime(char*);
    
    int main()
    {
        pid_t pid;
    
        if((pid = fork())< 0)
        {
            printf("fork error\n");
            return -1;
        }else if(pid == 0)
        {
            charatatime("output from child\n");
        }else
        {
            charatatime("output from parent\n");
        }
        exit(0);
    }
    
    static void charatatime(char* str)
    {
        char* ptr;
        int c;
        setbuf(stdout,NULL);
        for(ptr=str;(c=*ptr++)!=0;)
        {
            putc(c,stdout);
        }
    }

    多次执行该程序,结果都不一样,没有错误不代表没有竞争。因此必须要有一种机制保证一方先执行,这个留到信号去解决。

    exec函数

    #include <unistd.h>
    int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
    int execv(const char *pathname, char *const argv[]);
    int execle(const char *pathname, const char *arg0, ...
    /* (char *)0, char *const envp[] */ );
    int execve(const char *pathname, char *const argv[], char *const envp[]);
    int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
    int execvp(const char *filename, char *const argv[]);
    int fexecve(int fd, char *const argv[], char *const envp[]);
    //All seven return: −1 on error, no return on success

    当进程调用exec函数时,该进程执行的程序被完全替代为新程序,而新程序则从其main函数开始执行。

    如果

    int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );

    int execvp(const char *filename, char *const argv[]);

    filename包含/,把它当作路径处理,否则从环境变量中查找filename

    执行exec函数后,进程的下面属性保持不变:

    • 进程Id和父进程ID
    • 实际用户Id实际组ID
    • 附加组Id
    • 进程组ID,会话Id
    • 控制终端
    • 闹钟尚余留时间
    • 当前工作目录 ,根目录
    • 文件创建模式屏蔽字
    • 文件锁
    • 未处理的信号
    • 进程信号屏蔽字
    • 资源限制
    • 进程时间消耗值

    在执行exec后实际用户ID和实际组ID并没有改变,而有效ID是否变化取决于程序文件的设置用户ID位和设置组ID是否打开,如果程序文件设置用户ID位打开了,那么有效用户ID变为程序文件的所有者ID,否则有效用户ID不变。对有效组ID处理相同。

    下面是使用exec的小程序:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    char* env_init[]={"USER=unknow","PATH=/tmp",NULL};
    
    int main(void)
    {
        pid_t pid;
    
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
    
        }else if(pid == 0)
        {
            if(execle("/home/hero/source/echoall","echoall","ARG1","ARG2",(char*)0,env_init)<0)
            {
                printf("execle failed\n");
                return -1;
            }
        }
        if(waitpid(pid,NULL,0)<0)
        {
            printf("waitpid error\n");
            return -1;
        }
        if((pid = fork())<0)
        {
            printf("fork error\n");
            return -1;
    
        }else if(pid == 0)
        {
            if(execlp("echoall","echoall","only one",(char*)0)<0)
            {
                printf("execle failed\n");
                return -1;
            }
        }
        if(waitpid(pid,NULL,0)<0)
        {
            printf("waitpid error\n");
            return -1;
        }
        exit(0);
    }

    程序中用到的echoall源码:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc,char* argv[])
    {
        int i;
        char **ptr;
        extern char** environ;
    
        for(i=0;i<argc;i++)
        {
            printf("argv[%d]:%s\n",i,argv[i]);
        }
    
        for(ptr=environ;*ptr != 0;ptr++)
        {
            printf("%s\n",*ptr);
        }
    
        exit(0);
    }

    更改用户ID和组ID

    当需要访问一些资源时经常需要改变自己的UID和GID,可以用setuid改变实际用户ID和有效ID,使用setgid改变实际组ID和有效组ID。

    #include <sys/types.h>
     #include <unistd.h>
    
     int setuid(uid_t uid);
     int setgid(gid_t gid);
    //On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

    修改用户ID有下面限制(组ID同用户ID)

    进程具有超级用户特权,那么实际用户ID,有效用户ID和保存的用户ID都被设置为uid

    进程没有超级用户特权,但是uid等于实际用户ID或者保存的设置用户ID,那么函数将有效用户ID该为uid

    上述条件都不满足返回错误-1

    需要注意的是:

    只有超级用户进程可以改变实际用户ID。

    当对程序设置了设置用户ID位,exec函数才会设置有效用户ID,而保存的用户ID由exec根据文件的用户ID设置了进程有效用户ID后,将这个副本保存起来。如果程序没有设置用户ID位,那么进程的有效用户ID位不变。

    附加组ID不受这些函数影响。

    #include <sys/types.h>
    #include <unistd.h>
    
     int seteuid(uid_t euid);
    int setegid(gid_t egid);
    // On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

    上面的两个函数只设置有效用户Id和有效组ID。非特权进程可以将有效用户ID设置为实际用户ID或者保存的用户ID,而特权用户进程则可以将有效用户ID改为euid。

    解释器文件

    解释器通常是解释器文件第一行路径中的程序。

    解释器文件是文本文件,以#!开头,通常为#!pathname

    解释器pathname后面可以跟随可选参数,通常为 –f 指定解释器需要读的解释器文件。

    下面是使用exec执行解释器文件的情况:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    int main(void)
    {
        pid_t pid;
    
        if((pid = fork())< 0)
        {
            printf("fork error\n");
            return -1;
        }else if (pid == 0)
        {
            if(execl("/usr/bin/testinterp","testinterp","arg1","arg2",(char*)0) < 0)
            {
                printf("execl error\n");
                return -1;
            }
        }
    
        if(waitpid(pid,NULL,0)<0)
        {
            printf("waitpid error\n");
            return -1;
        }
    
        return 0;
    }

    解释器文件的优点:

    1.有些程序是使用脚本编写,解释器文件可以将这种情况隐藏起来

    2.方便使用

    3.提供了除/bin/bash 编写脚本的其他方式。

    system函数

    system函数为在程序中执行系统命令提供了方便,但是system函数对系统的依赖性很强,UNIX中扩展了system在ISO C中的定义。

    #include <stdlib.h>
    
     int system(const char *command);
    //如果command 为NULL,则仅当命令处理程序可用时返回非0值。

    因为system函数的实现中调用了fork,exec ,waitpid,因此有三种返回值:

    1.如果fork失败或者waitpid返回除EINTR之外的出错,则system函数返回-1,而且errno设置了错误类型。

    2.如果exec失败,则返回值如同shell执行了exit(127)一样

    3.如果三个函数都执行成功,返回值为shell的终止状态,其格式同waitpid中的status

    一个简单的system函数:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <errno.h>
    #include "common.h"
    
    int demosystem(char* cmdstring)
    {
        pid_t pid;
        int status;
    
        if(cmdstring == NULL)
        {
            return 1;//interpreter always avaliable
        }
    
        if((pid = fork()) < 0 )
        {
            status = -1;
        }else if(pid == 0)
        {
            if(execl("/bin/bash","bash","-c",cmdstring,(char*)0)< 0)
            {
                _exit(127);
            }
        }else
        {
            while(waitpid(pid,&status,0)<0 )
            {
                if(errno != EINTR)
                {
                    status = -1;
                    break;
                }
            }
        }
    
        return status;
    
    }
    
    int main(void)
    {
        int status;
        if((status = demosystem("date"))< 0)
        {
            fputs("system error\n",stderr);
        }
        pr_exit(status);
        if((status = demosystem("nosuchcmd"))< 0)
        {
            fputs("system error\n",stderr);
        }
        pr_exit(status);
        if((status = demosystem("who;exit 20"))< 0)
        {
            fputs("system error\n",stderr);
        }
        pr_exit(status);
    
    }

    sytem函数绝对不可以在设置用户ID和设置组ID的程序中调用,因为设置的特权会被fork和exec之后保留下来,这是一个安全漏洞。

    进程会计

    进程会计会在进程结束时内核产生一条会计记录,典型的会计记录包括命令名,所使用的CPU时间总量,用户ID和组ID,启动时间等。由于是每次进程终止时写一条记录,所以记录文件中是进程终止的顺序不是启动顺序。

    会计记录对应于进程而不是程序。exec函数不创建新的记录,但改变了记录中的名字,如果A—exec—B---exec---C--exit,那么最后记录的命令名为程序C,但CPU时间是程序A,B,C之和。

    用户标识

    系统通常记录用户登录时使用的名字,用getlogin函数可以获取登录名,如果调用此函数的进程没有连接到用户登录所使用的终端,则本函数会失败,通常称这些进程为守护进程。

    #include <unistd.h>
    
     char *getlogin(void);
    int getlogin_r(char *buf, size_t bufsize);
    // getlogin() returns a pointer to the username when successful, and NULL on failure, with errno set  to
     //      indicate the cause of the error.  getlogin_r() returns 0 when successful, and nonzero on failure.

    进程时间

    任何进程都可以使用times函数获取墙上时钟时间,用户CPU时间,系统CPU时间。tms_cutime包含了此进程用wait,waitpid或者waittid等待到的各个子进程值的tms_utime和tms_cutime的总和,同样tms_cstime包含了此进程用wait,waitpid或者waittid等待到的各个子进程值的tms_stime和tms_cstime的总和

    #include <sys/times.h>
    
    clock_t times(struct tms *buf);
     struct tms {
                   clock_t tms_utime;  /* user time */
                   clock_t tms_stime;  /* system time */
                   clock_t tms_cutime; /* user time of children */
                   clock_t tms_cstime; /* system time of children */
               };
    
    // times() returns the number of clock ticks that have elapsed since an arbitrary  point  in  the  past.
     //      The  return  value  may  overflow  the  possible  range  of  type clock_t.  On error, (clock_t) -1 is
     //      returned, and errno is set appropriately.

    下面是个测试程序:

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/times.h>
    #include <sys/wait.h>
    #include <stdlib.h>
    #include "common.h"
    
    static void pr_times(clock_t,struct tms*,struct tms*);
    static void do_cmd(char*);
    
    int main(int argc,char* argv[])
    {
        int i;
        setbuf(stdout,NULL);
        for(i=1;i<argc;i++)
        {
            do_cmd(argv[i]);
        }
    
        return 0;
    }
    
    static void do_cmd(char* cmd)
    {
        struct tms tmsstart,tmsend;
        clock_t start,end;
        int status;
    
        printf("\ncommand:%s\n",cmd);
    
        if((start = times(&tmsstart))== -1)
        {
            printf("times failed\n");
        }
    
        status = system(cmd);
        if(status < 0)
        {
            printf("system call failed\n");
        }
        if((start = times(&tmsend))== -1)
        {
            printf("times failed\n");
        }
    
        pr_times(end-start,&tmsstart,&tmsend);
    
        pr_exit(status);
    
    }
    
    static void pr_times(clock_t real,struct tms* tmsstart,struct tms* tmsend)
    {
        static long clktck = 0;
    
        if(clktck == 0)
        {
            if((clktck= sysconf(_SC_CLK_TCK))<0)
            {
                printf("sysconf error\n");
            }
        }
    
        printf("real:%7.2f\n",real/(double)clktck);
        printf("user:%7.2f\n",(tmsend->tms_utime - tmsstart->tms_utime)/(double)clktck);
        printf("user:%7.2f\n",(tmsend->tms_stime - tmsstart->tms_stime)/(double)clktck);
        printf("user:%7.2f\n",(tmsend->tms_cutime - tmsstart->tms_cutime)/(double)clktck);
        printf("user:%7.2f\n",(tmsend->tms_cstime - tmsstart->tms_cstime)/(double)clktck);
    }
  • 相关阅读:
    Linux 下安装JDK1.8
    Linux 常规操作
    C3P0连接池拒绝连接
    Oracle查看并修改最大连接数
    Oracle 建立 DBLINK
    Oracle 数据 update后怎么恢复到以前的数据
    Oracle 11g中解锁被锁定的用户
    身份证15位转18位
    Druid数据库连接池
    CentOS 下安装 LEMP 服务(Nginx、MariaDB/MySQL 和PHP)
  • 原文地址:https://www.cnblogs.com/xiaofeifei/p/4098085.html
Copyright © 2020-2023  润新知