ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向

Linux系统篇(二十三) 基础 IO:从“文件”到“文件描述符”,彻底理解重定向 观众老爷们大家好 这里是邪修KING的独家频道本文属于系列Linux系统篇 ——操作指令一起学Linux的小伙伴可订阅专栏 Linux系统篇学习定位这一篇只整理《基础 IO》前四部分。目标不是马上写工程而是把 “文件 → 系统调用 → fd → 重定向 → FILE” 这条线真正串起来。这也是后面 MiniShell 加入重定向功能的理论基础。一、先建立一个最重要的认识Linux 中“文件”到底是什么我们以前容易把文件理解成磁盘上的 test.txt、a.c、a.out。这是狭义的文件。但 Linux 更重要的思想是Linux 下一切皆文件。键盘、显示器、磁盘、网卡等都可以被操作系统抽象成类似“文件”的对象。这样程序就可以用统一的方式进行输入输出I/O。所以从操作系统角度看程序 ↓ 进程 ↓ 对“文件”进行操作 ↓ 输入 / 输出 ↓ 设备或磁盘而且要注意真正操作文件的不是 C 语言本身而是操作系统提供的系统调用C 标准库只是给我们提供了更方便的接口。二、C语言中的 fopen和 Linux 的 open 是两套层次以前我们写文件FILE*fpfopen(myfile,w);常见的还有fclose(fp);fread(...);fwrite(...);这些属于C 标准库函数libc而 Linux 提供的更底层接口是open()close()read()write()lseek()它们属于系统调用接口。可以这样理解我们写的程序 │ C标准库函数 fopen / fread / fwrite │ ▼ 系统调用接口 open / read / write │ ▼ Linux │ ▼ 磁盘 / 键盘 / 显示器所以不要认为fopen 和 open 是完全无关的东西更准确的理解是库函数是在系统调用之上提供的一层更方便的封装。三、真正需要重点理解文件描述符 fd这是这一章最核心的东西。1. fd是什么调用intfdopen(myfile,O_RDONLY);成功后可能得到fd3这个 3 就叫文件描述符file descriptorfd它本质上就是一个小整数用来代表当前进程打开的某个文件。PDF进一步解释了它的底层关系进程内部有文件描述符表fd本质上就是这张表的下标通过这个下标可以找到对应的打开文件对象。可以想成当前进程 │ ▼ 文件描述符表 下标 指向 0 ───→ 标准输入 1 ───→ 标准输出 2 ───→ 标准错误 3 ───→ myfile 4 ───→ another.txt所以以后看到read(fd, ...) write(fd, ...) close(fd)你要想到“拿着这个数字告诉操作系统我要操作哪个打开的文件。”四、0、1、2Shell、重定向的钥匙Linux 进程默认打开三个文件描述符0 → stdin → 标准输入 1 → stdout → 标准输出 2 → stderr → 标准错误一般情况下0 → 键盘 1 → 显示器 2 → 显示器所以read(0,buf,sizeof(buf));可以理解从标准输入读取。而write(1,buf,strlen(buf));就是向标准输出写。这也是为什么下面这个程序char buf[1024]; ssize_t s read(0, buf, sizeof(buf)); if(s 0) { write(1, buf, s); }本质上就是键盘 ↓ fd 0 ↓ read ↓ buf ↓ write ↓ fd 1 ↓ 显示器这里一定注意read/write 的第一个参数就是 fd。五、open如何打开一个文件最重要的接口#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即myfile表示我要打开/创建哪个文件。flags告诉操作系统我想以什么方式打开最常见O_RDONLY → 只读 O_WRONLY → 只写 O_RDWR → 读写三者必须选一个。还可以组合O_CREAT → 不存在就创建 O_APPEND → 追加写 O_TRUNC → 清空原内容例如open(log.txt,O_WRONLY|O_CREAT|O_TRUNC,0666);意思打开 log.txt只写不存在就创建如果原来有内容就清空。这里的 | 是按位或用于把多个选项组合起来。mode只有涉及创建文件时才重点看0666表示新创建文件的默认权限。open返回值 成功 → 返回新的fd 失败 → -1例如intfdopen(myfile,O_RDONLY);if(fd0){perror(open);}六、read / write / close真正进行 IO这三个先和 open 一起记。open ↓ 得到fd ↓ read / write ↓ closeread核心形式ssize_tread(intfd,void*buf,size_tcount);可以理解成read(从哪个文件读 → fd 读到哪里 → buf 最多读多少 → count)例如charbuf[1024];ssize_tsread(fd,buf,sizeof(buf));意思从 fd 对应的文件中读取数据放到 buf最多读取 1024 字节。write可以理解成write(写到哪个文件 → fd 写什么 → buf 写多少 → count)例如write(1,buf,s);意思把 buf 中的 s 个字节写到标准输出。closeclose(fd);意思关闭这个文件描述符。所以最基本的文件操作框架就是open ↓ fd ↓ read / write ↓ close七、真正理解“重定向”它不是魔法只是换了 fd 指向这是这一章最值得你记住的一张图。平时 printf() ↓ stdout ↓ fd 1 ↓ 显示器所以./test输出自然出现在屏幕。但是如果./testlog.txt本质上发生了原来fd 1 ─────────→ 显示器重定向后fd 1 ─────────→ log.txt于是程序仍然printf(hello);但它访问的是stdout ↓ fd 1 ↓ log.txt所以程序自己甚至不需要知道“我要输出到文件”。它还是向 stdout 输出只是 Shell 在运行程序之前把 fd 1 的去向换成了文件。PDF也通过关闭 1 后再 open 文件得到 fd1 的实验直观展示了为什么原本输出到显示器的内容会进入文件这就是输出重定向。dup2更规范地完成重定向接口#includeunistd.hintdup2(intoldfd,intnewfd);例如dup2(fd,1);可以理解成让 fd 1 也指向 fd 所指向的文件。于是fd ↓ log.txt fd 1 ↓ log.txt以后printf(...)最终走 stdout → fd 1自然就进入 log.txt。PDF的 MiniShell 重定向实现也是在子进程中先 open() 文件再通过 dup2(fd, 0/1) 替换标准输入或标准输出。八、把这一章和前面的进程控制串起来这就是你现在最应该形成的总框架Shell │ 用户输入命令 │ ▼ 解析命令 │ ▼ fork() ┌──────┴──────┐ │ │ 父进程 子进程 │ │ │ 判断有没有重定向 │ │ │ open() │ │ │ dup2() │ │ │ ▼ │ exec() │ │ │ ▼ │ ls / cat ... │ │ │ ▼ └──── wait ← exit你可以把它理解成前一章的 fork exec wait这一章又给子进程加上了 open dup2于是 Shell 就开始拥有真正的重定向能力。例如ls -l log.txt实际上就是Shell │ ├─ fork │ ├─ 子进程 │ │ │ ├─ open(log.txt, ...) │ │ │ ├─ dup2(fd, 1) │ │ │ └─ exec(ls) │ └─ 父进程 wait这就是为什么你之前学的 MiniShell 没有白学。九、FILE 和 fd最后把这两个概念分清你之前接触的是FILE*fp;现在又出现intfd;不要混。可以简单理解成FILE * ↓ C标准库提供的“高级文件对象” ↓ 内部最终还是要依靠 ↓ fd ↓ Linux系统调用PDF明确指出C 库的 IO 函数最终还是通过 fd 访问文件因此 FILE 结构体内部必然包含与 fd 相关的信息。所以高级 ↑ FILE *fp │ │ C标准库 ↓ int fd │ │ 系统调用 ↓ Linux内核 ↓ 文件/设备这条关系记住就够了。至于 PDF 后面讲的FILE 内部怎么维护缓冲区、为什么 fork 后 printf 会出现重复输出……这些都属于缓冲区部分你现在既然还没上这节先完全跳过。 最后6个问题检验你是不是真的理解了建议你先别看答案自己在脑子里说一遍。① 为什么 open() 返回的是一个整数 fd而不是一个 FILE *答案因为 open() 是 Linux 系统调用层面的接口它返回的是当前进程文件描述符表中的一个下标FILE * 是 C 标准库提供的更高层对象。② 为什么 Linux 默认有 0、1、2答案0 → stdin → 标准输入1 → stdout → 标准输出2 → stderr → 标准错误它们让程序可以用统一的 fd 进行输入输出。③ 为什么 write(1, buf, size) 能输出到显示器答案因为当前进程中 fd1 默认对应标准输出而标准输出通常指向终端显示器。④ ./test log.txt 为什么能让程序输出到文件答案Shell 在程序执行前改变了标准输出 fd 1 的指向让它指向 log.txt所以程序继续向 stdout/fd1 写时数据就进入文件。⑤ 为什么 dup2(fd, 1) 能实现重定向**答案**因为它让新的 fd1 指向与 fd 相同的打开文件对象因此程序后续向标准输出 1 写入时就会写入那个文件。⑥ FILE * 和 fd 到底是什么关系答案FILE * ↓ C标准库层 ↓ 底层依靠 fd ↓ Linux系统调用 ↓ 内核所以不是两个完全独立的 IO 系统而是不同层次的接口。 这一篇最终只记这一张图Linux 基础 IO │ ┌───────────┴───────────┐ ▼ ▼ 文件是什么 怎么操作文件 │ │ Linux下一切皆文件 C库 / 系统调用 │ ┌─────────────┴─────────────┐ ▼ ▼ C标准库 系统调用 fopen/fclose/fwrite open/read/write │ │ └──────────┬────────────────┘ ▼ fd │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ 0 1 2 stdin stdout stderr │ │ │ 输入 输出 错误 │ ▼ dup2(fd,1) │ ▼ 重定向 │ ▼ ./test log.txt你现阶段真正需要形成的认知链文件 → 操作系统管理 → 系统调用 → fd → 0/1/2 → dup2 → 重定向 → FILE 是更高层的封装而且这一次不要急着写代码。你老师让你先“理解吸收”是非常合理的现在你能把这张图和前面的 fork → exec → wait 串起来等老师后面真正开始做 MiniShell 重定向时你会突然发现——原来 、、 就是这么来的。
返回列表