内核文件表 文件
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 ; unsigned long *close_on_exec; unsigned long *open_fds; unsigned long *full_fds_bits; struct rcu_head rcu ; }; struct files_struct { atomic_t count; struct fdtable __rcu *fdt ; struct fdtable fdtab ; spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; struct embedded_fd_set close_on_exec_init ; struct embedded_fd_set open_fds_init ; struct file __rcu * fd_array [NR_OPEN_DEFAULT ]; };
下面看看files_struct
是如何使用默认的fdtab
和fd_array
的,init
是Linux
的第一个进程,它的文件表是一个全局变量,代码如下:
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.fdt
和init_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 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_struct
,fdtable
和files
的关系如下图所示。
除了文件名以外的所有文件信息,都存在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; gid_t i_gid; kdev_t i_rdev; loff_t i_size; struct timespec i_atime ; struct timespec i_mtime ; struct timespec i_ctime ; 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 ; 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 ) { 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_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, }; if (how.flags & O_PATH) how.flags &= O_PATH_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 ; 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_zero_bit(fdt->open_fds->fds_bits, fdt->max_fds, fd); error = expand_files(files, fd); if (error < 0 ) goto out; if (error) goto repeat; if (start <= files->next_fd) files->next_fd = fd + 1 ; FD_SET(fd, fdt->open_fds); if (flags & O_CLOEXEC) FD_SET(fd, fdt->close_on_exec); else FD_CLR(fd, fdt->close_on_exec); error = fd; #if 1 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 ); 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); if (fd >= fdt->max_fds) goto out_unlock; filp = fdt->fd[fd]; if (!filp) goto out_unlock; rcu_assign_pointer(fdt->fd[fd], NULL ); FD_CLR(fd, fdt->close_on_exec); __put_unused_fd(files, fd); spin_unlock(&files->file_lock); retval = filp_close(filp, files); 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_CLR(fd, fdt->open_fds); 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, .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 = 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 = 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; file = fget_light(fd, &fput_needed); if (!file) goto bad; retval = -EINVAL; if (origin <= SEEK_MAX) { loff_t res = vfs_llseek(file, offset, origin); retval = res; if (res != (loff_t )retval) retval = -EOVERFLOW; } 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 ); 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: if (offset == 0 ) { retval = file->f_pos; goto out; } offset += file->f_pos; break ; case SEEK_DATA: if (offset >= inode->i_size) { retval = -ENXIO; goto out; } break ; case SEEK_HOLE: if (offset >= inode->i_size) { retval = -ENXIO; goto out; } offset = inode->i_size; break ; } retval = -EINVAL; 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; file = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_read(file, buf, count, &pos); file_pos_write(file, pos); 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; if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) return -EFAULT; ret = rw_verify_area(READ, file, pos, count); if (ret >= 0 ) { count = ret; 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 = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_write(file, buf, count, &pos); file_pos_write(file, pos); 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; ret = rw_verify_area(WRITE, file, pos, count); if (ret >= 0 ) { count = ret; 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; struct file *file = fget_raw(fildes); if (file) { ret = get_unused_fd(); if (ret >= 0 ) { 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 ); 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) { if (unlikely(newfd == oldfd)) { struct files_struct *files = current->files; int retval = oldfd; rcu_read_lock(); if (!fcheck_files(files, oldfd)) retval = -EBADF; rcu_read_unlock(); return retval; } 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 ; if ((flags & ~O_CLOEXEC) != 0 ) return -EINVAL; if (unlikely(oldfd == newfd)) return -EINVAL; spin_lock(&files->file_lock); err = expand_files(files, newfd); 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); tofree = fdt->fd[newfd]; if (!tofree && FD_ISSET(newfd, fdt->open_fds)) goto out_unlock; get_file(file); rcu_assign_pointer(fdt->fd[newfd], file); FD_SET(newfd, fdt->open_fds); if (flags & O_CLOEXEC) FD_SET(newfd, fdt->close_on_exec); else FD_CLR(newfd, fdt->close_on_exec); spin_unlock(&files->file_lock); 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(0 ); sync_filesystems(1 ); 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){ 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); 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); 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 = fget(fd); if (file) { 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; err = filemap_write_and_wait_range(inode->i_mapping, start, end); if (err) return err; mutex_lock(&inode->i_mutex); ret = sync_mapping_buffers(inode->i_mapping); if (!(inode->i_state & I_DIRTY)) goto out; if (datasync && !(inode->i_state & I_DIRTY_DATASYNC)) goto out; 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; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; time_t st_atime; time_t st_mtime; time_t st_ctime; };
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; error = vfs_stat(filename, &stat); if (error) return error; 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; retval = security_inode_getattr(mnt, dentry); if (retval) return retval; if (inode->i_op->getattr) return inode->i_op->getattr(mnt, dentry, stat); 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; [...] 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_t fsuid = current_fsuid(); if (!(dir->i_mode & S_ISVTX)) return 0 ; if (current_user_ns() != inode_userns(inode)) goto other_userns; if (inode->i_uid == fsuid) return 0 ; 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; error = get_write_access(inode); if (error) goto mnt_drop_write_and_out; 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; } ret = should_remove_suid(dentry); if (ret) newattrs.ia_valid |= ret | ATTR_FORCE; 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) { long ret = do_sys_ftruncate(fd, length, 1 ); 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 = fget(fd); if (!file) goto out; 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; 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 ;extern struct _IO_FILE *stdin ; extern struct _IO_FILE *stdout ; extern struct _IO_FILE *stderr ; #ifdef __STDC__ #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 ; 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 : 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 ; } 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); 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); 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占用的内存。