前言
本文中涉及到的Linux源碼來源於linux 2.6.39.4。
就像windows中的句柄一樣,學習linux經常會碰到文件描述符,我們都知道文件描述符是一個非負整數,每一個文件描述符都唯一對應了一個打開的文件,那么文件描述符在內核中到底是以什么形式存在的呢?
這個問題是我在讀《UNIX環境高級編程》的時候感到疑惑的,在該書的3.10章節中,關於文件共享有以下一段描述:
從這段文字中,可以得出一個結論:在內核中每一個打開的文件都需要由3種數據結構來進行維護。
根據文中內容,這三種數據結構分別為:
1.每個進程對應一張打開文件描述符表,這是進程級數據結構,也就是每一個進程都各自有這樣一個數據結構;
2.內核維持一張打開文件表,文件表由多個文件表項組成,這是系統級數據結構,也就是說這樣的數據結構是針對於整個內核而言的,每個進程都可共享的;
3.每個打開的文件對應一個i節點(i-node)數據結構(Linux下只有i節點沒有v節點),由於這是每一個打開的文件與之對應的,因此這也是一個系統級數據結構,存在於內核中,非進程所獨有。
那么,這三種數據結構到底是什么呢?
打開文件描述符表
首先來看第一個數據結構—— 打開文件描述符表。
在Linux中,對於每一個進程,都會分配一個PCB數據結構,它其中包含了該進程的所有信息,而在代碼實現上,這個數據結構名為task_struct,在linux源碼的include/linux/sched.h中可以找到其定義,它是一個很龐大的結構體,部分定義如下:
struct task_struct {
......
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
......
};
在task_struct中,有一個成員變量名為files,注釋中提到這是一個描述打開文件信息的變量,其類型為struct files_struct *,那么現在再接着看files_struct,在linux源碼的include/linux/fdtable.h中可以找到其定義,定義如下:
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
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 中的成員struct file * fd_array[NR_OPEN_DEFAULT]它是一個指針數組,數組每一個元素都是一個指向file類型的指針,可想而知,這些指針都會指向一個打開的文件,並且file這一數據結構就是用來描述一個打開的文件的,而我們所說的文件描述符,實際上就是這個指針數組的索引。這也是為什么文件描述符是非負整數。
再來看第二種數據結構——文件表項。
文件表項
前面說了,每一個打開文件實際上就是用一個file結構體進行描述的,在linux源碼的include/linux/fs.h中可以找到其定義,定義如下:
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
}f_u;
struct path f_path;
//文件路徑,包括目錄項以及i-node
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
spinlock_t f_lock;
/* f_ep_links, f_flags, no IRQ */
#ifdef CONFIG_SMP
int f_sb_list_cpu;
#endif
atomic_long_t f_count;
//文件打開次數
unsigned int f_flags;
//文件打開時的flag,對應於open函數的flag參數
fmode_t f_mode;
//文件打開時的mode,對應於open函數的mode參數
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;
#endif
/* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};
在file結構體中,不得不再說一下它的f_path成員,這是一個struct path類型的變量,該類型定義於include/linux/path.h:
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
這里的dentry實際上就指向文件所在的目錄項了,struct dentry的類型定義於include/linux/dcache.h:
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags;
/* protected by d_lock */
seqcount_t d_seq;
/* per dentry seqlock */
struct hlist_bl_node d_hash;
/* lookup hash list */
struct dentry *d_parent;
/* parent directory */
struct qstr d_name;
struct inode *d_inode;
/* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN];
/* small names */
/* Ref lookup also touches following */
unsigned int d_count;
/* protected by d_lock */
spinlock_t d_lock;
/* per dentry lock */
const struct dentry_operations *d_op;
struct super_block *d_sb;
/* The root of the dentry tree */
unsigned long d_time;
/* used by d_revalidate */
void *d_fsdata;
/* fs-specific data */
struct list_head d_lru;
/* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child;
/* child of parent list */
struct rcu_head d_rcu;
}d_u;
struct list_head d_subdirs;
/* our children */
struct list_head d_alias;
/* inode alias list */
};
在dentry結構體中,描述了根結點、父節點等等信息,尤其還要注意的是struct inode *d_inode這一變量,它則是指向了一個i-node結點。
再回到file結構體中,有一個struct file_operations *f_op變量,其類型定義在include/linux/fs.h中:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
};
可見,在該成員中包含了所有文件操作相關的函數指針。
每一個打開的文件都對應於一個file結構體,在該結構體中,f_flags描述了文件標志,f_pos描述了文件的偏移位置,而在f_path中有含有一個指向一個inode結點的指針,這也符合了《UNIX環境高級編程》中的以下描述:
因此可以知道,文件表項的數據結構就是file結構體,而在實際上內核中也並不存在這樣一張文件表,只是每個打開的文件都對應一個file結構體,也就是一個文件表項,打開文件描述符表****struct file * fd_array[NR_OPEN_DEFAULT]數組中的每一項都會指向這樣一個文件表項,如下圖所示:
inode節點
第三種數據結構就是inode節點,在include/linux/fs.h中找到其定義如下:
struct inode {
/* RCU path lookup touches following: */
umode_t i_mode;
//權限
uid_t i_uid;
//用戶id
gid_t i_gid;
//組id
const struct inode_operations *i_op;
struct super_block *i_sb;
spinlock_t i_lock;
/* i_blocks, i_bytes, maybe i_size */
unsigned int i_flags;
struct mutex i_mutex;
unsigned long i_state;
unsigned long dirtied_when;
/* jiffies of first dirtying */
struct hlist_node i_hash;
struct list_head i_wb_list;
/* backing dev IO list */
struct list_head i_lru;
/* inode LRU list */
struct list_head i_sb_list;
union {
struct list_head i_dentry;
struct rcu_head i_rcu;
};
unsigned long i_ino;
//inode節點號
atomic_t i_count;
unsigned int i_nlink;
dev_t i_rdev;
unsigned int i_blkbits;
u64 i_version;
loff_t i_size;
//文件大小
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespec i_atime;
//最后一次訪問(access)的時間
struct timespec i_mtime;
//最后一次修改(modify)的時間
struct timespec i_ctime;
//最后一次改變(change)的時間
blkcnt_t i_blocks;
//塊數
unsigned short i_bytes;
struct rw_semaphore i_alloc_sem;
const struct file_operations *i_fop;
/* former ->i_op->default_file_ops */
struct file_lock *i_flock;
struct address_space *i_mapping;
//塊地址映射
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_head i_devices;
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
__u32 i_fsnotify_mask;
/* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
#ifdef CONFIG_IMA
atomic_t i_readcount;
/* struct files open RO */
#endif
atomic_t i_writecount;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
void *i_private;
/* fs or device private pointer */
};
代碼中只注釋了一部分,通過inode結構,可以知道文件數據塊的在磁盤上的位置以及文件大小等信息,這樣才能使得進程能夠通過file結構體來找到磁盤上相應文件的位置來進行文件讀寫。
另外補充一點,關於inode結構體中的struct inode_operations *i_op成員,其數據結構定義在include/linux/fs.h中:
struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
void * (*follow_link) (struct dentry *, struct nameidata *);
int (*permission) (struct inode *, int, unsigned int);
int (*check_acl)(struct inode *, int, unsigned int);
int (*readlink) (struct dentry *, char __user *,int);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
void (*truncate) (struct inode *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*truncate_range)(struct inode *, loff_t, loff_t);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
u64 len);
}____cacheline_aligned;
可見,在該成員變量所指向的數據結構中,包含了許多函數指針,這些函數指針大多針對於目錄、文件操作。
總結
進程、打開文件描述符表、文件表項和i-node結點關系如圖所示。(暫時忽略進程中0、1和2號文件描述符分別默認為標准輸入、標准輸出和標准錯誤的情況)
通過以上分析,我們可以得出以下結論:
1.每啟動一個進程都會為其分配一個task_struct結構體,在task_struct結構體中含有一個file_struct結構體指針,其所指向的file_struct結構體中,含有一個file*的指針數組fd_array,它就是打開文件描述符表,其中每一個元素都指向一個文件表項,這個數組的索引就是文件描述符。此外,file_struct結構體中的next_fd保存的是下一個分配的文件描述符,它會在調用open和close改變,最終使得每次open返回的都是當前可用的最小文件描述符;
2.每次調用open或者create(內部實際上還是調用的open),都會對新打開的文件分配一個file結構體,並且將打開文件的標志、狀態、權限等信息填入這個file結構體中。這個file結構體也叫文件表項;
3.磁盤中的每個文件都對應一個i-node,每一個文件表項都會指向一個文件的i-node,但是同一文件的i-node可以對應多個文件表項(當多次調用open打開同一個文件時就會出現這種情況,不管是同一進程多次打開同一文件(如圖中A進程的0號和2號文件描述符對應兩個文件表項,但是最終指向同一i-node即同一文件),還是不同進程多次打開同一文件(如圖中A進程3號文件描述符和B進程的3號文件描述符));
4.同一進程下的不同文件描述符是可以指向同一文件表項,即最終指向同一文件(如圖中A進程的0號文件描述符和1號文件描述符,使用dup函數即可實現)。
5.子進程在創建時會拷貝父進程的打開文件描述符表,因此父子進程是共享文件表項的,如圖所示:
而相互獨立的不同進程的打開文件描述符表是相互獨立的,因此相互獨立的多個進程之間的文件描述符可以相同,但是不同進程的文件描述符是不能指向同一文件表項的(除非這個文件描述符是從同一個祖先進程中繼承得來的),但是這並不妨礙不同進程訪問同一文件(如第3點結論);
6.指向同一文件表項的不同文件描述符(不同進程相同數值的文件描述符也看做不同)共享文件標志、文件偏移等信息;
7.每一個文件表項對應的file結構體中的f_count會記錄通過該文件表項打開文件的次數,當f_count計數歸0時這個文件表項才會被刪除,因此,對於指向同一文件表項的兩個不同文件描述符(如子進程所繼承的父進程的文件描述符,或同一進程中dup的兩個文件描述符指向同一個文件表項),即使其中一個文件描述符關閉了,只要仍然有文件描述符指向這個文件表項,那么就依然能通過這個文件表項訪問文件,直到所有指向該文件表項的文件描述符都關閉了才不能再進行訪問;
最后再加一張圖加以理解: