文件系统与inode编号

文件描述符fd

0&1&2

Linux 进程默认情况会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器 所以输入输出还可以采用如下方式

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));
    if (s > 0)
    {
        buf[s] = 0;
        write(1, buf, strlen(buf));
        write(2, buf, strlen(buf));
    }
    return 0;
}

文件系统与inode编号_第1张图片

文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表 files_struct, 该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的分配规则

我们通过一段代码来比较直接的观察
演示代码:
#include 
#include 
#include 
#include 
#include 
int main()
{
    int fd = open("myfile", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

运行结果:

因为0,1,2文件描述符都已经被占用了,直接从3开始好像挺好理解的。我们猜测文件描述符是从没有被占用的数字从小到大分配的,所以是不是呢?我们通过下面这段代码来验证一下
演示代码
#include 
#include 
#include 
#include 
#include 
int main()
{
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT);

    if (fd1 < 0 || fd2 < 0 || fd3 < 0)
    {
        perror("open");
        return 1;
    }

    printf("fd1: %d\n", fd1);
    printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}
运行结果
文件系统与inode编号_第2张图片
那如果我们先关掉 0,2呢(1是标准输出,为了方便观察结果,就不关闭它啦)
来看下代码
#include 
#include 
#include 
#include 
#include 
int main()
{
    close(0);
    close(2);
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT);
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT);

    if (fd1 < 0 || fd2 < 0 || fd3 < 0)
    {
        perror("open");
        return 1;
    }

    printf("fd1: %d\n", fd1);
    printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

运行结果

文件系统与inode编号_第3张图片
结论:文件描述符是按照未分配的数字从小到大分配的。

重定向

那如果关闭1呢?
演示代码:
#include 
#include 
#include 
#include 
#include 
int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    printf("hello dear programmer");
    fflush(stdout);//刷新缓冲区⭐

    close(fd);
    exit(0);
}

运行结果:
文件系统与inode编号_第4张图片
显示器上什么页没有输出,但当我们打开文件myfile发现,原本要输入到显示器的内容全部输入到文件中了,这就叫重定向。
通过这张图片,我们来了解一下重定向的本质

文件系统与inode编号_第5张图片dup2系统调用

man dup2  文件系统与inode编号_第6张图片
使用示例:
#include 
#include 
#include 
int main()
{
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    close(1);
    dup2(fd, 1);
    for (;;)
    {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0)
        {
            perror("read");
            break;
        }
        printf("%s", buf);
        fflush(stdout);
    }
    return 0;
}

运行结果:

文件系统与inode编号_第7张图片 FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。 所以C库当中的FILE必定封装了fd。
#include 
#include 
int main()
{
    const char *msg0 = "hello printf\n";
    const char *msg1 = "hello fwrite\n";
    const char *msg2 = "hello write\n";
    printf("%s", msg0);
    fwrite(msg1, strlen(msg0), 1, stdout);
    write(1, msg2, strlen(msg2));
    fork();
    return 0;
}

运行结果:

文件系统与inode编号_第8张图片但如果对进程实现输出重定向呢?执行 ./test > file 后,结果变成了

文件系统与inode编号_第9张图片

我们发现 printf fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用)。为什么呢?这里我们猜测可能和fork有关,通过屏蔽fork,我们发现结果是 文件系统与inode编号_第10张图片
(1)一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
(2)printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据 的缓冲方式由行缓冲变成了全缓冲。
(3)我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
(4)但是进程退出之后,会统一刷新,写入文件当中。
(5)但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。
(6)write 没有变化,说明没有所谓的行缓冲
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不在我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供
//缓冲区相关
/* 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. */

理解文件系统

我们使用  ls -l    的时候看到的除了看到文件名,还看到了文件元数据。
  文件系统与inode编号_第11张图片
 
每行包含 7 列:
         模式,硬链接数,文件所有者 ,组,大小文件名,最后修改时间
  文件系统与inode编号_第12张图片
ls -l 读取存储在磁盘上的文件信息,然后显示出来
文件系统与inode编号_第13张图片
另外 stat命令也可以查看文件信息
文件系统与inode编号_第14张图片

文件系统

文件系统与inode编号_第15张图片
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs -b 选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
GDT,Group Descriptor Table:块组描述符,描述块组属性信息
块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
i节点表:存放文件属性如文件大小,所有者,最近修改时间等
数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
[hty@iZ2vcboxg2e41nj4s5s6zrZ test]$ cd day3
[hty@iZ2vcboxg2e41nj4s5s6zrZ day3]$ touch file
[hty@iZ2vcboxg2e41nj4s5s6zrZ day3]$ ls -i file
1581596 file

文件系统与inode编号_第16张图片

创建一个新文件主要有一下4个操作:
1. 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。
3. 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件,内核将入口(263466,file)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
下面解释一下文件的三个时间:
         Access 最后访问时间
        Modify 文件内容最后修改时间
        Change 属性最后修改时间

你可能感兴趣的:(Linux,linux,运维,服务器)