【基础IO】-3-系统级别的文件操作

系统级别的文件操作

所谓系统级别的文件操作,其实就是Linux中与文件操作有关的系统调用。

1、学习系统调用

Linux有一些文件操作相关的系统调用,这些系统调用往往被封装在了上方库函数中,形成了我们在C语言见到的文件操作库函数(fopen fwrite fprintf ...)

1.1、open




c 复制代码
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);



系统调用open,顾名思义,就是用来打开文件的。

const char *pathname

这个参数,与fopen一样,都是填文件所处的路径,

  • 只输入文件名,open就去"当前路径"下找同名文件;
  • 输入指定路径,open就去指定的路径下找同名文件。

返回值







open的返回值,

  • 打开文件成功,返回文件描述符(fd);
  • 打开文件失败,返回-1,同时设置errno

int flags

这个参数,其实是一种"标志位",表示的是文件的打开模式。

我们可以观察到,Linux内部提供了一些宏以输入到flags参数位置上:




c 复制代码
O_RDONLY
O_WRONLY
O_APPEND
O_CREAT
...



要理解这些宏的含义,以及宏如何使用以能够设定文件的打开方式,我们可以先来看一个小demo:




c 复制代码
#include <stdio.h>

#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)

void Print(int flags)
{
    if (flags & ONE) 
        printf("ONE ");
    if (flags & TWO) 
        printf("TWO ");    
    if (flags & THREE) 
        printf("THREE ");
    if (flags & FOUR) 
        printf("FOUR ");
    if (flags & FIVE) 
        printf("FIVE ");

    printf("\n");
}

int main()
{
    Print(ONE);
    Print(ONE | TWO);
    Print(ONE | TWO | THREE);
    Print(ONE | TWO | THREE | FOUR);
    Print(ONE | TWO | THREE | FOUR | FIVE);

    return 0;
}






我们通过定义宏和位操作,将多个选项"融合"为一个变量flags,传入Print;Print中再对flags做解析,执行不同的选项。这样我们就做到了用一个变量接收多个选项(标志位)的目标。实际上这样的方法,借助了位图的思想:







O_RDONLY O_WRONLY O_APPEND O_CREAT O_TRUNC等宏,就是只有一个比特位是1的整型值,就像上面 demo 中定义的ONE TWO THREE FOUR FIVE。

我们想以哪些方式打开文件,就将哪些宏处理成一个值(按位或),传入flags;open内部就会进行解析,从而按照我们想要的方式打开文件。比如我们想要按照"创建新文件、只写、追加"的方式打开文件,我们就可以这样传参:




c 复制代码
open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);



标志位,一般表示的是 是 / 否 的问题。比如我们想以"创建新文件、只写"的方式打开文件,那么"创建新文件、只写"的标志位就是"是",其它标志位就是"否"。如果选项有多少就定义多少标志位,那么就会出现变量太多、空间浪费的情况。所以我们可以借助位图的思想,采取"用一个变量接收多个选项",来节省空间、提高效率。

mode_t mode

这个参数,表示的是文件的权限。

只有在文件不存在,需要新建文件的时候,才需要这个参数。因为已经存在的文件,权限是已经确定好了的。

对于不存在的文件,我们就需要新建同名文件。假设我们设置权限为"0666"(对拥有者、所属组、other都是rw):




c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    int fd = open("log.txt", O_CREAT | O_WRONLY, 0666); 
    // 没有 O_CREAT ,且文件不存在的时候, open 会执行失败
    // 一定要写 0666 ,不能写 666 。因为权限码是八进制数!
    if (fd < 0)
    {
        perror("open failed!\n");
        exit(1);
    }

    return 0;
}






但是,我们再看一眼log.txt的权限为:




不是 666 ,而是 664 !

想到了什么?权限掩码!文件的最终权限是:
最终权限 = 起始权限 & (~权限掩码)

我们在原来代码基础上,再加一行语句:




c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
    umask(0);

    int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
    if (fd < 0)
    {
        perror("open failed!\n");
        exit(1);
    }

    return 0;
}






代码中写umask(0),其实是在程序内部将权限掩码设置为了 0000 。我们看到结果是log.txt的权限是 0666 ,明显使用的是权限掩码 0000 。

所以,Linux中设定好的umask,和程序中重新设定的umask,优先采用程序中设定的umask。(就近原则)

1.2、其它系统调用

read ···>




c 复制代码
ssize_t read(int fd, void *buf, size_t count);



参数:

  • fd:文件描述符
  • buf:指向接收读取内容的位置(缓冲区)
  • count:接收读取内容位置的大小

返回值:

  • 如果成功,返回读取到的字节数;
  • 如果失败,返回-1。



write ···>




c 复制代码
ssize_t write(int fd, const void *buf, size_t count);



参数:

  • fd:文件描述符
  • buf:指向要写入文件的内容
  • count:写入文件内容的大小

返回值:

  • 成功,返回写入的字节数;
  • 失败,返回-1。



更多的系统调用,我们可能会在接下来的学习中遇到。

1.3、利用所学系统调用,简单模拟fopen的打开模式,进而读写文件

w模式:

  • 文件不存在,新建文件;
  • 清空文件历史内容;
  • 从文件的开始处写入(数组下标index = 0处开始)。

用open模拟是这样的:




c 复制代码
open("log.txt", O_CREAT | O_TRUNC | O_WRONLY, 0666);



用系统调用完成:仿照fopen中的w模式,打开文件做写入。

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int fd = open("log.txt", O_CREAT | O_TRUNC | O_WRONLY, 0666);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }

    const char *str = "Hello world.\n";
    int cnt = 9;
    while (cnt--)
        write(fd, str, strlen(str));

    close(fd);

    return 0;
}



a模式:

  • 文件不存在,新建文件;
  • 不会清空文件历史内容;
  • 从文件末尾开始写入。

用open模拟是这样的:




c 复制代码
open("log.txt", O_CREAT | O_APPEND | O_WRONLY, 0666);



用系统调用完成:仿照fopen中的a模式,打开文件做追加写入。

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int fd = open("log.txt", O_CREAT | O_APPEND | O_WRONLY, 0666);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }

    const char *str = "Hello world.\n";
    int cnt = 10;
    while (cnt--)
        write(fd, str, strlen(str));

    close(fd);

    return 0;
}



r模式就是读取文件的模式,可以将读取到的内容输入到指定位置(缓冲区)。

用open模拟是这样的:




c 复制代码
open("log.txt", O_RDONLY);



用系统调用完成:仿照fopen中的r模式,打开文件做内容读取。

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    // if (argc != 2)
    // {
    //     printf("Usage: %s filename\n", argv[0]);
    // }

    int fd = open("log.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(1);
    }

    char inbuffer[100001];
    // while (1)
    // {
    //     int n = read(fd, inbuffer, sizeof(inbuffer) - 1);
    //     if (n > 0)
    //     {
    //         inbuffer[n] = 0; // 最后一个位置放 \0
    //         printf("%s", inbuffer);
    //     }
    //     else if (0 == n)
    //     {
    //         printf("end of file!\n");
    //         break;
    //     }
    //     else
    //     {
    //         perror("read");
    //         exit(2);
    //     }
    // }

    int n = read(fd, inbuffer, sizeof(inbuffer) - 1);
    inbuffer[n] = 0; // 最后一个位置放 \0
    printf("%s", inbuffer);

    close(fd);

    return 0;
}



2、文件描述符(fd)

上面学习open等系统调用时,我们都见到了文件描述符fd。

事实上,用户无法直接访问操作系统里的内核数据结构,必须通过系统调用。而与文件操作相关的系统调用,几乎都需要文件描述符fd:

  • open打开文件成功后,返回该文件的文件描述符;
  • write需要传入文件描述符参数;
  • read需要传入文件描述符参数;
  • ......

那么,我们就可以粗浅地下一个定论:文件操作的过程中,操作系统只认文件描述符fd。

还记得吗?一些上方的C语言库函数,底层其实是封装了系统调用。

  • 文件操作相关的C语言库函数,封装了文件操作相关的系统调用------函数的封装
  • C函数能够使用的、描述文件的数据结构,肯定也要系统能够使用的、描述文件的数据结构------数据结构的封装

我们知道,系统能够使用的、描述文件的数据结构是文件描述符fd。那么C函数能够使用的、描述文件的数据结构,是什么呢?FILE




c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    FILE *pf = fopen("log.txt", "w");
    printf("%d\n", pf->_fileno); // FILE封装了fd
    fclose(pf);

    int fd = open("log.txt", O_RDONLY);
    printf("%d\n", fd);
    close(fd);

    return 0;
}






实际上,FILE是一个结构体,其中包含了文件描述符fd这个成员变量。即FILE封装了文件描述符fd。

我们将回答下面四个问题,来进一步理解文件描述符fd。

2.1、为什么从3开始?0、1、2哪去了?

小贴士1


对文件进行操作,需经过"读、改、写"三大步骤:

  • 读:将文件加载到内存中,打开文件,读取文件内容;
  • 改:进行对文件内容的修改;
  • 写:按照实际需求,刷新文件内容,将磁盘上的文件做更新,然后关闭文件。

一个系统可以同时创建多个进程,那么一个进程能否同时打开多个文件?答案是可以的。




c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("%d\n", fda);
    printf("%d\n", fdb);
    printf("%d\n", fdc);

    close(fda);
    close(fdb);
    close(fdc);

    return 0;
}



打开多文件的同时,我们很好奇:比如我们同时打开了三个文件loga.txt logb.txt logc.txt,那么打开这三个文件后获得的文件描述符,分别是多少呢?




分别是:3、4、5

那么问题来了:文件描述符fd,从 3 开始?0、1、2 哪去了?




c 复制代码
#include <stdio.h>

int main()
{
    printf("%d\n", stdin->_fileno);
    printf("%d\n", stdout->_fileno);
    printf("%d\n", stderr->_fileno);

    return 0;
}






还记得吗:程序运行(进程创建)的时候,默认打开了三个流:

  • stdin:标准输入流
  • stdout:标准输出流
  • stderr:标准错误流

查阅这三个流:

c 复制代码
//SYNOPSIS
       #include <stdio.h>

       extern FILE *stdin;
       extern FILE *stdout;
       extern FILE *stderr;



这三个流,本质都是FILE*量,那么底层一定也封装有文件描述符fd。

2.2、为什么是连续的?

在 2.1 中我们发现,文件描述符 0、1、2 分别对应stdin stdout stderr。又因为进程创建的时候,默认打开了这三个流。所以我们可以合理猜测,接下来进程打开文件(流)的时候,文件描述符就是从 3 开始。

我们可以进一步猜测:文件描述符,也许是数组下标?

2.3、为什么可以表示文件?

就像操作系统需要对多个进程进行管理一样,操作系统也需要对多个文件进行管理------先描述,再组织。

所以Linux内部,一定有一个数据结构,描述文件:







文件 = 属性 + 内容,那么文件结构体file里一定会包括属性和内容:




我们很容易能发现,结构体file中也有内嵌的链接字段struct list_head。所以创建出来的多个文件就能以内嵌指针的方式连接在一起:




文件与文件之间倒是联系起来了。但是进程如何找到文件呢?

实际上,进程PCB中,保存了一个数据结构files_struct:




深入探讨files_struct,发现其中还包含一个数组:

c 复制代码
struct files_struct {
	// ...
	struct file * fd_array[NR_OPEN_DEFAULT]; // 数组
};



数组fd_array的类型,就是文件结构体file的指针。




那么一切就明朗了:

打开文件后,文件结构体file的指针就会存入数组fd_array:




进程想要打开指定的文件,只需拿着下标,在数组fd_array中找到对应的file*指针,就能找到指定的文件,这就是系统调用open:




进程想要通过read读取文件,只需要拿着下标,找到对应的文件,进而找到文件的内容即可;进程想要通过write向文件写入内容,只需要拿着下标,找到对应的文件,进而找到文件的内容,对文件内容进行修改即可。

进程想要关闭文件,只需

  • 刷新文件内容到磁盘上;
  • 释放文件结构体file的空间;
  • 将fd_array对应下标存储的file*指针置空。

到这里,我们就可以回答"文件描述符fd为什么可以表示文件":

  • 进程通过文件描述符fd,访问file*数组对应下标位置上存储的file*指针,进而访问到文件结构体file,最终访问到文件的内容和属性。

2.4、fd的本质

有了 2.3 的认识,我们就可以回答fd的本质是什么:

  • 文件描述符fd,本质上就是一个数组下标(index)。fd是进程PCB中,类型为files_struct的结构体(指针形式存在)存储的fd_array数组的下标。

其中,我们称struct file *fd_arrar[];为打开文件表。

2.5、fd的分配规则

对于"fd是数组fd_array的下标"这一结论,有人就会有疑问:

  • 如果关闭了一个文件,再新打开一个文件 ,那么这个新打开的文件,文件file*指针存放在fd_array的哪个位置?
    • 找到当前最大的已经存入指针的下标,然后直接放到它的下一位?
    • 找到未存入指针的最小的下标,然后放进去?

我们可以做一个实验:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    close(0); // 关闭1号stdin流
    int fd = open("log.txt", O_CREAT, 0666);
    printf("log.txt: %d\n", fd);
    close(fd);

    return 0;
}






答案是后者。

所以文件描述符fd的分配规则是:

  • 新打开文件,统一分配fd_array中未存file*指针的 ,下标最小的fd给该文件。

3、将系统调用与C语言进行关联

C语言函数对系统调用进行了封装,C语言定义的FILE对文件描述符fd进行了封装。那么为什么要"封装"呢?直接用系统调用实现方法不就行了吗?

原因是:封装,使得语言具有了跨平台性。

不同平台的系统调用是不一样的,比如Linux的系统调用就和windows的系统调用不一样。C语言为了能够让更多的人使用,这些人包含,

  • Linux开发者
  • windows开发者
  • 其它系统开发者

就必须考虑到"不同平台的系统调用不同"这一点。所以C语言对于一种上方的库函数,会实现不同系统不同系统调用封装而成的版本,比如printf函数会实现Linux系统调用封装成的版本,和windows系统调用封装成的版本,以及其它系统系统调用封装成的版本。

C语言这么做,完善了C语言本身的生态,使得C语言至今仍是世界上使用最广泛的语言。而一些语言,比如汇编语言,由于没有做好跨平台性,已逐渐不再被更多人使用,直至淘汰。

相关推荐
下辈子不要当码农1 小时前
Linux驱动(2)
linux·运维·单片机
m0_587383001 小时前
碰碰好评卡功能落地实战 从需求梳理到上线部署全流程实操指南
java·linux·服务器·前端·javascript·vue.js·人工智能
筑梦之路1 小时前
Alibaba Cloud Linux 4 制作 Ansible RPM包实践(离线可复用)——筑梦之路
linux·运维·ansible
承渊政道1 小时前
Linux网络学习【应用层自定义协议与序列化实战:从手写序列化与反序列化到深入理解进程间关系与守护进程】
linux·网络·学习·ubuntu·json·序列化与反序列化
无垠的广袤1 小时前
【ED-CLAWBOX】智能家居枢纽
linux·物联网·docker·智能家居·树莓派·aiot·home assistant
玩AI的奶茶1 小时前
什么情况适合租算力?一张场景对照表,加三个自测问题
大数据·linux·服务器·数据库·人工智能·python
IT利刃出鞘1 小时前
Linux权限--adduser和useradd的区别
linux·运维·服务器
天意__1 小时前
Android手机上部署Linux服务器 在Termux里安装Ubuntu
android·linux·服务器
Ruiery1 小时前
Linux 6.6内核内存管理深度解析(五):page cache(页缓存)— 读缓存在内存、写延迟回写
linux·运维·服务器·缓存