3、标准IO和文件IO
文件是一组相关数据的有序集合,文件名是这个数据集合的名称。
文件按类型分类分为:常规文件r(ASCII码文件、二进制文件)、目录d、字符设备c、块设备b、有名管道p、套接字s、符号链接l(快捷方式)
2、标准I/O
(1)标准I/O的由来
标准I/O指的是ANSI C(C库)中定义的用于I/O操作的一系列函数,它是由Dennis Ritchie在1975年左右编写的。不仅在UNIX/Linux系统,在很多主流操作系统上都实现了标准I/O库。
标准I/O是在系统调用函数基础上构造的,标准I/O库以及其头文件stdio.h为底层I/O系统调用提供了一个通用的接口,便于用户使用。
标准IO就是对文件IO的二次封装。标准 I/O 库函数是构建于文件 I/O(open()、 read()、 write()、 lseek()、 close()等)这些系统调用之上的,譬如标准 I/O 库函数 fopen()就利用系统调用 open()来执行打开文件的操作、 fread()利用系统调用 read()来执行读文件操作、 fwrite()则利用系统调用 write()来执行写文件操作等等。
只要操作系统中安装了C库,标准I/O函数就可以调用。换句话说,如果程序中使用的是标准I/O函数,那么源代码不需要修改就可以在其它操作系统下编译运行,具有更好的可移植性。
除此之外,使用标准I/O可以减少系统调用的次数,提高系统效率。标准I/O函数在执行时也会用到系统调用。在执行系统调用时,Linux必须从用户态切换到内核态,处理相应的请求,然后在返回到用户态。如果频繁的执行系统调用会增加系统的开销。为了避免这种情况,标准I/O使用时在用户空间创建缓冲区,读写时先操作缓冲区,在合适的时机再通过系统调用访问实际的文件,从而减少了使用系统调用的次数。
  虽然标准 I/O 构建于文件 I/O 之上, 但标准 I/O 却有它自己的优势,标准 I/O 和文件 I/O 的区别如下:
 
    虽然标准 I/O 和文件 I/O 都是 C 语言函数,但是标准 I/O 是标准 C 库函数,而文件 I/O 则是 Linux系统调用;
     标准 I/O 是由文件 I/O 封装而来,标准 I/O 内部实际上是调用文件 I/O 来完成实际操作的;
     可移植性:标准 I/O 相比于文件 I/O 具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不一样的;而对于标准 I/O 来说,由于很多操作系统都实现了标准 I/O 库,标准 I/O 库在不同的操作系统之间其接口定义几乎是一样的,所以标准 I/O 在不同操作系统之间相比于文件 I/O 具有更好的可移植性。
     性能、效率: 标准 I/O 库在用户空间维护了自己的 stdio 缓冲区, 所以标准 I/O 是带有缓存的,而文件 I/O 在用户空间是不带有缓存的,所以在性能、效率上,标准 I/O 要优于文件 I/O。关于标准 I/O 库相关介绍就到这里了,从下小节开始将正式向大家介绍如何在我们的应用程序中使用标准 I/O 库函数。
(2)文件指针和流
所有文件 I/O 函数(open()、 read()、 write()、 lseek()等)都是围绕文件描述符进行的,而对于标准 I/O 库函数来说,它们的操作是围绕 FILE 指针进行的,当使用标准 I/O 库函数打开或创建一个文件时,会返回一个指向 FILE 类型对象的指针(FILE *) ,使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作(使用标准 I/O 库函数进行 I/O 操作),所以由此可知,FILE 指针的作用相当于文件描述符,只不过 FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件I/O 系统调用中。
FILE 是一个结构体数据类型,它包含了标准 I/O 库函数为管理文件所需要的所有信息,包括用于实际I/O 的文件描述符、指向文件缓冲区的指针、缓冲区的长度、当前缓冲区中的字节数以及出错标志等。 FILE数据结构定义在标准 I/O 库函数头文件 stdio.h 中。
标准IO的核心对象就是流。当用标准I/O打开一个文件时,就会创建一个FILE结构体描述该文件(或者理解为创建一个FILE结构体和实际打开的文件关联起来)。我们把这个FILE结构体形象的称为流。标准I/O函数都基于流进行各种操作。
文件指针(FILE指针):每个被使用的文件都在内存中开辟了一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为FILE。标准IO库的所有操作都是围绕流来进行的,在标准I/O中流用FILE *来描述。
流:所有的I/O操作仅是简单的从程序移进或移出,这种字节流就被成为流。它分为文本流和二进制流两种。
文本流:在流中处理的数据是以字符形式出现。在文本流中‘\n’被转换成回车符CR和换行符LF的ASCII码0DH和0AH。而当输出时,0DH和0AH被转换成‘\n’。
二进制流:流中处理的数据是二进制序列。若流中有字符,则用一个字节的二进制ASCII码值表示;若是数字,则用对应的二进制数表示。对‘\n’不进行变换。
(3)文件缓冲
缓冲文件系统(高级磁盘I/O):系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区再一起送到磁盘中去。从磁盘中读数据,则一次从磁盘文件将一批数据读入到内存缓冲区中,然后再从缓冲区逐个的将数据送到程序的数据区。它的目的在于尽量减少read/write的调用。标准I/O就是缓冲文件系统。
非缓冲文件系统(低级磁盘I/O):依靠于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出。文件I/O就是缓冲文件系统。
标准IO中的流的缓冲类型有以下3种
A、全缓冲。在这种情况下,当填满标准I/O缓冲区后才进行实际I/O操作,或者满足一定条件后,系统通过调用malloc来获取所需要的缓冲区域,默认值。对于存放在磁盘上的普通文件,用标准I/O打开时默认是全缓冲的。当缓冲区已满或执行flush刷新操作时才会进行磁盘操作。
B、行缓冲。在这种情况下,当在输入和输出中遇到换行符‘\n’时执行I/O操作。标准输入流和标准输出流就是使用行缓冲的典型例子。
C、无缓冲。不对IO操作进行缓冲,直接调用系统接口,即在对流的读写时会立刻操作实际的文件。标准出错流(stderr)就是不带缓冲的,这就使得出错信息可以立刻显示在终端上,而不管输出的内容是否包含换行符。
(4)stdin、stdout、stderr
标准I/O预定义3个流,他们可以自动的为进程所使用,它们都定义在stdio.h头文件中。当程序在启动时,这三个流是自动打开的。
| 标准输入 | 0 | STDIN_FILENO | stdin | 
| 标准输出 | 1 | STDOUT_FILENO | stdout | 
| 标准错误输出 | 2 | STDERR_FILENO | stderr | 
使用setbuf()和setvbuf()可以更改缓存的类型,在任何时刻,可以使用fflush强制刷新一个数据流。
所谓标准输入设备指的就是计算机系统的标准的输入设备,通常指的是计算机所连接的键盘;而标准输出设备指的是计算机系统中用于输出标准信息的设备,通常指的是计算机所连接的显示器;标准错误设备则指的是计算机系统中用于显示错误信息的设备,通常也指的是显示器设备。
用户通过标准输入设备与系统进行交互, 进程将从标准输入(stdin)文件中得到输入数据,将正常输出数据(譬如程序中 printf 打印输出的字符串) 输出到标准输出(stdout) 文件,而将错误信息(譬如函数调用报错打印的信息)输出到标准错误(stderr) 文件。
标准输出文件和标准错误文件都对应终端的屏幕,而标准输入文件则对应于键盘。
每个进程启动之后都会默认打开标准输入、标准输出以及标准错误, 得到三个文件描述符, 即 0、 1、2, 其中 0 代表标准输入、 1 代表标准输出、 2 代表标准错误; 在应用编程中可以使用宏 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 分别代表 0、 1、 2,这些宏定义在 unistd.h 头文件中:
/* Standard file descriptors. */ #define STDIN_FILENO 0 /* Standard input. */ #define STDOUT_FILENO1 /* Standard output. */ #define STDERR_FILENO2 /* Standard error output. */
0、 1、 2 这三个是文件描述符,只能用于文件 I/O(read()、 write()等),那么在标准 I/O 中,自然是无法使用文件描述符来对文件进行 I/O 操作的,它们需要围绕 FILE 类型指针来进行,在 stdio.h 头文件中有相应的定义,如下:
/* Standard streams. */ extern struct _IO_FILE *stdin; /* Standard input stream. */ extern struct _IO_FILE *stdout; /* Standard output stream. */ extern struct _IO_FILE *stderr; /* Standard error output stream. */ /* C89/C99 say they're macros. Make them happy. */ #define stdin stdin #define stdout stdout #define stderr stderr
Tips: struct _IO_FILE 结构体就是 FILE 结构体,使用了 typedef 进行了重命名。
所以,在标准 I/O 中,可以使用 stdin、 stdout、 stderr 来表示标准输入、标准输出和标准错误。
(5)标准IO函数的优缺点
标准标准I/O函数有两个优点:
- 标准I/O函数具有良好的移植性
- 标准I/O函数可以利用缓冲提高性能
关于移植性无需过多解释,不仅是I/O函数,所有标准函数都具有良好的移植性。因为,为了支持所有操作系统(编译器),这些函数都是按照ANSIC标准定义的。当然,这并不局限于网络编程,而是适用于所有编程领域
接下来讨论标准I/O函数的第二个优点,使用标准I/O函数时会得到额外的缓冲支持,这种表达方式也许会带来一些混乱,因为之前讲过,创建套接字时操作系统会准备I/O缓冲。造成更大混乱之前,先说明这两种缓冲之间的关系。创建套接字时,操作系统将生成用于I/O的缓冲。此缓冲在执行TCP协议时发挥着重要的作用,此时若使用标准I/O函数,将得到额外的另一缓冲的支持,如图1-1所示

从图中可以看到,使用标准I/O函数传输数据时,经过两个缓冲。例如,通过fputs函数传输字符串“Hello”时,首先将数据传递到标准I/O函数的缓冲。然后数据将移动到套接字输出缓冲,最后将字符串发送到对方主机
设置缓冲的主要目的是为了提高性能,但套接字中的缓冲主要是为了实现TCP协议而设立的。例如,TCP传输中丢失数据时将再次传递,而再次发送数据意味着在某地保存了数据。存在什么地方呢?套接字的输出缓冲。与之相反,使用标准I/O函数缓冲的主要目的是为了提高性能。
那么,使用缓冲可以提高性能吗?实际上,缓冲并非在所有情况下都能带来卓越的性能。但需要传输的数据越多,有无缓冲带来的性能差异越大,可以通过如下两个角度说明性能的提高:
- 传输的数据量
- 数据向输出缓冲移动的次数
比较一个字节的数据发送10次(10个数据包)的情况和累计10个字节发送1次的情况,发送数据时使用的数据包中含有头信息。头信息与数据大小无关,是按照一定的格式填入的。即使假设该头信息占用40个字节(实际上更大),需要传递的数据量也存在较大差别
- 1个字节10次:40*10=400字节
- 10个字节1次:40*1=40字节
另外,为了发送数据,向套接字输出缓冲移动数据也会消耗不少时间。但这同样与移动次数有关,1个字节数据共移动10次花费的时间将近10个字节数据移动1次花费时间的10倍
缺点:
- 不容易进行双向通信
- 有时可能频繁调用fflush函数
- 需要以FILE结构体指针的形式返回文件描述
当打开文件时,如果希望同时进行读写操作,则应以r+、w+、a+模式打开。但因为缓冲的缘故,每次切换读写工作状态时应调用fflush函数。这也会影响基于缓冲的性能提高,而且,为了使用I/O函数,需要FILE结构体指针。而创建套接字时默认返回文件描述符,因此需要将文件描述符转化为FILE指针
3、文件IO
文件 I/O 指的是对文件的输入/输出操作,说白了就是对文件的读写操作; Linux 下一切皆文件,文件作为 Linux 系统设计思想的核心理念,在 Linux 系统下显得尤为重要,所以对文件的 I/O 操作既是基础也是最重要的部分。
(1)POSIX规范
POSIX表示可移植操作系统接口规范。该标准最初由IEEE制定,目的是为了提高UNIX环境下应用程序的可移植性。POSIX已发展成一个庞大的标准族,其中的POSIX.1提供了源代码级别的C语言应用程序编程接口。通俗的讲,为一个POSIX兼容的操作系统编写的程序,可以在其它任何POSIX操作系统上编译执行。不仅仅是UNIX,很多操作系统如Linux也支持POSIX标准。
(2)虚拟文件系统
Linux系统成功的关键因素之一就是具有与其他操作系统和谐共存的能力。Linux的文件系统由两层结构构成:第一层是虚拟文件系统(VFS),第二层是各种不同的具体的文件系统。
VFS就是把各种具体的文件系统的公共部分抽取出来,形成一个抽象层,是系统内核的一部分。他位于用户程序和具体的文件系统之间。它对用户程序提供了标准的文件系统调用接口,对具体的文件系统(比如:Ext2、FAT32等),它通过一系列的对不同文件系统通用的函数指针来调用对应的文件系统函数,完成相应的操作。任何使用文件系统的程序必须通过这层接口来访问。通过这样的方式,VFS就对用户屏蔽了底层文件系统的实现细节和差异。
VFS不仅可以对具体文件系统的数据结构进行抽象,以一种统一的数据接口进行管理,还可以接受用户层的系统调用,如open()、read()、write()、stat()、link()等。
此外,他还支持多种具体文件系统之间的相互访问,接受内核其它子系统的操作请求。例如:内存管理和进程调度。VFS在Linux系统中的位置如图所示:

(3)文件和文件描述符
Linux操作系统是基于文件概念的。文件是以字符序列构成的信息载体。根据这一点,可以把IO设备当做文件来处理。因此,与磁盘上的普通文件进行交互所用的同一系统调用可以直接用于IO设备。这样大大简化了系统对不同设备的处理,提高了效率。Linux中的文件只要分为6种:普通文件、目录文件、符号链接文件、管道文件、套接字文件和设备文件。
那么,内核如何区分和引用特定的文件了?这里用到一个重要的概念---文件描述符。对于Linux而言,所有对设备和文件的操作都是通过文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;读写文件时,需要把文件描述符作为参数传递给相应的函数。
通常,一个文件启动时,都会打开3个流:标准输入、标准输出、标准错误。这3个流分别对应文件描述符0、1、2(对应的宏分别是STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO)。
基于文件描述符的IO操作虽然不能直接移植到类Linux以外的系统上去(如Windows),但它往往是实现某些I/O操作的唯一途径,如Linux中低层文件操作函数、多路I/O、TCP/IP套接字编程接口等。同时,它们也很好的兼容了POSIX标准,因此,可以很方便的移植到任何POSIX平台上。基于文件描述符的IO操作是Linux中最常用的操作之一。
4、 I/O 缓冲
  出于速度和效率的考虑,系统 I/O 调用(即文件 I/O, open、 read、 write 等)和标准 C 语言库 I/O 函数(即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲,本小节将讨论文件 I/O 和标准 I/O 这两种 I/O 方式的数据缓冲问题,并讨论其对应用程序性能的影响。
  除此之外,本小节还讨论了屏蔽或影响缓冲的一些技术手段,以及直接 I/O 技术—绕过内核缓冲直接访问磁盘硬件。
  文件 I/O 的内核缓冲
  read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据。譬如调用 write()函数将 5 个字节数据从用户空间内存拷贝到内核空间的缓冲区中:
  write(fd, "Hello", 5); //写入 5 个字节数据
  调用 write()后仅仅只是将这 5 个字节数据拷贝到了内核空间的缓冲区中,拷贝完成之后函数就返回了,在后面的某个时刻,内核会将其缓冲区中的数据写入(刷新)到磁盘设备中,所以由此可知,系统调用 write()与磁盘操作并不是同步的, write()函数并不会等待数据真正写入到磁盘之后再返回。如果在此期间, 其它进程调用 read()函数读取该文件的这几个字节数据,那么内核将自动从缓冲区中读取这几个字节数据返回给应用程序。
  与此同理,对于读文件而言亦是如此,内核会从磁盘设备中读取文件的数据并存储到内核的缓冲区中,当调用 read()函数读取数据时, read()调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存。
我们把这个内核缓冲区就称为文件 I/O 的内核缓冲。这样的设计,目的是为了提高文件 I/O 的速度和效率,使得系统调用 read()、 write()的操作更为快速,不需要等待磁盘操作(将数据写入到磁盘或从磁盘读取出数据),磁盘操作通常是比较缓慢的。同时这一设计也更为高效,减少了内核操作磁盘的次数,譬如线程1 调用 write()向文件写入数据"abcd",线程 2 也调用 write()向文件写入数据"1234",这样的话,数据"abcd"和"1234"都被缓存在了内核的缓冲区中,在稍后内核会将它们一起写入到磁盘中,只发起一次磁盘操作请求;加入没有内核缓冲区,那么每一次调用 write(),内核就会执行一次磁盘操作。
前面提到,当调用 write()之后,内核稍后会将数据写入到磁盘设备中,具体是什么时间点写入到磁盘,这个其实是不确定的, 由内核根据相应的存储算法自动判断。
通过前面的介绍可知,文件 I/O 的内核缓冲区自然是越大越好, Linux 内核本身对内核缓冲区的大小没有固定上限。内核会分配尽可能多的内核来作为文件 I/O 的内核缓冲区,但受限于物理内存的总量,如果系统可用的物理内存越多,那自然对应的内核缓冲区也就越大,操作越大的文件也要依赖于更大空间的内核缓冲。
刷新文件 I/O 的内核缓冲区
强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备中,对于某些应用程序来说,可能是很有必要的, 例如,应用程序在进行某操作之前, 必须要确保前面步骤调用 write()写入到文件的数据已经真正写入到了磁盘中, 诸如一些数据库的日志进程。
联系到一个实际的使用场景,当我们在 Ubuntu 系统下拷贝文件到 U 盘时,文件拷贝完成之后,通常在拔掉 U 盘之前,需要执行 sync 命令进行同步操作,这个同步操作其实就是将文件 I/O 内核缓冲区中的数据更新到 U 盘硬件设备,所以如果在没有执行 sync 命令时拔掉 U 盘,很可能就会导致拷贝到 U 盘中的文件遭到破坏!
控制文件 I/O 内核缓冲的系统调用
Linux 中提供了一些系统调用可用于控制文件 I/O 内核缓冲,包括系统调用 sync()、 syncfs()、 fsync()以及 fdatasync()。
㈠、 fsync()函数
系统调用 fsync()将参数 fd 所指文件的内容数据和元数据写入磁盘, 只有在对磁盘设备的写入操作完成之后, fsync()函数才会返回,其函数原型如下所示:
#include <unistd.h> int fsync(int fd);
参数 fd 表示文件描述符,函数调用成功将返回 0,失败返回-1 并设置 errno 以指示错误原因。
前面提到了元数据这个概念,元数据并不是文件内容本身的数据,而是一些用于记录文件属性相关的数据信息,譬如文件大小、时间戳、权限等等信息,这里统称为文件的元数据,这些信息也是存储在磁盘设备中的,在 3.1 小节中介绍过。
5、文件I/O和标准I/O的区别
(1)标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。
文件I/O中用文件描述符表示一个打开的文件,可以访问不同文件类型的文件,如普通文件、设备文件、管道文件等。
(2)标准I/O是按ANSI C标准库建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有良好的移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存,利用缓存可以提高性能。
A、全缓存:当填满标准I/O缓存后才进行实际的I/O操作。
B、行缓存:当输入或输出中遇到新行符时,标准I/O库执行I/O操作。
C、不带缓存:stderr就是了。
文件IO称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于linix或unix平台。
(3)标准I/O默认采用了缓冲机制,比如调用fopen函数,不仅打开一个文件,而且建立了一个缓冲区(读写模式下将建立两个缓冲区),还创建了一个包含文件和缓冲区相关数据的数据结构。
文件I/O一般没有采用缓冲,需要自己创建缓冲区,不过其实在linix或unix系统中,都是有使用称为内核缓冲的技术用于提高效率,读写调用是在内核缓冲区和进程缓冲区之间进行的数据复制。
(4)标准I/O针对的是控制台,打印输出到屏幕等,它操作的是字符流。对于不同设备得特性不一样,必须有不同api访问才最高效。
文件I/O主要针对文件操作,读写硬盘等,它操作的是文件描述符。
(5)标准I/O被称为高级磁盘I/O,遵循ANSIX相关标准。只要开发环境中有标准C库,标准I/O就可以使用。(Linux中使用的时候glibc,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux下既可以使用标准I/O,也可以使用文件I/O)。
文件IO又称为低级磁盘IO,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。
(6)标准IO可以看做在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时在访问实际文件,从而减少了系统调用的次数。
文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁德系统调用会增加系统开销。
(7)标准I/O可以看成是在文件I/O的基础上封装了缓冲机制,先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。
文件I/O读写文件时,每次操作都会执行相关系统调用,这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。
6、标准IO和文件IO所用的函数
| 
 | 标准IO | 文件IO | 
| 打开 | fopen,freopen,fdopen | open 
 | 
| 关闭 | fclose | close | 
| 读 | getc,fgetc,getchar fgets,gets fread | read 
 | 
| 写 | putc,fputc,putchar fputs,puts, fwrite | write | 
函数封装:可以查看https://www.cnblogs.com/The-explosion/p/12240211.html
 
                     
                    
                 
                    
                
 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号