
引言文件存放于磁盘这种外部设备中磁盘同时具备输入与输出能力。我们对文件的各类操作本质上是在内存和磁盘外设之间进行数据读写这类数据交互统称为 I/O。一、基础I/O操作库函数我们在 C 语言中有多种操作文件的函数比如 fopen、fwrite、fclose 等这些函数都是基础的 I/O 接口实现了磁盘的 I/O 操作。下面使用 C 语言进行基础的 I/O 操作。#includeiostream #includecstring int main() { FILE* fp fopen(user.txt, w); const char* mes Hello,world!\n; fwrite(mes, strlen(mes), 1, fp); fclose(fp); return 0; }这样我们就完成了一次简单的文件操作。可以发现当我们调用fopen时传入的并非绝对路径而是相对路径但文件依旧准确创建在当前目录下。这是因为每个进程都会维护自身的当前工作目录 CWDCurrent Working Directory当使用相对路径创建、查找文件时内核会以该进程的 CWD 作为基准拼接解析出完整路径。由此可知文件操作的主体实质上是进程文件只是被操作的对象。输出信息到显示器当我们将输出信息呈现到显示器上除了常见的 printf/cout 输出函数还有哪些方法呢#includeiostream #includecstring int main() { std::cout Hello,world! std::endl; const char* mes Hello,world!\n;\ fwrite(mes, strlen(mes), 1, stdout); fprintf(stdout, Hello,world!\n); return 0; }上面我们使用到了 fwrite 函数与 fprintf 函数二者都属于 C 标准库提供的文件写入函数底层最终都会调用 write 系统调用以此实现向显示器输出信息的效果。这里会产生一个疑问对比之前普通文件操作我们并没有手动打开 stdout为什么 fwrite 函数和 fprintf 函数就可以直接完成输出原因在于进程被创建时操作系统会默认帮进程打开三个文件流stdin标准输入、stdout标准输出、stderr标准错误。这三个文件流默认已经就绪所以程序可以直接使用无需手动调用 fopen 打开。二、基础I/O操作系统调用2.1 位图传递标志位在了解系统层面的I/O操作之前我们需要了解通过位图传递多个标志位的操作。#include stdio.h #define ONE 0001 //0000 0001 #define TWO 0002 //0000 0010 #define THREE 0004 //0000 0100 void func(int flags) { if (flags ONE) printf(ONE! ); if (flags TWO) printf(TWO! ); if (flags THREE) printf(THREE! ); printf(\n); } int main() { func(ONE); func(THREE); func(ONE | TWO); func(ONE | THREE | TWO); return 0; }我们定义的三个宏 ONE、TWO 和 THREE它们在 8 位二进制中都仅有一位为 1。当执行按位或|运算时可以借助位图的思想将多个标志合并在一起以此实现一次性传递多个标志位的效果。2.2 接口介绍open()第一个参数const char *pathname表示的是文件路径名。第二个参数int flags表示的是标志位常见的标志位包括O_RDONLY、O_WRONLY、O_RDWR、O_TRUNC、O_APPEND、O_CREAT传递多个标志位时就使用了位图的思想通过按位或|将多个标志组合在一起传入。第三个参数mode_t mode表示的是文件权限仅当使用O_CREAT标志创建文件时才需要提供该参数。返回值为文件标识符fd int后续对文件进行读写等操作时依靠这个文件描述符来标识目标文件。write()第一个参数int fd表示的是文件标识符。第二个参数const void *buf是缓冲区首地址指向内存中待写入的数据。第三个参数size_t count表示期望写入的字节数量。返回值为实际写入的字节数。close()close就很简单了只有一个参数就是目标文件的文件标识符作用是就是关闭目标文件关闭成功返回0关闭失败关返回-1。2.3 文件描述符fd我们在使用系统调用时多次使用到了文件描述符那么文件描述符到底是什么呢文件描述符就是对文件进行表示的符号用一个int整型表示。1 #includestdio.h 2 #includesys/types.h 3 #includefcntl.h 4 #includesys/stat.h 5 #includeunistd.h 6 7 int main() 8 { 9 int fd open(opfile.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 printf(fd %d\n, fd); 11 close(fd); 12 return 0; 13}当我们打开一个文件时显示该文件的文件描述符为3可为什么是3呢前面的 0,1,2 代表的又是谁呢其实很简单上面我们了解到当进程被创建时操作系统会默认的打开三个文件stdinstdoutstderr而这三个文件分别代表的就是文件描述符的012。上述我们明白了文件描述符 0,1,2 分别表示的是输入输出错误。那么文件描述符在哪里存储呢2.4 PCB和文件描述符在 PCB 中维护着一个files_struct结构体files_struct内部包含struct file* fd_array[]指针数组。文件描述符 fd 本质上就是这个数组的下标数组的每一项存放struct file *类型指针该指针指向内核中的struct file结构体对象。操作系统管理已打开文件遵循 “先描述再组织” 的思路将打开文件的运行时信息封装为struct file结构体注意该结构体保存文件打开状态、读写偏移量等信息而 inode 元信息存放在单独的 inode 结构中再通过相应的数据结构对这些struct file实例统一管理。文件描述符分配规则分配文件描述符时内核会在fd_array数组中查找最小且未被占用的数组下标将这个下标作为新的文件描述符。重定向结合前面的内容可以知道下标 0、1、2 分别对应标准输入、标准输出、标准错误。如果我们修改这几个下标在fd_array中指向的struct file实例就能够实现文件重定向。这里就要介绍对应的重定向函数dup2函数原型 dup2oldfdnewfd如果newfd已经打开就自动closenewfd然后把oldfd对应的struct file*复制到fd_array[fd]中增加引用计数。下面我们将opfile.txt文件替换位于1的stdout显示器1 #includestdio.h 2 #includesys/types.h 3 #includefcntl.h 4 #includesys/stat.h 5 #includeunistd.h 6 7 int main() 8 { 9 int fd open(opfile.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 dup2(fd, 1); 11 printf(Hello!!!\n); 12 printf(Hello!!!\n); 13 printf(Hello!!!\n); 14 close(fd); 15 return 0; 16 }根据结果我们可以发现本该在屏幕上输出的字符串被输出到opfile.txt文件中去了这样我们就做到了输出重定向。2.5 理解“一切皆文件”在Linux中我们有一个理念就是一切皆文件在Windows中被认为是文件的东西在Linux中也是文件而在Windows中例如键盘、显示器、网盘这些不被认为为文件的在Linux中也被抽象为文件我们可以以访问文件的方式访问它们。这样做的明显优势是操作者只需一套API和开发工具就能调用绝大多数资源绝大多数的读的操作都可以通过read函数进行操作绝大多数写的操作都可以通过write函数进行操作。文件被打开时内核会为其创建struct file结构体来描述打开实例。该结构体包含成员const struct file_operations *f_opf_op指向file_operations结构体这个结构体内部存放着一组函数指针。它的作用是把系统调用和底层设备驱动关联起来。不同类型的文件或设备各自实现对应的读写函数调用时通过file_operations里的函数指针完成回调。虽然各类文件、设备都提供 read、write 接口但底层实现逻辑各不相同。借助这套函数指针回调机制内核只需要通过struct file就可以统一访问绝大多数资源这正是 Linux “一切皆文件” 思想的核心。