1.VFS
Linux支持各种各样的文件系统格式,比如说ext2,ext3,reiserfs,FAT,NTFS,iso9660等,
不同的磁盘分区,光盘或者其他存储设备都有不同的文件系统格式,然后这些文件系统都可以
mount到某个目录下,使我们看到一个统一的目录树,各种文件上的目录和文件我们用ls命令
看起来都是一样的,读写操作起来也是一样的,这个是怎么做到的呢?Linux内核在各种不同
的文件系统格式之上做了一个抽象层,使得文件,目录,读写访问等概念称为抽象层的概念,因此
各种文件系统看起来都是一样的,这个抽象层称为虚拟文件系统(VFS,Vitual Filesystem).
2. 工作原理
应用层函数open,read,write等函数在操作文件的时候不需要关心文件存储在什么系统上,就是通过
VFS来实现的.
3.file结构体
D:\005-代码\001-开源项目源码\004-内核源码\linux-5.8.13\linux-5.8.13\include\Linux\fs.h
line 954
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
每个文件描述符都会对应一个file结构体,
file结构体中的重要成员变量:
读写指针位置(描述符会指向它)
flags(状态标识,O_CREATE,O_WRITE,阻塞非阻塞等)
f_op(是一个指针,会指向一组函数,比如说open,close,read,write等,这里的open,close,read,write等函数是驱动层的函数,
不是应用层的,这里的open,close,read,write知道如何去操作硬件)
f_dentry磁盘文件,里面会有Inode_operations这些操作Inode的文件系统驱动,如chmod,link,unlink
等文件系统驱动.
不管是文件描述符还是file结构体还是驱动层的file_operations结构体和文件系统的inode_operation,
这些都是存在3G到4G的内核空间中的,用户是看不见的.
文件引用计数.
问:文件的引用计数是在哪个结构体里面的?
是struct file还是struct inode
由这些一个一个结构体进行关联,最终形成了虚拟文件系统.
虚拟文件系统不是一个具体的东西,它是一张网,它将最上层的最上层应用层的open函数和最下层的硬件
联系到一起.
比如我们在代码里面写了write(3,"hello",5);当我们调用这个函数的时候,操作系统通过3这个文件描述符
找到这个file结构体,找到这个file机构体以后,file结构体里面记录了对3这个文件描述符的所有驱动层的
操作函数,由于我们调用的是write,那么通过f_op索引到驱动里面的write,驱动里面的write知道如何操作
硬件,不管3是一个磁盘文件还是一个终端文件还是一个串口文件或者网络文件,驱动write都知道怎么去处理.
考虑以下场景:
1.进程A中有两个文件描述符都指向文件file,第一个文件描述符写了“hello”,第二个文件描述符写了“world”,那么这时的文件的内容应该是“helloworld”;
2.进程A和进程B中分别有两个文件指向了文件file,进程A和进程B同时用open打开了一个文件,A写“hello”,B后写“world”,
A和B都维护了各自的结构体,后写的那个覆盖了前面写的那个,最后file文件里面是“world”.
3.进程A开始打开了一个file文件返回描述符3,在文件描述符3里写了“hello”,然后调用open重新打开
文件file,此时返回文件描述符4,描述符4指向一个新的文件结构体,在文件描述符里写了“world”,
会将文件描述符3在file文件中写的内容覆盖掉,最后文件里的内容是“world”.
4.Inode结构体
D:\005-代码\001-开源项目源码\004-内核源码\linux-5.8.13\linux-5.8.13\include\Linux\fs.h
line 646
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;
unsigned int __i_nlink;
};
dev_t i_rdev;
loff_t i_size;
struct timespec64 i_atime;
struct timespec64 i_mtime;
struct timespec64 i_ctime;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;
u8 i_blkbits;
u8 i_write_hint;
blkcnt_t i_blocks;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
/* Misc */
unsigned long i_state;
struct rw_semaphore i_rwsem;
unsigned long dirtied_when; /* jiffies of first dirtying */
unsigned long dirtied_time_when;
struct hlist_node i_hash;
struct list_head i_io_list; /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct bdi_writeback *i_wb; /* the associated cgroup wb */
/* foreign inode detection, see wbc_detach_inode() */
int i_wb_frn_winner;
u16 i_wb_frn_avg_time;
u16 i_wb_frn_history;
#endif
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;
struct list_head i_wb_list; /* backing dev writeback list */
union {
struct hlist_head i_dentry;
struct rcu_head i_rcu;
};
atomic64_t i_version;
atomic64_t i_sequence; /* see futex */
atomic_t i_count;
atomic_t i_dio_count;
atomic_t i_writecount;
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
atomic_t i_readcount; /* struct files open RO */
#endif
union {
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};
struct file_lock_context *i_flctx;
struct address_space i_data;
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct fsnotify_mark_connector __rcu *i_fsnotify_marks;
#endif
#ifdef CONFIG_FS_ENCRYPTION
struct fscrypt_info *i_crypt_info;
#endif
#ifdef CONFIG_FS_VERITY
struct fsverity_info *i_verity_info;
#endif
void *i_private; /* fs or device private pointer */
} __randomize_layout;
5.dup和dup2
下面两个函数都可用来复制一个现有的文件描述符。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
两函数的返回值:若成功,返回新的文件描述符;若出错,返回−1
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数
值。对于 dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,
则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,
fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec
时是打开状态。--摘自《APUE》
dup和dup2都可以用来复制一个现存的文件描述符,是两个文件描述符指
向同一个file结构体,如果两个文件描述符指向同一个file结构体,
File Status Flag和读写位置只保存一份在file结构体中,并且file
结构体的引用计数是2.如果两次open同一个文件得到两个文件描述符,则
每个描述符对应一个不同的file机构体,可以用不同的File Status Flag
和读写位置.注意区分两种情况.
如何才能新创建一个file结构体对象呢?
只有open一个文件的时候才能创建一个file结构体对象.
dup和dup2不创建新的file结构体对象.
dup --传入已有的文件描述符,返回新生成的文件描述符,新的文件描述符是文件描述符表中未使用的最小的文件描述符
dup2(dup two)--可以指定新的描述符为fd2,如果newfd是1(在一个程序中假设1就是指向标准输出),第一步会先关闭1,
然后重新打开1将其指向oldfd所指向的那个文件结构体对象.
dup的应用.
比如要给10000个随机数排序,将10000个随机数放在文件中,然后采用输入重定向,读取文件里面的数据进行排序.
如:
./a.out < file
6.不关闭打开的文件和内存泄漏
在一个进程中,如果文件描述符3和文件描述符都指向文件file,在程序结束之前close(3);但是
没有close(4).这时会产生内存泄漏,而且是内核中的内存泄漏.
程序运行时,在进程中文件描述符3和文件描述符4指向同一个File结构体对象,这个结构体对象的引用计数是2,当close(3)的时候,
会将file结构体对象的引用计数减去1,但是只有当file结构体对象的引用计数减成0的时候,内核才会真正的释放这个file结构体对象的空间.
想一想,打开1个文件会在内核中就会有1个file结构体对象,当同时打开10000个文件的时候,内核中就有10000个file结构体对象,会占用内
核中的内存,如果您用完文件没有及时关闭,这个也是一种内存泄漏.
细思极恐:
如果一个项目中有20个程序员,10个程序员写了打开文件的程序之后不知道close,或者引用计数大于1的时候没有对所有描述符进行close,
长此以往,内存不给泄漏完了吗??