Linux系统编程02--文件系统

二、文件系统

操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。从系统角度来看,文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

常见的文件系统有
winow环境下:

FAT16、FAT32、NTFS、exFAT

Linux环境下:

Ext文件系统,包含Ext2、Ext3、Ext4。最新版是Ext4

ext2文件系统

磁盘一个物理扇区大小为512字节,扇区是磁盘(硬件角度)的最小单元,文件管理系统是通过block块来储存文件,所以block都是512B的整数倍。pc机默认block大小为4KB即4096B即8个物理扇区。

  • 1个磁盘扇区 = 512B

  • 1个block = 8个物理扇区

  • 1个block = 4096B = 32768bit

磁盘组成:

  • boot block(启动块):整个磁盘的第一个扇区,规定大小1KB,记录着当前整个磁盘分了几个区,每个区起始地址,以及每个分区里面装的系统类型等本磁盘信息。

如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZOTdLNq-1645690204694)(Linux系统编程笔记.assets/文件系统.png)]

  • 超级块,描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount的时间等等。超级块在每个块组的开头都有一份拷贝。

  • 块组描述符表(GDT,Group Descriptor Table) 由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。

  • 块位图(Block Bitmap) 一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。

  • inode位图(inode Bitmap) 和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用。

  • inode表(inode Table) 我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls -l命令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有inode组成了inode表。

    inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个inode,换句话说,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费。如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候inode会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用mke2fs的-i参数手动指定每多少个字节分配一个inode。数据块(Data Block) 根据不同的文件类型有以下几种情况

  • 数据块(Data Block) 根据不同的文件类型有以下几种情况

    对于常规文件,文件的数据存储在数据块中。

    对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,ls -l命令看到的其它信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。

    文件储存过程

    下图为进程创建文件操作过程(假设数据块大小设置为1KB):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhjmpGsY-1645690204695)(Linux系统编程笔记.assets/inode.jpg)]
间接寻址(上图中的12为一级间接寻址,13为二级间接寻址,14为三级间接寻址):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DMHcedQg-1645690204695)(Linux系统编程笔记.assets/数据块寻址.png)]
从上图可以看出,索引项Blocks[13]指向两级的间接寻址块,最多可表示(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示64.26MB的文件。索引项Blocks[14]指向三级的间接寻址块,最多可表示(b/4)3+(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示16.06GB的文件。可见,这种寻址方式对于访问不超过12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的inode和数据块往往已经被内核缓存了,读大文件的效率也不会太低。

文件删除过程(rm)

那么删除文件是什么过程呢?

在使用电脑时会有这样的现象,储存或者复制一个大文件到磁盘内会很耗费时间,但是删除它则很快,这是因为写入操作是将文件数据按照上面过程一步步寻址写入,删除过程却不是这样,删除过程只是找到文件inode位图,将该文件对应inode位记录位置为0,并不是寻址一个个数据块擦除数据块内数据,而是直接将文件inode对应位图位置0,文件对应数据块位图位置0。删除过程其实并没有真正删除文件数据内容,只是切断系统到文件具体数据块的寻址路径。数据恢复就是沿着这条路恢复数据的。

目录中记录项文件类型

创建一个目录时,内核会给它分配一个数据块(新创建的文件夹默认大小为4096B即1Block),为该目录的记录项数据块,记录项数据块内有若干条记录,每条记录对应目录下的一个文件,每条记录内容为该文件的文件信息:1.文件名;2.inode号(4B);3.本条记录的长度;4.文件类型。

其中目录也是文件的一种,也有inode,与上述文件是一样的,只是目录文件的inode记录内的数据块指针指向的是记录项数据块。

根目录/的inode号是约定俗成的为2。比如vim /home/jiaojian/wdqk.txt,或者open("/home/jiaojian/wdqk.txt");的过程是:

先由inode=2找到/目录的inode记录,指向/目录的记录项数据块,通过”home“文件名匹配记录项,找到home记录,同时找到hone目录的inode号,在通过其inode号找到home目录的inode记录–>home目录记录项块,接着通过“jiaojian”文件名,匹配记录,得到jiaojian目录的inode号–>jiaojian记录项块,匹配文件名“wdqk.txt”,得到其inode号,再找到对应inode记录,数据块指针指向wdqk.txt文件的数据块即找到了文件内容。记录项内文件类型有(储存时以编号代替):

编码 文件类型
0 Unknown
1 Regular file
2 Directory
3 Character device
4 Block device
5 Named pipe
6 Socket
7 Symbolic link
基于inode的函数

inode 是 UNIX 操作系统中的一种数据结构,其本质是结构体,它包含了与文件系统中各个文件相关的一些重要信息。在 UNIX 中创建文件系统时,同时将会创建大量的 inode 。通常,文件系统磁盘空间中大约百分之一空间分配给了 inode 表。

有时,人们使用了一些不同的术语,如 inode索引编号 (inumber)。这两个术语非常相似,并且相互关联,但它们所指的并不是同样的概念。inode 指的是数据结构;而索引编号实际上是 inode 的标识编号,因此也称其为inode 编号 或者索引编号。索引编号只是文件相关信息中一项重要的内容。下一个部分将介绍 inode 中的其他一些属性。

注意:inode是文件节点,本质是一个结构体,里面成员记录了文件的基本属性信息,inode有一个编号与其对应,有时把这个编号简称也叫inode。

inode 表包含一份清单,其中列出了对应文件系统的所有 inode 编号。当用户搜索或者访问一个文件时,UNIX 系统通过 inode 表查找正确的 inode 编号。在找到 inode 编号之后,相关的命令才可以访问该 inode ,并对其进行适当的更改。

例如,使用 vi 来编辑一个文件。当您键入 vi 时,在 inode 表中找到 inode 编号之后,才允许您打开该 inode 。在 vi 的编辑会话期间,更改了该 inode 中的某些属性,当您完成操作并键入 :wq 时,将关闭并释放该 inode 。通过这种方式,如果两个用户试图对同一个文件进行编辑, inode 已经在第一个编辑会话期间分配给了另一个用户 ID (UID),因此第二个编辑任务就必须等待,直到该 inode 释放为止。

inode结构体内成员(常用):

  • inode 编号

  • 用来识别文件类型,以及用于 stat C 函数的模式信息

  • 文件的链接数目

  • 属主的组 ID (GID)

  • 文件的大小

  • 文件所使用的磁盘块的实际数目

  • 最近一次修改的时间

  • 最近一次访问的时间

  • 最近一次更改的时间

  • 权限

从根本上讲, inode 中包含有关文件的所有信息(除了文件的实际名称以及实际数据内容之外)。

stat()

也有一个shell命令为stat,stat 打印出一个信息节点(inode)的内容,它们显示为对人可读的格式的stat(2).

   下面是stat的一个示例输出:
   File: “/”
   Size: 1024         Allocated Blocks: 2            Filetype: Directory
   Mode: (0755/drwxr-xr-x)         Uid: (    0/    root)  Gid: (    0/  system)
   Device:  0,0   Inode: 2         Links: 20
   Access: Wed Jan  8 12:40:16 1986(00000.00:00:01)  最近访问时间:最近访问文件,即上次打开文件的时间
   Modify: Wed Dec 18 09:32:09 1985(00021.03:08:08)  最近更改时间:最近改动文件内容时间
   Change: Wed Dec 18 09:32:09 1985(00021.03:08:08)  直径改动时间:最近改动inode时间

stat()函数原型:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);功能与stat一样,
唯一区别是lstat不跟踪符号链接,即stat查询符号链接时是顺着符号链接去查询其指向的文件了,
而lstat则是查询符号链接本身信息,不跟踪链接。
描述:获取文件inode属性信息
pathname:文件名
struct stat *statbuf:stat结构体指针,接收stat查询inode的内容,并纪录到结构体内储存

stat()函数很简单,即查询文件inode信息,将信息放到预定的stat结构体内,stat结构体定义如下:

struct stat {
               dev_t     st_dev;         /* ID of device containing file */
               ino_t     st_ino;         /* Inode number */
               mode_t    st_mode;        /* File type(4) and mode(0755) */如40755,
               nlink_t   st_nlink;       /* Number of hard links */
               uid_t     st_uid;         /* User ID of owner */
               gid_t     st_gid;         /* Group ID of owner */
               dev_t     st_rdev;        /* Device ID (if special file) */
               off_t     st_size;        /* Total size, in bytes */long int
               blksize_t st_blksize;     /* Block size for filesystem I/O */block数量
               blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */物理扇区数量

               /* Since Linux 2.6, the kernel supports nanosecond
                  precision for the following timestamp fields.
                  For the details before Linux 2.6, see NOTES. */
               struct timespec st_atim;  /* Time of last access */
               struct timespec st_mtim;  /* Time of last modification */
               struct timespec st_ctim;  /* Time of last status change */
           #define st_atime st_atim.tv_sec      /* Backward compatibility */
           #define st_mtime st_mtim.tv_sec
           #define st_ctime st_ctim.tv_sec
           };

stat既有命令也有同名函数,用来获取文件 Inode里主要信息,stat 跟踪符号链接,lstat不跟踪符号链接。

stat里面时间辨析:atime(最近访问时间): mtime(最近更改时间):指最近修改文件内容的时间 ctime(最近改动时间):指最近改动Inode的时间

其中文件权限信息查询结果为st_mode,为八进制数,内容包含文件类型和文件权限位,man查询如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2nGe8Myo-1645690204696)(Linux系统编程笔记.assets/st_mode.png)]

access()

查询文件权限与是否存在,直接查询inode。

#include <unistd.h>

int access(const char *pathname, int mode);

mode 参数:
R_OK 是否有读权限
W_OK 是否有写权限
X_OK 是否有执行权限
F_OK 测试一个文件是否存在
return:查询成功返回0,查询失败返回-1

按实际用户ID和实际组ID测试文件属性,跟踪符号链接

有效用户与实际用户:比如在sudo下,有效用户为root,实际用户为jiaojian.

chmod()

更改文件权限,mode为八进制数字,字符串如何转换为八进制?

#include <sys/stat.h>

int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
更改文件权限,chmod与fchmod功能一样,只是fchmod以文件描述符为操作对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cgrv7Frz-1645690204696)(Linux系统编程笔记.assets/chmod.png)]

chown()

必须要有root权限才能操作。

更改文件所有者和所属组

#include <unistd.h>

int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group); 不跟踪软连接
utime()

修改文件上一次访问时间和更改时间。

#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

#include <sys/time.h>
int utimes(const char *filename, const struct timeval times[2]);

struct utimbuf 
{
    time_t actime;       /* access time 访问时间*/
    time_t modtime;      /* modification time 修改时间*/
};

truncate()

截断文件,与open()函数内的截断选项一样的意思。

#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
参数:1,文件路径,文件描述符
    2.需截断成的长度(字节),如截断成空,则length =0,截断为100B,则length = 100;
return:成功,返回0;失败:返回-1
链接函数

文件的硬链接数也记录在文件inode内。

软、硬链接本质是什么呢?

首先创建一个硬链接,若目录test/下有文件abc.c,其inode中硬链接数为1,此时用ln命令创建一个硬链接,ln abc.c app,即app --> abc.c。源文件硬链接数加1,变成2,并且查看app时发现其大小与源文件大小一模一样,这是与软连接本质上的区别,其细节为,创建硬链接时,并不是将磁盘中的文件复制一份,而是在当前目录指向的记录块内添加一条记录(文件名,inode,文件类型,记录长度),其中硬链接们的inode与原文件的inode是一模一样的,所以硬链接的本质是一条记录,也可以重新理解为系统在目录中重新创建了一条与inode所标记的磁盘文件的联系方式;而软连接其本质是创建一条链接到源文件(硬链接)的跟踪指定,其跟踪的是源文件(硬链接)的名字,而不是inode编号。对于软连接来说,改变源文件的文件名,链接就失效了,而硬链接不会,因为硬链接寻址磁盘文件不依赖源文件,而是直接通过文件inode寻址数据块。

当文件硬链接数大于1,rm删除目录下的文件,,其实是删除一个硬链接,即删除一条目录下的记录项,以及将该文件inode内硬链接计数减1,磁盘上的文件数据还在,并没有真正意义上的删除文件,只是删除了一条系统与磁盘文件的联系方式,还有另外的联系方式;当硬链接计数为1,再删除文件才是真正的删除(也不是物理意义上的删除,并没有把文件数据块全部抹成0),其过程也是删除记录项,但是此时是切断了系统与文件的所有联系方式,inode号回收,inode位图对应位置0。

link()

创建硬链接:

#include <unistd.h>
int link(const char *oldpath, const char *newpath);

1.硬链接通常要求位于同一个文件系统

2.符号链接没有文件系统的限制

3.通常情况下不允许创建目录的硬链接,容易造成死循环,如grep命令查找内容时加入死循环。

4.创建目录以及增加硬链接计数应当是一个原子操作(不可分割最小操作,中间不能被打断),即创建文件硬链接时有两个部分:1,增加目录记录项;2.inode内硬链接计数加1;前面的意思是,这两部分是不能分开的不能打断的,属于同一个操作内。

symlink()

创建软连接:软连接(符号链接)symbolic link

#include <unistd.h>
int symlink(const char *target, const char *linkpath);

readlink()

读取符号链接指向的文件名,即读取的是符号链接本身的内容,并不会跟踪到软连接指向的文件的内容。

#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
与read()函数类似,参数:
1.文件名(符号链接名)2.预设缓冲数组地址3.本次打算读取字符个数,储存到前面预设数组内
return:
成功:返回实际读取到(储存到buf内)的字符数(字节数);
失败:返回-1
unlink()
  • 如果是符号链接,删除符号链接
  • 如果是硬链接,硬链接计数减1,当减到0,释放inode和数据块(inode位图和块位图对应位置0)
  • 如果文件硬链接数为0,但有进程已打开该文件,并持有文件描述符,则等待该进程关闭该文件时,kernel才会去删除该文件,创建临时文件。
  • 临时文件(很重要):利用上面的特性创建临时文件,先open或create创建一个文件,马上unlink()此文件。
#include <unistd.h>
int unlink(const char *pathname);

rm命令的本质就是unlink()。

rename()

文件重命名

#include <stdio.h>
int rename(const char *oldpath, const char *newpath);

chdir()

改变当前进程工作目录,切换目录。

#include <unistd.h>

int chdir(const char *path);
int fchdir(int fd);
成功返回0,失败返回-1

chdir()将调用进程的当前工作目录更改为path中指定的目录。

fchdir()与chdir()相同;唯一的区别是这个目录是作为一个打开的文件描述符给出的。

如命令pwd为打印当前进程(shell)的工作目录,cd为切换当前进程(shell)的工作目录。

getcwd()

获取当前进程的工作目录

#include <unistd.h>
char *getcwd(char *buf, size_t size);
参数:buf为预设储存连接的字符数组;size:数组大小,防止前面数组写入越界
return:返回指向buf的指针。
pathconf()
#include <unistd.h>
long fpathconf(int fd, int name);
long pathconf(const char *path, int name);
查询系统允许文件最大文件名,最大硬链接数等等限制信息
目录操作
mkdir()

创建目录,并且设置权限

#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);

rmdir()

删除空目录。mkdir()与rmdir()一样都会shell命令一样

#include <unistd.h>
int rmdir(const char *pathname);

opendir()
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd);

打开目录,返回目录指针(目录流)DIR*,是一个结构体指针,指向目录记录块,DIR*和文件里面读写指针位置类似。这里DIR*是读取记录块内记录项的读写位置。

readdir()
#include <dirent.h>
struct dirent *readdir(DIR *dirp);

struct dirent {
               ino_t          d_ino;       /* Inode number */
               off_t          d_off;       /* Not an offset; see below */记录项长度
               unsigned short d_reclen;    /* Length of this record */记录项长度
               unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
               char           d_name[256]; /* Null-terminated filename */文件名
           };

结构体dirent内容为DIR*所指向记录项的内容,每次返回目录内一个文件的一条记录项,然后DIR*指向下一条记录项如此向下移动,如果到达目录记录块的末尾,返回NELL(errno不会设置因为到达目录记录块末尾不是错误,当发生错误时会设置错误)。

rewinddir()

把目录指针恢复到目录的起始位置。

#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);

telldir()

查询当前目录指针在什么位置

#include <dirent.h>
long telldir(DIR *dirp);

seekdir()

通过偏移量移动目录指针到指定位置,与lseek()类似

#include <dirent.h>
void seekdir(DIR *dirp, long loc);

closedir()

关闭目录,释放目录流。

#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
成功0,失败-1.
递归遍历目录***
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#define MAX_PATH 1024
/* dirwalk: apply fcn to all files in dir */
//回调函数(函数参数为以函数的函数称为回调)
void dirwalk(char *dir, void (*fcn)(char *))
{
	char name[MAX_PATH];
	struct dirent *dp;
	DIR *dfd;
	if ((dfd = opendir(dir)) == NULL) 
	{
		fprintf(stderr, "dirwalk: can't open %s\n", dir);
		return;
	}
	while ((dp = readdir(dfd)) != NULL)
	{
	if (strcmp(dp->d_name, ".") == 0
		|| strcmp(dp->d_name, "..") == 0)
		continue; /* skip self and parent */
	if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name))
		fprintf(stderr, "dirwalk: name %s %s too long\n",
			dir, dp->d_name);
	else 
	{
		sprintf(name, "%s/%s", dir, dp->d_name);
		(*fcn)(name);
	} 	
	}
	closedir(dfd);
}

/* fsize: print the size and name of file "name" */
void fsize(char *name)
{
	struct stat stbuf;
	if (stat(name, &stbuf) == -1) {
		fprintf(stderr, "fsize: can't access %s\n", name);
		return;
	}
    //这里的&操作参考st_mode的选项宏定义,判断文件类型
	if ((stbuf.st_mode & S_IFMT) == S_IFDIR)
		dirwalk(name, fsize);
	printf("%8ld %s\n", stbuf.st_size, name);
}

int main(int argc, char **argv)
{
	if (argc == 1) /* default: current directory */
		fsize(".");
	else
		while (--argc > 0)
			fsize(*++argv);
	return 0;
}
虚拟文件系统(VFS)抽象层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-htpumaZD-1645690204696)(Linux系统编程笔记.assets/VFS.png)]
VFS虚拟文件系统是在Linux内核内抽象出来的一层转换操作,如图,最上面是应用层,之间是内核和驱动层,最下面是磁盘文件系统。应用层函数open()等等都是不知道磁盘内是什么文件系统的,当open()调用时,内核会根据open()函数目标文件所在磁盘的文件系统对open()进行调整,调整为对应文件系统的格式操作。VFS即为内核中的一个抽象的文件系统转换机制。

理解VSF最重要的是明白,对于应用层来说,根本就不用关心磁盘到底是什么文件系统,内核会自动调整适应。

Linux支持各种各样的文件系统格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以mount到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用ls命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢?Linux内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(VFS,Virtual Filesystem)。这一节我们介绍运行时文件系统在内核中的表示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pCYRY0aU-1645690204696)(Linux系统编程笔记.assets/虚拟文件系统.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylpJcpvq-1645690204697)(Linux系统编程笔记.assets/vfs01.png)]
如上图,每一个文件描述符对应一个file结构体,file结构体内成员记录着操作硬件的file_operation驱动层函数,这是应用层操作函数到驱动层的调用,直接操作硬件。而另外一个成员f_dentry指向文件系统驱动操作函数inode_operations结构体,这是直接操作文件系统的。需要理解的是这些操作函数都是在内核里面抽象出来的一层抽象层,虚拟文件系统都是通过这两个操作函数进行文件系统转换配置,应用层根本不需要关心底层磁盘内的文件是那种文件系统,抽象层自动调整。

思考一个问题,两个文件描述符指向同一个文件,文件描述符3先写入hello,文件描述符4后写入world,文件描述符5也是后写入world,3和4指向同一个file结构体,会发生什么?他们的读写位置是共通的还是分开的?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFZBS0Yo-1645690204697)(Linux系统编程笔记.assets/file结构体.png)]
多个描述符指向同一条file结构体,其维护的成员变量是多个描述符共享的,共用。

多个描述符指向同一个文件,但是不同的file结构体,则他们各自维护的成员变量不是共享,而是相互独立的。

dup/dup2
#include <unistd.h>
//复制文件描述符,返回复制的新文件描述符,也根open()一样,默认取最小的未使用的描述符
//如dup(3) -->4,则4就是3的复制。4指向和3一样的file文件结构体
int dup(int oldfd);
//dup2是dup的升级版本,可以指定新的文件描述符为指定数字,如果新指定的文件描述符是已经被使用,
//则dup2先close原先的被使用的描述符,再将该描述符改为新描述符的复制
//即1-->stdout,3-->abc,dup2(3,1),则先把1close,再把1改为3的复制,1与3指向同一个file结构体
int dup2(int oldfd, int newfd);

dup和dup2都可用来复制一个现存的文件描述符,使两个文件描述符指向同一个file结构体。如果两个文件描述符指向同一个file结构体,File Status Flag和读写位置只保存一份在file结构体中,并且file结构体的引用计数是2。如果两次open同一文件得到两个文件描述符,则每个描述符对应一个不同的file结构体,可以有不同的File Status Flag和读写位置。请注意区分这两种情况。

返回值:两个函数的返回值成功为新描述符,失败为-1并写入errno。

重定向实例
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
	int fd, save_fd;
	char msg[] = "This is a test\n";
	fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);//特别注意这里控制属性与操作,有时是与掩码操作
	if(fd<0) {
		perror("open");
		exit(1);
	}
	save_fd = dup(STDOUT_FILENO);
	dup2(fd, STDOUT_FILENO);
	close(fd);
	write(STDOUT_FILENO, msg, strlen(msg));
	dup2(save_fd, STDOUT_FILENO);
	write(STDOUT_FILENO, msg, strlen(msg));
	close(save_fd);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pohihy41-1645690204697)(Linux系统编程笔记.assets/实例.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值