Linux-标准 I/O 库详解

文件操作fopen和open的区别

C语言中的fopen()和open()函数都是用来打开文件的,但它们分别属于不同的层次和功能范围,具有以下显著的区别:

所属库与接口风格:

fopen() 是 C 标准库中的函数,定义在 <stdio.h> 头文件中,遵循标准 I/O(stdio)接口。

open() 是 Unix 系统(包括 Linux、macOS 等)的系统调用函数,定义在 <sys/types.h>, <sys/stat.h> 和 <fcntl.h> 头文件中,属于底层系统接口。

返回值类型:

fopen() 返回一个指向 FILE 结构体的指针(通常称为文件指针),用于后续的文件操作。如果打开文件失败,返回 NULL。

open() 返回一个整数,即文件描述符(file descriptor),它是对打开文件的一种抽象引用。成功打开文件时,返回一个非负整数;失败则返回 -1 并设置 errno 错误码。

参数及模式:

fopen() 接受两个参数:文件名(字符串)和一个表示打开模式的字符串(如 "r"、"w"、"a"、"r+" 等),模式字符串还可以包含附加标志,如 "b" 表示二进制模式。模式字符串简洁易懂,适合大多数常规文件操作。

open() 接受三个参数:文件名、一组标志(如 O_RDONLY、O_WRONLY、O_APPEND 等,通常通过位或运算组合)和一个可选的权限模式(仅在创建新文件时有意义)。标志和权限通常需要使用特定的宏定义,显得较为底层和复杂,但提供了更精细的控制。

缓冲机制:

fopen() 打开的文件通常与标准 I/O 缓冲区关联,这意味着读写操作可能会先写入缓冲区,然后在适当的时候(如缓冲区满、遇到特定字符或显式刷新)将数据一次性传输到文件。这种缓冲提高了 I/O 效率,尤其对小规模的、频繁的读写操作有利。

open() 直接返回的文件描述符通常没有内置缓冲,操作系统的 I/O 系统调用(如 read()、write())通常是无缓冲的。这要求程序员自行管理缓冲策略,或者直接进行低级别的、可能效率较低的字节级操作。

配套操作:

使用 fopen() 打开的文件,应使用相应的 fclose() 函数关闭,以及配套的 fread()、fwrite()、fprintf()、fscanf() 等标准 I/O 函数进行读写操作。

使用 open() 打开的文件,应使用 close() 函数关闭,以及 read()、write()、pread()、pwrite()、lseek() 等系统调用进行读写和定位操作。

应用场景:

fopen() 适用于大部分通用的文件操作场景,尤其是面向文本的编程,因为它提供了一套易于使用的、与平台无关的接口,并自动处理了缓冲和编码转换等细节。

open() 更适合需要精细控制文件访问、低级别 I/O 或高性能、直接与操作系统交互的场景,如网络编程、设备驱动开发、系统工具或需要特定文件权限设置的应用。

总结来说,fopen() 提供了高级、便捷且跨平台的文件操作接口,适用于常规的文件读写任务。而 open() 提供了底层、灵活且功能强大的系统调用,适用于需要更多控制权或与系统紧密集成的场景。

也就是说,普通文件操作使用fopen,设备文件操作使用open。

标准 I/O 库简介 

本章介绍标准 I/O 库,不仅是 Linux,很多其它的操作系统都实现了标准 I/O 库。标准 I/O 虽然是对文件 I/O 进行了封装,但事实上并不仅仅只是如此,标准 I/O 会处理很多细节,譬如分配 stdio 缓冲区、以优化的块长度执行 I/O 等,这些处理使用户不必担心如何选择使用正确的块长度。

所谓标准 I/O 库则是标准 C 库中用于文件 I/O 操作(譬如读文件、写文件等)相关的一系列库函数的集合,通常标准 I/O 库函数相关的函数定义都在头文件<stdio.h>中,所以我们需要在程序源码中包含<stdio.h>头文件。

标准 I/O 库函数是构建于文件 I/O(open()、read()、write()、lseek()、close()等)这些系统调用之上的,譬如标准 I/O 库函数 fopen()就利用系统调用 open()来执行打开文件的操作、fread()利用系统调用 read()来执行读文件操作、fwrite()则利用系统调用 write()来执行写文件操作等等。

那既然如此,为何还需要设计标准 I/O 库?直接使用文件 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 库函数。

FILE 指针

在 0 中,所介绍的所有文件 I/O 函数(open()、read()、write()、lseek()等)都是围绕文件描述符进行的,当调用 open()函数打开一个文件时,即返回一个文件描述符 fd,然后该文件描述符就用于后续的 I/O 操作。

而对于标准 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 中。

标准输入、标准输出和标准错误

所谓标准输入设备指的就是计算机系统的标准的输入设备,通常指的是计算机所连接的键盘;而标准输出设备指的是计算机系统中用于输出标准信息的设备,通常指的是计算机所连接的显示器;标准错误设备则指的是计算机系统中用于显示错误信息的设备,通常也指的是显示器设备。

用户通过标准输入设备与系统进行交互,进程将从标准输入(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_FILENO 1    /* Standard output. */
#define STDERR_FILENO 2    /* 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 来表示标准输入、标准输出和标准错误。

打开文件 fopen()

在 0 所介绍的文件 I/O 中,使用 open()系统调用打开或创建文件,而在标准 I/O 中,我们将使用库函数fopen()打开或创建文件,fopen()函数原型如下所示:

#include <stdio.h>

FILE *fopen(const char *path, const char *mode);

使用该函数需要包含头文件 stdio.h。

函数参数和返回值含义如下:

path:参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。

mode:参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。

返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,后续的标准 I/O 操作将围绕 FILE 指针进行。如果失败则返回 NULL,并设置 errno 以指示错误原因。

参数 mode 字符串类型,可取值为如下值之一:

r
以只读方式打开文件。

r+
以可读、可写方式打开文件。

w
以只写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件不存在则创建该文件。

w+
以可读、可写方式打开文件,如果参数 path 指定的文件存在,将文件长度截断为 0;如果指定文件不存在则创建该文件。

a
以只写方式打开文件,打开以进行追加内容(在文件末尾写入),如果文件不存在则创建该文件。

a+
以可读、可写方式打开文件,以追加方式写入(在文件末尾写入),如果文件不存在则创建该文件。

新建文件的权限

由 fopen()函数原型可知,fopen()只有两个参数 path 和 mode,不同于 open()系统调用,它并没有任何一个参数来指定新建文件的权限。当参数 mode 取值为"w"、"w+"、"a"、"a+"之一时,如果参数 path 指定的文件不存在,则会创建该文件,那么新的文件的权限是如何确定的呢?

虽然调用 fopen()函数新建文件时无法手动指定文件的权限,但却有一个默认值:

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)

使用示例

使用只读方式打开文件:

fopen(path, "r");

使用可读、可写方式打开文件:

fopen(path, "r+");

使用只写方式打开文件,并将文件长度截断为 0,如果文件不存在则创建该文件:

fopen(path, "w");

关闭文件fclose()

调用 fclose()库函数可以关闭一个由 fopen()打开的文件,其函数原型如下所示:

#include <stdio.h>

int fclose(FILE *stream);

参数 stream 为 FILE 类型指针,调用成功返回 0;失败将返回 EOF(也就是-1),并且会设置 errno 来指示错误原因

读文件和写文件

当使用 fopen()库函数打开文件之后,接着我们便可以使用 fread()和 fwrite()库函数对文件进行读、写操作了,函数原型如下所示:

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

库函数 fread()用于读取文件数据,其参数和返回值含义如下:

ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;

size:fread()从文件读取 nmemb 个数据项,每一个数据项的大小为 size 个字节,所以总共读取的数据大小为 nmemb * size 个字节。

nmemb:参数 nmemb 指定了读取数据项的个数。

stream:FILE 指针。

返回值:调用成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数size 等于 1);如果发生错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb,那么到底发生了错误还是到达了文件末尾,fread()不能区分文件结尾和错误,究竟是哪一种情况,此时可以使用 ferror()或 feof()函数来判断,具体参考 4.7 小节内容的介绍。

库函数 fwrite()用于将数据写入到文件中,其参数和返回值含义如下:

ptr:将参数 ptr 指向的缓冲区中的数据写入到文件中。

size:参数 size 指定了每个数据项的字节大小,与 fread()函数的 size 参数意义相同。

nmemb:参数 nmemb 指定了写入的数据项个数,与 fread()函数的 nmemb 参数意义相同。

stream:FILE 指针。

返回值:调用成功时返回写入的数据项的数目(数据项数目并不等于实际写入的字节数,除非参数 size等于 1);如果发生错误,则 fwrite()返回的值将小于参数 nmemb(或者等于 0)。

由此可知,库函数 fread()、fwrite()中指定读取或写入数据大小的方式与系统调用 read()、write()不同,前者通过 nmemb(数据项个数)*size(每个数据项的大小)的方式来指定数据大小,而后者则直接通过一个 size 参数指定数据大小。

譬如要将一个 struct mystr 结构体数据写入到文件中,可按如下方式写入:

fwrite(buf, sizeof(struct mystr), 1, file);

当然也可以按如下方式写:

fwrite(buf, 1, sizeof(struct mystr), file);

使用示例

结合使用本小节与上小节所学内容,我们来编写一个简单地示例代码,使用标准 I/O 方式对文件进行读写操作。示例代码 4.5.1 演示了使用 fwrite()库函数将数据写入到文件中。

示例代码 4.5.1 标准 I/O 之 fwrite()写文件
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 char buf[] = "Hello World!\n";
 FILE *fp = NULL;
 /* 打开文件 */
 if (NULL == (fp = fopen("./test_file", "w"))) {
     perror("fopen error");
     exit(-1);
 }
 printf("文件打开成功!\n");
/* 写入数据 */
 if (sizeof(buf) > fwrite(buf, 1, sizeof(buf), fp)) {
     printf("fwrite error\n");
     fclose(fp);
     exit(-1);
 }
 printf("数据写入成功!\n");
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

首先使用 fopen()函数将当前目录下的 test_file 文件打开,调用 fopen()时 mode 参数设置为"w",表示以只写的方式打开文件,并将文件的长度截断为 0,如果指定文件不存在则创建该文件。打开文件之后调用fwrite()函数将"Hello World!"字符串数据写入到文件中。

写入完成之后,调用 fclose()函数关闭文件,退出程序。

示例代码 4.5.2 演示了使用库函数 fread()从文件中读取数据。

示例代码 4.5.2 标准 I/O 之 fread()读文件
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 char buf[50] = {0};
 FILE *fp = NULL;
 int size;
 /* 打开文件 */
 if (NULL == (fp = fopen("./test_file", "r"))) {
    perror("fopen error");
     exit(-1);
 }
 printf("文件打开成功!\n");
 /* 读取数据 */
 if (12 > (size = fread(buf, 1, 12, fp))) {
     if (ferror(fp)) { //使用 ferror 判断是否是发生错误
         printf("fread error\n");
         fclose(fp);
         exit(-1);
     }
     /* 如果未发生错误则意味着已经到达了文件末尾 */
 }
 printf("成功读取%d 个字节数据: %s\n", size, buf);
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

首先同样使用 fopen()打开当前目录下的 test_file 文件得到 FILE 指针,调用 fopen()时其参数 mode 设置为"r",表示以只读方式打开文件。

接着使用 fread()函数从文件中读取 12 * 1=12 个字节的数据,将读取到的数据存放在 buf 中,当读取到的字节数小于指定字节数时,表示发生了错误或者已经到达了文件末尾,程序中调用了库函数 ferror()来判断是不是发生了错误,该函数将会在 4.7 小节中介绍。如果未发生错误,那么就意味着已经达到了文件末尾,其实也就说明了在调用 fread()读文件时对应的读写位置到文件末尾之间的字节数小于指定的字节数。

最后调用 printf()打印结果,编译测试:

fseek 定位

库函数 fseek()的作用类似于 2.7 小节所学习的系统调用 lseek(),用于设置文件读写位置偏移量,lseek()用于文件 I/O,而库函数 fseek()则用于标准 I/O,其函数原型如下所示:

#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);

函数参数和返回值含义如下:

stream:FILE 指针。

offset:与 lseek()函数的 offset 参数意义相同。

whence:与 lseek()函数的 whence 参数意义相同。

返回值:成功返回 0;发生错误将返回-1,并且会设置 errno 以指示错误原因;与 lseek()函数的返回值意义不同,这里要注意!

调用库函数 fread()、fwrite()读写文件时,文件的读写位置偏移量会自动递增,使用 fseek()可手动设置文件当前的读写位置偏移量。

譬如将文件的读写位置移动到文件开头处:

fseek(file, 0, SEEK_SET);

将文件的读写位置移动到文件末尾:

fseek(file, 0, SEEK_END);

将文件的读写位置移动到 100 个字节偏移量处:

fseek(file, 100, SEEK_SET);

使用示例

示例代码 4.6.1 使用 fseek()调整文件读写位置
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *fp = NULL;
 char rd_buf[100] = {0};
 char wr_buf[] = "正点原子 http://www.openedv.com/forum.php\n";
 int ret;
 /* 打开文件 */
 if (NULL == (fp = fopen("./test_file", "w+"))) {
     perror("fopen error");
     exit(-1);
 }
 printf("文件打开成功!\n");
 /* 写文件 */
 if (sizeof(wr_buf) > fwrite(wr_buf, 1, sizeof(wr_buf), fp)) {
     printf("fwrite error\n");
     fclose(fp);
     exit(-1);
 }
 printf("数据写入成功!\n");
 /* 将读写位置移动到文件头部 */
 if (0 > fseek(fp, 0, SEEK_SET)) {
     perror("fseek error");
     fclose(fp);
     exit(-1);
 }
 /* 读文件 */
 if (sizeof(wr_buf) > (ret = fread(rd_buf, 1, sizeof(wr_buf), fp))) {
     printf("fread error\n");
     fclose(fp);
     exit(-1);
 }
 printf("成功读取%d 个字节数据: %s\n", ret, rd_buf);
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

程序中首先调用 fopen()打开当前目录下的 test_file 文件,参数 mode 设置为"w+";接着调用 fwrite()将wr_buf 缓冲区中的字符串数据"正点原子 http://www.openedv.com/forum.php"写入到文件中;由于调用了fwrite(),所以此时的读写位置已经发生了改变,不再是文件头部,所以程序中调用了 fseek()将读写位置移动到了文件头,接着调用 fread()从文件头部开始读取刚写入的数据,读取成功之后打印出信息。

运行测试:

ftell()函数

库函数 ftell()可用于获取文件当前的读写位置偏移量,其函数原型如下所示:

#include <stdio.h>

long ftell(FILE *stream);

参数 stream 指向对应的文件,函数调用成功将返回当前读写位置偏移量;调用失败将返回-1,并会设置errno 以指示错误原因。

我们可以通过 fseek()和 ftell()来计算出文件的大小,示例代码如下所示:

示例代码 4.6.2 使用 fseek()和 ftell()函数获取文件大小
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *fp = NULL;
 int ret;
 /* 打开文件 */
 if (NULL == (fp = fopen("./testApp.c", "r"))) {
 perror("fopen error");
 exit(-1);
 }
 printf("文件打开成功!\n");
 /* 将读写位置移动到文件末尾 */
 if (0 > fseek(fp, 0, SEEK_END)) {
 perror("fseek error");
 fclose(fp);
 exit(-1);
 }
/* 获取当前位置偏移量 */
 if (0 > (ret = ftell(fp))) {
 perror("ftell error");
 fclose(fp);
 exit(-1);
 }
 printf("文件大小: %d 个字节\n", ret);
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}

首先打开当前目录下的 testApp.c 文件,将文件的读写位置移动到文件末尾,然后再获取当前的位置偏移量,也就得到了整个文件的大小。

运行测试:

从上图可知,程序计算出的文件大小与 ls 命令查看到的文件大小是一致的。

检查或复位状态

调用 fread()读取数据时,如果返回值小于参数 nmemb 所指定的值,表示发生了错误或者已经到了文件末尾(文件结束 end-of-file),但 fread()无法具体确定是哪一种情况;在这种情况下,可以通过判断错误标志或 end-of-file 标志来确定具体的情况。

feof()函数

库函数 feof()用于测试参数 stream 所指文件的 end-of-file 标志,如果 end-of-file 标志被设置了,则调用feof()函数将返回一个非零值,如果 end-of-file 标志没有被设置,则返回 0。

其函数原型如下所示:

#include <stdio.h>

int feof(FILE *stream);

当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置。

if (feof(file)) {
/* 到达文件末尾 */
}
else {
/* 未到达文件末尾 */
}

更多待补充。 

ferror()函数

库函数 ferror()用于测试参数 stream 所指文件的错误标志,如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0。

其函数原型如下所示:

#include <stdio.h>

int ferror(FILE *stream);

当对文件的 I/O 操作发生错误时,错误标志将会被设置。

if (ferror(file)) {
/* 发生错误 */
}
else {
/* 未发生错误 */
}

更多待补充。 

clearerr()函数

库函数 clearerr()用于清除 end-of-file 标志和错误标志,当调用 feof()或 ferror()校验这些标志后,通常需要清除这些标志,避免下次校验时使用到的是上一次设置的值,此时可以手动调用 clearerr()函数清除标志。

clearerr()函数原型如下所示:

#include <stdio.h>

void clearerr(FILE *stream);

此函数没有返回值,调用将总是会成功!

对于 end-of-file 标志,除了使用 clearerr()显式清除之外,当调用 fseek()成功时也会清除文件的 end-offile 标志。

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 的内核缓冲区,但受限于物理内存的总量,如果系统可用的物理内存越多,那自然对应的内核缓冲区也就越大,操作越大的文件也要依赖于更大空间的内核缓冲。

stdio 缓冲

介绍完文件 I/O 的内核缓冲后,接下来我们聊一聊标准 I/O 的 stdio 缓冲。

标准 I/O(fopen、fread、fwrite、fclose、fseek 等)是 C 语言标准库函数,而文件 I/O(open、read、write、 close、lseek 等)是系统调用,虽然标准 I/O 是在文件 I/O 基础上进行封装而实现(譬如 fopen 内部实际上调用了 open、fread 内部调用了 read 等),但在效率、性能上标准 I/O 要优于文件 I/O,其原因在于标准 I/O 实现维护了自己的缓冲区,我们把这个缓冲区称为 stdio 缓冲区,接下来我们聊一聊标准 I/O 的 stdio 缓冲。

前面提到了文件 I/O 内核缓冲,这是由内核维护的缓冲区,而标准 I/O 所维护的 stdio 缓冲是用户空间的缓冲区,当应用程序中通过标准 I/O 操作磁盘文件时,为了减少调用系统调用的次数,标准 I/O 函数会将用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中。

通过这样的优化操作,当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得效率、性能得到优化。使用标准 I/O 可以使编程者免于自行处理对数据的缓冲,无论是调用 write()写入数据、还是调用 read()读取数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值