【Linux】基础IO(万字详解) —— 系统文件IO | 文件描述符fd | 重定向原理(下)

简介: 【Linux】基础IO(万字详解) —— 系统文件IO | 文件描述符fd | 重定向原理(下)

🥑那么现在就能解释了为什么打开文件返回的是3:


新打开一个文件本质是内核会为我们描述struct file结构,再把struct file地址填入到fd_array[]数组下标去,因为012已经被占用了,于是填到3号下标,对应的数组下标3返回给用户,这样就能通过fd从而找到了文件对象

这也解释了为什么write和read这样的系统调用接口为什么一定要传入文件描述符fd:执行系统调用接口是进程执行的,通过进程PCB,找到自己打开的文件列表,通过fd索引数组找到对应的文件,从而对文件进行操作


✅ 结论:文件描述符fd,本质是内核中进程和打开文件关联的数组下标


🍈接下来我们看看源代码:


0a2653c851af460fa595bd959398a8f1.png


刚好对应我们的猜测:

2d65d23f6d4748949b924e4057485923.png

我们理一下逻辑:


🔥调用fopen -> 底层是调用open -> 得到fd -> 封装成FILE -> 返回FILE*给给open,传到用户手里

🔥写:用户调用fwrite() -> FILE* -> 包含了fd -> 内部封装write -> 最后是write(fd, …)-> 自己执行操作系统内部的write -> 找到进程的task_struct -> 再找到*fs 指针 -> 找到文件描述符表 files_ struct -> fd_ array[ fd ] -> struct _file -> 找到了内存文件


🎨理性认识一切皆文件


一切皆文件是linux设计哲学,体现在操作系统的软件设计层面


Linux是C语言写的!那如何用C语言实现面向对象,甚至多态?


我们知道:类是由成员函数 + 成员方法组成,c语言里的struct就能实现

struct file
{
  int size:
  mode_t mode;
  int user;
  int group;
  ......
  //函数指针
  int (*readp)(int fd, void * buffer, int len);
  int (*writep)(int fd, void * buffer, int len);
  .....
}


对于键盘显示器等等这些外设,一定都有比如像read、write读写方法,因为由冯诺依曼体系结构知,外设是要和内存打交道的。这可能有些奇怪,比如键盘能读我知道,但能写吗注意,我们有统一的读写方法,但不代表非要每一个都实现,比如键盘就可以没有写方法,即方法为空


0a2653c851af460fa595bd959398a8f1.png


底层不同的硬件,一定对应的是不同的操作方法!!

上面所述的外设,每一个设备的核心访问函数,都可以是read,write (IO)

所有的设备都可以有自己的read和write,但是代码的实现方法一定是不一样的


那又是如何做到一切皆文件的呢?Linux中做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file. 上层的struct file是操作系统OS实行维护的


我们在每个struct file当中包含上一大堆的函数指针,这样,在struct file上层看来所有的文件都是调用统一的接口;在底层我们通过函数指针指向不同硬件的方法。


0a2653c851af460fa595bd959398a8f1.png


说白了就是,我上层不管你具体是什么鸡鸭鹅,都统一被我看成了动物类,类里面有具体的辨别方法,鹅的话就调用鹅的辨别方法,鸡就调用鸡的方法,这样就是一切皆动物的思维了,可以理解为C++的多态是漫长的软件开发摸索中实现“一切皆…”的高级版本/语言版本


在源代码中,struct file就有这样一个结构体指针,指向底层各种实现方法


2d65d23f6d4748949b924e4057485923.png


这就是面面向对象的手法


🎨文件描述符的分配规则


观察如下代码,可以看到,我把0关掉后,再打开文件是分配的文件描述符就是0 ~


0a2653c851af460fa595bd959398a8f1.png


⚡我们得出文件描述符的分配规则:每次给新文件分配的fd,是从fd_array[]中找一个最小的、未被使用的作为新的fd.


其实很好理解,就是从0开始遍历数组中找一个未被使用的下标,并填入文件地址


4. 重定向原理


🌍输出重定向


有没有细心的同学,上面我们唯独没有关闭1,我们现在上手试一下。按照文件描述符的规则,再打开就是打印我们刚刚关闭的1


2d65d23f6d4748949b924e4057485923.png


惊奇的发现,居然没有打印出来,而是全部打印到文件中呢?


0a2653c851af460fa595bd959398a8f1.png


本来应该显示到显示器中,却被打印到文件内部,这种行为我们早就知道叫做输出重定向。咱们无意之间居然完成了一次重定向操作,为什么是这样呢?


💢这是因为:我们以上来就close(1), 断开了和显示器文件的关系,相当于置NULL,对于新打开的log.txt,根据文件分配规则,1是指向log.txt的


2d65d23f6d4748949b924e4057485923.png


思考printf底层是在做什么?事实上,它本质是向标准输出(stdout)打印 ——


int fprintf(FILE *stream, const char *format, ...);
stdout -> FIEL{fileno = 1} -> log.txt// stdout只认识1,只对1输入输出


这就是重定向的本质:在OS内部,更改fd对应的内容的指向!!


🌍追加重定向

追加重定向与输出重定向唯一的差别就是在打开方式上不要O_TRUNC 清空,增加O_APPEND选项。


0a2653c851af460fa595bd959398a8f1.png


🌍输入重定向

输入重定向就是把本来应该从键盘获取内容变成从文件中获取


char *fgets(char *s, int size, FILE *stream);


2d65d23f6d4748949b924e4057485923.png


🌍dup2


以上情况都是先关闭了文件然后再打开文件这样重定向,但是情况不会总是这样子


🍑看文档得知:dup2是对指针做拷贝


#include <unistd.h>
int dup2(int oldfd, int newfd); //oldfd->newfd
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
*  If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
*  If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.


这里有点绕的,copy后最后要和谁一样?,嘿嘿我刚学也绕晕了


要跟oldfd的一样

💢假设:输出重定向 显示器(1)-> log.txt, dup2()的参数该怎么样填呢?


最终的目的:是让1不再指向显示器,而是指向log.txt

所以1的地址xxxx要换成yyyy

3的内容要拷贝到1里面, 最终要和3内容一致——> 也就是和oldfd一样

所以1是newfd ,3是oldfd 。dup2(3,1)


0a2653c851af460fa595bd959398a8f1.png


➰输出重定向


dup2(fd, 1);  本来应该显示到显示器的内容,写入到文件



注意,系统层面,open打开文件时带了选项O_TRUNC,以清空原来内容。而在C语言中"w"也会先把原始文件清空,说明上层封装了这个选项


➰追加重定向


只需在输出只写的基础上添加O_APPEND选项


2d65d23f6d4748949b924e4057485923.png


➰输入重定向


dup2(fd, 0);  原本从键盘读,现在从文件中读。


4cebaac233b3433da32a72337a77fc60.png


5.课后练习


Linux下两个进程可以同时打开同一个文件,这时如下描述错误的是:


A.两个进程中分别产生生成两个独立的fd

B.两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性

C.进程可以通过系统调用对文件加锁,从而实现对文件内容的保护

D.任何一个进程删除该文件时,另外一个进程会立即出现读写失败

E.两个进程可以分别读取文件的不同部分而不会相互影响

F.一个进程对文件长度和内容的修改另外一个进程可以立即感知


A选项正确,进程数据独有,各自有各自的文件描述信息表,因此各自打开文件会有自己独立的描述信息添加在各自信息表的不同位置,因此fd各自也相互独立


B选项正确,两个进程打开同一个文件,但是各有各的文件描述信息以及读写位置,互不影响,因此多个进程同时读写有可能会造成穿插覆盖的情况(原子性操作,被认为是一次性完成的操作,操作过程中间不会被打断,通常以此表示操作的安全性)


C选项正确,文件锁就是用于保护对文件当前的操作不会被打断,就算时间片轮转,因为已经对文件加锁,其他的进程也无法对文件内容进行操作,从而保护在本次文件操作过程是安全的。


D选项错误,删除文件实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息(可以编写代码进行尝试,在文件打开后,外界删除文件,然后看进程中是否还可以继续写入或读取数据)


E选项正确,如果仅仅是读取文件内容,两个不同进程其实都有自己各自的描述信息和读写位置,因此可以同时读取文件数据而不会受到对方的影响。


F选项正确,因为文件内容的修改是直接反馈至磁盘文件系统中的,因此当文件内容被修改,其他进程因为也是针对磁盘数据的操作,因此可以立即感知到(可以写代码尝试一个进程打开文件后,等其他进程修改了内容后然后再读取文件数据进行测试)


以下描述正确的是 [多选]


A.程序中打开文件所返回的文件描述符, 本质上在PCB中是文件描述符表的下标

B.多个文件描述符可以通过dup2函数进行重定向后操作同一个文件

C.在进程中多次打开同一个文件返回的文件描述符是一致的

D.文件流指针就是struct _IO_FILE结构体, 该结构体当中的int _fileno 保存的文件描述符, 是一对一的关系


解析:

A和D不用多说,都是正确的,重点我们来看B(我当时没选):不同信息表数组下标的位置可以存放相同的文件描述信息结构指针,dup2重定向的本质原理,就是改变对应位置的文件信息而改变操作对象文件的


答案是ABD


bash中,需要将脚本demo.sh的标准输出和标准错误输出重定向至文件demo.log,以下哪些用法是正确的 [多选]


A.bash demo.sh &>demo.log

B.bash demo.sh >&demo.log

C.bash demo.sh >demo.log 2>&1

D.bash demo.sh 2>demo.log 1>demo.log


题目解析:

比较典型的方式是:bash demo.sh 1>demo.log 2>&1


先将标准输出重定向到demo.log文件,然后将标准错误重定向到标准输入(这时候的标准输入已经是指向文件了,所以也就是将标准错误重定向到文件)

A command &> file 表示将标准输出stdout和标准错误输出stderr重定向至指定的文件file中。


B 与A选项功能雷同


C 比较典型的写法,将标准输出和标准错误都重定向到文件, >demo.log是一种把前边的标准输出1忽略的写法


D 比较直观的一种写法,不秀技,直观的将标准输入和标准错误分别重定向到文件


答案是ABCD


以下代码的功结果是


void func() {
     int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);
     if (fd < 0) {
     return -1;
     }
     dup2(fd, 1);
     printf("hello bit");
     return 0;
   }


A.将hello bit打印到终端显示

B.将hello bit 写入到tmp.txt中

C.将hello bit 打印到终端显示并且写入tmp.txt文件中

D.既不打印,也没有写入到文件中


解析:

选错选了C,忘记了printf是默认向1中打印,因为dup2,标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印


答案选B ,粗心大意


相关文章
|
1月前
|
搜索推荐 索引
【文件IO】实现:查找文件并删除、文件复制、递归遍历目录查找文件
【文件IO】实现:查找文件并删除、文件复制、递归遍历目录查找文件
33 2
|
1月前
|
编解码 Java 程序员
【文件IO】文件内容操作
【文件IO】文件内容操作
42 2
|
1月前
|
存储 Java API
【文件IO】文件系统操作
【文件IO】文件系统操作
40 1
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
1月前
|
存储 Java 程序员
【Java】文件IO
【Java】文件IO
34 0
|
2月前
|
Linux C语言
C语言 文件IO (系统调用)
本文介绍了Linux系统调用中的文件I/O操作,包括文件描述符、`open`、`read`、`write`、`lseek`、`close`、`dup`、`dup2`等函数,以及如何获取文件属性信息(`stat`)、用户信息(`getpwuid`)和组信息(`getgrgid`)。此外还介绍了目录操作函数如`opendir`、`readdir`、`rewinddir`和`closedir`,并提供了相关示例代码。系统调用直接与内核交互,没有缓冲机制,效率相对较低,但实时性更高。
|
3月前
|
存储 监控 Linux
性能分析之从 IO 高定位到具体文件
【8月更文挑战第21天】性能分析之从 IO 高定位到具体文件
40 0
性能分析之从 IO 高定位到具体文件
|
Linux
Linux救援模式rescue 拯救被删的系统文件
http://linux.ctocio.com.cn/214/8697714_3.shtml
581 0
|
15天前
|
运维 安全 Linux
Linux中传输文件文件夹的10个scp命令
【10月更文挑战第18天】本文详细介绍了10种利用scp命令在Linux系统中进行文件传输的方法,涵盖基础文件传输、使用密钥认证、复制整个目录、从远程主机复制文件、同时传输多个文件和目录、保持文件权限、跨多台远程主机传输、指定端口及显示传输进度等场景,旨在帮助用户在不同情况下高效安全地完成文件传输任务。
111 5
|
15天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
51 4