2、文件系统

Linux文件系统



一、Linux文件系统详解

本大节转自:https://blog.csdn.net/new0801/article/details/63687127

1、文件系统层次分析

  由上而下主要分为用户层、VFS层、文件系统层、缓存层、块设备层、磁盘驱动层、磁盘物理层。
  用户层:最上面用户层就是我们日常使用的各种程序,需要的接口主要是文件的创建、删除、打开、关闭、写、读等。
  VFS层:我们知道Linux分为用户态和内核态,用户态请求硬件资源需要调用System Call通过内核态去实现。用户的这些文件相关操作都有对应的System Call函数接口,接口调用 VFS对应的函数。
  文件系统层:不同的文件系统实现了VFS的这些函数,通过指针注册到VFS里面。所以,用户的操作通过VFS转到各种文件系统。文件系统把文件读写命令转化为对磁盘LBA的操作,起了一个翻译和磁盘管理的作用。
  缓存层:文件系统底下有缓存,Page Cache,加速性能。对磁盘LBA的读写数据缓存到这里。
  块设备层:块设备接口Block Device是用来访问磁盘LBA的层级,读写命令组合之后插入到命令队列,磁盘的驱动从队列读命令执行。Linux设计了电梯算法等对很多LBA的读写进行优化排序,尽量把连续地址放在一起。
  磁盘驱动层:磁盘的驱动程序把对LBA的读写命令转化为各自的协议,比如变成ATA命令,SCSI命令,或者是自己硬件可以识别的自定义命令,发送给磁盘控制器。Host Based SSD甚至在块设备层和磁盘驱动层实现了FTL,变成对Flash芯片的操作。
  磁盘物理层:读写物理数据到磁盘介质。

2、文件系统结构与工作原理(主要以ext4为例)

  我们都知道,windows文件系统主要有fat、ntfs等,而linux文件系统则种类多的很,主要有VFS做了一个软件抽象层,向上提供文件操作接口,向下提供标准接口供不同文件系统对接,下面主要就以EXT4文件系统为例,讲解下文件系统结构与工作原理:


  上面两个图大体呈现了ext4文件系统的结构,从中也相信能够初步的领悟到文件系统读写的逻辑过程。下面对上图里边的构成元素做个简单的讲解:
  引导块:为磁盘分区的第一个块,记录文件系统分区的一些信息,引导加载当前分区的程序和数据被保存在这个块中。一般占用2kB,
  超级块:超级块用于存储文件系统全局的配置参数(譬如:块大小,总的块数和inode数)和动态信息(譬如:当前空闲块数和inode数),其处于文件系统开始位置的1k处,所占大小为1k。为了系统的健壮性,最初每个块组都有超级块和组描述符表(以下将用GDT)的一个拷贝,但是当文件系统很大时,这样浪费了很多块(尤其是GDT占用的块多),后来采用了一种稀疏的方式来存储这些拷贝,只有块组号是3, 5 ,7的幂的块组(譬如说1,3,5,7,9,25,49…)才备份这个拷贝。通常情况下,只有主拷贝(第0块块组)的超级块信息被文件系统使用,其它拷贝只有在主拷贝被破坏的情况下才使用。
  块组描述符:GDT用于存储块组描述符,其占用一个或者多个数据块,具体取决于文件系统的大小。它主要包含块位图,inode位图和inode表位置,当前空闲块数,inode数以及使用的目录数(用于平衡各个块组目录数),具体定义可以参见ext3_fs.h文件中struct ext3_group_desc。每个块组都对应这样一个描述符,目前该结构占用32个字节,因此对于块大小为4k的文件系统来说,每个块可以存储128个块组描述符。由于GDT对于定位文件系统的元数据非常重要,因此和超级块一样,也对其进行了备份。GDT在每个块组(如果有备份)中内容都是一样的,其所占块数也是相同的。从上面的介绍可以看出块组中的元数据譬如块位图,inode位图,inode表其位置不是固定的,当然默认情况下,文件系统在创建时其位置在每个块组中都是一样的,如图2所示(假设按照稀疏方式存储,且n不是3,5,7的幂)
  块组:每个块组包含一个块位图块,一个 inode 位图块,一个或多个块用于描述 inode 表和用于存储文件数据的数据块,除此之外,还有可能包含超级块和所有块组描述符表(取决于块组号和文件系统创建时使用的参数)。下面将对这些元数据作一些简要介绍。
  块位图:块位图用于描述该块组所管理的块的分配状态。如果某个块对应的位未置位,那么代表该块未分配,可以用于存储数据;否则,代表该块已经用于存储数据或者该块不能够使用(譬如该块物理上不存在)。由于块位图仅占一个块,因此这也就决定了块组的大小。
  inode位图:inode位图用于描述该块组所管理的inode的分配状态。我们知道inode是用于描述文件的元数据,每个inode对应文件系统中唯一的一个号( 可通过ls -li命令或者stat <filename>进行查看),如果inode位图中相应位置位,那么代表该inode已经分配出去;否则可以使用。由于其仅占用一个块,因此这也限制了一个块组中所能够使用的最大inode数量。
  inode表:inode表用于存储inode信息。它占用一个或多个块(为了有效的利用空间,多个inode存储在一个块中),其大小取决于文件系统创建时的参数,由于inode位图的限制,决定了其最大所占用的空间。
  以上这几个构成元素所处的磁盘块成为文件系统的元数据块,剩余的部分则用来存储真正的文件内容,称为数据块,而数据块其实也包含数据和目录。
  了解了文件系统的结构后,接下来我们来看看操作系统是如何读取一个文件的:

大体过程如下:
 1、根据文件所在目录的inode信息,找到目录文件对应数据块
 2、根据文件名从数据块中找到对应的inode节点信息
 3、从文件inode节点信息中找到文件内容所在数据块块号
 4、读取数据块内容

  到这里,相信很多人会有一个疑问,我们知道一个文件只有一个Inode节点来存放它的属性信息,那么你可能会想如果一个大文件,那它的block一定是多个的,且可能不连续的,那么inode怎么来表示呢,下面的图告诉你答案:

  也就是说,如果文件内容太大,对应数据块数量过多,inode节点本身提供的存储空间不够,会使用其他的间接数据块来存储数据块位置信息,最多可以有三级寻址结构。

三级寻址
  在inode table中会存放多个inode,而每个inode都只存放一个文件名,一个指向与Data Block的指针。这就是Ext2的一级寻址。当文件过大(一个块为4K),会将第一个数据块用作记录其他指向的数据块。这就是二级寻址(第一个数据块存放的是地址,也就是4个字节,一共可以指向1024个地址块,可供寻址范围是4M),三级寻址可供选择寻址空间为16G,四级寻址则为TB级。

  到这里,应该都已经非常清楚文件读取的过程了,那么下面再抛出两个疑问:
   1、文件的拷贝、剪切的底层过程是怎样的?
   2、软连接和硬连接分别是如何实现的?
  下面来结合stat命令动手操作一下,便知真相:
  (1)拷贝文件:创建一个新的inode节点,并且拷贝数据块内容,会发现,拷贝得到的文件会分配一个新的inode节点。


  (2) 剪切文件:同个分区里边mv,inode节点不变,只是更新目录文件对应数据块里边的文件名和inode对应关系;跨分区mv,则跟拷贝一个道理,需要创建新的inode, 因为inode节点不同分区是不能共享的

  (3) 软连接: 创建软连接会创建一个新的inode节点,其对应数据块内容存储所链接的文件名信息,这样原文件即便删除了, 重新建立一个同名的文件,软连接依然能够生效

  (4) 硬链接: 创建硬链接,并不会新建inode节点,只是links加1,还有再目录文件对应数据块上增加一条文件名和inode对应关系记录; 只有将硬链接和原文件都删除之后,文件才会真正删除,即links为0才真正删除

3、文件顺序读写和随机读写

  从前面文章了解了磁盘工作原理之后,也已经明白了为什么文件随机读写速度会比顺序读写差很多,这个问题在windows里边更加明显,为什么呢?究其原因主要与文件系统工作机制有关,fat和ntfs文件系统设计上,每个文件所处的位置相对连续甚至紧靠在一起,这样没有为每个文件留下足够的扩展空间,因此容易产生磁盘碎片,用过windows系统的应该也知道,windows磁盘分区特别提供了磁盘碎片整理的高级功能。如下图:

  那回过来,看看linux 文件系统ext4,都说linux不需要考虑磁盘碎片,究竟是怎么回事?
  主要是因为Linux的文件系统会将文件分散在整个磁盘,在文件之间留有大量的自由空间,而不是像Windows那样将文件一个接一个的放置。当一个文件被编辑了并且变大了,一般都会有足够的自由空间来保存文件。如果碎片真的产生了,文件系统就会尝试在日常使用中将文件移动来减少碎片,所以不需要专门的碎片整理程序。但是,如果磁盘空间占用已经快满了,那碎片是不可避免的,文件系统的设计本来就是用来满足正常情况下使用的。如果磁盘空间不够,那要么就是数据冗余了,要么就该换容量更大的磁盘。你可以使用fsck命令来检测一下一个Linux文件系统的碎片化程度,只需要在输出中查看非连续i节点个数(non-contiguous inodes)就可以了。

二、Linux文件系统函数

1、基于inode的函数——stat

  inode中记录着 Linux文件的属性,首先,看一下inode的结构:

struct stat {
    dev_t     st_dev;     /* 文件的设备编号 */
    ino_t     st_ino;     /* inode number */
    mode_t    st_mode;    /* 文件的类型和存取权限 */
    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;    /* 文件字节数 */
    blksize_t st_blksize; /* blocksize for filesystem I/O */
    blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
    time_t    st_atime;   /* time of last access */
    time_t    st_mtime;   /* time of last modification */
    time_t    st_ctime;   /* time of last status change */
};

对inode结构体中三个时间变量的解释:
  st_atime:文件最近访问时间;
  st_mtime:文件内容最近更改时间;
  st_ctime:文件inode值最近改动时间。

函数解释
  函数功能:通过文件名filename获取文件信息,并保存在buf所指的结构体stat中;
  函数参数:其中path表示文件地址;fd表示文件描述符;buff表示文件信息结构体指针;
  返回值:执行成功则返回0,失败返回-1,错误代码存于errno。
  给定一个pathname:
   stat函数返回一个与此命名文件有关的信息结构;
   fstat函数获得已在描述符filedes上打开的文件的有关信息;
   lstat函数类似于stat,但是当命名的文件是一个符号连接时,lstat返回该符号连接的有关信息,而不是由该符号连接引用的文件的信息(不跟踪符号链接)。

2、文件(夹)”确认”函数——access

  对于Linux系统下的文件,每个文件或文件夹都有访问权限,在这里access就可以用于查看对应的访问权限。

函数解释
  函数功能:确定文件或文件夹的访问权限。即,检查某个文件的存取方式,比如说是只读方式、只写方式等;
  函数参数:其中pathname表示文件(夹)地址,mode表示想要查看的文件权限类型:
   mode:
     R_OK:是否具有读权限
     W_OK:是否具有写权限
     X_OK:是否具有执行权限
     F_OK:测试一个文件是否存在
  返回值:如果指定的存取方式有效,则函数返回0,否则函数返回-1。

示例:

#include <stdio.h>
#include <unistd.h>
#include <assert.h>
void accessTest(const char *filename,int mode)
{
    int result = access(filename, mode);
    char strPrint[50] = { 0 };
    switch (mode)
    {
    case R_OK:
        sprintf(strPrint, "File is %sReadable!", result == 0 ? "" : "Not ");
        break;
    case W_OK:
        sprintf(strPrint, "File is %sWritable!", result == 0 ? "" : "Not ");
        break;
    case X_OK:
        sprintf(strPrint, "File is %sExacutable!", result == 0 ? "" : "Not ");
        break;
    case F_OK:
        sprintf(strPrint, "File is %sExist!", result == 0 ? "" : "Not ");
        break;
    default:
        assert(0);
        break;
    }
    printf("%s\n", strPrint);
}

int main(void)
{
    char sz[] = "/home/richard/get.html";
    printf("Filename:%s\n", sz);
    accessTest(sz, R_OK);
    accessTest(sz, W_OK);
    accessTest(sz, X_OK);
    accessTest(sz, F_OK);
    return 0;
}

3、修改文件(夹)访问权限——chmod

  在Linux下我们用shell命令的chmod 来修改文件(夹)的访问权限,同样,我们也可以用Linux中的系统函数chmod和fchmod来修改文件(夹)的访问权限。

函数解释
  函数功能:修改文件(夹)的访问权限;
  函数参数:其中path表示文件(夹)地址;fd表示文件描述符;mode表示想要查看的文件权限类型:
  返回值:如果指定的存取方式有效,则函数返回0,否则函数返回-1。

关于文件权限类型值mode_t,可见下表:

modeNamemodeValuemodeDescription
S_ISUID 04000 文件的执行时设置用户ID(set user-id on execution)位
S_ISGID 02000 文件的执行时设置组ID(set group-id on execution)位
S_ISVTX 01000 文件的保存正文(粘着位sticky)位(黏住位用于swap分区。)
S_IRWXU 00700 文件所有者具有读、写、执行权限
S_IRUSR(S_IREAD) 00400 文件所有者具可读取权限
S_IWUSR(S_IWRITE) 00200 文件所有者具可写入权限
S_IXUSR(S_IEXEC) 00100 文件所有者具可执行权限
S_IRWXG 00700 用户组具有读、写、执行权限
S_IRGRP 00400 用户组具可读取权限
S_IWGRP 00200 用户组具可写入权限
S_IXGRP 00100 用户组具可执行权限
S_IRWXO 00700 其他用户具有读、写、执行权限
S_IROTH 00400 其他用户具可读取权限
S_IWOTH 00200 其他用户具可写入权限
S_IXOTH 00100 其他用户具可执行权限

变量名记忆:
  S_ISUID: SUID->Set User ID
  S_ISGID: SUID->Set Group ID
  S_ISVTX: SVTX->SaVe Text Bit
  S_IRWXU: RWX(可读可写可执行) U->User
  同理,后面的G->Group;O->Other。
变量使用示例:
  比如要修改test文件的权限,权限是744,那么可以采用以下几种方法:
   chmod(“/home/richard/test”, S_IRWXU|S_IRGRP|S_IROTH);
   chmod(“/home/richard/test”, 0744);
   chmod(“/home/richard/test”, 484);
  第一种方法是将00700和00040和00004进行或运算,最终得到的结果就是0744(八进制表示方法是首位为0,十六进制是0x),八进制的0744就等于十进制的484。所以上面三种方法是等效的。
  当我们给chmod函数传递参数时他会将对应的十进制的mode参数转换为相应的八进制进行运算。所以文件的权限为744时传递给函数chmod的参数就应该写成0744(八进制表示方法)或者484(十进制表示方法)。

4、创建文件夹——mkdir

函数解释
  函数功能:以mode方式创建一个以参数pathname命名的目录,mode定义新创建目录的权限;
  函数参数:其中pathname表示文件(夹)地址;mode表示目录的权限类型;
  返回值:如果创建成功,函数返回0,否则函数返回-1。
  当然一般创建目录的时候应该给可执行权限,也就是755,否则是无法cd切换目录进去的。

示例:

#include <stdio.h>
#include <sys/stat.h>  
int main(char argc, char *argv[])  
{  
  int ok = 0;  
  if((ok=mkdir(argv[1],0755)) != 0)  
  {  
    perror("mkdir %s", argv[1]);  
  }  
  return 0;     
}  

函数解释
  函数功能:为oldpath文件创建一个newpath硬链接
  函数参数:其中oldpath表示已存在文件路径 ,newpath表示目标链接文件地址(不存在的);
  返回值:如果创建成功,函数返回0,否则函数返回-1。
  当rm删除该链接时,只是删除了目录下的记录项和把inode硬链接计术减1,减为0时,才会真正的删除文件。(文件本身初始时就有一个基于本身的硬链接)

  读取链接
  
  读取到的链接所指向的文件名。不读取文件内容。
  
  删除链接
  
  如果是符号链接,删除符号链接
  如果是硬链接,硬链接数减一,当为0时释放inode和数据也块
  如果文件硬链接属性为0,但是进程已经打开该文件,并持有该文件描述符,则当文件关闭时,内核才会删除文件。
  利用此特性创建临时文件,进程中创建然后马上调用unlink,在文件未关闭之前可以存放临时数据,关闭后文件会被内核销毁。

6、文件(夹)重命名——rename

函数解释
  函数功能:用于重命名文件、改变文件路径或更改目录名称;
  函数参数:其中oldname为旧文件名,newname为新文件名;
  返回值:如果重命名成功,函数返回0,否则函数返回-1。
 重命名文件:
  如果newname指定的文件存在,则会被删除。
  如果newname与oldname不在一个目录下,则相当于移动文件。
 重命名目录:
  如果oldname和oldname都为目录,则重命名目录。
  如果newname指定的目录存在且为空目录,则先将newname删除。
  对于newname和oldname两个目录,调用进程必须有写权限。
  重命名目录时,newname不能包含oldname作为其路径前缀。例如,不能将/usr更名为/usr/foo/testdir,因为老名字( /usr/foo)是新名字的路径前缀,因而不能将其删除。

示例:

#include <stdio.h>
#include <fcntl.h>
int main(void)
{
    char oldname[100], newname[100];
    /* prompt for file to rename and new name */
    printf("请告诉我一个文件的完整路径: ");
    gets(oldname);
    printf("您想修改为: ");
    gets(newname);
    /* 更改文件名 */
    if (rename(oldname, newname) == 0)
        printf("已经把文件 %s 修改为 %s.\n", oldname, newname);
    else
        perror("rename");
    return 0;
}

7、改变工作目录——chdir

函数解释
  函数功能:用于改变当前进程的工作目录;
  函数参数:其中path为想要改变的目标目录;
  返回值:如果改变成功,函数返回0,否则函数返回-1。

8、获取当前工作目录——getcwd/getwd/get_current_dir_name

函数解释
  函数功能:用于获取当前进程的工作目录;
  函数参数:其中buf用于存放工作目录名称数据;size表示设置获取目录名称的长度;
  返回值:如果改变成功,函数返回一个指向所获取的目录的字符指针,否则函数返回NULL。

示例:

#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("Current dir name:%s\n", get_current_dir_name());

    char path[100] = { 0 };
    printf("Current path:%s;\t%s;\n", getwd(path), path);

    char samePath[100] = { 0 };
    printf("The same path is:%s;\t%s;\n", getcwd(samePath, 100), samePath); //Return NULL if the directory couldn't be determined or size was too small.
    return 0;
}

三、目录操作函数实例——递归遍历目录

示例:

#include <stdio.h>
#include <unistd.h>
void readDir(DIR *dir, const char *path)
{
    if (dir == NULL) {
        return;
    }

    char buff[100] = { 0 };
    struct dirent *ent;
    while ((ent = readdir(dir)) != NULL) {
        char type = ent->d_type;
        if (type == DT_DIR) {
            //如果是目录,就打开进入该目录
            if (strcmp(ent->d_name, ".") == 0 ||
                strcmp(ent->d_name, "..") == 0) {
                continue;
            }

            if (strlen(path) + strlen(ent->d_name) + 2 > sizeof(buff)) {
                printf("The buffer is full!\n");
                continue;
            }
            sprintf(buff, "%s%c%s", path, "\/", ent->d_name); // "/"需要转义

            printf("Dirname:%s\n", buff);
            DIR *ndir = opendir(buff);
            readDir(ndir, buff);
        }
        else {
            sprintf(buff, "%s%c%s", path, "\/", ent->d_name); // "/"需要转义
            printf("Filename:%s\n", buff);
        }
    }
}
int main(void)
{
    char path[100] = { 0 };
    printf("Current path:%s.\n", getwd(path));

    struct stat buf;
    int res = stat(path, &buf);
    if (res < 0) {
        printf("Get path stat error!\n");
        exit(1);
    }

    if (!S_ISDIR(buf.st_mode)) { //判断是否是目录
        printf("The path is not a directory!!\n");
        exit(1);
    }

    DIR *dir = opendir(path);
    readDir(dir, path);

    return 0;
}

  本次测试只能够递归显示一级目录,如果有兴趣的话,可以尝试去实现递归显示所有文件,甚至实现shell命令tree的功能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值