当前位置:   article > 正文

Linux文件系统 struct file 结构体解析

struct file

一、open系统调用

1.1 简介

NAME
       open, creat - open and possibly create a file or device

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

       int creat(const char *pathname, mode_t mode);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
DESCRIPTION
       Given  a  pathname  for a file, open() returns a file descriptor, a small, nonnegative integer for use in subsequent system calls (read(2), write(2), lseek(2), fcntl(2),
       etc.).  The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.
  • 1
  • 2
  • 3

open()函数接收一个文件路径名作为参数,并返回一个文件描述符,即fd(file descriptor)。文件描述符是一个小的非负整数,用于后续的系统调用(如read()、write()、lseek()、fcntl()等)。成功调用open()后返回的文件描述符将是进程当前未使用的最低编号的文件描述符。

调用open()会创建一个新的打开文件描述符,即系统范围的打开文件表中的一个条目。该条目记录了文件的偏移量和文件状态标志(可以通过fcntl()的F_SETFL操作进行修改)。文件描述符是对这些条目的引用;如果后续对路径名进行删除或修改以引用不同的文件,该引用不受影响。新的打开文件描述符最初不与任何其他进程共享,但可能通过fork()进行共享。

从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就是一个文件句柄。那么何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描述符后,可以通过它得到用于管理文件的真正的数据结构。

使用文件描述符即句柄,有两个好处:一是增加了安全性,句柄类型对用户完全透明,用户无法通过任何hacking的方式,更改句柄对应的内部结果,比如Linux内核的文件描述符,只有内核才能通过该值得到对应的文件结构;二是增加了可扩展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时发生变化,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代码。

文件描述符fd的取值范围:文件描述符的取值范围通常是从0到系统定义的最大文件描述符值。

当Linux新建一个进程时,会自动创建3个文件描述符0、1和2,分别对应标准输入、标准输出和错误输出。C库中与文件描述符对应的是文件指针,与文件描述符0、1和2类似,我们可以直接使用文件指针stdin、stdout和stderr。意味着stdin、stdout和stderr是“自动打开”的文件指针。

在Linux系统中,文件描述符0、1和2分别有以下含义:
文件描述符0(STDIN_FILENO):它是标准输入文件描述符,通常与进程的标准输入流(stdin)相关联。它用于接收来自用户或其他进程的输入数据。默认情况下,它通常与终端或控制台的键盘输入相关联。

文件描述符1(STDOUT_FILENO):它是标准输出文件描述符,通常与进程的标准输出流(stdout)相关联。它用于向终端或控制台输出数据,例如程序的正常输出、结果和信息。

文件描述符2(STDERR_FILENO):它是标准错误文件描述符,通常与进程的标准错误流(stderr)相关联。它用于输出错误消息、警告和异常信息到终端或控制台。与标准输出不同,标准错误通常用于输出与程序执行相关的错误和调试信息。

这些文件描述符是在进程创建时自动打开的,并且可以在程序运行期间使用。它们是程序与用户、终端和操作系统之间进行输入和输出交互的重要通道。通过合理地使用这些文件描述符,程序可以接收输入、输出结果,并提供错误和调试信息,以实现与用户的交互和数据处理。
C库源码如下:

/* Standard streams.  */
extern struct _IO_FILE *stdin;		/* Standard input stream.  */
extern struct _IO_FILE *stdout;		/* Standard output stream.  */
extern struct _IO_FILE *stderr;		/* Standard error output stream.  */
/* C89/C99 say they're macros.  Make them happy.  */
#define stdin stdin
#define stdout stdout
#define stderr stderr
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

从上面的源码可以看出,stdin、stdout和stderr确实是文件指针。而C标准要求stdin、stdout和stderr是宏定义,所以在C库的代码中又定义了同名宏。

typedef struct _IO_FILE FILE;

FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;
  • 1
  • 2
  • 3
  • 4
  • 5
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
  • 1
  • 2
  • 3

DEF_STDFILE是一个宏定义,用于初始化C库中的FILE结构。这里_IO_2_1_stdin、_IO_2_1_stdout和_IO_2_1_stderr这三个FILE结构分别用于文件描述符0、1和2的初始化,这样C库的文件指针就与系统的文件描述符互相关联起来了。大家注意最后的标志位,stdin是不可写的,stdout是不可读的,而stderr不仅不可读,且没有缓存。

通过上面的分析,可以得到一个结论:stdin、stdout和stderr都是FILE类型的文件指针,是由C库静态定义的,直接与文件描述符0、1和2相关联,所以应用程序可以直接使用它们。

因此一个进程打开一个文件,fd最新从整数3开始,即fd=3。

在早期的Linux内核中,文件描述符的最大值由系统常量NR_OPEN定义,通常为1024或者更小。然而,现代的Linux内核已经采用了更高效的文件描述符管理方式,使用了更大的取值范围。最大值可根据系统配置、内核版本和系统设置而异。
我目前接触到几台机器配置都是1024,这个值可以配置:
centos7:

# uname -r
3.10.0-1160.el7.x86_64
# ulimit -n
1024
  • 1
  • 2
  • 3
  • 4
$ uname -r
5.19.0-46-generic
$ ulimit -n
1024
  • 1
  • 2
  • 3
  • 4

1.2 files_struct

1.2.1 简介

Linux的每个进程都有自己的一组打开的文件,内核会打开的文件维护一个文件表,以便维护该进程打开文件的信息,包括打开的文件个数、每个打开文件的偏移量等信息。

每个进程都需要分配一个打开文件表以及对应数量的指针数组。对于大多数进程,打开文件的数量是有限的。一种优化的设计方式是为每个进程内置分配少量数目的文件描述符指针数组,当进程确实需要更多的指针时,可另行分配,即动态扩展。为此,进程并不直接使用打开文件表,而是引入一个新的files_struct结构,将它作为进程描述符的一个域。

内核中进程对应的结构是task_struct,进程的文件表保存在task_struct->files中。

struct task_struct {
	......
	/* Open file information: */
	struct files_struct		*files;
	.....
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
	atomic_t count;
	bool resize_in_progress;
	wait_queue_head_t resize_wait;

	struct fdtable __rcu *fdt;
	struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
	spinlock_t file_lock ____cacheline_aligned_in_smp;
	unsigned int next_fd;
	unsigned long close_on_exec_init[1];
	unsigned long open_fds_init[1];
	unsigned long full_fds_bits_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

struct files_struct 是 Linux 内核中用于管理进程文件相关操作和信息的数据结构。以下是成员说明:
(1)atomic_t count:原子计数器,用于记录对该结构体的引用计数,多线程会共享files_struct 结构体,fork父子进程不共享同一个files_struct 结构体,子进程赋值拷贝和父进程一样的files_struct 结构体。进程进行fork时,父子进程的fd会指向同一个struct file。
即每个进程有唯一一个files_struct 结构体,但在线程间共享,struct file在父子进程间共享。

(2)struct fdtable __rcu *fdt:指向当前文件描述符表的指针。是一个指向 struct fdtable 结构体的指针,用于跟踪进程的文件描述符。
(3)struct fdtable fdtab:文件描述符表的副本,用于读取操作。

struct fdtable {
	unsigned int max_fds;
	struct file __rcu **fd;      /* current fd array */
	unsigned long *close_on_exec;
	unsigned long *open_fds;
	unsigned long *full_fds_bits;
	struct rcu_head rcu;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

struct fdtable的结构体,用于表示文件描述符表的数据结构:

unsigned int max_fds:表示文件描述符表的最大文件描述符数量。
struct file __rcu **fd:指向当前文件描述符数组的指针。每个元素是一个指向struct file结构体的指针,表示打开的文件。
unsigned long *close_on_exec:指向一个位图数组的指针,用于记录在进程执行时自动关闭的文件描述符。每个位表示一个文件描述符,如果对应位为1,则表示该文件描述符在执行时会自动关闭。
unsigned long *open_fds:指向一个位图数组的指针,用于记录当前打开的文件描述符。每个位表示一个文件描述符,如果对应位为1,则表示该文件描述符是打开状态。
unsigned long *full_fds_bits:指向一个位图数组的指针,用于记录已使用的文件描述符。每个位表示一个文件描述符,如果对应位为1,则表示该文件描述符已被使用。
struct rcu_head rcu:用于实现RCU(Read-Copy-Update)机制的头部,用于在释放结构体时进行资源回收。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

其中struct file __rcu **fd就是指向 fdtable.fd 是一个指针字段,指向的内存地址还是存储指针的(元素指针类型为 struct file * )。换句话说,fdtable.fd 指向一个数组,数组元素为指针(指针类型为 struct file *)。
其中 max_fds 指明数组边界。

这个struct fdtable结构体是Linux内核中用于管理进程的文件描述符表的关键数据结构之一。它记录了文件描述符的打开状态、关闭状态和相关的文件信息。通过操作这些成员,内核能够有效地管理进程的文件描述符。
(4)unsigned int next_fd:下一个可用的文件描述符值。
(5)unsigned long close_on_exec_init[1]:初始化的位图数组,表示进程初始状态下需要在执行时自动关闭的文件描述符。
(6)unsigned long open_fds_init[1]:初始化的位图数组,表示进程初始状态下已打开的文件描述符。
(7)unsigned long full_fds_bits_init[1]:初始化的位图数组,表示进程初始状态下已使用的文件描述符。
(8)struct file __rcu * fd_array[NR_OPEN_DEFAULT]:当前打开文件的数组。是一个指向 struct file 结构体的指针数组,表示打开的文件列表。

#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64

/*
 * The default fd array needs to be at least BITS_PER_LONG,
 * as this is the granularity returned by copy_fdset().
 */
#define NR_OPEN_DEFAULT BITS_PER_LONG
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

其中fdt指向fdtab,fdtab的成员fd指向fd_array。

一个进程打开的文件管理本质上就是数组管理的方式,所有打开的文件结构都在一个数组里。将fd值作为数组的索引值即可找到fd对应的打开的文件结构。

可以看到struct files_struct有两个地方来管理所有打开的文件结构,即有两个数组来管理所有打开的文件结构。
第一个地方struct fdtable里面的二维指针 fd :

struct fdtable {
	......
	struct file __rcu **fd;      /* current fd array */
	......
};
  • 1
  • 2
  • 3
  • 4
  • 5

其中注释中说明:current fd array。fd 是指向当前文件描述符数组的指针,而 fd_array 则是一个固定大小的数组,当一个进程打开的文件比较少时,用于存储文件描述符。这样的设计使得可以方便地动态分配和替换文件描述符数组,以满足需要扩展文件描述符数组的情况。

/*
 * Open file table structure
 */
struct files_struct {
	......
	struct fdtable fdtab;
	......
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

第二个地方数组变量fd_array:

/*
 * Open file table structure
 */
struct files_struct {
	......
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

采用两种方式来保存一个进程打开的struct file指针,一种是静态数组形式,一种是动态指针形式。
struct file __rcu *fd_array[NR_OPEN_DEFAULT]:这是一个固定大小的文件描述符数组。NR_OPEN_DEFAULT 是一个宏,表示默认的最大文件描述符数,这里等于64。当一个进程打开的文件比较小时,使用静态数组形式即可,那么内核访问打开文件的速度就比较快,因为这是对静态数组的操作。

如果一个进程打开的文件比较多时,内核就要重新建立数组,struct file __rcu **fd指向动态分配的更大的文件描述符数组。

简单来说:当打开的文件比较少时,使用静态数组,当打开的文件比较多时,动态重新分配动态数组。大部分情况下,一个进程打开的文件都小于64,使用静态数组即可,效率比较高。

struct file __rcu **fd初始化时指向的是struct file __rcu *fd_array[NR_OPEN_DEFAULT]的第一个数组成员,即:

.fdtab.fd		= &init_files.fd_array[0],
  • 1

不管是打开文件使用静态数组形式还是动态指针形式,访问一个进程打开的文件时,都用.fdtab.fd 去访问打开的文件。
因为fd始终指向当前文件描述符数组的指针。

用户空间进程根据fd访问内核空间的struct file结构体过程如下:

struct files_struct *files = current->files;
struct file *file;
struct fdtable *fdt;

fdt = files_fdtable(files);
file = fdt->fd[fd];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当用户使用fd与内核交互时,内核可以用fd从fdt->fd[fd]中得到内部管理文件的结构struct file。

简易图如下所示:
在这里插入图片描述

1.2.2 init_files

struct task_struct init_task
= {
	......
	.files		= &init_files,
	......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

init_task是Linux的第一个进程,即0号进程,它的文件表是一个全局变量,如下:

struct files_struct init_files = {
	.count		= ATOMIC_INIT(1),
	.fdt		= &init_files.fdtab,
	.fdtab		= {
		.max_fds	= NR_OPEN_DEFAULT,
		.fd		= &init_files.fd_array[0],
		.close_on_exec	= init_files.close_on_exec_init,
		.open_fds	= init_files.open_fds_init,
		.full_fds_bits	= init_files.full_fds_bits_init,
	},
	.file_lock	= __SPIN_LOCK_UNLOCKED(init_files.file_lock),
	.resize_wait	= __WAIT_QUEUE_HEAD_INITIALIZER(init_files.resize_wait),
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

init_files.fdt和init_files.fdtab.fd都分别指向了自己已有的成员变量,并以此作为一个默认值。后面的进程都是从init进程fork出来的。fork的时候会调用dup_fd,而在dup_fd中其代码结构如下:

/* SLAB cache for files_struct structures (tsk->files) */
struct kmem_cache *files_cachep;

/*
 * Allocate a new files structure and copy contents from the
 * passed in files structure.
 * errorp will be valid only when the returned files_struct is NULL.
 */
struct files_struct *dup_fd(struct files_struct *oldf, int *errorp)
{
	struct files_struct *newf;
	struct file **old_fds, **new_fds;
	unsigned int open_files, i;
	struct fdtable *old_fdt, *new_fdt;

	*errorp = -ENOMEM;
	newf = kmem_cache_alloc(files_cachep, GFP_KERNEL);
	if (!newf)
		goto out;

	atomic_set(&newf->count, 1);

	spin_lock_init(&newf->file_lock);
	newf->resize_in_progress = false;
	init_waitqueue_head(&newf->resize_wait);
	newf->next_fd = 0;
	new_fdt = &newf->fdtab;
	new_fdt->max_fds = NR_OPEN_DEFAULT;
	new_fdt->close_on_exec = newf->close_on_exec_init;
	new_fdt->open_fds = newf->open_fds_init;
	new_fdt->full_fds_bits = newf->full_fds_bits_init;
	new_fdt->fd = &newf->fd_array[0];

	spin_lock(&oldf->file_lock);
	old_fdt = files_fdtable(oldf);
	open_files = count_open_files(old_fdt);

	/*
	 * Check whether we need to allocate a larger fd array and fd set.
	 */
	while (unlikely(open_files > new_fdt->max_fds)) {
		spin_unlock(&oldf->file_lock);

		if (new_fdt != &newf->fdtab)
			__free_fdtable(new_fdt);

		new_fdt = alloc_fdtable(open_files - 1);
		if (!new_fdt) {
			*errorp = -ENOMEM;
			goto out_release;
		}

		/* beyond sysctl_nr_open; nothing to do */
		if (unlikely(new_fdt->max_fds < open_files)) {
			__free_fdtable(new_fdt);
			*errorp = -EMFILE;
			goto out_release;
		}

		/*
		 * Reacquire the oldf lock and a pointer to its fd table
		 * who knows it may have a new bigger fd table. We need
		 * the latest pointer.
		 */
		spin_lock(&oldf->file_lock);
		old_fdt = files_fdtable(oldf);
		open_files = count_open_files(old_fdt);
	}

	copy_fd_bitmaps(new_fdt, old_fdt, open_files);

	old_fds = old_fdt->fd;
	new_fds = new_fdt->fd;

	for (i = open_files; i != 0; i--) {
		struct file *f = *old_fds++;
		if (f) {
			get_file(f);
		} else {
			/*
			 * The fd may be claimed in the fd bitmap but not yet
			 * instantiated in the files array if a sibling thread
			 * is partway through open().  So make sure that this
			 * fd is available to the new process.
			 */
			__clear_open_fd(open_files - i, new_fdt);
		}
		rcu_assign_pointer(*new_fds++, f);
	}
	spin_unlock(&oldf->file_lock);

	/* clear the remainder */
	memset(new_fds, 0, (new_fdt->max_fds - open_files) * sizeof(struct file *));

	rcu_assign_pointer(newf->fdt, new_fdt);

	return newf;

out_release:
	kmem_cache_free(files_cachep, newf);
out:
	return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103

进程调用fork函数创建子进程,函数 dup_fd使用slub管理器分配一个新的 files_struct 结构体,并从传入的旧 files_struct 复制内容到新的结构体中。

1.2.2 CLONE_FILES

对于多数进程来说,每个进程的struct files_struct *files指向唯一的files_struct 描述符,但是对于多线程来说,files_struct 描述符是共享的:

SYSCALL_DEFINE0(fork)
SYSCALL_DEFINE5(clone) --> CLONE_FILES
	-->_do_fork()
		-->copy_process()
			-->copy_files()
  • 1
  • 2
  • 3
  • 4
  • 5
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{
	struct files_struct *oldf, *newf;
	int error = 0;

	/*
	 * A background process may not have any files ...
	 */
	oldf = current->files;
	if (!oldf)
		goto out;

	//多线程共享files_struct 描述符,对于多线程来说,只是将files_struct 描述符的引用count+1
	if (clone_flags & CLONE_FILES) {
		atomic_inc(&oldf->count);
		goto out;
	}

	//进程调用fork创建子进程
	newf = dup_fd(oldf, &error);
	if (!newf)
		goto out;

	tsk->files = newf;
	error = 0;
out:
	return error;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
/*
 * cloning flags:
 */
......
#define CLONE_FILES	0x00000400	/* set if open files shared between processes */
......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这段代码的主要目的是根据 clone_flags 的设置,决定是共享还是复制文件表,然后将文件表指针赋值给新进程的 files 成员,完成文件描述符和文件表的复制或共享操作。

如果 clone_flags 中包含 CLONE_FILES 标志,进程调用pthread_creat(clone)创建多线程,表示需要共享文件表,那么函数会增加文件表的引用计数,并跳转到 out 标签处,然后返回错误码。

如果 clone_flags 中不包含 CLONE_FILES 标志,进程执行fork创建子进程,表示需要复制文件表,那么函数调用 dup_fd 函数复制文件表,并将复制得到的新文件表的指针赋值给 newf 变量。如果复制失败,函数跳转到 out 标签处,然后返回错误码。

1.3 源码分析

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(AT_FDCWD, filename, flags, mode);
}

SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
		umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;

	return do_sys_open(dfd, filename, flags, mode);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
	struct open_flags op;
	int fd = build_open_flags(flags, mode, &op);
	struct filename *tmp;

	if (fd)
		return fd;

	tmp = getname(filename);
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

	fd = get_unused_fd_flags(flags);
	if (fd >= 0) {
		struct file *f = do_filp_open(dfd, tmp, &op);
		if (IS_ERR(f)) {
			put_unused_fd(fd);
			fd = PTR_ERR(f);
		} else {
			fsnotify_open(f);
			fd_install(fd, f);
		}
	}
	putname(tmp);
	return fd;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

(1)构建打开标志:函数内部调用build_open_flags()函数根据传入的flags和mode参数构建了一个open_flags结构体op。
(2)获取文件名:函数使用getname()函数从用户空间获取文件名,并将结果存储在filename指针tmp中。
(3)获取未使用的文件描述符:使用get_unused_fd_flags()函数获取一个未使用的文件描述符,该描述符将用于打开文件。
(4)打开文件:使用slub管理器分配一个struct file结构体,用来管理打开的文件结构。
(5)文件操作:如果成功打开文件,调用fsnotify_open()函数通知文件系统事件监听器文件已打开,并使用fd_install()函数将文件描述符fd与文件结构体f关联起来。

从do_sys_open可以看出,打开文件时,内核主要使用了两种资源:文件描述符与内核管理文件结构file。即fd 和 strcut file。

打开文件的过程可以简单的概括为:为一个文件申请一个 fd,一个strcut file,并将 fd 与 strcut file 相关联起来。fd可以看成一个索引值,通过这个fd就可以找到该strcut file。这样用户在应用层操作 fd 其实就是在内核态操作strcut file。

1.3.1 get_unused_fd_flags

int get_unused_fd_flags(unsigned flags)
{
	return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags);
}
EXPORT_SYMBOL(get_unused_fd_flags);
  • 1
  • 2
  • 3
  • 4
  • 5

根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。

/*
 * allocate a file descriptor, mark it busy.
 */
int __alloc_fd(struct files_struct *files,
	       unsigned start, unsigned end, unsigned flags)
{
	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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

这个函数的作用是分配一个文件描述符,并将其标记为已使用。

函数的参数如下:
files:files_struct 结构体指针,表示文件描述符表所属的文件结构。
start:起始的文件描述符fd。
end:最大的文件描述符fd。
flags:标志位,用于指定文件描述符的属性。

函数的执行步骤如下:
(1)使用 files_fdtable 函数获取文件描述符表的指针 fdt。
(2)设置起始文件描述符号码 fd 为 start,如果 fd 小于 files->next_fd,则将其设置为 files->next_fd。
(3)如果 fd 小于文件描述符表的最大文件描述符数 fdt->max_fds,则使用 find_next_fd 函数在文件描述符表中查找下一个可用的文件描述符。
(4)如果 fd 大于等于 end,表示文件描述符已经超出了范围,将 error 设置为 -EMFILE,然后跳转到标签 out 处进行错误处理。
(5)调用 expand_files 函数扩展文件结构中的文件描述符表,以确保能够容纳 fd 号文件描述符。如果扩展文件描述符表时出现错误,将 error 设置为负值,然后跳转到标签 out 处进行错误处理。
(6)如果 error 非零,表示需要再次尝试分配文件描述符,跳转到标签 repeat 处重新执行分配操作。
(7)如果 start 小于等于 files->next_fd,将 files->next_fd 设置为 fd + 1,以确保下一次分配的文件描述符号码递增。
(8)调用 __set_open_fd 函数将文件描述符 fd 标记为已打开。
(9)如果 flags 中包含 O_CLOEXEC 标志位,调用 __set_close_on_exec 函数将文件描述符 fd 标记为在执行 exec 时关闭。如果不包含该标志位,则调用 __clear_close_on_exec 函数取消关闭标记。
(10)将 error 设置为分配的文件描述符号码 fd。

1.3.2 do_filp_open

do_filp_open()
	-->path_openat()
		-->alloc_empty_file()
			-->__alloc_file()
  • 1
  • 2
  • 3
  • 4
/* SLAB cache for file structures */
static struct kmem_cache *filp_cachep __read_mostly;

static struct file *__alloc_file(int flags, const struct cred *cred)
{
	struct file *f;
	int error;

	f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
	if (unlikely(!f))
		return ERR_PTR(-ENOMEM);

	f->f_cred = get_cred(cred);
	error = security_file_alloc(f);
	......

	return f;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这段代码是用于分配文件结构体的函数 __alloc_file。
代码注释如下:
(1)kmem_cache_zalloc 是一个内核函数,用于从内存缓存中分配一块内存,并将其初始化为零。在这里,它用于从 filp_cachep 缓存中分配一个文件结构体对象。
(2)f->f_cred 是一个指向 struct cred 结构体的指针,用于表示文件的访问凭证。get_cred(cred) 是一个函数调用,用于获取传入的 cred 参数的引用计数,并将其赋值给 f->f_cred。
(3)security_file_alloc 是一个安全模块提供的函数,用于在文件对象上执行安全分配操作,以确保文件对象的安全性。

do_filp_open函数使用slub管理器分配一个struct file文件结构体对象。

1.3.3 fd_install

void fd_install(unsigned int fd, struct file *file)
{
	__fd_install(current->files, fd, file);
}

EXPORT_SYMBOL(fd_install);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

fd_install函数用于将上述分配的fd和struct file组合起来,这样用户空间就可以通过fd在内核态访问struct file。

/*
 * Install a file pointer in the fd array.
 *
 * The VFS is full of places where we drop the files lock between
 * setting the open_fds bitmap and installing the file in the file
 * array.  At any such point, we are vulnerable to a dup2() race
 * installing a file in the array before us.  We need to detect this and
 * fput() the struct file we are about to overwrite in this case.
 *
 * It should never happen - if we allow dup2() do it, _really_ bad things
 * will follow.
 *
 * NOTE: __fd_install() variant is really, really low-level; don't
 * use it unless you are forced to by truly lousy API shoved down
 * your throat.  'files' *MUST* be either current->files or obtained
 * by get_files_struct(current) done by whoever had given it to you,
 * or really bad things will happen.  Normally you want to use
 * fd_install() instead.
 */

void __fd_install(struct files_struct *files, unsigned int fd,
		struct file *file)
{
	struct fdtable *fdt;

	rcu_read_lock_sched();

	if (unlikely(files->resize_in_progress)) {
		rcu_read_unlock_sched();
		spin_lock(&files->file_lock);
		fdt = files_fdtable(files);
		BUG_ON(fdt->fd[fd] != NULL);
		rcu_assign_pointer(fdt->fd[fd], file);
		spin_unlock(&files->file_lock);
		return;
	}
	/* coupled with smp_wmb() in expand_fdtable() */
	smp_rmb();
	fdt = rcu_dereference_sched(files->fdt);
	BUG_ON(fdt->fd[fd] != NULL);
	rcu_assign_pointer(fdt->fd[fd], file);
	rcu_read_unlock_sched();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

这个函数用于将文件指针 file 安装到指定的文件描述符位置 fd 上,如下访问fd对应的struct file:

fdt->fd[fd];
  • 1

二、struct file简介

struct file 是 Linux 内核中的一个重要数据结构,用于表示进程打开的文件,struct file是已经打开的文件在内存中的表示,存储与文件操作和状态相关的信息。

一个磁盘上的文件,多个进程可以同时打开和操作一个文件,因此多个进程对于该文件都会有自己的文件对象,因此一个文件可能存在多个对应的文件对象,但是其目录项对象和索引节点都是唯一的。

// linux-5.4.18/include/linux/fs.h

struct file {
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;
	......
	atomic_long_t		f_count;
	unsigned int 		f_flags;
	fmode_t			f_mode;

	loff_t			f_pos;

	const struct cred	*f_cred;

#ifdef CONFIG_SECURITY
	void			*f_security;
#endif

} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

下面是对 struct file 中各字段的简要说明:
(1)f_path 字段存储文件的路径信息,包括挂载点和文件名。它用于定位文件在文件系统中的位置。
struct path封装了下面两部分的信息:
文件名和inode之间的关联。
文件所在文件系统有关的信息。

struct path {
	struct vfsmount *mnt;
	struct dentry *dentry;
} __randomize_layout;
  • 1
  • 2
  • 3
  • 4

struct dentry 提供了文件名和inode之间的关联:

struct dentry {
	......
	struct inode *d_inode;
	......
}
  • 1
  • 2
  • 3
  • 4
  • 5

(2)f_inode 字段是指向与文件关联的 struct inode 的指针。struct inode 包含文件的元数据,如权限、时间戳和文件大小。通过 f_inode 可以访问与文件相关的其他属性和操作。
通过 struct file 获取其 struct inode API:

static inline struct inode *file_inode(const struct file *f)
{
	return f->f_inode;
}
  • 1
  • 2
  • 3
  • 4

(3)f_op 字段是指向文件操作函数表(struct file_operations)的指针。文件操作函数表包含读取、写入、打开、释放等文件操作的函数指针。可以通过 f_op 调用这些函数来执行各种文件操作,用来操作文件的实际数据内容。

struct file_operations {
	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 *);
	......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

(4)f_count 字段是原子长整型变量,表示文件的引用计数。它跟踪文件的打开引用次数。当计数值为零时,可以安全地关闭和释放文件。
(5)f_flags 字段存储文件的各种属性标志,例如是否可读、可写、可执行等。它是一个无符号整数类型。

#define O_ACCMODE	00000003
#define O_RDONLY	00000000
#define O_WRONLY	00000001
#define O_RDWR		00000002
#ifndef O_CREAT
#define O_CREAT		00000100	/* not fcntl */
#endif
#ifndef O_EXCL
#define O_EXCL		00000200	/* not fcntl */
#endif
#ifndef O_NOCTTY
#define O_NOCTTY	00000400	/* not fcntl */
#endif
#ifndef O_TRUNC
#define O_TRUNC		00001000	/* not fcntl */
#endif
#ifndef O_APPEND
#define O_APPEND	00002000
#endif
......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

参数flags必须包含以下访问模式之一:O_RDONLY(只读),O_WRONLY(只写)或O_RDWR(读写)。这些模式分别表示以只读、只写或读写的方式打开文件。

(6)f_mode 字段表示文件的模式或访问模式。它指定了文件的权限和访问权限,例如读、写、执行和其他权限位。fmode_t 是文件模式类型。

#define S_IRWXUGO	(S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO	(S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO		(S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO		(S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO		(S_IXUSR|S_IXGRP|S_IXOTH)
  • 1
  • 2
  • 3
  • 4
  • 5

O_CREAT是一个打开文件时可以使用的标志之一。如果文件不存在,将会创建该文件。文件的所有者(用户ID)将设置为进程的有效用户ID。文件的组所有权(组ID)将设置为进程的有效组ID或父目录的组ID(取决于文件系统类型、挂载选项和父目录的权限模式,参见mount()中描述的bsdgroups和sysvgroups挂载选项)。

mode参数指定了在创建新文件时使用的权限,只在创建文件时需要,用于指定所创建文件的权限位。当flags中指定了O_CREAT时,必须提供该参数;如果没有指定O_CREAT,则mode参数将被忽略。进程的umask按照通常的方式修改有效权限:创建的文件的权限是(mode & ~umask)。请注意,该权限模式仅适用于对新创建文件的未来访问;创建只读文件的open()调用可能会返回一个读/写文件描述符。

为mode提供了以下符号常量:

S_IRWXU  00700 user (file owner) has read, write and execute permission

S_IRUSR  00400 user has read permission

S_IWUSR  00200 user has write permission

S_IXUSR  00100 user has execute permission

S_IRWXG  00070 group has read, write and execute permission

S_IRGRP  00040 group has read permission

S_IWGRP  00020 group has write permission

S_IXGRP  00010 group has execute permission

S_IRWXO  00007 others have read, write and execute permission

S_IROTH  00004 others have read permission

S_IWOTH  00002 others have write permission

S_IXOTH  00001 others have execute permission
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

(7)f_pos 字段跟踪文件中当前的读写位置。随着从文件读取或写入数据,该位置将被更新。
(8)f_cred 字段是指向与文件相关联的凭证(struct cred)的指针。凭证包含与文件访问权限相关的信息,如用户和组的标识。
(9)f_security 字段是指向与文件相关的安全性相关数据的指针。仅在内核配置中启用了安全模块时存在。

三、其他

(1)进程fork
进程进行fork时,父子进程不共享struct files_struct,子进程拷贝父进程的struct files_struct,但父子进程共享struct file。
如下所示:
在这里插入图片描述
(2)进程创建多线程
进程创建多线程时,共享struct files_struct。
如下图所示:
在这里插入图片描述

(3)进程调用dup系统调用
一个进程调用dup系统调用时,借助于dup( )、dup2( )和 fcntl( ) 系统调用,两个文件描述符就可以指向同一个打开的文件,也就是说,数组的两个元素可能指向同一个文件对象。当用户使用shell结构(如2>&1)将标准错误文件重定向到标准输出文件上时,用户总能看到这一点。
如下图所示:
在这里插入图片描述

四、LKM demo

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>


static int __init hello_init(void)
{
    unsigned long get_max_files_number = get_max_files();
    printk("get_max_files_number = %ld\n", get_max_files_number);

    return -1;
}
 
 
module_init(hello_init);
 
MODULE_LICENSE("GPL");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
# dmesg -c
get_max_files_number = 9223372036854775807
  • 1
  • 2
// linux-5.4.18/fs/file_table.c

/* sysctl tunables... */
struct files_stat_struct files_stat = {
	.max_files = NR_FILE
};

/*
 * Return the maximum number of open files in the system
 */
unsigned long get_max_files(void)
{
	return files_stat.max_files;
}
EXPORT_SYMBOL_GPL(get_max_files);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

get_max_files 函数用于获取系统中可以打开的最大文件数。

“sysctl tunables”,即可调整的系统参数。在 Linux 内核中,sysctl 是一种机制,用于动态调整和配置内核的运行时参数。通过 sysctl 接口,可以修改一系列系统参数,包括文件系统相关的参数。

这段代码的目的是定义一个结构体变量 files_stat,并将其中的 max_files 成员初始化为默认的最大文件数。这样,可以在需要的时候通过修改 files_stat 结构体的成员来调整最大文件数。

Linux 系统将进程特定的信息和系统统计数据以文件的形式提供在 /proc 文件系统中。可以查看 /proc/sys/fs/file-max 文件来获取系统级别的最大文件数限制。运行以下命令可以查看系统级别的最大文件数限制:

# cat /proc/sys/fs/file-max
9223372036854775807
  • 1
  • 2

而对应的进程级别可以打开的最大文件数限制,可以使用ulimit -n查看:

# ulimit -n
1024
  • 1
  • 2

使用 ulimit 命令:ulimit 命令用于设置或显示进程资源限制。可以使用 ulimit -n 命令来显示当前进程的最大文件描述符数目限制。文件描述符数目限制也等同于最大文件数

最大文件数限制可以在系统级别和进程级别进行设置和修改。系统级别的限制适用于整个系统的所有进程,而进程级别的限制仅适用于特定进程。

参考资料

Linux 5.4.18

Linux环境编程从应用到内核

存储基础 — 文件描述符 fd 究竟是什么?
https://static.lwn.net/kerneldoc/filesystems/vfs.html
https://blog.csdn.net/gmy2016wiw/article/details/72594093

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/208440
推荐阅读
相关标签
  

闽ICP备14008679号