虚拟文件系统

  • 1 通用文件系统
  • 2 文件系统抽象层
  • 3 Unix文件系统
  • 4 VFS对象及其数据结构
    • 其他VFS对象
  • 5 超级快对象
    • 超级块操作
  • 6 索引节点对象
    • 索引节点操作
  • 7 目录项对象
    • 目录项状态
    • 目录项缓存
    • 目录项操作
  • 8 文件对象
  • 9 和文件系统相关的数据结构
  • 10 和进程相关的数据结构
  • 11 Linux中的文件系统

虚拟文件系统,简称VFS,是内核的子系统,为用户空间程序提供了文件系统相关的接口。系统中所有文件系统不但依赖VFS共存,而且也依靠VFS系统协同工作。通过虚拟文件系统,程序可以 利用标准的UNIX文件系统调用不同介质上不同文件系统进行读写操作。

如下图:使用cp命令从ext3文件系统格式的硬盘拷贝数据到ext2文件系统格式的可移动磁盘上。两种不同的文件系统,两种不同的介质,连接到同一个VFS上。

1 通用文件系统

VFS使得用戶可以直接使用open()、write()和read()这样的系统调用而无需考虑具体文件系统实际物理介质。系统调用还可以在这些不同的文件系统和介质之间执行。正是由于包括Linux在内的现代操作系统引入了抽象层,通过虚拟接口访问文件系统,才使得这种协作性和通用性成为可能,新的文件系统和新种类的存储介质都能找到进入Linux之路,程序无需重写,甚至无需重新编译。

2 文件系统抽象层

之所以可以使用这种通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个抽象层,该抽象层使Linux能够支持各种文件系统,即便是它们在功能和行为上存在很大差别。为了支持多文件系统,VFS提供了一个通用文件系统模型,该模型囊括了我们所能想到的文件系统的常用功能和行为。

VFS抽象层之所以能链接各种各样的文件系统,是因为它定义了所有文件系统都支持的基本的、概念上的接口和数据结构。同时实际文件系统也将自身的诸如如何打开文件、目录是什么等概念在形式上与VFS定义保持一致。因为实际文件系统的代码在同一的接口和数据结构下隐藏了具体的实现细节,所以在VFS层和内核的其他部分看来,所有的文件系统都是相同的。

内核通过抽象层能够方便、简单地支持各种类型的文件系统,实际文件系统通过编程提供VFS所期望的抽象接口和数据结构,这样,内核就可以毫不费力地和任何文件系统系统工作。并且这样提供给用户空间的接口,也可以和任何文件系统无缝连接在一起,完成实际工作。

我们在用户空间调用write方法,会首先在VFS找到一个通用系统调用sys_write()处理,sys_wirte()会找到所在的文件系统给出的写操作,然后通过该写操作,往物理介质上写数据。

3 Unix文件系统

Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点。

从本质上说文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。在Unix中,文件系统被安装在一个特定的安装点上,所有的已安装文件系统都作为根文件系统数的枝叶出现在系统中。

文件其实可以看做是一个有序字节串,字节串中第一个字节是文件的头,最后一个字节时文件的尾。文件通过目录组织起来,文件目录好比一个文件夹,用来容纳相关文件。因为目录也可以包含子目录,所以目录可以层层嵌套,形成文件路径。路径中的每一部分都被称作目录条目。/home/wolfman/butter的根目录是/,目录home、wolfman和文件butter都是目录条目,它们被统称为目录项。在Unix中,目录属于普通文件,它列出包含在其中的所有文件。由于VFS把目录当做文件对待,所以可以对目录和文件执行相同的操作。

Unix系统将文件的相关信息和文件本身这两个概念加以区分,文件的相关信息,有时被称作文件的元数据,被存储在一个单独的数据结构中,该结构被称为索引节点(inode)。

4 VFS对象及其数据结构

VFS其实采用的是面向对象的设计思路,使用一族数据结构来代表通用文件对象。VFS有四个主要的对象类型,它们分别是:

  • 超级块对象,它代表一个已安装的文件系统
  • 索引节点对象,它代表一个文件
  • 目录项对象,它代表一个目录项,是路径的一个组成部分。
  • 文件对象,它代表由进程打开的文件

注意,因为VFS将目录作为一个文件来处理,所以不存在目录对象。目录项代表的是路径中的一个组成部分,它可能包括一个普通文件,目录项不同于目录,但目录却和文件相同。

其他VFS对象

VFS使用了大量结构体对象,它所包括的对象远远多于上面提到的这几种主要对象。比如每个注册的文件系统都是由file_system_type结构体来表示,它描述了文件系统及其能力,另外,每一个安装点也都有vfsmount结构体表示,它包含安装点的相关信息。如位置和安装标志等。
后面还要介绍三个与进程相关的结构体,它们描述了文件系统以及和进程相关的文件,这三个结构体分别是file_struct、fs_struct和namespace。

5 超级快对象

各种文件系统都必须实现超级块,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如sysfs),它们会在使用现场创建超级块并将其保存到内存中。

超级块对象由spuer_block结构体表示,定义在文件linux/fs.h中。创建、管理和销毁超级块对象的代码位于文件fs/super.c中。超级块对象通过alloc_super()函数创建并初始化,在文件系统安装时,内核回调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。

超级块操作

超级块对象中最重要的一个域是s_op,它指向超级块的操作函数表。超级块操作函数表如下

struct super_operations {struct inode *(*alloc_inode)(struct super_block *sb);void (*destroy_inode)(struct inode *);void (*read_inode) (struct inode *);void (*dirty_inode) (struct inode *);int (*write_inode) (struct inode *, int);void (*put_inode) (struct inode *);void (*drop_inode) (struct inode *);void (*delete_inode) (struct inode *);void (*put_super) (struct super_block *);void (*write_super) (struct super_block *);int (*sync_fs)(struct super_block *sb, int wait);void (*write_super_lockfs) (struct super_block *);void (*unlockfs) (struct super_block *);int (*statfs) (struct super_block *, struct kstatfs *);int (*remount_fs) (struct super_block *, int *, char *);void (*clear_inode) (struct inode *);void (*umount_begin) (struct super_block *);int (*show_options)(struct seq_file *, struct vfsmount *);
};

该结构体中的每一项都是指向超级快操作函数的指针,超级块操作函数执行文件系统和索引节点的低层操作。
上面所有函数都是由VFS在进程上下文中调用,必要时,它们都可以阻塞,这其中的一些函数是可选的:在超级块操作表中,文件系统可以将不需要的函数指针设置成NULL,如果VFS发现操作函数指针是NULL,那它要么就会调用通用函数执行相应操作,要门什么也不做,如何选择取决于函数。

6 索引节点对象

索引节点对象包含了内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些消息。

索引节点对象由inode结构体表示,定义在文件linux/fs.h中。

索引节点操作

索引节点对象中的inode_operations项描述了VFS用以操作索引节点对象的所有方法,这些方法由文件系统实现。inode_operatiions结构体定义在文件linux/fs.h中

struct inode_operations {int (*create) (struct inode *,struct dentry *,int, struct nameidata *);struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);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 *);int (*mknod) (struct inode *,struct dentry *,int,dev_t);int (*rename) (struct inode *, struct dentry *,struct inode *, struct dentry *);int (*readlink) (struct dentry *, char __user *,int);int (*follow_link) (struct dentry *, struct nameidata *);void (*put_link) (struct dentry *, struct nameidata *);void (*truncate) (struct inode *);int (*permission) (struct inode *, int, struct nameidata *);int (*setattr) (struct dentry *, struct iattr *);int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);ssize_t (*listxattr) (struct dentry *, char *, size_t);int (*removexattr) (struct dentry *, const char *);
};

7 目录项对象

VFS把目录当作文件看待,所以在路径/bin/vi中,bin和vi都是文件,bin是特殊的目录文件,而vi是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。
为了方便查找操作,VFS引入了目录项的概念。每个目录项代表路径一个特定部分,对前一个例子来说,/、bin和vi都属于目录项对象。前两个是目录,最后一个是普通文件。
目录项也可包括安装点。在路径/mnt/cdrom/foo中,/、mnt、cdrom和foo都属于目录项对象。VFS在执行目录操作时,如果需要的话,会现场创建目录项对象。

目录项对象由dentry结构体表示,定义在文件linux/dcache.h中。

struct dentry {atomic_t d_count;unsigned int d_flags;        /* protected by d_lock */spinlock_t d_lock;     /* per dentry lock */struct inode *d_inode;     /* Where the name belongs to - NULL is* negative *//** The next three fields are touched by __d_lookup.  Place them here* so they all fit in a 16-byte range, with 16-byte alignment.*/struct dentry *d_parent; /* parent directory */struct qstr d_name;struct list_head d_lru;        /* LRU list */struct list_head d_child; /* child of parent list */struct list_head d_subdirs;   /* our children */struct list_head d_alias; /* inode alias list */unsigned long d_time;     /* used by d_revalidate */struct dentry_operations *d_op;struct super_block *d_sb;  /* The root of the dentry tree */void *d_fsdata;            /* fs-specific data */struct rcu_head d_rcu;struct dcookie_struct *d_cookie; /* cookie, if any */struct hlist_node d_hash;  /* lookup hash list */  int d_mounted;unsigned char d_iname[DNAME_INLINE_LEN_MIN];  /* small names */
};

不同于前面两个对象,目录项对象没有对应的磁盘数据结构,VFS根据字符串性式的路径名现场创建它,而且由于目录项对象并非真正保存在磁盘上,所以目录结构体没有是否被修改的标志。

目录项状态

目录项对象有三种有效状态:被使用、未被使用和负状态。
一个被使用的目录项对应一个有效的索引节点(d_inode指向相应的索引节点)并且表明该对象存在一个或多个使用者(d_count为正值)。一个目录项处于被使用状态,意味着它正被VFS使用并且指向有效的索引节点,因此不能被丢弃。

一个未被使用的目录项对应一个有效的索引节点,但是VFS当前并未使用它(d_count为0)。该目录项对象指向一个有效的对象,而且被保留在缓存中以便需要时在使用它。

一个负状态的目录项没有对应的有效索引节点(d_node为NULL),因为索引节点已经被删除了,或路径不再正确了,但是目录项仍然保留,以便快速解析以后的路径查询。

目录项缓存

如果VFS层遍历路径名中所有的元素并将它们逐个解析成目录项对象,这将是一件非常费力的工作,会浪费大量的时间,所以内核将目录项对象缓存在目录项缓存(简称dcache)中。
目录项缓存包括三个主要部分:

  • "被使用的"目录项链表
  • "最近被使用的"双向链表。该链表含有未被使用的和负状态的目录项对象。由于该链表以时间顺序插入,所以链头的结点是最新数据,当内核必须通过删除结点项回收内存时,会从链尾删除结点项,因为尾部的节点最旧,在近期内再次被使用的可能性最小
  • 散列表和相应的散列函数用来快速地将给定路径解析为相关目录项对象。

举个例子,假设你需要在自己目录中编译一个源文件,/home/dracula/src/foo.c,每一次对foo.c文件进行访问,VFS必须沿着嵌套的目录依次解析全部路径:/、home、dracula、src和最终的foo.c。为了避免每次访问该路径名都进行这种耗时的操作,VFS会先在目录项缓存中搜索路径名,如果找到了,直接访问。相反,如果该目录项在目录项缓存中并不存在,VFS就必须通过查文件系统为每个路径分量解析路径,解析完毕后,再将目录项对象加入dcache中,以便以后可以快速访问它。

目录项操作

dentry_operation结构体指明了VFS操作目录项的所有方法。
该结构体定义在文件<linux/dcache.h>中

struct dentry_operations {int (*d_revalidate)(struct dentry *, struct nameidata *);int (*d_hash) (struct dentry *, struct qstr *);int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);int (*d_delete)(struct dentry *);void (*d_release)(struct dentry *);void (*d_iput)(struct dentry *, struct inode *);
};

8 文件对象

文件对象表示进程已打开的文件。文件对象是已打开的文件在内存中的表示,该对象由相应的open()系统调用创建,由close()系统调用销毁。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象(一个进程打开了一个文件就会有一个文件对象)。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(索引节点),其实只有目录项对象才表示已打开的实际文件,虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象是唯一的。

文件对象由file结构体表示,定义在文件linux/fs中

struct file {struct list_head    f_list;struct dentry        *f_dentry;struct vfsmount         *f_vfsmnt;struct file_operations  *f_op;atomic_t      f_count;unsigned int        f_flags;mode_t          f_mode;int          f_error;loff_t          f_pos;struct fown_struct    f_owner;unsigned int        f_uid, f_gid;struct file_ra_state   f_ra;unsigned long      f_version;void          *f_security;/* needed for tty driver, and maybe others */void           *private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */struct list_head    f_ep_links;spinlock_t       f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */struct address_space    *f_mapping;
};

类似于目录项对象,文件对象实际上没有对应的磁盘数据,所以在结构体中没有代表其对象是否为脏,是否需要写回磁盘的标志。文件对象通过f_dentry指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。

文件对象的操作由file_operations结构体表示,定义在linux/fs.h中

struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, struct dentry *, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*dir_notify)(struct file *filp, unsigned long arg);int (*flock) (struct file *, int, struct file_lock *);
};

9 和文件系统相关的数据结构

除了以上几种VFS基础对象外,内核还使用了另外一些标准数据结构来管理文件系统的其他相关数据。第一个结构体是file_system_type,用来描述各种特定文件系统类型,比如ext3。第二个结构体是vfsmount,用来描述一个安装文件系统的实例。

因为Linux支持众多不同的文件系统,所以内核必须由一个特殊的结构体来描述每种文件系统的功能和行为。file_system_type定义在linux/fs.h中

struct file_system_type {const char *name;int fs_flags;struct super_block *(*get_sb) (struct file_system_type *, int,const char *, void *);void (*kill_sb) (struct super_block *);struct module *owner;struct file_system_type * next;struct list_head fs_supers;
};

每种文件系统,不管有多少个实例安装到系统中,还是根本就没有安装到奥系统中,都只有一个file_system_type结构。

当文件系统被实际安装时,将有一个vfsmount结构体在安装时被创建。该结构体用来代表文件系统的实例。
vfsmount结构被定义在linux/mount.h中

struct vfsmount
{struct list_head mnt_hash;struct vfsmount *mnt_parent; /* fs we are mounted on */struct dentry *mnt_mountpoint;    /* dentry of mountpoint */struct dentry *mnt_root;  /* root of the mounted tree */struct super_block *mnt_sb;   /* pointer to superblock */struct list_head mnt_mounts; /* list of children, anchored here */struct list_head mnt_child;    /* and going through their mnt_child */atomic_t mnt_count;int mnt_flags;int mnt_expiry_mark;        /* true if marked for expiry */char *mnt_devname;       /* Name of device e.g. /dev/dsk/hda1 */struct list_head mnt_list;struct list_head mnt_fslink;   /* link in fs-specific expiry list */struct namespace *mnt_namespace; /* containing namespace */
};

10 和进程相关的数据结构

系统中的每一个进程都有自己的一组打开的文件,像根文件系统、当前工作目录、安装点等。有三个数据结构将VFS层和系统的进程紧紧联系在一起,它们分别是:files_struct、fs_struct和namespace结构体。

files_struct结构体定义在文件linux/file.h中。该结构体由进程描述符中的files域指向。所有与每个进程相关的信息如打开的文件及文件描述符都包含在其中,其结构体描述如下:

struct files_struct {atomic_t count;spinlock_t file_lock;     /* Protects all the below members.  Nests inside tsk->alloc_lock */int max_fds;int max_fdset;int next_fd;struct file ** fd;      /* current fd array */fd_set *close_on_exec;fd_set *open_fds;fd_set close_on_exec_init;fd_set open_fds_init;struct file * fd_array[NR_OPEN_DEFAULT];
};

fd数组指针指向已打开的文件对象链表,默认情况下,指向fd_array数组,因为NR_OPEN_DEFALUT等于32,所以该数组可以容纳32个文件对象,如果一个进程锁打开的文件对象超过32个,内核将分配一个新数组,并且将fd指针指向它。

fs_struct由进程描述符的fs域指向,它包含文件系统和进程相关的信息,定义在linux/fs_struct.h中。

struct fs_struct {atomic_t count;rwlock_t lock;int umask;struct dentry * root, * pwd, * altroot;struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};

该结构包含了当前进程的当前工作目录(pwd)和根目录(root)。

namespace定义在文件linux/namespace.h中,由进程描述符中的namespace域指向。2.4内核以后,单进程命名空间被加入到内核中,它使得每一个进程都在系统中都看到唯一的安装系统文件

struct namespace {atomic_t       count;struct vfsmount * root;struct list_head   list;struct rw_semaphore    sem;
};

list域是连接已安装文件系统的双向链表,它包含的元素组成了全体命名空间。默认情况下,所有的进程共享同样的命名空间,只有在进行clone()操作时使用CLONE_NEWNS标志,才会给进程一个另外的命名空间结构体的拷贝。

11 Linux中的文件系统

Linux支持相当多种类的文件系统,从本地文件系统。如ext2和ext3,到网络文件系统,如NFS和Coda、VFS层提供了给这些文件系统一个统一的实现框架,而且还提供了能和标准系统调用交换工作的同一接口。由于VFS层的存在,使得Linux实现新文件系统的工作变得简单起来,它可以轻松地使这些文件系统通过Unix系统调用而协同工作。

Linux内核设计与实现---虚拟文件系统相关推荐

  1. linux 虚拟文件系统 源码,Linux内核源代码情状分析-虚拟文件系统

    Linux内核源代码情景分析-虚拟文件系统 我们先来看两张图: 第一张是VFS与具体文件系统的关系示意图: 第二张是Linux文件系统的层次结构: 特殊文件:用来实现"管道"的文件 ...

  2. Linux内核设计与实现(13)第十三章:虚拟文件系统

    Linux内核设计与实现(13)第十三章:虚拟文件系统 1. 文件系统 1.1 文件系统定义: 1.2 文件系统分类 1.3 标准文件系统:Ext文件系统族 1.4 VFS 1.4.1 VFS 背景 ...

  3. 赵晨雨:从文件系统的数据结构看Linux内核设计

    作者简介 赵晨雨:西安邮电大学2018级陈莉君教授研究生,天真无邪小白一枚,已经爱上linux内核而不能自拔,正在成长为内核狂热爱好者? 跟随陈老师学习linux内核两个月了,对linux内核产生了极 ...

  4. 《Linux内核设计与实现》读书笔记 - 目录 (完结)

    读完这本书回过头才发现, 第一篇笔记居然是 2012年8月发的, 将近一年半的时间才看完这本书(汗!!!). 为了方便以后查看, 做个<Linux内核设计与实现>读书笔记 的目录: < ...

  5. 读《Linux内核设计与实现》我想到了这些书

          从题目中可以看到,这篇文章是以我读<Linux内核设计与实现>而想到的其他我读过的书,所以,这篇文章的主要支撑点是<Linux内核>.       开始读这本书已经 ...

  6. 读 Linux内核设计与实现 我想到了这些书

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴!     ...

  7. Linux内核设计与实现学习笔记目录

    **注:**这是别人的笔记,我只是把目录抄过来 <Linux内核设计与实现学习笔记> 1.<Linux内核设计与实现>读书笔记(一)-内核简介 2.<Linux内核设计与 ...

  8. 《Linux内核设计与实现》读书笔记(十八)- 内核调试

    内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态. 也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态. 用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来 ...

  9. 《Linux内核设计与实现》读书笔记(十七)- 设备与模块

    本章主要讨论与linux的设备驱动和设备管理的相关的4个内核成分,设备类型,模块,内核对象,sysfs. 主要内容: 设备类型 内核模块 内核对象 sysfs 总结 1. 设备类型 linux中主要由 ...

最新文章

  1. python怎么打印出文件的内容,python怎么将打印输出日志文件
  2. ASP.NET MVC 学习6、学习使用Code First Migrations功能,把Model的更新同步到DB中
  3. 极速发展的饿了么订单系统架构演进--转
  4. hdu6380(2018 “百度之星”程序设计大赛 - 初赛(B))
  5. java与c++的区别-转
  6. 在Win10删除Ubuntu时直接删除分区后,如何删除启动项(EFI)
  7. SecureCRT日志配置
  8. 【Python】一些容易忽略的知识点
  9. int指针初始化_C++:变量,指针,引用const,extern,using,typedef,decltype关键字
  10. 不用背景图片,只用css代码实现面包屑样式
  11. 《原力计划【第二季】》第 3 周周榜揭晓!!!
  12. 从零开始搭二维激光SLAM --- 写作计划
  13. php中文件读写总结,PHP读取文件_2014.5.26的总结
  14. 西铁城手表最外圈数字是什么_手表外圈数字是什么意思 有什么作用
  15. iOS模拟器发送通知和UI测试
  16. 常见运维问题以及解决方案
  17. 飞鸽原创博客,真正的飞鸽官方博客
  18. AR涂涂乐⭐一、unity高版本ImageTarget识别图开始是空白的解决办法、UI自适度
  19. 基于51单片机的点阵贪吃蛇
  20. 最新微信合成大西瓜小游戏(合成版)源码+附带流量主功能

热门文章

  1. antd form 初始化时间
  2. vue命令行错误处理
  3. react-native页面间传递数据的几种方式
  4. 如何将视频设置为网页背景
  5. oracle在group by时某列有多个值的拼接
  6. 词云第一次实践,参考学校老师讲的一些知识点还有网上大佬的代码实现
  7. Windows 10 IoT Core 17101 for Insider 版本更新
  8. Hibernate 基础配置及常用功能(二)
  9. 玩转Win32开发(2):完整的开发流程
  10. 数据结构与算法分析-第一章Java类(02)