> 技术文档 > Linux的基础I/O

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 extern FILE *stdin; // 标准输入,键盘文件extern FILE *stdout; // 标准输出,显示器文件extern FILE *stderr; // 标准错误,显示器文件

3、系统文件I/O

3.1 一种传标志位的方式

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

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

注意:

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

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

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

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

3.3 库函数和系统调用

类型 示例函数 所属层级 特点 库函数 fopenfclosefreadfwrite C标准库(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 #include #include #include #include #include  // 添加read/write所需的头文件int main(){ char buf[1024]; // 从标准输入(文件描述符0)读取数据 ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留1字节给结尾的\\0 if(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 #include #include #include #include  // 添加 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 #include #include #include #include  // 添加 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 #include #include #include #include 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 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 #include #include #include #include #include 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值应该是1 close(fd); return 0;}

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

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

#include #include #include #include #include #include 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值应该是1 fflush(stdout); // 强制刷新 close(fd); return 0;}

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

现象2:
#include #include #include  // 添加 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 printfhello fwritehello write

显示器,行刷新;

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

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

hello writehello printfhello fwritehello printfhello fwrite

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

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