【Linux】基础I/O——理解ext2文件系统

掘根 2024-07-20 15:37:01 阅读 94

我们到现在为止讲的都是打开的文件现在我们讲讲没有打开的文件

如果一个文件没有被打开,那它就是在磁盘中被存储的,我们就要关心路径问题,存储问题,文件获取问题,那么操作系统是怎么处理这些问题的?不急,我们等会就讲

我们先回答几个问题

文件=内容+属性     也就是说,磁盘上存储一个文件的过程==存文件内容+存文件属性

上面显示的内容叫文件的属性

上面显示的是文件的内容

 在Linux中我们存文件内容采用块来存储,文件属性则是采用inode来存储

也就是说Linux文件在磁盘中存储,是将属性和内容分开存储的

1.磁盘/机械硬盘的物理构成

大家也没有见过磁盘?

        最近5年我们笔记本电脑一般使用SSD——固态硬盘,很少见到磁盘了。磁盘在十几年前很常见,它是电脑上唯一的机械部件,后来随着闪存技术的发展,就慢慢淡出大众的视野了。

不过磁盘有超大容量,成本也低,也比较稳定,磁盘这个东西在企业用的非常广泛,今后基本不会淘汰。

所以我们好好的学习一下磁盘

目前,磁盘是某些计算机(所有的服务器/十几年前的笔记本电脑/现在部分的台式机)的唯一的机械设备在冯诺依曼结构体系中,磁盘也是外设,它速度很慢

我们来见见机械磁盘/磁盘,在那之前,我们先来看看光盘

 光盘只有一面是可读的,另外一面就打印的什么广告,就像下面一样

其实我们把磁盘拆开来,里面有一个和光盘特别像的东西 

 这个跟光盘类似的叫盘片/磁盘,盘片两个面都能存储数据,盘片越多,存储的东西越多

硬盘其实是由许许多多的圆形碟片、机械手臂、磁头与主轴马达所组成的,

实际的数据都是写在具有磁性物质的碟片上面,而读写主要是通过在机械手臂上的磁头(head)来完成。

        实际运行时,主轴马达让碟片转动,然后机械手臂可伸展让磁头在碟片上面进行读写的操作。另外,由于单一碟片的容量有限,因此有的硬盘内部会有两个以上的碟片。

这个磁头是一面一个的,一片盘片对应2个磁头

注意: 磁头并非与盘面进行直接接触,而是以 <code>15 纳米的超低距离进行磁场更改

         这个距离相当于一架波音747距离地面1米进行超低空飞行,所以如果磁头制作工艺不够精湛,可能会导致磁头在写入/读取数据时,与盘面发生摩擦(高速旋转)发热,从而导致磁场消失,数据丢失

机械硬盘 不能在其运行时随意移动,因为角度的偏转也有可能导致发生摩擦,造成数据丢失,更不能用力拍打 机械磁盘

所以搭载了这磁盘的东西不能随便搬动,否则就数据损坏,这个就比较适合不移动的应用场景

        这个磁盘不能有灰尘,所以机械硬盘一旦拆开来就报废了

        计算机只认识二进制,所有的设备都认识二进制,磁盘也不例外,这个盘面也是,我们把这个盘面叫永久性存储介质

接下来我们来讲一下一个小故事就知道怎么存储这个

吸铁石有南极和北极之分,我们把北极称为1,南极是0,我们通电让南北极互换

磁盘可看作是由无数个吸铁石构成,磁头通过电磁特性来把磁盘的南北极逆置

我们怎么销毁数据?

加热磁盘?软件擦除才对!!!

2.盘面的构成

一个盘面=n条磁道(图中有8条)一条磁道=n个扇区(也叫磁盘块)每个扇区存储的数据量一样大

也就是说每个盘片被划分为一个个磁道,每个磁道又划分为一个个扇区。

        我们知道同心圆外圈的圆比较大,占用的面积比内圈多。所以,为了合理利用这些空间,外围的圆会具有更多的扇区。

此外,当碟片转一圈时,外圈的扇区数量比较多,因此如果数据写入在外圈,转一圈能够读写的数据量当然比内圈还要多。

        因此通常数据的读写会由外圈开始往内写,这是默认方式。

磁盘被访问的最基本单元是扇区,什么意思呢?

之前硬盘的扇区都是设计成512字节的大小,但目前绝大部分的高容量硬盘已经使用了4KB大小的扇区设计。现在我们就算是只修改某扇区里面的1个字节的内容,也要把整个扇区加载到内存里

我们怎么定位扇区?

首先我们得定位是磁片的哪一面,也就是要访问哪个磁头(一个磁头对应一面)其次我们要知道是哪个磁道最后得知道是哪个扇区

柱面的概念:

由于磁盘里面可能会有多个碟片,因此在所有碟片上面的同一个磁道可以组合成所谓的柱面(cylinder)。

有人就说了,你这柱面在哪里啊?

我们看定义——由于磁盘里面可能会有多个碟片,因此在所有碟片上面的同一个磁道可以组合成所谓的柱面(cylinder)。

所以上图3个蓝色的圆圈(所有碟片上的同一个磁道)就是一个柱面

至于为啥叫柱面,看下图你就明白了

怎么样?是不是很清晰明了 

磁头为什么要左右摆动?

这其实是定位磁道和柱面的过程 

当定位到柱面和磁道了之后磁头就不动,变成盘片自转来定位扇区

磁盘本身的效率是由什么决定的?

 磁头左右摆动确定磁道和柱面的过程磁头确定好了磁道和柱面时,通过盘片的自转来寻找扇区的过程

如果我们在磁盘里面乱放数据,这样磁头就要到处乱转,访问时间长,效率低下!!!

所以在对应的设计上,我们一定要有意识的将数据尽量存在一起

我们怎么寻找对应的扇区呢?

        我们可以先根据磁头(<code>head)确定盘面,再根据半径定位磁道(柱面 cylinder),最后根据块号确定扇区(sector),这种寻址方法称为 CHS 定位法,是机械设备查找具体扇区时的方法

3.对硬盘进行逻辑抽象

大家也没有见过磁带

 也没有勾起你童年的回忆?

这个磁带被复读机的转轴转动的时候,一边的磁带会变多,另一边的就会变少!!!

如果想重新读,就得把磁带卷回原来那个圈里去

这就说明数据是存储在这条带子上面

这个和磁盘有关系吗?

         我们要是把磁带扯出来,拉直,数据也是存在的,这个磁带和磁盘的存储信息的形式是异曲同工的,我们可以这么理解磁盘

我们完全可以将磁盘看成一条拉长的磁带,然后就能分成无数个扇区!!!!

将盘面分割为多个线性分区,通过下标 <code>N 计算出 CHS 地址,然后进行文件访问

将磁道拉长,会得到一串线性空间(数组),其中的每个单位(扇区)为 <code>512 字节(或者 4 kb

 磁盘就是n个扇区的数组 ,任意一个扇区都有下标

假如我们有10万个扇区,但是我们的硬件只认识CHS定位法,那怎么办呢?

假设每个盘面有2万个扇区,50个磁道,每个磁道有400个扇区,现在我们要找下标是28888的扇区,要确认盘面,磁道!!

        很简单,第1个盘面,第22个磁道的第88个扇区!!!!(注意0才是最开始)

C:22H:1S:88(这个不是0开始·)

这样子很厉害了啊 

接下来我们回归硬件啊

我们整个”计算机“里,不止CPU有寄存器,外设也有,包括我们今天谈的磁盘,磁盘中有3个快速接收地址的东西

控制寄存器(是要读还是写)数据寄存器(数据)地址寄存器(往哪里写)状态寄存器(写入成功?)

这说明硬件是可以被访问的!!!!

4.分区

我们说磁盘是按线性结构来存储的

假设磁盘有800GB,那么操作系统怎么对磁盘进行管理呢?

我们存的是文件,那么文件内容和属性是分开存储的,这样子注定不能简单的就使用扇区存储。

这就好比我们国家领土广袤,怎么管理呢?先分省,省内再分市,市内再分县,县下还有镇……

例如,在计算机里,我们通过分区来将800GB分成了几个区,像下面这样子的分区

按照现在的水平,我们完全可以把分区简单理解为就是通过设置一个开始点和结束点(虽然和实际情况八竿子打不着)

<code>struct partion

{

int start;

int end;

};

        800GB难管理,我们管理最前面的200GB怎么样??!!

         我们把200GB看作中央,其他分区相当于地方。在这200GB这里,我们只要拿到最基本的数据(包括分区数据等),我们所有决策从200GB这里发出,然后其他分区按命令行事,就能完成管理好那其他分区!!!

        有人说200GB也不好管理!!!这个时候我们就要来了解一下分区的结构了

5.分区结构

我们将200GB细分开来

 

这样子就把管理200GB就变成了管理好这些小部分即可 

一个分区就变成只有下面这两种东西

Boot block(启动块):位于第一个分区的最开头部分(其他分区也可能有),这个是启动部分,保存了操作系统在哪里,分区是什么情况,起始分区在哪里,结束分区在哪里 Block Group(块组):里面有图上这些东西

5.1.Boot block(启动块) 

启动块(Boot Block):文件系统中的一个特殊块,位于文件系统的起始位置。它包含了引导加载程序(Boot Loader)所需的信息,用于引导操作系统的启动过程。这是个非常重要的设计,因为如此一来我们就能够将不同的启动引导程序安装到别的文件系统最前端.

5.2.Block Group(块组)

Block Group(块组),它们有着统一的格式,具体内容如图所示:

5.2.1.Data blocks(数据块)

我们先谈Data blocks

        Data blocks(数据块):文件内容存放的地方,以块的形式给我们呈现出来,常见大小为4KB——文件系统块大小(哪怕我们只写1字节,操作系统都得找一个分区,一个块组,然后分4KB给你写,也就是8个扇区啊)

        我们说磁盘被访问的最基本单元是扇区(512字节),但实际上,操作系统访问磁盘的时候却以块的形式(4Kb)来加载到内存,写到磁盘里也得以块的形式(4个字节)来写入磁盘!!!

         凡是通过文件系统来访问磁盘文件,都是以一个数据块(4kb)来访问的

这个区域是整个Block Group里内存占比最大的 

5.2.2.inode table (索引图)

5.2.2.1.inode(索引节点)

inode,中文译名为“索引节点”

        首先我们要明白inode是个结构体!!!!

我们看看inode的源码

<code>struct inode {

struct hlist_node i_hash; 哈希表

struct list_head i_list; 索引节点链表

struct list_head i_dentry; 目录项链表

unsigned long i_ino; 节点号

atomic_t i_count; 引用记数

umode_t i_mode; 访问权限控制

unsigned int i_nlink; 硬链接数

uid_t i_uid; 使用者id

gid_t i_gid; 使用者id组

kdev_t i_rdev; 实设备标识符

loff_t i_size; 以字节为单位的文件大小

struct timespec i_atime; 最后访问时间

struct timespec i_mtime; 最后修改(modify)时间

struct timespec i_ctime; 最后改变(change)时间

unsigned int i_blkbits; 以位为单位的块大小

unsigned long i_blksize; 以字节为单位的块大小

unsigned long i_version; 版本号

unsigned long i_blocks; 文件的块数

unsigned short i_bytes; 使用的字节数

spinlock_t i_lock; 自旋锁

struct rw_semaphore i_alloc_sem; 索引节点信号量

struct inode_operations* i_op; 索引节点操作表

struct file_operations* i_fop; 默认的索引节点操作

struct super_block* i_sb; 相关的超级块

struct file_lock* i_flock; 文件锁链表

struct address_space* i_mapping; 相关的地址映射

struct address_space i_data; 设备地址映射

struct dquot* i_dquot[MAXQUOTAS]; 节点的磁盘限额

struct list_head i_devices; 块设备链表

struct pipe_inode_info* i_pipe; 管道信息

struct block_device* i_bdev; 块设备驱动

unsigned long i_dnotify_mask; 目录通知掩码

struct dnotify_struct* i_dnotify; 目录通知

unsigned long i_state; 状态标志

unsigned long dirtied_when; 首次修改时间

unsigned int i_flags; 文件系统标志

unsigned char i_sock; 套接字

atomic_t i_writecount; 写者记数

void* i_security; 安全模块

__u32 i_generation; 索引节点版本号

union {

void* generic_ip; 文件特殊信息

} u;

};

   inode:存放了单个文件的所有属性

有以下特点!!!!

每个 inode 大小均固定为 128 bytes;每个文件都仅会占用一个 inode ,因此文件系统能够创建的文件数量与 inode 的数量有关;一个文件只需要一个inode节点来存储文件的元信息就够了,所以文件和inode节点是一一对应的(注意这里是文件,不是文件名);

到现在我们就明白了我们最开始讲的:文件的属性和文件的内容是分开来存储的!!!

上面显示的内容叫文件的属性,存在inode里面

上面显示的是文件的内容,存在Data Block里面!!

这里我要告诉大家一个比较震惊的事实:文件属性不包含文件名,操作系统只知道文件的inode编号

我们怎么查这个编号呢?

最前面的就是inode编号 

每个inode都有一个号码,操作系统用inode号码来识别不同的文件

        Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

表面上,用户通过文件名,打开文件。

实际上,系统内部这个过程分成三步:

首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,分析 inode 所记录的权限与用户是否符合,找到文件数据所在的block,读出数据。

我们找到了inode,又怎么能找到我们的数据块的?

我们看看inode里面存了什么再说

在inode结构体中,指向文件的数据块的信息主要由i_blocks和i_bdev成员变量表示

        i_blocks记录了文件使用的块数,而i_bdev用于关联块设备驱动,通过这个驱动可以访问文件系统上的数据块。这种设计使得inode能够有效地管理文件数据在磁盘上的分布,同时也方便了文件系统对数据块的读取和写入操作。

 怎么样?

我们只需要找到inode编号,按照inode 表访问inode,然后就能访问到数据块

        我们举个例子,假设某文件的权限和属性信息存放到 inode 4 号位置,这个 inode 记录了实际存储文件数据的Data block 号有 4 个,分别为 2、7、13、15,由此,操作系统就能快速地找到文件数据的存储位置。

有的时候发现我们的文件太大了, 如果直接采用inode直接存储块的位置的话,要很多inode table,这样子不行,于是就有了下面这两种inode

直接索引:文件本身内容·间接索引:里面存的不是文件内容,存的是文件的块号

5.2.2.2.inode和inode table的区别和联系

inode table的作用与结构

索引节点的组织inode table作为一个数组或类似的数据结构,其每个元素都是一个inode。它为文件系统提供了一个有效的方式组织和查找所有的inode,即通过inode号来索引对应的inode,从而实现对文件和目录的高效管理。优化文件访问速度:通过inode table,文件系统能够快速定位到任何指定的inode,进而访问或修改文件的元数据和数据块位置信息,这对于提高文件系统的读写速度非常关键。二者的关系

相互依存inode是文件元数据的容器,而inode table则是这些容器的集合,负责组织和管理这些inode。没有inode,inode table无法发挥作用;反之,如果没有inode table的组织,inode则会变得孤立,难以被系统有效利用。功能互补:inode关注单个文件的元数据和数据块索引,而inode table则从宏观上组织所有inode,确保文件系统的有序和高效运行。两者的功能相互补充,共同支持文件系统的基本操作,如文件的创建、删除、读取和写入等。

 

有人想问整个inode_number在哪里?

源码里的unsigned long i_ino就是inode_number 

 5.2.2.Block Bitmap(块位图)

        我们现在面临一个问题,假设我们的Data Block有20000个块,创建一个文件就要申请块,删除一个文件就要释放块,这么多块,那我们怎么知道哪些块被使用了,哪些没有被使用呢?

        这就要通过区块对照表的辅助了。从区块对照表当中可以知道哪些区块是空的,因此我们的系统就能够很快速地找到可使用的空间来处理文件。

        同样,如果你删除某些文件时,那么那些文件原本占用的区块号码就要释放出来,此时在区块对照表当中对应到该区块号码的标志就要修改成为【未使用中】,这就是对照表的功能。

          block bitmap是一种数据结构,它的主要功能是用于追踪每个block group中哪些block已经被使用。具体来说,block bitmap是一个特殊的文件,其大小恰好为一个block,而在这个block中,每个bit表示一个对应block的占用情况。如果该位为0,则表示对应的block为空,如果为1,则表示相应的block中存有数据。

          这种block bitmap技术的优势在于,当需要读取或修改文件时,可以快速找到空闲的block位置。例如,当执行写入操作时,系统会首先检查block bitmap,找出哪些block是空闲的,然后将新的数据写入到这些空闲的block中。同样地,当某个block不再使用时,系统会在block bitmap中将相应的bit标记为可用,以供后续的存储操作使用。

          此外,需要注意的是,一个block group中最多只能包含8×4096=32768个block。因此,对于一个含有大量数据的磁盘分区来说,可能需要多个block bitmap来分别记录各个block group的使用情况。

 看到这里你也应该明白了,新建、删除文件只需要改块位图即可!

删一个文件的时候要不要把块清空???

不需要了,只需要把这个Block Bitmap里面的对应的位置清空 

这个就是我们拷贝东西很慢,但是删除东西很快的原因

5.2.3. inode bitmap(inode位图)

        这个其实与区块对照表的功能类似,只是区块对照表记录的是使用与未使用的区块号码,inode对照表则是记录使用与未使用的inode号码。

        在Linux系统中,inode bitmap是一种数据结构,它的主要功能是用于追踪inode table中的每个inode是否已经被使用。具体来说,每个bitmap中的每一个位都对应于inode table中的一个inode,如果该位为0,则表示对应的inode为空,如果为1,则表示相应的inode 已被使用。

       这种位图技术(bitmap)的优势在于,当需要修改文件系统时,可以快速找到空闲的inode位置。例如,当需要新建一个文件时,系统会先检查inode bitmap中哪些inode是空闲的,然后将新文件的信息存储到这个空闲的inode中。同样地,当文件被删除时,对应的inode会被标记为可用,以供后续的新建文件使用。

        inode bitmap的比特位的位置和inode编号映射起来,比特位的内容和inode的状态相关

5.2.4.GDT(Global Descriptor Table)

我们这个Block Group的所有属性都放在这里!!!

描述的是整个分组基本的使用情况,inode table和Data Block用了多少,还剩多少,总共多少,都在这里存着

5.2.5.Super Block(超级块)

就管理范围而言,Super Block和GDT有一点点点点点……点类似,GDT是管整个分组,Super Block是整个分区的情况,但是文件系统的信息可比分组的多了去了

         在Linux操作系统中,superblock是一个特殊的数据结构,它记录了文件系统的整体信息,包括inode和block的总量、使用量、剩余量,以及档案系统的格式与相关信息等。

        具体来说,它包含了文件系统的类型、block大小、block总数、inode大小、inode总数、group的总数等等。

需要注意的是只有个别组内会有SuperBlock,并不是所有的组都有。此外,有Super Block的Group 中的SuperBlock都是对 Main SuperBlock(主SuperBlock)的备份,用于处理主SuperBlock故障或者误删的情况。

6.格式化

至此,我们的文件系统讲完了,我们来讲讲格式化

除了inode table和Data Block这些真正存文件的地方之外,将inode Bitmap和Block Bitmap设置为空,GDT和Super Block更新好数据,哪怕我从来没用过这个分组!!!!

每个分区在被使用之前,都必须先将部分文件系统的属性信息提前写进对应的分区中,方便我们后续使用这个分区或者分组——这个过程其实就是格式化这不就是先描述,再组织吗?

 学完了之后,我们就对分区有了新的认识

这只是假象,我们的计算机里面还是只有1块物理盘 

看到格式化了吗!!!!!!如果我们按了格式化,那么就会将inode Bitmap和Block Bitmap设置为空。

 7.理解文件操作

 我们怎么理解文件操作呢?

我们先复习一下

Linux系统中,一个文件有一个inode,每个inode有自己的inode编号(只在自己的分区有效),inode的设置是以分区为单位的,不能跨分区inode表示文件的所有属性,但是文件名并不是属性

7.1.新建一个文件

首先我们创建文件是在某个路径下面创建,这个路径可以确定我们是在哪个分区下面,哪个分组下面,分区给我们的文件分配一个inode

假设我们想要新增一个文件,此时文件系统的操作是:

1.先确定用户对于欲新增文件的目录是否具有w与x的权限,若有的话才能新增;2.根据inode Bitmap找到没有使用的inode号码,并将新文件的权限/属性写入;3.根据Block Bitmap找到没有使用中的区块号码,并将实际的数据写入区块中,且更新inode的区块指向数据;4.将刚刚写入的inode与区块数据同步更新inode Bitmap与Block Bitmap,并更新Super Block的内容。

其实就是查查inode Bitmap里没有使用的inode

7.2.删除一个文件

删除文件的步骤

首先找到<code>inode编号再将该文件对应的inode,在inode Bitmap当中置为无效(置0)最后将该文件申请的数据块,在Block Bitmap当中置为无效(置0)

此删除操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,起到了访问不到就等于删除的效果

说白了:删除=允许被覆盖 

当我们删除文件后短时间内是可以恢复的

         为什么说是短时间内呢,

        因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。

为什么拷贝文件的时候很慢,而删除文件的时候很快?

因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。

文件误删后的解决方案

磁盘中的数据被删除后,还可以再恢复吗?

        答案是可以的,但不能完全恢复,并且越早断电、送修越好

        前面说过,删除并不是真删除,访问不到就行了,所以只要在删除后,根据 inode 找到 Data block,其中的内容没有被覆盖,数据就可以找回来

应急方案:

不要轻举妄动,避免 Data block 被覆盖通过 inode 将 inode Bitmap 中的位图置 1,使文件复活,再根据属性进行数据恢复如果自己不知道 inode,那就尽早断电,送给厂家恢复(专业)

如何避免误删文件?

学习 Windows 中的回收站,删除不是真删除,而是先将文件移入回收站(目录)中,留给用户反悔的时间

7.3.查找一个文件

cat找文件内容的路径如下:目录->分区->分组->inode Bitmap(看看对应inode的位置是不是1,如果是就去读取inode table)->inode table -> Datablock

7.4.修改一个文件

我们要修改什么?

改文件的什么?要修改之前都得找到inode,这跟上面不是一样吗??

8.如何理解目录

我怎么知道文件的inode?

使用者从来没有关心过inode,用到是文件名!!!

Linux下一切皆是文件,目录也是文件,目录也有自己独立的inode,自己的属性

 问题来了

目录有内容吗?-》目录要不要Data Block?

目录当然要,那目录的数据块存的是什么?

存放的是该目录下   的    所包含文件的文件名,以及该文件名对应的inode号码。

系统是根据文件名找到inode的!!!!

ls命令只列出目录文件中的所有文件名:

ls -i命令列出整个目录文件,即文件名和inode号码:

如果要查看文件的详细信息,就必须根据文件名找到inode号码,再访问inode节点,读取信息。ls -l命令列出文件的详细信息。

来回答几个问题

1.为什么同一个目录下不能有同名文件 

文件名是作为一个找到inode的路径存在的,如果有两个同名文件,则会对应2个inode,找不到文件

2.目录没有w权限,我们就无法创建新文件!

如果没有写权限,我们就不能将文件名和该文件名和inode的映射关系写进这个目录的Data Block里面

3. 目录没有r权限,我们就不知道里面的文件信息

这个就是没有权限访问到Data Block里面存的文件的文件名,以及该文件名对应的inode号码。拿不到inode,就不能读

4.目录没有x权限,我们就无法进入这个目录 

我们进入的时候会通过读取目录里对应文件的inode,看看也没有x权限,没有的话就不让进,如果有就让你进,并更新PWD环境变量

5.目录是文件,也有inode编号,那目录的inode编号在哪里?

 目录的文件名和inode肯定是上一层目录的Data Block里面的东西!!我们一直往上找,都会到达根目录——那这样子是会特别慢的,所以Linux会存储我们的一些常用路径进行缓存

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。