Linux VFS文件系统分析4(基于Linux6.6)---VFS与sys_open接口介绍
一、概述
下面将从进程中的文件相关参数与文件系统中的参数如何进行关联的角度进行概述。
1. 进程中的文件相关参数
在 Linux 中,进程访问文件的操作主要是通过文件描述符(File Descriptor, FD)来实现的。每个进程有自己的文件描述符表,其中保存着进程打开的文件的信息。进程与文件系统之间的文件操作是通过这些文件相关参数来完成的。
文件描述符表
每个进程在内核中都有一个文件描述符表,用于管理进程打开的文件。该表包含了文件描述符和文件结构体之间的映射关系。文件描述符是进程在文件系统中标识文件的整数索引。
-
文件描述符(FD):文件描述符是进程打开文件时分配的一个整数,用于标识该文件。文件描述符在进程的文件描述符表中有对应的条目。常见的文件描述符有标准输入(0)、标准输出(1)和标准错误输出(2)。
-
文件结构体(
struct file
):每个文件描述符都与一个struct file
结构体相关联。该结构体包含了关于文件的信息,如文件偏移量、文件操作方法、文件系统特定的数据等。
文件结构体 struct file
struct file
是一个描述文件的核心数据结构,包含了文件的状态信息、文件操作接口以及文件的底层数据指针。该结构体位于内核中,与文件的访问、读取、写入等操作紧密相关。
struct file {
const struct file_operations *f_op; // 文件操作接口
unsigned int f_flags; // 文件标志
struct dentry *f_dentry; // 文件的目录项
struct inode *f_inode; // 文件对应的 inode
off_t f_offset; // 文件的当前偏移量
// 其他字段...
};
- f_op:指向文件操作函数的指针,定义了对文件进行操作的接口(例如
read()
,write()
等)。 - f_flags:文件的状态标志,如是否是只读、是否以追加模式打开等。
- f_dentry:指向文件的目录项,包含路径信息,帮助内核找到文件在文件系统中的位置。
- f_inode:指向文件的 inode,保存文件的元数据(如权限、大小、类型等)。
- f_offset:文件的当前偏移量,指示下一次读取或写入操作的位置。
进程的文件描述符表
每个进程在内核中都有一个文件描述符表(files
),该表维护着一个指向打开文件的 struct file
结构体的数组。进程通过文件描述符(从 0 开始递增)来索引文件描述符表中的文件。
struct task_struct {
struct fs_struct *fs; // 文件系统结构
struct files_struct *files; // 文件描述符表
};
2. 文件系统中的参数
文件系统在 Linux 内核中提供了对文件的管理和存储功能。文件系统主要通过 inode
、superblock
和 dentry
等结构来管理文件。进程与文件系统之间的文件操作实际上是通过这些结构进行的。
inode(索引节点)
每个文件在文件系统中都有一个对应的 inode
结构,inode
主要包含文件的元数据,如文件类型、权限、所有者、文件大小、时间戳、数据块指针等。inode
并不存储文件名,而是通过 dentry
来关联文件名。
struct inode {
unsigned int i_mode; // 文件类型和权限
unsigned long i_ino; // inode号
unsigned int i_nlink; // 链接计数
off_t i_size; // 文件大小
struct timespec i_atime; // 最后访问时间
struct timespec i_mtime; // 最后修改时间
struct timespec i_ctime; // 最后状态改变时间
struct block_device *i_bdev; // 与文件相关的块设备
// 其他字段...
};
- i_mode:文件的类型和权限。
- i_size:文件的大小。
- i_nlink:文件的硬链接计数。
- i_atime, i_mtime, i_ctime:分别表示最后访问时间、最后修改时间、最后状态改变时间。
dentry(目录项)
dentry
是 Linux 内核中用于表示文件路径的结构。它将文件路径(如 /home/user/file
)映射到文件的 inode
。每个文件或目录都有一个 dentry
结构,用于在路径查找过程中提供缓存和加速访问。
struct dentry {
struct inode *d_inode; // 文件的 inode
struct dentry *d_parent; // 父目录项
const char *d_name; // 文件名
// 其他字段...
};
- d_inode:指向文件的
inode
,提供文件的元数据。 - d_name:文件的名称。
- d_parent:指向父目录的
dentry
,构成目录的层级结构。
superblock(超级块)
superblock
结构保存了文件系统的元数据,如文件系统的类型、大小、块设备信息等。每个挂载的文件系统都有一个对应的 superblock
结构。
struct super_block {
struct file_system_type *s_type; // 文件系统类型
unsigned long s_blocksize; // 块大小
struct block_device *s_bdev; // 块设备
// 其他字段...
};
3. 进程与文件系统的关联
进程中的文件相关参数与文件系统中的参数是通过一系列的内核数据结构关联起来的。这些数据结构通过虚拟文件系统(VFS)层进行协同工作。具体的关联过程如下:
-
进程文件描述符与
struct file
:进程通过文件描述符(FD)访问打开的文件,文件描述符指向进程的文件描述符表中的struct file
结构,而struct file
中又包含了文件的dentry
(目录项)和inode
(索引节点)。因此,进程操作的文件实际上是与文件系统中的数据结构进行交互的。 -
文件描述符表与
dentry
和inode
:每个进程的文件描述符表中的每个条目都会关联到一个struct file
结构。struct file
中的f_dentry
和f_inode
分别指向文件的目录项和索引节点,这些数据结构是文件系统用于管理文件的关键部分。 -
VFS 层的抽象:VFS 层为进程提供了一个统一的接口,通过
sys_open()
、sys_read()
、sys_write()
等系统调用,进程可以对文件进行操作,而这些操作会通过 VFS 层将请求转发到具体的文件系统(如 ext4、NFS 等),并通过inode
、dentry
和superblock
等结构与底层存储进行交互。
二、进程中的文件相关参数与文件系统中参数的关联
在应用程序打开一个文件时,系统调用函数sys_open则用于struct file指针变量的创建,inode节点的查找以及与struct file变量的关联等。
sys_open函数的调用流程如上所示,首先调用get_unused_fd_flags获取一样未使用的fd。该接口的定义如下
fs/file.c
int get_unused_fd_flags(unsigned flags)
{
return __get_unused_fd_flags(flags, rlimit(RLIMIT_NOFILE));
}
EXPORT_SYMBOL(get_unused_fd_flags);
int __get_unused_fd_flags(unsigned flags, unsigned long nofile)
{
return alloc_fd(0, nofile, flags);
}
主要是调用alloc_fd实现fd的申请,我们分析下这个函数的实现,如下所示该函数所做的工作大致如下:
1.查找一个可用的fd(若没有可用fd,或者查找的fd值大于当前进程可打开的fd,则返回失败)
2.若查找到可用fd,则调用expand_files,确认是否需要扩展fdt
3.程序返回。
fs/file.c
/*
* allocate a file descriptor, mark it busy.
*/
static int alloc_fd(unsigned start, unsigned end, unsigned flags)
{
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
fd = start;
if (fd < files->next_fd)
fd = files->next_fd;
if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd);
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
error = -EMFILE;
if (fd >= end)
goto out;
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;
if (start <= files->next_fd)
files->next_fd = fd + 1;
__set_open_fd(fd, fdt);
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt);
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
expand_files的作用如下:
- 若nr小于fd->max_fds,则无需扩展fdt;
- 判断nr是否大于当前进程可打开的文件数;若大于则返回失败;
- 若非以上两种情况,则需要扩展fdt,调用expand_fdtable扩展fdt。
fs/file.c
static int expand_files(struct files_struct *files, unsigned int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *fdt;
int expanded = 0;
repeat:
fdt = files_fdtable(files);
/* Do we need to expand? */
if (nr < fdt->max_fds)
return expanded;
/* Can we expand? */
if (nr >= sysctl_nr_open)
return -EMFILE;
if (unlikely(files->resize_in_progress)) {
spin_unlock(&files->file_lock);
expanded = 1;
wait_event(files->resize_wait, !files->resize_in_progress);
spin_lock(&files->file_lock);
goto repeat;
}
/* All good, so we try */
files->resize_in_progress = true;
expanded = expand_fdtable(files, nr);
files->resize_in_progress = false;
wake_up_all(&files->resize_wait);
return expanded;
}
expand_fdtab主要是根据新的nr,重新申请进程描述符中的fdt指针变量及fdt->fd、fdt->open_fds等
fs/file.c
static int expand_fdtable(struct files_struct *files, unsigned int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *new_fdt, *cur_fdt;
spin_unlock(&files->file_lock);
new_fdt = alloc_fdtable(nr);
/* make sure all fd_install() have seen resize_in_progress
* or have finished their rcu_read_lock_sched() section.
*/
if (atomic_read(&files->count) > 1)
synchronize_rcu();
spin_lock(&files->file_lock);
if (!new_fdt)
return -ENOMEM;
/*
* extremely unlikely race - sysctl_nr_open decreased between the check in
* caller and alloc_fdtable(). Cheaper to catch it here...
*/
if (unlikely(new_fdt->max_fds <= nr)) {
__free_fdtable(new_fdt);
return -EMFILE;
}
cur_fdt = files_fdtable(files);
BUG_ON(nr < cur_fdt->max_fds);
copy_fdtable(new_fdt, cur_fdt);
rcu_assign_pointer(files->fdt, new_fdt);
if (cur_fdt != &files->fdtab)
call_rcu(&cur_fdt->rcu, free_fdtable_rcu);
/* coupled with smp_rmb() in fd_install() */
smp_wmb();
return 1;
}
三、举例应用
3.1、进程打开文件 (open
系统调用)
当一个进程通过 open
系统调用打开文件时,内核会执行一系列步骤,通过进程的文件描述符表与文件系统的相关数据结构(如 inode
、dentry
)进行关联。
系统调用流程
假设一个进程调用 open("/home/user/test.txt", O_RDONLY)
来打开文件。
-
文件描述符:内核为这个文件分配一个文件描述符(比如 3)。该文件描述符会存储在进程的文件描述符表中。
-
struct file
:内核会创建一个struct file
结构体,并将其与该文件描述符关联。struct file
包含了文件的操作函数(如read
、write
),文件的偏移量、文件标志等。 -
dentry
:内核查找文件路径/home/user/test.txt
,并在文件系统中找到对应的目录项dentry
,它包含了文件的路径信息,并将其与struct file
关联。 -
inode
:内核还会找到文件对应的inode
,它包含了文件的元数据,如文件的大小、权限、时间戳等。
这些数据结构的关系如下:
struct file {
const struct file_operations *f_op; // 文件操作接口
unsigned int f_flags; // 文件标志
struct dentry *f_dentry; // 文件的目录项
struct inode *f_inode; // 文件的 inode
off_t f_offset; // 文件的当前偏移量
// 其他字段...
};
struct dentry {
struct inode *d_inode; // 文件的 inode
struct dentry *d_parent; // 父目录项
const char *d_name; // 文件名
};
struct inode {
unsigned int i_mode; // 文件类型和权限
unsigned long i_ino; // inode 号
unsigned int i_nlink; // 链接计数
off_t i_size; // 文件大小
struct timespec i_atime; // 最后访问时间
struct timespec i_mtime; // 最后修改时间
struct timespec i_ctime; // 最后状态改变时间
// 其他字段...
};
具体过程
-
open()
调用:进程调用open("/home/user/test.txt", O_RDONLY)
时,内核会解析路径/home/user/test.txt
。- 内核查找路径中的
dentry
结构,dentry
保存了文件的路径和指向inode
的指针。 - 如果文件存在,内核找到该文件的
inode
,并将其与打开的文件描述符绑定。
- 内核查找路径中的
-
分配文件描述符:内核会为该进程分配一个新的文件描述符(假设是 3),并在进程的文件描述符表中存储
struct file
结构。 -
返回文件描述符:
open
系统调用返回文件描述符 3,进程可以通过这个文件描述符来进行后续的读写操作。