• linux 文件系统


    1. 理解文件描述符

    1.1 文件描述符的概念

           文件描述符是个很小的正整数,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。例如,每个进程启动时都会打开3个文件:标准输入、标准输出和标准错误文件。这3个文件分别对应于文件描述符0、1和2。

           提示:应该使用<unistd.h>中定义的3个宏来代替数字0、1或2:STDIN_FILENO、STDOU_FILENO和STDERR_FILENO,因为你的程序可能会在一个stdin、stdout和stderr不与整数0、1、2相对应的系统上进行编译。

           许多Linux和UNIX系统调用都依赖于文件描述符。比如,低级的open、close、read和write调用都使用文件描述符。在Linux上,几乎每一样东西都是一个文件,至少描象地看是这样。这一事实也是Linux最具独创性的设计特色之一,因为它让大量的资源,比如内存、串口、伪终端、打印端口、声卡、鼠标甚至其他运行着的进程有了统一的编程接口。

     

    2. 文件操作的相关系统调用

           Linux的文件操作系统调用涉及创建、打开、读写和关闭文件。

    2.1 创建——调用成功后返回一个文件描述符,或者如果失败,则设置errno变量并返回-1。

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <fcntl.h>

    int creat(const char *filename , mode_t mode);

           参数mode指定新建文件的存取权限,它同umask一起决定文件的最终权限(mode&umask),其中umask代表了文件在创建进需要去掉的一些存取权限。umask可通过系统调用umask()来改变,如下所示:

    int umask(int newmask);

     

    2.2 打开——调用成功后返回一个文件描述符,或者如果失败,则设置errno变量并返回-1。

    #include <sys/types.h>             提供类型pid_t的定义

    #include <sys/stat.h>

    #include <fcntl.h>

    int open(const char *pathname , int flags);

    int open(const char *pathname , int flags , mode_t mode);

           open()函数有两个形式,其中pathname是我们要打开的文件名(包含路径名称,默认时认为是在当前路径下面)。flags可以是下面的一个传开中者是几个值的组合,在fcntl.h文件中定义具体的值。

    /******************include/asm-generic/fcntl.h******************/

           标志                                 含义

           O_RDONLY                       以只读的方式打开文件

           O_WRONLY                      以只写的方式打开文件

           O_RDWR                          以读写的方式打开文件

           O_CREAT                          创建一个文件

           O_EXCL                            仅与O_CREAT连用,如果文件已经存在,则强制open失败

           O_NOCTTY                       如果打开的文件是一个终端,就不会成为打开其进程的控制终端

           O_TRUNC                         如果文件已经存在,则删除文件的内容,将其长度截为0

           O_NOBLOCK                    以非阻塞的方式打开一个文件

           O_APPEND                       将文件指针设置到文件的结束处(如果打开来写),以追加的方式打开文件

           O_SYNC                            在数据被物理地写入磁盘或其他设备之后操作才返回

          

           O_RDONLY、O_WRONLY、O_RDWR这3个标志只能使用任意的一个。

           如果使用了O_CREATE标志,则使用的函数是int open(const char *pathname , int flags , mode_t mode);这个时候我们还要指定mode标志,用来表示文件的访问权限。mode可以是下面的值组合。

    /******************include/linux/stat.h******************/

           标志                                 含义

           S_IRUSR                           用户可以读

           S_IWUSR                          用户可以写

           S_IXUSR                           用户可以执行

           S_IRWXU                          用户可以读、写、执行

           S_IRGRP                           组可以读

           S_IWGRP                          组可以写

           S_IXGRP                           组可以执行

           S_IRWXG                          组可以读、写、执行

           S_IROTH                           其他人可以读

           S_IWOTH                          其他人可以写

           S_IXOTH                           其他人可执行

           S_IRWXO                          其他人可以读、写、执行

           S_ISUID                            设置用户的执行ID

           S_ISGID                            设置组的执行ID

     

           除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以用数字来表示,Linux总共用了5个数字来表示文件的各种权限;第一位表示设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;第五位表示其他人的权限。每个数字可以取1(执行权限)、2(写权限)、4(读权限)、0(无)或者是这些值的和。

           例如,如果要创建一个用户可读、可写、可执行,但是组没有权限,其他人可以读、可以执行的文件,并设置用户ID位。那么,应该使用的模式是1(设置用户ID)、0(不设置组ID)、7(1+2+4,读、写、执行)、0(没有权限)、5(1+4,读、执行)即10705,如下所示:

    open("test" , O_CREAT , 10705);

    上述语句等介于:

    open("test" , O_CREAT , S_IRWXU | S_IROTH | S_IXOTH | S_ISUID);  

           如果文件打开成功,open函数会返回一个文件描述符,以后对该文件的所有操作就可以通过这个文件描述符进行操作来实现。

           以O_CREAT为标志的open函数实际上实现了文件的创建功能,因此,下面的函数等同creat()函数:

    int open(pathname , O_CREAT | OWRONLY | O_TRUNC , mode);

     

    2.3 读写——成功时,返回读写的字节大小,错误时返回-1,并设置errno变量。

           用于向文件描述符对应的文件写入数据,在文件打开之后,我们才可以对文件进行读写,Linux系统中提供文件读写的系统调用是read、write函数,如下所示:

    #include <unistd.h>

    ssize_t read(int fd , const void *buf , size_t length);

    ssize_t write(int fd , const void *buf , size_t length);

           其中参数fd是以前的open调用返回的有效文件描述符,参数buf为指向缓冲区的指针,length为缓冲区的大小(以字节为单位)。函数read()实现从文件描述符fd所指定的文件中读取length个字节到buf所向的缓冲区中,返回值为实际读取的字节数。函数write实现把length个字节从buf指向的缓冲区中写到文件描述符fd所指向的文件中,返回值为实际写入的字节数。

    /*

    // unistd.h

    #define STDIN_FILENO    0       // Standard input. 

    #define STDOUT_FILENO   1       // Standard output. 

    #define STDERR_FILENO   2       // Standard error output

     */

    #include <stdio.h>

    #include <unistd.h>

    int main(void)

    {

           int len;

           char buf[1024];

           while((len=read(STDIN_FILENO,buf,sizeof(buf)))>0)

           {

                  if(write(STDOUT_FILENO,buf,len)!=len)

                  {

                         perror("write! ");

                         return 1;

                  }

           }

           if(len<0)

           {

                  perror("read! ");

                  return 1;

           }

           return 0;

    }      

     

    2.4 定位

           对于随机文件,我们可以随机地指定位置读写,使用如下函数进行定位:

    #include <unistd.h>

    #include <sys/type.h>

    int lseek(int fd , offset_t offset , int whence);

           lseek()将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件头的位置。失败时,返回(offset)-1,并设置error参数,文件的偏移量不变。参数whence可以使用如下值:

    SEEK_SET:当前位置为文件开头,新位置为偏移量的大小。

    SEEK_CUR:当前位置为文件读写指针的位置,新位置为当前位置加上偏移量。

    SEEK_END:当前位置为文件末尾,新位置为文件的大小加上偏移量的大小。

           移动到文件的文件开头:

    lseek(fd , 0 , SEEK_SET);

           offset可取负值,例如下述调用可将文件指针相对当前位置向前移动5个字节:

    lseek(fd , -5 , SEEK_CUR);

           由于lseek函数的返回值为文件指针相对于文件头的位置,因此下列调用的返回值就是文件的长度:

    lseek(fd , 0 , SEEK_END);


    例程:读取文件test.dat的内容,并判断有字符simple几次
    /*name: lseek.test.c*/
    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <string.h> 
    #include <sys/stat.h>
    #include <fcntl.h>

    int main(int argc , char *argv[])
    {
     //len将用于保存输入要查询的字符串长度
     int len;
     //fd用于保存打开文件描述符
     int fd;
     //offset为文件偏移量
     long offset=0;
     //buffer用于存储读出的文件内容
     char buffer[1024];
     //flag用于统计查找到了几次相同字符
     int flag=0;
     //判断参数个数是否符合要求
     if(argc!=3){
      printf("Usage: %s "string" filename " , argv[0]);
      return 1;
     }
     //取得要查找的字符长度
     len=strlen(argv[1]);
     if((fd=open(argv[2],O_RDONLY))==-1){
      perror("Cannot open the desired file");
     return 1;
     }
     //不断读取文件内容,然后与给定的字符串比较,如果相同则将flag计数加1
     while(1){
      if(lseek(fd,offset,SEEK_SET)==-1){
       perror("Cannot move the file pointer");
       return 1;
      }
      if(read(fd,buffer,len)<len)
       break;
      buffer[len]='';
      if(strcmp(buffer,argv[1])==0)
       flag++;
      offset++;
     }
     //如果flag大于0,则证明查找到了该字符,输出结果
     if(flag>0)
      printf("Find the string: %s in the file: %s %d times ",argv[1],argv[2],flag);
     if(close(fd)==-1){
      perror("Cannot close the desired file");
      return 1;
     }
     return 0;
    }

    $ cat test.dat
    This is a simple file!
    abcdefghigklmnopqrstuvwxyz

    $gcc -o test lseek_test.c
    $./test "simple" test.dat
    Find the string: simple in the file: test.dat 1 times
            之前,由于open函数少写了一对括号,总出现错误:
    Cannot move the file pointer: Illegal seek

     

    2.5 ioctl——通常来说,成功操作是返回0,但极少数ioctl()请求使用返回值作为输入参数,并返回一个非负数。错误时,返回-1并设置errno变量。

    #include <sys/ioctl.h>

    int ioctl(int d , int requets, ...);

           用于设置或检索文件的多种有关参数并对文件进行一些其他的操作。参数d必须是一个打开的文件描述符,参数requets是一个依赖于设备的请求代码。第三个参数是一个无类型的指向内存的指针。

          

    2.6 关闭——close只有一个参数fd,即open返回的文件描述符。调用成功后返回0,或者如果失败,则设置errno变量并返回-1。

    #include <unistd.h>

    int close(int fd);

           当操作完成以后,就要关闭文件了,只要调用close函数就可以了, 其中fd是要关闭的文件描述符。

           注意,不检查close()的返回值是一个严重的编程错误,原因有二:首先,在网络文件系统中,例如NFS,close调用会因为网络延迟而失败。其次,许多系统都配置有写后缓冲(write-behind caching)的作用,这意味着既使write调用成功返回,操作系统也要等到一个更方便的时候执行实际的磁盘写入操作。正如close(2)手册上面所叙述的:“错误状态可能会在写入操作结束后的晚些时候才报告,但肯定会在关闭文件时报告。在关闭文件时不检查返回值可能导致在不知情的情况下丢失数据。”

           例程:编写一个程序,在当前目录下创建用户可读写的文件“hello.txt”,在其中写入"Hello World!",关闭该文件。再次打开该文件,读取其中的内容并输出在屏幕上。

    #include <unistd.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <fcntl.h>   //包含O_CREAT等定义

    #include <stdio.h>

    #include <stdlib.h>     //包含exit()

    #include <string.h> //add by fantity include strlen

     

    #define LENGTH 50

     

    int main()

    {

            int fd,len,size;

            char *buf="Hello! I'm writing to this file!";

            char str[LENGTH];

            len=strlen(buf);

            /*首先调用open函数创建文件,并指定相应的权限*/

            if((fd=open("hello.txt",O_CREAT | O_RDWR,S_IRUSR | S_IWUSR,0666))<0)

            {  

                    perror("open:");

                    exit(1);

            }  

            else

            {  

                    printf("open file: hello.txt %d ",fd);

            }  

     

            if((size=write(fd,buf,len))<0)

            {  

                    perror("write:");

                    exit(1);

            }  

            else

            {  

                    printf("write:%s ",buf);

            }

            if(close(fd)<0)

            {

                    perror("close:");

                    exit(1);

            }

            else

            {

                    printf("close file:hello.txt ");

            }

     

            if((fd=open("hello.txt",O_RDWR))<0)

            {

                    perror("open:");

                    exit(1);

            }

            else

            {

                    printf("open file:hello.txt again %d ",fd);

            }

     

            /*调用lseek函数,将文件指针调到文件起始,并读取其中的10个字符*/

            lseek(fd,0,SEEK_SET);

            if(read(fd,str,LENGTH)<0)

            {

                    perror("read: ");

                    exit(1);

            }

            else

            {

                    str[len]='';

                    printf("read from file:%s ",str);

            }

            if(close(fd)<0)

            {

                    perror("close:");

                    exit(1);

            }

            else

            {

                    printf("close file:hello.txt,again! ");

            }

            exit(0);

            //return 0;

    }

     

    #include <unistd.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <fcntl.h>   //包含O_CREAT等定义

    #include <stdio.h>

    #include <string.h> //add by fantity include strlen

    #define LENGTH 20

    int main()

    {

            int fd,len;

            char str[LENGTH];

     

            fd=open("hello.txt",O_CREAT | O_RDWR,S_IRUSR | S_IWUSR);

     

            if(fd)

            {

                    write(fd,"Hello World!",strlen("Hello World!"));

                    close(fd);

     

            }

     

            fd=open("hello.txt",O_RDWR);

            len=read(fd,str,LENGTH);

            str[len]='';

            printf("%s ",str);

            close(fd);

            return 0;

    }

     

    2.7 fcntl——处理文件的共享问题,调用成功后返回0,或者如果失败,则设置errno变量并返回-1。

    2.7.1 fcntl函数说明

           主要用于文件已经共享的时候如何时操作,也就是当多个用户共同使用、操作一个文件的情况,这时,Linux通常采用的方法是给文件上锁,来避免共享的资源产生竞争的状态。

           文件锁包括建议锁和强制锁。建议锁要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统不使用建议锁。强制锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。

           在Linux中,实现文件上锁的函数有flock和fcntl,其中flock用于对文件施加建议性锁,而fcntl不仅可以施加建议锁,还可以施加强制锁。同时,fcntl还能对文件的某一记录进行上锁,也就是记录锁。

           记录锁又可分为读取锁和写入锁,其中读取锁又称为共享锁,它能够使多个进程都能在文件的同一部分建立读取锁。而写入锁又称为排斥锁,在任何时刻只能有一个进程在文件的某个部分上建立写入锁。当然,在文件的同一部分不能同时建立读取锁和写入锁。

     

    2.7.2 fcntl函数格式

    #include <sys/types.h>

    #include <unistd.h>

    #include <fcntl.h>

    int fcntl(int fd , int cmd , struct flock *lock);

           fd为文件描述符;

           cmd可取的命令为:

    F_DUPFD:复制文件描述符

    F_GETFD:获得fd的close-on-exec标志,若标志未设置,则文件经过exec函数之后仍保持打开状态。

    F_SETFD:设置close-on-exec标志,该标志以参数arg的FD_CLOEXEC位决定。

    F_GETFL:得到open设置的标志

    F_SETFL:改变open设置的标志

    F_GETFK:根据lock描述,判断能否上文件锁:如果可以上锁,将lock域中的l_type改写为F_UNLCK,其它部分保持不变;如果不能上锁,则返回锁中关于 the l_type, l_whence, l_start, and l_len的细节,并将I_pid设置为持有锁的进程的PID。

    F_SETFK:设置lock描述的文件锁

    F_SETLKW:这是F_SETLK的阻塞版本(命令中的W表示等待wait)。如果存在其他锁,则调用进程睡眠;如果捕捉到信号则睡眠中断。

    F_GETOWN:检索将收到SIGIO和SIGURG信号的进程号或进程组号

    F_SETOWN:设置进程号或进程组号

           lock:结构为flock,设置记录锁的具体状态。

           这里lock的结构如下:

    struct flock

    {

           short l_type;

           off_t l_start;

           short l_whence;

           off_t l_len;

           pid_t l_pid;

    }

           lock结构中,每个变量的取值含义为:

           l_type:

    F_RDLCK:读取锁(共享锁)

    F_WRLCK:写入锁(排斥锁)

    F_UNLCK:解锁

           l_start:相对位移量

           l_whence:相对位移量的起点,同lseek中的whence

    SEEK_SET:当前位置为文件开头,新位置为偏移量的大小。

    SEEK_CUR:当前位置为文件读写指针的位置,新位置为当前位置加上偏移量。

    SEEK_END:当前位置为文件末尾,新位置为文件的大小加上偏移量的大小。

           小技巧:为加锁整个文件,通常的方法是将l_start设置为0,l_whence说明为SEEK_SET,l_len说明为0。

     

    2.7.3 fcntl使用实例

           下面首先给出了使用fcntl函数的文件记录锁函数。在该函数中,首先给flock结构体的对应位赋予相应的值。接着使用两次fcntl函数分别用于给相关文件上锁和判断文件是否可以上锁,这里用到的cmd的值分别是F_SETLK和F_GETLK。

    #include <unistd.h>

    #include <sys/file.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <stdio.h>

    #include <stdlib.h>

    /*lock_set函数 */

    void lock_set(int fd, int type)

    {

     struct flock lock;

     lock.l_whence=SEEK_SET;

     lock.l_start=0;

     lock.l_len=0;

     

     while(1)

     {

      lock.l_type=type;

     /*根据不同的type值给文件上锁或解锁*/

      if((fcntl(fd,F_SETLK,&lock))==0)

      {

       if(lock.l_type==F_RDLCK)

        printf("read lock set by %d ",getpid());

       else if(lock.l_type==F_WRLCK)

        printf("write lock set by %d ",getpid());

       else if(lock.l_type==F_UNLCK)

        printf("release lock by %d ",getpid());

       return;

      }

     /*判断文件是否可以上锁*/

      fcntl(fd,F_GETLK,&lock);

     /*判断文件不能上锁的原因*/

      if(lock.l_type!=F_UNLCK)

      {

       if(lock.l_type==F_RDLCK)

        printf("read lock already by %d ",lock.l_pid);

       else if (lock.l_type==F_WRLCK)

        printf("write lock already by %d ",lock.l_pid);

       getchar();

      }

     }

    }

     

    int main(void)

    {

     int fd;

     fd=open("hello.txt",O_RDWR|O_CREAT,0666);

     if(fd<0)

     {

      perror("open:");

      exit(1);

     }

     

     /*给文件写入锁*/

     //lock_set(fd,F_WRLCK);     /*选择其中一个*/

     lock_set(fd,F_RDLCK);

     getchar();

     

     /*给文件解锁*/

     lock_set(fd,F_UNLCK);

     getchar();

     if(close(fd)<0)

     {

      perror("close:");

      exit(1);

     }

     exit(0);

    }

    $ gcc fcntl_write.c -o fcntl_write

    运行分两种

    1). 在一个终端下运行:

    $ ./fcntl_write

    write lock set by 3490

    release lock by 3490

     

    2). 分别在两个终端下运行:

    $1 ./fcntl_write

    write lock set by 3490

    $2 ./fcntl_write

    write lock already by 3490

    $1 Enter

    release lock by 3490

    $2 Enter

    write lock set by 3493

    release lock by 3493

           由此可见,写入锁为互斥锁,同一时刻只能有一个写入锁存在。

           当使用读取锁时,再分别在两个终端下运行:

    $1 ./fcntl_write

    read lock set by 3623

    release lock by 3623      

    $2 ./fcntl_write

    read lock set by 3637

    release lock by 3637

     

    2.8 select——处理I/O复用的情况,

    2.8.1 函数说明

           总的来说,I/O处理的模型有5种:

    *      阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能就会使进程挂起,直到相关数据到才会出错返回。如常见对管道设备、终端设备和网络设备进行读写时经常会出现这种情况。

    *      非阻塞模型:在这种模型下,当请求的I/O操作不能完成时,则不让进程睡眠,而且返回一个错误。非阻塞I/O用户可以调用不会永远阻塞的I/O操作,如open、write和read。如果操作不能完成,则会立即出错返回,且表示I/O如果该操作继续执行就会阻塞。

    *      I/O多路转接模型:在这种模型下,如果请求的I/O操作阻塞,且它不是真正的阻塞I/O

     

     

     

     

    3. C库函数的文件操作

           C库函数的文件操作实际上是独立于具体的操作系统平台的,不管是在DOS、Windows、Linux还是在VxWorks中都是这些函数。

    3.1 创建和打开——fopen返回一个文件指针,该指针可以传递给别的标准I/O函数来标识这个流。文件指针指向一个描述流的结构。在出现错误的情况下,fopen返回NULL并把errno变量设为恰当的值。如果用fopen打开供写入数据的文件不存在,那么它会以权限0666来创建该文件,这和用进程的umask来设置权限的情况类似。

    #include <stdio.h>

    FILE *fopen(const char *path , const char *mode);

           fopen()实现打开指定文件filename,FILE * 标识出一个文件指针,其中的mode为打开模式,C库函数中支持的打开模式如下所示:

           标志                                 含义

           r、rb                               以只读方式打开

           w、wb                            以只写方式打开,如果文件不存在,则创建文件,否则文件被截断。

           a、ab                              以追加方式打开。如果文件不存在,则创建该文件。

           r+、r+b、rb+                 以读写方式打开

           w+、w+b、wh+             以读写方式打开。如果文件不存在,则创建新文件,否则文件被截断。

           a+、a+b、ab+               以读和追加方式打开。如果文件不存在。则创建新文件。

           其中b用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux系统不区分二进制文件和文本文件。

     

    3.2 读写

           C库函数支持以字符、字符串等为单位,支持按照某种格式进行文件的读写,这一组函数为:

    #include <stdio.h>

    int fgetc(FILE *stream);

    int fputc(int c , FILE *stream);

    char *fgets(char *s , int n , FILE *stream);

    int fputs(const char *s , FILE *stream);

    int fprintf(FILE *stream , const char *format , ...);

    int fscanf(FILE *stream , const char *format , ... );

    size_t fread(void *ptr , size_t size , size_t n , FILE *stream);

    size_t fwrite(const void *ptr , size_t size , size_t n FILE *stream);

           fread()实现从stream中读取n个字段,每个字段为size个字节,并将读取的字段放放ptr所指的字符数组中,返回实际已读取的字段数。在读取的字段数小于num时,可能是在函数调用时出现错误,也可能是读到了文件的结尾。所以要通过调用feof()和ferror()来判断。

           fwrite()实现从缓冲ptr所指的数组中把n个字段写到stream中,每个字段长为size个字节,返回实际写入的字段数。

           另个,C库函数还提供了读写过程中的定位能力,这些函数包括:

    int fgetpos(FILE *stream , fpos_t *pos);

    int fsetpos(FILE *stream , const fpos_t *pos);

    int fseek(FILE *stream , long offset , int whence);

     

    3.3 关闭

           利用C库函数关闭文件依然是很简单的操作,如下所示:

    int fclose(FILE *stream);

           例程:将上面的例子用C库函数来实现,如下:

    #include <stdio.h>

    #define LENGTH 20

    int main()

    {

            FILE *fd;

            char str[LENGTH];

       

            fd = fopen("hello.txt","w+");

            if(fd)

            {

                    fputs("Hello World!",fd);

                    fclose(fd);

            }

     

            fd=fopen("hello.txt","r");

            fgets(str,LENGTH,fd);

            printf("%s ",str);

            fclose(fd);

            return 0;

    }

     

    4. 库函数和系统调用的区别在于系统调用能够让你直接访问Linux内核提供的丰富服务,比如基于文件描述符的I/O操作。可以把系统调用看作是内核的低级接口。另一方面,库调用处于Linux的编程接口中较高的层次。实际上,许多库函数都是用系统调用来实现的。库函数与系统调用之间的第二个关键区别在系统调用存在于内核空间,而大多数的库调用都是用模式的例程

  • 相关阅读:
    Floppy Disk Driver Primer
    王少川: 现阶段 我国没必要开发自己的操作系统
    Why does DOS use 100% CPU under Virtual PC?
    “情感计算”的危机与哲学错误
    [转载] 国产OS? 中文CPU?
    理想与现实的关系思考
    在Virtual PC 中安 装ms dos 6.22 的方法
    How Microsoft Lost the API War.
    svn 功能概览
    as3里的regex不需要转义
  • 原文地址:https://www.cnblogs.com/fanzhongxing/p/3339367.html
Copyright © 2020-2023  润新知