【linux基础I/O(一)】文件系统调用接口&文件描述符详谈

🎬 个人主页:HABuo

📖 个人专栏:《C++系列》 《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》

⛰️ 如果再也不能见到你,祝你早安,午安,晚安


目录

📚一、C语言的文件接口

📚二、文件相关共识问题

📚三、操作文件的系统调用接口

[📖3.1 open](#📖3.1 open)

[📖3.2 close](#📖3.2 close)

[📖3.3 write](#📖3.3 write)

[📖3.4 read](#📖3.4 read)

📚四、文件描述符

[📖4.1 文件描述符的本质](#📖4.1 文件描述符的本质)

📚五、总结


前言

前几篇博客我们认识了进程控制的相关知识,如何创建进程、退出进程有哪些信息需要给父进程、进程等待是什么、进程程序替换做了那些事?如果记忆模糊请返回继续阅读。本篇博客我们将进入文件部分相关知识的了解和学习当中!无论是C语言或者C++亦或是Java都拥有文件操作,但是它们的使用还都不一样,让人挺恼火,有没有什么根本的东西来应对呢?当然有,在冯诺依曼体系结构那篇博客中我们知道,用户级接口是在系统调用接口基础上实现的,那么是不是就意味着我们把文件操作的系统调用接口熟悉了,那一切妖魔鬼怪也就都消散了,没错!所以让我们一探究竟吧!

本章重点:

本篇文章着重讲解I/O的四个系统调用接口 ,以及文件描述符fd的认识与fd的本质,在此之前,会先复习一下C语言的文件相关的库函数


📚一、C语言的文件接口

我们将C语言的文件操作接口在这里进行大总结:

  • C打开文件: fopen
  • C的读取: fread, fscanf, fgets
  • C的写入: fwrite, fprintf, fputs
  • C关闭文件: fclose

整理成表格如下:

函数 主要功能 参数说明 返回值 头文件
fopen 打开文件 filename:文件名, mode:模式 成功:FILE*, 失败:NULL stdio.h
fclose 关闭文件 stream:文件指针 成功:0, 失败:EOF stdio.h
fgetc 读取字符 stream:文件指针 成功:字符, 失败/EOF:EOF stdio.h
fputc 写入字符 character:字符, stream:文件指针 成功:字符, 失败:EOF stdio.h
fgets 读取字符串 str:缓冲区, n:最大长度, stream:文件指针 成功:str, 失败/EOF:NULL stdio.h
fputs 写入字符串 str:字符串, stream:文件指针 成功:非负值, 失败:EOF stdio.h
fprintf 格式化写入 stream:文件指针, format:格式串, ...:参数 成功:写入字符数, 失败:负值 stdio.h
fscanf 格式化读取 stream:文件指针, format:格式串, ...:参数 成功:匹配项数, 失败/EOF:EOF stdio.h
fread 二进制读取 ptr:缓冲区, size:项大小, count:项数, stream:文件指针 成功读取的项数 stdio.h
fwrite 二进制写入 ptr:数据区, size:项大小, count:项数, stream:文件指针 成功写入的项数 stdio.h
fflush 刷新缓冲区 stream:文件指针 成功:0, 失败:EOF stdio.h

📚二、文件相关共识问题

经过之前知识的学习,我们先要有以下共识:

  • 空文件,也要在磁盘中占据空间
  • 文件 = 内容 + 属性
  • 文件操作 = 对内容 + 对属性 or 对内容和属性
  • 标定一个文件,必须使用:文件路径+文件名(唯一性)
  • 如果没有指明对应的文件路径,默认是在当前路径进行文件访问
  • 当我们把fopen、fclose、fread、fwrite等接口写完之后,代码编译之后,形成了二进制可执行程序之后,但是没运行,文件对应操作是没有被执行的,因此对文件的操作本质是进程对文件的操作
  • 一个文件如果没有被打开,不可以直接进行文件访问,即一个文件要被访问,就必须先被打开

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

什么叫做当前路径:

我们在命令行解释器中:这一串路径我们很清楚它就是当前路径,但是我们仅仅知道它就是路径,我们并不知道它真正的是什么,请看下图:

可以看到实际上当前路径就是该进程文件下cwd文件,它指明了当前进程执行的是磁盘路径下的哪一个程序!因此对于上篇博客我们实现的cd命令,在父进程不执行的情况下,cd命令是无效的,这正是因为,我们修改的是子进程文件下的cwd路径,父进程文件下的并没有修改,因此我们pwd查看时依然查看的是父进程的路径!

没有被打开的文件怎么办?

我们得到文件操作本质是:进程和被打开文件的关系,没有被打开的文件属于文件系统,我们将在接下来的博客中进行介绍!

文件操作:

C语言有文件操作接口,C++有文件操作接口,Java有,Python,php,go,shell都有,而且它们的操作接口都不一样!但是就如我们在前言当中所说的那样,我们怎么以不变应万变?

cpp 复制代码
文件在哪里  
--->  磁盘  
--->  磁盘属于硬件   
--->  所有人想访问磁盘(硬件)不能绕过OS  
--->  使用OS提供文件级别的系统调用接口
--->  语言级别在系统调用接口基础上封装供人们使用 

所以我们清楚了,学习了OS提供的文件级别的系统调用接口,语言级别的就是小儿科,你不就是在我的基础上做的封装嘛!

📚三、操作文件的系统调用接口

一共四个函数:

  1. open: 打开文件
  2. close: 关闭文件
  3. write: 向文件写入
  4. read: 从文件中读取

我们一个一个来进行介绍:

📖3.1 open

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

这个flag比较特殊,虽然它是整型,但是内部却当作了位图在使用,即传递过来的选项,会被当作位图中的不同位,通过判断某位是否为1来查看是否有这个选项,怎么理解这个宏,请看下面这个巧妙的代码:

cpp 复制代码
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
void show(int flags)
{
    if(flags & ONE) printf("ONE\n");
    if(flags & TWO) printf("TWO\n");
    if(flags & THREE) printf("THREE\n");
    if(flags & FOUR) printf("FOUR\n");
}
int main()
{
    show(ONE);
    show(TWO);
    show(ONE | TWO);
    show(ONE | TWO | THREE);
    show(ONE | TWO | THREE | FOUR);
}

通过位的操作实现了不同选项执行不同功能的一段代码!那么你对open里面的那个宏选项相信你会更加理解,事实就是通过这样的一种方式实现的!

cpp 复制代码
flags:打开方式标志(必须包含以下之一):
    O_RDONLY:只读
    O_WRONLY:只写
    O_RDWR:读写
可选标志(通过按位或|组合):
    O_CREAT:文件不存在则创建
    O_TRUNC:先清零再进行写入
    O_APPEND:追加模式
mode:创建文件时的权限(八进制数,如0644)


返回值:
    成功:返回文件描述符(非负整数)
    失败:返回-1,设置errno

代码示例:

cpp 复制代码
// 只读打开现有文件
int fd = open("file.txt", O_RDONLY);
// 创建新文件(读写权限),权限为rw-r--r--
fd = open("new.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
// 以追加模式打开文件
fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);

📖3.2 close

cpp 复制代码
#include <unistd.h>
int close(int fd);

参数:

  • fd:要关闭的文件描述符

返回值:

  • 成功:返回0

  • 失败:返回-1,设置errno

代码示例:

cpp 复制代码
int fd = open("test.txt", O_RDONLY);
if (fd >= 0) {
    // 使用文件...
    close(fd);  // 关闭文件
}

📖3.3 write

cpp 复制代码
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数:

  • fd:文件描述符

  • buf:要写入的数据缓冲区

  • count:要写入的字节数

返回值:

  • 成功:返回实际写入的字节数

  • 失败:返回-1,设置errn

代码示例:

cpp 复制代码
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
const char *text = "Hello, World!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
if (bytes_written == -1) {
    perror("写入失败");
}
close(fd);

📖3.4 read

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

参数:

  • fd:文件描述符

  • buf:存放读取数据的缓冲区

  • count:要读取的字节数

返回值:

  • 成功:返回实际读取的字节数

  • 到达文件尾:返回0

  • 失败:返回-1,设置errno

cpp 复制代码
int fd = open("data.txt", O_RDONLY);
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
    // 处理读取的数据
    write(STDOUT_FILENO, buffer, bytes_read);
}
if (bytes_read == -1) {
    perror("读取失败");
}

📚四、文件描述符

上面我们fd,fd的使用的快起兴,但是它是什么呢?让我们来认识一下:

首先先看下述推导链:

cpp 复制代码
进程可以打开多个文件吗?当然可以
--->  系统中一定会存在大量的被打开的文件
--->  被打开的文件,要不要被OS管理起来呢?
--->  如何管理?-先描还,在组织
--->  操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构表示文件
--->  struct_file与C语言当中的FILE没有关系
--->  包含了文件的大部分属性

上述内容想说明的就是OS为每个文件都抽象出了一个struct_file结构体来描述文件(被打开的),管理这些被打开的文件也就变成了对这些结构体的管理!

我们看下述代码的现象:

cpp 复制代码
int fd1 = open("/mnt/workspace/test/test5/text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n",fd1,fd2,fd3,fd4);

带来下述问题:

  • 0、1、2哪去了?
  • 为什么是3、4、5、6连续的数字?

📖4.1 文件描述符的本质

通过上面的推导与演示,我们来认识文件描述符的本质,请看下图:

为了证明stdin、stdout、stderr占用了012三个文件描述符,请看下述代码:

cpp 复制代码
printf("stdin->%d\n", fileno(stdin));
printf("stdout->%d\n", fileno(stdout));
printf("stderr->%d\n", fileno(stderr));
int fd1 = open("/mnt/workspace/test/test5/text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n",fd1,fd2,fd3,fd4);

那按照这个意思,我们关闭其中一个再建立一个新文件就能用012了?对的,请看下述代码:

cpp 复制代码
printf("stdin->%d\n", fileno(stdin));
close(0);
printf("stdout->%d\n", fileno(stdout));
printf("stderr->%d\n", fileno(stderr));
int fd1 = open("/mnt/workspace/test/test5/text1.txt", O_WRONLY | O_CREAT);
int fd2 = open("/mnt/workspace/test/test5/text2.txt", O_WRONLY | O_CREAT);
int fd3 = open("/mnt/workspace/test/test5/text3.txt", O_WRONLY | O_CREAT);
int fd4 = open("/mnt/workspace/test/test5/text4.txt", O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n", fd1, fd2, fd3, fd4);

所以通过上述的演示,我们能得到一下结论:

  • 文件描述符就是files_struct结构体里的一个指针数组的下标,这个数组中的指针指向struct_file,而files_struct是被PCB所维护的
  • 文件描述符的分配就是按照从小到大,按照顺序寻找最小的且没有被占用的fd

📚五、总结

本篇博客我们介绍学习了系统调用接口、以及文件描述符相关知识,并对C语言的文件操作接口进行了回顾。

小结一下:

系统调用接口:

open:

  • 参数:"文件名", 执行选项(O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_TRUNC、O_APPEND), 权限
  • 返回值:成功:文件描述符,失败:-1

close:

  • 参数:文件描述符
  • 返回值:成功:0,失败:-1

write:

  • 参数:write(要写入文件的文件描述符, 写入的内容(void *buf), 要写入的字节数)
  • 返回值:成功:实际写入的字节数,失败:-1

read:

  • 参数:read(要读取文件的文件描述符, 存放读取数据的缓冲区(void *buf), 要读取的字节数)
  • 返回值:成功:实际读取的字节数。读到结尾返回0。失败:-1

文件描述符:

是什么:就是files_struct中fd_array指针数组的下标,整体的关系:PCB通过指向files_struct,files_struct结构体通过fd_array指针数组指向对应的struct_file即文件!

文件描述符的分配规则:按照从小到大,按照顺序寻找最小的且没有被占用的fd。系统运行时天然打开了stdin、stdout、stderr文件,因此文件描述符我们自己操作时会从3开始

相关推荐
拾光Ծ2 小时前
【Linux】一切皆文件:深入理解文件与文件IO
linux·c语言·运维开发·系统编程·重定向·linux开发·文件io
biubiubiu07062 小时前
Devops(gitlab和jenkins)安装
运维·devops
J_liaty2 小时前
客户端负载均衡与服务端负载均衡解释与对比
java·运维·负载均衡
2401_891450462 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
windows_62 小时前
MISRA C:2004 逐条分析
c语言
梦想的旅途22 小时前
企微API自动化:外部群消息高效推送
运维·自动化·企业微信
先生先生3932 小时前
docker/linux
linux·运维·服务器
独隅2 小时前
Ollama 在 Linux 上的完整安装与使用指南:从零部署到熟练运行大语言模型
linux·运维·语言模型
小y要自律2 小时前
08 string容器 - 字符串比较
开发语言·c++·stl