【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 ,粗心大意


相关文章
|
6天前
|
Unix Linux Shell
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
14 0
|
4天前
|
存储 Linux C语言
|
6天前
|
存储 JSON 安全
Python中的文件操作与文件IO操作
【5月更文挑战第14天】在Python中,文件操作是常见任务,包括读取、写入和处理文件内容。`open()`函数是核心,接受文件路径和模式(如&#39;r&#39;、&#39;w&#39;、&#39;a&#39;、&#39;b&#39;和&#39;+&#39;)参数。本文详细讨论了文件操作基础,如读写模式,以及文件IO操作,如读取、写入和移动指针。异常处理是关键,使用`try-except`捕获`FileNotFoundError`和`PermissionError`等异常。进阶技巧涉及`with`语句、`readline()`、`os`和`shutil`模块。数据序列化与反序列化方面,介绍了
18 0
|
6天前
|
Java 开发者
Java一分钟之-Java IO流:文件读写基础
【5月更文挑战第10天】本文介绍了Java IO流在文件读写中的应用,包括`FileInputStream`和`FileOutputStream`用于字节流操作,`BufferedReader`和`PrintWriter`用于字符流。通过代码示例展示了如何读取和写入文件,强调了常见问题如未关闭流、文件路径、编码、权限和异常处理,并提供了追加写入与读取的示例。理解这些基础知识和注意事项能帮助开发者编写更可靠的程序。
19 0
|
6天前
|
Unix Linux 开发工具
【探索Linux】P.11(基础IO,文件操作)
【探索Linux】P.11(基础IO,文件操作)
13 0
|
6天前
|
C++ 数据格式
【C++】C++中的【文件IO流】使用指南 [手把手代码演示] & [小白秒懂]
【C++】C++中的【文件IO流】使用指南 [手把手代码演示] & [小白秒懂]
【C++】C++中的【文件IO流】使用指南 [手把手代码演示] & [小白秒懂]
|
6天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
24 0
|
6天前
|
存储 Java Linux
【Java EE】 文件IO的使用以及流操作
【Java EE】 文件IO的使用以及流操作
|
6天前
|
存储 缓存 安全
Java 中 IO 流、File文件
Java 中 IO 流、File文件
|
6天前
|
Java Unix Windows