• 【Linux】基础IO ——中


    🎇Linux:基础IO


    • 博客主页:一起去看日落吗
    • 分享博主的在Linux中学习到的知识和遇到的问题
    • 博主的能力有限,出现错误希望大家不吝赐教
    • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

    在这里插入图片描述

    💨 💦 🎶 🎵


    💨 1. 承接上文

    💦 1.1 测试用例1

    #include
    #include
    #include
    #include
    #include
    
    int main()
    {
    	close(1);
    	int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
    	if(fd < 0)
    	{
    		perror("open");
    		return 1;	
    	}
    	fprintf(stdout, "hello world!: %d\n", fd);
    	close(fd);
    		
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    close 1 后,1 就不再表示显示器文件,而 open log.txt 后,1 就表示 log.txt 文件,所以 fprintf 并不会往显示器上输出,而是会往 log.txt 里输出,可是 log.txt 中没有内容。通常数据流动过程是:先把语言上的数据写到用户层的缓冲区 ➡ 然后数据通过文件描述符在操作系统内核中,找到自己的 task_struct ➡ 然后通过 task_struct 中的 struct files_struct* files 指针找到 struct files_struct 中 struct files* fd_array[] 以文件描述符为下标的位置 ➡ 然后再通过下标的内容找到要写的 struct_file,并把用户层缓冲区的数据拷贝到内核层缓冲区 ➡ 操作系统再由自己的刷新策略和时机通过磁盘驱动刷新到磁盘设备。注意因为用的是 C,所以这里的用户层缓冲区是 C 提供的,如果是其它语言,那么用的缓冲区就是其它语言提供的。所以之所以操作系统没有由用户层把数据刷新到内核层是因为现在 1 指向的是磁盘文件。显示器是行刷新策略,磁盘是全缓冲策略,这两种策略既可以被用户层采纳,也可以被内核层采纳。

    上层只要把数据写到用户层缓冲区中就不用管了,剩下的就由操作系统来完成,所以对用户来讲,就完成了用户层和内核层之间的完全解耦。而用户要自己拷贝数据到内核层,还需要提升权限,效率太低。

    所以用户层中存在缓冲区可以让用户和底层之间的差异屏蔽掉,以此来提升效率。同理内核层中存在缓冲区也有着解耦、提高效率的意义。


    💦 1.2 测试用例2

    #include  
    #include
    #include
    #include
    #include
    #include
    
    int main()
    {
        //c call
        printf("hello printf\n");
        fprintf(stdout, "hello fprintf\n");
        fputs("hello fputs\n", stdout);
        //system call
        const char* byh = "hello write\n";
        write(1, byh, strlen(byh));
                                           
        fork();
    
        return 0;                          
    }                                      
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注意观察,当我们重定向之后,为什么除了write以外都打印了两次呢?
    请添加图片描述

    我们发现往显示器上输出的结果是合理的,但是往普通文件上输出的结果却很诡异,它输出了 7 条信息,且使用 C 语言接口的都输出了两次,使用系统调用接口的输出了一次。

    我们就能理解重定向后,刷新策略由行刷新变为全缓冲,也就是说 fork 时,数据还在 C 缓冲区中,所以重定向后,C 接口的数据输出了两份;而向显示器输出时,因为显示器的刷新策略是行刷新,且这里的每条数据都有 \n,所以每执行完 printf,数据就立马刷新出来,最后 fork 时便无意义了。

    而重定向后,系统接口没有受影响的原因是 write 会绕过语言层缓冲区,写到内核层缓冲区,而其实只要是数据都要写时拷贝,但大部分情况只针对用户数据,对于内核数据,数据属于操作系统不会写时拷贝,属于进程会写时拷贝,但这种情况很少考虑,现在我们就认为写时拷贝主要拷贝的是用户数据。

    通常我们不建议所语言接口和系统接口混合使用,因为可能会出现一些难以理解的现象。


    💨 2. dup2

    • 输出重定向
    #include
    #include
    #include
    #include
    #include
    #include
    
    //输出重定向
    int main()
    {
        int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
        if(fd < 0)
        {
            perror("open");
            return 1;
        }
    
        dup2(fd, 1);//此时再写入就不是标准输出,而是fd                             
        const char* msg = "hello dup2->output\n";
        int i = 0;
        while(i < 5)
        {
            write(1, msg, strlen(msg));
            i++;
        }
    
        close(fd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    请添加图片描述

    • 输入重定向
    #include
    #include
    #include
    #include
    #include
    #include
    
    //输入重定向
    int main02()
    {
        int fd = open("log.txt", O_RDONLY);
        if(fd < 0)
        {
            perror("open");
            return 1;
        }
    
        dup2(fd, 0);//此时再读就不是从标准输入读,而是fd
        char buffer[1024];
        ssize_t sz = read(0, buffer, sizeof(buffer) - 1);
        if(sz > 0)                                       
        {
            buffer[sz] = 0;
            printf("%s", buffer);
        }
    
        close(fd);
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    请添加图片描述

    • 追加重定向
    #include
    #include
    #include
    #include
    #include
    #include
    
    //追加重定向  
    int main03()
    {
        int fd = open("log.txt", O_WRONLY|O_APPEND);
        if(fd < 0)
        {
            perror("open");
            return 1;
        }
    
        dup2(fd, 1);//此时再写入就不是标准输出,而是fd
        const char* msg = "hello dup2->append\n";
        int i = 0;
        while(i < 5)
        {
            write(1, msg, strlen(msg));
            i++;
        }                                            
    
        close(fd);
    
        return 0;                                    
    }                                                
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    请添加图片描述
    echo 是一个进程;“hello world” 默认是调用 printf 或 write 往显示器上输出;log.txt 是调用 open 使用 O_WRONLY|O_CREAT 打开;> 是调用 dup2,将默认标准输出 1 的内容改为 log.txt;

    << 就是 dup2(fd, 0),且 open 文件的方式是 O_RDONLY;

    >>同 >,都是 dup2(fd, 1),只不过它打开文件的方式是 O_WRONLY|O_APPEND;

    将来 fork,创建子进程,子进程会以父进程的大部分数据为模板,子进程进行程序替换时,并不会影响曾经打开的文件,也就不会影响重定向对应的数据结构。


    💨 3. 理解文件系统

    上文咱们一直在谈论打开的文件,那如果一个文件没有被打开呢?它静静的躺在磁盘上。那咱们现在就要了解一下磁盘上的文件系统。首先要了解磁盘结构,这有助于咱们形象理解“把数据刷新到磁盘”这种话,而不是一听而过。

    💦 3.1 了解磁盘结构

    请添加图片描述
    磁盘是计算机中的一个机械设备。

    这个磁盘的盘片就像光盘一样,数据就在盘片上放着,只不过光盘是只读的,磁盘是可读可写的。

    机械硬盘的寻址的工作方式:盘片不断旋转,磁头不断摆动,定位到特定位置。

    请添加图片描述
    类比磁带,我们可以把磁盘盘片想象成线性结构

    站在OS角度,我们就认为磁盘是线性结构,要访问某一扇区,就要定位数组下标LBA(logic block address);要写到物理磁盘上,就要把LBA地址转化成磁盘的三维地址(磁头,磁道,扇区)。这种关系类似于我们之前的虚拟地址空间和物理内存。

    请添加图片描述


    💦 3.2 inode

    为了能解释清楚inode我们先简单了解一下文件系统

    在这里插入图片描述
    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一个新文件来看看如何工
    作。

    [root@localhost linux]# touch abc
    [root@localhost linux]# ls -i abc
    
    • 1
    • 2

    为了说明问题,我们将上图简化:

    在这里插入图片描述
    请添加图片描述

    创建一个新文件主要有一下4个操作:

    1. 存储属性
      内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
    2. 存储数据
      该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
      复制到300,下一块复制到500,以此类推。
    3. 记录分配情况
      文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
    4. 添加文件名到目录

    新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

    创建文件:遍历inode Bitmap位图中找0,申请一个未被使用的inode,填入属性信息。并把这个映射关系写到当前目录的Data blocks中。

    查看目录:根据目录inode找到与其映射的文件名

    向文件写入:遍历block Map找到若干未被使用的块儿,将该文件的inode与这些blocks建立映射关系,再向blocks中写入内容。

    查看文件内容:cat hello.c → 查看当前目录lesson15的data Blocks数据块儿 → 找到映射关系:文件名儿对应的inode编号 → 在inode Table中找到inode → 找到对应的blocks[] → 打印文件内容。 查看文件属性类似。

    理解目录
    我们知道程序员定位一个文件,是通过绝对路径或相对路径定位的,但不管是绝对路径还是相对路径最终一定是要有一个目录。目录当然是一个文件,也有独立的 inode,也有自己的数据块,目录中的 block 数组能找到对应的数据块,目录的数据块维护的是文件名和 inode 的映射关系。换言之,在目录下创建文件时,除了给文件申请 inode、数据块之外,还要把文件名和申请创建成功之后文件的 inode 编号写到目录的数据块中。所以现在就能理解为什么大多数操作系统下同一个目录中不允许存在同名文件。所以只要我们找到了目录就可以找到文件名,根据映射然后可以找到文件 inode,通过 inode 读取文件的属性,也可以通过 inode 中的数组读取文件的内容。所以 ls -l 时就可以读到文件的属性信息,它是在当前目录对应的 inode 下找到对应数据块中文件名和文件名映射的 inode,再去找对应文件的 inode,此时就看到文件的属性了。

    理解删除文件

    之前我们说过,计算机中删除一个文件并不是真正的删除,而是把那块空间标识为无效,现在理解的是不用把 inode 属性清空,不用把 inode 对应的数据块清空,只要把两个位图中对应的比特位由 1 到 0,再把所在的目录下中的对应的映射关系去掉,此时空间就是无效的,下一次再新建文件时,就可以直接把无效的空间覆盖。

    删除后的文件当然可以恢复,Windows 下的回收站就是一个目录,当你删除时就是把文件移动到回收站目录下,移动时就是把其它目录下数据块中的映射关系移动到回收站目录下的数据块中。Windows 下就算把回收站的内容删除也是能恢复的,Linux 下,如果要恢复删除的文件是有一些恢复工具的,但有可能在恢复过程中,创建各种临时文件,可能就会把想恢复的文件的信息覆盖掉,你想自己恢复删除的文件,就需要更深入的了解文件系统原理。


    💨 4. 软硬链接

    💦 4.1 软链接

    请添加图片描述

    ln -s text.txt soft_link
    
    • 1

    给text.txt建立软链接 soft_link,text.txt 的 inode是921928,soft_link的inode是 921926,也就是说软链接 soft_link 就是一个普通的正常文件,有自己独立的inode,soft_link 中的数据块中保存着它指向的文件 text.txt 的路径,就类似于 Winodws 下的快捷方式,比如桌面看到的软件保存的是其它的路径,在系统中可能你要运行的可执行程序在一个很深的目录下,就可以在较上层的目录中建立软链接。

    ——简称快捷方式


    💦 4.2 硬链接

    请添加图片描述

    ln text.c hard_link
    
    • 1

    建立硬链接,硬链接和它链接的文件的 inode 是一样的,硬链接没有独立的 inode,所以严格来说硬链接不是一个文件,硬链接本质就是在 text.cpp 文件所在目录的数据块中重新创建一个映射关系,也就是给 text.cpp 的 inode 重新起了一个别名,我们发现了链接后的 text.cpp 的有一个属性信息由 1 变为 2,所以这里 ls -l 显示的这一列数据表示的不是软链接,而是硬链接。

    请添加图片描述为什么创建普通目录的硬链接是 2 ?创建普通文件的硬链接是 1 ?—— 普通文件是 1 好理解,因为当前目录中只包含一组 file 和 file 的 inode;

    请添加图片描述
    而普通目录是 2 的原因是因为除了当前目录下包含了 dir 和 dir 的 inode,还有 dir 目录下中隐藏的 " . ",这个点叫做当前路径,此时我们发现这个点的 inode 和 dir 的 inode 是一样的,所以 dir 的 inode 编号是 2。 这个点就是 dir 的别名,因为当前路径的使用频率很高,所以它是为了方便我们对当前路径的索引

    请添加图片描述
    我们再在 dir 下建立一个目录 mytext,此时 dir 的硬链接数就变成了 3,mytext 的硬链接数就是 2。—— mytext 的是 2 能理解,因为 other inode 和 . inode;

    请添加图片描述
    而 dir 之所以是 3,是因为要 " cd … ",所以 mytext 下还有一个点点,它是 dir 的别名。

    所以硬链接最典型的应用场景就是方便进行路径转换。


  • 相关阅读:
    tcpdump
    微服务 第二章 CountDownLatch和Semaphone的应用
    《痞子衡嵌入式半月刊》 第 63 期
    算法通关村第二关|白银|链表反转拓展【持续更新】
    【CSS】CSS实现三角形(一)
    IO流的学习1
    【AGC】开放式测试示例
    为什么建议框架源码学习从Mybatis开始?能说这么清楚的,少见了
    Flink学习笔记(四):Flink 四大基石之 Window 和 Time
    基于Layabox引擎的魔塔微信小游戏设计与实现
  • 原文地址:https://blog.csdn.net/m0_60338933/article/details/127681953