Linux 内存管理一
磁盘
文件存储在硬盘上,硬盘的最小存储单位叫做”扇区”(Sector)。每个扇区储存512字节(磁盘不能定位到每一个地址,职能定位到扇区,所以读写操作常常是以一整个扇区为单位,例如要变某一个地方的值,是将整个扇区读出来,然后更改完后在写回去)。
操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,即连续八个 sector组成一个 block。
文件描述符fd和文件指针(FILE结构体)
文件描述符fd
linux中一切接文件,而所有文件都通过文件描述符来操作,文件描述符是一个非负的整数,每当打开现有文件或者创建新文件,内核会像进程都会返回一个文件描述符,一个新创建的进程中,默认有三个文件描述符,0、1、2,如果创建或者打开新的文件,则会返回3,即没增加一个文件,则该进程返回的文件描述符+1。
每个进程都有一个文件描述符的表,用来管理文件描述符,父进程如果fork子进程的话,子进程会继承这个文件描述符的表。相同的文件可以有不同的进程打开,一个文件也能被一个进程打开多次,会生成多个文件描述符,但是都是指向的同一个文件。
进程打开文件过程:
执行open通过系统调用打开文件,该进程获得文件描述符之后能操作文件。并且进程会为该文件创建file对象,将file对象的指针存入进程描述符表。即文件描述符fd是一个整数,他对应着文件描述符表中的key,根据这个key能找的val,即文件指针。
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
FILE结构体
//C语言文件指针域文件描述符之间可以相互转换
int fileno(FILE * stream)
FILE * fdopen(int fd, const char * mode)
struct _iobuf {
char *_ptr; //缓冲区当前指针
int _cnt;
char *_base; //缓冲区基址
int _flag; //文件读写模式
int _file; //文件描述符
int _charbuf; //缓冲区剩余自己个数
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
FILE结构体中包含文件描述符和缓冲区。
注意:文件描述符和文件指针的区别
int fd = open("");
这里的fd代表的是文件描述符,每个进程的PCB中保存着一份文件描述符表,这个fd表示已打开的文件在这个表中的索引。每个表项都有一个指针指向打开的文件
FILE *fd;代表的是文本文件,使用fopen、fread、fwrite进行读写
这里的fd表示文件指针,通过FILE结构体来构建。FILE包括一个IO缓冲区和文件描述符,可以理解为FILE包含了文件描述符
文件描述符和文件指针的转换:
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
与文件描述符相关的三张表
- 进程中的文件描述符表
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
struct task_struct {
//...
struct files_struct *files // 进程级别的文件描述符表
//...
};
- linux内核维护的文件描述符表
struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /*打开文件时所指定的标志*/
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void *private_data; /* tty驱动程序所需 */
};
内核对所有打开的文件通过一个链表建立链接。并将链表中的每个节点称为文件句柄,一个打开文件的句柄存储了打开文件的全部信息,包括:当前操作文件的偏移、标识状态(open时的flag)、文件权限、文件属性、对inode的引用等。
2. 文件系统的inode表
struct inode {
struct hlist_node i_hash;
struct list_head i_list;
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev; //该成员表示设备文件的inode结构,它包含了真正的设备编号。
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
}
将硬盘格式化为ext文件系统是,硬盘会被分数据区(block)和inode区,block存放数据,inode存放文件包含的信息。包括文件字节数、占用了那几个磁盘块、文件属性和权限、文件时间戳、链接树(内核的文件描述符表中有多少文件指向这个inode)、文件数据block的位置,以及inode编号。(通过df -i
指令可以看到inode信息,通过ls -il
最前面的一列就是inode编号)。
虚拟文件系统(VFS)
进程所有的文件操作都通过VFS,由VFS来适配各种底层不同的文件系统,完成实际的文件操作。
VFS是内核的一个子系统,VFS向上提供统一的操作接口。
一个具体的文件系统想要被Linux支持,必须按照VFS的规范编写自己的操作函数,同时将自己的操作细节对内核的其他子系统隐藏
通俗的说,VFS就是定义了一个通用文件系统的接口层和适配层,一方面为用户进程提供了一组统一的访问文件,目录和其他对象的统一方法,另一方面又要和不同的底层文件系统进行适配。具体应用:例如mount、umount、sysfs、chown、mkdir等,特别,异步IO的select和poll是VFS。
主要作用
主要作用:
- 支持多种具体文件系统直接的相互访问(例如ext的文件系统通过mount指令在某个目录下挂在Fat文件系统的U盘,粘贴复制移动等操作都不被影响是因为VFS的原因)
- 接受系统的调用,read、write、open
- 对具体文件系统数据结构进行抽象,以统一的数据结构进行管理
- 接受其他子系统的操作
Linux中文件系统逻辑关系架构
VFS的核心数据结构
- 超级块(superblock)模块:用于保存一个文件系统的所有元数据,相当于这个文件系统的信息库,为其他的模块提供信息。因此一个超级块可代表一个文件系统。文件系统的任意元数据修改都要修改超级块。超级块对象是常驻内存并被缓存的。
- 索引节点(inode)模块:管理一个具体的文件,是文件的唯一标识,一个文件对应一个inode。通过inode可以方便的找到文件在磁盘扇区的位置。同时inode模块可链接到address_space模块,方便查找自身文件数据是否已经缓存。
- 目录项(dentry)模块:管理路径的目录项。比如一个路径 /home/foo/hello.txt,那么目录项有home, foo, hello.txt。目录项的块,存储的是这个目录下的所有的文件的inode号和文件名等信息。其内部是树形结构,操作系统检索一个文件,都是从根目录开始,按层次解析路径中的所有目录,直到定位到文件。
- 文件(file)模块:包含所有内核已经打开的文件。已经打开的文件对象由open系统调用在内核中创建,也叫文件句柄。打开文件列表模块中包含一个列表,每个列表表项是一个结构体struct file,结构体中的信息用来表示打开的一个文件的各种状态参数。
- 文件操作(file_operations)模块。这个模块中维护一个数据结构,是一系列函数指针的集合,其中包含所有可以使用的系统调用函数,例如open、read、write、mmap等。每个打开文件(打开文件列表模块的一个表项)都可以连接到file_operations模块,从而对任何已打开的文件,通过系统调用函数,实现各种操作。
- address_space模块,它表示一个文件在页缓存中已经缓存了的物理页。它是页缓存和外部设备中文件系统的桥梁。如果将文件系统可以理解成数据源,那么address_space可以说关联了内存系统和文件系统。
文件读写过程
linux系统采用虚拟内存机制后,页是虚拟内存管理的最小单位。有两个概念需要区分一下:buffer chahe和page cache,buffer是面向块设备的,而page(页)是面向虚拟内存的。文件io之和page缓存交互,不和内存交互。
读过程
- 通过read函数进程系统调用发起读请求
- 内核检查进程的文件描述符表定位到文件指针
- 根据文件指针找到目录项和inode等信息
- 根据inode计算出要读取的地址和页的信息
- 通过inode找到文件对应的address_space
- 在address_space中访问该文件的页缓存树,查找对应的页缓存结点:如果页缓存命中,那么直接返回文件内容;如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;
写过程
- 前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:
- 如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。
- 如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。
- 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:手动调用sync()或者fsync()系统调用把脏页写回,pdflush进程会定时把脏页写回到磁盘。同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
参考博客
- https://blog.csdn.net/qq_20817327/article/details/107093167?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165371513316782246462636%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=165371513316782246462636&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_ecpm_v1~rank_v31_ecpm-8-107093167-null-null.nonecase&utm_term=%E5%AD%98%E5%82%A8&spm=1018.2226.3001.4450