ARTICLE DETAIL

资讯详情

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

重定向细节与 “一切皆文件”

重定向细节与 “一切皆文件” 重定向细节与一切皆文件详解一、重定向的完整写法其实是1平时写的并不完整它的完整写法是1./a.outlog.txt# 等价于./a.out1log.txt# 1 就是 stdout 的 fd可以省略为什么 1 可以省略因为stdoutfd 1是重定向的默认对象shell 对它做了特殊处理。同理重定向标准错误要写2这里的 2 就是 stderr 的 fd./a.out2err.txt# 只重定向 stderr二、stdout 与 stderr为什么要分成两个流1. 它们指向同一个文件fd 1stdout和 fd 2stderr都指向显示器这个文件。也就是说显示器这个文件被进程打开了两次——同一个struct file被 fd 1 和 fd 2 指向涉及引用计数后面讲f_count时会再遇到它。所以平时运行程序常规输出和错误信息都往屏幕上打看起来混在一起。2. 分离的意义日志debug 的时候我们最关心的是错误信息。有了重定向能力就可以把常规消息和错误消息分离到不同文件里./a.out1log.txt2err.txt这就是日志形成的基础正常日志一个文件错误日志一个文件排查问题时直接看 err 文件。3.1 log 2 log的坑想把 stdout 和 stderr 重定向到同一个文件直觉写法是./a.out1log.txt2log.txt# ✖ 错误写法为什么错文件每次被打开都会被清空O_TRUNC。1 log.txt打开并清空 log.txtfd 1 指向它2 log.txt再次打开又把 log.txt 清空了fd 2 指向它最终文件里往往只剩下标准错误的内容标准输出被吃掉了。解决办法之一给第二个重定向加追加语义./a.out1log.txt2log.txt三、 log.txt 21合并重定向的标准写法./a.outlog.txt2121这段不是系统调用干的是 shell 解释的。它的含义把 fd 2 里的内容也写到 fd 1 指向的文件里去——让 fd 2 和 fd 1 指向同一个struct file同一个打开的文件。关键区别2 log.txt重新打开log.txt触发清空21不重新打开文件只是让 fd 2 复用 fd 1 已经打开的那个文件所以没有二次清空的坑。注意顺序 log.txt要写在21前面先让 fd 1 指向 log.txt再让 fd 2 跟过去。 这是 shell 里最常见的合并重定向写法记牢它。四、补充输入重定向与 dup2课件补充简要输入重定向把文件内容作为程序的标准输入./a.outinput.txtdup2重定向除了close(1) open(...)这种腾位置的做法还可以用系统调用dup2一步到位#includeunistd.hintdup2(intoldfd,intnewfd);// 让 newfd 指向 oldfd 所指向的文件原 newfd 若已打开会被关闭// 常用姿势dup2(fd, 1) 把 stdout 重定向到 fd 指向的文件本篇重点是后面的原理dup2 的细节在前面基础 IO 篇已讲过这里不再展开。五、fd 的本质一次内核源码之旅前面说过 fd 是数组下标这次顺着源码把这个数组真正挖出来。1. 从 task_struct 出发task_struct进程描述符 └── struct files_struct *files // 进程打开的文件信息 └── fd_array[] // 文件描述符数组指针数组 └── struct file * // 每个元素指向一个打开的文件对象每个进程都有一个task_struct里面有一个struct files_struct。files_struct里有一个指针数组fd_array默认大小32/64 个早期版本不够可以扩展最大可到65535数组的每一个元素都是一个指针指向struct file。fd 的本质就是这个数组的下标——所谓文件描述符就是fd_array的下标而已。所以open的返回值、read(fd, ...)的第一个参数全都是在拿这个下标做文章。2. 为什么 0/1/2 天生被占用进程被创建时操作系统默认帮你打开了三个文件fd 0 → 标准输入键盘fd 1 → 标准输出显示器fd 2 → 标准错误显示器因此用户open新文件拿到的一定是从3开始的 fd。重定向的close(1) open玩法本质就是把数组下标 1 的位置腾出来让新打开的文件占住 1 号位——之后所有往 fd 1 写的数据就都进了文件。六、struct file 详解三个关键字段struct file是内核中描述一个被打开的文件的结构体重点看三个字段1.f_count引用计数记录有多少个指针fd指向这个 struct file。前面说的显示器被打开两次fd 1 和 fd 2 指向同一个 struct file对应的就是 f_count 为 2。引用计数归 0这个 file 对象才可以被释放配合 LRU 等内存管理算法。2.f_mode打开方式记录这个文件是以什么权限打开的只读 O_RDONLY / 只写 O_WRONLY / 读写 O_RDWR 等。3.f_pos当前读写位置记录当前读/写到了文件的哪个位置offset。对应 C 语言里的fseek / ftell / rewind系统调用层面就是lseek#includesys/types.h#includeunistd.hoff_tlseek(intfd,off_toffset,intwhence);// whence: SEEK_SET开头/ SEEK_CUR当前/ SEEK_END结尾 追加写的实现原理每次写之前把 f_pos 挪到文件末尾SEEK_END。4. 文件的本质char 类型的一维数组文件在内核眼里的本质就是一维 char 数组文本文件数组的每个元素是字符二进制文件数组的每个元素是8 位二进制数二者本质相同只是看数据的视角不同。f_pos 就是这个数组的下标读一个字节下标 1写一个字节下标 1。5. 推论为什么文件很少读写方式打开一个 struct file只有一个 f_pos读和写共用同一个位置以 O_RDWR 打开文件后先 write 一段内容f_pos 已经挪到了末尾紧接着 read从当前 f_pos 往后读——什么也读不到读到 EOF想读出来必须先lseek把位置归零。所以文件一般要么以读方式打开要么以写方式打开很少同时例外场景进程间文件通信 / 管道那是有意利用读写位置做同步的。七、struct file 与内核缓冲区struct file还能找到这个文件对应的内核缓冲区通过struct address_space地址空间结构再经由radix tree基数树一种多叉树定位到具体的4KB 内存页struct page。操作系统管理内存是以4KB页为单位划分的文件内容就按页缓存在内存里。串起来一条链fd数组下标 → fd_array[] → struct file → address_space → radix tree → struct page4KB 页缓冲区 → 最终落到磁盘上的文件数据八、inodestruct file 里没有的东西观察struct file会发现它里面没有文件名、没有文件大小、没有 ACM 三个时间、没有权限这些信息。为什么因为这些是磁盘级文件级的属性不属于某一次打开——同一个文件可以被打开多次多个 struct file但属性只有一份。这些属性存在inode索引节点里。struct file 可以直接/间接地找到文件的 inode从而拿到文件属性和文件缓冲区。 一句话记忆struct file 管这一次打开inode 管这个文件本身。九、一切皆文件详解1. 什么是一切皆文件Windows 里是文件的东西Linux 里也是文件——这不稀奇。稀奇的是Windows 里不是文件的东西Linux 里也给你做成文件进程/proc目录磁盘块设备文件显示器、键盘、鼠标设备文件网卡、网络套接字socket 文件管道管道文件收益一套文件接口open/read/write/close就能访问绝大部分资源不必为每种资源单独学一套 API。2. 软硬件体系结构用户应用程序 ↑ 系统调用 操作系统 ↑ 驱动层每种硬件有自己的驱动、自己的读写方法 ↑ 硬件硬件五花八门每种硬件的读写方式都不同这个差异由驱动层消化。3. 设备管理先描述再组织描述设备struct device—— 包含设备的类型、状态、属性以及读写方法。组织设备用list_head双链表把所有设备串起来。于是设备管理就转化成了老朋友——链表的增删查改。4. 函数指针C 语言的多态问题C 的结构体里不能放函数实现struct file 怎么拥有读写方法答案结构体里可以放函数指针。struct file中有一个字段conststructfile_operations*f_op;// 文件操作集内核源码中真实存在file_operations里是一堆函数指针read、write、open、close……。关键设计所有 struct file 的 read/write 函数签名返回值、参数完全一样但指针指向的具体实现各不相同指向普通文件 → 磁盘的读写方法指向显示器 → 显示器驱动的写方法指向键盘 → 键盘驱动的读方法……这就是用 C 语言实现的多态——同一个接口底层行为不同屏蔽了硬件差异。 C 的多态虚函数底层就是这么干的函数指针 虚函数表。5. VFS虚拟文件系统struct file属于VFSVirtual File System虚拟文件系统不属于任何具体文件系统ext4、NTFS……。VFS 位于具体文件系统/设备之上向下对接各种差异向上提供统一的struct filefile_operations接口。“骗了进程就是骗了用户”——进程只能通过 fd struct file 访问一切资源它看到的世界里全部都是文件所以在进程用户看来一切皆文件。6. 经典名言加一层任何计算机问题都可以通过增加一层软件层来解决。VFS 就是这句话的完美体现加一层抽象把底层差异挡在下面。体系结构规律越靠近上层抽象程度越高基类 / 虚基类 / 接口类抽象程度最高。十、举例write 系统调用的完整链路把本篇所有内容串成一条线一次write(1, buf, size)的旅程用户层缓冲区buf → 系统调用 write(fd1, ...) → 文件缓冲区struct file → address_space → radix tree → 4KB 页 → struct file 的 f_op-write 函数指针 → 底层具体设备的写方法磁盘驱动 / 显示器驱动 / 网卡驱动…… → 设备fd 是数组下标 → 找到 struct file → 找到缓冲区页 → 找到写方法 → 落到设备。这条链路上每一环就是本篇的全部内容。
返回列表