Linux内核将一切视为文件,那么Linux的文件是什么呢?其既可以是事实上的真正的物理文件,也可以是设备、管道,甚至还可以是一块内存。狭义的文件是指文件系统中的物理文件,而广义的文件则可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一致的文件操作接口。
从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就是一个文件句柄。那么何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描述符后,可以通过它得到用于管理文件的真正的数据结构。
使用文件描述符即句柄,有两个好处:一是增加了安全性,句柄类型对用户完全透明,用户无法通过任何hacking的方式,更改句柄对应的内部结果,比如Linux内核的文件描述符,只有内核才能通过该值得到对应的文件结构;二是增加了可扩展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时发生变化,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代码。
Linux的每个进程都会维护一个文件表,以便维护(并不是指包含,其中有指针指向file结构(偏移量,引用计数,文件信息))该进程打开文件的信息,包括打开的文件个数、每个打开文件的偏移量等信息,
内核中进程对应的结构是PCB(task_struct)pcb中的一个指针此进程独有的文件表结构(包含文件描述符表)(struct files_struct)
1. struct files_struct {
2. /* count为文件表files_struct的引用计数 */
3. atomic_t count;
4. /* 文件描述符表 */
5. /*
6. 为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针, 而fdtab为普通变量。一般情况下,
7. fdt是指向fdtab的, 当需要它的时候, 才会真正动态申请内存。因为默认大小的文件表足以应付大多数
8. 情况, 因此这样就可以避免频繁的内存申请。
9. 这也是内核的常用技巧之一。在创建时, 使用普通的变量或者数组, 然后让指针指向它, 作为默认情况使
10. 用。只有当进程使用量超过默认值时, 才会动态申请内存。
11. *//*
12. struct fdtable __rcu *fdt;
13. struct fdtable fdtab;
14. * written part on a separate cache line in SMP
15. */
16. /* 使用____cacheline_aligned_in_smp可以保证file_lock是以cache
17. line 对齐的, 避免了false sharing */
18. spinlock_t file_lock ____cacheline_aligned_in_smp;
19. /* 用于查找下一个空闲的fd */
20. int next_fd;
21. /* 保存执行exec需要关闭的文件描述符的位图 */
22. struct embedded_fd_set close_on_exec_init;
23. /* 保存打开的文件描述符的位图 */
24. struct embedded_fd_set open_fds_init;
25. /* fd_array为一个固定大小的file结构数组。struct file是内核用于文
26. 件管理的结构。这里使用默认大小的数组, 就是为了可以涵盖大多数情况, 避免动
27. 态分配 */
28. struct file __rcu * fd_array[NR_OPEN_DEFAULT];
29. };
30.
31.
32.
以下来源: http://blog.csdn.net/kennyrose/article/details/7595013#
每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。每次打开一个文件,除非明确要求,否则文件位置都被置为0,即文件的开始处,此后的读或写操作都将从文件的开始处执行,但你可以通过执行系统调用LSEEK(随机存储)对这个文件位置进行修改。Linux中专门用了一个数据 结构file来保存打开文件的文件位置,这个结构称为打开的文件描述(open file description)。这个数据结构的设置是煞费苦心的,因为它与进程的联系非常紧密,可以说这是VFS中一个比较难于理解的数据结构,file结构中主要保存了文件位置,此外,还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。
1. struct file
2. {
3.
4. struct list_head f_list; /*所有打开的文件形成一个链表*/
5.
6. struct dentry *f_dentry; /*指向相关目录项的指针*/
7.
8. struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
9.
10. struct file_operations *f_op; /*指向文件操作表的指针*/
11.
12. mode_t f_mode; /*文件的打开模式*/
13.
14. loff_t f_pos; /*文件的当前位置*/
15.
16. unsigned short f_flags; /*打开文件时所指定的标志*/
17.
18. unsigned short f_count; /*使用该结构的进程数*/
19.
20. unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
21.
22. /*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
23.
24. int f_owner; /* 通过信号进行异步I/O数据的传送*/
25.
26. unsigned int f_uid, f_gid; /*用户的UID和GID*/
27.
28. int f_error; /*网络写操作的错误码*/
29.
30.
31.
32. unsigned long f_version; /*版本号*/
33.
34. void *private_data; /* tty驱动程序所需 */
35.
36. };
内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述表中每一项都是一个指针,指向一个用于 描述打开的文件的数据块———file对象,file对象中描述了文件的打开模式,读写位置等重要信息,当进程打开一个文件时,内核就会创建一个新的 file对象。需要注意的是,file对象不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的file对象,从而共享这个打开的文件。 file对象有引用计数,记录了引用这个对象的文件描述符个数,只有当引用计数为0时,内核才销毁file对象,因此某个进程关闭文件,不影响与之共享同 一个file对象的进程.
file对象中包含一个指针,指向dentry对象。dentry对象代表一个独立的文件路径,如果一个文件路径被打开多次,那么会建立多个file对象,但它们都指向同一个dentry对象。
dentry对象中又包含一个指向inode对象的指针。inode对象代表一个独立文件。因为存在硬链接与符号链接,因此不同的dentry 对象可以指向相同的inode对象.inode 对象包含了最终对文件进行操作所需的所有信息,如文件系统类型、文件的操作方法、文件的权限、访问日期等。
打开文件后,进程得到的文件描述符实质上就是文件描述符表的下标,内核根据这个下标值去访问相应的文件对象,从而实现对文件的操作。
注意,同一个进程多次打开同一个文件时,内核会创建多个file对象。
当进程使用fork系统调用创建一个子进程后,子进程将继承父进程的文件描述符表,因此在父进程中打开的文件可以在子进程中用同一个描述符访问。
---------------------------------------------------------------open解析---------------------------------------------------
1. int open(const char *pathname, int flags);
2. int open(const char *pathname, int flags, mode_t mode);
前一个是glibc封装的函数,后一个是系统调用
open源码追踪:
1. long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
2. {
3. struct open_flags op;
4. /* flags为用户层传递的参数, 内核会对flags进行合法性检查, 并根据mode生成新的flags值赋给 lookup */
5. int lookup = build_open_flags(flags, mode, &op);
6. /* 将用户空间的文件名参数复制到内核空间 */
7. char *tmp = getname(filename);
8. int fd = PTR_ERR(tmp);
9. if (!IS_ERR(tmp)) {
10. /* 未出错则申请 新的文件描述符 */
11. fd = get_unused_fd_flags(flags);
12. if (fd >= 0) {/* 申请新的文件管理结构file */
13. struct file *f = do_filp_open(dfd, tmp, &op, lookup);
14. if (IS_ERR(f)) {
15. put_unused_fd(fd);
16. fd = PTR_ERR(f);
17. } else {
18. /* 产生文件打开的通知事件 */
19. fsnotify_open(f);
20. /* 将文件描述符fd与文件管理结构file对应起来, 即安装 */
21. fd_install(fd, f);
22. }
23. }
24. putname(tmp);
25. }
26. return fd;
27. }
从上面来看,打开文件,内核消耗了2种资源:文件描述符跟内核管理文件结构file
根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。Linux是如何实现这一标准的呢?
在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))来选择文件描述符,代码如下
1. int alloc_fd(unsigned start, unsigned flags)
2. {
3. struct files_struct *files = current->files;//获取当前进程的对应包含文件描述符表的结构
4. unsigned int fd;
5. int error;
6. struct fdtable *fdt;
7. /* files为进程的文件表, 下面需要更改文件表, 所以需要先锁文件表 */
8. spin_lock(&files->file_lock);
9. repeat:
10. /* 得到文件描述符表 */
11. fdt = files_fdtable(files);
12. /* 从start开始, 查找未用的文件描述符。在打开文件时, start为0 */
13. fd = start;
14. /* files->next_fd为上一次成功找到的fd的下一个描述符。使用next_fd, 可以快速找到未用的文件描述符;*/
15. if (fd < files->next_fd)
16. fd = files->next_fd;
17. /*
18. 当小于当前文件表支持的最大文件描述符个数时, 利用位图找到未用的文件描述符。
19. 如果大于max_fds怎么办呢?如果大于当前支持的最大文件描述符, 那它肯定是未
20. 用的, 就不需要用位图来确认了。
21. */
22. if (fd < fdt->max_fds)
23. fd = find_next_zero_bit(fdt->open_fds->fds_bits,
24. fdt->max_fds, fd);
25. /* expand_files用于在必要时扩展文件表。何时是必要的时候呢?比如当前文件描述符已经超过了当
26. 前文件表支持的最大值的时候。 */
27. error = expand_files(files, fd);
28. if (error < 0)
29. goto out;
30. /*
31. * If we needed to expand the fs array we
32. * might have blocked - try again.
33. */
34. if (error)
35. goto repeat;
36. /* 只有在start小于next_fd时, 才需要更新next_fd, 以尽量保证文件描述符的连续性。*/
37. if (start <= files->next_fd)
38. files->next_fd = fd + 1;
39. /* 将打开文件位图open_fds对应fd的位置置位 */
40. FD_SET(fd, fdt->open_fds);
41. /* 根据flags是否设置了O_CLOEXEC, 设置或清除fdt->close_on_exec */
42. if (flags & O_CLOEXEC)
43. FD_SET(fd, fdt->close_on_exec);
44. else
45. FD_CLR(fd, fdt->close_on_exec);
46. error = fd;
47. #if 1
48. /* Sanity check */
49. if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {
50. printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
51. rcu_assign_pointer(fdt->fd[fd], NULL);
52. }
53. #endif
54. out:
55. spin_unlock(&files->file_lock);
56. return error;
57. }
下面内核使用fd_install将文件管理结构file与fd组合起来,具体操作请看如下代码:
1. void fd_install(unsigned int fd, struct file *file)
2. {
3. struct files_struct *files = current->files;//获得进程文件表(包含文件描述符表)
4. struct fdtable *fdt;
5. spin_lock(&files->file_lock);
6. /* 得到文件描述符表 */
7. fdt = files_fdtable(files);
8. BUG_ON(fdt->fd[fd] != NULL);
9. /*
10. 将文件描述符表中的file类型的指针数组中对应fd的项指向file。
11. 这样文件描述符fd与file就建立了对应关系
12. */
13. rcu_assign_pointer(fdt->fd[fd], file);
14. spin_unlock(&files->file_lock);
15. }
当用户使用fd与内核交互时,内核可以用fd从fdt->fd[fd]中得到内部管理文件的结构struct file。
-------------------------------------------close(关闭文件)------------------------------
close用于关闭文件描述符。而文件描述符可以是普通文件,也可以是设备,还可以是socket。在关闭时,VFS会根据不同的文件类型,执行不同的操作。
下面将通过跟踪close的内核源码来了解内核如何针对不同的文件类型执行不同的操作。
1. 分析close源码跟踪
首先,来看一下close的源码实现,代码如下
1. SYSCALL_DEFINE1(close, unsigned int, fd)
2. {
3. struct file * filp;
4. /* 得到当前进程的文件表 */
5. struct files_struct *files = current->files;
6. struct fdtable *fdt;
7. int retval;
8. spin_lock(&files->file_lock);
9. /* 通过文件表, 取得文件描述符表 */
10. fdt = files_fdtable(files);
11. /* 参数fd大于文件描述符表记录的最大描述符, 那么它一定是非法的描述符 */
12. if (fd >= fdt->max_fds)
13. goto out_unlock;
14. /* 利用fd作为索引, 得到file结构指针 */
15. filp = fdt->fd[fd];
16. /*
17. 检查filp是否为NULL。正常情况下, filp一定不为NULL。
18. */
19. if (!filp)
20. goto out_unlock;
21. /* 将对应的filp置为0*/
22. rcu_assign_pointer(fdt->fd[fd], NULL);
23. /* 清除fd在close_on_exec位图中的位 */
24. FD_CLR(fd, fdt->close_on_exec);
25. /* 释放该fd, 或者说将其置为unused。*/
26. __put_unused_fd(files, fd);
27. spin_unlock(&files->file_lock);
28. /* 关闭file结构 */
29. retval = filp_close(filp, files); //这里将引用计数
30. /* can't restart close syscall because file table entry was cleared */
31. if (unlikely(retval == -ERESTARTSYS ||
32. retval == -ERESTARTNOINTR ||
33. retval == -ERESTARTNOHAND ||
34. retval == -ERESTART_RESTARTBLOCK))
35. retval = -EINTR;
36. return retval;
37. out_unlock:
38. spin_unlock(&files->file_lock);
39. return -EBADF;
40. }
41. EXPORT_SYMBOL(sys_close);
请注意26行的__put_unused_fd,源码如下所示:
1. static void __put_unused_fd(struct files_struct *files, unsigned int fd)
2. {
3. /* 取得文件描述符表 */
4. struct fdtable *fdt = files_fdtable(files);
5. /* 清除fd在open_fds位图的位 */
6. __FD_CLR(fd, fdt->open_fds);
7. /* 如果fd小于next_fd, 重置next_fd为释放的fd */
8. if (fd < files->next_fd)
9. files->next_fd = fd;
10. }
看到这里,我们来回顾一下之前分析过的alloc_fd函数,就可以总结出完整的Linux文件描述符选择计划:
•Linux选择文件描述符是按从小到大的顺序进行寻找的,文件表中next_fd用于记录下一次开始寻找的起点。当有空闲的描述符时,即可分配。
•当某个文件描述符关闭时,如果其小于next_fd,则next_fd就重置为这个描述符,这样下一次分配就会立刻重用这个文件描述符。
以上的策略,总结成一句话就是“Linux文件描述符策略永远选择最小的可用的文件描述符”。——这也是POSIX标准规定的。
从__put_unused_fd退出后,close会接着调用filp_close,其调用路径为filp_close->fput。在fput中,会对当前文件struct file的引用计数减一并检查其值是否为0。当引用计数为0时,表示该struct file没有被其他人使用,则可以调用__fput执行真正的文件释放操作,然后调用要关闭文件所属文件系统的release函数,从而实现针对不同的文件类型来执行不同的关闭操作。
下一节让我们来看看Linux如何针对不同的文件类型,挂载不同的文件操作函数files_operations。
以下一段来源: http://www.voidcn.com/blog/u014338577/article/p-5769774.html
每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。对于同一个文件系统上打开的常规文件来说,read、write等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的file结构体指向同一个file_operations结构体。如果打开一个字符设备文件,那么它的read、write操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以file结构体应该指向不同的file_operations结构体,其中的各种文件操作函数由该设备的驱动程序实现。
每个file结构体都有一个指向dentry结构体的指针,“dentry”是directory entry(目录项)的缩写。我们传给open、stat等函数的参数的是一个路径,例如/home/akaedu/a,需要根据路径找到文件的inode。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体,只要沿着路径各部分的dentry搜索即可,从根目录/找到home目录,然后找到akaedu目录,然后找到文件a。dentry cache只保存最近访问过的目录项,如果要找的目录项在cache中没有,就要从磁盘读到内存中。
每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。每个inode结构体都有一个指向inode_operations结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。和file_operations不同,inode_operations所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各inode结构体可以指向同一个inode_operations结构体。
inode结构体有一个指向super_block结构体的指针。super_block结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。super_block结构体的s_root成员是一个指向dentry的指针,表示这个文件系统的根目录被mount到哪里,在上图的例子中这个分区被mount到/home目录下。
file、dentry、inode、super_block这 几个结构体组成了VFS的核心概念。对于ext2文件系统来说,在磁盘存储布局上也有inode和超级块的概念,所以很容易和VFS中的概念建立对应关 系。而另外一些文件系统格式来自非UNIX系统(例如Windows的FAT32、NTFS),可能没有inode或超级块这样的概念,但为了能mount到Linux系统,也只好在驱动程序中硬凑一下,在Linux下看FAT32和NTFS分区会发现权限位是错的,所有文件都是rwxrwxrwx,因为它们本来就没有inode和权限位的概念,这是硬凑出来的
----------------------------------------------------以下来看自定义的files_operations,以socket举例,有一个struct file_operations结构体定义了很多函数指针,对应不同的读写关之类的操作,socket的读写关闭等操作分别对应不同的内核函数
1. static const struct file_operations socket_file_ops = {
2. .owner = THIS_MODULE,
3. .llseek = no_llseek,
4. .aio_read = sock_aio_read,
5. .aio_write = sock_aio_write,
6. .poll = sock_poll,
7. .unlocked_ioctl = sock_ioctl,
8. #ifdef CONFIG_COMPAT
9. .compat_ioctl = compat_sock_ioctl,
10. #endif
11. .mmap = sock_mmap,
12. .open = sock_no_open, /* special open code to disallow open via /proc */
13. .release = sock_close,
14. .fasync = sock_fasync,
15. .sendpage = sock_sendpage,
16. .splice_write = generic_splice_sendpage,
17. .splice_read = sock_splice_read,
18. };
在socket中,底层的函数sock_alloc_file用于申请socket文件描述符及文件管理结构file结构。它调用alloc_file来申请管理结构file,并将socket_file_ops这个结构体作为参数,如下所示:
1. file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
2. &socket_file_ops);
1. struct file *alloc_file(struct path *path, fmode_t mode,
2. const struct file_operations *fop)
3. {
4. struct file *file;
5. /* 申请一个file */
6. file = get_empty_filp();
7. if (!file)
8. return NULL;
9. file->f_path = *path;
10. file->f_mapping = path->dentry->d_inode->i_mapping;
11. file->f_mode = mode;
12. /* 将自定义的文件操作函数指针结构体赋给file->f_op */
13. file->f_op = fop;
14. ……
15. }
在初始化file结构的时候,socket文件系统将其自定义的文件操作赋给了file->f_op,从而实现了在VFS中可以调用socket文件系统自定义的操作。
----------------------------------遗忘close造成的后果---------------------------
文件描述符没有被释放。
用于文件管理的某些内存结构也没有被释放
对于普通进程来说,即使应用忘记了关闭文件,当进程退出时,Linux内核也会自动关闭文件,释放内存(详细过程见后文)。但是对于一个常驻进程来说,问题就变得严重了。
先看第一种情况,如果文件描述符没有被释放,那么再次申请新的描述符时,就不得不扩展当前的文件描述符表,如果文件描述发表始终不释放,个数迟早会达到上限,返回EMFILE错误
-----------------------如何查看文件资源泄露--------------
使用lsof工具
---------------------------------读取文件
Linux中读取文件操作时,最常用的就是read函数,其原型如下
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被信号中断所造成的。这两种情况基本上都可以视为正常情况。
1. SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
2. {
3. struct file *file;
4. ssize_t ret = -EBADF;
5. int fput_needed;
6. /* 通过文件描述符fd得到管理结构file */
7. file = fget_light(fd, &fput_needed);
8. if (file) {
9. /* 得到文件的当前偏移量 */
10. loff_t pos = file_pos_read(file);
11. /* 利用vfs进行真正的read */
12. ret = vfs_read(file, buf, count, &pos);
13. /* 更新文件偏移量 */
14. file_pos_write(file, pos);
15. /* 归还管理结构file, 如有必要, 就进行引用计数操作*/
16. fput_light(file, fput_needed);
17. }
18. return ret;
19. }
查看VFS_read代码:
1. ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
2. {
3. ssize_t ret;
4. /* 检查文件是否为读取打开 */
5. if (!(file->f_mode & FMODE_READ))
6. return -EBADF;
7. /* 检查文件是否支持读取操作 */
8. if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
9. return -EINVAL;
10. /* 检查用户传递的参数buf的地址是否可写 */
11. if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
12. return -EFAULT;
13. /* 检查要读取的文件范围实际可读取的字节数 */
14. ret = rw_verify_area(READ, file, pos, count);
15. if (ret >= 0) {
16. /* 根据上面的结构, 调整要读取的字节数 */
17. count = ret;
18. /*
19. 如果定义read操作, 则执行定义的read操作
20. 如果没有定义read操作, 则调用do_sync_read—其利用异步aio_read来完成同步的read操作。
21. */
22. if (file->f_op->read)
23. ret = file->f_op->read(file, buf, count, pos);
24. else
25. ret = do_sync_read(file, buf, count, pos);
26. if (ret > 0) {
27. /* 读取了一定的字节数, 进行通知操作 */
28. fsnotify_access(file);
29. /* 增加进程读取字节的统计计数 */
30. add_rchar(current, ret);
31. }
32. /* 增加进程系统调用的统计计数 */
33. inc_syscr(current);
34. }
35. return ret;
36. }
上面的代码为read公共部分的源码分析,具体的读取动作是由实际的文件系统决定的。
1.6.2 部分读取
前文中介绍read可以返回比指定count少的字节数,那么什么时候会发生这种情况呢?最直接的想法是在fd中没有指定count大小的数据时。但这种情况下,系统是不是也可以阻塞到满足count个字节的数据呢?那么内核到底采取的是哪种策略呢?
让我们来看看socket文件系统中UDP协议的read实现:socket文件系统只定义了aio_read操作,没有定义普通的read函数。根据前文,在这种情况下
do_sync_read会利用aio_read实现同步读操作。
其调用链为sock_aio_read->do_sock_read->__sock_recvmsg->__sock_recvmsg_nose->udp_recvmsg,代码如下所示:
1. int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
2. size_t len, int noblock, int flags, int *addr_len)
3. ……
4. ulen = skb->len - sizeof(struct udphdr);
5. copied = len;
6. if (copied > ulen)
7. copied = ulen;
8. ……
当UDP报文的数据长度小于参数len时,就会只复制真正的数据长度,那么对于read操作来说,返回的读取字节数自然就小于参数count了。
看到这里,是否已经得到本小节开头部分问题的答案了呢?当fd中的数据不够count大小时,read会返回当前可以读取的字节数?很可惜,答案是否定的。这种行为完全由具体实现来决定。即使同为socket文件系统,TCP套接字的读取操作也会与UDP不同。当TCP的fd的数据不足时,read操作极可能会阻塞,而不是直接返回。注:TCP是否阻塞,取决于当前缓存区可用数据多少,要读取的字节数,以及套接字设置的接收低水位大小。
因此在调用read的时候,只能根据read接口的说明,小心处理所有的情况,而不能主观臆测内核的实现。比如本文中的部分读取情况,阻塞和直接返回两种策略同时存在。
------------------------------------write跟read的实现差不多,这里就不列出来了,主要讨论多个文件同时写-------------
前面说过,文件的读写操作都是从当前文件的偏移处开始的。这个文件偏移量保存在文件表中,而每个进程都有一个文件表。那么当多个进程同时写一个文件时,即使对write进行了锁保护,在进行串行写操作时,文件依然不可避免地会被写乱。根本原因就在于文件偏移量是进程级别的。
当使用O_APPEND以追加的形式来打开文件时,每次写操作都会先定位到文件末尾,然后再执行写操作。
Linux下大多数文件系统都是调用generic_file_aio_write来实现写操作的。在generic_file_aio_write中,有如下代码:
1. mutex_lock(&inode->i_mutex);//加锁
2. blk_start_plug(&plug);
3. ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);//发现文件是追加打开,直接从inode读取最新文件大小作为偏移量
4. mutex_unlock(&inode->i_mutex); //解锁
这里有一个关键的语句,就是使用mutex_lock对该文件对应的inode进行保护,然后调用__generic_file_aio_write->generic_write_check。其部分代码如下:
1. if (file->f_flags & O_APPEND)
2. *pos = i_size_read(inode);
上面的代码中,如果发现文件是以追加方式打开的,则将从inode中读取到的最新文件大小作为偏移量,然后通过__generic_file_aio_write再进行写操作,这样就能保证写操作是在文件末尾追加的。
----------------------------------文件描述符的复制----------------------------
1. int dup(int oldfd);
2. int dup2(int oldfd, int newfd);
•dup会使用一个最小的未用文件描述符作为复制后的文件描述符。
•dup2是使用用户指定的文件描述符newfd来复制oldfd的。如果newfd已经是打开的文件描述符,Linux会先关闭newfd,然后再复制oldfd。
dup的实现
1. SYSCALL_DEFINE1(dup, unsigned int, fildes)
2. {
3. int ret = -EBADF;
4. /* 必须先得到文件管理结构file, 同时也是对描述符fildes的检查 */
5. struct file *file = fget_raw(fildes);
6. if (file) {
7. /* 得到一个未使用的文件描述符 */
8. ret = get_unused_fd();
9. if (ret >= 0) {
10. /* 将文件描述符与file指针关联起来 */
11. fd_install(ret, file);
12. }
13. else
14. fput(file);
15. }
16. return ret;
17. }
在dup中调用get_unused_fd,只是得到一个未用的文件描述符,那么如何实现在dup接口中使用最小的未用文件描述符呢?这就需要回顾1.4.2节中总结过的Linux文件描述符的选择策略了。
Linux总是尝试给用户最小的未用文件描述符,所以get_unused_fd得到的文件描述符始终是最小的可用文件描述符。
查看dup代码实现的第11行
1. void fd_install(unsigned int fd, struct file *file)
2. {
3. struct files_struct *files = current->files;
4. struct fdtable *fdt;
5. /* 对文件表进行保护 */
6. spin_lock(&files->file_lock);
7. /* 得到文件表 */
8. fdt = files_fdtable(files);
9. BUG_ON(fdt->fd[fd] != NULL);
10. /* 让文件表中fd对应的指针等于该文件关联结构file */
11. rcu_assign_pointer(fdt->fd[fd], file);
12. spin_unlock(&files->file_lock);
13. }
在fd_install中,fd与file的关联是利用fd来作为指针数组的索引的,从而让对应的指针指向file。对于dup来说,这意味着数组中两个指针都指向了同一个file。而file是进程中真正的管理文件的结构,文件偏移等信息都是保存在file中的。这就意味着,当使用oldfd进行读写操作时,无论是oldfd还是newfd的文件偏移都会发生变化。
---------------------看一下dup2的实现-------------------
1. SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)
2. {
3. /* 如果oldfd与newfd相等, 这是一种特殊的情况 */
4. if (unlikely(newfd == oldfd)) { /* corner case */
5. struct files_struct *files = current->files;
6. int retval = oldfd;
7. /*
8. 检查oldfd的合法性, 如果是合法的fd, 则直接返回oldfd的值;
9. 如果是不合法的, 则返回EBADF
10. */
11. rcu_read_lock();
12. if (!fcheck_files(files, oldfd))
13. retval = -EBADF;
14. rcu_read_unlock();
15. return retval;
16. }
17. /* 如果oldfd与newfd不同, 则利用sys_dup3来实现dup2 */
18. return sys_dup3(oldfd, newfd, 0);
19. }
20.
21.
------------------------------------文件的元数据获取--------------
什么是文件的元数据呢?其包括文件的访问权限、上次访问的时间戳、所有者、所有组、文件大小等信息。
1. int stat(const char *path, struct stat *buf);
2. int fstat(int fd, struct stat *buf);
3. int lstat(const char *path, struct stat *buf);
这三个函数都可用于得到文件的基本信息,区别在于stat得到路径path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,而lstat与stat则基本相同,只有当path是一个链接文件时,lstat得到的是链接文件自己本身的基本信息而不是其指向文件的信息。
所得到的文件基本信息的结果struct stat的结构如下:
1. struct stat {
2. dev_t st_dev; /* ID of device containing file */
3. ino_t st_ino; /* inode number */
4. mode_t st_mode; /* protection */
5. nlink_t st_nlink; /* number of hard links */
6. uid_t st_uid; /* user ID of owner */
7. gid_t st_gid; /* group ID of owner */
8. dev_t st_rdev; /* device ID (if special file) */
9. off_t st_size; /* total size, in bytes */
10. blksize_t st_blksize; /* blocksize for file system I/O */
11. blkcnt_t st_blocks; /* number of 512B blocks allocated */
12. time_t st_atime; /* time of last access */
13. time_t st_atime; /* time of last access */
14. time_t st_mtime; /* time of last modification */
15. time_t st_ctime; /* time of last status change */
16. };
17.
st_mode要注意一点的是:st_mode,其注释不仅仅是protection,同时也表示文件类型,比如是普通文件还是目录
stat代码实现:
1. SYSCALL_DEFINE2(stat, const char __user *, filename,struct __old_kernel_stat __user *, statbuf){
2. struct kstat stat;
3. int error;
4. /* vfs_stat用于读取文件元数据至stat */
5. error = vfs_stat(filename, &stat);
6. if (error)
7. return error;
8. /* 这里仅是从内核的元数据结构stat复制到用户层的数据结构statbuf中 */
9. return cp_old_stat(&stat, statbuf);
10. }
第5行,vfs_stat是关键。进入vfs_stat->vfs_fstatat->vfs_getattr,代码如下:
1. int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
2. {
3. struct inode *inode = dentry->d_inode;
4. int retval;
5. /* 对获取inode属性操作进行安全性检查 */
6. retval = security_inode_getattr(mnt, dentry);
7. if (retval)
8. return retval;
9. /* 如果该文件系统定义了这个inode的自定义操作函数, 就执行它 */
10. if (inode->i_op->getattr)
11. return inode->i_op->getattr(mnt, dentry, stat);
12. /* 如果文件系统没有定义inode的操作函数, 则执行通用的函数 */
13. generic_fillattr(inode, stat);
14. return 0;
15. }
不失一般性,也可以通过查看第13行的generic_fillattr来进一步了解,代码如下:
1. void generic_fillattr(struct inode *inode, struct kstat *stat)
2. {
3. stat->dev = inode->i_sb->s_dev;
4. stat->ino = inode->i_ino;
5. stat->mode = inode->i_mode;
6. stat->nlink = inode->i_nlink;
7. stat->uid = inode->i_uid;
8. stat->gid = inode->i_gid;
9. stat->rdev = inode->i_rdev;
10. stat->size = i_size_read(inode);
11. stat->atime = inode->i_atime;
12. stat->mtime = inode->i_mtime;
13. stat->ctime = inode->i_ctime;
14. stat->blksize = (1 << inode->i_blkbits);
15. stat->blocks = inode->i_blocks;
16. }
从这里可以看出,所有的文件元数据均保存在inode中,而inode是Linux也是所有类Unix文件系统中的一个概念。这样的文件系统一般将存储区域分为两类,一类是保存文件对象的元信息数据,即inode表;另一类是真正保存文件数据内容的块,所有inode完全由文件系统来维护。但是Linux也可以挂载非类Unix的文件系统,这些文件系统本身没有inode的概念,怎么办?Linux为了让VFS有统一的处理流程和方法,就必须要求那些没有inode概念的文件系统,根据自己系统的特点——如何维护文件元数据,生成“虚拟的”inode以供Linux内核使用。
从数值上看,文件描述符是一个非负整数,其本质就是一个句柄,所以也可以认为文件描述符就是一个文件句柄。那么何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互;而内核拿到文件描述符后,可以通过它得到用于管理文件的真正的数据结构。
使用文件描述符即句柄,有两个好处:一是增加了安全性,句柄类型对用户完全透明,用户无法通过任何hacking的方式,更改句柄对应的内部结果,比如Linux内核的文件描述符,只有内核才能通过该值得到对应的文件结构;二是增加了可扩展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时发生变化,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代码。
Linux的每个进程都会维护一个文件表,以便维护(并不是指包含,其中有指针指向file结构(偏移量,引用计数,文件信息))该进程打开文件的信息,包括打开的文件个数、每个打开文件的偏移量等信息,
内核中进程对应的结构是PCB(task_struct)pcb中的一个指针此进程独有的文件表结构(包含文件描述符表)(struct files_struct)
1. struct files_struct {
2. /* count为文件表files_struct的引用计数 */
3. atomic_t count;
4. /* 文件描述符表 */
5. /*
6. 为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针, 而fdtab为普通变量。一般情况下,
7. fdt是指向fdtab的, 当需要它的时候, 才会真正动态申请内存。因为默认大小的文件表足以应付大多数
8. 情况, 因此这样就可以避免频繁的内存申请。
9. 这也是内核的常用技巧之一。在创建时, 使用普通的变量或者数组, 然后让指针指向它, 作为默认情况使
10. 用。只有当进程使用量超过默认值时, 才会动态申请内存。
11. *//*
12. struct fdtable __rcu *fdt;
13. struct fdtable fdtab;
14. * written part on a separate cache line in SMP
15. */
16. /* 使用____cacheline_aligned_in_smp可以保证file_lock是以cache
17. line 对齐的, 避免了false sharing */
18. spinlock_t file_lock ____cacheline_aligned_in_smp;
19. /* 用于查找下一个空闲的fd */
20. int next_fd;
21. /* 保存执行exec需要关闭的文件描述符的位图 */
22. struct embedded_fd_set close_on_exec_init;
23. /* 保存打开的文件描述符的位图 */
24. struct embedded_fd_set open_fds_init;
25. /* fd_array为一个固定大小的file结构数组。struct file是内核用于文
26. 件管理的结构。这里使用默认大小的数组, 就是为了可以涵盖大多数情况, 避免动
27. 态分配 */
28. struct file __rcu * fd_array[NR_OPEN_DEFAULT];
29. };
30.
31.
32.
以下来源: http://blog.csdn.net/kennyrose/article/details/7595013#
每个文件都有一个32位的数字来表示下一个读写的字节位置,这个数字叫做文件位置。每次打开一个文件,除非明确要求,否则文件位置都被置为0,即文件的开始处,此后的读或写操作都将从文件的开始处执行,但你可以通过执行系统调用LSEEK(随机存储)对这个文件位置进行修改。Linux中专门用了一个数据 结构file来保存打开文件的文件位置,这个结构称为打开的文件描述(open file description)。这个数据结构的设置是煞费苦心的,因为它与进程的联系非常紧密,可以说这是VFS中一个比较难于理解的数据结构,file结构中主要保存了文件位置,此外,还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表,其最大长度是NR_FILE,在fs.h中定义为8192。
1. struct file
2. {
3.
4. struct list_head f_list; /*所有打开的文件形成一个链表*/
5.
6. struct dentry *f_dentry; /*指向相关目录项的指针*/
7.
8. struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
9.
10. struct file_operations *f_op; /*指向文件操作表的指针*/
11.
12. mode_t f_mode; /*文件的打开模式*/
13.
14. loff_t f_pos; /*文件的当前位置*/
15.
16. unsigned short f_flags; /*打开文件时所指定的标志*/
17.
18. unsigned short f_count; /*使用该结构的进程数*/
19.
20. unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
21.
22. /*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
23.
24. int f_owner; /* 通过信号进行异步I/O数据的传送*/
25.
26. unsigned int f_uid, f_gid; /*用户的UID和GID*/
27.
28. int f_error; /*网络写操作的错误码*/
29.
30.
31.
32. unsigned long f_version; /*版本号*/
33.
34. void *private_data; /* tty驱动程序所需 */
35.
36. };
内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述表中每一项都是一个指针,指向一个用于 描述打开的文件的数据块———file对象,file对象中描述了文件的打开模式,读写位置等重要信息,当进程打开一个文件时,内核就会创建一个新的 file对象。需要注意的是,file对象不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的file对象,从而共享这个打开的文件。 file对象有引用计数,记录了引用这个对象的文件描述符个数,只有当引用计数为0时,内核才销毁file对象,因此某个进程关闭文件,不影响与之共享同 一个file对象的进程.
file对象中包含一个指针,指向dentry对象。dentry对象代表一个独立的文件路径,如果一个文件路径被打开多次,那么会建立多个file对象,但它们都指向同一个dentry对象。
dentry对象中又包含一个指向inode对象的指针。inode对象代表一个独立文件。因为存在硬链接与符号链接,因此不同的dentry 对象可以指向相同的inode对象.inode 对象包含了最终对文件进行操作所需的所有信息,如文件系统类型、文件的操作方法、文件的权限、访问日期等。
打开文件后,进程得到的文件描述符实质上就是文件描述符表的下标,内核根据这个下标值去访问相应的文件对象,从而实现对文件的操作。
注意,同一个进程多次打开同一个文件时,内核会创建多个file对象。
当进程使用fork系统调用创建一个子进程后,子进程将继承父进程的文件描述符表,因此在父进程中打开的文件可以在子进程中用同一个描述符访问。
---------------------------------------------------------------open解析---------------------------------------------------
1. int open(const char *pathname, int flags);
2. int open(const char *pathname, int flags, mode_t mode);
前一个是glibc封装的函数,后一个是系统调用
open源码追踪:
1. long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
2. {
3. struct open_flags op;
4. /* flags为用户层传递的参数, 内核会对flags进行合法性检查, 并根据mode生成新的flags值赋给 lookup */
5. int lookup = build_open_flags(flags, mode, &op);
6. /* 将用户空间的文件名参数复制到内核空间 */
7. char *tmp = getname(filename);
8. int fd = PTR_ERR(tmp);
9. if (!IS_ERR(tmp)) {
10. /* 未出错则申请 新的文件描述符 */
11. fd = get_unused_fd_flags(flags);
12. if (fd >= 0) {/* 申请新的文件管理结构file */
13. struct file *f = do_filp_open(dfd, tmp, &op, lookup);
14. if (IS_ERR(f)) {
15. put_unused_fd(fd);
16. fd = PTR_ERR(f);
17. } else {
18. /* 产生文件打开的通知事件 */
19. fsnotify_open(f);
20. /* 将文件描述符fd与文件管理结构file对应起来, 即安装 */
21. fd_install(fd, f);
22. }
23. }
24. putname(tmp);
25. }
26. return fd;
27. }
从上面来看,打开文件,内核消耗了2种资源:文件描述符跟内核管理文件结构file
根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。Linux是如何实现这一标准的呢?
在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0,(flags))来选择文件描述符,代码如下
1. int alloc_fd(unsigned start, unsigned flags)
2. {
3. struct files_struct *files = current->files;//获取当前进程的对应包含文件描述符表的结构
4. unsigned int fd;
5. int error;
6. struct fdtable *fdt;
7. /* files为进程的文件表, 下面需要更改文件表, 所以需要先锁文件表 */
8. spin_lock(&files->file_lock);
9. repeat:
10. /* 得到文件描述符表 */
11. fdt = files_fdtable(files);
12. /* 从start开始, 查找未用的文件描述符。在打开文件时, start为0 */
13. fd = start;
14. /* files->next_fd为上一次成功找到的fd的下一个描述符。使用next_fd, 可以快速找到未用的文件描述符;*/
15. if (fd < files->next_fd)
16. fd = files->next_fd;
17. /*
18. 当小于当前文件表支持的最大文件描述符个数时, 利用位图找到未用的文件描述符。
19. 如果大于max_fds怎么办呢?如果大于当前支持的最大文件描述符, 那它肯定是未
20. 用的, 就不需要用位图来确认了。
21. */
22. if (fd < fdt->max_fds)
23. fd = find_next_zero_bit(fdt->open_fds->fds_bits,
24. fdt->max_fds, fd);
25. /* expand_files用于在必要时扩展文件表。何时是必要的时候呢?比如当前文件描述符已经超过了当
26. 前文件表支持的最大值的时候。 */
27. error = expand_files(files, fd);
28. if (error < 0)
29. goto out;
30. /*
31. * If we needed to expand the fs array we
32. * might have blocked - try again.
33. */
34. if (error)
35. goto repeat;
36. /* 只有在start小于next_fd时, 才需要更新next_fd, 以尽量保证文件描述符的连续性。*/
37. if (start <= files->next_fd)
38. files->next_fd = fd + 1;
39. /* 将打开文件位图open_fds对应fd的位置置位 */
40. FD_SET(fd, fdt->open_fds);
41. /* 根据flags是否设置了O_CLOEXEC, 设置或清除fdt->close_on_exec */
42. if (flags & O_CLOEXEC)
43. FD_SET(fd, fdt->close_on_exec);
44. else
45. FD_CLR(fd, fdt->close_on_exec);
46. error = fd;
47. #if 1
48. /* Sanity check */
49. if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {
50. printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
51. rcu_assign_pointer(fdt->fd[fd], NULL);
52. }
53. #endif
54. out:
55. spin_unlock(&files->file_lock);
56. return error;
57. }
下面内核使用fd_install将文件管理结构file与fd组合起来,具体操作请看如下代码:
1. void fd_install(unsigned int fd, struct file *file)
2. {
3. struct files_struct *files = current->files;//获得进程文件表(包含文件描述符表)
4. struct fdtable *fdt;
5. spin_lock(&files->file_lock);
6. /* 得到文件描述符表 */
7. fdt = files_fdtable(files);
8. BUG_ON(fdt->fd[fd] != NULL);
9. /*
10. 将文件描述符表中的file类型的指针数组中对应fd的项指向file。
11. 这样文件描述符fd与file就建立了对应关系
12. */
13. rcu_assign_pointer(fdt->fd[fd], file);
14. spin_unlock(&files->file_lock);
15. }
当用户使用fd与内核交互时,内核可以用fd从fdt->fd[fd]中得到内部管理文件的结构struct file。
-------------------------------------------close(关闭文件)------------------------------
close用于关闭文件描述符。而文件描述符可以是普通文件,也可以是设备,还可以是socket。在关闭时,VFS会根据不同的文件类型,执行不同的操作。
下面将通过跟踪close的内核源码来了解内核如何针对不同的文件类型执行不同的操作。
1. 分析close源码跟踪
首先,来看一下close的源码实现,代码如下
1. SYSCALL_DEFINE1(close, unsigned int, fd)
2. {
3. struct file * filp;
4. /* 得到当前进程的文件表 */
5. struct files_struct *files = current->files;
6. struct fdtable *fdt;
7. int retval;
8. spin_lock(&files->file_lock);
9. /* 通过文件表, 取得文件描述符表 */
10. fdt = files_fdtable(files);
11. /* 参数fd大于文件描述符表记录的最大描述符, 那么它一定是非法的描述符 */
12. if (fd >= fdt->max_fds)
13. goto out_unlock;
14. /* 利用fd作为索引, 得到file结构指针 */
15. filp = fdt->fd[fd];
16. /*
17. 检查filp是否为NULL。正常情况下, filp一定不为NULL。
18. */
19. if (!filp)
20. goto out_unlock;
21. /* 将对应的filp置为0*/
22. rcu_assign_pointer(fdt->fd[fd], NULL);
23. /* 清除fd在close_on_exec位图中的位 */
24. FD_CLR(fd, fdt->close_on_exec);
25. /* 释放该fd, 或者说将其置为unused。*/
26. __put_unused_fd(files, fd);
27. spin_unlock(&files->file_lock);
28. /* 关闭file结构 */
29. retval = filp_close(filp, files); //这里将引用计数
30. /* can't restart close syscall because file table entry was cleared */
31. if (unlikely(retval == -ERESTARTSYS ||
32. retval == -ERESTARTNOINTR ||
33. retval == -ERESTARTNOHAND ||
34. retval == -ERESTART_RESTARTBLOCK))
35. retval = -EINTR;
36. return retval;
37. out_unlock:
38. spin_unlock(&files->file_lock);
39. return -EBADF;
40. }
41. EXPORT_SYMBOL(sys_close);
请注意26行的__put_unused_fd,源码如下所示:
1. static void __put_unused_fd(struct files_struct *files, unsigned int fd)
2. {
3. /* 取得文件描述符表 */
4. struct fdtable *fdt = files_fdtable(files);
5. /* 清除fd在open_fds位图的位 */
6. __FD_CLR(fd, fdt->open_fds);
7. /* 如果fd小于next_fd, 重置next_fd为释放的fd */
8. if (fd < files->next_fd)
9. files->next_fd = fd;
10. }
看到这里,我们来回顾一下之前分析过的alloc_fd函数,就可以总结出完整的Linux文件描述符选择计划:
•Linux选择文件描述符是按从小到大的顺序进行寻找的,文件表中next_fd用于记录下一次开始寻找的起点。当有空闲的描述符时,即可分配。
•当某个文件描述符关闭时,如果其小于next_fd,则next_fd就重置为这个描述符,这样下一次分配就会立刻重用这个文件描述符。
以上的策略,总结成一句话就是“Linux文件描述符策略永远选择最小的可用的文件描述符”。——这也是POSIX标准规定的。
从__put_unused_fd退出后,close会接着调用filp_close,其调用路径为filp_close->fput。在fput中,会对当前文件struct file的引用计数减一并检查其值是否为0。当引用计数为0时,表示该struct file没有被其他人使用,则可以调用__fput执行真正的文件释放操作,然后调用要关闭文件所属文件系统的release函数,从而实现针对不同的文件类型来执行不同的关闭操作。
下一节让我们来看看Linux如何针对不同的文件类型,挂载不同的文件操作函数files_operations。
以下一段来源: http://www.voidcn.com/blog/u014338577/article/p-5769774.html
每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。对于同一个文件系统上打开的常规文件来说,read、write等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的file结构体指向同一个file_operations结构体。如果打开一个字符设备文件,那么它的read、write操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以file结构体应该指向不同的file_operations结构体,其中的各种文件操作函数由该设备的驱动程序实现。
每个file结构体都有一个指向dentry结构体的指针,“dentry”是directory entry(目录项)的缩写。我们传给open、stat等函数的参数的是一个路径,例如/home/akaedu/a,需要根据路径找到文件的inode。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体,只要沿着路径各部分的dentry搜索即可,从根目录/找到home目录,然后找到akaedu目录,然后找到文件a。dentry cache只保存最近访问过的目录项,如果要找的目录项在cache中没有,就要从磁盘读到内存中。
每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。每个inode结构体都有一个指向inode_operations结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。和file_operations不同,inode_operations所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各inode结构体可以指向同一个inode_operations结构体。
inode结构体有一个指向super_block结构体的指针。super_block结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。super_block结构体的s_root成员是一个指向dentry的指针,表示这个文件系统的根目录被mount到哪里,在上图的例子中这个分区被mount到/home目录下。
file、dentry、inode、super_block这 几个结构体组成了VFS的核心概念。对于ext2文件系统来说,在磁盘存储布局上也有inode和超级块的概念,所以很容易和VFS中的概念建立对应关 系。而另外一些文件系统格式来自非UNIX系统(例如Windows的FAT32、NTFS),可能没有inode或超级块这样的概念,但为了能mount到Linux系统,也只好在驱动程序中硬凑一下,在Linux下看FAT32和NTFS分区会发现权限位是错的,所有文件都是rwxrwxrwx,因为它们本来就没有inode和权限位的概念,这是硬凑出来的
----------------------------------------------------以下来看自定义的files_operations,以socket举例,有一个struct file_operations结构体定义了很多函数指针,对应不同的读写关之类的操作,socket的读写关闭等操作分别对应不同的内核函数
1. static const struct file_operations socket_file_ops = {
2. .owner = THIS_MODULE,
3. .llseek = no_llseek,
4. .aio_read = sock_aio_read,
5. .aio_write = sock_aio_write,
6. .poll = sock_poll,
7. .unlocked_ioctl = sock_ioctl,
8. #ifdef CONFIG_COMPAT
9. .compat_ioctl = compat_sock_ioctl,
10. #endif
11. .mmap = sock_mmap,
12. .open = sock_no_open, /* special open code to disallow open via /proc */
13. .release = sock_close,
14. .fasync = sock_fasync,
15. .sendpage = sock_sendpage,
16. .splice_write = generic_splice_sendpage,
17. .splice_read = sock_splice_read,
18. };
在socket中,底层的函数sock_alloc_file用于申请socket文件描述符及文件管理结构file结构。它调用alloc_file来申请管理结构file,并将socket_file_ops这个结构体作为参数,如下所示:
1. file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
2. &socket_file_ops);
1. struct file *alloc_file(struct path *path, fmode_t mode,
2. const struct file_operations *fop)
3. {
4. struct file *file;
5. /* 申请一个file */
6. file = get_empty_filp();
7. if (!file)
8. return NULL;
9. file->f_path = *path;
10. file->f_mapping = path->dentry->d_inode->i_mapping;
11. file->f_mode = mode;
12. /* 将自定义的文件操作函数指针结构体赋给file->f_op */
13. file->f_op = fop;
14. ……
15. }
在初始化file结构的时候,socket文件系统将其自定义的文件操作赋给了file->f_op,从而实现了在VFS中可以调用socket文件系统自定义的操作。
----------------------------------遗忘close造成的后果---------------------------
文件描述符没有被释放。
用于文件管理的某些内存结构也没有被释放
对于普通进程来说,即使应用忘记了关闭文件,当进程退出时,Linux内核也会自动关闭文件,释放内存(详细过程见后文)。但是对于一个常驻进程来说,问题就变得严重了。
先看第一种情况,如果文件描述符没有被释放,那么再次申请新的描述符时,就不得不扩展当前的文件描述符表,如果文件描述发表始终不释放,个数迟早会达到上限,返回EMFILE错误
-----------------------如何查看文件资源泄露--------------
使用lsof工具
---------------------------------读取文件
Linux中读取文件操作时,最常用的就是read函数,其原型如下
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被信号中断所造成的。这两种情况基本上都可以视为正常情况。
1. SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
2. {
3. struct file *file;
4. ssize_t ret = -EBADF;
5. int fput_needed;
6. /* 通过文件描述符fd得到管理结构file */
7. file = fget_light(fd, &fput_needed);
8. if (file) {
9. /* 得到文件的当前偏移量 */
10. loff_t pos = file_pos_read(file);
11. /* 利用vfs进行真正的read */
12. ret = vfs_read(file, buf, count, &pos);
13. /* 更新文件偏移量 */
14. file_pos_write(file, pos);
15. /* 归还管理结构file, 如有必要, 就进行引用计数操作*/
16. fput_light(file, fput_needed);
17. }
18. return ret;
19. }
查看VFS_read代码:
1. ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
2. {
3. ssize_t ret;
4. /* 检查文件是否为读取打开 */
5. if (!(file->f_mode & FMODE_READ))
6. return -EBADF;
7. /* 检查文件是否支持读取操作 */
8. if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
9. return -EINVAL;
10. /* 检查用户传递的参数buf的地址是否可写 */
11. if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
12. return -EFAULT;
13. /* 检查要读取的文件范围实际可读取的字节数 */
14. ret = rw_verify_area(READ, file, pos, count);
15. if (ret >= 0) {
16. /* 根据上面的结构, 调整要读取的字节数 */
17. count = ret;
18. /*
19. 如果定义read操作, 则执行定义的read操作
20. 如果没有定义read操作, 则调用do_sync_read—其利用异步aio_read来完成同步的read操作。
21. */
22. if (file->f_op->read)
23. ret = file->f_op->read(file, buf, count, pos);
24. else
25. ret = do_sync_read(file, buf, count, pos);
26. if (ret > 0) {
27. /* 读取了一定的字节数, 进行通知操作 */
28. fsnotify_access(file);
29. /* 增加进程读取字节的统计计数 */
30. add_rchar(current, ret);
31. }
32. /* 增加进程系统调用的统计计数 */
33. inc_syscr(current);
34. }
35. return ret;
36. }
上面的代码为read公共部分的源码分析,具体的读取动作是由实际的文件系统决定的。
1.6.2 部分读取
前文中介绍read可以返回比指定count少的字节数,那么什么时候会发生这种情况呢?最直接的想法是在fd中没有指定count大小的数据时。但这种情况下,系统是不是也可以阻塞到满足count个字节的数据呢?那么内核到底采取的是哪种策略呢?
让我们来看看socket文件系统中UDP协议的read实现:socket文件系统只定义了aio_read操作,没有定义普通的read函数。根据前文,在这种情况下
do_sync_read会利用aio_read实现同步读操作。
其调用链为sock_aio_read->do_sock_read->__sock_recvmsg->__sock_recvmsg_nose->udp_recvmsg,代码如下所示:
1. int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
2. size_t len, int noblock, int flags, int *addr_len)
3. ……
4. ulen = skb->len - sizeof(struct udphdr);
5. copied = len;
6. if (copied > ulen)
7. copied = ulen;
8. ……
当UDP报文的数据长度小于参数len时,就会只复制真正的数据长度,那么对于read操作来说,返回的读取字节数自然就小于参数count了。
看到这里,是否已经得到本小节开头部分问题的答案了呢?当fd中的数据不够count大小时,read会返回当前可以读取的字节数?很可惜,答案是否定的。这种行为完全由具体实现来决定。即使同为socket文件系统,TCP套接字的读取操作也会与UDP不同。当TCP的fd的数据不足时,read操作极可能会阻塞,而不是直接返回。注:TCP是否阻塞,取决于当前缓存区可用数据多少,要读取的字节数,以及套接字设置的接收低水位大小。
因此在调用read的时候,只能根据read接口的说明,小心处理所有的情况,而不能主观臆测内核的实现。比如本文中的部分读取情况,阻塞和直接返回两种策略同时存在。
------------------------------------write跟read的实现差不多,这里就不列出来了,主要讨论多个文件同时写-------------
前面说过,文件的读写操作都是从当前文件的偏移处开始的。这个文件偏移量保存在文件表中,而每个进程都有一个文件表。那么当多个进程同时写一个文件时,即使对write进行了锁保护,在进行串行写操作时,文件依然不可避免地会被写乱。根本原因就在于文件偏移量是进程级别的。
当使用O_APPEND以追加的形式来打开文件时,每次写操作都会先定位到文件末尾,然后再执行写操作。
Linux下大多数文件系统都是调用generic_file_aio_write来实现写操作的。在generic_file_aio_write中,有如下代码:
1. mutex_lock(&inode->i_mutex);//加锁
2. blk_start_plug(&plug);
3. ret = __generic_file_aio_write(iocb, iov, nr_segs, &iocb->ki_pos);//发现文件是追加打开,直接从inode读取最新文件大小作为偏移量
4. mutex_unlock(&inode->i_mutex); //解锁
这里有一个关键的语句,就是使用mutex_lock对该文件对应的inode进行保护,然后调用__generic_file_aio_write->generic_write_check。其部分代码如下:
1. if (file->f_flags & O_APPEND)
2. *pos = i_size_read(inode);
上面的代码中,如果发现文件是以追加方式打开的,则将从inode中读取到的最新文件大小作为偏移量,然后通过__generic_file_aio_write再进行写操作,这样就能保证写操作是在文件末尾追加的。
----------------------------------文件描述符的复制----------------------------
1. int dup(int oldfd);
2. int dup2(int oldfd, int newfd);
•dup会使用一个最小的未用文件描述符作为复制后的文件描述符。
•dup2是使用用户指定的文件描述符newfd来复制oldfd的。如果newfd已经是打开的文件描述符,Linux会先关闭newfd,然后再复制oldfd。
dup的实现
1. SYSCALL_DEFINE1(dup, unsigned int, fildes)
2. {
3. int ret = -EBADF;
4. /* 必须先得到文件管理结构file, 同时也是对描述符fildes的检查 */
5. struct file *file = fget_raw(fildes);
6. if (file) {
7. /* 得到一个未使用的文件描述符 */
8. ret = get_unused_fd();
9. if (ret >= 0) {
10. /* 将文件描述符与file指针关联起来 */
11. fd_install(ret, file);
12. }
13. else
14. fput(file);
15. }
16. return ret;
17. }
在dup中调用get_unused_fd,只是得到一个未用的文件描述符,那么如何实现在dup接口中使用最小的未用文件描述符呢?这就需要回顾1.4.2节中总结过的Linux文件描述符的选择策略了。
Linux总是尝试给用户最小的未用文件描述符,所以get_unused_fd得到的文件描述符始终是最小的可用文件描述符。
查看dup代码实现的第11行
1. void fd_install(unsigned int fd, struct file *file)
2. {
3. struct files_struct *files = current->files;
4. struct fdtable *fdt;
5. /* 对文件表进行保护 */
6. spin_lock(&files->file_lock);
7. /* 得到文件表 */
8. fdt = files_fdtable(files);
9. BUG_ON(fdt->fd[fd] != NULL);
10. /* 让文件表中fd对应的指针等于该文件关联结构file */
11. rcu_assign_pointer(fdt->fd[fd], file);
12. spin_unlock(&files->file_lock);
13. }
在fd_install中,fd与file的关联是利用fd来作为指针数组的索引的,从而让对应的指针指向file。对于dup来说,这意味着数组中两个指针都指向了同一个file。而file是进程中真正的管理文件的结构,文件偏移等信息都是保存在file中的。这就意味着,当使用oldfd进行读写操作时,无论是oldfd还是newfd的文件偏移都会发生变化。
---------------------看一下dup2的实现-------------------
1. SYSCALL_DEFINE2(dup2, unsigned int, oldfd, unsigned int, newfd)
2. {
3. /* 如果oldfd与newfd相等, 这是一种特殊的情况 */
4. if (unlikely(newfd == oldfd)) { /* corner case */
5. struct files_struct *files = current->files;
6. int retval = oldfd;
7. /*
8. 检查oldfd的合法性, 如果是合法的fd, 则直接返回oldfd的值;
9. 如果是不合法的, 则返回EBADF
10. */
11. rcu_read_lock();
12. if (!fcheck_files(files, oldfd))
13. retval = -EBADF;
14. rcu_read_unlock();
15. return retval;
16. }
17. /* 如果oldfd与newfd不同, 则利用sys_dup3来实现dup2 */
18. return sys_dup3(oldfd, newfd, 0);
19. }
20.
21.
------------------------------------文件的元数据获取--------------
什么是文件的元数据呢?其包括文件的访问权限、上次访问的时间戳、所有者、所有组、文件大小等信息。
1. int stat(const char *path, struct stat *buf);
2. int fstat(int fd, struct stat *buf);
3. int lstat(const char *path, struct stat *buf);
这三个函数都可用于得到文件的基本信息,区别在于stat得到路径path所指定的文件基本信息,fstat得到文件描述符fd指定文件的基本信息,而lstat与stat则基本相同,只有当path是一个链接文件时,lstat得到的是链接文件自己本身的基本信息而不是其指向文件的信息。
所得到的文件基本信息的结果struct stat的结构如下:
1. struct stat {
2. dev_t st_dev; /* ID of device containing file */
3. ino_t st_ino; /* inode number */
4. mode_t st_mode; /* protection */
5. nlink_t st_nlink; /* number of hard links */
6. uid_t st_uid; /* user ID of owner */
7. gid_t st_gid; /* group ID of owner */
8. dev_t st_rdev; /* device ID (if special file) */
9. off_t st_size; /* total size, in bytes */
10. blksize_t st_blksize; /* blocksize for file system I/O */
11. blkcnt_t st_blocks; /* number of 512B blocks allocated */
12. time_t st_atime; /* time of last access */
13. time_t st_atime; /* time of last access */
14. time_t st_mtime; /* time of last modification */
15. time_t st_ctime; /* time of last status change */
16. };
17.
st_mode要注意一点的是:st_mode,其注释不仅仅是protection,同时也表示文件类型,比如是普通文件还是目录
stat代码实现:
1. SYSCALL_DEFINE2(stat, const char __user *, filename,struct __old_kernel_stat __user *, statbuf){
2. struct kstat stat;
3. int error;
4. /* vfs_stat用于读取文件元数据至stat */
5. error = vfs_stat(filename, &stat);
6. if (error)
7. return error;
8. /* 这里仅是从内核的元数据结构stat复制到用户层的数据结构statbuf中 */
9. return cp_old_stat(&stat, statbuf);
10. }
第5行,vfs_stat是关键。进入vfs_stat->vfs_fstatat->vfs_getattr,代码如下:
1. int vfs_getattr(struct vfsmount *mnt, struct dentry *dentry, struct kstat *stat)
2. {
3. struct inode *inode = dentry->d_inode;
4. int retval;
5. /* 对获取inode属性操作进行安全性检查 */
6. retval = security_inode_getattr(mnt, dentry);
7. if (retval)
8. return retval;
9. /* 如果该文件系统定义了这个inode的自定义操作函数, 就执行它 */
10. if (inode->i_op->getattr)
11. return inode->i_op->getattr(mnt, dentry, stat);
12. /* 如果文件系统没有定义inode的操作函数, 则执行通用的函数 */
13. generic_fillattr(inode, stat);
14. return 0;
15. }
不失一般性,也可以通过查看第13行的generic_fillattr来进一步了解,代码如下:
1. void generic_fillattr(struct inode *inode, struct kstat *stat)
2. {
3. stat->dev = inode->i_sb->s_dev;
4. stat->ino = inode->i_ino;
5. stat->mode = inode->i_mode;
6. stat->nlink = inode->i_nlink;
7. stat->uid = inode->i_uid;
8. stat->gid = inode->i_gid;
9. stat->rdev = inode->i_rdev;
10. stat->size = i_size_read(inode);
11. stat->atime = inode->i_atime;
12. stat->mtime = inode->i_mtime;
13. stat->ctime = inode->i_ctime;
14. stat->blksize = (1 << inode->i_blkbits);
15. stat->blocks = inode->i_blocks;
16. }
从这里可以看出,所有的文件元数据均保存在inode中,而inode是Linux也是所有类Unix文件系统中的一个概念。这样的文件系统一般将存储区域分为两类,一类是保存文件对象的元信息数据,即inode表;另一类是真正保存文件数据内容的块,所有inode完全由文件系统来维护。但是Linux也可以挂载非类Unix的文件系统,这些文件系统本身没有inode的概念,怎么办?Linux为了让VFS有统一的处理流程和方法,就必须要求那些没有inode概念的文件系统,根据自己系统的特点——如何维护文件元数据,生成“虚拟的”inode以供Linux内核使用。