当前位置: 首页 > news >正文

详细介绍:09.【Linux系统编程】“文件“读写操作,Linux下一切皆文件!

详细介绍:09.【Linux系统编程】“文件“读写操作,Linux下一切皆文件!

目录

  • 1. 理解"文件"
    • 1.1 什么是文件(狭义理解)
    • 1.2 什么是文件(广义理解)
    • 1.3 文件操作的归类认知
    • 1.4 系统角度
  • 2. 回顾C文件接口(内存级打开文件)
    • 2.1 文件创建路径
    • 2.2 myfile.c写文件
    • 2.3 myfile.c读文件
    • 2.4 输出信息到显示器的不同方式
    • 2.5 stdin & stdout & stderr
    • 2.6 打开文件的不同方式
  • 3. 系统文件I/O
    • 3.1 系统文件I/O入口,open函数的使用
    • 3.2 open传递flags标志位的方法(每个bit位各为一种功能标志,通过‘|’传递多个标志)
    • 3.3 C调用Linux系统函数write写文件
    • 3.4 C调用Linux系统函数read读文件
    • 3.5 open函数理解(系统调用&库函数)
    • 3.6 文件描述符fd
      • 3.6.1 特殊文件描述符0 & 1 & 2
      • 3.6.2 文件描述符的分配规则
    • 3.7 文件管理(task_struct→files_struct管理文件)
      • 3.7.1 了解 struct file数组 - 存放文件指针
      • 3.7.2 struct file中为何要存在struct list_head?
    • 3.8 重定向
      • 3.8.1 重定向概念
      • 3.8.2 使用dup2系统调用实现重定向
    • 3.9 文本写入 VS 二进制写入(系统层面只有二进制方式)
    • 3.10 关于fopen对open的封装(可跨平台移植原因)

1. 理解"文件"

1.1 什么是文件(狭义理解)

1.2 什么是文件(广义理解)

  • Linux 下一切皆文件(键盘、显示器、网卡、磁盘…… 这些都是抽象化的过程)(后面会讲如何去理解)

1.3 文件操作的归类认知

1.4 系统角度

  • 访问文件,需要先打开文件!谁打开文件?谁对文件进行操作?

    • 进程打开的文件,对文件的操作本质是进程对文件的操作
  • 磁盘的管理者是操作系统。

  • 文件的读写本质不是通过 C 语言 / C++ 的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的。

  • 操作系统管理文件:先描述,再组织!

2. 回顾C文件接口(内存级打开文件)

2.1 文件创建路径

#include <stdio.h>int main(){FILE *fp = fopen("log.txt", "w");if(!fp){printf("fopen error!\n");}while(1);fclose(fp);return 0;}

打开的log.txt文件在哪个路径下?

  • 在程序的当前路径下,那系统怎么知道程序的当前路径在哪里呢?

  • 可以使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息,其中:

    • cwd:指向当前进程运行目录的一个符号链接。
    • exe:指向启动当前进程的可执行文件(完整路径)的符号链接。

打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此OS就能知道要创建的文件放在哪里。

2.2 myfile.c写文件

#include<stdio.h>#include<string.h>int main(){FILE *fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}const char *msg = "hello world: ";int cnt = 1;while(cnt <= 10){char buffer[1024];snprintf(buffer, sizeof(buffer), "%s%d\n", msg, cnt++);fwrite(buffer, strlen(buffer), 1, fp);}fclose(fp);return 0;}

2.3 myfile.c读文件

// myfile.c
// gcc -o myfile myfile.c
// ./myfile filename
#include<stdio.h>#include<string.h>// cat myfile.txtint main(int argc, char *argv[]){// 只有一个参数,直接返回if(argc != 2){printf("Usage: %s filename\n", argv[0]);return 1;}// 两个参数,第二个参数是要查看的文件FILE *fp = fopen(argv[1], "r");if(NULL == fp){perror("fopen");return 2;}//读文件内容并打印1while(1){char buffer[128];memset(buffer, 0, sizeof(buffer));// fread返回读到的元素个数,sizeof(buffer)-1中的-1是为了保存\0int n = fread(buffer, 1, sizeof(buffer)-1, fp);if(n > 0){printf("%s", buffer);}if(feof(fp))// 判断是否到文件末尾break;}fclose(fp);return 0;}

2.4 输出信息到显示器的不同方式

#include<stdio.h>#include<string.h>int main(){printf("hello world\n");fprintf(stdout, "hello fprintf\n");const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);return 0;}

2.5 stdin & stdout & stderr

• C默认会打开三个输入输出流,分别是stdin, stdout, stderr

• 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针

#include <stdio.h>extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;

2.6 打开文件的不同方式

文件使用方式含义(读(输入)从文件读到程序,写(输出)从程序写到文件)如果指定文件不存在
“r”(只读)为了输入(写)数据,打开一个已经存在的文本文件出错
“rb”(只读)为了输入(写)数据,打开一个二进制文件出错
“r+”(读写)为了读和写,打开一个文本文件出错
“rb+”(读写)为了读和写打开一个二进制文件出错
“w”(只写)为了输出(读)数据,清空并打开一个文本文件建立一个新的文件
“wb”(只写)为了输出(读)数据,清空并打开一个二进制文件建立一个新的文件
“w+”(读写)为了读和写,清空并打开一个文本文件建立一个新的文件
“wb+”(读写)为了读和写,清空并打开一个二进制文件建立一个新的文件
“a”(追加)打开一个文本文件,向文件尾写数据建立一个新的文件
“ab”(追加)打开一个二进制文件,向文件尾写数据建立一个新的文件
“a+”(读写)打开一个文本文件,在文件尾部进行读写建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾部进行读和写建立一个新的文件

3. 系统文件I/O

打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件IO之前,先要了解下如何给函数传递标志位,该方法在系统文件IO接口中会使用到

3.1 系统文件I/O入口,open函数的使用

// 函数原型
#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);// pathname - 要打开或创建的目标文件路径// flags - 标志位(核心参数)使用位掩码组合,主要分为三类:// mode - 权限模式(仅在使用 O_CREAT 时需要)// 返回值://		成功:新打开的文件描述符//		失败:-1#include <unistd.h>int close(int fd);	// 关闭文件// 参数:fd文件描述符,即open函数的返回值
  • flags - 标志位分类
分类标志位含义
文件访问模式(必选其一)O_RDONLY只读模式
O_WRONLY只写模式
O_RDWR读写模式
文件创建和状态标志(可组合)O_CREAT文件不存在时创建
O_EXCL与O_CREAT一起使用,文件必须不存在
O_TRUNC如果文件存在且为普通文件,将其长度截断为0
O_APPEND追加模式(每次写操作前定位到文件末尾)
文件状态标志O_NONBLOCK非阻塞模式
O_SYNC同步写入(数据立即写入磁盘)
常用组合功能组合方式对应C函数模式
创建、清空并写入`int fd = open(“log.txt”, O_CREATO_WRONLY
创建、追加并写入`int fd = open(“log.txt”, O_CREATO_WRONLY
读文件int fd = open("log.txt", O_RDONLY);"r"
  • mode - 文件权限设置参考文中的第4部分:02.【Linux系统编程】Linux权限(root超级用户和普通用户、创建普通用户、sudo短暂提权、权限概念、权限修改、粘滞位)-CSDN博客

    • 可以通过umask设置程序中新建文件的权限掩码(使用示例见3.3)

      #include <sys/types.h>#include <sys/stat.h>mode_t umask(mode_t mask);//umask函数只是修改当前进程下创建的文件的掩码,并不改变系统的掩码

3.2 open传递flags标志位的方法(每个bit位各为一种功能标志,通过‘|’传递多个标志)

使用整型的32个bit位,每个bit位各为一种功能标志,举例如下,给Print函数传不同参数则执行函数中不同的功能。

#include <stdio.h>#define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001#define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010#define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100#define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000void Print(int flags){if (flags & ONE_FLAG)   printf("One!\n");if (flags & TWO_FLAG)   printf("Two\n");if (flags & THREE_FLAG) printf("Three\n");if (flags & FOUR_FLAG)  printf("Four\n");}int main(){Print(ONE_FLAG);    printf("\n");Print(ONE_FLAG | TWO_FLAG);    printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);    printf("\n");Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);    printf("\n");Print(ONE_FLAG | FOUR_FLAG);    printf("\n");return 0;}
One!
One!
Two
One!
Two
Three
One!
Two
Three
Four
One!
Four

3.3 C调用Linux系统函数write写文件

操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面功能一模一样的代码。

#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>int main(){umask(0);	// 修改文件权限掩码int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);//int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);//int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);int cnt = 5;const char *msg = "hello world\n";while(cnt){write(fd, msg, strlen(msg)); //fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据cnt--;}close(fd);return 0;}

3.4 C调用Linux系统函数read读文件

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>int main(){umask(0);// int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);// int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);int fd = open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);while(1){char buffer[64];int n = read(fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;printf("%s", buffer);}else if(n == 0){break;}}close(fd);return 0;}

3.5 open函数理解(系统调用&库函数)

在认识返回值之前,先来认识一下两个概念: 系统调用和库函数

  • 上面的fopenfclosefreadfwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

  • openclosereadwritelseek 都属于系统提供的接口,称之为系统调用接口

回忆一下我们讲操作系统概念时,画的一张图

系统调用接口和库函数的关系,一目了然。所以,可以认为, f开头 系列的函数,都是对系统调用的封装,方便二次开发。
在这里插入图片描述

3.6 文件描述符fd

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

3.6.1 特殊文件描述符0 & 1 & 2

• Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0(stdin), 标准输出1(stdout), 标准错误2(stderr).

• 0,1,2对应的物理设备一般是:键盘,显示器,显示器

所以输入输出还可以采用如下方式:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <string.h>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;}

在这里插入图片描述
而现在知道,文件描述符就是从0开始的小整数。

当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。

而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!

  • 所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

(其他问题见3.7:既然有fd文件描述符,那么file类型的文件指针中为什么还要存在list_head结构体?)

3.6.2 文件描述符的分配规则

直接看代码:

#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
  • 结论(文件描述符的分配规则):在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。(当然也可以使用close关闭三个标准文件输入/输出,则再次新建文件则同样遵循此规则)

举例:

#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <stdlib.h>int main(){umask(0);int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd1 < 0) exit(1);if(fd2 < 0) exit(1);if(fd3 < 0) exit(1);if(fd4 < 0) exit(1);// FILE 结构体确实封装了文件描述符 fd,成员为int _fileno; // ✅ 封装的文件描述符fdprintf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n",stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return 0;}
stdin: 0
stdout: 1
stderr: 2
fd1: 3
fd2: 4
fd3: 5
fd4: 6

3.7 文件管理(task_struct→files_struct管理文件)

3.7.1 了解 struct file数组 - 存放文件指针

struct task_struct {
// ...
struct files_struct *files;  // 文件管理结构
// ...
};
struct files_struct {
struct file __rcu * fd_array[NR_OPEN_DEFAULT];  // ✅ 文件指针数组
//// 数组的每个元素都是:struct file __rcu *   // 带RCU注解的file结构体指针
// ...
};

3.7.2 struct file中为何要存在struct list_head?

  • struct files_struct 结构体中可以通过下标fd来找到指定的文件指针struct file,那么为什么struct file结构体中还有struct list_head指针将各个文件链接起来呢?

fd_array[] - 进程视角

  • 目的:让单个进程快速访问自己打开的文件
  • 用法read(fd, ...)fd_array[fd] → 找到文件
  • 技术:数组索引,O(1)时间复杂度
  • 场景:系统调用 read(fd), write(fd)

各种list_head - 系统视角

  • 目的:让内核管理系统中的所有文件关系
  • 用法:文件系统维护、inode引用管理、资源清理等
  • 技术:链表遍历,维护系统关系
  • 场景:文件系统卸载、inode引用管理、资源清理

3.8 重定向

3.8.1 重定向概念

​ 那如果关闭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);fflush(stdout);close(fd);exit(0);}
  • 此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile当中,其中,fd=1。这种现象叫做输出重定向
  • 命令行指令中,常见的重定向有: > , >> , <
    在这里插入图片描述

3.8.2 使用dup2系统调用实现重定向

函数原型如下:

#include <unistd.h>int dup2(int oldfd, int newfd);
  • dup2的功能:就是修改files_struct结构体中保存文件指针的 指针数组 struct file的内容,将oldfd下标中的内容拷贝newfd下标中。

示例:

#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_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd, 1);printf("fd:%d\n", fd);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");const char *msg = "hello world\n";write(fd, msg, strlen(msg));return 0;}
$ cat log.txt
hello world
fd:3
hello printf
hello fprintf
  • printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1,但此时,fd:1下标所表示内容,已经变成了log.txt的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向同理。

3.9 文本写入 VS 二进制写入(系统层面只有二进制方式)

  • 文本写入即字符写入

  • 都是语言层面,系统层面只有二进制方式,其他都转成二进制方式写入。

3.10 关于fopen对open的封装(可跨平台移植原因)

http://www.hskmm.com/?act=detail&tid=20817

相关文章:

  • 数据类型-元组
  • BindingList的应用与改进
  • 谷歌 SEO 新词 xx animate 等实操教程
  • 完整教程:【读书笔记】架构整洁之道 P6 实现细节
  • Print Conductor打印软件安装教程!一款非常好用的批量打印软件!支持PDF、Word、Excel、图片等
  • Python 面向对象编程基础:类与对象初体验
  • 面向对象的设计原则
  • 反电动势法控制BLDC电机的原理图分析
  • 完整教程:Altium Designer(AD)设计规则检查设置
  • 企业物联网安全必须优先考虑的5个不可否认的理由
  • PSM敏捷认证自考学习指南
  • 2025内网聊天工具排行 4款好用的内网聊天软件推荐
  • 独立开发在线客服系统手记:实现对 PostgreSQL 的支持,以及与 MySQL 的对比
  • 方言普通话识别大模型,支撑中英+202种方言识别
  • ffmpeg一些使用记录,防止忘记
  • BLE从机(20)BLE区分主机(IOS/安卓/WIN)
  • Windows 驱动开发基础
  • 基于MATLAB实现基于距离的离群点检测算法
  • 国产DevOps工具链的突围之路:Gitee如何重塑企业研发效能
  • 阿里云抵御CC攻击利器边缘安全加速ESA
  • 生产者-消费者问题
  • Manim实现闪电特效
  • QAction的使用
  • Gitee:中国开发者生态的数字化转型加速器
  • 大模型提示词技巧Prompt Engineering,看这一篇就够了 - 知乎
  • sg.测试 PySimpleGUI 取值方法
  • Gitee DevOps:本土化基因驱动中国企业研发效能革命
  • 快速查看Navicat数据库连接密码实战
  • 老旧系统接入统一认证
  • 每周读书与学习-初识JMeter 元件(三)