一、什么是文件系统
计算机的文件系统是一种存储和组织计算机数据的方法,它使得对其访问和查找变得容易,文件系统使用文件和树形目录的抽象逻辑概念,用户使用文件系统来保存数据不必关心数据实际保存在硬盘的地址为多少的数据块上,只需要记住这个文件的所属目录和文件名。
上图是linux系统的目录系形式,从以上描述可以猜测到 一个文件系统的功能有:
- 提供文件操作 读出、修改、写入的系统接口。
- 管理和调度文件的存储空间,提供文件的逻辑结构、物理结构和存储方法。
- 还有文件的保护措施,提供权限机制。
严格的说:文件系统是一套实现了数据的存储、分级组织、访问和获取等操作的抽象数据类型(Abstract data type)。
二、文件系统的分类
-
基于磁盘的文件系统(Disk-based Filesystem)
ext2、ext3、ext4、jffs2、Xfs
-
网络文件系统(Network Filesystem)
nfs
-
特殊文件系统
不同于以上两大类、不管理具体的磁盘空间。/proc文件
三、linux文件系统架构
这种图大体上描述了在linux系统上,应用层序对磁盘上的文件读写时,从上到下经历了那些事情。
四、通用文件模型
下图显示了linux操作系统中负责文件管理对象的基本组件。上半区域为用户模式,下半区域为内核模式。
标准库(glibc/uclibc):作为应用程序的运行时库,然后通过操作系统,将其转换为系统调用(system-call interface),系统调用是内核定义的接口,该接口允许用户程序的I/O操作转换为内核的接口调用,这层抽象允许用户程序的I/O操作转换为内核的接口调用。VFS提供了一个抽象层,将POSIX API接口与不同存储设备的具体接口实现进行了分离,使得底层的文件系统类型、设备类型对上层应用程序透明。
VFS :虚拟文件系统,是一个内核软件层,在具体的文件系统之上抽象的一层,用来处理Posix文件系统相关的所有调用。表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。因此,VFS 不仅充当抽象层,而且实际上它提供了一个文件系统的基本实现,可以由不同的实现来使用和扩展。所以,要了解文件系统是如何工作的,就要先了解VFS 。
注:可移植操作系统接口(英语:Portable Operating System Interface,缩写为POSIX)是IEEE为要在各种UNIX操作系统上运行软件,而定义API的一系列互相关联的标准的总称,
1、VFS介绍
VFS是一种软件机制,只存在于内存中,VFS主要的作用是对上层应用屏蔽底层不同的调用方法,提供一套统一的调用接口,二是便于对不同的文件系统进行组织管理。
VFS提供了一个抽象层,将POSIX API接口与不同存储设备的具体接口实现进行了分离,使得底层的文件系统类型、设备类型对上层应用程序透明。
例如read,write,那么映射到VFS中就是sys_read,sys_write,那么VFS可以根据你操作的是哪个“实际文件系统”(哪个分区)来进行不同的实际的操作!这个技术也是很熟悉的“钩子结构”技术来处理的。
其实就是VFS中提供一个抽象的struct结构体,然后对于每一个具体的文件系统要把自己的字段和函数填充进去,这样就解决了异构问题(内核很多子系统都大量使用了这种机制)。
2、VFS的四大对象
为了对文件系统进行统一的管理与组织,Linux创建了一个公共根目录和全局文件系统树。要访问一个文件系统中的文件,必须先将这个文件系统挂载在全局文件系统树的某个根目录下,这一挂载过程被称作文件系统的挂载,所挂载的目录称为挂载点。
传统的文件系统在磁盘上的布局
Linux为了对超级块,i节点,逻辑块这三部分进行高效的管理,Linux创建了几种不同的数据结构,分别是**文件系统类型(super_block)、inode、dentry **等几种。
-
超级块对象(superblock object)
内存:文件系统安装时创建,存放文件系统的有关信息
磁盘:对应于存放在磁盘上的文件系统控制块(filesystem control block)
超级块反映的是文件系统整体的控制信息,一个超级块对应一个文件系统。
-
索引节点对象(inode object)
内存:访问时创建,存放关于具体文件的一般信息(
inode 结构
)磁盘:对应于存放在磁盘上的文件控制块(file control block)
每个索引节点对象都有一个索引节点号,唯一地标识文件系统的文件
-
目录项对象(dentry object)
内存:目录项一旦被读入内存,VFS就会将其转换成
dentry 结构
的目录项对象磁盘:特定文件系统以特定的方式存储在磁盘上
存放目录项(即,文件名称)与对应文件进行链接的有关信息
-
文件对象(file object)
内存:打开文件时创建,存放 打开文件 与进程之间进行交互的有关信息(
file 结构
)打开文件信息,仅当进程访问文件期间存在于内核内存中。
-
目录树
综合来说,Linux 的 根文件系统(system’s root filessystem) 是内核启动mount的第一个文件系统。内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后,从中把一些基本的初始化脚本和服务等加载到内存中去运行(文件系统和内核是完全独立的两个部分)。其他文件系统,则后续通过脚本或命令作为子文件系统安装在已安装文件系统的目录上,最终形成整个目录树。
start_kernel vfs_caches_init mnt_init init_rootfs // 注册rootfs文件系统 init_mount_tree // 挂载rootfs文件系统 … rest_init kernel_thread(kernel_init, NULL, CLONE_FS);
就单个文件系统而言,在文件系统安装时,创建超级块对象;沿树查找文件时,总是首先从初识目录的中查找匹配的目录项,以便获取相应的索引节点,然后读取索引节点的目录文件,转化为dentry对象,再检查匹配的目录项,反复执行以上过程,直至找到对应的文件的索引节点,并创建索引节点对象。
a. 超级块
- 超级块对象数据结构
struct super_block {
struct list_head s_list; //将该成员设置于起始位置
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;
unsigned long s_blocksize; /* 文件系统中数据块的大小,以字节为单位 */
loff_t s_maxbytes; /* Max file size 最大文件长度 */
const struct super_operations *s_op; /* 包含了一个函数指针的结构,提供了一个接口用于处理超级快相关操作 */
const struct dquot_operations *dq_op;
const struct quotactl_ops *s_qcop;
const struct export_operations *s_export_op;
struct file_system_type *s_type;
struct sb_writers s_writers;
char s_id[32]; /* Informational name */
u8 s_uuid[16]; /* UUID */
void *s_fs_info; /* 文件系统私有数据 */
unsigned int s_max_links;
/* s_inode_list_lock protects s_inodes */
spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp;
struct list_head s_inodes; /* 所有inode的链表*/
};
- 超级块对象的操作
struct super_operations {
//该函数在给定的超级块下创建并初始化一个新的索引节点对象
struct inode *(*alloc_inode)(struct super_block *sb);
//释放指定的索引结点 。
void (*destroy_inode)(struct inode *);
//VFS在索引节点被修改时会调用此函数。
void (*dirty_inode) (struct inode *, int flags);
// 将指定的inode写回磁盘。
int (*write_inode) (struct inode *, struct writeback_control *wbc);
//删除索引节点。
int (*drop_inode) (struct inode *);
void (*evict_inode) (struct inode *);
//用来释放超级块
void (*put_super) (struct super_block *);
//使文件系统的数据元素与磁盘上的文件系统同步,wait参数指定操作是否同步。
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
//获取文件系统状态。把文件系统相关的统计信息放在statfs中
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
int (*show_devname)(struct seq_file *, struct dentry *);
int (*show_path)(struct seq_file *, struct dentry *);
int (*show_stats)(struct seq_file *, struct dentry *);
#ifdef CONFIG_QUOTA
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
#endif
int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
long (*nr_cached_objects)(struct super_block *, int);
long (*free_cached_objects)(struct super_block *, long, int);
};
b. 索引节点(inode)
索引节点inode:保存的其实是实际的数据的一些信息,这些信息称为“元数据”(也就是对文件属性的描述)。
例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。( 注意数据分成:元数据+数据本身 )
- inode节点是如何生成的
每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定(现代OS可以动态变化),一般每2KB就设置一个inode。
一般文件系统中很少有文件小于2KB的,所以预定按照2KB分,一般inode是用不完的。所以inode在文件系统安装的时候会有一个默认数量,后期会根据实际的需要发生变化。可以通过df查看文件系统中已经使用的inode数量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVvPB1xw-1686791310788)(文件系统\inode_used.png)]
注意inode号:inode号是唯一的,表示不同的文件。其实在Linux内部的时候,访问文件都是通过inode号来进行的,所谓文件名仅仅是给用户容易使用的。
当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。
- inode数据结构
当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。inodes最大数量就是文件的最大数量。
struct inode {
umode_t i_mode; /* 访问权限 */
unsigned short i_opflags;
kuid_t i_uid;/* 使用者id */
kgid_t i_gid;/* 使用组id */
unsigned int i_flags; /* 文件系统标志 */
const struct inode_operations *i_op; /* 索引节点操作表 */
struct super_block *i_sb; /* 相关的超级块 */
struct address_space *i_mapping; /* 相关的地址映射 */
/* Stat data, not accessed from path walking */
unsigned long i_ino; /* 索引节点号 */
union {
const unsigned int i_nlink;
unsigned int __i_nlink; /* 硬链接数 */
};
dev_t i_rdev; /* 实际的设备表示符号 */
loff_t i_size;
struct timespec i_atime; /* 文件最后访问时间 */
struct timespec i_mtime; /* 文件最后修改时间 */
struct timespec i_ctime; /* 节点最后改变事件 */
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes; /* 使用的字节数 */
unsigned int i_blkbits;
blkcnt_t i_blocks; /* 文件的块数 */
union {
struct hlist_head i_dentry; /* 目录项链表 */
struct rcu_head i_rcu;
};
u64 i_version;
atomic_t i_count; /* 引用计数 */
atomic_t i_dio_count;
atomic_t i_writecount; /* 写者计数 */
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock_context *i_flctx;
struct address_space i_data; /* 表示被inode读写的页面 */
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev; /* 块设备驱动节点 */
struct cdev *i_cdev; /* 字符设备驱动节点 */
char *i_link;
};
__u32 i_generation; /* 索引节点版本号 */
};
-
文件分类
普通文件、目录、符号链接、字符设备文件、块设备文件、命名管道、套接字(socket)。
字符设备文件、块设备文件、命名管道、套接字(socket)这四种是特殊文件,这些文件只有索引节点,没有数据。字符设备文件和块设备文件用来存储设备号,直接把设备号存储在索引节点中,
- 软链接只创建了索引节点,inode节点中存储了仅仅是目标文件的路径字符串,所以可以表示任意一个文件系统的文件或者目录。
- 硬链接与源文件共用同一个inode索引节点,同时将__i_nlink加一,不增加额外的空间。删除时对链接计数不减一,才将文件从磁盘中删除。
- 区别:硬链接不可跨文件系统,不可为目录创建硬链接。软连接克服了硬链接的缺点。
-
节点(inode)的相关操作
struct inode_operations {
/* 查找指定文件的dentry */
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
/* 解释inode索引节点所指定的符号链;如果该符号链是相对路径名,从指定的dir目录开始进行查找 */
void * (*follow_link) (struct dentry *, struct nameidata *);
int (*permission) (struct inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int);
int (*readlink) (struct dentry *, char __user *,int);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*create) (struct inode *,struct dentry *, umode_t, bool);
/* 创建一个新的名为new_dentry硬链接,这个新的硬连接指向dir目录下名为的old_dentry文件 */
int (*link) (struct dentry *,struct inode *,struct dentry *);
/* 从dir目录删除dentry目录项所指文件的硬链接 */
int (*unlink) (struct inode *,struct dentry *);
/* 在某个目录下,为与目录项相关的符号链创建一个新的索引节点 */
int (*symlink) (struct inode *,struct dentry *,const char *);
/* 系统调用mkdir调用,创建一个新的目录,创建时使用mode指定初始模式 */
int (*mkdir) (struct inode *,struct dentry *,umode_t);
/* 系统调用rmdir调用,删除dir目录中的dentry目录代表的文件 */
int (*rmdir) (struct inode *,struct dentry *);
/* 创建设备节点 */
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*rename2) (struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
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 *);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
int (*update_time)(struct inode *, struct timespec *, int);
} ____cacheline_aligned;
c. 目录项(dentry)
目录项是描述文件的逻辑属性,只存在于内存中,并没有实际对应的磁盘上的描述,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计。
索引节点与目录项的关系:
inode与dentry 是不同的两个概念,一个 inode 可以对应多个 dentry,即多个目录项可以指向同一个 inode,这在 Linux 文件系统中很常见。例如,当用户创建一个硬链接时,实际上就是创建了一个新的 dentry,但其所指向的 inode 与原始文件的 inode 是相同的。i_dentry是inode 的成员代表目录项链表,
注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。
例如:open一个文件/home/xxx/yyy.txt,那么/、home、xxx、yyy.txt都是一个目录项,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。
注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。
- 目录项数据结构
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* 父目录的目录项对象*/
struct qstr d_name; /* 目录项的名称 */
struct inode *d_inode; /* 与该目录项关联的inode */
unsigned char d_iname[DNAME_INLINE_LEN]; /* 文件名称 */
/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op; /* 目录项的操作函数 */
struct super_block *d_sb; /* //目录项对应的超级块 */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */
struct list_head d_lru; /* LRU list */
struct list_head d_child; /* 通过该变量链接到父目录的d_subdirs中 */
struct list_head d_subdirs; /* 本目录所有子目录链表 */
};
整个结构其实就是一棵树,目录其实就是文件再加上一层封装,这里所谓的封装主要就是增加两个指针,一个是指向父目录,一个是指向该目录所包含的所有文件(普通文件和目录)的链表头。
struct dentry_operations {
/* 该函数判断目录对象是否有效。VFS准备从dcache中使用一个目录项时,会调用该函数. */
int (*d_revalidate)(struct dentry *, unsigned int);
int (*d_weak_revalidate)(struct dentry *, unsigned int);
/* 该目录生成散列值,当目录项要加入到散列表时,VFS要调用此函数。 */
int (*d_hash)(const struct dentry *, struct qstr *);
/* 该函数来比较name1和name2这两个文件名。使用该函数要加dcache_lock锁。 */
int (*d_compare)(const struct dentry *, const struct dentry *,
unsigned int, const char *, const struct qstr *);
/* 当d_count=0时,VFS调用次函数。使用该函数要叫 dcache_lock锁。 */
int (*d_delete)(const struct dentry *);
/* 当该目录对象将要被释放时,VFS调用该函数。 */
void (*d_release)(struct dentry *);
void (*d_prune)(struct dentry *);
/* 当一个目录项丢失了其索引节点时,VFS就掉用该函数。 */
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
struct vfsmount *(*d_automount)(struct path *);
int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;
d. 文件对象
文件对象描述的是进程已经打开的文件。由open() 系统调用创建,由close()系统调用删除,多个进程同时打开和操作同一文件,存在多个对应的文件对象。
进程其实是通过文件描述符来操作文件的,每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。
- 文件描述符成员
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op; /* 指向文件操作表的指针 */
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count; /* 文件对象的使用计数 */
unsigned int f_flags; /* 打开文件时所指定的标志 */
fmode_t f_mode; /* 文件的访问模式(权限等) */
struct mutex f_pos_lock;
loff_t f_pos; /* 文件当前的位移量 */
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version; /* 版本号 */
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* 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;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping; /* 页缓存映射 */
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
重点解释一些重要字段:
- 首先,f_flags、f_mode和f_pos代表的是这个进程当前操作这个文件的控制信息。这个非常重要,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说,操作这个文件是异步的,所以这个三个字段就很重要了。
- 对于引用计数f_count,当我们关闭一个进程的某一个文件描述符时候,其实并不是真正的关闭文件,仅仅是将f_count减一,当f_count=0时候,才会真的去关闭它。对于dup,fork这些操作来说,都会使得f_count增加.
- f_op也是很重要的!是涉及到所有的文件的操作结构体。例如:用户使用read,最终都会调用file_operations中的读操作,而file_operations结构体是对于不同的文件系统不一定相同。里面一个重要的操作函数是release函数,当用户执行close时候,其实在内核中是执行release函数,这个函数仅仅将f_count减一,这也就解释了上面说的,用户close一个文件其实是将f_count减一。只有引用计数减到0才关闭文件。
- 文件方法(操作)file_operations
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 (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/* 将指定文件映射指定到地址空间上,由系统调用mmap调用 */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 指定打开文件 */
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
/* 释放已打开的文件,当打开文件的引用计数 f_count 为0时,该函数被调用 */
int (*release) (struct inode *, struct file *);
/* 文件在缓冲的数据写回磁盘 */
int (*fsync) (struct file *, loff_t, loff_t, 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 (*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 (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
3、进程与FS四大对象之间的关系
Linux中,常常用文件描述符(file descriptor)来表示一个打开的文件,这个描述符的值往往是一个大于或等于0的整数。 而这个整数,其实就是在files_struct中file数组fd的下标。 对于所有打开的文件, 这些文件描述符会存储在open_fds的位图中。
4、标准函数
VFS层提供有用的资源是用于读写数据的标准函数,这些操作对于所有的文件系统来说,在一定程度上都是相同的。
VFS流程sys_write(),Glibc提供的write()函数调用由内核的write系统调用实现,对应的系统调用函数为sys_write()定义如下:
asmlinkage long sys_write(unsigned int fd, const char __user *buf, size_t count);
sys_write()的实现在fs/read_write.c里:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
sys_write() 主要通过 vfs_write 实现数据写入。
vfs_write()函数定义如下:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE))
return -EINVAL;
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
count = ret;
file_start_write(file);
ret = __vfs_write(file, buf, count, pos);
if (ret > 0) {
fsnotify_modify(file);
add_wchar(current, ret);
}
inc_syscw(current);
file_end_write(file);
}
return ret;
}
EXPORT_SYMBOL(vfs_write);
vfs_write () 主要通过 __vfs_write() 实现数据写入。
__vfs_write ()函数定义如下:
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
new_sync_write
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = *ppos;
iov_iter_init(&iter, WRITE, &iov, 1, len);
ret = filp->f_op->write_iter(&kiocb, &iter);
BUG_ON(ret == -EIOCBQUEUED);
if (ret > 0)
*ppos = kiocb.ki_pos;
return ret;
}
file->f_op->write_iter 不同的问价系统由不同对应的函数,Ext4为函数ext4_write_iter,xfs文件系统为xfs_file_write_iter
如ext4
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = generic_file_read_iter,
.write_iter = ext4_file_write_iter,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
五、块设备驱动
1、块设备驱动框架
-
通用块层:
由于不同块设备(如磁盘,机械硬盘等)有着不同的设备驱动程序,为了让文件系统有统一的读写块设备接口,Linux实现了一个
通用块层
。- 引入为了提供一个统一的接口让文件系统实现者使用,而不用关心不同设备驱动程序的差异,这样实现出来的文件系统就能用于任何的块设备。
- 将对不同块设备的操作转换成对逻辑数据块的操作,也就是将不同的块设备都抽象成是一个数据块数组,而文件系统就是对这些数据块进行管理。如下图:
注意:不同的文件系统可能对逻辑数据块定义的大小不一样,比如 ext2文件系统 的逻辑数据块大小为 4KB。
通过对设备进行抽象后,不管是磁盘还是机械硬盘,对于文件系统都可以使用相同的接口对逻辑数据块进行读写操作。
-
I/O调度层
当我们使用 read() 和 write() 系统调用向内核提交读写文件操作时,内核并不会立刻向硬盘发送 I/O 请求,而是先将 I/O 请求交给 I/O 调度层进行排序和合并处理。经过 I/O 调度层加工处理后,才会将 I/O 请求发送给块设备驱动进行最终的 I/O 操作。I/O 调度层主要完成的工作如下:
- 对新提交的 I/O 请求进行排序。
- 如果新的 I/O 请求能与旧的 I/O 请求进行合并,那么将会把两个 I/O 请求合并成一个 I/O 请求。
- 向块设备驱动层发送 I/O 请求。
(电梯调度算法:https://www.zhihu.com/question/19747924/answer/2128252878)
块设备驱动框架
2、重点结构体
-
struct gendisk 表示一个磁盘设备或一个分区
struct gendisk { int major; /* 主设备号 */ int first_minor; /*第 1 个次设备号*/ int minors; /* 最大的次设备数,如果不能分区,则为 1*/ char disk_name[32]; /* 设备名称,将来显示在/proc/partitions、sysfs中 */ struct hd_struct **part; /* 磁盘上的分区信息 */ struct block_device_operations *fops; /*块设备操作结构体*/ struct request_queue *queue; /*请求队列*/ void *private_data; /*私有数据*/ sector_t capacity; /*扇区数, 512 字节为 1 个扇区*/ };
struct gendisk结构体在内核中用于表示块设备或者分区,如果是同一块设备的不同分区,则各个分区共享同一主设备号,次设备号不同;向内核注册块设备就是构建一个struct gendisk结构体,然后调用注册函数进行注册;
-
struct request_queue request形成的队列,将所有的请求列成一个队列
struct request_queue { ... /* 保护队列结构体的自旋锁 */ spinlock_t _ _queue_lock; spinlock_t *queue_lock; /* 队列 kobject */ struct kobject kobj; /* 队列设置 */ unsigned long nr_requests; /* 最大的请求数量 */ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching; unsigned short max_sectors; /* 最大的扇区数 */ unsigned short max_hw_sectors; unsigned short max_phys_segments; /* 最大的段数 */ unsigned short max_hw_segments; unsigned short hardsect_size; /* 硬件扇区尺寸 */ unsigned int max_segment_size; /* 最大的段尺寸 */ unsigned long seg_boundary_mask; /* 段边界掩码 */ unsigned int dma_alignment; /* DMA 传送的内存对齐限制 */ struct blk_queue_tag *queue_tags; atomic_t refcnt; /* 引用计数 */ };
- 在块设备驱动框架中,将应用对块设备的操作用请求来表达,然后用一个请求队列来管理这些请求;
- 请求队列会保存用于描述这个设备能够支持的请求的类型信息、它们的最大大小、多少不同的段可进入一个请求、硬件扇区大小、对齐要求等参数,需要保证不会向设备提交一个不能处理的请求;
- 请求队列和IO调度器紧密相关,I/O调度器会对请求队列中的请求进行排序或者合并,使得以最优的性能向设备驱动提交I/O请求;
-
struct request 对设备的每一次操作(譬如读或者写一个扇区)
struct request { struct list_head queuelist;/*请求链表*/ struct list_head donelist; request_queue_t *q; /*请求所属队列*/ unsigned int cmd_flags; enum rq_cmd_type_bits cmd_type; sector_t sector; /* 当前扇区 */ sector_t hard_sector; /*要传输的下一个扇区*/ unsigned long nr_sectors; unsigned long hard_nr_sectors; unsigned int current_nr_sectors;/*当前传送的扇区*/ unsigned int hard_cur_sectors; /*当前要被完成的扇区数目*/ struct bio *bio; /*请求的block i/o(bio)结构体链表首 request请求 第一个bio*/ struct bio *biotail;/*请求的bio结构体链表尾 request请求 最后个bio*/ char *buffer; /*request请求中断 第一个bio*/ int ref_count;/*引用计数*/ ..................... };
-
struct bio 通用块层用bio来管理一个请求
struct bio { sector_t bi_sector; /* 要传输的第一个扇区 */ struct bio *bi_next; /* 下一个 bio */ struct block_device *bi_bdev; unsigned long bi_flags; /* 状态、命令等 */ unsigned long bi_rw; /* 低位表示 READ/WRITE,高位表示优先级*/ struct bvec_iter bi_iter; /* 迭代器,标明数据要操作的块设备的位置 */ unsigned short bi_vcnt; /* bio_vec 数量 */ unsigned short bi_idx; /* 当前 bvl_vec 索引 */ /*不相邻的物理段的数目*/ unsigned short bi_phys_segments; /*物理合并和 DMA remap 合并后不相邻的物理段的数目*/ unsigned short bi_hw_segments; unsigned int bi_size; /* 以字节为单位所需传输的数据大小 */ /* 为了明了最大的 hw 尺寸,我们考虑这个 bio 中第一个和最后一个 虚拟的可合并的段的尺寸 */ unsigned int bi_hw_front_size; unsigned int bi_hw_back_size; unsigned int bi_max_vecs; /* 我们能持有的最大 bvl_vecs 数 */ struct bio_vec *bi_io_vec; /* 实际的 vec 列表 */ };
3、结构体之间的关联
- 把应用程序操作块设备的动作用request来表达,每个块设备都有一个request_queue队列来管理对块设备的request;
- 由于I/O调度器的优化,可能合并请求,所以一个request可能包含多个bio,也就是一个请求包含多个I/O操作;
4、重要函数
- register_blkdev(kernel/block/genhd.c,属于通用块层),内核提供的注册块设备驱动的注册接口,在块设备驱动框架中的地位,等同于register_chrdev在字符设备驱动框架中的地位。
- blk_init_queue 用来实例化产生一个等待队列,将来应用层对本块设备所做的所有的读写操作,都会生成一个request然后被加到这个等待队列中来。函数接收2个参数,第一个是等待队列的回调函数(do_my_ramblock_request),这个函数是驱动提供的用来处理等待队列中的request的函数(IO调度层通过电梯算法从等待队列中取出一个request,就会调用这个回调函数来处理这个请求),第二个参数是一个自旋锁,这个自旋锁是要求我们驱动提供给等待队列去使用的。
- blk_fetch_request函数是IO调度层提供的接口,作用是从request_queue中(按照电梯算法)取出一个(算法认为当前最应该去被执行的一个请求,是被算法排序、合并后的)请求,取出的请求其实就是当前硬件(块设备)最应该去执行的那个读写操作。一个请求对应一次读写操作。
- blk_rq_pos:获取request结构的current_nr_sector (当前传送的扇区),得到要写入磁盘的数据大小,扇区为单位。
- blk_rq_cur_bytes:获取request结构的sector(sector),得到要写入磁盘的扇区位置,扇区为单位。
- rq_data_dir :获取request申请结构体的命令标志(cmd_flags成员),当返回READ(0)表示读扇区命令,否则为写扇区命令
- bio_data: 该函数返回数据缓冲区的内核虚拟地址.
5、块设备驱动的一般步骤
- 分配一个gendisk结构体
- 设置参数
- 分配/设置队列: 提供读写能力
- 设置其他属性: 比如容量
- 硬件相关操作
- 向内核注册驱动
6、块设备驱动示例(ramblock):
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/timer.h>
#include <linux/types.h> /* size_t */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/hdreg.h> /* HDIO_GETGEO */
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/buffer_head.h> /* invalidate_bdev */
#include <linux/bio.h>
#define RAMBLOCK_SIZE (1024 * 1024) // 1MB,2048扇区
static struct gendisk *my_ramblock_disk; // 磁盘设备的结构体
static struct request_queue *my_ramblock_queue; // 等待队列
static DEFINE_SPINLOCK(my_ramblock_lock); // 自旋锁
static int major;
static unsigned char *my_ramblock_buf; // 虚拟块设备的内存指针
static void do_my_ramblock_request(struct request_queue *q)
{
struct request *req;
static int r_cnt = 0; // 实验用,打印出驱动读与写的调度方法
static int w_cnt = 0;
/* 从request_queue中(按照电梯算法取出一个算法当前认为最应该被去执行的一个请求。操作当前硬件) */
req = blk_fetch_request(q);
while (NULL != req)
{
unsigned long start = blk_rq_pos(req) * 512;
unsigned long len = blk_rq_cur_bytes(req);
if (rq_data_dir(req) == READ) // 判断是读请求还是写请求
{
// 读请求
// memcpy(req->buffer, my_ramblock_buf + start, len); //读操作,
memcpy(bio_data(req->bio), my_ramblock_buf + start, len); // 读操作,
printk("do_my_ramblock-request read %d times\n", r_cnt++);
}
else
{
// 写请求
// memcpy( my_ramblock_buf+start, req->buffer, len); //写操作
memcpy(my_ramblock_buf + start, bio_data(req->bio), len); // 写操作
printk("do_my_ramblock request write %d times\n", w_cnt++);
}
if (!__blk_end_request_cur(req, 0))
{
req = blk_fetch_request(q);
}
}
}
static int blk_ioctl(struct block_device *dev, fmode_t no, unsigned cmd, unsigned long arg)
{
return -ENOTTY;
}
static int blk_open(struct block_device *dev, fmode_t no)
{
printk("blk mount succeed\n");
return 0;
}
static int blk_release(struct gendisk *gd, fmode_t no)
{
printk("blk umount succeed\n");
return 0;
}
static const struct block_device_operations my_ramblock_fops =
{
.owner = THIS_MODULE,
.open = blk_open,
.release = blk_release,
.ioctl = blk_ioctl,
};
static int my_ramblock_init(void)
{
major = register_blkdev(0, "my_ramblock");
if (major < 0)
{
printk("fail to regiser my_ramblock\n");
return -EBUSY;
}
// 实例化
my_ramblock_disk = alloc_disk(1); // 次设备个数 ,分区个数 +1
// 分配设置请求队列,提供读写能力
my_ramblock_queue = blk_init_queue(do_my_ramblock_request, &my_ramblock_lock);
// 设置硬盘属性
my_ramblock_disk->major = major;
my_ramblock_disk->first_minor = 0;
my_ramblock_disk->fops = &my_ramblock_fops;
sprintf(my_ramblock_disk->disk_name, "my_ramblcok"); // /dev/name
my_ramblock_disk->queue = my_ramblock_queue;
set_capacity(my_ramblock_disk, RAMBLOCK_SIZE / 512);
/* 硬件相关操作 */
my_ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL);
add_disk(my_ramblock_disk); // 向驱动框架注册一个disk或者一个partation的接口
return 0;
}
static void my_ramblock_exit(void)
{
unregister_blkdev(major, "my_ramblock");
del_gendisk(my_ramblock_disk);
put_disk(my_ramblock_disk);
blk_cleanup_queue(my_ramblock_queue);
kfree(my_ramblock_buf);
}
module_init(my_ramblock_init);
module_exit(my_ramblock_exit);
MODULE_LICENSE("GPL");
7、驱动的使用与演示
-
装载驱动 查看驱动装载情况。
-
格式化:mkfs.vfat /dev/my_ramblock
-
挂载:mount /dev/my_ramblcok /share,之后在/share进行的操作(创建文件,写内容),都是在此块
-
设备进行操作
-
卸载:umount /tmp,卸载之后,/tmp没有之前的文件
-
再次挂载:依旧能看到之前的文件。
注:重点注意在写块设备驱动时的发送的请求是什么样的。