1 打开文件
进程读写文件之前需要打开文件,得到文件描述符,然后通过文件描述符读写文件.
1.1 编程接口
内核提供了两个打开文件的系统调用open 和 openat.
1.2 技术原理
打开文件的主要步骤如下:
(1)需要在父目录的数据中查找文件对应的目录项,从目录项得到索引节点的编号,然后在内存中创建索引节点的副本.因为各种文件系统类型的物理结构不同,所以需要提供索引节点操作集合的lookup方法和文件操作集合的open方法.
(2)需要分配文件的一个打开实例--file结构体,关联到文件的索引节点.
(3)在进程的打开文件表中分配一个文件描述符,把文件描述符和打开实例的映射添加到进程的打开文件表中.
2 关闭文件
进程可通过使用系统调用close关闭文件.
系统调用close的执行流程如下:
(1)解除打开文件表和file实例的关联.
(2)在close_on_exec位图中清楚文件描述符对应的位.
(3)释放文件描述符,在文件描述符位图中清除文件描述符对应的位.
(4)调用函数fput释放file实例:把引用计数减1,如果引用计数是0,那么把file实例添加到链表delayed_fput_list中,然后调用延迟工作项delayed_fput_work.
延迟工作项delayed_fput_work的处理函数是flush_delayed_fput,遍历链表delayed_fput_list,针对每个file实例,调用函数__fput来加以释放.
3 创建文件
3.1 使用方法
创建不同类型的文件,需要使用不同的命令.
(1)普通文件:touch FILE,这条命令本来用来更新文件的访问时间和修改时间,如果文件不存在,创建文件.
(2)目录:mkdir DIRECTORY.
(3)符号链接(软链接):ln -s TARGET LINK_NAME或ln --symbolic TARGET LINK_NAME.
(4)字符或块设备文件:mknod NAME TYPE [MAJOR MINOR].
(5)命名管道:mkpipe NAME.
(6)硬连接:命令"ln TARGET LINK_NAME".给已经存在的文件增加新的名称,文件的索引节点有一个硬链接计数,如果文件有n个名称,那么硬链接计数是n.
3.2 技术原理
创建文件需要在文件系统中分配一个索引节点,然后在父目录的数据中增加一个目录项来保存文件的名称和索引节点编号.
4 删除文件
4.1 使用方法
删除文件的命令如下:
(1)删除任何类型文件:unlink FILE.
(2)rm FILE,默认不删除目录,如果使用"-r""-R"或"-recursive",可以删除目录和目录的内容.
(3)删除目录:rmdir DICTIONARY.
内核提供了unlink,unlinkat用来删除文件的名称,如果文件的硬链接计数变成0,并且没有进程打开这个文件,那么删除文件.提供了rmdir删除目录.
4.2 技术原理
删除文件需要从父目录的数据中删除文件对应的目录项,把文件的索引节点的硬链接计数减1(一个文件可以有多个名称,Linux把文件名称称为硬链接),如果索引节点的硬链接计数变成0,那么释放索引节点.因为各种文件系统的物理结构不同,所以需要提供索引节点操作集合的unlink方法.
5 设置文件权限
5.1 使用方法
设置文件权限的命令如下:
(1)chmod [OPTION]... MODE[, MODE]... FILE...
mode : 权限设定字串,格式[ugoa...][[+-=][rwxX]...][,...]
其中:
- u 表示该文件的拥有者,g 表示与该文件的拥有者属于同一个群体(group)者,o 表示其他以外的人,a 表示这三者皆是.
- +表示增加权限、- 表示取消权限、= 表示唯一设定权限.
- r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当该文件是个子目录或者该文件已经被设定过为可执行, s表示当文件被执行时,根据who参数指定的用户类型设置文件的setuid或者setgid权限。
(2)chmod [OPTION]... OCTAL-MODE FILE...
参数OCTAL-MODE是八进制数值.
系统调用chmod负责修改文件权限.
5.2 技术原理
修改文件权限需要修改文件的索引节点的文件模式字段,文件模式字段包含文件类型和访问权限.因为各种文件系统类型的索引节点不同,所以需要提供索引节点操作集合的setattr方法.
6 页缓存
访问外部存储设备的速度很慢,为了避免每次读写文件时访问外部存储设备,文件系统模块为每个文件在内存中创建一个缓存,因为缓存的单位是页,所以称为页缓存.
(1)索引节点的成员i_mapping指向地址空间结构体(address_space).进程在打开文件的时候,文件打开实例(file结构体)的成员f_mapping也会指向文件的地址空间.
(2)每个文件有一个地址空间结构体address_space,成员page_tree的类型是结构体radix_tree_root:成员gfp_mask是分配内存页的掩码,成员rnode指向基数树的根节点.
(3)使用基数树管理页缓存,把文件的页索引映射到内存页的页描述符.
6.1 地址空间
每个文件都有一个地址空间结构体address_space,用来建立数据缓存(在内存中为某种数据创建的缓存)和数据来源(即存储设备)之间的关联.结构体address_space如下:
struct address_space {
struct inode *host; /* owner: inode, block_device 指向索引节点*/
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and lock protecting it */
atomic_t i_mmap_writable;/* count VM_SHARED mappings */
struct rb_root i_mmap; /* tree of private and shared mappings */
struct rw_semaphore i_mmap_rwsem; /* protect tree, count, list */
/* Protected by tree_lock together with the radix tree */
unsigned long nrpages; /* number of total pages */
unsigned long nrshadows; /* number of shadow entries */
pgoff_t writeback_index;/* writeback starts here */
const struct address_space_operations *a_ops; /* methods 指向地址空间操作集合*/
unsigned long flags; /* error bits/gfp mask */
spinlock_t private_lock; /* for use by the address_space */
struct list_head private_list; /* ditto */
void *private_data; /* ditto */
} __attribute__((aligned(sizeof(long))));
struct radix_tree_root {
unsigned int height;
gfp_t gfp_mask;/*分配内存页的掩码*/
struct radix_tree_node __rcu *rnode; /*rnode指向基数树的根节点*/;
};
地址空间操作结合address_space_operations的主要成员如下:
struct address_space_operations {
/* 用来把文件的一页写到存储设备 */
int (*writepage)(struct page *page, struct writeback_control *wbc);
/* 用来把文件的一页从存储设备读到内存 */
int (*readpage)(struct file *, struct page *);
/* Write back some dirty pages from this mapping. 用来把文件的多个脏页写到储存设备 */
int (*writepages)(struct address_space *, struct writeback_control *);
/* Set a page dirty. Return true if this dirtied it. 用来给文件的一页设置脏标记,表示数据被修改过, 还没写回到设备*/
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
int (*write_begin)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata);
int (*write_end)(struct file *, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata);
/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
sector_t (*bmap)(struct address_space *, sector_t);
void (*invalidatepage) (struct page *, unsigned int, unsigned int);
int (*releasepage) (struct page *, gfp_t);
void (*freepage)(struct page *);
ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter, loff_t offset);
/*
* migrate the contents of a page to the specified target. If
* migrate_mode is MIGRATE_ASYNC, it must not block.
*/
int (*migratepage) (struct address_space *,
struct page *, struct page *, enum migrate_mode);
int (*launder_page) (struct page *);
int (*is_partially_uptodate) (struct page *, unsigned long,
unsigned long);
void (*is_dirty_writeback) (struct page *, bool *, bool *);
int (*error_remove_page)(struct address_space *, struct page *);
/* swapfile support */
int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
sector_t *span);
void (*swap_deactivate)(struct file *file);
};
6.2 编程接口
页缓存的常用操作函数如下:
(1)函数find_get_page根据文件的页索引在页缓存中查找内存页.
static inline struct page *find_get_page(struct address_space *mapping,
pgoff_t offset);
(2)函数find_or_create_page根据文件的页索引在页缓存中查找内存页,如果没有找到内存页,那么分配一个内存页,然后添加到页缓存中.
static inline struct page *find_or_create_page(struct address_space *mapping,
pgoff_t offset, gfp_t gfp_mask);
(3)函数add_to_page_cache_lru把一个内存页添加到页缓存和LRU链表中.
int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
pgoff_t index, gfp_t gfp_mask);
(4)函数delete_from_page_cache从页缓存中删除一个内存页.
void delete_from_page_cache(struct page *page);
7 读文件
7.1 编程接口
进程读文件的方式有3种:
(1)调用内核提供的读文件的系统调用.
(2)调用glibc库封装的读文件的标准I/O流函数.
(3)创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接读内存.
第2种方式在用户空间创建了缓冲区,能减少系统调用的次数,提高性能.第3种方式可以避免系统调用,性能最高.
7.2 技术原理
读文件的主要步骤如下:
(1)调用具体文件系统类型提供的文件操作集合的read和read_iter方法来读文件.
(2)read或read_iter方法根据页索引在文件的页缓存中查找页,如果没有找到,那么调用具体文件系统类型提供的地址空间集合的readpage方法来从存储设备读取文件页到内存中.
为了提高读文件的速度,从存储设备读取文件页到内存中的时候,除了读取请求的文件页,还会预读后面的文件页.如果进程按顺序读文件,预读文件页可以提高读文件的速度;如果进程随机读文件,预读文件页对提高读文件的速度帮助不大.
8 写文件
8.1 编程接口
进程写文件的方式有3种:
(1)调用内核提供的写文件的系统调用.
(2)调用glibc库封装的写文件的标准I/O流函数.
(3)创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟空间,然后直接写内存.
第2种方式在用户空间创建了缓冲区,能够减少系统调用的次数,提高性能.第3种方式可以避免系统调用,性能最高.
8.2 技术原理
写文件的主要步骤如下:
(1)调用具体文件系统类型提供的文件操作集合的write或write_iter方法来写文件.
(2)write或write_iter方法调用文件的地址空间操作集合的write_begin方法,在页缓存查找页,如果页不存在就分配页;然后把数据从用户缓冲区复制到页缓存的页中;最后调用文件的地址空间操作集合的write_end方法.
9 文件回写
进程写文件时,内核的文件系统模块把数据写到文件的页缓存,没有立即写回到存储设备.文件系统模块会定期把脏页写回到存储设备,进程也可以调用系统调用把脏页强制写回到存储设备.
9.1 编程接口
管理员可以执行命令"sync",把内存中所有修改过的文件元数据和文件数据写回到存储设备.
内核提供了sync,syncfs,fsync,fdatasync,sync_file_range等系统调用用于文件写回.
9.2 技术原理
把文件写回到存储设备的时机如下:
(1)周期回写.
(2)当脏页的数量达到限制的时候,强制回写.
(3)进程调用sync和syncfs等系统调用.
10 DAX
对于类似内存的块设备,例如NVDIMM设备,不需要把文件从存储设备复制到页缓存.DAX绕过页缓存,直接访问存储设备,对于基于文件的内存映射,直接把存储设备映射到进程的虚拟地址空间.
调用系统调用mmap创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,这会调用具体文件系统类型提供的文件操作集合的mmap方法.mmap方法针对设置了标志位S_DAX的索引节点,处理方法如下:
(1)给虚拟内存区域设置标志位VM_MIXEDMAP和VM_HUGEPAGE.
(2)设置虚拟内存操作集合,提供fault,huge_fault,page_mkwrite和pfn_mkwrite方法.