自学内容网 自学内容网

【Linux应用编程】标准IO库~详细剖析,重学c语言底层实现逻辑

标准 I/O 库

个人深感是重学一遍c语言底层实现了qwq

引言:

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

主要内容

  • 流和 FILE 对象;

  • 标准输入、标准输出以及标准错误;

  • 使用标准 I/O 库函数打开、读写、关闭文件;

  • 格式化 I/O,格式化输入输出 scanf、 printf;

  • 文件 I/O 缓冲,内核缓冲和 stdio 缓冲区;

  • 文件 I/O 与标准 I/O 混合编程;

标准 I/O 库简介
  • 标准 I/O 库函数是构建于文件 I/O(open()、read()、write()、lseek()、close()等)这些系统调用之上;

  • 库函数提供比底层系统调用更方便、好用调用接口,标准 I/O 构建于文件 I/O 上,但标准 I/O 有自己优势;

区别

在这里插入图片描述

FILE 指针

FILE和FILE指针、FILE指针又称流(stream )。

定义

FILE本质为是一个结构体数据类型,数据结构定义头文件 stdio.h 中;

包含标准 I/O 库函数为管理文件所需要的所有信息,于实际I/O 的文件描述符、指向文件缓冲区的指等;

作用

FILE 指针的作用相当于文件描述符,FILE 指针用于标准 I/O 库函数中、而文件描述符则用于文件I/O 系统调用;

标准 I/O 打开或创建一个文件,返回一个指向 FILE 类型对象的指针(FILE *),使用该 FILE 指针与被打开或创建的文件相关联,然后该 FILE 指针就用于后续的标准 I/O 操作;

标准输入、输出、错误

分别对应计算机系统的标准的输入(键盘)、输出、显示错误信息的设备(显示屏);

image-20240716113156648
/*
进程启动之后都会默认打开标准输入、标准输出以及标准错误,得到三个文件描述符,即 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 类型指针来进行:
*/
/* 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 进行了重命名。

SO,在标准 I/O 中,可以使用 stdin、stdout、stderr 来表示标准输入、标准输出和标准错误

打开文件 fopen()
函数原型
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);

path:参数 path 指向文件路径,可以是绝对路径、也可以是相对路径。
mode:参数 mode 指定了对该文件的读写权限,是一个字符串,稍后介绍。
返回值:  调用成功返回一个指向 FILE 类型对象的指针(FILE *),该指针与打开或创建的文件相关联,
     后续标准 I/O 操作围绕 FILE 指针进行;如果失败则返回 NULL,并设置 errno 以指示错误原因;

参数 mode 字符串类型,取值:

在这里插入图片描述

新建文件的权限

fopen()参数 path 指定的文件不存在,并没有任何一个参数来指定新建文件的权限,但却有一个默认值:

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)
使用示例

在这里插入图片描述

fclose()关闭文件

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

#include <stdio.h>
int fclose(FILE *stream);

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

读文件和写文件
fread()和 fwrite()
#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
参数:
ptr:fread()将读取到的数据存放在参数 ptr 指向的缓冲区中;
size:fread()从文件读取 nmemb 个数据项,每个数据项大小为 size 个字节,所以总共读取的数据大小为nmemb * size 个字节。
nmemb:参数 nmemb 指定了读取数据项的个数。
stream:FILE 指针;

返回值:
成功时返回读取到的数据项的数目(数据项数目并不等于实际读取的字节数,除非参数size 等于 1);
错误或到达文件末尾,则 fread()返回的值将小于参数 nmemb;
发生了错误还是到达了文件末尾,read()不能区分文件结尾和错误,可使用 ferror()或feof()判断;
*/
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
参数:
ptr:将参数 ptr 指向的缓冲区中的数据写入到文件中。
size:参数 size 指定了每个数据项的字节大小,与 fread()函数的 size 参数意义相同。
nmemb:参数 nmemb 指定了写入的数据项个数,与 fread()函数的 nmemb 参数意义相同。
stream:FILE 指针。

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

文件io与标准io指定读取或写入数据大小的方式与不同:

库函数 fread()、fwrite()中 nmemb(数据项个数)*size(每个数据项的大小)的方式来指定数据大小;
而系统调用 read()、write()则直接通过一个 size 参数指定数据大小;
示例

在这里插入图片描述

典例代码

1>用 fopen()函数将当前目录下的 test_file 文件打开,调用 fopen()时 mode 参数设置为"w"/”r“;

2>fwrite():open时将文件的长度截断为 0,如果指定文件不存在则创建该文件;

​ 打开文件之后调用fwrite()函数将"Hello World!"字符串数据写入到文件中。

3> fread():使用 fread()函数从文件中读取 12 * 1=12 个字节的数据,将读取到的数据存放在 buf 中,当读取到的字节数小于指定字节数时,表示发生了错误或者已经到达了文件末尾,程序中调用了库函数 ferror()来判

断是不是发生了错误;

/*示例代码 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);
}
/*示例代码 4.5.1 标准 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);
}
fseek 定位
  • 用于设置文件读写位置偏移量

  • 与 lseek()函数的返回值意义不同

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
/*
函数参数:
stream:FILE 指针。
offset:与 lseek()函数的 offset 参数意义相同。
whence:与 lseek()函数的 whence 参数意义相同。
返回值:
成功返回 0;
错误返回-1,并且会设置 errno 以指示错误原因;与 lseek()函数的返回值意义不同,这里要注意!
lseek返回值:
成功返回从文件头部开始算起的位置偏移量(字节为单位),也就是当前的读写位置;
发生错误将返回-1;
*/
区别

见上代码框;

示例

在这里插入图片描述

典例代码

1>首先调用 fopen()打开当前目录下的 test_file 文件,参数 mode 设置为"w+";

2>接着调用 fwrite()将wr_buf 缓冲区中的字符串数据"正点原**"写入文件中;

3>由于调用了fwrite(),所以此时的读写位置已经发生了改变,不再是文件头部;

4>所以程序中调用了 fseek()将读写位置移动到了文件头,接着调用 fread()从文件头部开始读取刚写入的数据,读取成功之后打印出信息;

/*示例代码 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);
} 
ftell()获取定位
  • 用于获取文件当前的读写位置偏移量

  • 调用成功将返回当前读写位置偏移量;调用失败将返回-1,并会设置errno 以指示错误原因;

#include <stdio.h>
long ftell(FILE *stream);
典例代码

通过 fseek()和 ftell()来计算出文件的大小:

首先打开当前目录下的 testApp.c 文件,将文件的读写位置移动到文件末尾,然后再获取当前的位置偏

移量,也就得到了整个文件的大小;

/*使用 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);
}
检查或复位状态

end-of-file、错误标准进行检查;

调用 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()函数
功能
  • 用于测试参数 stream 所指文件的错误标志

  • 如果错误标志被设置了,则调用 ferror()函数将返回一个非零值,如果错误标志没有被设置,则返回 0

原型
#include <stdio.h>
int ferror(FILE *stream);
实例

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

if (ferror(file)) 
{
/* 发生错误 */
}
else 
{
/* 未发生错误 */
}
clearerr()函数
功能
  • 用于清除 end-of-file 标志和错误标志
  • end-of-file 标志,除了使用 clearerr()显式清除,当调用 fseek()成功也会清除文件的 end-of-file 标志
原型
#include <stdio.h>
void clearerr(FILE *stream);
实例
/*示例代码 4.7.1 clearerr()函数使用示例*/

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
 FILE *fp = NULL;
 char buf[20] = {0};
 /* 打开文件 */
 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 (10 > fread(buf, 1, 10, fp)) 
 {
 if (feof(fp))
 printf("end-of-file 标志被设置,已到文件末尾!\n");
 clearerr(fp); //清除标志
 }
    
 /* 关闭文件 */
 fclose(fp);
 exit(0);
}
格式化 I/O
格式控制字符串 format
  • 普通字符(非%字符) + 转换说明 = 格式控制字符串

  • 每个转换说明都是以%字符开头;

    %[flags][width][.precision][length]type//输入format
    /*    
    flags:标志,可包含 0 个或多个标志;
    width:输出最小宽度,表示转换后输出字符串的最小宽度;
    precision:精度,前面有一个点号" . ";
    length:长度修饰符;
    type:转换类型,指定待转换数据的类型。
    */
        
    %[*][width][length]type//输出format
    %[m][width][length]type
    /*
    width:最大字符宽度;
    length:长度修饰符,与格式化输出函数的 format 参数中的 length 字段意义相同。
    type:指定输入数据的类型
    */
    可以看到,只有%和 type 字段为必须!
        
    这些转换说明对应的字符和对应数据类型,可参照表的数据或者查询stdio库;
    
实例
printf("转换说明 1 转换说明 2 转换说明 3", arg1, arg2, arg3);
格式化输出
  • 格式化输出:将格式化数据写入到标准输出

  • 格式化输出包括:printf()、fprintf()、dprintf()、sprintf()、snprintf()

#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
int snprintf(char *buf, size_t size, const char *format, ...);
/*
可变参函数,它们都有一个共同的参数 format,这是一个字符串,称为格式控制字符串,用于指定后续的参数如何进行格式转换,所以才把这些函数称为格式化输出,因为它们可以以调用者指定的格式进行转换输出;

学习这些函数的重点就是掌握这个格式控制字符串 format 的书写格式以及它们所代表的意义;
*/

在这里插入图片描述

总结
  • printf()函数用于将格式化数据写入到标准输出;
  • dprintf()和 fprintf()函数用于将格式化数据写入到指定的文件中,两者不同之处在于,fprintf()使用 FILE 指针指定对应的文件、而 dprintf()则使用文件描述符 fd 指定对应的文件;
  • sprintf()、snprintf()函数可将格式化的数据存储在用户指定的缓冲区 buf 中;
格式化输入
  • 从标准输入中获取格式化数据

  • 格式化输入包括:scanf()、fscanf()、sscanf()

#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
总结
  • scanf()函数可将用户输入(标准输入)的数据进行格式化转换;

  • fscanf()函数从 FILE 指针指定文件中读取数据,并将数据进行格式化转换;

  • sscanf()函数从参数 str 所指向的字符串中读取数据,并将数据进行格式化转换;

I/O 缓冲
  • 出于速度和效率的考虑,系统 I/O 调用(即文件 I/O,open、read、write 等)和标准 C 语言库 I/O 函数(即标准 I/O 函数)在操作磁盘文件时会对数据进行缓冲;

  • 屏蔽或影响缓冲的一些技术手段,以及直接 I/O 技术—绕过内核缓冲直接访问磁盘硬件;

文件 I/O 的内核缓冲
  • read()和 write()系统调用在进行文件读写操作的时候并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区(kernel buffer cache)之间复制数据;

  • 系统调用 write()与磁盘操作并不是同步的,write()函数并不会等待数据真正写入到磁盘之后再返回;

  • 内核缓冲区就称为文件 I/O 的内核缓冲,目的是为了提高文件 I/O 的速度和效率,使得系统调用 read()、write()的操作更为快速,不需要等待磁盘操作(将数据写入到磁盘或从磁盘读取出数据),磁盘操作通常是比较缓慢的。

  • 当调用 write()之后,内核稍后会将数据写入到磁盘设备中,具体是什么时间点写入到磁盘,

    这个其实是不确定的;

刷新文件 I/O 的内核缓冲区
  • 强制将文件 I/O 内核缓冲区中缓存的数据写入(刷新)到磁盘设备;

  • 系统调用可用于控制文件 I/O 内核缓冲,包括系统调用 sync()、syncfs()、fsync()以及 fdatasync()

直接 I/O:绕过内核缓冲

允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O);

可针对某一文件或块设备执行直接 I/O,要做到这一点,需要在调用 open()函数打开文件时,指定O_DIRECT 标志;

fd = open(filepath, O_WRONLY | O_DIRECT);
标准 I/O 的 stdio 缓冲
  • 标准 I/O 也实现维护了自己的缓冲区,我们把这个缓冲区称为 stdio 缓冲区;

  • 标准 I/O 函数会将用户写入或读取文件的数据缓存在 stdio 缓冲区,然后再一次性将 stdio 缓冲区中缓存的数据通过调用系统调用 I/O(文件 I/O)写入到文件 I/O 内核缓冲区或者拷贝到应用程序的 buf 中;

  • 目的:当操作磁盘文件时,在用户空间缓存大块数据以减少调用系统调用的次数,使得效率、性能得到优化。使用标准 I/O 可以使编程者免于自行处理对数据的缓冲

对 stdio 缓冲进行设置

库函数可用于对标准 I/O 的 stdio 缓冲区进行相关的一些设置,包括 setbuf()、setbuffer()以及 setvbuf();

I/O 缓冲小节

概括说明文件 I/O 内核缓冲区和 stdio 缓冲区之间的联系与区别,以及各种 stdio 库函数:

在这里插入图片描述

1>自上而下,首先应用程序调用标准 I/O 库函数将用户数据写入到 stdio 缓冲区中,stdio 缓冲区是由 stdio 库所维护的用户空间缓冲区。针对不同的缓冲模式,当满足条件时,stdio 库会调用文件 I/O(系统调用 I/O)将 stdio 缓冲区中缓存的数据写入到内核缓冲区中,内核缓冲区位于内核空间。最终由内核向磁盘设备发起读写操作,将内核缓冲区中的数据写入到磁盘(或者从磁盘设备读取数据到内核缓冲区);

2>应用程序调用库函数可以对 stdio 缓冲区进行相应的设置,设置缓冲区缓冲模式、缓冲区大小以及由调用者指定一块空间作为 stdio 缓冲区,并且可以强制调用 fflush()函数刷新缓冲区;而对于内核缓冲区来说,应用程序可以调用相关系统调用对内核缓冲区进行控制,譬如调用 fsync()fdatasync()sync()来刷新内核缓冲区(或通过 open 指定 O_SYNC 或 O_DSYNC 标志),或者使用直接 I/O 绕过内核缓冲区(open 函数指定 O_DIRECT 标志);
文件描述符与 FILE 指针互转
  • 在同一个文件上执行 I/O 操作时,可将文件 I/O(系统调用 I/O)与标准 I/O 混合使用;

  • 此时可借助于库函数 fdopen()、fileno()来完成将文件描述符和 FILE 指针对象之间进行转换;

fileno()和 fdopen()
功能
  • 将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen()则进行着相反的操作;
原型
#include <stdio.h>
int fileno(FILE *stream);
/*
成功:根据传入的 FILE 指针得到整数文件描述符,通过返回值得到文件描述符;
错误:将返回-1,并且会设置 errno 来指示错误原因;
*/
FILE *fdopen(int fd, const char *mode);
实例

当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题

文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写入到内核缓冲区。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("print");
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}
  • 在同一个文件上执行 I/O 操作时,可将文件 I/O(系统调用 I/O)与标准 I/O 混合使用;

  • 此时可借助于库函数 fdopen()、fileno()来完成将文件描述符和 FILE 指针对象之间进行转换;

fileno()和 fdopen()
功能
  • 将标准 I/O 中使用的 FILE 指针转换为文件 I/O 中所使用的文件描述符,而 fdopen()则进行着相反的操作;
原型
#include <stdio.h>
int fileno(FILE *stream);
/*
成功:根据传入的 FILE 指针得到整数文件描述符,通过返回值得到文件描述符;
错误:将返回-1,并且会设置 errno 来指示错误原因;
*/
FILE *fdopen(int fd, const char *mode);
实例

当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题

文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存,而标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write()将 stdio 缓冲区中的数据写入到内核缓冲区。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("print");
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}

执行结果:先输出了"write"字符串信息,接着再输出了"print"字符串信息;


原文地址:https://blog.csdn.net/Thmos_vader/article/details/140480720

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!