目录

Linux第18节-重定向与文件IO的基本认识

Linux第18节 — 重定向与文件IO的基本认识

一、引入一些文件的共识原理:

  1. 文件 = 内容 + 属性;
  2. 文件分为打开的文件和没被打开的文件;
  3. 打开的文件是谁打开的?(进程!)因此本质上是研究进程和文件之间的关系!
  4. 没打开的文件在哪里放着?(磁盘上!)此时我们应该关注什么问题?(没有被打开的文件非常多,文件如何被分门别类的放置好,即文件应该如何存储?—> 这样做的目的是方便我们快速的找到文件并进行 增删查改 的工作!) 文件被打开,首要的是必须被加载到对应的内存当中(理想情况是内容和属性都加载,但是属性是必须的,内容如果不进行读取等操作可以不加载); 一个进程可以打开多个文件; https://i-blog.csdnimg.cn/direct/adee01e4d4674135a6890e8ab61757ac.png 对文件的管理即:先描述再组织! 默认情况下,C/C++会打开三个标准流(输入,输出,错误) — > 一个进程可以打开多个文件; 当以"w"的形式读取一个文件的时候,如果该文件不存在,会创建一个新文件; https://i-blog.csdnimg.cn/direct/165a538acf9a4543a3f986f5c40dbacf.png 此时创建文件的时候,默认是在当前路径下创建文件!(什么是当前路径?) 即当前该进程运行的时候,所对应的路径! 这里我们可以通过proc目录下的cwd查看当前进程的当前路径! https://i-blog.csdnimg.cn/direct/183d9fd496a740a1be0eb8358a43aa60.png 当前路径:当前进程的cwd路径! 如果我们想要改变当前的工作路径,可以通过系统调用接口chdir来实现! 如下所示: https://i-blog.csdnimg.cn/direct/66d8ab4e384a4df19d59edee41919d40.png 此时新创建的文件就会在我们chdir指定的路径中! 接下来我们介绍两个C语言中关于文件的函数:

1、 fopen 函数:打开文件

fopen 用于打开文件并建立文件流,其原型为:

FILE *fopen(const char *filename, const char *mode);
  • 参数
  • filename :文件名(含路径),需注意路径中的转义字符(如 C:\\file.txt 需写成 "C:\\\\file.txt" ) 。
  • mode :文件打开模式,决定读写权限和操作方式(见下表)。
  • 返回值 :成功返回 FILE 指针,失败返回 NULL ,需通过判断返回值确保文件打开成功 。

文件打开模式 https://i-blog.csdnimg.cn/direct/b4cd23e9ebd04883942a33c57bd217e2.png

  • 注意事项
  • "r" 模式下文件不存在会失败,而 "w""a" 会自动创建文件 。
  • 二进制模式( b )用于非文本数据(如图片、结构体),避免编码转换问题 。 总结:w方法在写入前会对文件进行清空! https://i-blog.csdnimg.cn/direct/55cef4ed32da4f8ca6bc71bded34863c.png 类似的,在之前我们使用的输出重定向也是以 “w"的形式进行写入的!

2、 fwrite 函数:写入数据

1. 功能与原型

fwrite 用于将数据块写入文件,支持二进制操作,其原型为:

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 参数
  • ptr :待写入数据的指针(如数组、结构体地址)。
  • size :单个数据块的字节数(如 sizeof(int) )。
  • nmemb :数据块数量。
  • stream :由 fopen 返回的文件指针 。
  • 返回值 :实际写入的数据块数目,若与 nmemb 不同可能表示错误 。 写入字符串的时候,如果写入的字符包含\0,此时就会以乱码的形式显示! https://i-blog.csdnimg.cn/direct/c5f287a469c242ada7fded307c5d6d26.png 因此我们写入字符串的时候不要带\0!!! 带\0是C语言的规定,但是对于写入到文件的内容来说,是需要其他人或软件或语言来读取的,这种读取和C语言的规定没有关系! a方式写入是在文本末尾追加的形式写入!如果该文件不存在此时也会创建文件! 对于重定向:>是以w的方式打开         »是以a的方式打开!

二、认识文件的系统调用

https://i-blog.csdnimg.cn/direct/5f7869965a174becb375422b7988e484.png 这里,当我们的文件还没有被加载的时候,是存放在磁盘当中的,因此访问磁盘文件,实际上就是访问硬件! 而用户如果需要访问硬件,那么只能经过操作系统; 而操作系统不相信用户,因此只能通过系统调用接口来进行访问; 此时的printf/fprintf/fscanf/fwrite等库函数实际上底层调用的就是系统调用接口! https://i-blog.csdnimg.cn/direct/626feb70b68b46088c62ad0607b66df9.png 上面一个函数经常用于当前文件已经存在,我们要对其进行写入; 下面一个函数经常用于当前文件不存在,此时我们要进行写入的时候需要调整其权限! 使用open的示例代码: https://i-blog.csdnimg.cn/direct/c772778bbd984da6b2962b115d9ee969.png https://i-blog.csdnimg.cn/direct/bcda09f805b0400bbaba23e000be5357.png 注意点一:如果该文件不存在,此时我们还需要 + O_CREAT这个选项; 这时候显示的代码权限完全不正确! 这是因为在Linux中,如果我们新建文件,必须要指定文件的权限! 所以此时我们还需要第三个参数! https://i-blog.csdnimg.cn/direct/9e4e50e7569a4071af5fea9925d7fe04.png https://i-blog.csdnimg.cn/direct/0d48fc7c854b47739f018f740505b191.png 但是这里当我们指定权限为666的时候,最后的权限结果却为664! 这是因为还存在权限掩码! 因为我们可以调用一个系统调用接口为umask来改变当前进行的掩码! https://i-blog.csdnimg.cn/direct/2e8f37afd42c42238582e35d36511b01.png 该设置不会影响这个系统,但是会影响该进程! open成功后返回的整数是文件描述符! 如果我们想覆盖式写入:可以采用下面这个宏: O_TRUNC; 如果我们想追加写入,可以采用下面这种形式: O_APPEND https://i-blog.csdnimg.cn/direct/ef1154028b7c41ba9d0f90f2fb00d117.png 因此此时我们可以推断出:对于上面的C语言类型的fopen,封装的系统调用一定是按照下面的形式进行的! https://i-blog.csdnimg.cn/direct/f889e565356a454a9798a03bede7e394.png 当一个进程打开多个文件的时候,此时需要对该文件进行组织! 即先描述再组织,这里也就是struct_file(描述已打开的文件的信息); struct_file里面包含了该的大部分信息; deepseek解释如下: 在Linux内核中, struct file 是表示 已打开文件 的核心数据结构,用于管理文件在进程中的动态状态。以下是其关键特性的详细解析:

  1. 基本定义
  • struct file 定义在 include/linux/fs.h 中,内核在调用 open() 打开文件时创建该结构体,关闭文件后释放 。
  • 核心作用
  • 跟踪文件的 打开状态 (如读写位置、权限模式等)。
  • 关联文件操作函数(如 readwrite ),通过 file_operations 结构体实现。
  • 存储进程与文件交互的上下文信息(如缓冲区、私有数据) 。 在进程task_struct结构体里面。包含了下面这个指针: struct files_struct *files(包含的是打开文件的信息) https://i-blog.csdnimg.cn/direct/682fda963a2d414ebf19d2faea5511d7.png https://i-blog.csdnimg.cn/direct/eea0d20233494d1084978413af80a866.png 这里的 struct files_struct *files是一个指向指针数组的指针!struct files_struct *files指向一个表,该表称为文件描述表; 里面存放的是已经打开的文件的信息,即struct_file; https://i-blog.csdnimg.cn/direct/26d12fd600044e1bb2d8a3e4bf70ea02.png 当我们调用open的时候,返回的对象是一个int类型;该返回值实际上就是文件描述符表的下标! 结论:fd的返回值本质上是数组的下标! 此时无论是write / close实际上都是对这个下标对应的空间进行管理! 右边的struct file也需要用双链表进行联系! https://i-blog.csdnimg.cn/direct/f68a19ff8cfc4aa794ce52105158d32f.png 用户自己用的时候,默认的第一个文件描述符是3!(失败返回-1) https://i-blog.csdnimg.cn/direct/114aa1699e204fcca3d42128216fd751.png 0,1,2在系统打开的时候,已经被默认占用了! 操作系统中,只认fd!而其他的例如stdin,stdout,stderr都是C语言规定的!本质上只是再封装! https://i-blog.csdnimg.cn/direct/57108bd00e1e4f02b8014e53c58dffbc.png https://i-blog.csdnimg.cn/direct/23a13de60cef42dc93552b540da8ba3d.png 这里我们直接往1里面进行写入,实际上效果与printf没有区别! (直接往2里面写效果也一样!) 接下来我们再尝试fd = 0的情况: https://i-blog.csdnimg.cn/direct/9f6e7525ccfb401bae2cb768719563da.png 这里我们按照读取字符串的形式来读取,保险起见将字符数组最后一个元素设置为\0,方便操作系统能识别到是字符串!(最终效果和scanf一样!) 结论:stdin,stdout,stderr不是系统的特性!是操作系统的特性!系统会默认打开键盘和显示器!

问题:C语言中的FILE实际上是什么?

答:本质是C库自己封装的一个结构体!(这个结构体里面必须包含文件描述符!!!) https://i-blog.csdnimg.cn/direct/f40a968d6fde477aab309908fd00f302.png 这里我们可以通过->来查看对应的文件描述符!!! _fileno实际上就是我们对应的文件描述符 运行结果如下所示: https://i-blog.csdnimg.cn/direct/09d09703b89f41dc87fea474e530c696.png 问题:当我们用close(1)将显示器关闭,此时再输出会出现什么结果? https://i-blog.csdnimg.cn/direct/0decc080ab3b4ee7ab17961899af9e36.png 此时printf无法打印正常的结果!(但是fprint会打印结果!) printf返回值为打印的字符的个数! fprint能打印出来的是因为其文件描述符指定的是2号!1号被关闭与它没影响! https://i-blog.csdnimg.cn/direct/bb67191d26e34186860cdcdb54182ae8.png struct file里面为了记录该文件被几个进程调用,此时会有一个变量count! count是为了实现引用计数! https://i-blog.csdnimg.cn/direct/b439cb05399343859ecb01bdf6500b43.png 此时有几个文件描述符指向,对应的count就会有多少; 因此此时每close一个,对应的count –; 当count = 0;此时文件描述符表就会对该空间进行回收 + 数组下标置空!

三、重定向和缓冲区

https://i-blog.csdnimg.cn/direct/efdf2e04aa274586b9951bf0f9fe0c63.png 当我们尝试创建一个文件并往里面写入东西时,如果我们先将close(0),即将键盘输入关闭,此时printf输出的fd即为0! 若close(1),此时printf无法在显示器上打印! https://i-blog.csdnimg.cn/direct/27dad37a62db4884928ec37e7b208d5f.png 如果我们将2关闭,即close(2),此时printf会正常打印,且打印结果为2! 因此此时我们可以得到一下结论: 文件描述符对应的分配规则是什么? 从0下标开始,依次寻找从0开始的最小数组下标位置,它的下标就是新文件的文件描述符; 接下来我们做一个测试:close(1),然后依次往显示器中写入,看最终的结果是什么? https://i-blog.csdnimg.cn/direct/1d5da76520394cce9bf8152e8ab57bc1.png https://i-blog.csdnimg.cn/direct/72a442451b6d43a3bba5a0350dde046e.png 此时我们可以发现:最终应该显示在显示器中的文件,却写入到文件中! 本来应该写入到显示器中的文件,最后却写入到文件中!这个现象叫做输出重定向! 解释现象:因为刚开始我们就进行了close(1),因此此时创建的fd就是1!然后我们在write在1中写入也就是在fd中写入!最终写入到文件中! https://i-blog.csdnimg.cn/direct/8ced9b14879046458e3edd013570b4ca.png 整体的流程如上所示:主要是对应的文件描述符表的1号下标中的显示器替换为此时我们的log.txt!

重定向的系统调用接口

共有dup1,dup2和dup3,其中最常用的是dup2! https://i-blog.csdnimg.cn/direct/b33247d5f6bb42f3b7a3efbd221a79c3.png 思路:此时我们仅需要将三号的对应的文件的地址覆盖到一号即可; dup2 函数 https://i-blog.csdnimg.cn/direct/32b6bfa8e35c4059b7d4f7a3032cf2ef.png newfd最终会被oldfd所覆盖! (这里的newfd为1,oldfd为log.txt) 实际上是文件描述符对应的指针内容进行了拷贝! https://i-blog.csdnimg.cn/direct/7191c6c71e1e44dc9e3e614d1c67a659.png 这里我们通过重定向可以实现和之前一样的效果! https://i-blog.csdnimg.cn/direct/56544559729b483a85d1d182bfcf52b0.png 这里我们再将对应的fd设置为O_APPEND,即为追加重定向! https://i-blog.csdnimg.cn/direct/4c0b4948094945cca4d9ef98a789ac19.png 这里对于read系统调用接口:count指的是期望读取的字节格式; 返回值为实际上读取的字节个数! 输入重定向:默认从键盘读取数据,然后改成从文本中读取数据; https://i-blog.csdnimg.cn/direct/99527d570a2c4ca08e5d53f312fcd1f2.png https://i-blog.csdnimg.cn/direct/76c166a5c765486bbdae1b94bc7f2aff.png 我们通过dup2将fd重定向到键盘中,然后此时通过read要从键盘当中读取,直接变成了从文件当中读取! 效果与我们之前通过cat实现的输入重定向一样: https://i-blog.csdnimg.cn/direct/46fa413f8b8140448d808ab2db10e8de.png 问题:重定向会影响程序替换吗? https://i-blog.csdnimg.cn/direct/d9608279f3ec47168077ca70285f533e.png 当我们进行重定向得到时候,实际上改变的都是左侧的内核数据结构!和右边的对应的进程的结构无关! 问题:标准输出stdout和标准错误stderr有什么区别? 假设当前我们有如下所示的代码: https://i-blog.csdnimg.cn/direct/dc5b347d9db9417581b98e78f782d655.png 如果我们正常进行打印: https://i-blog.csdnimg.cn/direct/22eb422e49b142b1a9b8b6c40502b362.png 在显示器上两个结果都能显示; 如果我们进行重定向: https://i-blog.csdnimg.cn/direct/97d7441a16864a129699fcc61688ab59.png 结果为:只有stdout的内容会被重定向到文件中;stderr的内容打印在显示器中; https://i-blog.csdnimg.cn/direct/184c6cdbd5d441fa98bfe82a4bcae816.png 这是因为这里只有显示器1被重定向!显示器2没有! 接下来我们可以尝试这样子进行重定向: https://i-blog.csdnimg.cn/direct/214527c0b59b48d9b96a04c87d80c121.png

./mytest 1>normal.log 2>err.log

实际上是分别对1和2进行了重定向! 我们还可以下面这种方式进行写入: https://i-blog.csdnimg.cn/direct/efd9bc8d0a114bafa0f573a75ae3b883.png 2>&1(把1号文件描述符对应的内容写入到2号文件描述符当中!) 刚开始1已经写入到all.log当中,然后此时对应的内容写入到2当中!(2和1一样!)

2>&1 的语法解析(deepseek)

  • > :表示输出重定向。例如 > file 默认将 stdout(fd=1)重定向到文件。
  • 2> :表示将 stderr(fd=2)重定向到指定目标。
  • &1& 符号表示后面的 1 是文件描述符(而非普通文件名)。 &1 指代当前 stdout 指向的位置。

四、如何理解一下Linux中的一切皆文件?

https://i-blog.csdnimg.cn/direct/bb74c2ba85d143d68fe3c528f61c2ff4.png 我们从下面往上面看:

  • 当前我们的计算机有各种不同的零件,操作系统可以对这些硬件进行描述,即struct_device,在这些结构体中主要包括硬件的输入(read)和输出(write) — 如果有哪些硬件没有对应的方法,我们可以将其设置为空!;
  • 而struct device这一层,实际上就是驱动层!
  • 对于已打开的文件,会有一个对应的结构体struct file,而在这个结构体当中,有一个指向操作函数表的指针ops;
  • 操作函数表operation_func中包含了一些文件和设备的具体操作,与struct_device对应! https://i-blog.csdnimg.cn/direct/c51f75f6d376424f8edbc7e18b7fa219.png 因此当我们调用read这样的一个系统调用函数的时候,实际上是从task_struct结构体内获取到files的地址,即文件描述符表,根据对应的fd获取到对应数组位置上的指针,通过指针找到对应的struct file,而在struct file这个结构体中包含了调用函数的指针,再通过调用函数的指针ops找到对应驱动的读写方法!