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

Linux的基础I/O

目录

1、理解“文件”

1.1 狭义理解

1.2 广义理解

1.3 文件操作的归类认知

1.4 系统角度

2、回顾C文件接口

2.1 文件的打开与关闭

2.2 文件的读写函数

2.3 stdin & stdout & stderr

3、系统文件I/O

3.1 一种传标志位的方式

3.2 文件的系统调用接口

3.2.1 open()

3.2.2 read() & write() & close()

3.3 库函数和系统调用

3.4 文件描述符fd

3.4.1 0 & 1 & 2

3.4.2 文件描述符的分配规则

3.4.3 重定向

3.4.4 重定向系统调用dup2()

4、理解一切皆文件

5、缓冲区

5.1 缓冲区的定义

5.2 缓冲区的作用

5.3 缓冲区的机制

现象1:

现象2:


1、理解“文件”

1.1 狭义理解

  • 文件在磁盘里
  • 磁盘是永久性存储介质,因此文件在磁盘上永久性存储
  • 磁盘是外设(即是输出设备也是输入设备)。
  • 对磁盘文件的所有操作(如读取、写入)本质上都是对外设的输入/输出,简称I/O(Input/Output)。

1.2 广义理解

  • Linux中,一切皆文件(键盘、显示器、网卡、磁盘……这些都是抽象化的过程)(后面会深入理解)。

1.3 文件操作的归类认知

  • 文件 = 属性(元数据)+ 内容
  • 对于0KB的空文件是占用磁盘空间的,有文件属性。
  • 所有的文件操作本质文件内容操作文件属性操作

1.4 系统角度

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

文件分为“内存级(被打开)”文件“磁盘级(未打开)”文件

本节主讲“内存级(被打开)”文件

2、回顾C文件接口

2.1 文件的打开与关闭

FILE *fopen(const char *path, const char *mode);

mode含义文件不存在时文件存在时写入方式
"r"只读返回 NULL正常打开不可写入
"r+"读写返回 NULL正常打开从当前位置覆盖
"w"只写(新建)新建文件清空原内容从头写入
"w+"读写(新建)新建文件清空原内容从头写入
"a"追加(只写)新建文件保留内容,追加到末尾只能末尾追加
"a+"追加(读写)新建文件保留内容,可读/追加写入可读,但写入仅限末尾

int fclose(FILE *fp);

注意:

ls /proc/[ 进程 id] -l 命令,查看当前正在运行进程的信息。

  • cwd:指向进程的当前工作目录创建文件和打开文件的默认路径
  • exe:指向启动当前进程的可执行文件的路径

2.2 文件的读写函数

函数名功能描述适用流类型参数说明返回值备注
fgetc从流中读取单个字符所有输入流
(如stdin、文件)
FILE *stream(文件指针)读取的字符(int
失败返回EOF
通常用于逐字符处理
fputc向流写入单个字符所有输出流
(如stdout、文件)
int char(字符)
FILE *stream
写入的字符(int
失败返回EOF
fgets从流中读取一行文本所有输入流char *str(缓冲区)
int n(最大长度)
FILE *stream
成功返回str
失败返回NULL
保留换行符\n
fputs向流写入一行文本所有输出流const char *str(字符串)
FILE *stream
成功返回非负值
失败返回EOF
不自动添加换行符
fscanf格式化输入(类似scanf所有输入流FILE *stream
const char *format(格式字符串)
...(变量地址)
成功匹配的参数数量
失败返回EOF
需注意缓冲区溢出风险
fprintf格式化输出(类似printf所有输出流FILE *stream
const char *format
...(变量值)
成功返回写入字符数
失败返回负值
fread二进制输入(块读取)文件流void *ptr(缓冲区)
size_t size(每块大小)
size_t nmemb(块数)
FILE *stream
实际读取的块数用于结构体等二进制数据
fwrite二进制输出(块写入)文件流const void *ptr(数据地址)
size_t size
size_t nmemb
FILE *stream
实际写入的块数

注意:

写字符串,不用写\0,因为这是C语言的规定,不是文件的规定,写进去会乱码。 

2.3 stdin & stdout & stderr

C程序启动默认打开三个输入输出流,分别是stdin,stdout,stderr

#include <stdio.h>extern FILE *stdin;  // 标准输入,键盘文件
extern FILE *stdout; // 标准输出,显示器文件
extern FILE *stderr; // 标准错误,显示器文件

3、系统文件I/O

3.1 一种传标志位的方式

使用位图,用比特位作为标志位。

#include <stdio.h>#define ONE     (1 << 0)  // 0000 0001 (二进制)
#define TWO     (1 << 1)  // 0000 0010 (二进制)
#define THREE   (1 << 2)  // 0000 0100 (二进制)void func(int flags) {if (flags & ONE)   printf("flags has ONE! ");if (flags & TWO)   printf("flags has TWO! ");if (flags & THREE) printf("flags has THREE! ");printf("\n");
}int main() {func(ONE);                // 输出: flags has ONE!func(THREE);              // 输出: flags has THREE!func(ONE | TWO);          // 输出: flags has ONE! flags has TWO!func(ONE | TWO | THREE);  // 输出: flags has ONE! flags has TWO! flags has THREE!return 0;
}

3.2 文件的系统调用接口

man 2 系统调用,有具体说明。

3.2.1 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:打开文件时的选项标志,可以使用以下常量通过"或"运算(|)组合:

必须指定且只能指定一个的选项:

  • O_RDONLY只读打开。

  • O_WRONLY只写打开。

  • O_RDWR读写打开。

可选标志:

  • O_CREAT:若文件不存在则创建它(需要mode参数,设置新文件的访问权限)。

  • O_APPEND追加写模式。

  • O_TRUNC:如果文件已存在且为普通文件,打开时会将其长度截断为0逻辑上的清空(类似与vector的size)

return value:

  • 成功:返回新打开的文件描述符fd非负整数

  • 失败:返回-1

注意:

那么C语言的fopen的flag就是:

“r” = O_RDONLY;

“w” = O_CREAT | O_WRONLY | O_TRUNC;

“a” = O_CREAT | O_WRONLY | O_APPEND;

“r+” = O_RDWR;

“w+” = O_CREAT | O_RDWR | O_TRUNC;

“a+” = O_CREAT | O_RDWR | O_APPEND。

3.2.2 read() & write() & close()

类比C文件相关接口。

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
fd:文件描述符
buf:存储读取数据的缓冲区
count:请求读取的字节数
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符
buf:包含待写入数据的缓冲区
count:请求写入的字节数
#include <unistd.h>int close(int fd);
fd:要关闭的文件描述符

注意:

read()和write()的buf都是void*,不关心数据格式,以二进制流输入输出。

那么为什么语言层,有字符流的输入输出?

  • 首先,底层都是二进制流的输入输出。
  • 字符按ASCII输入(读出),按ASCII输出(写入)。对于字符设备,字符通过ASCII转化成二进制写到里面,然后通过ASCII解释,以字符的形式显示。

字符流的输入输出,是因为,我们输入输出的是字符串

3.3 库函数和系统调用

类型示例函数所属层级特点
库函数fopenfclosefreadfwriteC标准库(libc)1. 提供更高级的抽象
2. 带缓冲区
3. 可移植性更好
4. 最终会调用系统调用
系统调用openclosereadwritelseek操作系统接口1. 直接与内核交互
2. 无缓冲区
3. 效率更高但更底层
4. 与具体操作系统相关

3.4 文件描述符fd

3.4.1 0 & 1 & 2

Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是

标准输入 0标准输出 1标准错误 2

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

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

0,1,2是自动打开的

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>  // 添加read/write所需的头文件int main()
{char buf[1024];// 从标准输入(文件描述符0)读取数据ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留1字节给结尾的\0if(s > 0) {buf[s] = '\0';  // 添加字符串结束符// 将输入内容同时输出到标准输出(1)和标准错误(2)write(1, buf, s);write(2, buf, s);}return 0;
}

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

注意:

C语言的stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一个FILE结构体的指针,FILE结构体里面封装了文件描述符fd,其他语言也一样。

3.4.2 文件描述符的分配规则

直接看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>  // 添加 close 函数所需的头文件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

关闭 fd = 0 或者 fd = 2,再看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>  // 添加 close() 所需的头文件int main()
{close(0);  // 关闭标准输入(文件描述符 0)// close(2);  // 注释掉的关闭标准错误(文件描述符 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

结论:

在 Linux 系统中,文件描述符的分配原则最小的没有被使用下标,作为fd,给新打开的文件

3.4.3 重定向

那如果关闭 fd = 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_CREAT | O_WRONLY | O_TRUNC, 0644);if(fd < 0) {perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

因为语言层只认stdout中的fd = 1,此时下标为1的指针指向myfile,所以

本来应该输出到显示器上的内容,输出到了myfile文件中。

这种现象叫做输出重定向

常见的重定向有: >,>>,<。

输出重定向的本质:

注意:

cat log.txt > myfile,实际上是cat log.txt 1>myfile只重定向了标准输出

cat log.txt 1>myfile 2>&1重定向了标准输出和标准错误

3.4.4 重定向系统调用dup2()
#include <unistd.h>int dup2(int oldfd, int newfd);

oldfd的指针 覆盖  newfd的指针 。

如:dup2(fd,0),实现输入重定向,dup2(fd,1),实现输出重定向。

所以,重定向 = 文件打开方式 + dup2()

4、理解一切皆文件

首先,在 Windows 中是文件的东西,它们在 Linux 中也是文件;其次一些在 Windows 中不是文件的东西,比如进程、磁盘、显示器、键盘这样的硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的 socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。

这样做最明显的好处是,开发者仅需要使用一套 API ,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 PIPE)的操作都可以用 read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。

上图中的外设,每个设备都可以有自己的 read、write,但一定是对应着不同的操作方法!!但通过 struct file 下的 struct file_operations 中的各种函数回调,让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源!!这便是 "Linux 下一切皆文件" 的核心理解。

封装+多态的体现。 

5、缓冲区

5.1 缓冲区的定义

临时存储数据的内存区域。

5.2 缓冲区的作用

提高使用者的效率。

5.3 缓冲区的机制

  • 用户级语言层缓冲区,避免频繁调用系统调用(成本高),提高C语言接口的效率。
  • 文件内核缓冲区,提高系统调用的效率。
  • 可以通过fsync(),将文件内核缓冲区的数据刷新到硬件。
  • 一般认为数据交给OS,就相当于交给硬件。

基于上面的机制,可以理解下面的现象:

现象1:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 关闭标准输出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd);  // 注意:这里打印的fd值应该是1close(fd);return 0;
}

这个时候,对于普通文件,应该是满了刷新,可是没满,也没有强制刷新,然后关闭了fd,在程序退出时,刷新,但fd已经关闭了,刷新不了,所以log.txt中不会有数据。

可以使用 fflush() 强制刷新下缓冲区。

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 关闭标准输出(文件描述符1)close(1);int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0664);if (fd < 0) {perror("open");return 1;}printf("hello world: %d\n", fd);  // 注意:这里打印的fd值应该是1fflush(stdout); // 强制刷新close(fd);return 0;
}

注意:stderr是不带缓冲区,即立即刷新

现象2:
#include <stdio.h>
#include <string.h>
#include <unistd.h>  // 添加 write() 和 fork() 所需的头文件int main() {const char *msg0 = "hello printf\n";const char *msg1 = "hello fwrite\n";const char *msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, 1, strlen(msg1), stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

结果:

hello printf
hello fwrite
hello write

显示器,行刷新;

系统调用write(),直接写入内核。

但是重定向一下 ./hello > file,结果:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

系统调用write(),直接写入内核;

重定向,改变了刷新方式,普通文件,满了刷新,可是没慢,也没有强制刷新,程序退出时,刷新,父子进程各刷新一份。

http://www.dtcms.com/a/273669.html

相关文章:

  • 买小屏幕的时候注意避坑
  • [Java 17] 无模版动态生成 PDF:图片嵌入与动态表格渲染实战
  • Linux磁盘限速(Ubuntu24实测)
  • 算法学习笔记:17.蒙特卡洛算法 ——从原理到实战,涵盖 LeetCode 与考研 408 例题
  • cnpm exec v.s. npx
  • C语言常见面试知识点详解:从入门到精通
  • 亿级流量下的缓存架构设计:Redis+Caffeine多级缓存实战
  • Web安全 - 基于 SM2/SM4 的前后端国产加解密方案详解
  • Flutter优缺点
  • Java学习第三十二部分——异常
  • 【爬虫】- 爬虫原理及其入门
  • 【批量文件查找】如何从文件夹中批量搜索所需文件复制到指定的地方,一次性查找多个图片文件并复制的操作步骤和注意事项
  • 基于Python的豆瓣图书数据分析与可视化系统【自动采集、海量数据集、多维度分析、机器学习】
  • 从Excel到PDF一步到位的台签打印解决方案
  • 学习笔记(34):matplotlib绘制图表-房价数据分析与可视化
  • Java小白-String
  • Allegro 17.4操作记录
  • 平板柔光屏与镜面屏的区别有哪些?技术原理与适用场景全解析
  • 飞算JavaAI:重构Java开发的“人机协同”新范式
  • Python数据读写与组织全解析(查缺补漏篇)
  • 使用Spring Boot和PageHelper实现数据分页
  • 【MySQL】———— 索引
  • 【字节跳动】数据挖掘面试题0016:解释AUC的定义,它解决了什么问题,优缺点是什么,并说出工业界如何计算AUC。
  • 【理念●体系】从零打造 Windows + WSL + Docker + Anaconda + PyCharm 的 AI 全链路开发体系
  • SQL开窗函数
  • 5G IMS注册关键一步:UE如何通过ePCO获取P-CSCF地址
  • 微服务引擎 MSE 及云原生 API 网关 2025 年 6 月产品动态
  • 拓扑排序之 leetcode 207.课程表
  • 突破分子设计瓶颈:融合bVAE与GPU伊辛机的智能优化策略
  • Tomasulo算法是什么?