Linux 文件系列:深入理解文件形貌符fd,重定向,自定义shell当中重定向的模拟 ...

打印 上一主题 下一主题

主题 827|帖子 827|积分 2481

一.预备知识


颠末刚才的分析,我们可以一个很重要的结论:
   一个文件要被打开,肯定要先在OS中形成被打开的文件对象
  下面我们往返顾一下C语言中常见的文件接口
我们会发现重定向跟它们有所联系
二.回首C语言中常见的文件接口跟重定向建立联系

关于C语言文件操作的具体内容,各人可以看我的这篇博客:
C语言文件操作详解
1.fopen函数的先容



2.fclose函数的先容


3.代码演示

1.以"w"(写)的方式打开


   以"w"(写)的方式打开,假如文件不存在,就会在当前进程所在的路径当中创建它
  

创建乐成
我们用vim写一些内容,再用w打开,看看w是否会清空之前的内容


清空乐成
2.跟输出重定向的联系



我们会发现,fopen的"w"选项跟输出重定向很像啊
下面我们再来看看"a"选项的方式打开跟追加重定向的关系
3.以 “a”(追加)的方式打开

   "a"也是写入,不过是从文件末了处开始写入,是追加式写入,并不会清空文件
  


并没有清空原有内容
4.跟追加重定向的联系



我们会发现,fopen的"a"选项跟追加重定向很像啊
三.认识并利用体系接口

下面我们来认识并利用一下体系调用接口
起首我们达成1个共识:
   C语言的文件操作接口,它的底层肯定封装了体系调用接口
  1.open

1.open和fopen的联系(引出 FILE和struct file的联系)

这是C语言提供的库函数:fopen:

这是体系调用接口pen:

可见,这个fd跟我们之前常用的FILE*指针很像啊,
实在它们的功能是一样的,C语言的FILE是一个结构体,这个结构体内里封装了fd
而这个fd是被打开的文件的结构体(struct file内核数据结构)中的一个属性,是用来区分不同文件的
2.open的进一步先容

刚才我们还没有先容第2个参数呢,下面我们来看一下

至此我们也理解了fopen是怎样对open进行封装的
下面我们来利用一下open函数并且看看这个fd到底是啥啊?
3.open函数的利用

1.close函数


2.开始利用并且看看这个fd到底是什么?



如今我们有了两个问题:

  • 0 1 2去哪了?
  • 为什么会是 3 4 5 6?
下面就让我们借助这两个问题来深入理解一下文件形貌符fd
四.理解文件形貌符fd

1.文件形貌符fd的本质


2.标准输入,标准输出,标准错误

在C语言的学习中我们都听说过
   C语言步调(也就是进程),只要运行起来,默认就打开3个流
  

今天我们要说明的是:

3.理解Linux下齐备皆文件的设计理念


五.理解struct file内核数据结构


六.fd的分配规则

1.先抛出结论


2.代码演示

分配规则1就不言而喻了,我们来验证分配规则2

我们先关闭stdin,然后在打开log.txt
假如该进程中log.txt被分配的fd是0,那么验证乐成

验证乐成
3.替换标准输出时的现象

下面我们先关闭stdout,然后再打开log.txt


为什么最后的
  1. printf("log.txt的fd是: %d\n",fd);
复制代码
没有乐成打印呢?
由于stdout是标准输出流,是显示器对应的流,
我们平常printf是将字符串打印到stdout当中,但是我们在printf之前已经把stdout关掉了
以是不会打印到显示器
但是当我加了一行代码

cat log.txt之后

发现刚才printf中原来要往显示器上打印的数据如今写到了log.txt内里
这说明:
  1. 1.printf只认识stdout,也就是fd为1的文件
  2. 2.上层的fd并没有改变,但是底层fd指向的内容发生改变了
  3. 本来fd值为1的这个fd应该要指向显示器这个设备文件的
  4. 但是在这个进程当中  现在指向log.txt了
  5. 3.也就是说这个过程其实就是进行了一种类似于狸猫换太子式的指向的改变
复制代码
七.理解重定向

1.重定向的本质

颠末刚才的print的例子之后,我们可以发现:
由此可以得出重定向的本质:

   重定向的本质,实在就是修改特定文件fd的指向
  2.演示一下重定向

1.输出重定向

这是log.txt之前的数据



输出重定向乐成
2.追加重定向



追加重定向乐成
3.输入重定向

要进行输入重定向,我们要利用fread函数
1.fread函数


2.演示



输入重定向乐成
八.dup2函数:实现两个fd之间的重定向

实在库内里给我们提供了一个函数dup2
可以实现两个fd之间的重定向
下面我们利用dup2函数再来演示一下重定向

1.dup2实现输出重定向



此时log.txt的fd是3
2.dup2实现追加重定向



实现乐成
3.dup2实现输入重定向



实现乐成
九.自定义shell当中重定向的模拟实现

颠末上面的练习之后,下面我们修改一下我们的myshell.c代码,模拟实现一下重定向
关于myshell.c代码的实现,各人可以看我的博客当中的
Linux自定义shell的编写,内里实现了自定义shell
1.原myshell.c代码

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/wait.h>
  7. //#define DEBUG 1
  8. #define SEP " "
  9. char cwd[1024]={'\0'};
  10. int lastcode=0;//上一次进程退出时的退出码
  11. char env[1024][1024]={'\0'};
  12. int my_index=0;
  13. const char* getUsername()
  14. {
  15.     const char* username=getenv("USER");
  16.     if(username==NULL) return "none";
  17.     return username;
  18. }
  19. const char* getHostname()
  20. {
  21.     const char* hostname=getenv("HOSTNAME");
  22.     if(hostname==NULL) return "none";
  23.     return hostname;
  24. }
  25. const char* getPwd()
  26. {
  27.     const char* pwd=getenv("PWD");
  28.     if(pwd==NULL) return "none";
  29.     return pwd;
  30. }
  31. //分割字符串填入usercommand数组当中
  32. //例如: "ls -a -l" 分割为"ls" "-a" "-l"
  33. void CommandSplit(char* usercommand[],char* command)
  34. {
  35.     int i=0;
  36.     usercommand[i++]=strtok(command,SEP);
  37.     while(usercommand[i++]=strtok(NULL,SEP));
  38. }
  39. //解析命令行
  40. void GetCommand(char* command,char* usercommand[])
  41. {
  42.     command[strlen(command)-1]='\0';//清理掉最后的'\0'
  43.     CommandSplit(usercommand,command);
  44. #ifdef DEBUG
  45.     int i=0;
  46.     while(usercommand[i]!=NULL)
  47.     {
  48.         printf("%d : %s\n",i,usercommand[i]);
  49.         i++;
  50.     }
  51. #endif
  52. }
  53. //创建子进程,完成任务
  54. void Execute(char* usercommand[])
  55. {
  56.     pid_t id=fork();
  57.     if(id==0)
  58.     {
  59.         //子进程执行部分
  60.         execvp(usercommand[0],usercommand);
  61.         //如果子进程程序替换失败,已退出码为1的状态返回
  62.         exit(1);
  63.     }
  64.     else
  65.     {
  66.         //父进程执行部分
  67.         int status=0;
  68.         //阻塞等待
  69.         pid_t rid=waitpid(id,&status,0);
  70.         if(rid>0)
  71.         {
  72.             lastcode=WEXITSTATUS(status);
  73.         }
  74.     }
  75. }
  76. void cd(char* usercommand[])
  77. {
  78.     chdir(usercommand[1]);
  79.     char tmp[1024]={'\0'};
  80.     getcwd(tmp,sizeof(tmp));
  81.     sprintf(cwd,"PWD=%s",tmp);
  82.     putenv(cwd);
  83.     lastcode=0;
  84. }   
  85. int echo(char* usercommand[])
  86. {
  87.     //1.echo后面什么都没有,相当于'\n'
  88.     if(usercommand[1]==NULL)
  89.     {
  90.         printf("\n");
  91.         lastcode=0;
  92.         return 1;
  93.     }
  94.     //2.echo $?  echo $PWD echo $
  95.     char* cmd=usercommand[1];
  96.     int len=strlen(cmd);
  97.     if(cmd[0]=='$' && len>1)
  98.     {
  99.         //echo $?
  100.         if(cmd[1]=='?')
  101.         {
  102.             printf("%d\n",lastcode);
  103.             lastcode=0;
  104.         }
  105.         //echo $PWD
  106.         else
  107.         {
  108.             char* tmp=cmd+1;
  109.             const char* env=getenv(tmp);
  110.             //找不到该环境变量,打印'\n',退出码依旧为0
  111.             if(env==NULL)
  112.             {
  113.                 printf("\n");
  114.             }
  115.             else
  116.             {
  117.                 printf("%s\n",env);
  118.             }
  119.             lastcode=0;
  120.         }
  121.     }
  122.     else
  123.     {
  124.         printf("%s\n",cmd);
  125.     }
  126.     return 1;
  127. }
  128. void export(char* usercommand[])
  129. {
  130.     //export
  131.     if(usercommand[1]==NULL)
  132.     {
  133.         lastcode=0;
  134.         return;
  135.     }
  136.     strcpy(env[my_index],usercommand[1]);
  137.     putenv(env[my_index]);
  138.     my_index++;
  139. }
  140. int doBuildIn(char* usercommand[])
  141. {
  142.     //cd
  143.     if(strcmp(usercommand[0],"cd")==0)
  144.     {
  145.         if(usercommand[1]==NULL) return -1;
  146.         cd(usercommand);
  147.         return 1;
  148.     }
  149.     //echo
  150.     else if(strcmp(usercommand[0],"echo")==0)
  151.     {
  152.         return echo(usercommand);
  153.     }
  154.     //export
  155.     else if(strcmp(usercommand[0],"export")==0)
  156.     {
  157.         export(usercommand);
  158.     }
  159.     return 0;
  160. }
  161. int main()
  162. {
  163.     while(1)
  164.     {
  165.         //1.打印提示符信息并获取用户的指令
  166.         printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
  167.         char command[1024]={'\0'};
  168.         fgets(command,sizeof(command),stdin);
  169.         char* usercommand[1024]={NULL};
  170.         //2.解析command字符串,放入usercommand指针数组当中
  171.         GetCommand(command,usercommand);
  172.         //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
  173.         int flag=doBuildIn(usercommand);
  174.         //返回值!=0说明是内建命令,无需执行第4步
  175.         if(flag!=0) continue;
  176.         //4.创建子进程,交由子进程完成任务
  177.         Execute(usercommand);
  178.     }
  179.     return 0;
  180. }
复制代码
2.怎样实现重定向

以输出重定向为例:
  1. 指令 > log.txt
复制代码
输出重定向的作用实在就是把原来应该往显示器上打印的内容打印到了log.txt上
也就是说进行输出重定向的话,我们的log.txt就替代了显示器的位置
也就是说实行指令之前我们只需要实行一个
dup2(fd,1)即可
fd是log.txt的文件形貌符,1是显示器的文件形貌符
也就是说对于用户输入的一个完备的指令
比方:
  1. ls -a -l > log.txt
复制代码

我们要做的是:
1.检测是否需要进行重定向(检测指令当中是否有> 大概 >> 大概<)
2.假如需要,把这个指令拆分为两部门
“ls -a -l"和"log.txt”
后半部门是重定向到哪个文件当中
前半部门是真正的指令
怎样拆分呢?把>改为’\0’,>>改为’\0’>,<改为’\0’即可
注意:
  1. ls -a -l >                        log.txt
复制代码
如许写也是可以的,因此我们要取出log.txt的时候要跳过空格
3.定义全局变量

第一步:
我们定义全局变量redir和四个宏常量,文件名和跳过空格的宏


注意:
在解析命令行之前就要检测是否要进行重定向
由于假如要进行重定向,就会对命令行进行拆分,拆分之后的指令才是真正要实行的指令
在后续实行指令时只需要根据全局变量redir是否是NoneRedir来判断是否要进行重定向
假如要进行重定向,根据redir具体的值来判断要进行输出/追加/输入重定向
进而判断filename的打开方式和dup2要覆盖显示器还是键盘
然后分类打开和覆盖即可
4.检测是否要进行重定向的函数

  1. //跳过空格的宏
  2. #define SKIP_SPACE(pos) do{ while(isspace(*pos)) pos++; }while(0)
  3. //检测是否要进行重定向
  4. void CheckRedir(char* command)
  5. {
  6.     int len=strlen(command);
  7.     char* start=command,*end=command+len-1;
  8.     while(end>=start)
  9.     {
  10.         //输入重定向
  11.         //cat < log.txt
  12.         if(*end=='<')
  13.         {
  14.             *end='\0';
  15.             filename=end+1;
  16.             SKIP_SPACE(filename);
  17.             redir=InputRedir;
  18.             break;
  19.         }
  20.         else if(*end=='>')
  21.         {
  22.             //追加重定向
  23.             //ls -a -l >> log.txt
  24.             if(end>start && *(end-1)=='>')
  25.             {
  26.                 *(end-1)='\0';
  27.                 filename=end+1;
  28.                 SKIP_SPACE(filename);
  29.                 redir=AppendRedir;
  30.                 break;
  31.             }
  32.             //输出重定向
  33.             else
  34.             {
  35.                 *end='\0';
  36.                 filename=end+1;
  37.                 SKIP_SPACE(filename);
  38.                 redir=OutPutRedir;
  39.                 break;
  40.             }
  41.         }
  42.         else
  43.         {
  44.             end--;
  45.         }
  46.     }
  47. }
复制代码

在这里我们就只演示非内建命令的重定向操作了
由于只演示非内建命令就能够做到让各人很好地去理解重定向了
5.创建子进程进行步调替换的函数修改


  1. //创建子进程,完成任务
  2. void Execute(char* usercommand[])
  3. {
  4.     pid_t id=fork();
  5.     if(id==0)
  6.     {
  7.         //检测是否要进行重定向
  8.         int fd=0;
  9.         //输出重定向
  10.         if(redir==OutPutRedir)
  11.         {
  12.             fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
  13.             dup2(fd,1);
  14.         }
  15.         //追加重定向
  16.         if(redir==AppendRedir)
  17.         {
  18.             fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
  19.             dup2(fd,1);
  20.         }
  21.         //输入重定向
  22.         if(redir==InputRedir)
  23.         {
  24.             fd=open(filename,O_RDONLY);
  25.             dup2(fd,0);
  26.         }
  27.         //子进程执行部分
  28.         execvp(usercommand[0],usercommand);
  29.         //如果子进程程序替换失败,已退出码为1的状态返回
  30.         exit(1);
  31.     }
  32.     else
  33.     {
  34.         //父进程执行部分
  35.         int status=0;
  36.         //阻塞等待
  37.         pid_t rid=waitpid(id,&status,0);
  38.         if(rid>0)
  39.         {
  40.             lastcode=WEXITSTATUS(status);
  41.         }
  42.     }
  43. }
复制代码
6.main函数的修改


  1. int main()
  2. {
  3.     while(1)
  4.     {
  5.         redir=NoneRedir;
  6.         filename=NULL;
  7.         //1.打印提示符信息并获取用户的指令
  8.         printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
  9.         char command[1024]={'\0'};
  10.         fgets(command,sizeof(command),stdin);
  11.         command[strlen(command)-1]='\0';//清理掉最后的'\n'
  12.         //2.检测重定向
  13.         CheckRedir(command);
  14.         char* usercommand[1024]={NULL};
  15.         //3.解析command字符串,放入usercommand指针数组当中
  16.         GetCommand(command,usercommand);
  17.         //4.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
  18.         int flag=doBuildIn(usercommand);
  19.         //返回值!=0说明是内建命令,无需执行第4步
  20.         if(flag!=0) continue;
  21.         //5.创建子进程,交由子进程完成任务
  22.         Execute(usercommand);
  23.     }
  24.     return 0;
  25. }
复制代码
7.修改之后myshell.c代码

模拟实现重定向的目的是为了让我们更好地去理解重定向
因此本次实现重定向只是简单的模拟实现,跟体系的重定向并不完全相同
  1. #include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <sys/wait.h>#include <ctype.h>#include <sys/stat.h>#include <fcntl.h>//#define DEBUG 1#define SEP " "#define NoneRedir 0#define OutPutRedir 1#define AppendRedir 2#define InputRedir 3int redir=NoneRedir;char* filename=NULL;char cwd[1024]={'\0'};int lastcode=0;//上一次进程退出时的退出码char env[1024][1024]={'\0'};int my_index=0;const char* getUsername(){    const char* username=getenv("USER");    if(username==NULL) return "none";    return username;}const char* getHostname(){    const char* hostname=getenv("HOSTNAME");    if(hostname==NULL) return "none";    return hostname;}const char* getPwd(){    const char* pwd=getenv("PWD");    if(pwd==NULL) return "none";    return pwd;}//分割字符串填入usercommand数组当中//比方: "ls -a -l" 分割为"ls" "-a" "-l"void CommandSplit(char* usercommand[],char* command){    int i=0;    usercommand[i++]=strtok(command,SEP);    while(usercommand[i++]=strtok(NULL,SEP));}//解析命令行void GetCommand(char* command,char* usercommand[]){    if(strlen(command)==0) return;    CommandSplit(usercommand,command);#ifdef DEBUG    int i=0;    while(usercommand[i]!=NULL)    {        printf("%d : %s\n",i,usercommand[i]);        i++;    }#endif}//创建子进程,完成任务
  2. void Execute(char* usercommand[])
  3. {
  4.     pid_t id=fork();
  5.     if(id==0)
  6.     {
  7.         //检测是否要进行重定向
  8.         int fd=0;
  9.         //输出重定向
  10.         if(redir==OutPutRedir)
  11.         {
  12.             fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
  13.             dup2(fd,1);
  14.         }
  15.         //追加重定向
  16.         if(redir==AppendRedir)
  17.         {
  18.             fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
  19.             dup2(fd,1);
  20.         }
  21.         //输入重定向
  22.         if(redir==InputRedir)
  23.         {
  24.             fd=open(filename,O_RDONLY);
  25.             dup2(fd,0);
  26.         }
  27.         //子进程执行部分
  28.         execvp(usercommand[0],usercommand);
  29.         //如果子进程程序替换失败,已退出码为1的状态返回
  30.         exit(1);
  31.     }
  32.     else
  33.     {
  34.         //父进程执行部分
  35.         int status=0;
  36.         //阻塞等待
  37.         pid_t rid=waitpid(id,&status,0);
  38.         if(rid>0)
  39.         {
  40.             lastcode=WEXITSTATUS(status);
  41.         }
  42.     }
  43. }
  44. void cd(char* usercommand[]){    chdir(usercommand[1]);    char tmp[1024]={'\0'};    getcwd(tmp,sizeof(tmp));    sprintf(cwd,"PWD=%s",tmp);    putenv(cwd);    lastcode=0;}   int echo(char* usercommand[]){        //1.echo后面什么都没有,相称于'\n'        if(usercommand[1]==NULL)        {            printf("\n");            lastcode=0;            return 1;        }        //2.echo $?  echo $PWD echo $        char* cmd=usercommand[1];        int len=strlen(cmd);        if(cmd[0]=='$' && len>1)        {            //echo $?            if(cmd[1]=='?')            {                printf("%d\n",lastcode);                lastcode=0;            }            //echo $PWD            else            {                char* tmp=cmd+1;                const char* env=getenv(tmp);                //找不到该环境变量,打印'\n',退出码仍旧为0                if(env==NULL)                {                    printf("\n");                }                else                {                    printf("%s\n",env);                }                lastcode=0;            }        }        else        {            printf("%s\n",cmd);        }        return 1;}int doBuildIn(char* usercommand[]){    if(usercommand[0]==NULL) return 0;    //cd    if(strcmp(usercommand[0],"cd")==0)    {        if(usercommand[1]==NULL) return -1;        cd(usercommand);        return 1;    }    //echo    else if(strcmp(usercommand[0],"echo")==0)    {        return echo(usercommand);    }    //export    else if(strcmp(usercommand[0],"export")==0)    {        //export        if(usercommand[1]==NULL)        {            lastcode=0;            return 1;        }        strcpy(env[my_index],usercommand[1]);        putenv(env[my_index]);        my_index++;    }    return 0;}//跳过空格的宏#define SKIP_SPACE(pos) do{ while(isspace(*pos)) pos++; }while(0)//检测是否发生了重定向void CheckRedir(char* command){    int len=strlen(command);    char* start=command,*end=command+len-1;    while(end>=start)    {        //输入重定向        //cat < log.txt        if(*end=='<')        {            *end='\0';            filename=end+1;            SKIP_SPACE(filename);            redir=InputRedir;            break;        }        else if(*end=='>')        {            //追加重定向            //ls -a -l >> log.txt            if(end>start && *(end-1)=='>')            {                *(end-1)='\0';                filename=end+1;                SKIP_SPACE(filename);                redir=AppendRedir;                break;            }            //输出重定向            else            {                *end='\0';                filename=end+1;                SKIP_SPACE(filename);                redir=OutPutRedir;                break;            }        }        else        {            end--;        }    }}int main()
  45. {
  46.     while(1)
  47.     {
  48.         redir=NoneRedir;
  49.         filename=NULL;
  50.         //1.打印提示符信息并获取用户的指令
  51.         printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
  52.         char command[1024]={'\0'};
  53.         fgets(command,sizeof(command),stdin);
  54.         command[strlen(command)-1]='\0';//清理掉最后的'\n'
  55.         //2.检测重定向
  56.         CheckRedir(command);
  57.         char* usercommand[1024]={NULL};
  58.         //3.解析command字符串,放入usercommand指针数组当中
  59.         GetCommand(command,usercommand);
  60.         //4.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
  61.         int flag=doBuildIn(usercommand);
  62.         //返回值!=0说明是内建命令,无需执行第4步
  63.         if(flag!=0) continue;
  64.         //5.创建子进程,交由子进程完成任务
  65.         Execute(usercommand);
  66.     }
  67.     return 0;
  68. }
复制代码
十.stderr的作用

起首先先容一下2>&1这一语法
1.先容2>&1

下面我们用fprintf来演示一下


假如我们如今就是想要把标准错误和标准输出都往显示器上打印呢?
  1. ./mycmd > log.txt 2>&1
复制代码

又由于我们先把1重定向到log.txt中,再把2重定向到1中
因此就做到把2和1中的内容全都往log.txt中打印了
2.stderr的作用

我们平常学习编程的时候,步调写的并不大
步调运行时的错误信息和正常信息我们都同一往显示器上打印了
但是一旦步调特别大,要打印的信息特别多,此时区分显示器上的正常信息和错误信息就很贫苦了
而区分正常信息和错误信息之后就能够方便我们对错误信息进行同一排查,提高效率
因此标准输出的作用是:接收打印的正常信息
标准错误的作用是接收打印的错误信息
3.演示

还是刚才那份代码
如今我们想把正常信息重定向到log.txt中
错误信息重定向到log.txt.error中
  1. ./mycmd 1>log.txt 2>log.txt.error
  2. 把1重定向给log.txt
  3. 把2重定向给log.txt.error
复制代码

注意:如许重定向时不能带空格
也就是说不能如许写:
  1. ./mycmd 1 > log.txt 2 > log.txt.error
复制代码

十一.重定向和步调替换之间是互不影响的

为什么它们之间是互不影响的呢?
由于步调替换时改变的是进程结构体当中的页表中虚拟地址空间和物理地址空间的映射和进程地址空间中的相干属性
而重定向改变的是文件形貌符表中fd的指向
两者互不影响
   以上就是Linux 文件系列:深入理解文件fd,重定向,自定义shell当中重定向的模拟实现的全部内容,盼望能对各人有所资助!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊雷无声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表