基于操作系统的基础IO

这篇具有很好参考价值的文章主要介绍了基于操作系统的基础IO。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.c语言中的文件输入输出

在观看本章节的时候,大家可以去看看之前c语言的文件输入输出的有关博客。

好那么废话不多说之间进入本章正题:

我们都知道c程序默认会打开三个输入输出流:

  • stdin:相当于键盘

  • stdout:相当于显示器

  • stderr:相当于显示器

    这些输入输出流的类型为FILE* ,其由c语言提供,当作文件来处理

    下面我将引进一个概念,其可能在现阶段听起来很突兀,不过随着之后的逐步讲解会越来越感同身受-----一切皆文件法则(脑海中先有这个意识就好)

2.文件的调用接口介绍

在我们介绍之前先回想一下一些我们曾经用过的文件操作细节:

比如fputs函数是向一般文件或者硬件设备写入,其本质是在磁盘上写,由此可推测我们之前所设计到的几乎所有语言的有关文件操作符的底层一点需要使用os的系统调用。

2.1linux下系统调用函数open介绍

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。(其实就是位运算,这些选项可以看出#define 提前设置好的)
参数:
 O_RDONLY: 只读打开
 O_WRONLY: 只写打开
 O_RDWR : 读,写打开
 这三个常量,必须指定一个且只能指定一个
 O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 O_APPEND: 追加写
  返回值:
 成功:新打开的文件描述符
 失败:-1
2.2write ,read, close, lseek介绍

在Linux下,这几个函数用于文件IO操作:

  • write: 将数据写入文件,语法为write(fd, buf, count)。其中fd是文件描述符,buf是存放数据的缓冲区,count是要写入的字节数。
  • read: 从文件读取数据,语法为read(fd, buf, count)。和write类似,读取fd文件描述符的文件,读取的数据存放在buf缓冲区,读取的最大字节数为count。
  • close: 关闭文件,语法为close(fd)。关闭文件描述符fd对应的文件。
  • lseek: 移动文件读写指针,语法为lseek(fd, offset, whence)。它可以移动fd文件的读写指针,offset是移动的字节数,whence可以是:

SEEK_SET: 相对文件开始处移动
SEEK_CUR: 相对当前位置移动
SEEK_END: 相对文件结束处移动

具体使用例子:

#include <stdio.h> 
#include <unistd.h> 
#include <fcntl.h> 

int main() {
  // 打开文件
  int fd = open("test.txt", O_RDWR); 
  
  // write写入10个字节
  char buf[10] = "hello";
  write(fd, buf, 10);
  
  // lseek移动5字节
  lseek(fd, 5, SEEK_SET); 
  
  // 读5个字节
  char buf2[5];
  read(fd, buf2, 5); 
  printf("%s\n", buf2); // world
  
  // 关闭文件
  close(fd);
} 
2.3open的返回值

总结一下上面的内容:上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口,回顾之前的一张os各部分分层图系统调用接口和库函数的关系,一目了然。

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

从上面open的描述中可以知道,其返回值类型是一个int整形

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.

0,1,2对应的物理设备一般是:键盘,显示器,显示器

所以输入输出还可以采用如下方式:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
 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;
}
2.4files_struct

​ 其实所有文件操作本质上是进程去执行一些对应的函数,进程对文件操作时,必须先打开文件,而打开文件的本质是把文件的相关属性加载到内存中。

一个进程往往可以打开多个文件,所以操作系统得去管理,那怎么去管理呢?

我们的老话"先管理,在组织"便派上了用处,原来在每个进程的pcb的结构体中给了一个指针*file指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

如图:(这里补充说明一点,这个数组当中存的地址实际上是对应文件的虚拟地址)

基于操作系统的基础IO

2.5从硬件层在来看问题

基于操作系统的基础IO

实质上不同硬件的读写方式是不一样的,而为了解决这一问题 ,vfs(虚拟文件系统)便诞生了

vfs不关心到底是什么外设,只是创建一个又一个一模一样的结构体(files_struct)以文件模仿硬件,一切皆文件这个概念在这里再次显现!

3.文件描述符的分配规则

​ 规则:给新文件分配的fd,是从fd_array中找一个最小的,没有被使用的分配出去,作为新的fd。(0,1,2默认被打开) -----不管什么情况牢记这个规则就行了

4.重定向

先来看这样的一段代码:

int main()
{
    close(1);
    int fd = open("./log.txt",O_CREAT | O_WRONLY , 00644);
    printf("hello world\n");
}

会观察到这样的一个现象------所有的文件都没有在屏幕上打印出来,而是写入了log.txt文件内部,这其实就是输出重定向的现象。

为什么呢?

基于操作系统的基础IO

​ 这张图解释的就很好。printf为c语言中的打印本质是向标准输出打印(stdout 类型为FILE* 实质上为语言层上的结构体),可以推测出这个FILE 结构体中一定包含了一个整数,是对应在系统层面的打开这个文件所对应的fd,而我们的close相当于把fd为1的数组里面置换成了nullptr,而此刻在给open打开的文件分配fd的时候,按照文件描述符的分配规则,分配到了1,所以此时printf实际上是在文件当中写入内容。

5.dup2系统调用

上面的例子是在文件未打开的时候用了close并利用文件打开时新分配fd来实现重定向的,那么我们应该如何在文件已经打开后完成重定向呢?

dup系列的函数可以做到这一点。

dup2() 是一个系统调用函数,用于复制文件描述符。它可以将一个已经打开的文件描述符复制到另一个文件描述符,从而使得这两个文件描述符都指向同一个文件、管道或者 socket 等。

dup2() 函数的定义如下:

#include <unistd.h>

int dup2(int oldfd, int newfd);

其中,oldfd 是要被复制的文件描述符,newfd 是要复制到的文件描述符。dup2() 函数返回值为新的文件描述符,如果出现错误则返回 -1。

当 dup2() 调用成功后,newfd 所对应的文件描述符将指向与 oldfd 所对应的文件相同的文件对象。如果 newfd 已经打开,dup2() 函数会先关闭 newfd,然后将其复制为 oldfd。如果 oldfd 和 newfd 相等,则不执行任何操作。

dup2() 函数一般用于重定向标准输入、标准输出和标准错误输出。例如,如果我们想要将命令行程序的标准输出重定向到文件,可以使用 dup2() 函数将标准输出文件描述符(stdout)复制到文件描述符对应的文件上,代码示例如下:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
    dup2(fd, STDOUT_FILENO);
    printf("Hello, world!\n"); // 该语句的输出将被重定向到 output.txt 文件中
    close(fd);
    return 0;
}

在上述示例中,我们首先通过 open() 函数打开了一个名为 “output.txt” 的文件,并将其文件描述符保存在变量 fd 中。随后,我们使用 dup2() 函数将标准输出文件描述符(STDOUT_FILENO)复制到 fd 对应的文件上,这样,所有输出语句(如 printf())的输出就会被重定向到 “output.txt” 文件中。最后,我们关闭了文件描述符 fd 并退出程序。

需要注意的是,如果在使用 dup2() 函数时 newfd 所对应的文件描述符已经被打开,那么它将会被先关闭,这意味着对于 newfd 所对应的文件的任何更改都将丢失。因此,在使用 dup2() 函数时,应该确保 newfd 不会对应任何重要的文件描述符,或者在调用 dup2() 函数之前先保存 newfd 的状态。

6.对FILE的进一步理解

6.1概念

其实在我们上文当中就讲过这个问题,其本质就是定义了c语言的缓冲区并且拥有fd的一个由c语言提供的结构体。(在c语言层面上使用类似于fwrite,printf这样的函数时,实际上就是在向文件写入时,这些数据会先放在c语言层面的缓冲区上缓存以提高I/O性能)

当进程退出的时候,会刷新FILE内部的数据到OS的内核缓冲区。

6.2文件内核缓冲区(这里其实是个略讲,后面会有详细讲解)

先看一张图:

基于操作系统的基础IO

文件内核缓冲区是操作系统内核中用于文件I/O的缓冲区。具体来说,当进行文件读操作时,内核会将文件的数据块读取到内核缓冲区中,然后在刷新到用户缓冲区中。当进行文件写操作时,数据会先被写入用户缓冲区中,然后再被刷新到内核缓冲区中,最后再被写入磁盘或其他设备中(这两种情况实质就是发生系统调用时所发生的)。为了减少数据刷新的次数和性能损失,C 语言提供了多种缓冲区操作函数,如 fread()、fwrite()、fgets()、fputs() 等,它们可以直接访问用户缓冲区,避免了多次数据的多次刷新。

为什么能减少次数呢?这里给大家举个具体例子:
如果要调用write写10次一次写一个字节,那么就得要从用户态切换回内核态10次,如果此时加上了用户级缓冲区,那么这10次写的字节可以先积累在用户态中然后用户级缓冲区里面,在通过一定条件给文件内核缓冲区

6.3缓冲区的刷新策略(用户->OS)
  1. 立即刷新(不缓冲),如进程退出时

  2. 行刷新(行缓冲\n),比如显示器打印

  3. 全缓冲(缓冲区满了才刷新) 当缓冲区满时,标准库会自动将其内容写入文件。

    (默认刷新方式为行缓冲)

6.4缓冲区究竟以一种什么方式存在

在 Linux 下,用户缓冲区和内核缓冲区所分配的内存都是实实在在的物理内存。它们不是虚拟内存,而是直接映射到物理内存中。

当一个进程需要访问文件或设备时,操作系统会将数据从磁盘或设备读取到内核缓冲区中,然后将数据从内核缓冲区复制到用户缓冲区中,最终让进程访问用户缓冲区中的数据。这个过程中,数据的传输是在物理内存中完成的。因此,用户缓冲区和内核缓冲区都是占用物理内存的。

需要注意的是,Linux **中的内存管理机制是基于虚拟内存的。**操作系统会将物理内存分为多个虚拟地址空间,每个进程都有自己的虚拟地址空间。当进程访问内存时,操作系统会将虚拟地址映射到物理地址上,从而让进程能够访问内存。需要补充说明的是,Linux 中的用户缓冲区和内核缓冲区所占用的物理内存是通过操作系统的内存管理机制分配和管理的。在内核中,有专门的内存管理子系统来管理内核空间的内存分配和释放,而在用户空间中,进程的内存分配和释放则由 C 库的 malloc() 和 free() 等内存分配函数来完成。这些函数实际上是通过系统调用 brk() 或 mmap() 来向操作系统请求内存,由操作系统来分配和管理物理内存。因此,用户缓冲区和内核缓冲区所占用的物理内存都是由操作系统来管理的。

6.5几个经典的例子
#include <stdio.h>
#include <string.h>
int main()
{
 const char *msg0="hello printf\n";
 const char *msg1="hello fwrite\n";
 const char *msg2="hello write\n";
 printf("%s", msg0);
 fwrite(msg1, strlen(msg0), 1, stdout);
 write(1, msg2, strlen(msg2));
 fork();
 return 0;
}

打印结果:

hello printf
hello fwrite
hello write

对进程实现输出重定向 ./hello > file(相当于把上面这段代码的运行结果写入了名为"file"的文件当中(代码里面相当于在最前面加个close(1))) , 结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

为什么会出现这一现象呢?(ps:以下的缓冲区默认为用户级缓冲区)

  • 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。

  • printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据

  • 的缓冲方式由行缓冲变成了全缓冲。

  • 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后

  • 但是进程退出之后,会统一刷新,写入文件当中。

  • 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的

  • 一份数据,随即产生两份数据。

  • write 没有变化,说明没有带缓冲区。

7.进一步了解文件系统

基于操作系统的基础IO

上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。

  • Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相

    同的结构组成。政府管理各区的例子

  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,

    未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的

    时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个

    文件系统结构就被破坏了

  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下

  • 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没

    有被占用

  • inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。

  • i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等

  • 数据区:存放文件内容

    从上图可以看出数据区和超级块(记录相关属性)是分开放置的,那么这两板块的内容是通过什么方式来运作的呢?
    基于操作系统的基础IO

    以上的图片就做了4个步骤:

  1. 存储属性

内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。

  1. 存储数据

该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据

复制到300,下一块复制到500,以此类推。

  1. 记录分配情况

文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

  1. 添加文件名到目录

新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文

件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

通过上述的理解,下面我们再来理解两个概念:

硬链接:我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。

软链接:硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
指令:ln -s 源文件 目标文件 (删除用unlink 就好)文章来源地址https://www.toymoban.com/news/detail-462337.html

到了这里,关于基于操作系统的基础IO的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 基于文件流操作文件系统

    这里涉及一系列的类: 字节流: InputStream和OutputStream是以操作 字节 为单位(二进制文件). 字符流: Reader和Write是以操作 字符 为单位(文本文件) 字节数组存储read来的字节数据. 在操作系统中, 操作系统是一个广义的概念,System.in也是一个特殊的文件,对应标准输入. Scanner也可以来读取

    2024年04月26日
    浏览(32)
  • 基于Linux操作系统中的MySQL数据库操作(三十)

    MySQL数据库安装 目录 一、概述 (一)什么是数据库 (二)数据库能干什么     1、企业应用     2、金融行业     3、电子商务     4、社交媒体     5、物联网 (三)为什么要用数据库,优势、特性?     1、可靠性和稳定性     2、数据管理能力     3、数据共享和集成  

    2024年02月15日
    浏览(61)
  • 文件操作介绍及C语言实现通讯录管理系统3.0最终版(文件操作版本)

    上一篇文章我们学习了动态内存开辟的相关知识点,并用动态内存函数优化了我们的通讯录,但通讯录还有需要改进的地方,比如,正常情况下的通讯录,应该可以一直保存联系人信息,而不是退出就清空了,这就需要我们实实在在的保存下来一个通讯录。 接下来我会给大家

    2023年04月08日
    浏览(59)
  • 【C语言】简易登录注册系统(登录、注册、改密、文件操作)

            本登录注册系统通过使用C语言中的结构体、函数、文件操作以及指针等,设计与实现了一个小型用户登录注册系统的登录、注册、修改密码等基本功能。         本系统全部功能基本运行良好、用户界面友好、操作简单、使用方便。但系统仍然有不完善之处。例如

    2024年02月03日
    浏览(37)
  • 如何在linux(ubantu)操作系统运行c语言文件

    关于linux的其他文章: ​1.linux命令、vi命令、vim命令、shell语法(完整详细) 2.linux(ubantu)系统如何安装vim编辑器以及如何使用vim 3.如何在linux(ubantu)操作系统运行c语言文件 4.如何在Linux(ubantu)系统通过c程序将文档1指定内容替换到文档2的指定内容 5.如何在Linux(ubantu)系统

    2023年04月08日
    浏览(44)
  • 操作系统- IO多路复用

    1) IO多路复用是操作系统的原理,但是很多中间件的实现都是基于它去做的,IO多复用需要 知道整个链路是样子的,输入是什么,输出是什么 2) 了解IO多路复用作用的位置是哪里 3.1.1 IO多路复用的输入 IO多路复用的输入是Socket文件 3.1.2 IO多路复用的输出 IO多路复用的输出是读

    2023年04月08日
    浏览(37)
  • [操作系统]5.IO控制

    老规矩,仅为整理知识点,知识框架来自于王道操作系统 并且IO控制的更多知识其实是在计算机组成原理的部分,所以这里一些关于中断具体措施这种细节就先跳过了 (1)IO设备概述 io设备是计算机中最复杂的一类设备,涉及到的基本都是系统层面的调用 例如显示器,键盘,音响这种

    2024年02月08日
    浏览(72)
  • Hadoop HDFS分布式文件系统(介绍以及基础操作命令)

    目录 一、为什么需要分布式存储? 二、分布式的基础架构分析  三、HDFS基础架构 1.HDFS简介 四、HDFS集群启停命令 1.一键启停脚本 2.单进程启停 五、HDFS基本操作命令 1.创建文件夹  2.查看指定目录下内容  3.上传文件到HDFS指定目录下  4.查看HDFS文件内容 5.下载HDFS文件  6.拷贝

    2024年02月05日
    浏览(71)
  • 基于Linux操作系统中的MySQL数据库SQL语句(三十一)

    MySQL数据库SQL语句 目录 一、SQL语句类型 1、DDL 2、DML 3、DCL 4、DQL 二、数据库操作 1、查看 2、创建 2.1、默认字符集 2.2、指定字符集 3、进入  4、删除 5、更改 6、练习 三、数据表操作 (一)数据类型 1、数值类型 1.1、TINYINT 1.2、SMALLINT 1.3、INT 1.4、BIGINT 1.5、FLOAT(M,D) 2、时间

    2024年02月15日
    浏览(68)
  • 【操作系统】基于动态优先级的进程调度算法-C语言实现(有代码)

    本文章将会介绍如何编写动态优先级的进程调度算法,并使用从语言实现。 一、什么是动态优先级的调度算法        进程运行一个时间片后,如果进程已占用 CPU时间已达到所需要的运行时间,则撤消该进程;如果运行一个时间片后进程的已占用CPU时间还未达所需要的运行

    2024年02月06日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包