Linux 文件系统原理 / 虚拟文件系统VFS
Linux 文件系统原理 / 虚拟文件系统VFS
- 虚拟文件系统 VFS
- VFS 定义
- VFS 的对象演绎
- 超级块 super_block
- 索引节点 inode
- 目录项 dentry
- 文件 file
- 文件共享
- 打开文件流程
- 参考文献
虚拟文件系统 VFS
VFS 定义
VFS是一个抽象层,其向上提供了统一的文件访问接口,而向下则兼容了各种不不同类型的文件系统。不仅仅是诸如Ext2、Ext3、Ext4、XFS、windows家族的NTFS和Btrfs等常规意义上的文件系统,还可以是比如上图的proc等伪文件系统和设备,也可以是诸如NFS、CIFS等网络文件系统。
VFS 采用标准的Linux系统调用读写位于不同物理介质上的不同文件系统,即为各类文件系统提供了一个统一的操作界面和应用编程接口, VFS是一个内核软件层 。 VFS是一个可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作的 抽象层 ,如下图所示:
VFS 的对象演绎
虚拟文件系统在磁盘中并没有对应的存储的信息。尽管 Linux 支持多达几十种文件系统,但这些真实的文件系统并不是一下子都挂在系统中的,它们实际上是按需挂载的。另外,这些实的文件系统只有安装到系统中,VFS 才予以认可,也就是说, VFS 只管理挂载到系统中的实际文件系统 。
VFS 有 4 个主要对象:
超级块(Superblock)
:存放系统中已安装文件系统的有关信息。文件索引节点(inode)
:存放关于具体文件的一般信息。目录项对象(dentry)
:存放目录项与对应文件进行链接的信息。路径中的每一个部分被称作目录项,例如 /home/clj/myfile 中,根目录是 / ,而 home,clj 和文件 myfile 都是目录项。
文件对象(file)
:存放打开文件与进程之间进行交互的有关信息。
超级块是对一个 文件系统 的描述 ; 索引节点是对一个 文件物理属性 的描述 ; 而目录项是对一个 文件逻辑属性 的描述 。
超级块 super_block
超级块用来描述整个文件系统的信息,包括文件系统的大小、有多少是空的和已经填满的占多少,以及他们各自的总数和其他诸如此类的信息。超级块占用1号物理块,就是文件系统的控制块 ,要 使用一个分区来进行数据访问,那么第一个要访问的就是超级块 。所以,超级块坏了,那磁盘也就基本没救了。
当内核在对一个文件系统进行初始化和注册时 在内存为其 分配一个超级块 。此时的超级块为 VFS 超级块 。也就是说,VFS 超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时被自动删除。 VFS 超级块只存放在内存中 。
对于每个具体的文件系统来说,都有各自的超级块,如 Ext2 超级块和 Ext3 超级块,它存放在磁盘上,内容包括:文件系统的大小、空闲块数目、空闲块索引表、空闲i节点数目、空闲i节点索引表、封锁标记等。超级块是系统为文件分配存储空间、回收存储空间的依据。这一部分的拓扑结构如下图:
其中 Block Group
存储的各部分含义如下:
- indoe bitmap (indoe对照表): 用来记录当前文件系统的indoe哪些是已经使用的,哪些又是未使用的。
- block bitmap (块对照表): 用来记录当前文件系统哪些block已经使用,哪些又是未使用的。
- inode table (inode 表格):inode是用来记录文件的属性以及该文件实际数据所在的block的号码。
- GDT(Global Descriptor Table):用来描述每个block group开始和结束的block号码以及每个区段位于哪一个block号码之间。相当于文件系统描述的是每个block group的信息。
- data blocks:数据块,用于存放数据
超级块的数据结构定义如下:
struct super_block
{dev_t s_dev; // unsigned long s_blocksize; // 以字节为单位数据块的大小unsigned char s_blocksize_bits; // 块大小的值所占用的位数,...struct list_head s_list; // 指向超级块链表的指针struct file_system_type *s_type; // 指向文件系统的 file_system_type 的指针struct super_operation *s_op; // 指向具体文件系统的用于超级块操作的函数集合struct mutex s_lock;struct list_head s_dirty;...void *s_fs_info; // 指向具体文件系统的超级块
};
从上面定义的数据结构可知:所有的超级块对象都以双向循环链表的形式链接在一起。链表中第一个元素用 super_blocks 变量来表示。
与超级块关联的方法就是所谓的超级块操作表,其数据结构是 super_operations,定义如下:
struct super_operations
{void (*write_super) (struct super_block *); // 将超级块的信息写回磁盘void (*put_super) (struct super_block *); // 释放超级块对象void (*read_inode) (struct inode *); // 读取某个文件系统的索引节点void (*write_inode) (struct inode *, int); // 把索引节点写回磁盘void (*put_inode) (struct inode *); // 逻辑上释放索引节点void (*delete_inode) (struct inode *); // 从磁盘上删除索引节点};
索引节点 inode
文件系统处理文件所需要的所有信息都存放在索引节点中。 在同一个文件系统中,每个索引节点号都是唯一的 。具体文件系统的索引节点是存放在磁盘上,是一种静态结构,要使用它,必须调入内存,填写 VFS 的索引节点,因此,也称 VFS 索引节点是 动态节点 。
我们的磁盘在进行分区、格式化的时候会分为两个区域, 一个是 数据区 ,用于存储文件中的数据 ; 另一个是 inode区 ,用于存放 inode table
(inode表) , inode table
中存放的是一个一个的 inode
(也称为inode节点),不同的 inode
就可以表示不同的文件,每一个文件都必须对应一个 inode
, inode
实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如:
- 文件字节大小
- 文件所有者
- 文件对应的读/写/执行权限
- 文件时间戳(创建时间、更新时间等)
- 文件类型
- 文件数据存储的block(块)位置
- ………
inode 结构体
定义在<linux/fs.h>
中,主要包含:存放的内容如下:
struct inode
{struct list_head i_hash; // 指向哈希表的指针struct list_head i_list; // 指向索引节点链表的指针struct list_head i_dentry; // 指向目录项链表的指针...unsigned long i_ino; // 索引节点号umode_t i_mode; // 文件的类型与访问权限kdev_t i_rdev; // 实际设备标识号uid_t i_uid; // 文件拥有者标识号gid_t i_gid; // 文件拥有者所在组的标识号...struct inode_operations *i_op; // 指向对该节点进行操作的一组函数struct super_block *i_sb; // 指向该文件系统超级块的指针atomic_t i_count; // 当前使用该节点的进程数,计数为0时,表明该节点可丢弃或重新使用struct file_operations *i_fop; // 指向文件操作的指针...struct vm_area_struct *i_op; // 指向对文件进行映射所使用的虚存区指针unsigned long i_state; // 索引节点的状态标志unsigned int i_flags; // 文件系统的安装标志union // 联合结构体,其成员指向具体文件系统的 inode 结构{struct minix_inode_info minix_i;struct Ext2_inode_info Ext2_i;}
};
inode
的索引流程如图所示(注意, 文件名并不是记录在 inode
中,而是目录项 dentry
中 ):
所以由此可知, inode table
本身也需要占用磁盘的存储空间。在同一个文件系统中,每一个文件都有唯一的一个 inode
,每一个 inode
都有一个与之相对应的数字编号 ,内核可以根据索引节点号的哈希值查找其 inode 结构,前提是内核要知道索引节点号和对应文件所在文件系统的超级块对象的地址。 在Linux系统下,我们可以通过"ls -i"命令查看文件的 inode编号
,如下所示:
上图中 ls
打印出来的信息中,每一行前面的一个数字就表示了对应文件的 inode编号
。除此之外,还可以使用 stat
命令查看,用法如下:
与索引节点关联的方法叫索引节点操作表,由 inode_operations
结构来描述:
struct inode_operations
{// 创建一个新的磁盘索引节点int (*create) (struct inode *, struct dentry *, int);// 查找一个索引节点所在的目录struct dentry * (*lookup) (struct inode *, struct dentry *);// 创建一个新的硬链接int (*link) (struct dentry *, struct inode *, struct dentry *);// 删除一个硬链接int (*unlink) (struct inode *, struct dentry *);// 为符号链接创建一个新的索引节点int (*symlink) (struct inode *, struct dentry *, const char *);// 为目录项创建一个新的索引节点int (*mkdir) (struct inode *, struct dentry *, int);// 删除一个目录项的索引节点int (*rmdir) (struct inode *, struct dentry *);
};
目录项 dentry
每个文件除了有一个索引节点inode数据结构外,还有一个目录项dentry数据结构。 目录项反应了文件系统的树状结构,目前主流的操作系统基本都是用树状结构来组织文件的。linux也不例外。dentry表示一个目录项,目录项下面又有子目录。
文件系统树形结构
目录在文件系统中的存储方式与常规文件类似,常规文件包括了 inode节点
以及 文件内容数据存储块(block)
;但对于目录来说,其存储形式则是由 inode节点
和 目录块
所构成, 目录块当中记录了有哪些文件组织在这个目录下,记录它们的 文件名
以及对应的 inode编号
。所以对此总结如下:
- 普通文件由
inode节点
和数据块
构成 - 目录由
inode节点
和目录块
构成
其存储形式如下图所示:
对于 dentry
和 inode
的区别可以如此总结:
dentry
结构代表的是逻辑意义上的文件,描述的是文件逻辑上的属性, 目录项对象在磁盘上并没有对应的映像 。inode
结构代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体的文件系统, 其inode结构在磁盘上就有对应的映像 。
当打开一个文件时,按照目录树搜索的过程如下:
这一流程大概如下图所示:
**一个索引节点对象可能对应多个目录项对象(因为路径的每一部分称作目录项,而文件的路径很长)。**目录项由dentry
结构体标识,定义在<linux/dcache.h>
中,主要包含:
struct dentry
{atomic_t d_count; // 目录项引用器unsigned int d_flags; // 目录项标志struct inode *d_inode; // 与文件名关联的索引节点struct dentry *d_parent; // 父目录的目录项 struct list_head d_hash; // 目录项形成的哈希表struct list_head d_lru; // 未使用的 LRU 链表struct list_head d_child; // 父目录的子目录项所形成的链表struct list_head d_subdirs; // 该目录项的子目录所形成的的链表struct list_head d_alias; // 索引节点别名的链表int d_mounted; // 目录项的安装点struct qstr d_name; // 目录项名(可快速查找)struct dentry_operations *d_op; // 操作目录项的函数struct super_block *d_sb; // 目录项树的根unsigned long d_vfs_flags;void *d_fsdata; // 具体文件系统的数据unsigned char d_iname[DNAME_INLINE_LEN]; // 短文件名...};
目录项有三种状态:
被使用:该目录项指向一个有效的索引节点,并有一个或多个使用者,不能被丢弃。
未被使用:也对应一个有效的索引节点,但VFS还未使用,被保留在缓存中。如果要回收内存的话,可以撤销未使用的目录项。
负状态:没有对应有效的索引节点,因为索引节点被删除了,或者路径不正确,但是目录项仍被保留了。
将整个文件系统的目录结构解析成目录项,是一件费力的工作,为了节省VFS操作目录项的成本,内核会将目录项缓存起来。
文件 file
文件对象是进程打开的文件在内存中的实例。Linux用户程序可以通过open()
系统调用来打开一个文件,通过close()
系统调用来关闭一个文件。由于多个进程可以同时打开和操作同一个文件, 所以同一个文件,在内存中也存在多个对应的文件对象,但对应的索引节点和目录项是唯一的 。
一个进程所处的位置是由 fs_strcut 来描述的 , 而一个进程(或者用户)打开的文件是由 files_struct
/ fdtable
来描述的 , 而整个系统所打开的文件是由 file
结构来描述的 。
文件对象由file
结构体表示,file 结构形成了一个双链表,称为 系统打开文件表 。其定义在<linux/fs.h>
中,主要包含:
struct file
{struct list_head f_list; // 所有打开的文件形成一个链表struct dentry *f_dentry; // 与文件相关的目录项对象struct vfsmount *f_mount; // 该文件所在的已安装文件系统struct file_operations *f_op; // 指向文件操作表的指针mode_t f_mode; // 文件的打开模式loff_t f_pos; // 文件的当前位置unsigned short f_flags; // 打开文件时所指定的标志unsigned short f_count; // 使用该结构的进程数...
};
对文件进行操作的一组函数叫 文件操作表 ,由 file_operations
结构描述,如下:
struct file_operations
{// 修改文件指针loff_t (*llseek) (struct file *, loff_t, int);// 从文件中读取若干个字节ssize_t (*read) (struct file *, char *, size_t, loff_t *);// 给文件中写若干个字节ssize_t (*write) (struct file *, const char *, size_t, loff_t *);// 文件到内存的映射int (*mmap) (struct file *, struct vm_area_struct *);// 打开文件int (*open) (struct inode *, struct file *);// 关闭文件时减少 f_count 计数int (*flush) (struct file *);// 释放 file 对象int (*release) (struct inode *, struct file *);// 文件在缓冲区的数据写回磁盘int (*fsync) (struct file *, struct dentry *, int datasync);...
};
文件描述符是用来描述打开的文件的。每一个进程用一个 files_struct 结构来记录文件描述符的使用情况,即一个进程可以有多个文件描述符,因为一个进程可以打开多个文件。而通过 dup()、dup2() 和 fcntl() 两个文件描述符可以指向同一个打开的文件,数组的两个元素可能指向同一个文件对象。
每个进程都有 自己的根目录和当前工作目录 ,内核使用 struct fs_struct
来记录这些信息,其定义为:
struct fs_struct
{atomic_t count; // 表示共享同一 fs_struct 表进程数目rwlock_t lock;int umask; // 为新创建的文件设置初始文件许可权struct dentry *root, *pwd, *altroot; // 对目录项的描述struct vfsmount *rootmnt, *pwdmnt, *altrootmnt; // 目录安装点的描述
};
除了根目录和当前工作目录,进程还需要记录自己打开的文件。进程已经打开的所有文件使用 struct files_struct
来记录,进程描述符的 files
字段便指向该进程的files_struct结构。它是 进程的私有数据 ,其定义如下:
struct files_struct
{atomic_t count; // 共享该表的进程数rwlock_t file_lock; // 保护以下的所有域int max_fds; // 当前文件对象的最大数int max_fdset; // 当前文件描述符的最大数int next_fd; // 已分配的文件描述符加 1struct file ** fd; // 指向文件对象指针数据的指针fd_set *close_on_exec; // 指向指向 exec() 时需要关闭的文件描述符fd_set *open_fds; // 指向打开的文件描述符的指针fd_set close_on_exec_init; // 执行 exec() 时需要关闭的文件描述符的初值集合fd_set open_fds_init; // 文件描述符的初值集合struct file *fd_array[32]; // 文件对象指针的初始化数组
};
旧版本的内核中, struct files_struct
中有一个 fd字段
,指向文件对象的指针数组。通常fd指向fd_array,如果进程打开的文件数目多于32个,内核就分配一个新的更大的文件对象的指针数组,并将其地址存放在fd字段中,这个数组所包含的元素数目存放在 max_fds字段
。
新版本的内核将 fd
, max_fds
以及其他几个相关字段组织在一起,增加一个新的独立数据结构 struct fdtable
,称为 文件描述符表 ,定义于 include/linux/fdtable.h
,其主要数据结构定义如下所示:
struct fdtable {unsigned int max_fds;struct file __rcu **fd; /* current fd array */unsigned long *close_on_exec;unsigned long *open_fds;struct rcu_head rcu;
};
文件共享
所谓文件共享指的是同一个文件(譬如磁盘上的同一个文件,对应同一个inode)被 多个独立的读写体同时进行IO操作 。同时进行IO操作指的是 一个读写体操作文件尚未调用 close
关闭的情况下,另一个读写体去操作文件 。
文件共享的意义有很多,多用于多进程或多线程编程环境中,譬如我们可以通过文件共享的方式来实现多个线程同时操作同一个大文件,以减少文件读写时间、提升效率。
常见的三种文件共享的实现方式有:
同一个进程中 通过
dup(dup2)
函数 对文件描述符进行 复制 ,其数据结构关系如下图所示:同一个进程中 多次调用
open
函数 打开同一个文件 ,各数据结构之间的关系如下图所示:不同进程中 分别使用
open
函数 打开同一个文件 ,其数据结构关系图如下所示:
因为 文件描述符表是相对每个进程而言 ;而 文件表(打开文件表)是相对于整个系统而言 。所以单个进程内不论是否多次打开相同文件还是打开多个文件,导致的都只是 文件描述符fd
的增加。
而 open
和 dup
的区别在于, open
是打开文件操作,会 获得新的fd并在系统打开文件表内创造新条目 ;而 dup
只是复制文件描述符fd,并不会增加打开文件表 。
打开文件流程
文件在没有被打开的情况下一般都是存放在磁盘中的,譬如电脑硬盘、移动硬盘、U盘等外部存储设备, 文件存放在磁盘文件系统中,并且以一种固定的形式进行存放,我们把他们称为 静态文件 。
当我们调用 open
函数去打开文件的时候,内核会申请一段内存(一段缓冲区),并且将静态文件的数据内容从磁盘这些存储设备中读取到内存中进行管理、缓存 (也 把内存中的这份文件数据叫做 动态文件 、内核缓冲区 )。 打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件进行相关的操作,而并不是针对磁盘中存放的静态文件 。
当我们对动态文件进行读写操作后,此时内存中的动态文件和磁盘设备中的静态文件就不同步了, 数据的同步工作由内核完成,内核会在之后将内存这份动态文件更新(同步)到磁盘设备中 。
因为磁盘、硬盘、U盘等存储设备基本都是Flash块设备,因为块设备硬件本身有读写限制等特征,块设备是以一块一块为单位进行读写的(一个块包含多个扇区,而一个扇区包含多个字节), 一个字节的改动也需要将该字节所在的block全部读取出来进行修改,修改完成之后再写入块设备中,所以导致对块设备的读写操作非常不灵活 ;而 内存可以按字节为单位来操作,而且可以随机操作任意地址数据,非常地很灵活 ,所以对于操作系统来说,会先将磁盘中的静态文件读取到内存中进行缓存,读写操作都是针对这份动态文件,而不是直接去操作磁盘中的静态文件,不但操作不灵活,效率也会下降很多,因为内存的读写速率远比磁盘读写快得多。
在Linux系统中,内核会为每个进程设置一个专门的数据结构用于管理该进程,譬如记录进程的状态信息、运行特征等,我们把这个称为 进程控制块(Process control block,PCB)
。
PCB数据结构体中有一个指针指向了 文件描述符表(File descriptors)
,文件描述符表中的每一个元素索引到对应的 文件表(File table)
,文件表也是一个数据结构体,其中记录了很多文件相关的信息,譬如 文件状态标志、引用计数、当前文件的读写偏移量以及i-node指针(指向该文件对应的inode)等
,进程打开的所有文件对应的文件描述符都记录在文件描述符表中,每一个文件描述符都会指向一个对应的文件表,其示意图如下所示:
我们打开文件的过程也就是对 文件表file
的初始化的过程。在打开文件的过程中会将 inode
部分关键信息填充到 file
中,特别是文件操作的函数指针 。在 task_struct
中保存着一个 file类型
的数组,而用户态的文件描述符其实就是数组的下标。这样通过文件描述符就可以很容易到找到 file
,然后通过其中的函数指针访问数据。
通过以上介绍可知,打开一个文件,系统内部会将这个过程大概分为下面三步:
- 传入文件路径,系统根据
dentry
找到这个文件名所对应的inode编号
; - 通过
inode编号
从inode table
中找到对应的inode struct
,包含 inode 详细信息 ; - 根据
inode结构体
中记录的信息,确定文件数据所在的block
,并读出数据到内存,建立动态文件;
同时将inode关键信息
填充到打开文件表file
中,并返回该表的下标(其实就是文件描述符fd
)
我们以Ext2文件系统的写数据为例来看看文件处理流程和各个层级之间的关系,如下图:
在调用用户态的写数据接口的时候,需要传入文件描述符。内核根据文件描述符找到file,然后调用函数接口(file->f_op->write)文件磁盘数据。其中file结构体的f_op指针就是在打开文件的时候通过inode初始化的。
参考文献
1:文件系统理论详解,Linux操作系统原理与应用_Great Macro的博客-CSDN博客
2:Linux内核的5个子系统 - schips - 博客园
3:理解linux文件系统(VFS主要数据结构及之间的关系) - 周围静地出奇 - 博客园
4:文件系统入门知识_Linux教程_Linux公社-Linux系统门户网站
5:Linux 文件系统详解_hguisu的博客-CSDN博客
6:谈谈linux内核学习:虚拟文件系统(VFS) - 知乎
7:【正点原子】STM32MP1嵌入式Linux C应用编程指南V1.4 - 第三章
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
Copyright © 2023.05 by Mr.Idleman. All rights reserved.
Linux 文件系统原理 / 虚拟文件系统VFS相关推荐
- Linux文件系统概述:硬盘驱动>通用块设备层>文件系统>虚拟文件系统(VFS)
目录 一.概述 1. 硬盘驱动 2. 通用块设备层 General Block Device Layer 3. 文件系统 4. 虚拟文件系统(VFS) 二.存储介质 闪存(Flash Memory) ...
- 深入linux内核架构--虚拟文件系统VFS
[推荐阅读] Linux内核源码分析--内核启动之zImage自解压过程 你应该知道的Linux内核基础及内核编译 深入理解LINUX内核堆栈 [零声教育]vico老师教你怎么学习Linux内核 值得 ...
- linux 文件系统_Linux 虚拟文件系统
虚拟文件系统 为了支持各种本机文件系统,且同时允许访问其他操作系统的文件,Linux 内核在用户进程与实际文件系统实现之间引入了一个抽象层,该层称为虚拟文件系统.它的任务并不简单,一方面它要提供一套管 ...
- linux文件系统dentry_Linux 文件系统(一)---虚拟文件系统VFS----超级块、inode、dentry、file...
一: 什么是文件系统,详见:http://zh.wikipedia.org/zh/%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F 其实一句话就是管理这块文件的机制(组织方式, ...
- linux内核之虚拟文件系统
一.虚拟文件系统概述 虚拟文件系统VFS(也成虚拟文件交换)作为内核子系统,为用户空间程序提供了文件和文件系统相关的统一接口.通过VFS,应用程序可以使用相同接口完成不同介质上不同文件系统的数据读写操 ...
- java 虚拟文件系统_虚拟文件系统VFS
Maven引用坐标: org.tinygroup vfs 0.0.12 一开始,本人抱着对Apache的绝对信任,选择了Apache VFS来进行文件访问的封装,确实,他的API是统一的.优雅的,支持 ...
- Linux文件系统二(虚拟文件系统VFS实现原理)
创作人QQ:851301776,邮箱:lfr890207@163.com 欢迎大家一起技术交流,本博客主要是自己学习的心得体会,只为每天进步一点点! 个人座右铭: 1 ...
- Linux虚拟文件系统VFS的相关数据结构和操作
最近看到几篇介绍VFS的韩语文章,觉得里面的众多绘图清晰易懂,冒昧将其摘选出来,分享给大家,希望大家可以从更多的角度去理解和认识VFS的构成和原理.原文地址位于 https://m.blog.nave ...
- Linux·VFS虚拟文件系统
目录 1 概念 2 架构 3 接口适配示例 4 跨设备/文件系统示例 5 VFS的抽象接口 6 Linux系统VFS支持的文件系统 7 统一文件模型(common file model) 7.1 Su ...
最新文章
- 航空频率表 2020_飞亚达2020时光勋章品牌年会——往昔作序,来日为章
- 用户 'IIS APPPOOL\**' 登录失败的解决方案(项目部署到本地IIS上打开网页出现报错)...
- 未转变者空投指令服务器,未转变者空投指令 | 手游网游页游攻略大全
- Blazor 数据绑定开发指南
- acm之vim的基本配置
- notepad python设置_NotePad++上配置Python
- Vista初级使用技巧及故障总结
- 消息称ARM CEO已辞职 与660亿美元卖身NVIDIA失败无关
- 原理 rpm_图文详解,微型直流电机的工作原理
- bzoj 4401: 块的计数(结论)
- mysql中文版下载5.6_mysql5.6官方版下载
- 绿盾加密如何顺利切换成IP-Guard加密
- 架构之美第八章-软件架构的含义
- java数字时钟_java Swing数字时钟
- 【ps-course】layer 图层
- android q状态栏,用腻了导航栏?在一加Android Q beta中强行开启全面屏手势
- Vivado安装找不到matlab,vivado安装System Generator不支持新版Matlab怎么办?
- 谭浩强C语言程序设计(1-3章代码学习)
- 【Canvas】js如何设置canvas绕图形中心旋转
- Android resource linking failed AAPT: error: resource android:color/system_neutral1_1000 not found.