文件IO1(计算机的组成部分/linux系统的目录/文件操作)

计算机的组成部分

⦁ 硬件系统

计算机的硬件主要是由五部分组成:控制器、运算器、存储器、输入设备、输出设备。其中运算器和控制器的总称是中央处理器(CPU),指的是计算机中对信息进行高速运算处理的主要部件。

存储器则是用来存储程序、数据和文件,一般是由快速的内部存储器(容量可达数百兆字节,甚至数G字节)和慢速的外部存储器(容量可达数十G或数百G以上)组成,比如计算机中的内存条和固态硬件就属于存储器。

在这里插入图片描述
输入设备与输出设备就是用于实现人机交互的信息转换器,由计算机的输入输出控制系统负责管理外部设备与主存储器之间的信息交换,常用的输入设备主要有键盘以及鼠标,输出设备则是显示器、打印机以及其他可连接到计算机上的I/O设备。

⦁ 软件系统

如果计算机只有硬件系统,那么计算机是无法正常工作的,因为计算机是由基本的电子元件组成的,而电子元件的状态是需要由软件来控制的,所以计算机必须要搭载软件系统,计算机的软件系统与硬件系统是互相依赖的。

计算机的软件系统一般由两部分组成:系统软件、应用软件。系统软件指的是计算机中的操作系统以及硬件驱动程序等,应用软件指的是用户可以使用的各种程序设计语言以及利用程序设计语言设计的应用程序的集合。

⦁ 文件系统

计算机的系统软件和应用软件其实都是大量的程序和数据组成的,也就是说两者都需要存储在计算机的存储器中,这些程序和数据以什么样的格式存储到存储器中就由文件系统决定。

大家可以把文件系统理解为是一种“标准”或者“格式”,只要遵循这套标准就可以正确的访问存储在磁盘中的数据。

当然,标准的制定则是至关重要,但是由于技术原因或者商业利益考虑,导致现在文件系统没有办法统一。所以目前存在多种文件系统,常见的有FAT32、NTFS、ext4…,比如电脑的本地磁盘的文件系统就是NTFS格式。

Linux系统也支持多种文件系统类型,可以在Linux系统根目录的/proc目录下查阅filesystems

在这里插入图片描述
计算机中搭载的Linux操作系统就属于系统软件,操作系统的作用是用来连接应用软件和底层硬件,因为涉及到Linux内核的安全管理机制,所以用户空间是没有办法直接访问硬件设备的。
在这里插入图片描述
那么用户必须要通过Linux内核提供的相关函数接口才能实现硬件的控制,这样用户也不需要关心硬件应该如何控制,只需要把参数提交给内核,然后由内核把控制参数传递给硬件设备,从而让内核控制硬件设备完成相关动作。

也正因如此,用户需要先利用程序设计语言设计出源文件(xxx.c),然后操作系统内核需要从磁盘中访问源文件,再通过编译器把源文件编译生成可执行文件,最后内核再访问存储在磁盘中的可执行文件,从而把控制硬件的参数读取出来。

所以访问磁盘中的文件也是操作系统要做的工作之一。所以操作系统和文件系统可以理解为一种“合作”关系,文件系统指定了读写文件的标准,而操作系统会按照这套标准去完成访问文件的动作。

Linux系统的目录

  1. 根文件系统说明

需要注意:Linux系统是离不开文件系统的,在Linux内核启动之后首先搭载的就是根文件系统rootfs,根文件系统rootfs本质就是文件系统,只不过是Linux内核挂载的第一个文件系统。而Linux内核源码是存储在文件系统中,所以在linux系统启动的过程中启动引导程序uboot会加载内核并利用内核挂载根文件系统。

在这里插入图片描述
根文件系统被挂载之后,一些服务程序和一些脚本文件才可以存储在文件系统中,其他的文件系统才可以被挂载。

根文件系统会提供一些服务:比如根文件系统会提供一个根目录(存储文件和目录)、提供一个shell终端(用户才可以输入命令)、根文件系统会存储应用程序。总而言之,Linux内核离开根文件系统是无法正常工作的。
在这里插入图片描述
Linux系统的所有程序和数据都是以“文件”的形式存储在文件系统中,所有Linux 用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。这种机制有利于用户和操作系统的交互,这也是Linux系统“一切皆文件”的原因。

其实就是借鉴了Unix系统的设计风格,体现了面向对象的设计思想,把脚本、文档、硬件设备等都抽象为文件对象,再通过统一的POSIX操
作接口对文件进行操作,简化了操作难度。

  1. 目录的结构分析
    在 Linux 操作系统中,所有的文件和目录都被组织成以一个根节点“/”开始的倒置的树状结构。如下图:
    在这里插入图片描述
    文件系统的最顶层是由根目录开始的,系统使用“/”来表示根目录,在根目录之下的既可以是目录(目录就相当于 Windows 中的文件夹),也可以是文件,而每一个目录中又可以包含(子)目录或文件。如此反复就可以构成一个庞大的文件系统。

使用这种树状、具有层次的文件结构主要目的是方便文件系统的管理和维护,想象一下,如果所有的文件都放在一个目录下,其文件系统的管理和维护将变成一场噩梦。

同样,为了方便观察Linux系统的目录结构,所以Linux内核也提供了一个shell命令:tree,该命令可以以树状结构打印出目录的层次,当然,tree命令需要在Linux系统中提前安装。

在shell终端中输入命令: sudo apt-get install tree 安装tree命令

  1. Linux的文件类型
    由于程序和数据在Linux系统中都是以文件的形式存在,那不同格式的数据所对应的文件类型也各不相同,在Linux系统下可以把文件的类型分为以下七种:
  1. 普通文件 :存在于外部存储器中,用于存储普通数据。
  2. 目录文件 :用于存放目录项,是文件系统管理的重要文件类型。
  3. 管道文件 :一种用于进程间通信的特殊文件,也称为命名管道FIFO。
  4. 套接字文件 :一种用于网络间通信的特殊文件。
  5. 链接文件 :用于间接访问另外一个目标文件,相当于windows快捷方式。
  6. 字符设备文件:字符设备在应用层的访问接口。
  7. 块设备文件 :块设备在应用层的访问接口。

@ubuntu:~$ ls -l
-rw-r–r-- 1 gec gec 345 Sep 12:38 a.zip //-(regular) 普通文件
drwxr-xr-x 2 gec gec 1024 Sep 12:38 dir/ //d(directory) 目录文件
prw-r–r-- 1 gec gec 0 Sep 12:38 pipe //p(pipe) 管道文件
srw-r–r-- 1 gec gec 0 Sep 12:38 socket //s(socket) 套接字文件
lrw-r–r-- 1 gec gec 4 Sep 12:38 link -> a.zip //l(link) 链接文件
crw-r–r-- 1 gec gec 1, 3 Sep 12:38 character //c(character) 字符设备文件
brw-r–r-- 1 gec gec 5, 1 Sep 12:38 block //b(block) 块设备文件

文件操作接口说明

上面提到数据都是以文件的形式存储在Linux系统中,并且Linux系统为了简化不同类型文件的操作流程,在设计访问接口时也遵循POSIX标准,而POSIX标准就是对不同操作系统的访问接口做出统一的规范,目的是提高程序的兼容性和可移植性。

大家经常使用的C语言同样具有语法标准,并且C语言标准在发布的时候也会发布对应的库函数提供给用户。这些库函数也同样遵循POSIX标准进行设计,而遵循POSIX标准设计出来的函数的集合也被称为标准库,比如大家使用的标准C库中提供了标准的输入输出函数,这些函数在Linux系统可以使用,同样也可以在Windows系统中使用。用户可以根据标准输入输出头文件<stdio.h>中的函数声明进行调用,Linux系统下该头文件路径为 /user/include。
在这里插入图片描述
另外,由于任何一种操作系统都会有访问磁盘文件的需求,所以POSIX标准中同样对访问文件的输入输出接口做出了约束,这些访问文件的函数接口在C语言标准中都有具体的描述。

标准C库中关于文件输入输出的函数接口一般被称为标准IO,访问文件常用的标准IO函数有fopen()、fread()、fwrite()、fclose()、fgetc()、fputc()、fgets()、fputs()、fprintf()、fscanf()等。
在这里插入图片描述
⦁ 打开文件

想要对文件进行读写访问的前提是必须先打开文件,标准IO中提供了一个函数叫做fopen(),用户只需要包含标准输入输出头文件 #include <stdio.h> 即可调用。

在这里插入图片描述
fopen函数是有返回值的,如果文件打开成功,则返回值返回指向该文件的文件流指针,如果文件打开失败,则返回值为NULL。
在这里插入图片描述

思考:fopen函数的返回值是一个指向被打开文件的FILE类型的指针,请问FILE类型是什么?

回答:FILE类型其实是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,比如包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。头文件stdio.h中有关于FILE类型的相关描述,如:
在这里插入图片描述
思考:可以看到FILE类型其实就是一个结构体,结构体类型名称为struct _IO_FILE,但是经过查找之后发现头文件stdio.h中并没有关于该结构体的定义,那这个结构体中到底都有哪些成员?

回答:阅读stdio.h中的条件编译选项可以发现在stdio.h中还包含了另一个头文件<libio.h>,这个头文件中才有关于FILE结构体类型的定义,该头文件的路径同样在Linux系统的/user/include目录下。

在这里插入图片描述
打开头文件<libio.h>之后,可以找到关于FILE结构体类型的定义,可以发现FILE结构体类型中的成员数量很多
在这里插入图片描述
可以看到FILE结构体类型中有一个成员是FILE类型的指针变量chain,该指针可以指向下一个被打开文件的文件信息区,也就是可以把FILE类型当做数据结构中的链表的结点,结点中除了可以存储数据域之外,还可以利用指针域存储下一个结点的地址。

简单理解:用户可以在一个程序中利用fopen函数打开多个文件,每次打开一个文件,内核就会从堆内存中申请一块FILE结构体大小的空间用来存储文件的所有信息,然后按照文件打开的顺序把每个打开的文件的结构体形成一条链表,然后使用链表头进行管理。

注意:打开文件的目的无非就是对文件进行读写操作,所以每次当程序运行的时候已经有三个文件流被打开,分别是标准输入stdin、标准输出stdout、标准出错stderr,这三者在stdio.h中也是FILE指针。
在这里插入图片描述
所以内核在管理被打开文件的时候,链表中已经有三个结点存在,然后再把新节点头插入到链表中。
在这里插入图片描述
思考:请问为什么内核在为文件流申请内存的时候是申请的堆内存?请问有什么具体依据?
在这里插入图片描述
注意:使用标准IO的时候,是不可以反复关闭相同的文件,因为释放已经被释放的堆内存,会导致段错误!!

⦁ 读取数据

用户打开文件后可以从文件中读取数据,标准C库中提供了多个读取函数来满足用户的不同需求,这些函数大体分为三类:字符读取(fgetc)、按行读取(fgets)、按块读取(fread)。

(1) 字符读取
在这里插入图片描述
标准库中提供了一个fgetc函数,通过C99标准可以知道该函数的作用是从文件指针stream指向的文件中读取一个字符,并在读取一个字节后把文件的光标位置向后移一个字节,然后把读取到的字符所对应的ASCII码通过返回值返回。

在调用该函数时如果文件的光标已经到达文件末尾或者遇到读取错误时,则函数会返回EOF,EOF是文件结束标志,其实是个宏定义,宏定义的值为 -1,在头文件libio.h中有相关描述。

在这里插入图片描述
另外,在标准库中还提供了另一个函数getc(),这个函数的作用等效于fgetc()函数,只不过getc()函数的实现是利用宏定义而已。

在这里插入图片描述
在这里插入图片描述
练习:在本地磁盘打开一个存储少量数据的文本demo.txt,利用fgetc函数把文本中的字符输出到屏幕,当文本中所有字符都输出完成后就结束程序。
在这里插入图片描述
(2) 按行读取
在这里插入图片描述
标准库中提供了一个fgets函数,通过C99标准可以知道该函数的作用是从文件指针stream指向的文件中读取一行字符,并把读取的字符存储在指针s所指向的字符串内,当读取到n-1个字符、或者已经读取到文件末尾(EOF)、或者读取到换行符’\n’时,则函数调用停止。
在这里插入图片描述
思考:为什么fgets函数读取到换行符\n时会结束?fgets函数中的参数n的意义是什么??

回答:用户调用fopen打开文件之后,可以把数据写入到文件中以及从文件中读取数据,但是实现读取和写入的过程中其实内核并没有直接操作文件,而是在操作指向文件的结构体指针FILE,也就是用户写入的数据和读取的数据会先存储在FILE结构体的缓冲区中,当用户调用刷新缓冲区的函数或者其他读写函数时,FILE结构体的缓冲区会被刷新,数据才会被系统写入文件。

在这里插入图片描述
可以看到,每当使用标准IO的读操作函数,试图将数据从文件 a.txt读取出来时,数据都会流过标准输入缓冲区,然后再在适当的时刻冲洗(或称刷新,flush)到内核缓冲区,最后才真正得到数据。

缓冲区的出现其实就是由于输入设备和输出设备对于数据的读写速度比较慢,其实就是CPU为了降低输入输出次数,目的是为了提高运行效率,避免长时间的等待,所以内核就在内存中提供了一块空间作为缓冲区,缓冲区也可以称为缓存(Cache),是属于内存空间的一部分。

根据IO设备的不同,可以把缓冲区分为输入缓冲区和输出缓冲区,同样,根据刷新形式的不同,可以把缓冲区分为三种:全缓冲、行缓冲、无缓冲。

全缓冲:指的是当缓冲区被填满就立即把数据冲刷到文件、或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把数据冲刷到文件,一般读写文件的时候会采用

无缓冲:指的是没有缓冲区,直接输出,一般linux系统的标准出错stderr就是采用无缓冲,这样可以把错误信息直接输出。

行缓冲:指的是当缓冲区被填满(一般缓冲区为4KB,就是4096字节)或者缓冲区中遇到换行符’\n’时,或者在关闭文件、读取文件内容以及修改缓冲区类型时也会立即把 数据冲刷到文件中,一般操作IO设备时会采用,比如printf函数就是采用行缓冲。

当然,全缓冲和行缓冲除了以上几种情况外,当程序结束时缓冲区也会被刷新,另外,也可以采用函数库中的fflush函数手动刷新缓冲区。

注意:对于标准输出stdout而言默认是采用行缓冲的,而对于标准出错stderr而言默认是采用无缓冲的,对于普通文件而言默认是采用全缓冲的。

(3) 按块读取

标准库中提供了一个fgets函数,通过C99标准可以知道该函数的作用是从给定的文件输入流stream中读取最多nmemb个对象到指针ptr指向的字符串中,每个对象的大小为size字节,函数返回成功读取的对象个数,若出现错误或到达文件末尾,则可能小于nmemb。若size或nmemb为零,则fread函数返回0且不进行其他动作。
在这里插入图片描述
在这里插入图片描述
思考:可以知道函数的返回值如果小于nmemb则说明可能出现读取错误或者到达文件末尾,那应该如何区分这两种情况?

回答:可以通过标准库中提供的两个函数区分,一个函数是feof(),另一个则是ferror函数。
在这里插入图片描述
⦁ 写入文件
(1) 字符写入
在这里插入图片描述
(2) 按行写入
在这里插入图片描述
(3) 按块写入
在这里插入图片描述
⦁ 关闭文件

刚才提到利用fopen()打开文件之后内核会申请一块堆内存用来存储文件信息,申请的堆内存大小就是FILE结构体类型的大小,那么如果用户完成了对文件的读写访问之后,则需要利用fclose()函数来关闭文件,这样这块堆内存就会被内核先从链表中删除,然后再释放掉。
在这里插入图片描述
可以知道,对fclose函数的成功调用会导致刷新流指向的流,并关闭关联的文件。流的任何未写入的缓冲数据都将被传送到主机环境以写入文件;任何未读的缓冲数据都将被丢弃。无论调用是否成功,流都与文件解除关联,setbuf或setvbuf函数设置的任何缓冲区都与流解除关联(如果自动分配,则释放)。
在这里插入图片描述
⦁ 文件位置
(1) 设置位移
在这里插入图片描述
(2) 获取位移
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值