文件我们并不陌生,其位于磁盘,磁盘本质也是外设,兼备输入和输出功能,那么当我们进行文件操作的时候实质上是对于磁盘进行输入/输出操作,这里简称为IO。
一、基础IO操作(库函数)
在C语言学习中,我们学习过很多文件操作比如fopen()、fwrite()、fclose()等等,这些函数都是基础的IO接口,让我们实现与磁盘的IO交互。比如我们可以用简单一段代码实现一次基础IO:
#include<iostream>
#include<cstring>
int main(){
FILE *fp=fopen("log.txt","w");
const char *mes="hello world\\n";
fwrite(mes,strlen(mes),1,fp);
fclose(fp);
}

这样就简单的实现了一次文件的写入操作,可以看到在执行 fopen 函数时,我们并未传入绝对路径,但文件仍能精准创建在当前目录下,这是因为每个运行中的进程都会维护自己的当前工作目录(Current Working Directory, CWD);当通过相对路径新建 / 访问文件时,系统会以该进程的 CWD 为基准解析路径,最终完成文件操作。这一现象直观证明:文件操作的执行者本质是进程,文件只是被进程操作的对象。
如何向显示器打印
当我们想到想显示器打印就轻而易举想到std::cout/printf接口调用,那么还有没有其他办法呢?
其实,利用文件操作同样可以实现向显示器打印:
#include<iostream>
#include<cstring>
int main(){
std::cout<<"hello,CSDN!"<<std::endl;
const char *mes="hello,CSDN!\\n";
fwrite(mes,strlen(mes),1,stdout);
fprintf(stdout,mes);
}
我们利用以上代码,调用fwrite和fopen接口

可以发现,照样可以实现向显示器打印的功能,但是此时就有问题出现,我们之前在进行文件IO操作前,都需要调用fopen接口先以特定方式打开文件,为什么向stdout输入无需提前打开,这是因为,在我们进程启动的时候,系统就自动为我们打开了stdin(输入),stdout(输出),stderr(错误)三个文件。但是还有一个问题:显示器不是外设吗,为什么会以文件打开,那么就引出:
Linux下一切皆文件
Linux 中 “一切皆文件” 绝不是简单的 “把所有东西都叫文件”,而是操作系统从内核层给所有资源设计了一套统一的 “文件式操作规则” —— 不管是普通的文本文件、硬件设备(网卡 / 串口)、进程间通信的管道,还是进程 / 内存等系统资源,都能通过「打开→读写→关闭」这一套相同的方式操作,不用区分底层是 “文件” 还是 “硬件”。本质是给所有系统资源(文件、硬件、进程、通信)套上了 “文件” 的 “外衣”—— 用相同的接口、相同的路径、相同的操作逻辑,让程序员和系统能极简地管理所有资源。因此我们可以简单的使用文件调用接口去管理、操作各种设备,输入、输出以及错误就是其中个例:
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
上述操作所用到的接口(fwrite、fopen、fclose)其实都是C语言提前封装好的库函数调用,底层封装原型是open、write以及close这些系统调用接口,那么下面,我将浅谈系统调用函数。
二、基础IO操作(系统调用)
前置知识:位图传递标志位
假如我们有多种方案需要进行组合并且只用一个标志进行信息传递的时候,二进制就排上了很大的用途,比如
#define FLAG1 0x01 // 二进制0001
#define FLAG2 0x02 // 二进制0010
int flag = FLAG1 | FLAG2; // 0011,表示同时启用FLAG1和FLAG2
我们使用二进制进行位标志,当我们需要进行组合的时候,简单利用按位或运算即可精确记录组合标志位,方便我们进行后续操作。在系统文件IO调用接口就会用到该方法。
接口介绍
open()
函数原型 int open(const char *pathname, int flags, … /* mode_t mode */);
open 用于在内核层打开或创建指定路径的文件,参数包含文件路径、打开模式(如只读 O_RDONLY、只写 O_WRONLY、读写 O_RDWR,创建文件需附加 O_CREAT)及文件权限(创建时生效,如 0644),返回唯一的文件描述符(fd,整数)作为后续操作标识;在该接口中打开模式就用到了位图传递标志位的方法,其中不同模式代表不同位图标志,用按位或思想就实现了不同场景的使用。
write()
函数原型 ssize_t write(int fd, const void *buf, size_t count);
write 通过文件描述符 fd,将用户态缓冲区的数据写入对应文件 / 资源,参数为 fd、数据缓冲区指针、待写入字节数,返回实际写入字节数;
close()
函数原型 int close(int fd);
close 接收文件描述符 fd 作为唯一参数,关闭该 fd 并释放内核分配的文件资源,切断进程与文件的内核层关联,需在操作完成后调用以避免资源泄漏。
文件描述符
上述接口调用过程中都都涉及到了一个新概念文件描述符也就是fd,那么什么是文件描述符呢?文件描述符又存在于何处呢?
其实顾名思义,文件描述符就是对于文件进行标识的符号,我们可以看到,open函数打开成功就会返回文件fd,那我们可以用几行代码进行简单实践,所用头文件我们可以通过man进行手册查询
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<iostream>
#include<unistd.h>
int main(){
int fd=open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0644);
std::cout<<fd<<std::endl;
close(fd);
return 0;
}

那我们进行编译运行后可以看到,显示器打印了数字3也就是fd,那么就很奇怪,下标不一般都是0开始吗,难道0、1、2以及被占用了?是的,记得我们之前提到的,在进程开始运行时,会默认打开三个文件也就是输入、输出、错误。前三个文件描述符正是被这三个文件占用,因此后续申请文件描述符就需要从3开始,文件描述在文件中名称为fileno,我们可以通过代码进行验证:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<iostream>
#include<unistd.h>
int main(){
int fd_0=stdin->_fileno;
int fd_1=stdout->_fileno;
int fd_2=stderr->_fileno;
int fd=open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0644);
std::cout<<"fd_0="<<fd_0<<std::endl;
std::cout<<"fd_1="<<fd_1<<std::endl;
std::cout<<"fd_2="<<fd_2<<std::endl;
std::cout<<"fd="<<fd<<std::endl;
return 0;
}

那么可以看到0、1、2正对应输入、输出以及错误。那么文件描述符位于哪里呢?
PCB与文件描述符
文件描述符(fd)的核心存储位置是进程 PCB(task_struct)中,进程每打开一个文件,内核会创建一个file_struct(全局文件表项),task_struct中的files_struct包含fd_array数组 ——fd 本质是fd_array的整数下标,fd_array[fd]指向对应的file_struct(存储文件读写位置、打开模式等核心信息);fd 与file_struct是进程内的 “一对一” 映射,而file_struct最终指向磁盘文件的 inode,因此 fd 通过多层关联实现与文件的绑定(非 fd 与文件直接一对一)
文件描述符申请规则
文件描述符申请遵循优先取最小下标原则:比如我们用close关闭stdin,也就是执行close(0),接下里再进行文件打开,那么此时新文件返回fd就是0。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<iostream>
#include<unistd.h>
int main(){
close(0);
int fd=open("log.txt",O_CREAT | O_RDONLY );
std::cout<<fd<<std::endl;
return 0;
}
重定向实现
那么根据前置知识,我们其实可以想到,std::cout本质是向fd为0的文件进行输入从而实现打印,那么如果我们是否可以用其他文件代替显示器文件,用std::cout完成向文件的打印呢?dup2函数则可以完成该想法。
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup2函数是Linux系统调用,用于将指定的文件描述符`oldfd`复制到目标文件描述符`newfd`,若`newfd`已打开则先关闭它,最终让`newfd`与`oldfd`指向同一个内核文件表项(file_struct),实现两个文件描述符操作同一文件的效果。
代码实践
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<iostream>
#include<unistd.h>
int main(){
int fd=open("log.txt",O_CREAT | O_WRONLY | O_APPEND);
if(dup2(fd,1)==-1){
perror("dup2 failed!");
exit(1);
}
int cnt=10;
while(cnt–)
std::cout<<"hello,CSDN!\\n";
return 0;
}

可以看到,本该输出到显示器的内容输出到了目标文件,实现了重定向。





