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 库函数和系统调用
类型 | 示例函数 | 所属层级 | 特点 |
---|---|---|---|
库函数 | fopen , fclose , fread , fwrite | C标准库(libc) | 1. 提供更高级的抽象 2. 带缓冲区 3. 可移植性更好 4. 最终会调用系统调用 |
系统调用 | open , close , read , write , lseek | 操作系统接口 | 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(),直接写入内核;
重定向,改变了刷新方式,普通文件,满了刷新,可是没慢,也没有强制刷新,程序退出时,刷新,父子进程各刷新一份。