一、理解"文件"
1、狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设(即是输出设备也是输入设备)
讲解 Linux 系统文件 I/O 基础,涵盖文件概念、C 库函数与系统调用的区别、文件描述符 fd 机制及分配规则。重点阐述了标准输入输出重定向的原理,演示了通过 close 和 dup2 系统调用实现输出重定向的代码示例,并提及在 minishell 项目中的实际应用。

#include <stdio.h>
int main() {
FILE *fp = fopen("myfile", "w");
if (!fp) {
printf("fopen error!\n");
}
while (1);
fclose(fp);
return 0;
}
打开的 myfile 文件在哪个路径下?
xz@xzlinux:~$ ps ajx |head -1;ps ajx |grep catme PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 336450336595336595336435 pts/0 336595 R+ 10000:02 ./catme 336584336599336598336569 pts/1 336598 S+ 10000:00 grep--color=auto catme xz@xzlinux:~$ ls /proc/336595 -l total 0...... -r--r--r-- 1 xz xz 0 May 815:34 cpuset lrwxrwxrwx 1 xz xz 0 May 815:34 cwd -> /home/xz/z/IOleran -r-------- 1 xz xz 0 May 815:34 environ lrwxrwxrwx 1 xz xz 0 May 815:34 exe -> /home/xz/z/IOleran/catme dr-x------ 2 xz xz 4 May 815:34 fd ......
其中:
打开文件,本质是进程打开,所以,进程知道自己在哪里,即便文件不带路径,进程也知道。由此 OS 就能知道要创建的文件放在哪里。
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("myfile", "w");
if (!fp) {
printf("fopen error!\n");
}
const char*msg = "hello bit!\n";
int count = 5;
while (count--) {
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("myfile", "r");
if (!fp) {
printf("fopen error!\n");
return 1;
}
char buff[1024];
const char*msg = "hello bit!\n";
while (1) {
ssize_t s = fread(buff, 1, strlen(msg), fp);
if (s > 0) {
buff[s] = 0;
printf("%s", buff);
}
if (feof(fp)) {
break;
}
}
fclose(fp);
return 0;
}
稍作修改,实现简单 cat 命令:
#include <stdio.h>
#include <string.h>
// 简单实现 cat 命令
int main(int argc, char*argv[]) {
if (argc != 2) {
printf("argv error!\n");
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (!fp) {
printf("fopen error!\n");
return 2;
}
char buf[1024];
while (1) {
int s = fread(buf, 1, sizeof(buf), fp);
if (s > 0) {
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp)) {
break;
}
}
fclose(fp);
return 0;
}
#include <stdio.h>
#include <string.h>
int main() {
const char*msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
r Open text file for reading. The stream is positioned at the beginning of the file.
r+ Open for reading and writing. The stream is positioned at the beginning of the file.
w Truncate file to zero length or create text file for writing. The stream is po‐ sitioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, other‐ wise it is truncated. The stream is positioned at the beginning of the file.
a Open forappending(writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
a+ Open for reading and appending(writing at end of file). The file is created if it does not exist. Output is always appended to the end of the file. POSIX is silent on what the initial read position is when using this mode. For glibc, the initial file position for reading is at the beginning of the file, but for An‐ droid/BSD/MacOS, the initial file position for reading is at the end of the file.
如上,是文件相关操作。还有 fseek ftell rewind 的函数,在 C 部分已经有所涉猎。
打开文件的方式不仅仅是 fopen,ifstream 等流式,语言层的方案,其实系统才是打开文件最底层的方案。不过,在学习系统文件 IO 之前,先要了解下如何给函数传递标志位,该方法在系统文件 IO 接口中会使用到:
#include <stdio.h>
#include <string.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0001
#define THREE 0004 //0000 0001
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);
func(THREE);
func(ONE | THREE);
func(ONE | THREE | TWO);
return 0;
}
操作文件,除了上面的 C 接口(当然,C++ 也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以系统代码的形式,实现和上面一模一样的代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
umask(0);
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
int count = 5;
const char* msg = "hello xz!\n";
int len = strlen(msg);
while (count--) {
write(fd, msg, len); //fd: 后面讲,msg:缓冲区首地址。
//len: 本次读取,期望写入多少个字节的数据。
//返回值:实际写了多少字节数据
}
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("myfile", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
const char* msg = "hello bit!\n";
char buf[1024];
while (1) {
//ssize_t
ssize_t s = read(fd, buf, strlen(msg)); //类比 write
if (s > 0) {
printf("%s", buf);
} else {
break;
}
}
close(fd);
return 0;
}
#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: 打开⽂件时,可以传⼊多个参数选项,flags。
O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR: 读,写打开O_CREAT: 若⽂件不存在,则创建它。需要使⽤ mode 选项,来指明新⽂件的访问权限O_APPEND: 追加写返回值:
mode_t 理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的 open。
write read close lseek ,类比 C 文件相关接口。
在认识返回值之前,先来认识一下两个概念:系统调用 和 库函数
fopen fclose fread fwrite 都是 C 标准库当中的函数,我们称之为库函数(libc)。open close read write lseek 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。
所以,可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main() {
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if (s > 0) {
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体。表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 *files, 指向一张表 files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
对于以上原理结论我们可通过内核源码验证:
首先要找到 task_struct 结构体在内核中为位置,地址为: /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.h(3.10.0-1160.71.1.el7.x86_64 是内核版本,可使用 uname -a 自行查看服务器配置,因为这个文件夹只有一个,所以也不用刻意去分辨,内核版本其实也随意)
要查看内容可直接用 vscode 在 windows 下打开内核源代码
相关结构体所在位置
struct task_struct:/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/sched.hstruct files_struct:/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fdtable.hstruct file:/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
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
关闭 0 或者 2,再看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
close(0); //close(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 ,可见,文件描述符的分配规则:在 files_struct 数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
那如果关闭 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_WRONLY | O_CREAT, 00644);
if (fd < 0) {
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:> , >> , <
那重定向的本质是什么呢?
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);
示例代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
for (;;) {
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf)-1);
if (read_size < 0) {
perror("read");
break;
}
printf("%s", buf);
fflush(stdout);
}
return 0;
}
printf 是 C 库当中的 IO 函数,一般往 stdout 中输出,但是 stdout 底层访问文件的时候,找的还是 fd:1, 但此时,fd:1 下标所表示内容,已经变成了 myfile 的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
参考项目中的重定向实现逻辑。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online