我们知道,每个进程都有一个描述该进程相关属性的数据结构:进程控制块(PCB),Linux中的PCB叫做task_struct,它的部分源码如下:
注意到task_struct中有一个类型为files_struct的结构体指针files,我们跳转到这个结构体类型的定义:
所谓打开文件描述符表,实际上就是files_struct中的成员fd_array[NR_OPEN_DEFAULT]。它是一个指针数组,元素类型为file*,可想而知,这些指针都会指向一个打开的文件,并且file这一数据结构就是用来描述一个打开的文件的。
fd_array的下标有什么意义?
这里的下标编号叫做文件描述符,进程每打开一个文件都会为该文件创建一个file类型的结构体,并把该结构体对象的地址填入到fd_array中,填入下标编号的规则是最小并且未被使用的,对应file_struct结构体中的next_fd保存的就是下一个分配的文件描述符,它会在调用open和close时调整,最终使得每次open返回的都是当前可用的最小文件描述符。同时还规定进程启动的时候,默认会打开三个文件:0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4…
PS:这里的标准输入、标准输出和标准错误对应的是键盘、显示器、显示器,而不是C语言里的stdin、stdout和stderr,这三个是C语言专门定义的FILE类型的对象:
Linux系统配置下每个进程最大打开的文件描述符个数?
为了控制每个进程消耗的文件资源,内核会对单个进程最大打开文件数做默认限制,即用户级限制。32位系统默认值一般是1024,64位系统默认值一般是65535可以使用 ulimit -n 命令查看。
我是XShell登录的远端云服务器,这里进程的最大文件描述符个数默认设置为100001。
当然我们也可以自己去更改进程最大打开文件描述符的个数:
用什么方法查看特定进程的打开文件描述符表?
执行如下程序,程序"mytest"启动后在当前目录下打开一个文件log.txt,然后死循环使进程一直处于运行状态。
//可执行程序名称:mytest
void test()
{
int fd = open("log.txt", O_RDWR|O_CREAT, 0666);
while(1)
{}
}
在打开另一个Shell,输入命令:pidof mytest获取进程mytest的pid号,然后 ll /proc/pid/fd 查看"mytest"进程所使用的文件描述符表。
这里展现出来的"mytest"进程打开文件描述符表中每一个表项都是软连接。
/dev/pts是远程登陆后创建的控制台设备文件所在的目录。因为我是通过Xshell远程登录的,所以标准输入,标准输出,标准错误对应的文件描述符0、1、2指向虚拟终端控制台 /dev/pts/0 。而我们自己打开文件log.txt的绝对路径被被放置在3号文件描述符位置上。
在Linux系统中一切皆可以看成是文件,文件又分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。而文件描述符就是内核为了高效管理这些已经被打开的文件所创建的索引,它是一个非负整数,用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现,这个我们下面会介绍到。
每启动一个进程操作系统都会为其创建一个task_struct结构体,在task_struct结构体中含有一个类型为files_struct的结构体指针,该结构体里又含有一个元素类型为file*的指针数组fd_array,它就是打开文件描述符表,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表的下标,数组的内容就是指向一个个打开的文件的指针。
接着上面的,我们来看看所谓描述打开文件信息的file结构体的声明:
struct file {
// 记录头结点
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;
#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
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_u:
可以推测file结构体之间是通过链表组织起来的,每一个file结构体叫做一个文件表项,它们组合而成的链表叫做打开文件表,这张表是系统级别的,为所有进程共享,但组成该表的每一个文件表项是进程级的。
存放文件操作函数的结构体
file结构体中,有一个struct file_operations *类型的成员f_op,这个成员中存储了一系列文件读写操作相关的函数指针,这些操作函数是系统级的:
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类型的文件表项,并把这个文件表项的地址填入到进程级别的打开文件描述表中,并返回下标标号,即文件描述符。
头文件
#include
#include
#include
函数原型(有两种形式,下面我们主要介绍第二种形式)
函数参数:
返回值:打开成功则返回文件描述符,否则返回-1。
flags参数详解:
上面三种模式在flags参数中不能同时出现但必须有一种出现,下面的参数是可选的。
使用举例:
在当前路径下创建一个名为"log.txt"的文件,打开方式为只写,如果不存在的话就新创建,对应拥有者、所属组、其他人的rwx权限是666。
int test()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
cout<<"fd:"<<fd<<endl;// fd:3
return 0;
}
说明1:什么是当前路径?
我们知道,当open以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是该程序所存储的路径码?
还是上面的那个程序,我们在当前目录cur_direct下执行它,确实在当前目录下生成了一个文件log.txt
退回到上级目录,在上级目录执行程序,发现文件也可以在上级目录生成,可以推测所谓的“打开文件时如果文件不存在,默认在当前目录下这个文件”,这里的当前路径指的是程序执行时所在的路径,而不是这个程序本身所存储的路径。
当该可执行程序运行起来变成进程后,我们可以获取该进程的PID,然后根据PID在根目录下的proc中查看该进程的信息。
其中我们可以看到两个软链接文件cwd和exe,cwd就是程序执行时我们所处的路径,而exe是该可执行程序本身所存储的路径。
说明2:第二个参数flags的具体含义
这个参数对应很多选项,这些选项可以通过或运算组合起来使用,为什么可以这样呢?其实这些参数都是系统定义的宏,它们对应都是只有一个比特位为1的整数,所以理论上可以有32种参数,这样可以通过它们按位或后的结果来区分开到底是那个选项被使用了。
PS:file结构体中的f_flags保存着我们打开文件时传入的第二个参数。
说明3:第三个参数mode的使用场景
第三个参数是在第二个参数中有O_CREAT时才起作用的。若没有O_CREAT,则第三个参数可以忽略(对应open()函数原型的第一种写法,没有第三个参数)。
当创建新文件时,我们可以指定文件的权限mask,如果不指定新建文件默认权限mask=0666即 -rw-rw-rw- ,新建目录默认权限mask=0777,即drwxrwxrwx,不论指定与否最终实际创建的文件的权限=mask & (~umask),这里的umask是权限掩码,普通用户的权限掩码(umask)默认为0002,超级用户的默认为0022。
正确地传入mode参数应该是由八进制的四位数字给出的,如 0666 ,要纠正一个错误,就是之前我认为 权限数字前面的 0 代表的是八进制的含义,其实并不是这样的,前面的 0 代表了权限修饰位,也就是set-user-id位、set-group-id位和sticky这三位的权限,所以最前面的0是一定要写到的,不然会出现错误。
通俗的去理解umask的操作原理就是对比新建文件的默认权限,如果umask对应比特位上是1,那么文件权限中与之对应的那个比特位上的权限就会被去除(如果有的话,没有的话就不用去除)。
为了去除权限掩码的干扰,我们可以通过umask()函数来设置umask的值为0,这样我们直接传入的mode参数就是该文件对应的权限了。
同样的mode值在file结构体中也有保存:
头文件:#include <unistd.h>
函数原型:int close(int fildes);
作用:将进程中打开文件描述符表对应下标的内容(文件表项地址)剥除,有可能还会清除对应的文件表项。
返回值:关闭文件成功返回0,关闭文件失败则返回-1。
问题:close关闭文件时是否要把对应文件的file结构体删除?
前面说过文件表项是系统级别的,所以可能存在多个进程的打开文件描述符表都指向同一个文件表项,比如子进程继承父进程的文件描述符表时就会有这种情况;或者一个进程的打开文件描述符表的多个文件描述符都存有同一个文件表项的地址,这个可以通过dup2重定向来实现,即一个文件表项到底要不要被清除取决于它还要不要被使用。
文件表项中有一个成员f_count用来记录该文件表项被使用的次数,每个进程执行close()把打开文件描述符表对应下标的内容剥除之前,先会找到该文件表项的f_count使其减一,如果f_count减一后变成0了,系统就会删除该文件表项。
read
头文件:#include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
功能:从fd指向的文件中读取count个数据到buf中。
返回值:成功的话返回读取到的字节数,出错返回-1,如果在调read之前已到达文件末尾,则这次read返回0。
说明1:关于文件偏移位置
file结构体中有一个成员f_pos,记录的是文件当前读写到的位置,每次读写后该成员的值都会发生调整。
说明2:关于返回值
返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,同时文件的当前读写位置f_pos移到最后,下次read将返回0。
write
头文件:#include <unistd.h>
函数原型:ssize_t write(int fd, const void *buf, size_t count);
功能:从buf里写count个数据到fd指向的文件中。
返回值:成功返回写入的字节数,出错返回-1。
使用举例:
int test2()
{
// 1、对文件进行写入
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0664);
const char* source = "I Can See You\n";
write(fd, source, strlen(source));
close(fd);
// 2、读取文件内容到显示器
fd = open("log.txt", O_RDONLY);
char ch;
while(1)
{
ssize_t s = read(fd, &ch, 1);
if(s <= 0)
break;
write(1, &ch, 1);// 1号文件描述符对应标准输出
}
close(fd);
return 0;
}
不同操作系统对文件操作的系统调用接口不同,但对于语言而言为了保证同一套方法能够在不同操作系统上执行,就要去封装其他操作系统的接口。下面我们以C语言的用户操作接口封装Linux的系统调用接口举例。
Linux中的文件表项对应的数据结构是struct file,而C语言中描述文件信息的结构体是struct FILE。我们可以在/usr/include/stdio.h头文件中可以找到下面这段代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名:
typedef struct _IO_FILE FILE;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
注意到这里面有很多IO缓冲区的信息,关于缓冲区后面会讲到,除此之外还有一个重要的成员叫做_fileno,这个就是文件描述符,也就是说C语言的FILE结构体封装了系统级的文件描述符,这里的系统指的是所有支持C语言的操作系统,像Linux、Windows等等。
前面有说过C语言还专门声明了三个FILE类型的结构体成员:stdin、stdout、stderr。他各自的_fileno对应的值为0、1、2这是定死的。
C语言文件操作函数的底层实现
在C语言中我们打开一个文件用的是fopen函数,调用该函数时系统会生成一个该文件对应的FILE结构体,并且会初始化好里面成员_fileno的值,然后返回该FILE结构体的指针,C语言文件操作函数都是通过这个FILE结构体指针来完成的,具体如何完成的呢?就是拿到成员_fileno的值再去调用系统调用接口如Linux中的:open、read、write、close等等。
下面部分是C语言提供的文件操作函数,它们的底层实现都是拿到文件描述符再去调用系统提供的文件操作函数接口。
头文件:#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fclose(FILE *fp);
...
C语言输入输出函数的底层实现
这里的输入输出函数指printf、fprintf、scanf、fscnaf等。
头文件:#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
学习C语言时我们知道printf函数可以把格式化数据打印输出到显示器上,对应到fprintf如果第一个参数传入stdout也是把格式化数据打印到显示器上,这种操作底层是如何实现的呢?
void test()
{
fprintf(stdout, "hello world\n");//hello world
}
stdout里的_fileno是1,前面说过一个进程运行起来之后会默认打开三个file类型文件表项:标准输入(键盘)、标准输出(显示器)和标准错误(显示器)并把他们的地址填入到进程级的打开文件描述符表中下标为0、1、2的位置:
fprintf执行时首先找到FILE结构体中的_fileno,然后调用系统调用接口write把_fileno作为第一个参数传入,又因为stdout中的_fileno值为1,对应打开文件描述符表中指向的是标准输出,所以最后是把内容写入到了标准输出即显示器中。
重定向的本质就是修改文件描述符下标对应的struct file*指针。修改方法有两种:
PS:下面介绍的例子都以间接修改的方式来实现重定向。
输出重定向
输出重定向就是把我们本该输出到一个文件的内容输出到另外一个文件。
比如本该输出到显示器上的内容最终输出到了文件log.txt中:我们一开始先把1号文件描述符所指向的文件(标准输出)关闭,然后再打开一个新的文件log.txt,这样log.txt分配到的文件描述符就是1,再利用write向1号文件描述符指向的文件写入内容就不会在写入到显示器上而是写入到log,txt中了。
void test()
{
close(1);
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
// 调用c接口的写入函数
printf("hello world");
}
运行程序后屏幕上什么都没输出,再来看看log.txt发现写入了内容:
追加重定向
追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。
比如我们想在刚刚输出重定向创建出来的文件log.txt中追加数据,只需改变传入的选项为:O_WRONLY|O_APPEND即可,因为log.txt已经存在,所以可以不用传第三个参数:
void test()
{
close(1);
int fd = open("log.txt", O_WRONLY|O_APPEND);
printf("Im appended data");
}
执行程序后发现,本该输出到显示器上的内容以追加的方式重定向到了log.txt中:
输入重定向
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。
比如我们想让本应该从“标准输入”读取数据的scanf函数,改为从log.txt文件当中读取数据。那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“标准输入”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0,scanf就会从log.txt中读取数据了。
void test()
{
close(0);
int fd = open("log.txt", O_RDONLY);
char str[40];
while (scanf("%s", str) != EOF)
{
printf("%s\n", str);
}
}
头文件:#include <unistd.h>
函数原型:int dup2(int oldfd, int newfd);
作用:把fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,最终内容同oldfd一致。
返回值:调用成功,返回newfd,否则返回-1。
PS:可以先使用close关闭描述符为newfd的文件,不用也行系统最后会自动帮你关闭;如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
比如我们在输出重定向时可以直接用dup2替换打开文件描述符表中1下标的内容为fd下标的内容,这时1下标和fd下标都存的是log.txt文件表项的地址。
void test()
{
umask(0);
int fd = open("log.txt", O_WRONLY|O_CREAT, 0666);
dup2(1, fd);
printf("hello world");
}
在进行文件之间的数据交互时,通常会把交互的数据先存放到缓冲区里,到合适的时候一次性把缓冲区的数据交给运算器和中央处理器去处理完成交互,而不是拿到数据后马上处理,这样的话效率太低了。
数据的缓冲方式分为以下三种:
这三种缓冲方式里,要保证最高数据处理效率的话,应该是无缓冲最快,即进程终止后把所有数据一次性统一处理。但是现实我们在进行文件间数据交互过程中还需要拿到中间交互的结果,保证中间过程正确才进行下一步交互,所以综合后有了行缓冲。
下面说说缓冲区,前面讲1.4 C语言对Linux系统调用接口的封装时有提到struct FILE源码中保存有一系列C语言提供的缓冲区的数据,下面我们来探讨C库函数以及Linux系统调用接口在进行文件数据交互时的缓冲方式。
void test()
{
// C库接口
fprintf(stdout,"hello printf\n");
// 系统调用接口
write(1, "hello write\n", 12);
fork();
}
执行程序,fprintf函数和write函数的交互数据都打印到了显示器上:
我们重定向把数据输出到log.txt这个普通文件中,发现fprintf函数的格式化数据输出了两次并且是在write之后输出的:
原因是因为系统调用函数是以无缓冲方式来交互数据的,所以write执行时就马上把所有数据刷新到指向文件里了;而fprintf输出到标准输出时是行缓冲,重定向后输入到普通文件里,缓冲方式变为全缓冲,这个时候即使有"\n"也不会刷新缓冲区数据,这些数据依然保留在C语言缓冲区当中,fork()后子进程继承一份父进程的缓冲区,在最后两个进程都结束时操作系统强制刷新C语言缓冲区,才会出现先打印write的数据,再打印两份fprintf的格式化数据这种结果。
打开文件表是由一个个文件表项组成的,这些文件表项包含文件读写操作的函数指针、以及读写操作相关的成员变量:记录偏移量的f_pos、记录权限的f_mode、记录文件操作方式的f_flag等等。在对文件进程读写操作时,根据传入的文件描述符在打开文件描述符表中找到相应的文件表项,最后调用文件表项里的读写函数完成文件读写操作。
同一个进程的不同文件描述符可以指向同一个文件表项。比如通过dup2函数改变文件描述符的内容:
子进程在创建时会拷贝父进程的打开文件描述符表,因此刚创建子进程时,父子进程是共享文件表项的,如图所示:
文件表项的file结构体中有一个f_path成员,类型为struct path,该类型定义如下:
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
继续看看struct dentry的定义:
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 */
};
特别注意该结构体中有一个成员d_inode,其类型struct inode的定义如下:
struct inode {
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;// 文件锁
unsigned int i_flags;// 文件打开方式
struct mutex i_mutex;
unsigned long i_state;
unsigned long dirtied_when;
// inode表的头结点
struct hlist_node i_hash;
struct list_head i_wb_list;
struct list_head i_lru;
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;// inode打开次数
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;// 文件最后被访问的时间
struct timespec i_mtime;// 文件内容最后的修改时间
struct timespec i_ctime;// 文件属性最后的修改时间
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就是inode表的组成成分,每一个inode以双链表的形式组织成inode表。inode,全称index node即索引节点,该结构体的功能是描述文件的属性信息。
在Linux操作系统中的任何资源都被当作文件来管理。如目录、光驱、终端设备等等,都被当作是一种文件。从这方面来说,Linux操作系统中的所有的目录、硬件设备都跟普通文件一样,具有共同的属性。而这些属性信息都保存在inode块中。
属性也称为元信息,如文件的创建、修改时间、文件大小等等,这些基本属性可以通过:ls -l命令查看。但是需要注意的是,有一个属性是不包括在inode中的,就是文件名,至于为什么后面讲到目录时再作说明。
下面我们来分析struct inode中的几个成员:
1、目录文件操作的结构体
inode中有个成员i_op,类型为const struct inode_operations *,定义如下:
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;
可见,在该成员变量所指向的数据结构中,包含了许多函数指针,这些函数指针大多针对于目录文件。当然inode里也有针对普通文件操作数据结构:struct file_operations,注意这个类型的对象在file结构体中也有一个。
2、inode号
成员i_ino代表的就是该文件所对应的inode编号,每个文件创建后都有自己inode号,inode号是文件存在的唯一标识。
查看文件inode号的命令:ls -i
3、inode打开次数
前面的file结构体中有一个成员叫做f_count记录的是该文件表项被放入打开文件描述符表的数量,每close该文件表项一次f_count对应减一,直到最后减为0时才会删除file结构体。对应inode中也有一个成员叫做i_count,记录的是该inode被多少个文件表项所保存。
4、软、硬链接
硬链接
一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Linux系统允许,多个文件名指向同一个inode号码。
这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。
创建硬链接的命令:ln 源文件 目标文件
运行上面这条命令以后,产生的连接文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做"链接数",记录指向该inode的文件名总数,这时就会增加1。
反过来,删除一个文件名,就会使得inode节点中的"链接数"减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。
我们创建一个新目录后可以直接查看这个新目录的硬链接数:
inode结构体中的i_nlink成员对应记录的就是硬连接数,那么为什么新创建的目录一开始硬链接数就是2呢?
创建目录时,新目录默认会生成两个目录项:".“和”…"。前者的inode号码就是当前目录的inode号码,算作一个当前目录的"硬链接";后者的inode号码就是当前目录的父目录的inode号码,算作一个父目录的"硬链接"。该目录自己加上该目录下的"."都指向同一个inode所以一开始硬连接数是2。
软连接
除了硬链接以外,还有一种特殊情况。
文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。
这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:“No such file or directory”。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,所以文件B的inode"硬链接数"不会因此发生变化。
创建软链接的命令:ln -s 源文文件或目录 目标文件或目录
磁盘分区
磁盘分区是告诉操作系统“我这块磁盘在此分区可以访问的区域是A柱面到B柱面之间的块”,这样操作系统就知道它可以在所指定的块内进行文件数据的读、写、查找等操作。磁盘分区即指定分区的起始与结束柱面。一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(如mkfs命令)格式化成某种格式的文件系统,然后才能存储文件,格式化过程中会在磁盘上写一些管理存储布局的信息。一个分区只能格式化成一个文件系统。
分区格式化的原因:每种操作系统所配置的文件属性/权限并不相同, 为了存放这些文件所需的数据,因此就需要将分区进行格式化,以成为操作系统能够利用的『文件系统格式(filesystem)』。
在Linux系统中可以通过df命令查看磁盘空间的使用情况:
ext2文件系统
EXT2第二代扩展文件系统(second extended filesystem,缩写为 ext2),是Linux内核所用的文件系统,于1993年1月加入Linux核心支持之中。
文件系统是如何运行的呢?这与操作系统的文件数据有关。较新的操作系统的文件数据除了文件实际内容外, 通常含有非常多的属性,例如 Linux 操作系统的文件权限(rwx)与文件属性(拥有者、群组、时间参数等)。 文件系统通常会将这两部份的数据分别存放在不同的区块,属性信息放置到inode中,至于实际数据则放置到 data block 区块中。 另外,还有一个超级区块 (superblock) 会记录整个文件系统的整体信息,包括 inode 与 block 的总量、使用量、剩余量等。
一开始介绍的磁盘分区是上图的第一列结构,接下来我们介绍第二列结构:组结构,它包括启动块和块组。启动块(Boot Block),用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划分成若干个同样大小的块组(Block Group)。
块组的组成
1、超级块(Super Block)描述整个分区的文件系统信息,如inode/block的大小、总量、使用量、剩余量,以及文件系统的格式与相关信息。超级块在每个块组的开头都有一份拷贝(第一个块组必须有,后面的块组可以没有)。 为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的super block信息在这种情况下也能正常访问。所以一个文件系统的super block会在多个block group中进行备份,这些super block区域的数据保持一致。
超级块记录的信息有:
PS:superblock的相关信息可以使用:dumpe2fs 这个命令查询
超级快的作用
当操作系统启动后,系统内核会把超级块中的内容复制到内存中,并周期性的利用内存里的最新内容去更新硬盘上的超级块中的内容。由于这个更新存在 一个时间差,为此内存中的超级块信息与硬盘中的超级块信息往往只有在开机与关机的某个特定时刻是同步的;而在其他时间都是不同步的。假设发生操作系统宕机或者因为断电而造成的意外事故时,内存中的超级块信息没有及时保存到硬盘中,此时文件系统的完整性就会受到破坏。轻者导致刚建立的丢失,重则的话会导致 文件系统瘫痪。遇到这种情况时,以前系统工程师往往需要利用系统提供的sync命令在系统出现故障的那一刻把内存里的内容复制到磁盘上。现在这个过程往往操作系统会自动完成,这也是为什么Linux操作系统要比Windows操作系统稳定的一个重要原因。在操作系统重新启动的过程中,系统内核会对内存和硬盘中的信息进行比较,根据他们之间的差异,给文件系统打上干净或者脏的标签。这个信息也是存储在文件系统的超级块中。
可见超级块如果发生损坏的话,对于文件系统的破坏性非常的大。轻者的话导致某个文件系统无法挂载,重则的话导致整个操作系统崩溃。在Linux操作系统中,除了可以利用sync命令来保证硬盘上的内容决不会比内存里的内容更新慢之外,操作系统会将超级块内容保存到不同块组中。当其中一个超级块出现问题时,操作系统会自动采用另外一个超级块。等到系统运行正常后,系统内容就会把可用的超级块去替换那个故障的超级块,这样文件系统与操作系统就可以正常挂载与启动。否则的话,仅有有一个超级块是可用的,那么这一个坏了整个文件系统就坏了。这种机制在很大程度上提高了超级块的安全性和Linux操作系统的稳定性。
inode源码中超级快的位置
2、块组描述符表(Group Descriptor Table,简称GDT)存储该块组的描述信息,整个分区分成多个块组就对应有多少个块组描述符。
每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始是Data Blocks,空闲的inode和数据块还有多少个等等。
3、块位图(Block Bitmap)用来描述整个块组中哪些块已用哪些块空闲。块位图本身占一个块,其中的每个bit代表本块组的一个block,这个bit为1代表该块已用,为0表示空闲可用。假设格式化时block大小为1KB,这样大小的一个块位图就可以表示1024*8个块的占用情况,因此一个块组最多可以有10248个块。
4、inode位图(inode Bitmap)和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。 Inode bitmap的作用是记录该块组中Inode区域的使用情况。
5、inode表(inode Table)由一个块组中的所有inode组成。一个文件除了数据需要存储之外,一些描述信息也需要存储,如文件类型,权限,文件大小,创建、修改、访问时间等,这些信息存在inode中而不是数据块中。inode表占多少个块在格式化时就要写入块组描述符中。 在ext2文件系统中,每个文件在磁盘上的位置都由文件系统块组中的一个Inode指针进行索引,Inode将会把具体的位置指向一些真正记录文件数据的block块,需要注意的是这些block可能和Inode同属于一个block group也可能分属于不同的block group。我们把文件系统上这些真实记录文件数据的block称为Data blocks。
6、数据块(Data Block)是用来放置文件内容数据的地方。根据不同的文件类型有以下几种情况:
inode源码中的数据块
一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。这个数组对应inode中的成员i_data,除此之外inode中还有一个记录块数的成员i_blocks。
由于inode号码与文件名分离,这种机制导致了一些Linux系统特有的现象。
有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
移动文件或重命名文件,只是改变文件名,不影响inode号码。
打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。通常来说,系统无法从inode号码得知文件名。
第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新inode,旧版文件的inode则被回收。
磁盘中的每个文件都对应一个inode,每一个文件表项都会指向一个文件的inode,但是同一文件的inode可以对应多个文件表项(当多次调用open打开同一个文件时就会出现这种情况)。不管是同一进程多次打开同一已存在文件(如图中A进程的0号和2号文件描述符对应两个文件表项,但是最终指向同一inode即同一文件),还是不同进程多次打开同一已存在文件(如图中A进程3号文件描述符和B进程的3号文件描述符)。