飞道的博客

【Linux修炼】12.深入了解系统文件

425人阅读  评论(0)

每一个不曾起舞的日子,都是对生命的辜负。

一. 重新谈论文件

1. 共识的问题

在这篇正式开始之前,大家要有一些共识性的问题:

  1. 空文件也要在磁盘占据空间
  2. 文件 = 内容 + 属性
  3. 文件操作 = 对内容+对属性+对内容和属性的操作
  4. 标定一个问题,必须使用:文件路径+文件名【唯一性】
  5. 如果没有指明对应的文件路径,默认是在当前路径(进程当前路径)进行文件访问
  6. 当我们把fopen、fclose、fread、fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作有没有被执行呢?没有,对文件的操作的本质是进程对文件的操作,因此没有运行不存在进程,所以不会被执行。
  7. 一个文件如果没有被打开,可以直接进行文件访问吗?不能!一个文件要被访问,就必须先被打开!(打开的方式:用户进程+OS)

那是不是所有磁盘的文件都被打开呢?显然不是这样!因此我们可以将文件划分成两种:a.被打开的文件b.没有被打开的文件 。对于文件操作,一定是被打开的文件才能进行操作,本篇文章只会讲解被打开的文件。

  • 文件操作的本质:进程和被打开文件的关系

2. 重谈C语言文件操作

2.1 概要

  1. 语言有文件操作接口、C++有文件操作接口,Java、Python、php、go、shell都有文件操作接口,他们实际上的底层都是相同的函数接口,因为都需要通过OS调用。
  2. 文件在磁盘上,磁盘是硬件,只有操作系统有资格访问,所有人想访问磁盘都不能绕过操作系统,必须使用操作系统调用的接口,即OS会提供文件级别的系统调用接口。
  3. 所以上层语言无论如何变化,库函数底层必须调用系统调用接口。
  4. 库函数可以千变万化,但是底层不变,因此这样能够降低学习成本—>学习不变的东西。

2.2 C语言文件实操

复习一下:下面fp按顺序对应以下三个操作依次:写入文件、打印文本信息、追加文本信息到文件中。

  • 细节问题:以w方式单纯的打开文件,c会自动清空内部的数据。

对于C语言调用的fopen打开文件,实际上底层调用的是操作系统的接口open,其他语言也是这样,只不过语言级别的接口是多了一些特性,下面就看看:man 2 open

对于flag标记位,一般来说对于C语言,一个int类型代表一个标记位,那如果要传10个标记位呢?对于整形来说,实际上有32个比特位,那是不是可以将每一个比特位赋予特定的含义,通过比特位传递选项,从而实现对应的标记呢?一定是可以的。因此在介绍open函数之前,先来介绍一下标记位的实现:

  • 注意:一个比特位一个选项,不能重复。(标记位传参)

因此我们再看这个open函数,就明白了是什么含义,就是通过不同的flags,传入不同的标记位,那接下来看看open函数怎么用:

2.3 OS接口open的使用(比特位标记)

不废话,我们知道了上面用的头文件就直接开始使用了:

int open(const char* pathname, int flags )
int open(const char* pathname, int flags, mode_t mode )

第一个函数是在文件已经存在的基础上使用的,如果不存在源文件,那么就需要用第二个函数,即第二个函数如果文件不存在就会自动创建文件。

这样就能创建出权限为0666的log.txt了。
fd的值也是有讲究的,这里看一下fd的值:

这个值暂时记住,下面将会讲到。

2.4 写入操作

对于C语言来讲,除了打开关闭,还有写入fwrite等函数接口,因此对于OS也存在一个接口:write

无论这个buf是什么类别,在OS看来都是二进制!至于这个类别是文本还是图片,都是由语言本身决定的。

下面开始:

可以看出,对于C语言中的w,封装了文件接口的标识符:O_WRONLY(写)O_CREAT(不存在就创建文件)O_TRUNC(清空文件),以及权限。

2.5 追加操作

想要把清空变成追加,只需要将open内部的最后一个清空标识符改成追加的标识符:

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);

2.6 只读操作

将标识符换成读的标识符

int fd = open(FILE_NAME, O_RDONLY);

这就是以上接口:open/close/write/read,分别对应C语言的fopen/fclose/fwrite/fread,此外对于读还有lseek,对应C语言的fseek。

系统调用接口 对应的C语言库函数接口
open fopen
close fclose
write fwrite
read fread
lseek fseek

即库函数接口是封装了系统调用接口的,所有语言的库函数都存在系统调用的影子。

二. 如何理解文件

  • 文件操作的本质:进程和被打开文件的关系

1. 提出问题

进程可以打开多个文件,那是不是意味着系统中一定会存在大量的被打开的文件,被打开的文件要不要被操作系统管理起来呢?答案是一定的。

那么OS如何管理呢? 先描述,再组织。因此操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构标识文件:struct file{}(包含了文件的大部分属性) 因此将结构体链式链接,通过找到链表的首地址从而实现对链表内容的增删查改。

同时创建多个文件并打印其返回值:

  1. 为什么从3开始,0、1、2呢?
  2. 连续的小整数->数组->数组下标

在回答这个问题之前,我们需要了解三个标准的输入输出流:stdin,stdout,stderr!

FILE* fp = fopen();

这个FILE实际上是一个结构体,而对于上面的三个输入输出流,实际上也是FILE的结构体:

对于这个结构体必有一个字段–>文件描述符,下面就看一下这个文件描述符的值是什么:

2. 文件描述符fd

  • 通过对open函数的学习,我们知道了文件描述符就是一个小整数,即open的返回值

因此这也就解释了为什么文件描述符默认是从3开始的,因为0,1,2默认被占用。我们的C语言的这批接口封装了系统的默认调用接口。同时C语言的FILE结构体也封装了系统的文件描述符。

那为什么是0,1,2,3,4,5……呢?下面就来解释:

PCB中包含一个files指针,他指向一个属于进程和文件对应关系的一个结构体:struct files_struct,而这个结构体里面包含了一个数组叫做struct file* fd _array[]的指针数组,因此如图前三个0、1、2被键盘和显示器调用,这也就是为什么之后的文件描述符是从3开始的,然后将文件的地址填入到三号文件描述符里,此时三号文件描述符就指向这个新打开的文件了。

再把3号描述符通过系统调用给用户返回就得到了一个数字叫做3,所以在一个进程访问文件时,需要传入3,通过系统调用找到对应的文件描述符表,从而通过存储的地址找到对应的文件,文件找到了,就可以对文件进行操作了。因此文件描述符的本质就是数组下标。

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

三. 文件fd的分配规则

直接看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
   
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

输出发现是 fd: 3

关闭0或2,再看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
   
    close(0);
    //close(2);
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
   
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    close(fd);
    return 0;
}

 

发现是结果是: fd: 0 或者 fd 2

因此,我们知道了,文件fd的分配规则就是将这个array数组从小到大,按照循序寻找最小的且没有被占用的fd,这就是fd的分配规则。

四. 重定向

1. 什么是重定向

对于上面的例子,我们关闭了文件描述符0和2对应的文件吗,那么如果关闭1呢?

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
   
    close(1);
    int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
    if(fd < 0){
   
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    fprintf(stdout, "fd :%d\n", fd);//与上面打印是一样的功能
    close(fd);
    return 0;
}

 

根据上面讲到的文件描述符的分配规则,这段代码我们按照顺序进行解释:

首先关闭文件描述符1所对应的stdout(标准输出:输出到显示器),然后通过f分配,这个文件的fd会从小到大扫描发现1的位置没有被使用,于是就会将这个新创建的文件myfile与对应的指针进行连接:因此当我们的printf以及fprintf打印到stdout时,由于上层的文件描述符stdout对应的还是1,就会在内核中找到array[1]中对应的文件进行操作,但此时1对应的已经不是标准输出到显示器,而是myfile文件,因此我们在打印时也就不会在显示器中看到fd的值,而是在myfile文件中。

那我们来看看是不是这样:

在log.txt没有打印是由于缓冲区的问题,在fprintf的下面加上:fflush(stdout);再看看:

即当所有现象都符合我们的预期时,这种现象就是重定向。

  • 重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file*的地址。

常见的重定向有:>(输入), >>(追加), <(输出)。

2. dup2 系统调用的重定向

在上面演示的无论是分配规则还是重定向,直接以close关闭的操作非常的挫,因为这样的close操作不够灵活,所以现在介绍一个系统调用的重定向接口:dup2

int dup2(int oldfd, int newfd);//newfd的内容最终会被oldfd指向的内容覆盖
  • dup2的返回值也就是fd的文件描述符,失败返回-1

那就来演示一下刚才的功能:打印到文件里

可以发现,这样操作简化了刚才的操作,另外,fd的值也不会被改变。

输出重定向演示完了,那我们就可以实现我们刚才提到的三个重定向剩下的追加、输入重定向了。

1. 追加重定向

2. 输入重定向

上面是从键盘中读取,如果不想从键盘读,我们可以重定向到向指定文件中读取:

3. 理解:>、>>、<

在之前的学习中,我们模拟过shell部分功能的实现,在这里为了理解这三个常见的重定向,用shell模拟实现这三个重定向:代码链接:lesson18/myshell/myshell.c · 每天都要进步呀/Linux - 码云 - 开源中国 (gitee.com)

此外,这几个常见的重定向的使用方法:文章链接

  • 注:文件是共享的,不会因为进程不同而权限不同,因为文件是磁盘上的,与进程之间是独立的。即当子进程被创建并且发生写时拷贝时,原来的文件并不会再次被拷贝一次。

五. 如何理解Linux一切皆文件

一张图描述:

即我们利用虚拟文件系统就可以摒弃掉底层设备之间的差别,统一使用文件接口的方式进行文件操作。

文件的引用计数:(1条消息) Linux文件引用计数的逻辑_sherlock-wang的博客-CSDN博客


转载:https://blog.csdn.net/NEFUT/article/details/128576198
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场