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

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 什么是文件(狭义理解)

  • 文件在磁盘里。

  • 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。

  • 磁盘是外设(即是输出设备也是输入设备)。

  • 对磁盘上文件的所有操作的本质,都是对外设的输入和输出 简称 IO。

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

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

1.3 文件操作的归类认知

  • 0KB 的空文件是占用磁盘空间的(文件属性占空间)。

  • 文件是文件属性(元数据)和文件内容的集合(文件 = 属性(元数据)+ 内容)。

  • 所有的文件操作本质是文件内容操作和文件属性操作。

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.txt
int 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!
TwoOne!
Two
ThreeOne!
Two
Three
FourOne!
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 files_struct 结构体来管理的,而struct files_struct结构体编程又存放在struct tast_struct进程控制块中。在Linux内核的task_struct中。

  • struct files_struct 结构体中有一个struct file __rcu *指针数组,下标就是文件描述符fd,内容是文件指针

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的封装(可跨平台移植原因)

  • C库封装系统调用,条件编译实现跨平台

    • 在C语言库中,fopen封装了Linux系统对文件操作的open函数,所以可以使用fopen来完成Linux系统下对文件的操作。
    • 同样的,C语言库中也封装了windows系统中对文件操作的函数,上层也封装成fopen函数。在其他系统中同样,他们使用条件编译来区分不同系统。由此就可以实现C语言文件在不同系统中的移植,即上层都是使用fopen对文件操作,底层由C语言库分别适应不同的操作系统。
  • 目的

    • 目的是提高可移植性和开发效率
    • 技术需求、市场需求、标准推动的共同结果。而是技术进步和经济效益的平衡
http://www.dtcms.com/a/418940.html

相关文章:

  • SkyVLN: 城市环境中无人机的视觉语言导航和 NMPC 控制;香港科技大学
  • 【React 状态管理深度解析:Object.is()、Hook 机制与 Vue 对比实践指南】
  • react-lottie动画组件封装
  • 哈尔滨网站建设吕新松做搜索引擎网站
  • PostgreSQL 流复制参数 - synchronous_commit
  • BPEL:企业流程自动化的幕后指挥家
  • 企业网站开发一薇设计说明英语翻译
  • 搭建 Nexus3 私服并配置第三方 Maven 仓库(阿里云等)和优先级
  • JVM 深入研究 -- 详解class 文件
  • Apache Airflow漏洞致敏感信息泄露:只读用户可获取机密数据
  • 第十六周-基本量子3
  • 手机微网站怎么制作缪斯国际设计董事长
  • 在 Spring Cloud Gateway 中实现跨域(CORS)的两种主要方式
  • SQL Server从入门到项目实践(超值版)读书笔记 27
  • 【Git】项目管理全解
  • rdm响应式网站开发企业年报网上申报流程
  • 昆山开发区网站制作网站建设文档模板
  • PySide6调用OpenAI的Whisper模型进行语音ASR转写
  • 网站怎么被黑磁力蜘蛛
  • nginx反向代理和负载均衡
  • 外贸seo外贸推广外贸网站建设外贸网站建设网站域名信息查询
  • 新广告法 做网站的python和c++学哪个好
  • 数据科学与数据分析:真正的区别是什么?
  • default-route-advertise always 概念及题目
  • Python爬虫实战:获取东方财富网CPI信息与数据分析
  • Filebeat写ElasticSearch故障排查思路(上)
  • 网站开发进度安排文档青岛关键词优化排名
  • C# TCP 服务端与客户端代码分析与补充
  • 族蚂建站郴州网站建设费用价格
  • 对象分配在哪块内存?