本帖最后由 dirty 于 2025-1-24 10:18 编辑
本篇阅读学习文件系统及使用。
概述
在Linux 系统中,一切皆文件,除了通常所说的狭义的文件(文本文件和二进制文件)以外,目录、设备、套接字和管道等都是文件。
文件系统在不同的上下文中有不同的含义。
①在存储设备上组织文件的方法,包括数据结构和访问方法。
②按照某种文件系统类型格式化的一块存储介质。我们常说在某个目录下挂载或卸载文件系统,这里的文件系统就是这种意思。
③内核中负责管理和存储文件的模块,即文件系统模块。
Linux文件系统架构如下:
用户空间层面
应用程序可以直接使用内核提供的系统调用访问文件;
①一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能诸问这个文件系统。
②系统调用umount用来卸载某个目录下挂载的文件系统。
③使用 open 打开文件。
④使用close 关闭文件。
⑤使用read 读文件。
⑥使用 write 写文件。
⑦使用lseek 设置文件偏移。
⑧当我们写文件的时候,内核的文件系统模块把数据保存在页缓存中,不会立即写到存储设备。
应用程序也可以使用 glibc 厍封装的标准 O 流函数访问文件,glibc 库封装的标准 O 流函数如下所示:
①使用 fopen 打开流。
②使用 fclose 关闭流
③使用 fead 读流。
④使用 fwrite 写流。
⑤使用 fseek 设置文件偏移。
⑥使用fwrite可以把数据写到用户空间缓冲区,但不会立即写到内核。我们可以使用fflush 冲刷流,即把写到用户空间缓冲区的数据立即写到内核。
硬件层面
外部存储设备分为块设备、闪存和NVDIMM 设备3类.
块设备主要有以下两种:机械硬盘、闪存类块设备。如SSD、eMMC、UFS。
闪存(Flash Memory)按存储结构分为NAND闪存和NOR闪存。
NVDIMM (Non-Volatile DIMM,非易失性内存; DIMM 是 Dual-Inline-Memory-Modules的缩写,表示双列直插式存储模块,是内存的一种规格)断电后数据不丢失。在断电的瞬间,超级电容提供电力,把内存中的数据转移到NAND闪存。
内核空间层面
在内核的目录下可以看到,内核支持多种文件系统类型。为了对用户程序提供统一的文件操作接口。
为了使不同的文件系统实现能够其存,内核实现了一个抽象层,称为虚拟文件系统(Vitual File System,VFS),也称为虚拟文件系统功换(virnal Filesystem Switch,VFS)。
文件系统分为以下4种。
①块设备文件系统,存储设备是机械硬盘和固态硬盘等块设备,常用的块设备文件系统是EXT和 btfs(读作|bAtS|)。EXT文件系统是Limux原创的文件系统,目前有3个版本:EXT2、EXT3 和 EXT4。
②闪存文件系统,存储设备是NAND闪存和NOR闪存,常用的闪存文件系统是JFFS2(日志型闪存文件系统版本2,Journalling Flash File System version2)和 UBIFS(无序区块镜像文件系统,Unsorted Block Image File System)。
③内存文件系统,文件在内存中,断电以后文件丢失,常用的内存文件系统是tmpfs,用来创建临时文件。
④伪文件系统,是假的文件系统,只是为了使用虚拟文件系统的编程接口。常用伪文件系统sockfs、proc 文件系统、sysf、hugetlbfs、cgroup、cgroup2
虚拟文件系统的数据结构
虽然不同文件系统类型的物理结构不同,但是虚拟文件系统定义了一套统一的数据结构。
①超级块。
②虚拟文件系统在内存中把目录组织为一棵树。
③每种文件系统的超级块的格式不同,需要向虚拟文件系统注册文件系统类型fle_system_type,并且实现 mount 方法用来读取和解析超级块。
④索引节点。
⑤目录项。
⑥当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:fle结构体,然后在进程的打开文件表中分配一个索引,这个索引称为文件描述符,最后把文件描述符和 fle 结构体的映射添加到打开文件表中。
注册文件系统类型
函数register flesystem用来注册文件系统类型:
int register filesystem(struct file_system type *fs);
函数unregister flesystem 用来注销文件系统类型:
int unregister filesystem(struct file_system_type *fs);
管理员可以执行命令“cat/proc/flesystems”来查看已经注册的文件系统类型。
挂载文件系统
虚拟文件系统在内存中把目录组织为一棵树。一个文件系统,只有挂载到内存中目树的一个目录下,进程才能访问这个文件系统。
glibc库封装了挂载文件系统的函数mount:
int mount(const char*dev_name, const char *dir_name,const char *type,unsigned long flags,const void *data);
glibc库封装了两个卸载文件系统的函数:
①函数umount,对应内核的系统调用oldumount。
int umount(const char *target);
②函数umount2,对应内核的系统调用umount。
int umount2(const char*target,int flags);
打开文件
进程读写文件之前需要打开文件,得到文件描述符,然后通过文件描述符读写文件。
内核提供了两个打开文件的系统调用。
①int open(const char *pathname, int flags, mode t mode);
②int openat(int dirfd, const char *pathname, int flags, mode t mode),
如果打开文件成功,那么返回文件描述符,值大于或等于0;如果打开文件失败,返回负的错误号。
创建文件
创建不同类型的文件,需要使用不同的命令。
①普通文件:touchFE,这条命今本来用来更新文件的访问时间和修改时间,如果文件不存在,创建文件。
②目录:mkdir DIRECTORY
③符号链接(也称为软链接):In-STARGETLINKNAME或In--symbolC TARGETLINK NAME
④字符或块设备文件:mknod NAME TYPE [MAJOR MINOR]
参数 TYPE:b表示带缓冲区的块设备文件,c表示带缓冲区的字符设备文件,u表示不带缓冲区的字符设备文件,p表示命名管道。
⑤命名管道:mkpipe NAME
⑥命令“In TARGETLINK NAME”用来创建硬链接,给已经存在的文件增加新的名文件的索引节点有一个硬链接计数,如果文件有几个名称,那么硬链接计数是n。
内核提供了下面这些创建文件的系统调用。
①创建普通文件。
int creat(const char*pathname,mode_t mode);
也可以使用open和openat创建普通文件
int open(const char *pathname,int flags,mode_t mode);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
②创建目录。
int mkdir(const char*pathname,mode_t mode);
int mkdirat(int dirfd,const char *pathname, mode_t mode);
③创建符号链接。
int symlink(const char *oldpath,const char *newpath);
int symlinkat(const char *oldpath, int newdirfd, const char *newpath);
④mknod通常用来创建字符设备文件和块设备文件,也可以创建普通文件、命名管道和套接字。
int mknod(const char *pathname,modet mode, dev_t dev);
int mknodat(int dirfd, const char *pathname, mode_t mode, dev_t dev);
⑤link用来创建硬链接,给已经存在的文件增加新的名称。
int link(const char *oldpath,const char*newpath);
int linkat(int olddfd, const char *oldpath, int newdfd, const char *newpath);
glibc库封装了和上面的系统调用同名的库函数,还封装了创建命名管道的库函数。库函数 mkffo 是通过调用系统调用 mknod 来实现的。
int mkfifo(const char*pathname,mode t mode);
删除文件
删除文件的命今如下。
①删除任何类型的文件:unlink PILE.
②m FJLE,默认不删除目录,如果使用选项“”“_只”或“_recursive”,可以删除目录和目录的内容。
③删除目录:mdirDIRECTORY.
内核提供了下面这些刑除文件的系统调用。
①unlink用来删除文件的名称,如果文件的硬链接计数变成0,并且没有进程打开这个文件,那么删除文件。
int unlinx(conet char*pathname);
int unlinrat(int dirfd, const char *pathname, int flags);
②删除目录。
int rmdir(const char *pathname);
设置文件权限
内核提供了下面这些设置文件权限的系统调用
①int chmod(const char *path, mode t mode),
②int fchmod(int fd, mode t mode);
③int fchmodat(int dfd, const char *filename, mode t mode);
读文件
进程读文件的方式有3种。
①调用内核提供的读文件的系统调用。
②调用glibc 库封装的读文件的标准 O 流函数。
③创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空国直接读内存。
内核提供了下面这些读文件的系统调用。
①系统调用read从文件的当前偏移读文件,把数据存放在一个缓冲区.
ssize_t read(int fd,void *buf,size_t count);
②系统调用pread64 从指定偏移开始读文件。
ssize_t pread64(int fd, void *buf, sizet count, off_t offset);
③系统调用readv从文件的当前偏移读文件,把数据存放在多个分散的缓冲区。
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
④系统调用preadv从指定偏移开始读文件,把数据存放在多个分散的缓冲区
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
⑤系统调用preadv2在系统调用preadv 的基础上增加了参数“int flags”。
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);
其中preadv和preadv2是Linux内核私有的系统调用。
写文件
进程写文件的方式有3种。
①调用内核提供的写文件的系统调用。
②调用 glibc 库封装的写文件的标准 IO 流函数。
③创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接写内存。
内核提供了下面这些写文件的系统调用。
①函数 write 从文件的当前偏移写文件,调用进程把要写入的数据存放在一个缓冲区
ssize_t write(int fd,const void *buf,size_t count);
②函数 pwrite64 从指定偏移开始写文件。
ssize_t pwrite64(int fd, const void *buf,size_t count, off_t offset);
③函数 writev从文件的当前偏移写文件,调用进程把要写入的数据存放在多个分散的缓冲区。
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
④函数pwritev从指定偏移开始写文件,调用进程把要写入的数据存放在多个分散的缓冲区。
ssize_t pwritev(int fd, const struct iovec *iov, int iovont, off_t offset);
⑤函数 pwritev2 在函数 pwritev 的基础上增加了参数“int flags”。
ssize_t pwritev2(int fd, const struct iovec *jov, int iovent, off_t offset, int flags) ;
其中 pwritev和pwritev2是Linux内核私有的系统调用。
文件回写
进程写文件时,内核的文件系统模块把数据写到文件的页缓存,没有立即写回到存储设备。文件系统模块会定期把脏页(即数据被修改过的文件页)写回到存储设备,进程也可以调用系统调用把脏页强制写回到存储设备。
内核提供了下面这些把文件同步到存储设备的系统调用。
①sync把内存中所有修改过的文件元数据和文件数据写回到存储设备。
void sync(void);
②syncfs把文件描述符fd引用的文件所属的文件系统写回到存储设备。
int syncfs(int fd);
③fsyc把文件描述符fd引用的文件修改过的元数据和数据写回到存储设备
int fsync(int fd);
④fdatasync把文件描述符d引用的文件修改过的数据写回到存储设备,还会把检索这些数据需要的元数据写回到存储设备。
int fdatasync(int fd);
⑤Linux 私有的系统调用sync fle range 把文件的一个区间修改过的数据写回到存储设备
int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags);
glibc库针对这些系统调用封装了同名的库函数,还封装了一个把数据从用户空间缓冲区写到内核的标准 I/O 流函数:
int fflush(FILE *stream);
下面以打开、读、写、关闭文件示例代码如下
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
char buf_w[30] = "HELLO WORLD!!!\n";
char buf_r[30] = "HELLO WORLD!!!\n";
int fd = open("./test", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("error: test open\n");
return -1;
}
if( write(fd, buf_w, strlen(buf)) < 0 )
{
printf("error: test write\n");
return -1;
}
if(read(fd, buf, 30) < 0)
{
printf("error: test read\n");
return -1;
}
printf("read fd:%s", buf);
ret = close(fd);
if (ret < 0)
{
printf("error: test close\n");
return -1;
}
if(ret = 0)
{
printf("succeed: test close\n");
return 0;
}
return 0;
}
至此,对文件系统进行了梳理学习与使用,对Linux文件系统加深了认识。
|