Linux中的文件
文件:
Linux内核将一切视为文件,那么Linux中文件是什么呢?其既可以是事实上的真正的物理文件,也可以是设备,管道,甚至可以是一块内存,狭义的文件是指文件系统中的物理文件,广义上的文件可以是Linux管理的所有对象。这些广义的文件利用VFS机制,以文件系统的形式挂载在Linux内核中,对外提供一系列的文件操作接口。
文件描述符:
对于linux而言,所有对设备和文件的操作都使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
通常,一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这3个文件分别对应文件描述符为0、1 和 2
从数值上看,文件描述符是一个非负整数,其实质就是 一个句柄,所以可以认为文件描述符就是一个文件句柄,那何为句柄呢?一切对于用户透明的返回值,即可视为句柄。用户空间利用文件描述符与内核进行交互,而内核拿到文件 描述符之后,可以通过它得到用于管理文件的真正的数据结构。
使用文件描述符的好处:
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为普通变量,一般情况下,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是内核用于文件管理的结构,这里使用了默认大小的数组,就是为了涵盖大多数情况,避免动态分配
};
在初始状态下,files_struct 与 fdtable、files的关系为:
文件表,文件描述符表以及文件结构的关系图如上所示
我们通常这样描述,”打开一个文件“,那么这个所谓的打开究竟打开了什么呢?内核在这个过程中,又做了什么事情?
我们来跟踪内核open源码open–>do_sys_open 来剖析
long do_sys_open(int dfd,const char __user *filename,int flags,int mode)
{
struct open_flags op; //flags为用户层传递的参数,内核会对flags进行合法性检查,并根据mode生成新的flags值赋给lookup
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)
{
//申请新的文件管理结构file
struct file* f= do_filp_open(dfd,tmp,&op,lookup);
if(IS_ERR(f))
{
put_unused_fd(fd);
fd = PTR_ERR(f);
}
else
{
fsnotity_open(f);// 产生文件打开的通知事件
fd_install(fd,f);//将文件描述符fd与文件管理结构file对应起来,即安装
}
}
putname(tmp);
}
return fd;
}
//从do_sys_open 可以看出,打开文件时,内核主要消耗了两种资源,文件描述符与内核管理文件结构file
文件描述符的选择:
根据POSIX标准,当获取一个新的文件描述符时,要返回最低的未使用的文件描述符。Linux是如何实现这一标准的呢?在Linux中,通过do_sys_open->get_unused_fd_flags->alloc_fd(0, (flags))来选择文件描述符,代码如下:
int alloc_fd(unsigned start, unsigned flags)
{
struct files_struct *files = current->files;
unsigned int fd;
int error;
struct fdtable *fdt;
/* files为进程的文件表,下面需要更改文件表,所以需要先锁文件表 */
spin_lock(&files->file_lock);
repeat:
/* 得到文件描述符表 */
fdt = files_fdtable(files);
/* 从start开始,查找未用的文件描述符。在打开文件时,start为0 */
fd = start;
/* files->next_fd为上一次成功找到的fd的下一个描述符。使用next_fd,可以快速找到未用的文
件描述符;*/
if (fd < files->next_fd)
fd = files->next_fd;
/*
当小于当前文件表支持的最大文件描述符个数时,利用位图找到未用的文件描述符。
如果大于max_fds怎么办呢?如果大于当前支持的最大文件描述符,那它肯定是未
用的,就不需要用位图来确认了。
*/
if (fd < fdt->max_fds)
fd = find_next_zero_bit(fdt->open_fds->fds_bits,
fdt->max_fds, fd);
/* expand_files用于在必要时扩展文件表。何时是必要的时候呢?比如当前文件描述符已经超过了当
前文件表支持的最大值的时候。 */
error = expand_files(files, fd);
if (error < 0)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;
/* 只有在start小于next_fd时,才需要更新next_fd,以尽量保证文件描述符的连续性。*/
if (start <= files->next_fd)
files->next_fd = fd + 1;
/* 将打开文件位图open_fds对应fd的位置置位 */
FD_SET(fd, fdt->open_fds);
/* 根据flags是否设置了O_CLOEXEC,设置或清除fdt->close_on_exec */
if (flags & O_CLOEXEC)
FD_SET(fd, fdt->close_on_exec);
else
FD_CLR(fd, fdt->close_on_exec);
error = fd;
#if 1
/* Sanity check */
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与文件管理结构file
内核使用fd_install 将文件管理结构file与fd组合起来,具体操作如下:
void fd_install(unsignet 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);
//将文件描述符表中的file类型的指针数组中 对应fd的项 指向 file,这样文件描述符fd就与file建立了对应关系
rcu_assign_pointer(fdt->fd[fd],file);
spin_unlock(&files->file_lock);
}
// 得到:当用户使用fd与内核交时,内核可以用fd从 fdt->fd[fd]中得到内部管理文件的结构struct file
当新建一个程序,因为进程启动时,打开了标准输入、标准输出和标准出错处理三个文件,因此返回的文件描述符fd 为3,程序测试:
c语言中,文件类型指针:
在缓冲文件系统中,关键概念是“”文件指针“”,每个被使用的文件都在内存中开辟一个区,用来存放文件的信息(如文件的名字,文件的状态,文件当前的位置等),这些信息是保存在一个结构体变量中,该结构体类型是由系统定义的,为 FILE ,文件类型声明为
typedef struct
{
short level; //缓冲区“满”或“空”的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char* buffer //数据缓冲区位置
unsigned char* curp //指针,当前的指向
unsigned istemp //临时文件,指示器
short token //用于有效性检测
}FILE;
FILE f[5]
//定义了一个结构体数组,他有5个元素,可以用来存放5个文件信息
FILE * fp
//fp是一个指向FILE 类型结构体的指针变量,可以使fp指向某一个文件的结构体变量,从而通过该结构体变量中的文件信息能够访问该文件,也就是说,通过文件指针能找到与他相关的文件,如果有n个文件,一般对应n个指针变量,使他们分别指向n个文件,以实现对其访问。