浅析Linux内核之文件与IO

韩乔落

内核文件表

文件

Linux内核将一切视为文件,既可以是事实上的真正的物理文件,也可以是设备、管道,甚至还可以是一块内存。狭义的文件是指文件系统中的物理文件,而广义的文件则可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一致的文件操作接口。

文件描述符

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

文件表

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

文件表的实现

内核中进程对应的结构是task_struct,进程的文件表保存在task_struct->files中。其结构代码如下所示。

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
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;
};

struct files_struct {
/* count为文件表files_struct的引用计数*/
atomic_t count;
/* 文件描述符表*/
/*
* 为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针,而fdtab为普通变量。
* 一般情况下,fdt是指向fdtab的,当需要它的时候,才会真正动态申请内存。
* 因为默认大小的文件表足以应付大多数情况,因此这样就可以避免频繁的内存申请。
* 这也是内核的常用技巧之一。在创建时,使用普通的变量或者数组,然后让指针指向它,作为默认情况使用。
* 只有当进程使用量超过默认值时,才会动态申请内存。
*/
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
/* 使用 ____cacheline_aligned_in_smp 可以保证file_lock是以cacheline对齐的,避免了false sharing */
spinlock_t file_lock ____cacheline_aligned_in_smp;
/* 用于查找下一个空闲的 fd */
int next_fd;
/* 保存执行exec需要关闭的文件描述符的位图*/
struct embedded_fd_set close_on_exec_init;
/* 保存打开的文件描述符的位图 */
struct embedded_fd_set open_fds_init;
/* fd_array为一个固定大小的file结构数组。
* struct file是内核用于文件管理的结构。这里使用默认大小的数组,就是为了可以涵盖大多数情况,避免动态分配
*/
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

下面看看files_struct是如何使用默认的fdtabfd_array的,initLinux的第一个进程,它的文件表是一个全局变量,代码如下:

1
2
3
4
5
6
7
8
9
10
11
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 = (fd_set *)&init_files.close_on_exec_init,
.open_fds = (fd_set *)&init_files.open_fds_init,
},
.file_lock = __SPIN_LOCK_UNLOCKED(init_task.file_lock),
};

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

1
2
3
4
5
6
7
8
9
10
11
12
newf = kmem_cache_alloc(files_cachep, GFP_KERNEL);
if (!newf)
goto out;
atomic_set(&newf->count, 1);
spin_lock_init(&newf->file_lock);
newf->next_fd = 0;
new_fdt = &newf->fdtab;
new_fdt->max_fds = NR_OPEN_DEFAULT;
new_fdt->close_on_exec = (fd_set *)&newf->close_on_exec_init;
new_fdt->open_fds = (fd_set *)&newf->open_fds_init;
new_fdt->fd = &newf->fd_array[0];
new_fdt->next = NULL;

初始化new_fdt,同样是为了让new_fdt和new_fdt->fd指向其本身的成员变量fdtab和fd_array。


/proc/pid/status为对应pid的进程的当前运行状态,其中FDSize值即为当前进程max_fds的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
# cat /proc/3576/status 
Name: su
Umask: 0022
State: S (sleeping)
Tgid: 3576
Ngid: 0
Pid: 3576
PPid: 3447
TracerPid: 0
Uid: 1000 0 0 0
Gid: 1000 1000 1000 1000
FDSize: 64
[...]

因此,初始状态下,files_structfdtablefiles的关系如下图所示。

内核文件表

除了文件名以外的所有文件信息,都存在inode之中。

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
struct inode {
struct hlist_node i_hash; /* 哈希表 */
struct list_head i_list; /* 索引节点链表 */
struct list_head i_dentry; /* 目录项链表 */
unsigned long i_ino; /* 节点号 */
atomic_t i_count; /* 引用记数 */
umode_t i_mode; /* 访问权限控制 */
unsigned int i_nlink; /* 硬链接数 */
uid_t i_uid; /* 使用者id */
gid_t i_gid; /* 使用者id组 */
kdev_t i_rdev; /* 实设备标识符 */
loff_t i_size; /* 以字节为单位的文件大小 */
struct timespec i_atime; /* 最后访问时间 */
struct timespec i_mtime; /* 最后修改(modify)时间 */
struct timespec i_ctime; /* 最后改变(change)时间 */
unsigned int i_blkbits; /* 以位为单位的块大小 */
unsigned long i_blksize; /* 以字节为单位的块大小 */
unsigned long i_version; /* 版本号 */
unsigned long i_blocks; /* 文件的块数 */
unsigned short i_bytes; /* 使用的字节数 */
spinlock_t i_lock; /* 自旋锁 */
struct rw_semaphore i_alloc_sem; /* 索引节点信号量 */
struct inode_operations *i_op; /* 索引节点操作表 */
struct file_operations *i_fop; /* 默认的索引节点操作 */
struct super_block *i_sb; /* 相关的超级块 */
struct file_lock *i_flock; /* 文件锁链表 */
struct address_space *i_mapping; /* 相关的地址映射 */
struct address_space i_data; /* 设备地址映射 */
struct dquot *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */
struct list_head i_devices; /* 块设备链表 */
struct pipe_inode_info *i_pipe; /* 管道信息 */
struct block_device *i_bdev; /* 块设备驱动 */
unsigned long i_dnotify_mask; /* 目录通知掩码 */
struct dnotify_struct *i_dnotify; /* 目录通知 */
unsigned long i_state; /* 状态标志 */
unsigned long dirtied_when; /* 首次修改时间 */
unsigned int i_flags; /* 文件系统标志 */
unsigned char i_sock; /* 可能是个套接字吧 */
atomic_t i_writecount; /* 写者记数 */
void *i_security; /* 安全模块 */
__u32 i_generation; /* 索引节点版本号 */
union {
void *generic_ip; /* 文件特殊信息 */
} u;
};

打开文件

open 函数简介

open在手册中有两个函数原型,如下所示:

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

在Linux内核中,实际上只提供了一个系统调用,对应的是上述两个函数原型中的第二个。当我们调用open函数时,实际上调用的是glibc封装的函数,然后由glibc通过自陷指令,进行真正的系统调用。也就是说,所有的系统调用都要先经过glibc才会进入操作系统。这样的话,实际上是glibc提供了一个变参函数open来满足两个函数原型,然后通过glibc的变参函数open实现真正的系统调用来调用原型二。

函数参数pathname:表示要打开的文件路径。flags:用于指示打开文件的选项,常用的有O_RDONLY、O_WRONLY和O_RDWR。这三个选项必须有且只能有一个被指定。O_RDWR!=O_RDONLY|O_WRONLY,Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。之所以有这样违反常规的设计遗留至今,就是为了兼容以前的程序。当然还有很多其他选项这里不一一列举。

open 内核源码追踪

下面是open的内核源码:

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
long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
struct open_flags op;
/* flags为用户层传递的参数,内核会对flags进行合法性检查,并根据mode生成新的flags值赋给lookup */
int lookup = build_open_flags(flags, mode, &op);
/* 将用户空间的文件名参数复制到内核空间 */
char *tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
/* 未出错则申请新的文件描述符*/
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
/* 申请新的文件管理结构file */
struct file *f = do_filp_open(dfd, tmp, &op, lookup);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
/* 产生文件打开的通知事件 */
fsnotify_open(f);
/* 将文件描述符fd与文件管理结构file对应起来,即安装 */
fd_install(fd, f);
}
}
putname(tmp);
}
return fd;
}

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

特别的其在linux kernel 6.0以上版本代码如下:

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
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
inline struct open_how build_open_how(int flags, umode_t mode)
{
struct open_how how = {
.flags = flags & VALID_OPEN_FLAGS,
.mode = mode & S_IALLUGO,
};

/* O_PATH beats everything else. */
if (how.flags & O_PATH)
how.flags &= O_PATH_FLAGS;
/* Modes should only be set for create-like flags. */
if (!WILL_CREATE(how.flags))
how.mode = 0;
return how;
}
static long do_sys_openat2(int dfd, const char __user *filename,
struct open_how *how)
{
struct open_flags op;
int fd = build_open_flags(how, &op);
struct filename *tmp;

if (fd)
return fd;

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

fd = get_unused_fd_flags(how->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 {
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}

多了一道检查,借此说明,我们分析的是内核的核心逻辑,后面有机会会做新内核的安全性分析,写此系列文章也只是做一下学习笔记


根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))来选择文件描述符,代码如下:

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
int alloc_fd(unsigned start, unsigned flags)
{
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
/* files为进程的文件表,下面需要更改文件表,所以需要先锁文件表 */
spin_lock(&files->file_lock);
repeat:
/* 得到文件描述符表*/
fdt = files_fdtable(files);
/* 从start开始,查找未用的文件描述符。在打开文件时,start为 0 */
fd = start;
/* files->next_fd为上一次成功找到的fd的下一个描述符。使用next_fd,可以快速找到未用的文件描述符;*/
if (fd < files->next_fd)
fd = files->next_fd;
/*
* 当小于当前文件表支持的最大文件描述符个数时,利用位图找到未用的文件描述符。
* 如果大于max_fds,也就是大于当前支持的最大文件描述符,那它肯定是未用的,就不需要用位图来确认了。
*/
if (fd < fdt->max_fds)
fd = find_next_zero_bit(fdt->open_fds->fds_bits,
fdt->max_fds, fd);
/* expand_files用于在必要时扩展文件表。比如当前文件描述符已经超过了当前文件表支持的最大值的时候。*/
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;
/* 只有在start小于next_fd时,才需要更新next_fd,以尽量保证文件描述符的连续性。*/
if (start <= files->next_fd)
files->next_fd = fd + 1;
/* 将打开文件位图open_fds对应fd的位置置位*/
FD_SET(fd, fdt->open_fds);
/* 根据flags是否设置了O_CLOEXEC,设置或清除fdt->close_on_exec */
if (flags & O_CLOEXEC)
FD_SET(fd, fdt->close_on_exec);
else
FD_CLR(fd, fdt->close_on_exec);
error = fd;
#if 1
/* Sanity check */
if (rcu_dereference_raw(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;
}

前文已经说过,内核使用fd_install将文件管理结构file与fd组合起来,具体操作请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void fd_install(unsigned int fd, struct file *file)
{
struct files_struct *files = current->files;
struct fdtable *fdt;
spin_lock(&files->file_lock);
/* 得到文件描述符表*/
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
/*
* 将文件描述符表中的file类型的指针数组中对应fd的项指向file。
* 这样文件描述符fd与file就建立了对应关系
*/
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}

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

创建文件

creat 函数简介

creat函数用于创建一个新文件,其等价于open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode)。由于历史原因,早期的Unix版本中,open的第二个参数只能是0、1或者2。这样就没有办法打开一个不存在的文件。因此,一个独立系统调用creat被引入,用于创建新文件。现在的open函数,通过使用O_CREAT和O_TRUNC选项,可以实现creat的功能,因此creat已经不是必要的了。内核creat的实现代码如下所示:

1
2
3
4
SYSCALL_DEFINE2(creat, const char __user *, pathname, int, mode)
{
return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
}

关闭文件

close 内核源码追踪

close用于关闭文件描述符。而文件描述符可以是普通文件,也可以是设备,还可以是socket。在关闭时,VFS会根据不同的文件类型,执行不同的操作。下面将通过跟踪close的内核源码来了解内核如何针对不同的文件类型执行不同的操作。

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
SYSCALL_DEFINE1(close, unsigned int, fd)
{
struct file * filp;
/* 得到当前进程的文件表 */
struct files_struct *files = current->files;
struct fdtable *fdt;
int retval;
spin_lock(&files->file_lock);
/* 通过文件表,取得文件描述符表 */
fdt = files_fdtable(files);
/* 参数fd大于文件描述符表记录的最大描述符,那么它一定是非法的描述符 */
if (fd >= fdt->max_fds)
goto out_unlock;
/* 利用fd作为索引,得到file结构指针 */
filp = fdt->fd[fd];
/* 检查filp是否为NULL。正常情况下,filp一定不为NULL。*/
if (!filp)
goto out_unlock;
/* 将对应的filp置为0 */
rcu_assign_pointer(fdt->fd[fd], NULL);
/* 清除fd在close_on_exec位图中的位*/
FD_CLR(fd, fdt->close_on_exec);
/* 释放该fd,或者说将其置为unused。*/
__put_unused_fd(files, fd);
spin_unlock(&files->file_lock);
/* 关闭file结构 */
retval = filp_close(filp, files);
/* can't restart close syscall because file table entry was cleared */
if (unlikely(retval == -ERESTARTSYS ||
retval == -ERESTARTNOINTR ||
retval == -ERESTARTNOHAND ||
retval == -ERESTART_RESTARTBLOCK))
retval = -EINTR;
return retval;
out_unlock:
spin_unlock(&files->file_lock);
return -EBADF;
}
EXPORT_SYMBOL(sys_close);

__put_unused_fd源码如下所示:

1
2
3
4
5
6
7
8
9
10
static void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
/* 取得文件描述符表*/
struct fdtable *fdt = files_fdtable(files);
/* 清除fd在open_fds位图的位*/
__FD_CLR(fd, fdt->open_fds);
/* 如果fd小于next_fd,重置next_fd为释放的fd */
if (fd < files->next_fd)
files->next_fd = fd;
}

结合之前的alloc_fd()可知,Linux文件描述符策略永远选择最小的可用的文件描述符。从__put_unused_fd退出后,close会接着调用filp_close,其调用路径为filp_close->fput。在fput中,会对当前文件struct file的引用计数减一并检查其值是否为0。当引用计数为0时,表示该struct file没有被其他人使用,则可以调用__fput执行真正的文件释放操作,然后调用要关闭文件所属文件系统的release函数,从而实现针对不同的文件类型来执行不同的关闭操作。


这里选择socket文件系统作为示例,来说明Linux如何挂载文件系统指定的文件操作函数files_operations。socket.c中定义了其文件操作函数file_operations,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.open = sock_no_open, /* special open code to disallow open via /proc */
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};

函数sock_alloc_file用于申请socket文件描述符及文件管理结构file结构。它调用alloc_file来申请管理结构file,并将socket_file_ops作为参数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);

struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
struct file *file;
/* 申请一个file */
file = get_empty_filp();
if (!file)
return NULL;
file->f_path = *path;
file->f_mapping = path->dentry->d_inode->i_mapping;
file->f_mode = mode;
/* 将自定义的文件操作函数赋给file->f_op */
file->f_op = fop;
[...]
}

在初始化file结构的时候,socket文件系统将其自定义的文件操作赋给了file->f_op,从而实现了在VFS中可以调用socket文件系统自定义的操作。

文件偏移

文件偏移是基于某个打开文件来说的,一般情况下,读写操作都会从当前的偏移位置开始读写(所以read和write都没有显式地传入偏移量),并且在读写结束后更新偏移量。

lseek函数简介

lseek 原型如下:

1
off_t lseek(int fd, off_t offset, int whence);

参数

该函数用于将fd的文件偏移量设置为以whence为起点,偏移为offset的位置。其中whence可以为三个值:SEEK_SET、SEEK_CUR和SEEK_END,分别表示为“文件的起始位置”、“文件的当前位置”和“文件的末尾”,而offset的取值正负均可。lseek执行成功后,会返回新的文件偏移量。

返回值

当lseek执行成功时,它会返回最终以文件起始位置为起点的偏移位置。如果出错,则返回-1,同时errno被设置为对应的错误值。也就是说,一般情况下,对于普通文件来说,lseek都是返回非负的整数,但是对于某些设备文件来说,是允许返回负的偏移量。因此要想判断lseek是否真正出错,必须在调用lseek前将errno重置为0,然后再调用lseek,同时检查返回值是否为-1及errno的值。只有当两个同时成立时,才表明lseek真正出错了。因为这里的文件偏移都是内核的概念,所以lseek并不会引起任何真正的I/O操作

lseek 内核源码追踪

lseek的源码位于read_write.c中,如下:

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
SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, origin)
{
off_t retval;
struct file * file;
int fput_needed;
retval = -EBADF;
/* 根据fd得到file指针 */
file = fget_light(fd, &fput_needed);
if (!file)
goto bad;
retval = -EINVAL;
/* 对初始位置进行检查 */
if (origin <= SEEK_MAX) {
loff_t res = vfs_llseek(file, offset, origin);
/*
* 下面这段代码,先使用res来给retval赋值,然后再次判断res是否与retval相等。
* 为什么会有这样的逻辑呢?什么时候两者会不相等呢?只有在retval与res的位数不相等的情况下。
* retval的类型是off_t->__kernel_off_t->long;
* 而res的类型是loff_t->__kernel_off_t->long long;
* 在32位机上,前者是32位,而后者是64位。当res的值超过了retval的范围时,两者将会不等。
* 即实际偏移量超过了long类型的表示范围。
*/
retval = res;
if (res != (loff_t)retval)
retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
}
fput_light(file, fput_needed);
bad:
return retval;
}

然后进入vfs_llseek,代码如下:

1
2
3
4
5
6
7
8
9
10
11
loff_t vfs_llseek(struct file *file, loff_t offset, int origin)
{
loff_t (*fn)(struct file *, loff_t, int);
/* 默认的lseek操作是no_llseek,当file没有对应的llseek实现时,就会调用no_llseek,并返回-ESPIPE错误 */
fn = no_llseek;
if (file->f_mode & FMODE_LSEEK) {
if (file->f_op && file->f_op->llseek)
fn = file->f_op->llseek;
}
return fn(file, offset, origin);
}

当file支持llseek操作时,就会调用具体的llseek函数。在此,选择default_llseek作为实例,代码如下:

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
loff_t default_llseek(struct file *file, loff_t offset, int origin)
{
struct inode *inode = file->f_path.dentry->d_inode;
loff_t retval;
mutex_lock(&inode->i_mutex);
switch (origin) {
case SEEK_END:
/* 最终偏移等于文件的大小加上指定的偏移量*/
offset += i_size_read(inode);
break;
case SEEK_CUR:
/* offset为0时,并不改变当前的偏移量,而是直接返回当前偏移量*/
if (offset == 0) {
retval = file->f_pos;
goto out;
}
/* 若offset不为0,则最终偏移等于指定偏移加上当前偏移 */
offset += file->f_pos;
break;
case SEEK_DATA:
/*
* In the generic case the entire file is data, so as
* long as offset isn't at the end of the file then the
* offset is data.
*/
/* 如注释所言,对于一般文件,只要指定偏移不超过文件大小,那么指定偏移的位置就是数据位置 */
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
break;
case SEEK_HOLE:
/*
* There is a virtual hole at the end of the file, so
* as long as offset isn't i_size or larger, return
* i_size.
*/
/* 只要指定偏移不超过文件大小,那么下一个空洞位置就是文件的末尾*/
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
offset = inode->i_size;
break;
}
retval = -EINVAL;
/* 对于一般文件来说,最终的offset必须大于或等于0,或者该文件的模式要求只能产生无符号的偏移量。否则就会报错 */
if (offset >= 0 || unsigned_offsets(file)) {
/* 当最终偏移不等于当前位置时,则更新文件的当前位置*/
if (offset != file->f_pos) {
file->f_pos = offset;
file->f_version = 0;
}
retval = offset;
}
out:
mutex_unlock(&inode->i_mutex);
return retval;
}

读取文件

read 函数简介

read函数原型如下:

1
ssize_t read(int fd, void *buf, size_t count);

read尝试从fd中读取count个字节到buf中,并返回成功读取的字节数,同时将文件偏移向前移动相同的字节数。返回0的时候则表示已经到了“文件尾”。read还有可能读取比count小的字节数。使用read进行数据读取时,要注意正确地处理错误,也是说read返回-1时,如果errno为EAGAIN、EWOULDBLOCK或EINTR,一般情况下都不能将其视为错误。因为前两者是由于当前fd为非阻塞且没有可读数据时返回的,后者是由于read被信号中断所造成的。这两种情况基本上都可以视为正常情况。

read 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
/* 通过文件描述符fd得到管理结构file */
file = fget_light(fd, &fput_needed);
if (file) {
/* 得到文件的当前偏移量*/
loff_t pos = file_pos_read(file);
/* 利用vfs进行真正的read */
ret = vfs_read(file, buf, count, &pos);
/* 更新文件偏移量*/
file_pos_write(file, pos);
/* 归还管理结构file,如有必要,就进行引用计数操作*/
fput_light(file, fput_needed);
}
return ret;
}

再进入vfs_read,代码如下:

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
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
/* 检查文件是否为读取打开 */
if (!(file->f_mode & FMODE_READ))
return -EBADF;
/* 检查文件是否支持读取操作 */
if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
return -EINVAL;
/* 检查用户传递的参数buf的地址是否可写 */
if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
return -EFAULT;
/* 检查要读取的文件范围实际可读取的字节数 */
ret = rw_verify_area(READ, file, pos, count);
if (ret >= 0) {
/* 根据上面的结构,调整要读取的字节数 */
count = ret;
/*
* 如果定义read操作,则执行定义的read操作。
* 如果没有定义read操作,则调用do_sync_read—其利用异步aio_read来完成同步的read操作。
*/
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else
ret = do_sync_read(file, buf, count, pos);
if (ret > 0) {
/* 读取了一定的字节数,进行通知操作 */
fsnotify_access(file);
/* 增加进程读取字节的统计计数 */
add_rchar(current, ret);
}
/* 增加进程系统调用的统计计数 */
inc_syscr(current);
}
return ret;
}

上面的代码为read公共部分的源码分析,具体的读取动作是由实际的文件系统决定的。


pread 简介

Linux还提供pread从指定偏移位置读取数据。其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE4(pread64, unsigned int, fd, char __user *, buf,
size_t, count, loff_t, pos)
{
struct fd f;
ssize_t ret = -EBADF;

if (pos < 0)
return -EINVAL;

f = fdget(fd);
if (f.file) {
ret = -ESPIPE;
if (f.file->f_mode & FMODE_PREAD)
ret = vfs_read(f.file, buf, count, &pos);
fdput(f);
}

return ret;
}

pread不会从文件表中获取当前偏移,而是直接使用用户传递的偏移量,并且在读取完毕后,不会更改当前文件的偏移量。

写入文件

write 函数简介

write 函数原型如下:

1
ssize_t write(int fd, const void *buf, size_t count);

write尝试从buf指向的地址,写入count个字节到文件描述符fd中,并返回成功写入的字节数,同时将文件偏移向前移动相同的字节数。write有可能写入比指定count少的字节数。

write 内核源码追踪

write的源码与read的很相似,位于read_write.c中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
/* 得到file管理结构指针 */
file = fget_light(fd, &fput_needed);
if (file) {
/* 得到当前的文件偏移 */
loff_t pos = file_pos_read(file);
/* 利用VFS写入 */
ret = vfs_write(file, buf, count, &pos);
/* 更新文件偏移量 */
file_pos_write(file, pos);
/* 释放文件管理指针 file */
fput_light(file, fput_needed);
}
return ret;
}

进入vfs_write,代码如下:

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
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_op || (!file->f_op->write && !file->f_op->aio_write))
return -EINVAL;
/* 检查用户给定的地址范围是否可读取 */
if (unlikely(!access_ok(VERIFY_READ, buf, count)))
return -EFAULT;
/* 验证文件从pos起始是否可以写入count个字节数,并返回可以写入的字节数 */
ret = rw_verify_area(WRITE, file, pos, count);
if (ret >= 0) {
/* 更新写入字节数 */
count = ret;
/*
* 如果定义write操作,则执行定义的write操作。
* 如果没有定义write操作,则调用do_sync_write—其利用异步aio_write来完成同步的write操作。
*/
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
else
ret = do_sync_write(file, buf, count, pos);
if (ret > 0) {
/* 写入了一定的字节数,进行通知操作 */
fsnotify_modify(file);
/* 增加进程读取字节的统计计数 */
add_wchar(current, ret);
}
/* 增加进程系统调用的统计计数 */
inc_syscw(current);
}
return ret;
}

pwrite 简介

pwrite 与 pread 类似,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE4(pwrite64, unsigned int, fd, const char __user *, buf,
size_t, count, loff_t, pos)
{
struct fd f;
ssize_t ret = -EBADF;

if (pos < 0)
return -EINVAL;

f = fdget(fd);
if (f.file) {
ret = -ESPIPE;
if (f.file->f_mode & FMODE_PWRITE)
ret = vfs_write(f.file, buf, count, &pos);
fdput(f);
}

return ret;
}

文件描述符的复制

Linux提供了三个复制文件描述符的系统调用,分别为:

1
2
3
int dup(int oldfd);
int dup2(int oldfd, int newfd);
int dup3(int oldfd, int newfd, int flags);
  • dup 会使用一个最小的未用文件描述符作为复制后的文件描述符。

  • dup2 是使用用户指定的文件描述符newfd来复制oldfd的。如果newfd已经是打开的文件描述符,Linux会先关闭newfd,然后再复制oldfd。

  • dup3 只有定义了feature宏“_GNU_SOURCE”才可以使用,它比dup2多了一个参数,可以指定标志,不过目前仅仅支持O_CLOEXEC标志,可在newfd上设置O_CLOEXEC标志。定义dup3的原因与open类似,可以在进行dup操作的同时原子地将fd设置为O_CLOEXEC,从而避免将文件内容暴露给子进程。

dup 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SYSCALL_DEFINE1(dup, unsigned int, fildes)
{
int ret = -EBADF;
/* 必须先得到文件管理结构file,同时也是对描述符fildes的检查 */
struct file *file = fget_raw(fildes);
if (file) {
/* 得到一个未使用的文件描述符*/
ret = get_unused_fd();
if (ret >= 0) {
/* 将文件描述符与file指针关联起来*/
fd_install(ret, file);
}
else
fput(file);
}
return ret;
}

fd_install 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
void fd_install(unsigned int fd, struct file *file)
{
struct files_struct *files = current->files;
struct fdtable *fdt;
/* 对文件表进行保护 */
spin_lock(&files->file_lock);
/* 得到文件表 */
fdt = files_fdtable(files);
BUG_ON(fdt->fd[fd] != NULL);
/* 让文件表中fd对应的指针等于该文件关联结构 file */
rcu_assign_pointer(fdt->fd[fd], file);
spin_unlock(&files->file_lock);
}

dup2 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)
{
/* 如果oldfd与newfd相等,这是一种特殊的情况 */
if (unlikely(newfd == oldfd)) { /* corner case */
struct files_struct *files = current->files;
int retval = oldfd;
/* 检查oldfd的合法性,如果是合法的fd,则直接返回oldfd的值;如果是不合法的,则返回EBADF */
rcu_read_lock();
if (!fcheck_files(files, oldfd))
retval = -EBADF;
rcu_read_unlock();
return retval;
}
/* 如果oldfd与newfd不同,则利用sys_dup3来实现dup2 */
return sys_dup3(oldfd, newfd, 0);
}

dup3 内核源码追踪

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
SYSCALL_DEFINE3(dup3, unsigned int, oldfd, unsigned int, newfd, int, flags)
{
int err = -EBADF;
struct file * file, *tofree;
struct files_struct * files = current->files;
struct fdtable *fdt;
/* 对标志flags进行检查,支持O_CLOEXEC */
if ((flags & ~O_CLOEXEC) != 0)
return -EINVAL;
/* 与dup2不同,当oldfd与newfd相同的时候,dup3返回错误 */
if (unlikely(oldfd == newfd))
return -EINVAL;
spin_lock(&files->file_lock);
/* 根据newfd决定是否需要扩展文件表的大小 */
err = expand_files(files, newfd);
/*检查oldfd,如果是非法的,就直接返回;不过我更倾向于先检查oldfd后扩展文件表,如果是非法的,就不需要扩展文件表了*/
file = fcheck(oldfd);
if (unlikely(!file))
goto Ebadf;
if (unlikely(err < 0)) {
if (err == -EMFILE)
goto Ebadf;
goto out_unlock;
}
err = -EBUSY;
/* 得到文件表 */
fdt = files_fdtable(files);
/* 通过newfd得到对应的file结构 */
tofree = fdt->fd[newfd];
/* tofree是NULL,但是newfd已经分配的情况 */
if (!tofree && FD_ISSET(newfd, fdt->open_fds))
goto out_unlock;
/* 增加file的引用计数 */
get_file(file);
/* 将文件表newfd对应的指针指向file */
rcu_assign_pointer(fdt->fd[newfd], file);
/*
* 将newfd加到打开文件的位图中,如果newfd已经是一个合法的fd,重复设置位图则没有影响;
* 如果newfd没有打开,则必须将其加入位图中。
*/
FD_SET(newfd, fdt->open_fds);
/*
* 如果flags设置了O_CLOEXEC,则将newfd加到close_on_exec位图;
* 如果没有设置,则清除lose_on_exec位图中对应的位
*/
if (flags & O_CLOEXEC)
FD_SET(newfd, fdt->close_on_exec);
else
FD_CLR(newfd, fdt->close_on_exec);
spin_unlock(&files->file_lock);
/* 如果tofree不为空,则需要关闭newfd之前的文件 */
if (tofree)
filp_close(tofree, files);
return newfd;
Ebadf:
err = -EBADF;
out_unlock:
spin_unlock(&files->file_lock);
return err;
}

文件数据的同步

为了提高性能,操作系统会对文件的I/O操作进行缓存处理。对于读操作,如果要读取的内容已经存在于文件缓存中,就直接读取文件缓存。对于写操作,会先将修改提交到文件缓存中,在合适的时机或者过一段时间后,操作系统才会将改动提交到磁盘上。Linux提供了三个同步接口:

1
2
3
void sync(void);
int fsync(int fd);
int fdatasync(int fd);

sync 内核源码追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SYSCALL_DEFINE0(sync)
{
/* 唤醒后台内核线程,将“脏”缓存冲刷到磁盘上 */
wakeup_flusher_threads(0, WB_REASON_SYNC);
/*
* 为什么要调用两次sync_filesystems呢?这是一种编程技巧,第一次sync_filesystems(0),参数0表示不等待,
* 可以迅速地将没有上锁的inode同步。第二次sync_filesystems(1),参数1表示等待。
* 对于上锁的inode会等待到解锁,再执行同步,这样可以提高性能。因为第一次操作中,
* 上锁的inode很可能在第一次操作结束后,就已经解锁,这样就避免了等待
*/
sync_filesystems(0);
sync_filesystems(1);
/*
* 如果是laptop模式,那么因为此处刚刚做完同步,因此可以停掉后台同步定时器
*/
if (unlikely(laptop_mode))
laptop_sync_completion();
return 0;
}

再看一下sync_filesystems->iterate_supers->sync_one_sb->__sync_filesystem,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int __sync_filesystem(struct super_block *sb, int wait)
{
/*
* This should be safe, as we require bdi backing to actually
* write out data in the first place
*/
if (sb->s_bdi == &noop_backing_dev_info)
return 0;
/* 磁盘配额同步 */
if (sb->s_qcop && sb->s_qcop->quota_sync)
sb->s_qcop->quota_sync(sb, -1, wait);
/*
* 如果wait为true,则一直等待直到所有的脏inode写入磁盘如果wait为false,则启动脏inode回写工作,但不必等待到结束
*/
if (wait)
sync_inodes_sb(sb);
else
writeback_inodes_sb(sb, WB_REASON_SYNC);
/* 如果该文件系统定义了自己的同步操作,则执行该操作 */
if (sb->s_op->sync_fs)
sb->s_op->sync_fs(sb, wait);
/* 调用block设备的flush操作,真正地将数据写到设备上 */
return __sync_blockdev(sb->s_bdev, wait);
}

从sync的代码实现上看,Linux的sync是阻塞调用。

fsync 内核源码追踪

sync只同步fd指定的文件,并且直到同步完成才返回。sync不仅同步数据,还会同步所有被修改过的文件元数据,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SYSCALL_DEFINE1(fsync, unsigned int, fd)
{
return do_fsync(fd, 0);
}

static int do_fsync(unsigned int fd, int datasync)
{
struct file *file;
int ret = -EBADF;
/* 得到file管理结构 */
file = fget(fd);
if (file) {
/* 利用vfs执行sync操作 */
ret = vfs_fsync(file, datasync);
fput(file);
}
return ret;
}

进入vfs_fsync->vfs_fsync_range,代码如下:

1
2
3
4
5
6
7
int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
{
/* 调用具体操作系统的同步操作 */
if (!file->f_op || !file->f_op->fsync)
return -EINVAL;
return file->f_op->fsync(file, start, end, datasync);
}

真正执行同步操作的fsync是由具体的文件系统的操作函数file_operations决定的。下面选择一个常用的文件系统同步函数generic_file_fsync,代码如下。

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
int generic_file_fsync(struct file *file, loff_t start, loff_t end,
int datasync)
{
struct inode *inode = file->f_mapping->host;
int err;
int ret;
/* 同步该文件缓存中处于start到end范围内的脏页 */
err = filemap_write_and_wait_range(inode->i_mapping, start, end);
if (err)
return err;
mutex_lock(&inode->i_mutex);
/* 同步该inode对应的缓存 */
ret = sync_mapping_buffers(inode->i_mapping);
/* inode状态没有变化,无需同步,可以直接返回 */
if (!(inode->i_state & I_DIRTY))
goto out;
/* 如果是fdatasync则仅做数据同步,并且若该inode没有影响任何数据方面操作的变化(比如文件长度),则可以直接返回 */
if (datasync && !(inode->i_state & I_DIRTY_DATASYNC))
goto out;
/* 同步inode的元数据 */
err = sync_inode_metadata(inode, 1);
if (ret == 0)
ret = err;
out:
mutex_unlock(&inode->i_mutex);
return ret;
}

fdatasync的性能会优于fsync。在不需要同步所有元数据的情况下,选择fdatasync会得到更好的性能。只有在inode被设置了I_DIRTY_DATASYNC标志时,fdatasync才需要同步inode的元数据。

文件的元数据

文件的元数据包括文件的访问权限、上次访问的时间戳、所有者、所有组、文件大小等信息。Linux环境提供了三个获取文件信息的API:

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

这三个函数都可用于得到文件的基本信息,区别在于stat得到路径path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,而lstat与stat则基本相同,只有当path是一个链接文件时,lstat得到的是链接文件自己本身的基本信息而不是其指向文件的信息。所得到的文件基本信息的结果struct stat的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
/* st_mode,其不仅仅是权限管理,同时也用于表示文件类型,比如是普通文件还是目录。 */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};

stat 内核源码追踪

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
SYSCALL_DEFINE2(stat, const char __user *, filename,
struct __old_kernel_stat __user *, statbuf)
{
struct kstat stat;
int error;
/* vfs_stat用于读取文件元数据至stat */
error = vfs_stat(filename, &stat);
if (error)
return error;
/* 这里仅是从内核的元数据结构stat复制到用户层的数据结构statbuf中 */
return cp_old_stat(&stat, statbuf);
}

进入vfs_stat->vfs_fstatat->vfs_getattr,代码如下:

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
int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
{
struct inode *inode = dentry->d_inode;
int retval;
/* 对获取inode属性操作进行安全性检查 */
retval = security_inode_getattr(mnt, dentry);
if (retval)
return retval;
/* 如果该文件系统定义了这个inode的自定义操作函数,就执行它 */
if (inode->i_op->getattr)
return inode->i_op->getattr(mnt, dentry, stat);
/* 如果文件系统没有定义inode的操作函数,则执行通用的函数 */
generic_fillattr(inode, stat);
return 0;
}

void generic_fillattr(struct inode *inode, struct kstat *stat)
{
stat->dev = inode->i_sb->s_dev;
stat->ino = inode->i_ino;
stat->mode = inode->i_mode;
stat->nlink = inode->i_nlink;
stat->uid = inode->i_uid;
stat->gid = inode->i_gid;
stat->rdev = inode->i_rdev;
stat->size = i_size_read(inode);
stat->atime = inode->i_atime;
stat->mtime = inode->i_mtime;
stat->ctime = inode->i_ctime;
stat->blksize = (1 << inode->i_blkbits);
stat->blocks = inode->i_blocks;
}

所有的文件元数据均保存在inode中,而inode是Linux也是所有类Unix文件系统中的一个概念。这样的文件系统一般将存储区域分为两类,一类是保存文件对象的元信息数据,即inode表;另一类是真正保存文件数据内容的块,所有inode完全由文件系统来维护。但是Linux也可以挂载非类Unix的文件系统,这些文件系统本身没有inode的概念,Linux为了让VFS有统一的处理流程和方法,就必须要求那些没有inode概念的文件系统,根据自己系统的特点——如何维护文件元数据,生成“虚拟的”inode以供Linux内核使用。

权限位解析

文件常见的权限位有r、w和x,分别表示可读、可写和可执行。下面重点解析三个不常用的标志位。

SUID 权限位

当文件设置SUID权限位时,就意味着无论是谁执行这个文件,都会拥有该文件所有者的权限。passwd命令正是利用这个特性,来允许普通用户修改自己的密码,因为只有root用户才有修改密码文件的权限。当普通用户执行passwd命令时,就具有了root权限,从而可以修改自己的密码。以修改文件属性的权限检查代码为例,inode_change_ok用于检查该进程是否有权限修改inode节点的属性即文件属性,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
int inode_change_ok(const struct inode *inode, struct iattr *attr)
{
unsigned int ia_valid = attr->ia_valid;
[...]
/* Make sure a caller can chown. */
/* 只有在uid和suid都不符合条件的情况下,才会返回权限不足的错误 */
if ((ia_valid & ATTR_UID) &&
(current_fsuid() != inode->i_uid ||
attr->ia_uid != inode->i_uid) && !capable(CAP_CHOWN))
return -EPERM;
[...]
}

SGID 权限位

SGID与SUID权限位类似,当设置该权限位时,就意味着无论是谁执行该文件,都会拥有该文件所有者所在组的权限。

Stricky 权限位

Stricky位只有配置在目录上才有意义。当目录配置上sticky位时,其效果是即使所有的用户都拥有写权限和执行权限,该目录下的文件也只能被root或文件所有者删除。

内核实现

1
2
3
4
5
6
7
8
9
10
static int may_delete(struct inode *dir,struct dentry *victim,int isdir)
{
[...]
if (check_sticky(dir, victim->d_inode)||
IS_APPEND(victim->d_inode)||
IS_IMMUTABLE(victim->d_inode) ||
IS_SWAPFILE(victim->d_inode))
return -EPERM;
[...]
}

在删除文件前,内核要调用may_delete来判断该文件是否可以被删除。在这个函数中,内核通过调用check_sticky来检查文件的sticky标志位,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline int check_sticky(struct inode *dir, struct inode *inode)
{
/* 得到当前文件访问权限的uid */
uid_t fsuid = current_fsuid();
/* 判断上级目录是否设置了sticky标志位 */
if (!(dir->i_mode & S_ISVTX))
return 0;
/* 检查名称空间 */
if (current_user_ns() != inode_userns(inode))
goto other_userns;
/* 检查当前文件的uid是否与当前用户的uid相同 */
if (inode->i_uid == fsuid)
return 0;
/* 检查文件所处目录的uid是否与当前用户的uid相同 */
if (dir->i_uid == fsuid)
return 0;
/* 该文件不属于当前用户 */
other_userns:
return !ns_capable(inode_userns(inode), CAP_FOWNER);
}

当文件所处的目录设置了sticky位,即使用户(root用户除外)拥有了对应的权限,只要不是目录或文件的拥有者,就无法删除该文件。

文件截断

Linux提供了两个截断文件的API:

1
2
3
4
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

两者之间的唯一区别在于,truncate截断的是路径path指定的文件,ftruncate截断的是fd引用的文件。length可以大于文件本身的大小,这时文件长度将变为length的大小,扩充的内容均被填充为0。需要注意的是,尽管ftruncate使用的是文件描述符,但是其并不会更新当前文件的偏移。

truncate 内核源码追踪

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
SYSCALL_DEFINE2(truncate, const char __user *, path, long, length)
{
return do_sys_truncate(path, length);
}

static long do_sys_truncate(const char __user *pathname, loff_t length)
{
struct path path;
struct inode *inode;
int error;
error = -EINVAL;
/* 长度不能为负数 */
if (length < 0)
goto out;
/* 得到路径结构 */
error = user_path(pathname, &path);
if (error)
goto out;
inode = path.dentry->d_inode;
error = -EISDIR;
/* 目录不能被截断 */
if (S_ISDIR(inode->i_mode))
goto dput_and_out;
error = -EINVAL;
/* 不是普通文件不能被截断 */
if (!S_ISREG(inode->i_mode))
goto dput_and_out;
/* 尝试获得文件系统的写权限 */
error = mnt_want_write(path.mnt);
if (error)
goto dput_and_out;
/* 检查是否有文件写权限 */
error = inode_permission(inode, MAY_WRITE);
if (error)
goto mnt_drop_write_and_out;
error = -EPERM;
/* 文件设置了追加属性,则不能被截断 */
if (IS_APPEND(inode))
goto mnt_drop_write_and_out;
/* 得到inode的写权限 */
error = get_write_access(inode);
if (error)
goto mnt_drop_write_and_out;
/* 查看是否与文件lease锁相冲突 */
error = break_lease(inode, O_WRONLY);
/* 检查是否与文件锁相冲突 */
if (error)
goto put_write_and_out;
error = locks_verify_truncate(inode, NULL, length);
if (!error)
error = security_path_truncate(&path);
/* 如果没有错误,则进行真正的截断 */
if (!error)
error = do_truncate(path.dentry, length, 0, NULL);
put_write_and_out:
put_write_access(inode);
mnt_drop_write_and_out:
mnt_drop_write(path.mnt);
dput_and_out:
path_put(&path);
out:
return error;
}

int do_truncate(struct dentry *dentry, loff_t length, unsigned int time_attrs,
struct file *filp)
{
int ret;
struct iattr newattrs;
if (length < 0)
return -EINVAL;
/* 设置要改变的属性,对于截断来说,最重要的是文件长度 */
newattrs.ia_size = length;
newattrs.ia_valid = ATTR_SIZE | time_attrs;
if (filp) {
newattrs.ia_file = filp;
newattrs.ia_valid |= ATTR_FILE;
}
/*
* suid权限一定会被去掉同时设置sgid和xgrp时,sgid权限也会被去掉
*/
ret = should_remove_suid(dentry);
if (ret)
newattrs.ia_valid |= ret | ATTR_FORCE;
/* 修改inode属性 */
mutex_lock(&dentry->d_inode->i_mutex);
ret = notify_change(dentry, &newattrs);
mutex_unlock(&dentry->d_inode->i_mutex);
return ret;
}

ftruncate 内核源码追踪

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
SYSCALL_DEFINE2(ftruncate, unsigned int, fd, unsigned long, length)
{
/* 真正的工作函数do_sys_ftruncate */
long ret = do_sys_ftruncate(fd, length, 1);
/* avoid REGPARM breakage on x86: */
asmlinkage_protect(2, ret, fd, length);
return ret;
}
static long do_sys_ftruncate(unsigned int fd, loff_t length, int small)
{
struct inode * inode;
struct dentry *dentry;
struct file * file;
int error;
error = -EINVAL;
/* 长度检查 */
if (length < 0)
goto out;
error = -EBADF;
/* 从文件描述符得到file指针 */
file = fget(fd);
if (!file)
goto out;
/* 如果文件是以O_LARGEFILE选项打开的,则将标志small置为0即假 */
if (file->f_flags & O_LARGEFILE)
small = 0;
dentry = file->f_path.dentry;
inode = dentry->d_inode;
error = -EINVAL;
/* 如果文件不是普通文件或文件不是写打开,则报错 */
if (!S_ISREG(inode->i_mode) || !(file->f_mode & FMODE_WRITE))
goto out_putf;
error = -EINVAL; /* Cannot ftruncate over 2^31 bytes without large file support */
/* 如果文件不是以O_LARGEFILE打开的话,长度就不能超过MAX_NON_LFS */
if (small && length > MAX_NON_LFS)
goto out_putf;
error = -EPERM;
/* 如果是追加模式打开的,也不能进行截断 */
if (IS_APPEND(inode))
goto out_putf;
/* 检查是否有锁冲突 */
error = locks_verify_truncate(inode, file, length);
if (!error)
error = security_path_truncate(&file->f_path);
if (!error) {
/* 执行截断操作—前文已经分析过 */
error = do_truncate(dentry, length, ATTR_MTIME|ATTR_CTIME, file);}
out_putf:
fput(file);
out:
return error;
}

标准I/O库

stdin,stdout和stderr

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

查看C库头文件stdio.h中的源码:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IO_FILE FILE;
/* 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. */
#ifdef __STDC__
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#endif

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

1
2
3
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

继续查看_IO_2_1_stdin_等的定义,代码如下:

1
2
3
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);

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

fdopen与fileno

Linux提供了文件描述符,而C库又提供了文件流。在平时的工作中,有时候需要在两者之间进行切换,因此C库提供了两个API:

1
2
3
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);

fdopen用于从文件描述符fd生成一个文件流FILE,而fileno则用于从文件流FILE得到对应的文件描述符。

fdopen 实现如下:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
FILE *
_IO_new_fdopen (int fd, const char *mode)
{
int read_write;
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f;
int i;
int use_mmap = 0;

/* Decide whether we modify the offset of the file we attach to and seek to
the end of file. We only do this if the mode is 'a' and if the file
descriptor did not have O_APPEND in its flags already. */
bool do_seek = false;

switch (*mode)
{
case 'r':
read_write = _IO_NO_WRITES;
break;
case 'w':
read_write = _IO_NO_READS;
break;
case 'a':
read_write = _IO_NO_READS|_IO_IS_APPENDING;
break;
default:
__set_errno (EINVAL);
return NULL;
}
for (i = 1; i < 5; ++i)
{
switch (*++mode)
{
case '\0':
break;
case '+':
read_write &= _IO_IS_APPENDING;
break;
case 'm':
use_mmap = 1;
continue;
case 'x':
case 'b':
default:
/* Ignore */
continue;
}
break;
}
int fd_flags = __fcntl (fd, F_GETFL);
if (fd_flags == -1)
return NULL;

if (((fd_flags & O_ACCMODE) == O_RDONLY && !(read_write & _IO_NO_WRITES))
|| ((fd_flags & O_ACCMODE) == O_WRONLY && !(read_write & _IO_NO_READS)))
{
__set_errno (EINVAL);
return NULL;
}

/* The May 93 draft of P1003.4/D14.1 (redesignated as 1003.1b)
[System Application Program Interface (API) Amendment 1:
Realtime Extensions], Rationale B.8.3.3
Open a Stream on a File Descriptor says:

Although not explicitly required by POSIX.1, a good
implementation of append ("a") mode would cause the
O_APPEND flag to be set.

(Historical implementations [such as Solaris2] do a one-time
seek in fdopen.)

However, we do not turn O_APPEND off if the mode is "w" (even
though that would seem consistent) because that would be more
likely to break historical programs.
*/
if ((read_write & _IO_IS_APPENDING) && !(fd_flags & O_APPEND))
{
do_seek = true;
if (__fcntl (fd, F_SETFL, fd_flags | O_APPEND) == -1)
return NULL;
}

new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd,
#if _G_HAVE_MMAP
(use_mmap && (read_write & _IO_NO_WRITES))
? &_IO_wfile_jumps_maybe_mmap :
#endif
&_IO_wfile_jumps);
_IO_JUMPS (&new_f->fp) =
#if _G_HAVE_MMAP
(use_mmap && (read_write & _IO_NO_WRITES)) ? &_IO_file_jumps_maybe_mmap :
#endif
&_IO_file_jumps;
_IO_new_file_init_internal (&new_f->fp);
/* We only need to record the fd because _IO_file_init_internal will
have unset the offset. It is important to unset the cached
offset because the real offset in the file could change between
now and when the handle is activated and we would then mislead
ftell into believing that we have a valid offset. */
new_f->fp.file._fileno = fd;
new_f->fp.file._flags &= ~_IO_DELETE_DONT_CLOSE;

_IO_mask_flags (&new_f->fp.file, read_write,
_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);

/* For append mode, set the file offset to the end of the file if we added
O_APPEND to the file descriptor flags. Don't update the offset cache
though, since the file handle is not active. */
if (do_seek && ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
== (_IO_IS_APPENDING | _IO_NO_READS)))
{
off64_t new_pos = _IO_SYSSEEK (&new_f->fp.file, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE)
return NULL;
}
return &new_f->fp.file;
}

其基本工作是创建一个新的文件流FILE,并建立文件流FILE与描述符的对应关系。


fileno 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int
__fileno (FILE *fp)
{
CHECK_FILE (fp, EOF);

if (!(fp->_flags & _IO_IS_FILEBUF) || _IO_fileno (fp) < 0)
{
__set_errno (EBADF);
return -1;
}

return _IO_fileno (fp);
}

从fileno的实现基本上就可以得知文件流与文件描述符的对应关系。文件流FILE保存了文件描述符的值。当从文件流转换到文件描述符时,可以直接通过当前FILE保存的值_fileno得到fd。而从文件描述符转换到文件流时,C库返回的都是一个重新申请的文件流FILE,且这个FILE的_fileno保存了文件描述符。因此无论是fdopen还是fileno,关闭文件时,都要使用fclose来关闭文件,而不是用close。因为只有采用此方式,fclose作为C库函数,才会释放文件流FILE占用的内存。

  • Title: 浅析Linux内核之文件与IO
  • Author: 韩乔落
  • Created at : 2024-01-22 10:07:36
  • Updated at : 2024-04-28 19:47:52
  • Link: https://jelasin.github.io/2024/01/22/浅析Linux内核之文件与IO/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments