当前位置: 首页 > news >正文

Linux --基础IO

理解"文件"

  1.狭义理解:(1)文件在磁盘里  (2)磁盘是永久性储存介质,所以文件在磁盘上的储存是永久的。(3)磁盘是外设,既是输入也是输出设备。 (4)磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO。

  2.广义理解:Linux下一切皆文件(键盘,显示器,网卡,磁盘抽象化的过程)

  3.文件操作的归类认知:(1)对于0kb的空文件也是会占用磁盘空间的,显示kb是因为文件内容为空。 (2)文件是文件属性和文件内容的集合  (3)所有的文件操作本质都是对文件内容和文件属性的操作。

  4.系统角度:

  (1)对文件的操作本质是进程对文件的操作。当一个文件被读写访问的时候实际上是进程在进行访问,根据冯诺伊曼体系此时文件就会从磁盘被加载到内存之中,进程可以通过cpu访问文件的内容数据和属性。一个进程可以打开多个文件,那么多个进程就可以打开多个文件,此时OS需要管理内存中的文件就必须通过数据结构来将众多的文件关联起来,所以在Linux内核中文件就由一个struct file的结构体管理着每一个文件的内容和属性,然后通过指针的方式将每一个struct file对象连接起来,这样OS就完成了对文件的组织管理,这和进程的管理思想是统一的。

  (2)⽂件的读写本质不是通过 C 语⾔ / C++ 的库函数来操作的(这些库函数只是为⽤⼾提供⽅便),⽽是通过⽂件相关的系统调⽤接⼝来实现的,所以语言层次的库函数也只是封装了系统调用。

  这里使用了fopen来打开文件,此时文件被加载到内存中,通过fwrite,fprintf,fread等接口可以完成对文件的读写操作。

  使用对文件读写的方式模拟cat读取文件,注意:buffer[bytes_read] 不会自动添加 '\0',需手动处理。

  5.stdin & stdout & stderr

  在进程打开以后会自动打开三个输入输出流,分别是stdin,stdout,stderr,这三个流类型都是FILE*,在上面提到过一切皆文件,所以如果进程会自动打开这三个输入输出流那么我们就能够直接对他们进行读写,stdout和stderr都代表显示器,stdin代表键盘。

  

  

  所以我们可以通过写入文件的方式将字符写到显示器然后打印出来。

系统⽂件I/O

  1.打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的⽅案,其中底层有两个宏封装的函数可以进行打开文件。

  这两个函数唯一的区别是多了一个mode,对应的是文件的权限,即可以在打开一个不存在的文件时会新建这个文件并设置他的权限,不过他会继承父进程的umask,所以我们设置的权限是会受umask影响的。结论:如果一个文件不需要新建则使用第一个open,需要创建则需要使用第二个open,否则文件权限会错乱。

  pathname对应的是一个目录下的文件名字,如果只写文件名字不带路径默认在当前进程工作木兰下进行查找。flags参数对应文件操作的标志位,这是一个32个比特位的参数,可以在不同的比特位设置1表示不同的读写权限等,这样就可以一次传"多个"参数,我们可以用以下的代码来理解这种以比特位传递标志位的方法。

  通过比特位的按位操作符我们就能够使传入一个参数执行符号多个标志位的操作,而open中定义了几个常用的标志位

 

  所以我们就能够通过这些参数来实现不同的文件打开读写方式

  所以意味着在c语言中的文件打开,读写函数都是通过调用系统接口以及传递不同的标志来实现的,fopen("file.txt","w") = open("file.txt",O_WRONLY|O_CREAT|O_TRUNC) ,fopen("file.txt","a") = open("file.txt",O_WRONLY|O_CREAT|O_APPEND)  

  系统调⽤接⼝和库函数的关系,⼀⽬了然。所以,可以认为, f# 系列的函数,都是对系统调⽤的封装,⽅便⼆次开发。

  其中open的返回值fd是一个文件描述符,我们可以通过fd对指定的文件进行读写操作,打开失败返回-1。Linux进程默认情况下会有3个缺省打开的⽂件描述符,分别是标准输⼊0, 标准输出1, 标准错误2。0,1,2对应的物理设备⼀般是:键盘,显⽰器,显⽰器。

  所以输⼊输出还可以采⽤如下⽅式:

char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;

  那么这个fd究竟是什么?fd其实是一个数组的下标,是系统层面访问文件的唯一方式。C语言中的FILE* 其实是对fd进行了一个封装,printf,scanf函数这些也是语言层面对系统调用的封装,这不仅是为了用户的方便操作,不需要传递多个标志位等复杂的操作,也是对多系统的一个可移植性,因为上层使用的都是fopen,但是底层有不同的fopen的实现,用户在使用的时候不需要关系系统,只需要关注语言本身即可。

  为什么说fd是一个数组下标,在进程task_struct的内部有一个struct files_struct *files指针,在这个files_struct结构体中又在存在一个struct file* fd_array[]的指针数组,这个file就是我们上面提到的OS用来管理文件的内容和属性的结构

  这个指针数组是每个进程PCB内部独享的,每个元素都是一个file*,文件在被加载到内存中就会创建自己的file对象,其中包括路径属性等一系列信息,通过这个指针就能读写该文件,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

  由于进程在打开以后默认就会打开三个输入输出流,所以我们其他文件的文件描述符都是从3开始的,但是如果我们将其中stdout关闭,下标1就没有元素,此时如果有新打开的文件就会使用1作为它的文件描述符,可⻅,⽂件描述符的分配规则:在files_struct数组当中,找到 当前没有被使⽤的最⼩的⼀个下标,作为新的⽂件描述符

  printf()函数默认是向stdout写入数据,这里的stdout其实是封装的一个宏,代表1的文件描述符,所以是不是意味着如果我们将stdout关闭,再打开一个新文件,此时再使用printf则会向新文件写入?我这里写了一个代码来测试一下

 

  结果显然易见确实将打印的内容输出到文件之中了,这种操作我们称之为输出重定向,常⻅的重定向有: > , >> , <。

  重定向的本质其实是将关闭了的stdout的下标位置的元素的file*指针覆盖成为新打开文件的file*指针,printf是C库当中的IO函数,⼀般往 stdout 中输出,但是stdout底层访问⽂件的时候,找的还是fd:1, 但此时,fd:1下标所表⽰内容,已经变成了log.txt的地址,不再是显⽰器⽂件的地址,所以,输出的任何消息都会往⽂件中写⼊,进⽽完成输出重定向。

  在linux中我们有对应的系统调用函数dup2(int oldfd, int newfd)来完成这种操作.

  但是还有一个问题就是不同的硬件为什么可以使用同样的读写操作?其实并不是,在struct file内部有一个struct file_operation *f_op,file_operation其中的成员变量都是一些函数指针,包括读写函数,也就是说文件或者硬件在初始化的时候都会将它们的读写方法函数进行初始化,每个硬件即使读写方法不同但是也可以通过这种类似多态的方式完成相关的操作,所以说Linux下一切都是文件,包括硬件。这样做最明显的好处是,开发者仅需要使⽤⼀套 API 和开发⼯具,即可调取 Linux 系统中绝⼤部分的资源。举个简单的例⼦,Linux 中⼏乎所有读(读⽂件,读系统状态,读PIPE)的操作都可以⽤read 函数来进⾏;⼏乎所有更改(更改⽂件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进⾏。

  file_operation 就是把系统调⽤和驱动程序关联起来的关键数据结构,这个结构的每⼀个成员都对应着⼀个系统调⽤。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从⽽完成了Linux设备驱动程序的⼯作。

缓冲区

  缓冲区是内存空间的一部分,在内存空间中对文件进行的IO操作并不是直接写入文件的,而是会将需要写入的内容拷贝到缓冲区,在合适的时候由OS写入文件中。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

  为什么要引入缓冲区机制?读写⽂件时,如果不开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作快于对磁盘的操作,故应⽤缓冲区可提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

  标准I/O提供了3种类型的缓冲区:

  1.全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。

  2.⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准
I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
  3.⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来。
这里写了一个代码关闭stdou t然后再打开log.txt想完成重定向输出的过程,但是执行的时候却并没有输出到文件之中,这是因为缓冲区的刷新已经由行缓冲区转变为了全缓冲区,此时缓冲区还没有满所以不会刷新到文件中,但是我们可以强制刷新到文件中使用fflush,或者不关闭文件,因为进场结束也会自动刷新缓冲区,但是提前关闭了文件就导致无法缓冲区内容无法刷新到文件中。
 
所以我们就可以在上一章我们写的myshell中增加重定向输出,输入以及追加,需要注意的是要在子进程中进行重定向操作,否则会影响进程的后续操作,先在子进程重定向再替换程序也会影响替换的程序,因为替换程序只是替换了代码和数据,但是fd_arry是存在PCB内部的,不受进程替换的影响。
  我们只需要在解析命令之前提前分析传入的argv之中是否有重定向符号,返回将标识符设为对应重定向操作,提取需要重定向的文件,接着在执行命令的时候根据标识符提前完成重定向,再替换程序完成指令。
  我们上面说过FILE是对fd的一个封装,因为IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通过fd访问的。
  所以有这么一个代码需要我们来研究
此时打印为
但是如果将重定向到文件中就会printf和fwrite重复打印
这是因为第一个打印fork之前文件描述符stdout遇到换行符就会刷新到屏幕中,缓冲区被清空,

write 直接写入内核,也不会在标准 I/O 缓冲区停留。所以 fork 时,父进程的标准 I/O 缓冲区已经是空的,子进程复制父进程地址空间后,不会有残留的缓冲区内容需要输出。最终父进程和子进程退出时,标准 I/O 缓冲区无内容,因此不会重复打印

  当输出被重定向到文件时,printf/fwrite 的标准 I/O 缓冲区会切换为 全缓冲(需要缓冲区写满、显式 fflush 或程序退出时才会刷新)。fork 的关键影响:程序执行流程中,printf 和 fwrite 的内容会先放到标准 I/O缓冲区(因为全缓冲,没触发刷新),然后执行 fork:fork 会复制父进程的整个地址空间,包括标准 I/O 缓冲区里未刷新的内容(printf 和 fwrite 的数据)。之后,父进程和子进程各自退出时,标准库会自动刷新缓冲区,导致:
  printf 的内容:父进程退出刷一次,子进程退出刷一次 → 重复。
  fwrite 的内容:父进程退出刷一次,子进程退出刷一次 → 重复。
  write 是系统调用,直接写入内核,不经过标准 I/O 缓冲区,所以只输出一次。

  所以说明了这个缓冲区是存在于语言层面的,与操作系统内核级的缓冲区并不相同,这也是c/c++语言高效的原因之一,为了减少系统调用采用了和内核级缓冲区一样的思路去储存用户输入输出的内容,因为系统调用也是有消耗的。那么这个缓冲区究竟是在哪里?我们可以在库中发现是和fd一起封装在FILE结构体之中,会将用户读写的内容开辟内存空间进行储存,当需要全缓冲区满了或者行缓冲区遇到换行符就会将其中的内容写到系统的内核级缓冲区,由系统自主写入文件之中。

  了解到了以上的原理知识我们知道了IO相关的操作都是对系统调用封装,所以我们自己也能去封装属于自己的库,这里就模仿封装一个简单的libc库

  完整代码已经上传gitee仓库,需要的可以自取https://gitee.com/toutie40/study_code.git

http://www.xdnf.cn/news/1027225.html

相关文章:

  • 大模型的开发应用(十):对话风格微调项目(上):数据工程与模型选型
  • 安卓开发常用框架与库详解
  • 发现 Kotlin MultiPlatform 的一点小变化
  • 技术干货 | 注塑件电磁网格划分指南(HyperMesh+SimLab)
  • BIO网络通信基础(TCP协议)
  • Dock最新方法
  • 第二十三章 23.Wireless LAN(CCNA)
  • Linux 文件系统核心概念
  • Atlassian AI(Rovo)在不同场景中的实际应用:ITSM、HR服务、需求管理、配置管理
  • Git Switch 与 Git Restore 详解
  • yum查看历史操作
  • 高并发场景下接口安全实现方案:全方位构建防护体系
  • 重复的囚徒困境博弈中应该如何决策?--阿克塞尔罗德竞赛(Axelrod‘s Tournament)实验
  • Spring注解的深层含义
  • 人工智能 倒底是 智能 还是 智障?
  • OmoFun动漫官网,动漫共和国最新入口|网页版
  • java集合篇(七) ---- ArrayList 类
  • BeckHoff_FB --> F_SEQ_X3_TrigJob 函数
  • TCP客户端进程分割输入输出
  • 【Qt】工具介绍和信号与槽机制
  • SpringCloud2020-alibaba
  • DDD各种架构详细介绍
  • CLONE——面向长时任务的闭环全身遥操:其MoE架构可实现“蹲着走”,且通过LiDAR里程计和VR跟踪技术解决位置偏差问题
  • 【61 Pandas+Pyecharts | 基于Apriori算法及帕累托算法的超市销售数据分析可视化】
  • 力扣-279.完全平方数
  • 三维重建 —— 3. 单视几何
  • 国产用例管理工具评测:Gitee Test、禅道、蓝凌测试、TestOps 哪家更懂研发协同?
  • 全流程TOUGH系列软件实践技术应用
  • electron-builder打包配置(应用名、安装包、图标、快捷方式、自定义文件关联启动等)
  • Matlab的GUI编程之一