[转]简直不要太硬了!一文带你彻底理解文件系统(一)
感谢作者,原文链接:https://mp.weixin.qq.com/s/jSG6qLcXWuJTvo1ROBLDtA
这是Java建设者第79篇原创长文
文章图片
所有的应用程序都需要存储和检索信息。进程运行时,它能够在自己的存储空间内存储一定量的信息。然而,存储容量受虚拟地址空间大小的限制。对于一些应用程序来说,存储空间的大小是充足的,但是对于其他一些应用程序,比如航空订票系统、银行系统、企业记账系统来说,这些容量又显得太小了。
第二个问题是,当进程终止时信息会丢失。对于一些应用程序(例如数据库),信息会长久保留。在这些进程终止时,相关的信息应该保留下来,是不能丢失的。甚至这些应用程序崩溃后,信息也应该保留下来。
第三个问题是,通常需要很多进程在同一时刻访问这些信息。解决这种问题的方式是把这些信息单独保留在各自的进程中。
因此,对于长久存储的信息我们有三个基本需求:
必须要有可能存储的大量的信息
信息必须能够在进程终止时保留
必须能够使多个进程同时访问有关信息
磁盘(Magnetic disk)一直是用来长久保存信息的设备。近些年来,固态硬盘逐渐流行起来。
文章图片
固态硬盘不仅没有易损坏的移动部件,而且能够提供快速的随机访问。相比而言,虽然磁带和光盘也被广泛使用,但是它们的性能相对较差,通常应用于备份。我们会在后面探讨磁盘,现在姑且把磁盘当作一种大小固定块的线性序列好了,并且支持如下操作
读块 k
写块 k
文章图片
事实上磁盘支持更多的操作,但是只要有了读写操作,原则上就能够解决长期存储的问题。
然而,磁盘还有一些不便于实现的操作,特别是在有很多程序或者多用户使用的大型系统上(如服务器)。在这种情况下,很容易产生一些问题,例如
你如何找到这些信息?
你如何保证一个用户不会读取另外一个用户的数据?
你怎么知道哪些块是空闲的?等等问题
我们可以针对这些问题提出一个新的抽象 -文件。进程和线程的抽象、地址空间和文件都是操作系统的重要概念。如果你能真正深入了解这三个概念,那么你就走上了成为操作系统专家的道路。
文件(Files)是由进程创建的逻辑信息单元。一个磁盘会包含几千甚至几百万个文件,每个文件是独立于其他文件的。事实上,如果你能把每个文件都看作一个独立的地址空间,那么你就可以真正理解文件的概念了。
进程能够读取已经存在的文件,并在需要时重新创建他们。存储在文件中的信息必须是持久的,这也就是说,不会因为进程的创建和终止而受影响。一个文件只能在当用户明确删除的时候才能消失。尽管读取和写入都是最基本的操作,但还有许多其他操作,我们将在下面介绍其中的一些。
文件由操作系统进行管理,有关文件的构造、命名、访问、使用、保护、实现和管理方式都是操作系统设计的主要内容。从总体上看,操作系统中处理文件的部分称为文件系统(file system),这就是我们所讨论的。
从用户角度来说,用户通常会关心文件是由什么组成的,如何给文件进行命名,如何保护文件,以及可以对文件进行哪些操作等等。尽管是用链表还是用位图记录内存空闲区并不是用户所关心的主题,而这些对系统设计人员来说至关重要。下面我们就来探讨一下这些主题
1
文件
1.1
文件命名
文件是一种抽象机制,它提供了一种方式用来存储信息以及在后面进行读取。可能任何一种机制最重要的特性就是管理对象的命名方式。在创建一个文件后,它会给文件一个命名。当进程终止时,文件会继续存在,并且其他进程可以使用名称访问该文件。
文件命名规则对于不同的操作系统来说是不一样的,但是所有现代操作系统都允许使用 1 - 8 个字母的字符串作为合法文件名。
某些文件区分大小写字母,而大多数则不区分。UNIX属于第一类;历史悠久的MS-DOS属于第二类(顺便说一句,尽管 MS-DOS 历史悠久,但 MS-DOS 仍在嵌入式系统中非常广泛地使用,因此它绝不是过时的);因此,UNIX 系统会有三种不同的命名文件:maria、Maria、MARIA。在 MS-DOS ,所有这些命名都属于相同的文件。
文章图片
这里可能需要在文件系统上预留一个位置。Windows 95 和 Windows 98 都使用了 MS-DOS 文件系统,叫做FAT-16,因此继承了它的一些特征,例如有关文件名的构造方法。Windows 98 引入了对 FAT-16 的一些扩展,从而导致了FAT-32的生成,但是这两者很相似。另外,Windows NT,Windows 2000,Windows XP,Windows Vista,Windows 7 和 Windows 8 都支持FAT文件系统,这种文件系统有些过时。然而,这些较新的操作系统还具有更高级的本机文件系统(NTFS),有不同的特性,那就是基于Unicode编码的文件名。事实上,Windows 8 还配备了另一种文件系统,简称ReFS(Resilient File System),但这个文件系统一般应用于 Windows 8 的服务器版本。下面除非我们特殊声明,否则我们在提到 MS-DOS 和 FAT 文件系统的时候,所指的就是 Windows 的 FAT-16 和 FAT-32。这里要说一下,有一种类似 FAT 的新型文件系统,叫做exFAT。它是微软公司对闪存和大文件系统开发的一种优化的 FAT 32 扩展版本。ExFAT 是现在微软唯一能够满足OS X读写操作的文件系统。
许多操作系统支持两部分的文件名,它们之间用.分隔开,比如文件名prog.c。原点后面的文件称为文件扩展名(file extension),文件扩展名通常表示文件的一些信息。例如在 MS-DOS 中,文件名是 1 - 8 个字符,加上 1 - 3 个字符的可选扩展名组成。在 UNIX 中,如果有扩展名,那么扩展名的长度将由用户来决定,一个文件甚至可以包括两个或更多的扩展名,例如homepage.html.zip,html 表示一个 web 网页而 .zip 表示文件homepage.html已经采用 zip 程序压缩完成。一些常用的文件扩展名以及含义如下图所示
扩展名含义
bak备份文件
cc 源程序文件
gif符合图形交换格式的图像文件
hlp帮助文件
htmlWWW 超文本标记语言文档
jpg符合 JPEG 编码标准的静态图片
mp3符合 MP3 音频编码格式的音乐文件
mpg符合 MPEG 编码标准的电影
o目标文件(编译器输出格式,尚未链接)
pdfpdf 格式的文件
psPostScript 文件
tex为 TEX 格式化程序准备的输入文件
txt文本文件
zip压缩文件
在 UNIX 系统中,文件扩展名只是一种约定,操作系统并不强制采用。
名为file.txt的文件是文本文件,这个文件名更多的是提醒所有者,而不是给计算机传递信息。但是另一方面,C 编译器可能要求它编译的文件以.c结尾,否则它会拒绝编译。然而,操作系统并不关心这一点。
对于可以处理多种类型的程序,约定就显得及其有用。例如 C 编译器可以编译、链接多种文件,包括 C 文件和汇编语言文件。这时扩展名就很有必要,编译器利用它们区分哪些是 C 文件,哪些是汇编文件,哪些是其他文件。因此,扩展名对于编译器判断哪些是 C 文件,哪些是汇编文件以及哪些是其他文件变得至关重要。
与 UNIX 相反,Windows 就会关注扩展名并对扩展名赋予了新的含义。用户(或进程)可以在操作系统中注册扩展名,并且规定哪个程序能够拥有扩展名。当用户双击某个文件名时,拥有该文件名的程序就启动并运行文件。例如,双击 file.docx 启动了 Word 程序,并以 file.docx 作为初始文件。
1.2
文件结构
文件的构造有多种方式。下图列出了常用的三种构造方式
文章图片
上图中的 a 是一种无结构的字节序列,操作系统不关心序列的内容是什么,操作系统能看到的就是字节(bytes)。其文件内容的任何含义只在用户程序中进行解释。UNIX 和 Windows 都采用这种办法。
把文件看成字节序列提供了最大的灵活性。用户程序可以向文件中写任何内容,并且可以通过任何方便的形式命名。操作系统不会为为用户写入内容提供帮助,当然也不会干扰阻塞你。对于想做特殊操作的用户来说,后者是十分重要的。所有的 UNIX 版本(包括 Linux 和 OS X)和 Windows 都使用这种文件模型。
图 b 表示在文件结构上的第一步改进。在这个模型中,文件是具有固定长度记录的序列,每个记录都有其内部结构。把文件作为记录序列的核心思想是:读操作返回一个记录,而写操作重写或者追加一个记录。第三种文件结构如上图 c 所示。在这种组织结构中,文件由一颗记录树构成,记录树的长度不一定相同,每个记录树都在记录中的固定位置包含一个key字段。这棵树按 key 进行排序,从而可以对特定的 key 进行快速查找。
在记录树的结构中,可以取出下一个记录,但是最关键的还是根据 key 搜索指定的记录。如上图 c 所示,用户可以读出指定的pony记录,而不必关心记录在文件中的确切位置。用户也可以在文件中添加新的记录。但是用户不能决定添加到何处位置,添加到何处位置是由操作系统决定的。
1.3
文件类型
很多操作系统支持多种文件类型。例如,UNIX(同样包括 OS X)和 Windows 都具有常规的文件和目录。除此之外,UNIX 还具有字符特殊文件(character special file)和块特殊文件(block special file)。常规文件(Regular files)是包含有用户信息的文件。用户一般使用的文件大都是常规文件,常规文件一般包括可执行文件、文本文件、图像文件,从常规文件读取数据或将数据写入时,内核会根据文件系统的规则执行操作,写入可能被延迟,记录日志或者接受其他操作。
字符特殊文件和输入/输出有关,用于串行 I/O 类设备,如终端、打印机、网络等。块特殊文件用于磁盘类设备。我们主要讨论的是常规文件。
常规文件一般分为ASCII码文件或者二进制文件。ASCII 码文件由文本组成。在一些系统中,每行都会用回车符结束(ASCII 码是 13,控制字符 CR,转义字符\r。),另外一些则会使用换行符(ASCII 码是 10,控制字符 LF,转义字符\n)。一些系统(比如 Windows)两者都会使用。
ASCII 文件的优点在于显示和打印,还可以用任何文本编辑器进行编辑。进一步来说,如果许多应用程序使用 ASCII 码作为输入和输出,那么很容易就能够把多个程序连接起来,一个程序的输出可能是另一个程序的输入,就像管道一样。
文章图片
其他与 ASCII 不同的是二进制文件。打印出来的二进制文件是无法理解的。下面是一个二进制文件的格式,它取自早期的 UNIX 。尽管从技术上来看这个文件只是字节序列,但是操作系统只有在文件格式正确的情况下才会执行。
文章图片
这个文件有五个段:文件头、正文、数据、重定位位和符号表。文件头以魔数(magic number)为开始,表明这个文件是一个可执行文件(以防止意外执行非此格式的文件)。然后是文件各个部分的大小,开始执行的标志以及一些标志位。程序本身的正文和数据在文件头后面,他们被加载到内存中或者重定位会根据重定位位进行判断。符号表则用于调试。
二进制文件的另外一种形式是存档文件,它由已编译但没有链接的库过程(模块)组合而成。每个文件都以模块头开始,其中记录了名称、创建日期、所有者、保护码和文件大小。和可执行文件一样,模块头也都是二进制数,将它们复制到打印机将会产生乱码。
所有的操作系统必须至少能够识别一种文件类型:它自己的可执行文件。以前的 TOPS-20 系统(用于 DECsystem 20)甚至要检查要执行的任何文件的创建时间,为了定位资源文件来检查自动文件创建后是否被修改过。如果被修改过了,那么就会自动编译文件。在 UNIX 中,就是在 shell 中嵌入make程序。此时操作系统要求用户必须采用固定的文件扩展名,从而确定哪个源程序生成哪个二进制文件。
什么是 make 程序?在软件发展过程中,make 程序是一个自动编译的工具,它通过读取称为Makefiles的文件来自动从源代码构建可执行程序和库,该文件指定了如何导出目标程序。尽管集成开发环境和特定语言的编译器功能也可以用于管理构建过程,但 Make 仍被广泛使用,尤其是在 Unix 和类似 Unix 的操作系统中使用。
当程序从文件中读写数据时,请求会转到内核处理程序(kernel driver)。如果文件是常规文件,则数据由文件系统驱动程序处理,并且通常存储在磁盘或其他存储介质上的某块区域中,从文件中读取的数据就是之前在该位置写入的数据。
当数据读取或写入到设备文件时,请求会被设备驱动程序处理。每个设备文件都有一个关联的编号,该编号标示要使用的设备驱动程序。设备处理数据的工作是它自己的事儿。
块设备也叫做块特殊文件,它的行为通常与普通文件相似:它们是字节数组,并且在给定位置读取的值是最后写入该位置的值。来自块设备的数据可以缓存在内存中,并从缓存中读取;写入可以被缓冲。块设备通常是可搜索的,块设备的概念是,相应的硬件可以一次读取或者写入整个块,例如磁盘上的一个扇区
字符设备也称为字符特殊文件,它的行为类似于管道、串行端口。将字节写入字符设备可能会导致它在屏幕上显示,在串行端口上输出,转换为声音。
目录(Directories)是管理文件系统结构的系统文件。它是用于在计算机上存储文件的位置。目录位于分层文件系统中,例如 Linux,MS-DOS 和 UNIX。
文章图片
它显示所有本地和子目录(例如,cdn 目录中的 big 目录)。当前目录是 C 盘驱动器的根目录。之所以称为根目录,是因为该目录下没有任何内容,而其他目录都在该目录下分支。
1.4
文件访问
早期的操作系统只有一种访问方式:序列访问(sequential access)。在这些系统中,进程可以按照顺序读取所有的字节或文件中的记录,但是不能跳过并乱序执行它们。顺序访问文件是可以返回到起点的,需要时可以多次读取该文件。当存储介质是磁带而不是磁盘时,顺序访问文件很方便。
在使用磁盘来存储文件时,可以不按照顺序读取文件中的字节或者记录,或者按照关键字而不是位置来访问记录。这种能够以任意次序进行读取的称为随机访问文件(random access file)。许多应用程序都需要这种方式。
随机访问文件对许多应用程序来说都必不可少,例如,数据库系统。如果乘客打电话预定某航班机票,订票程序必须能够直接访问航班记录,而不必先读取其他航班的成千上万条记录。
有两种方法可以表示从何处开始读取文件。第一种方法是直接使用read从头开始读取。另一种是用一个特殊的seek操作设置当前位置,在 seek 操作后,从这个当前位置顺序地开始读文件。UNIX 和 Windows 使用的是后面一种方式。
1.5
文件属性
文件包括文件名和数据。除此之外,所有的操作系统还会保存其他与文件相关的信息,如文件创建的日期和时间、文件大小。我们可以称这些为文件的属性(attributes)。有些人也喜欢把它们称作元数据(metadata)。文件的属性在不同的系统中差别很大。文件的属性只有两种状态:设置(set)和清除(clear)。下面是一些常用的属性
属性含义
保护谁可以访问文件、以什么方式存取文件
密码(口令)访问文件所需要的密码(口令)
创建者创建文件者的 ID
所有者当前所有者
只读标志0 表示读/写,1 表示只读
隐藏标志0 表示正常,1 表示不再列表中显示
系统标志0 表示普通文件,1 表示系统文件
存档标志0 表示已经备份,1 表示需要备份
ASCII / 二进制标志0 表示 ASCII 文件,1 表示二进制文件
随机访问标志0 表示只允许顺序访问,1 表示随机访问
临时标志0 表示正常,1 表示进程退出时删除该文件
加锁标志0 表示未加锁,1 表示加锁
记录长度一个记录中的字节数
键的位置每个记录中的键的偏移量
键的长度键字段的字节数
创建时间创建文件的日期和时间
最后一次存取时间上一次访问文件的日期和时间
最后一次修改时间上一次修改文件的日期和时间
当前大小文件的字节数
最大长度文件可能增长到的字节数
没有一个系统能够同时具有上面所有的属性,但每个属性都在某个系统中采用。
前面四个属性(保护,口令,创建者,所有者)与文件保护有关,它们指出了谁可以访问这个文件,谁不能访问这个文件。
保护(File Protection):用于保护计算机上有价值数据的方法。文件保护是通过密码保护文件或者仅仅向特定用户或组提供权限来实现。
在一些系统中,用户必须给出口令才能访问文件。标志(flags)是一些位或者短属性能够控制或者允许特定属性。
隐藏文件位(hidden flag)表示该文件不在文件列表中出现。
存档标志位(archive flag)用于记录文件是否备份过,由备份程序清除该标志位;若文件被修改,操作系统则设置该标志位。用这种方法,备份程序可以知道哪些文件需要备份。
临时标志位(temporary flag)允许文件被标记为是否允许自动删除当进程终止时。
记录长度(record-length)、键的位置(key-position)和键的长度(key-length)等字段只能出现在用关键字查找记录的文件中。它们提供了查找关键字所需要的信息。
不同的时间字段记录了文件的创建时间、最近一次访问时间以及最后一次修改时间,它们的作用不同。例如,目标文件生成后被修改的源文件需要重新编译生成目标文件。这些字段提供了必要的信息。
当前大小字段指出了当前的文件大小,一些旧的大型机操作系统要求在创建文件时指定文件最大值,以便让操作系统提前保留最大存储值。但是一些服务器和个人计算机却不用设置此功能。
1.6
文件操作
使用文件的目的是用来存储信息并方便以后的检索。对于存储和检索,不同的系统提供了不同的操作。以下是与文件有关的最常用的一些系统调用:
Create,创建不包含任何数据的文件。调用的目的是表示文件即将建立,并对文件设置一些属性。
Delete,当文件不再需要,必须删除它以释放内存空间。为此总会有一个系统调用来删除文件。
Open,在使用文件之前,必须先打开文件。这个调用的目的是允许系统将属性和磁盘地址列表保存到主存中,用来以后的快速访问。
Close,当所有进程完成时,属性和磁盘地址不再需要,因此应关闭文件以释放表空间。很多系统限制进程打开文件的个数,以此达到鼓励用户关闭不再使用的文件。磁盘以块为单位写入,关闭文件时会强制写入最后一块,即使这个块空间内部还不满。
Read,数据从文件中读取。通常情况下,读取的数据来自文件的当前位置。调用者必须指定需要读取多少数据,并且提供存放这些数据的缓冲区。
Write,向文件写数据,写操作一般也是从文件的当前位置开始进行。如果当前位置是文件的末尾,则会直接追加进行写入。如果当前位置在文件中,则现有数据被覆盖,并且永远消失。
append,使用 append 只能向文件末尾添加数据。
seek,对于随机访问的文件,要指定从何处开始获取数据。通常的方法是用 seek 系统调用把当前位置指针指向文件中的特定位置。seek 调用结束后,就可以从指定位置开始读写数据了。
get attributes,进程运行时通常需要读取文件属性。
set attributes,用户可以自己设置一些文件属性,甚至是在文件创建之后,实现该功能的是 set attributes 系统调用。
rename,用户可以自己更改已有文件的名字,rename 系统调用用于这一目的。
2
目录
文件系统通常提供目录(directories)或者文件夹(folders)用于记录文件的位置,在很多系统中目录本身也是文件,下面我们会讨论关于文件,他们的组织形式、属性和可以对文件进行的操作。
2.1
一级目录系统
目录系统最简单的形式是有一个能够包含所有文件的目录。这种目录被称为根目录(root directory),由于根目录的唯一性,所以其名称并不重要。在最早期的个人计算机中,这种系统很常见,部分原因是因为只有一个用户。下面是一个单层目录系统的例子
文章图片
该目录中有四个文件。这种设计的优点在于简单,并且能够快速定位文件,毕竟只有一个地方可以检索。这种目录组织形式现在一般用于简单的嵌入式设备(如数码相机和某些便携式音乐播放器)上使用。
2.2
层次目录系统
对于简单的应用而言,一般都用单层目录方式,但是这种组织形式并不适合于现代计算机,因为现代计算机含有成千上万个文件和文件夹。如果都放在根目录下,查找起来会非常困难。为了解决这一问题,出现了层次目录系统(Hierarchical Directory Systems),也称为目录树。通过这种方式,可以用很多目录把文件进行分组。进而,如果多个用户共享同一个文件服务器,比如公司的网络系统,每个用户可以为自己的目录树拥有自己的私人根目录。这种方式的组织结构如下
文章图片
根目录含有目录 A、B 和 C ,分别属于不同的用户,其中两个用户个字创建了子目录。用户可以创建任意数量的子目录,现代文件系统都是按照这种方式组织的。
【[转]简直不要太硬了!一文带你彻底理解文件系统(一)】2.3
路径名
当目录树组织文件系统时,需要有某种方法指明文件名。常用的方法有两种,第一种方式是每个文件都会用一个绝对路径名(absolute path name),它由根目录到文件的路径组成。举个例子,/usr/ast/mailbox意味着根目录包含一个子目录usr,usr 下面包含了一个mailbox。绝对路径名总是以/开头,并且是唯一的。在 UNIX 中,路径的组件由/分隔。在 Windows 中,分隔符为\。在 MULTICS 中,它是>。因此,在这三个系统中,相同的路径名将被编写如下
Windows \usr\ast\mailbox UNIX /usr/ast/mailbox MULTICS >usr>ast>mailbox
不论使用哪种方式,如果路径名的第一个字符是分隔符,那就是绝对路径。
另外一种指定文件名的方法是相对路径名(relative path name)。它常常和工作目录(working directory)(也称作当前目录(current directory))一起使用。用户可以指定一个目录作为当前工作目录。例如,如果当前目录是/usr/ast,那么绝对路径/usr/ast/mailbox可以直接使用mailbox来引用。也就是说,如果工作目录是/usr/ast,则 UNIX 命令
cp /usr/ast/mailbox/usr/ast/mailbox.bak
和命令
cp mailbox mailbox.bak
具有相同的含义。相对路径通常情况下更加方便和简洁。而它实现的功能和绝对路径安全相同。
一些程序需要访问某个特定的文件而不必关心当前的工作目录是什么。在这种情况下,应该使用绝对路径名。
支持层次目录结构的大多数操作系统在每个目录中有两个特殊的目录项.和..,读作dot和dotdot。dot 指的是当前目录,dotdot 指的是其父目录(在根目录中例外,在根目录中指向自己)。可以参考下面的进程树来查看如何使用。
文章图片
一个进程的工作目录是/usr/ast,它可采用..沿树向上,例如,可用命令
cp ../lib/dictionary .
把文件usr/lib/dictionary复制到自己的目录下,第一个路径告诉系统向上找(到 usr 目录),然后向下到lib目录,找到 dictionary 文件
第二个参数.指定当前的工作目录,当 cp 命令用目录名作为最后一个参数时,则把全部的文件复制到该目录中。当然,对于上述复制,键入
cp /usr/lib/dictionary .
是更常用的方法。用户这里采用.可以避免键入两次 dictionary 。无论如何,键入
cp /usr/lib/dictionary dictionary
也可正常工作,就像键入
cp /usr/lib/dictionary /usr/lib/dictionary
一样。所有这些命令都能够完成同样的工作。
2.4
目录操作
不同文件中管理目录的系统调用的差别比管理文件的系统调用差别大。为了了解这些系统调用有哪些以及它们怎样工作,下面给出一个例子(取自 UNIX)。
Create,创建目录,除了目录项.和..外,目录内容为空。
Delete,删除目录,只有空目录可以删除。只包含.和..的目录被认为是空目录,这两个目录项通常不能删除
opendir,目录内容可被读取。例如,未列出目录中的全部文件,程序必须先打开该目录,然后读其中全部文件的文件名。与打开和读文件相同,在读目录前,必须先打开文件。
closedir,读目录结束后,应该关闭目录用于释放内部表空间。
readdir,系统调用 readdir 返回打开目录的下一个目录项。以前也采用 read 系统调用来读取目录,但是这种方法有一个缺点:程序员必须了解和处理目录的内部结构。相反,不论采用哪一种目录结构,readdir 总是以标准格式返回一个目录项。
rename,在很多方面目录和文件都相似。文件可以更换名称,目录也可以。
link,链接技术允许在多个目录中出现同一个文件。这个系统调用指定一个存在的文件和一个路径名,并建立从该文件到路径所指名字的链接。这样,可以在多个目录中出现同一个文件。有时也被称为硬链接(hard link)。
unlink,删除目录项。如果被解除链接的文件只出现在一个目录中,则将它从文件中删除。如果它出现在多个目录中,则只删除指定路径名的链接,依然保留其他路径名的链接。在 UNIX 中,用于删除文件的系统调用就是 unlink。
推荐阅读
- 这辈子我们都不要再联系了
- 流转
- 一起来学习C语言的字符串转换函数
- 【58】转移注意力
- 考前焦虑——接纳情绪,转移注意力
- leetcode|leetcode 92. 反转链表 II
- 视频转换器哪种好用()
- 2018年7月11日|2018年7月11日 星期三 多云转晴(18)
- 以太坊中的计量单位及相互转换
- 不要和满身负能量的人交朋友