Linux当中的文件
Linux内核将一切视为文件,那么Linux中文件是什么呢?
其既可以是事实上的真正的物理文件,也可以是设备,管道,甚至可以是一块内存,
狭义的文件是指文件系统中的物理文件,广义上的文件可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一系列的文件操作接口。
文件描述符
对于linux而言,所有对设备和文件的操作都使用文件描述符来进行的。
文件描述符是一个非负的整数,它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
通常,一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这3个文件分别对应文件描述符为0、1 和 2。
从数值上看,文件描述符是一个非负整数,其实质就是 一个句柄,所以可以认为文件描述符就是一个文件句柄,那何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互,而内核拿到文件 描述符之后,可以通过它得到用于管理文件的真正的数据结构。
POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。
使用文件描述符的好处:
1、为了增加安全性, 句柄类型对用户完全透明,用户无法通过任何hacking的方式,更改句柄对应内部结果,Linux内核的文件描述符,只有内核才能通过该值得到对应的文件结构;
2、增加了可扩展性,用户的代码只依赖于句柄的值,这样实际结构的类型就可以随时改变,与句柄的映射关系也可以随时改变,这些变化都不会影响任何现有的用户代码。
Linux的每个进程都会维护一个文件表,以便维护该进程打开文件的信息,包括打开的文件个数,每个打开文件的偏移量等信息。
文件表的实现:
内核中进程对应的结构是task_struct,进程的文件表保存在task_struct—>files中,结构代码:
struct files_struct
{
atomic_t count; //引用计数
struct fdtable _rcu *fdt;
struct fdtable fdtab;
/* 为什么会有两个 fdtable? 这是内核的一种优化策略,fdt为指针,fdtab为普通变量,
表足以满足大多数情况,因此这样就避免了频繁的申请空间,在创建时,使用普通的变量或数组,
然后让指针指向他,作为默认情况,当进程使用量超过默认值时,才会动态申请内存 */
int next_fd;
//用于查找下一个空闲的fd
struct embedded_fd_set open_fds_init;
//保存打开文件描述符的位图
struct embedded_fd_set close_on_exec_init;
//保存执行exec需要关闭的文件描述符的位图
struct files_struct __rcu* fd_array[NR_OPEN_DEFAULT];
//fd_array是一个固定大小的file的结构数组,struct file是内核用于文件管理的结构,
//这里使用了默认大小的数组,就是为了涵盖大多数情况,避免动态分配
};
就是说打开一个文件其实是在Linux内核里创建一个内核管理文件结构体file,再创建一个文件描述符与这个结构体关联,外部用户就可以使用这个描述符操作这个文件。文件管理的数据结构和文件描述符都是系统的重要资源。
文件描述符合打开文件之间的关系
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。
1. 进程级的文件描述符表
2. **系统级**的打开文件描述符表
3. 文件系统的i-node表
进程级的描述符表的每一条目记录了单个文件描述符的相关信息。
1. 控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志)
2. 对打开文件句柄的引用
内核对所有打开的文件的文件维护有一个系统级的描述符表格(open file description table)。有时,也称之为打开文件表(open file table),并将表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
1. 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
2. 打开文件时所使用的状态标识(即,open()的flags参数)
3. 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
4. 与信号驱动相关的设置
5. 对该文件i-node对象的引用
6. 文件类型(例如:常规文件、套接字或FIFO)和访问权限
7. 一个指针,指向该文件所持有的锁列表
8. 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
下图展示了文件描述符、打开的文件句柄以及i-node之间的关系,图中,两个进程拥有诸多打开的文件描述符。
在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。
进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系),或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
总结
1.由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件
2. 两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由调用read()、write()或lseek()所致),那么从另一个描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。
3. 要获取和修改打开的文件标志(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。
4. 文件描述符标志(即,close-on-exec)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符