Linux 基础IO(上)

发布于:2025-06-01 ⋅ 阅读:(29) ⋅ 点赞:(0)

目录

前言

重谈文件

文件操作

1.打开和关闭

2.对文件打开之后操作

理解文件fd

1.文件fd的分配规则与重定向

2.理解shell中的重定向

3.关于Linux下一切皆文件

关于缓冲区

1.为什么要有缓冲区

2.缓冲区刷新策略的问题

3.缓冲区的位置


前言

  本篇到了我们linux中的文件部分,是我们文件部分的第一篇,再通过往后几篇内容,大家可以把我们之前在c语言中学过的文件操作与linux下文件部分联系起来,并且也能加深我们对于系统中文件的理解!!

重谈文件

1.空文件也要在磁盘占据空间

2.文件 = 内容 + 属性

3.文件操作 = 对内容 + 对属性 or 对内容和属性

4.标定一个文件,必须使用:文件路径 + 文件名(具有唯一性)

5.如果没有指明对应的文件路径,默认是在当前路径进行文件访问

6.当我们把fopen,fclose,fread,fwrite等接口写完之后,代码编译之后,形成可执行程序之后,但是没有运行,文件对应的操作有没有被执行呢? 答:没有被执行——对文件的操作本质进程对文件的操作

7.一个文件要被访问,就必须要被先打开(被用户进程+操作系统打开)

是不是所有磁盘上的文件都被打开了?答:不是

a.被打开的文件

b.没有被打开的文件(在文件系统时会谈)

所以,文件操作的本质:进程 和 被打开文件 的关系

image-20250530184902773

小tip:vim批量化注释:esc界面,ctrl+v之后,j下拉选择代码,输入大写i,再输入//,按esc就注释了所选行;取消按u

文件操作

1.打开和关闭

先了解一下标记位传参

image-20250502153434891

了解了flags这样的构成之后,我们看在系统层面是用open来打开文件

image-20250502153722743

open函数的三个参数

参数1:要打开的文件

参数2:flags也就是相应的标记位(只读/只写/读写)(使用不同的比特位来表示不同的含义)

image-20250502153922197

参数3:要给文件的权限

失败返回-1

系统层面使用close配open来关闭文件

image-20250502154412951

注意:如果我们用open传第二个参数时只传了读写的标记位,如果当前没有这个文件,在运行这个文件操作的程序时,会提示说没有该文件,在c层面会自动帮我们创建文件是因为它封装时添加了这个功能,我们要想在open这里让它自动创建文件得在该标记位后再加一个标记位|O_CREAT

image-20250502155702623

image-20250502155740856

image-20250502155901545

但是看到这里我们这样来操作创建的文件是标红的,看前面的权限是混乱的,所以这里我们要传第三个参数来设置文件的默认权限

image-20250502162522081

[^]  目录起始权限777,普通文件666 

image-20250502162600169

但是看这里的权限和我们给的是对应不上的,因为我们系统给的默认掩码umask是0022,也就是让设置的权限&(~umask)之后得到的才是文件的权限,如果不想这样,可以通过umask函数将子进程执行时的系统掩码设置为0

image-20250502163440306

image-20250502163603241

image-20250502163626833

[^]  现在就是我们自己设置的权限 

2.对文件打开之后操作

1.写文件

image-20250502164008887

我们在系统层面就这一个写的函数

第一个参数:要写入的文件

第二个参数:想写的时候对应的缓冲区数据在哪里(操作系统看来都是二进制)

第三个参数:缓冲区当中的字节个数

返回值就是我们写了几个字节(在网络那边比较常见)

我们说以w方式单纯的打开文件,c会自动清空内部的数据 ,但是在系统层面可不会自动清空文件的数据,所以这样如果我们想两次写入不同的内容,覆盖清理会不完全,所以在open打开函数那里的第二个参数还得传一个标记位——O_TRUNC

image-20250502172854935

将O_TRUNC改为O_APPEND为追加

2.读文件

以读的方式打开文件需要open第二个参数传O_RDONLY

读取函数read()

image-20250503105352036

[^]: ssize_t为系统定制的类型,是有符号整数,可以>/</=0

第二个参数是读到哪个缓冲区

第三个参数是要读多少个到缓冲区

成功会返回自己读到了多少个字节,0表明读到了文件结尾

image-20250503112617119

总结:

fopen fclose fwrite fread fseek ——库函数接口(封装了系统调用接口)

open close write read lseek ——系统调用接口

理解文件fd

fd为文件描述符

image-20250530184601823

image-20250503120956051

回答a

三个标准输入输出流:

stdin —— 键盘

stdout —— 显示器

stderr ——显示器

image-20250503121939168

FILE是结构体,其中必定有一个字段——文件描述符

所以取出来它们分别是0,1,2,被占用了,所以我们自己的文件描述符是从3开始的

image-20250503121931027

回答B

文件描述符的本质就是数组的下标

操作系统内标定进程和文件之间的关系用的就是文件描述符表,用数组来标定文件内容

image-20250503140825973

1.文件fd的分配规则与重定向

从小到大按照顺序寻找最小的且没有被占用的fd

image-20250505162913542

所以如果我们先把0这个位置文件关掉,那么我们下面创建一个文件时所占的文件描述符是0

image-20250505163238539

image-20250505163256651

我们在关闭fd为2的时候也是和上面一样,可当我们关闭fd为1的文件时,结果却没有打印,是什么原因?

image-20250531172230972

因为我们的printf是输出到标准输入中的,可现在fd为1的位置的stdout被我们关掉了,现在是被我们写的文件占用,所以没法显示,但是如果我们在下面用fflush(stdout)刷新缓冲区,那么就会在我们的文件中看到我们打印的内容,因为此时的1是被我们的文件占用,自然刷新之后就可以在我们的文件中找到

image-20250505210417230

我们把上面这种特性叫做重定向

c重定向的本质:上层用的fd不变,在内核中更改fd对应的struct file*的地址

我们有没有什么方法直接重定向呢?

image-20250505212121018

[^]: 头文件为:#include<unistd.h>>

用dup2函数将一个文件描述符对应文件的内容拷贝到另一个文件描述符对应的文件中

所以如何直接重定向,让1指向我们的文件呢?

答:我们需要把fd中的内容拷贝到1中,也就是dup2(fd,1);

image-20250505213754772

追加重定向和dup2没有关系,和文件描述符表里面的内容做修改没有关系,只和打开文件的方式有关

image-20250506094423399

输入重定向:让我们不从键盘上读取内容,而是到我们的文件中去读取

dup2(fd,0);

image-20250506095317415

2.理解shell中的重定向

(> 输出重定向 >>追加 <输入重定向)

1.执行程序替换的时候,会不会影响曾经进程打开的重定向的文件?

答:不会,替换的是数据和代码,和内核数据结构没关系

image-20250531171638946

如果我们的父进程删除了这个文件,或者是子进程把文件关掉了,是不是父进程就访问不了了?

答:不会,因为一个被打开文件的里面包含了引用计数,指向文件时会让count++,表示有几个指针指向文件,关闭文件其实是让count--,我们close文件其实操作系统做的是让count--,而不是真正的关闭释放文件,我们打开文件其实是让count++

image-20250506205747804

3.关于Linux下一切皆文件

这就是多态的体现!

image-20250506201604856

关于缓冲区

先来看个问题:

image-20250507101257032

image-20250507101321038

可以从上图中看到c接口的函数都被打印了两次,系统接口前后都只是被打印了一次

经过测试,这与fork()函数有关,也和缓冲区有关

1.为什么要有缓冲区

缓冲区的本质:就是一段内存,是内存的一部分

缓冲区的意义:

image-20250507115858081

要把进程的数据拷贝到缓冲区,fwrite这个函数,与其理解为是写入到文件的函数,倒不如理解成是拷贝函数:将数据从进程拷贝到“缓冲区”或者外设中

2.缓冲区刷新策略的问题

如果有一块数据,一次性写入到外设(效率最高)vs 多次少批量写入外设

缓冲区一定会结合具体的设备,定制自己的刷新策略:

a. 立即刷新——无缓冲

b. 行刷新——行缓存——显示器

c. 缓冲区满再刷新——全缓冲——磁盘文件 (效率最高)

两种特殊情况:

1.用户强制刷新

2.进程退出—— 一般都要进行缓冲区刷新

3.缓冲区的位置

image-20250531172709645

从上面的问题的图片可以看到,缓冲区一定不在内核中,因为如果在内核中,write也应该打印两次,所以我们之前谈论的所有缓冲区,都指的是用户级语言层面给我们提供的缓冲区

这个缓冲区在stdout、stdin、stderr的类型FILE*这个结构体中(不仅包含fd,还包括一个缓冲区),所以我们要自己强制刷新时的fflush(文件指针),以及关闭文件时的fclose(文件指针)都要传一个文件指针,都是因为这些文件指针内包含了内部的缓冲区

关于FILE

image-20250530184801162

image-20250507165056213

所以此时我们可以解决一开始的fork()之后重定向三个c语言接口重复打印两次的问题:

代码结束之前,进行创建子进程

  1. 如果我们没有进行>,就只会看到4条打印的信息 ,是因为stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印输出到显示器上(外设),你的FILE内部,进程内部不存在对应的数据啦

  2. 如果我们进行了>, 写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n,但是不足以让stdout缓冲区写满,数据并没有被刷新!!! 当我们执行fork的时候,stdout属于父进程,创建子进程时, 紧接着就是进程退出! 谁先退出,就一定要进行缓冲区刷新(就是修改) ,发生了写时拷贝!!!数据最终会显示两份

  3. write为什么没有呢?上面的过程都和wirte无关,wirte没有FILE,而用的是fd,就没有C提供的缓冲区

缓冲区和操作系统的关系,我们在C语言封装的接口中要拷贝到缓冲区一次,到os中也要拷贝一次到内核缓冲区中,并且操作系统的刷新策略更复杂

image-20250508170308290

os完全自主决定刷新数据出缓冲区的时机,如果os宕机了,数据要是还没更新出去就会导致数据丢失,如果是在银行这种对数据丢失0容忍的地方肯定是不行的

所以我们可以在用户层告知操作系统强制将对应的该文件的内核缓冲区的数据更新到磁盘上

image-20250508171227521

image-20250508171807739

c++的文件IO流也是一样的,是一个类,其中也要包含缓冲区和文件描述符(fd)

尾声

  本篇发布时正好是端午节,那就祝大家端午节快乐!\^o^/