基础IO
共识原理
- 文件 = 内容 + 属性
- 被打开的文件需要加载到内存中
- 内存中的文件需要被操作系统管理
用户级文件接口
Linux系统调用接口
fd 文件描述符 与访问文件的本质

fd(file descriptor),即文件描述符,下文的系统调用接口经常以fd命名变量,fd是整形变量,作为数组下标,用于管理打开的文件
可以看到,一个进程通过struct files _struct里的指针数组,管理多个同时打开的文件
且每个进程启动时,会默认打开三个文件,且默认fd固定
- stdout
read
所需头文件#include <unistd.h>
声明ssize_t read(int fd, void *buf, size_t count);
参数
- fd即为目标文件的文件描述符
- buf为要从文件读取字节到的内存地址
- count为最大读取字节数
返回值
- 若成功,返回读取文件的字节数,类型为ssize_t,是层层封装的long int
- 若失败,返回-1,并设置errno的值
write
所需头文件#include <unistd.h>
声明ssize_t write(int fd, const void *buf, size_t count);
参数
- fd为目标文件的文件描述符
- buf为要写入文件的- 源内存地址,输入字节数量取决于- count形参
- count为要输入的字节数量,若要输入为字符串,且要输入字符串的全部内容,建议使用- strlen(buf),防止输入- \0,因为对于文件来说,- \0是非法字符
返回值
- 若成功,返回写入文件的字节数,类型为ssize_t,是层层封装的long int
- 若失败,返回-1,并设置errno的值
特别的read函数从文件中读取的是字节内容,不把读取的内容看作字符串,因此,不会自动添加\0在写入buf内容的结尾
用法见后文对open的介绍
open
所需头文件
| 1 | 
声明int open(const char *pathname, int flags);int open(const char *pathname, int flags,mode_t mode);
参数:
- pathname为文件路径,若只有文件名,则默认在当前工作路径搜索
- flag则是一个- 位图,而不应看作整型参数,传参时可用- |位运算传递多个参数到位图中,例如- O_CREAT | O_WRONLY
- mode则是在创建文件时,传入权限信息,这里使用- 八进制表示法,例如传入- 0666
 返回值:
- 若成功,返回打开文件的fd值
- 若失败,则返回-1
写入操作
相关的flags
- O_WRONLY仅写入
- O_CREAT如果文件不存在,就创建,注新文件的权限由- open函数传入的- mode参数决定
- O_TRUNC如果文件已存在且是- 常规文件,并且打开的模式组合支持写入操作(- O_RDWR或- O_WRONLY),该文件内容将会被清除。但如果该文件是- FIFO(命名管道)文件或- 终端设备文件,则- O_TRUNC将会被忽略
- O_EXCL保证此次- open操作打开新文件。必须和- O_CREAT联合使用,否则打开失败。若- pathname存在,即该路径的文件存在时,也会打开失败
打开已有文件并写入
| 1 | 
 | 
当原本log.txt为空文件时
| 1 | this is a msg | 
当原log.txt不为空且内容长度大于程序输入的msg时,发生部分覆写
例如原内容为0000111100001111时,执行后为
| 1 | this is a msg111 | 
可以看到有一部分没有被覆盖
打开空文件 或 创建空文件
| 1 | 
 | 
追加写入
追加写入只需把O_TRUNC改成O_APPEND即可
| 1 | 
 | 
这里我们事先删除log.txt文件,然后运行两次编译出的程序,可以获得如下内容
| 1 | this is a msgthis is a msg | 
可以看到内容追加了两次
读取操作
相关的flags
- O_RDONLY只读模式打开文件
- O_RDWR读写模式打开文件
只读模式读取内容
| 1 | 
 | 
实现准备内容为123456的log.txt文件
然后运行./mycmd
得到输出和文件内容
| 1 | 456 | 
| 1 | zzz456 | 
关于read没读取到前面新写入的zzz,是因为wtrite和read操作都是从文件的同一处继续操作的,并不会发生回退
close
int close(int fd);
用于冲刷缓冲区,并关闭一个文件描述符
dup2 文件重定向
int dup2(int oldfd, int newfd);

如图所示,dup2能将oldfd对应的数组元素覆盖到newfd对应的数组元素处,完成对newfd对应文件的重定向
图中就是完成了对标准输出的重定向,像printf之类的函数会直接输出内容到文件中,而不是显示器
| 1 | 
 | 
运行代码后,可以看到终端没有输出
而打开log.txt
| 1 | output1 | 
子进程 与 父进程的文件关系
子进程对父进程的拷贝
先运行一段代码测试 子进程是否继承父进程的打开文件
| 1 | 
 | 
这段代码中,我们在fork之前完成了对标准输出的重定向,然后fork之后令父进程和子进程进行不同的标准输出
运行结果为父进程和子进程的标准输出都重定向到了文件
| 1 | child output | 
子进程和父进程的 独立性
接下来一段代码测试 父进程 和 子进程 的打开文件是否独立
| 1 | 
 | 
这里我们在fork之前都不进行重定向,fork后仅对子进程进行了标准输出重定向,而父进程不作任何重定向
在运行后发现子进程的输出重定向不会影响父进程,二者有独立性
| 1 | parent output | 
| 1 | child output | 
进程替换
先在同级文件夹准备一个待替换的程序
execute.c
| 1 | 
 | 
然后运行gcc -o execute execute.c编译获得一个程序
然后准备主程序
mycmd.c
| 1 | 
 | 
这里我们使子进程先标准输出重定向, 再进行进程替换,发现替换后的进程,也是标准输出重定向的状态
| 1 | exe output | 
结论
进程替换不会改变原进程的文件打开状态和重定向关系


