Linux系统编程-文件IO标准库IO刷新缓冲模式

Posted 一口Linux

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux系统编程-文件IO标准库IO刷新缓冲模式相关的知识,希望对你有一定的参考价值。

原文:https://www.toutiao.com/i6963239107937960480/

Linux系统编程的主要内容,就是分门别类的讲解Linux操作系统各个部分的原理,然后介绍或展示相关的系统调用API函数。

这一部分的内容非常多,几乎牵涉到了从第1章开始以来的大部章节中所提及的概念。所以要分三部分讲解。这里是最为基础的A部分。

1 系统调用

我们再次回顾一下系统调用的概念。

Linux系统编程013A部分-文件IO、标准库IO、刷新、缓冲模式

一个系统调用的流程

系统调用,英文名叫“system call”,它是操作系统内核里的一些内建的函数库,不光Linux平台上有,Windows、Andoris、ios,以及华为新出的鸿蒙上也有。这些函数可以用来完成一些系统系统调用把应用程序的请求传给内核,内核再调用更底层的内核函数完成所需的处理,并将处理结果返回给应用程序。这些函数集合起来就叫做程序接口或应用编程接口(Application Programming Interface,API)。

把内核与应用程序分开,是诸如Linux、UNIX、Andorid之类操作系统大展神威的基础,也是操作系统发展的必然。我们要在这个系统上编写各种应用程序,就得通过这个API接口来调用系统内核里面的函数。这也是操作系统一惯的通行做法。如果没有系统调用API和内核函数,那么应用程序就失去内核的支持,用户程序开发将寸步难行,编写大型应用程序更是天方夜谭!

关于系统调用的详细解释,请参看我的《Linux系统编程003-系统调用、API、标准C库》。

2 I/O-输入/输出

I/O就是input/output,中文意思就是输入/输出。人们为了避免麻烦,常常省去中间的“/”,直接写作IO。

如果你以动作为视角,Input就是写到文件中,Output就是从文件中读出。如果你以文件为视角,IO就是写文件、读文件的意思,我们中文一般说“读写”,而不是“写读”,所以IO的中文概念说起来就是:读写文件。

在inux上,正因为一切皆文件,所以对文件的IO操作无处不在。不论是shell命令处理文件、chrome上网、还是smplayer娱乐看电影,背后无一不是在读写文件、进行IO操作。

Linux操作系统提供了两种途径,供你对文件进行IO读写。一种是SHELL命令和常用的工具软件。另外一种方法就是自已编写程序。本文主要是从编程角度来讲述一系列与IO相关的话题。

其实shell命令的本身,除了内部命令-内核本身提供的命令之外,大多也是深度调用系统API来实现的功能。比如你想自已动手来一个自已的类似于cp命令的程序,非常简单,但前提是,得了解文件IO、stdio库IO,还要清楚缓冲、文件系统inode、文件描述符等这些基本的概念才行。(如果还不了解的,请参考我的《Linux shell命令:管道操作的深度理解和代码实证》、《Linux系统编程笔记-文件描述符》、《Linux系统编程学习004-文件描述符、文件IO、C库IO》等前期文档。)

3 文件IO与stdio库IO

Linux系统编程013A部分-文件IO、标准库IO、刷新、缓冲模式

 

既然Linux中一切皆文件,那么对文件的基本编程操作-IO操作,是我们绕不开的话题。编写程序有两套函数可供使用,系统API提供的IO函数、或者标准C库的IO函数来对文件进行读写。

(1)文件IO。Linux系统提供了
open/read/write/fcntl/dup/lseek/close等系统IO函数。这些文件IO函数是系统API函数的一部分,是供面向底层开发、进行系统编程而调用的函数集。

文件IO函数是Linux操作系统提供的底层API函数,它没有通用性。比如windows上面类似的API函数是:
CreateFile/WriteFile/ReadFIle/DeleteFile/GetFileSize等。当然,windows下面的文件概念windows是面向设备的,不同的设备可能API都不相同。这与Linux上面一切皆文件有着本质的不同。

(2)标准IO。由标准C库提供的
fopen/fread/fwrite/fseek/ftell/fclose/printf等sdio库函数,也称之为标准C函数库。这些函数是在文件IO函数的基础上进行了封装,是面上层应用开发、进行应用编程而调用的函数集。

比如:printf(string,format)函数的输出,其实相当于系统IO的write(1,formated_string)。

标准C库函数是通用的,当前主流操作系统都支持,可以跨本台使用。就是华为鸿蒙OS出来了一定也得无条件支持。

4 标准IO编程示例

标准IO是对文件IO的封装,因为面向上层应用开发,使用上简单,非常容易上手。然而,一些太容易到手的东西,都是陷阱,请看下例:

#include <stdio.h>
#include <unistd.h>
void stdio_printf_test(void)
{
	printf("This is a sample\\n");
	for(int i=0;i<10;++i)
	{
		printf("wait %d seconds, ", i);
		sleep(1);			
	}	
}
int main(int argc,char *argv[])
{
	stdio_printf_test(void);
  return 0;
}

上面一段简单代码目的是:先输出“This is a sample”,然后每隔一秒打印“wait 1 second, wait 2 sencods ... ...。然而,测试的结果是:先输出“This is a sample”,然后等待10秒之后,哗啦一下,把 wait 1 seconds、wait 2 seconds到wait 10 seconds一下子输出来。

$ gcc main.c 
$ ./a.out 
This is a sample
wait 1 seconds, wait 2 seconds, wait 3 seconds, wait 4 seconds, wait 5 seconds,wait 6 seconds, wait 7 seconds, wait 8 seconds, wait 9 seconds,wait 10 seconds, 

测试结果表明,代码运行结果不正确,不像我们预料的那样简单。标准IO调用,容易产生问题,那么文件IO呢?下面针对文件IO调用再试一下。

5 文件IO编程示例

地球人都知道,文件IO编程的通用步骤只有三板斧:

  • (1)先open打开一个文件,得到一个文件描述符
  • (2)然后对文件进行read/write/lseek等操作
  • (3)关闭文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
void fileio_write_to_file ( int argc,char *argv[] )
{
	char read_buf[64];
	ssize_t read_len = 0;
	ssize_t write_len = 0;
  //argc,argv判断部分略
	int fd = open( argv[1],O_RDWR|O_CREAT,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);/*rw-rw-rw-*/
	if ( fd < 0 ) 	{		perror("open");		return ;	}
	write_len = write( fd, "This is a sample" ,16 );
	if ( write_len < 0)	{		perror("write");		return ;	}
	//lseek(fd,0,SEEK_SET);
	read_len = read(fd , read_buf , 16);
	if ( read_len < 0 )	{		perror("read");		return ;	}
	printf("write %ld bytes , read %ld bytes, data : %s\\n",write_len, read_len ,read_buf);
	if ( close(fd))	{		perror("close");	}	
}
int main(int argc,char *argv[])
{
	fileio_write_to_file(argc,argv);	
	return 0;
}

编译上面的示例代码,运行之后的结果如下:

$ gcc main.c 
$ ./a.out abc.txt
write 17 bytes , read 0 bytes, data :
$ cat abc.txt 
This is a sample

write_to_file()函数的目的是对文件“abc.txt”写入"This is a sample",然后再读出来。可惜,事与愿违,啥也没有读出来。可是你真的去读一下abc.txt的内容"cat abc.txt",发现write写入是正确的。那么,虽说文件IO操作简单到只有三板斧,可是上面的简单例子write写入正确,而read却又不正确。

6 缓冲问题

这么简单的两个测试小例子,测试标准IO的stdio_printf_test()和测试文件IO的fileio_write_to_file(),均不能正常运行。

我们仔细观察测试输出,虽不能如愿,但结果也有部分的正确:stdio_printf_test()函数最终向终端输出了所有的字符串,fileio_write_to_file()也最终把“This is a sample”写入了文件abc.txt。

问题出在缓冲上。标准stdio库在操作磁盘文件时,一般是先把数据缓冲起来,直到缓冲区填满,或者达到指定的条件,才会调用文件IO进行实际上的写入动作。所以人们把标准IO称为“buffered IO”-带缓存的IO。

与此对应,文件IO就是无缓存的IO(unbuffered IO)。把stdio_printf_test()函数改成文件IO的方式,“wait 1 second, wait 2 sencods... ...“便可以顺序输出了。修改后的fileio_printf_test()代码如下:

void fileio_printf_test(void)
{
	write(1,"This is a sample\\n",17);
	char buf[32];
	for(int i=0;i<10;++i)
	{
		sprintf(buf,"wait %d seconds, ", i);
		write(1,buf,strlen(buf));
		sleep(1);			
	}	
}	

7 标准I/O缓冲类型

标准I/O库中的stream(即FILE结构的流文件)提供了三种类型的stdio缓冲:

全缓冲(fully buffered):这种缓冲模式下,只有在stdio缓冲区被填满后才会进行实际的IO操作(即调用read/write系统IO),也就是说单次读、写数据的大小与stdio缓冲区大小相同。通常打开的文件流是全缓冲的(文件位于磁盘上,而磁盘是块设备)。

行缓冲(line buffered):这种缓冲模式下,当在输入和输出流遇到换行符时,标准I/O库执行I/O操作(即调用read()或者write()系统调用)。通常情况下,stdin和stdout都是涉及的键盘显示器这些字符设备,所以是行缓冲的。

无缓冲(unbuffered):这种缓冲模式很好理解,就是不存在stdio缓冲区,每次I/O操作就直接调用read()/write()系统IO。stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。

正因为stdout是行缓冲,所以前面的例子stdio_printf_test()中,”printf("This is a sample\\n")“中有换行符”\\n“,所以先输出。后面的“printf("wait %d seconds, ", i)”没有换行符,缓冲也没有填满,到最后才把所有内容一次性地显示出来。

我们可以调用以下几个函数来显示地改变stdio缓冲的模式。

#include <stdio.h>
void setbuf(FILE *stream,char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
void setlinebuf(FILE * stream);
int setvbuf(FILE *stream, char *buf,int mode , size_t size);
  • setvbuf()的第二个参数mode可以设置为_IONBF、_IOLBF和_IOFBF,分别对应无缓冲、行缓冲和全缓冲。
  • setbuf()相当于 setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ); 也就是说:buf为空,则是_IONBF无缓冲,buf非空则是_IOFBF全缓冲。
  • setbuffer()相当于 setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size);
  • setlinebuf()相当于 setvbuf(stream, NULL, _IOLBF, 0);

除了可以调用上述几个函数可以显示地改变stdio缓冲的模式,我们还可以直接强制新刷新stdio缓冲区:调用fflush()函数可以立刻将stdio缓冲中的内容写入文件中去。

站在内核的角度来看,所谓flush的本质:就是立即调用无缓冲的wirte()强制stdio缓冲区中的数据写入内核高速缓冲区。

明白了以上的道理,stdio_printf_test()做以下改动就可以:要么通过“setvbuf(stdout,NULL,_IONBF,0) ;”把标准输出置为无缓冲,要么每次prinf()之后主动刷新“fflush(stdout); “。

#include <stdio.h>
#include <unistd.h>
void stdio_printf_test(void)
{
	printf("This is a sample\\n");
#ifdef USE_NO_BUFFERED
	setvbuf(stdout,NULL,_IONBF,0) ;  //对其返回值的判断略去		
#endif
  for(int i=0;i<10;++i)
	{
	printf("wait %d seconds, ", i);
#ifdef USE_FLUSH    
	fflush(stdout);   //
#endif
	sleep(1);			
	}	
}
int main(int argc,char *argv[])
{
	stdio_printf_test(void);
  return 0;
}

下面有更好的测试代码,可以方便的测试stdio的三种缓冲模式和强制刷新模式,只要用相对应的宏编译即可。

#ifdef USE_LINE_BUFFERED
char static_line_buf[50];  //行缓冲,满50个字节就输出,也就是满20字节底层会write到文件
#endif
#ifdef USE_FULLY_BUFFERED
char static_full_buf[500];  //全缓冲,满500个字节就输出,也就是满500字节底层会write到文件
#endif
void stdio_write_file_test(void)
{
	FILE * fp;
	char buf[32];
	
	fp = fopen(“abc.txt”,"w+");
#ifdef USE_NO_BUFFERED
	setvbuf(fp,NULL,_IONBF,0) ;  //对其返回值的判断略去		
#endif
#ifdef USE_LINE_BUFFERED
	setvbuf(fp,static_line_buf,_IOLBF,sizeof(static_line_buf));  //对其返回值的判断略去	
#endif	
#ifdef USE_FULLY_BUFFERED
	setvbuf(fp,static_full_buf,_IOLBF,sizeof(static_full_buf));  //对其返回值的判断略去	
#endif	
	fwrite("This is a sample\\n",17,1,fp);
	for(int i=0;i<20;++i)
	{				
		sprintf(buf,"wait %d seconds,", i);
		fwrite(buf,strlen(buf),1,fp);
#ifdef USE_FLUSH
		fflush(fp);
#endif		
		sleep(1);			
	}		
	//fclose(fp);
}

int main(int argc,char *argv[])
{
  stdio_write_file_test();
	return 0;
}

编译:

无缓冲测试: gcc main.c -o no_buf_test.out -D USE_NO_BUFFERED
行缓冲测试: gcc main.c -o line_buf_test.out -D USE_LINE_BUFFERED
全缓冲测试: gcc main.c -o full_buf_test.out -D USE_FULLY_BUFFERED
强制刷新测试:gcc main.c -o flush_buf_test.out -D USE_FLUSH

测试方法:开启两个终端,一个用来观测执行命令“tail -F abc.txt”,另一个就用来依次执行上面的输出程序no_buf_test.out、line_buf_test.out、full_buf_test.out或flush_buf_test.out。

Linux系统编程013A部分-文件IO、标准库IO、刷新、缓冲模式

 

当然,上面的示例代码编译后,测试上需要开两个终端,运行两个进程去测试。如果你嫌这样做麻烦,可以对标准输出stdout进行同样的测试。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
  
#ifdef USE_LINE_BUFFERED
char static_line_buf[50];   //行缓冲,满50个字节就输出,也就是满20字节底层会write到文件
#endif
#ifdef USE_FULLY_BUFFERED
char static_full_buf[500];  //全缓冲,满500个字节就输出,也就是满500字节底层会write到文件
#endif
void stdio_print_test(void)
{
#ifdef USE_NO_BUFFERED
	setvbuf(stdout,NULL,_IONBF,0);  //对其返回值的判断省略	
#endif
#ifdef USE_LINE_BUFFERED
	setvbuf(stdout,static_line_buf,_IOLBF,sizeof(static_line_buf));  //对其返回值的判断省略	
#endif	
#ifdef USE_FULLY_BUFFERED
	setvbuf(stdout,static_full_buf,_IOFBF,sizeof(static_full_buf));  //对其返回值的判断省略		
#endif	
	printf("This is a sample\\n");
	
	for(int i=0;i<10;++i)
	{				
		printf("wait %d seconds,", i);
#ifdef USE_FLUSH  //强制刷新
		fflush(stdout);
#endif				
		sleep(1);			
	}	
}
int main(int argc,char *argv[])
{
	stdio_print_test();	
	return 0;
}

可以像上个例程那样进行编译:

无缓冲测试: gcc main.c -o stdout_no_buf_test.out -D USE_NO_BUFFERED
行缓冲测试: gcc main.c -o stdout_line_buf_test.out -D USE_LINE_BUFFERED
全缓冲测试: gcc main.c -o stdout_full_buf_test.out -D USE_FULLY_BUFFERED
强制刷新测试:gcc main.c -o stdout_flush_buf_test.out -D USE_FLUSH

因为是写到屏幕上面的,所以不用tail命令去跟踪,分别运行stdout_no_buf_test.out、stdout_line_buf_test.out等,立马可以看各种缓冲模式或刷新效果。

8 标准IO行缓冲flush刷新条件

遇到下面五种情况,行缓冲就会被刷新。

  • 遇到换行符
  • 缓冲区已满
  • 需要从一个无缓冲的流中读取数据,或从行缓冲的流中(需要从内核读取数据)读取数据
  • 手动调用fflush函数
  • 显示地调用fclose关闭流,或者程序结束时调用exit

除了上述5条之外,《Linux/Unix系统编程手册》告诉我们:在包括GLIBC库在内的许多C函数库实现中,若stdin和stdout指向终端,那么无论何时从stdin中读取输入时,都将隐含调用一次fflush(stdout)函数,这将刷新写入stdout的任何数据。然后这并不是标准,要保证程序的可移植性,应该显示地调用fflush(stdout)。

9 标准IO全缓冲flush刷新条件

下面三种情况,全缓冲会被刷新:

  • 缓冲区已满
  • 手动调用fflush函数
  • 显示地调用fclose关闭流,或者程序结束时调用exit

当然,如果需要及时刷新,不论是全缓冲还是行缓冲,手动调用fflush函数,是最为稳妥的方法。

为什么进程结束时调用exit()时,在缓冲区中的内容也会被刷新呢?那是因为,调用exit()函数会执行一些释放进程资源的动作,其中就包括了关闭所有标准IO流。所以,stdio_write_file_test()函数的最后,虽然fclose(fp)被注释掉了,可还是能写成功。(当然,这样做是不可取的,最好还是显示的fclose(fp)。)

10 如何知道标准IO-stdout的行缓冲大小?

读C库源码可以知道。当然,如果你没有这份闲功夫的话,下面一段代码是可以测试出来的。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
  
int main(int argc,char *argv[])
{
	int num = atoi(argv[1]);  //命令行要输入写入的字节数
	for (int i = 0;i < num ; ++i)
	{
		printf("a");
	}	
	sleep(5);
	return 0;
}

输入命令加上“字节数”参数,比如"./a.out 1000",或"./a.out 1024"。当你发现1024字节以下,都需等5秒才能显示出来,而过了1024字节之后,是先把前面1024字节的内容显示出来,再延时5秒,然后再把剩余的内容显示出来。这就说明标准C库的行缓冲是1024字节。当然,上面的main测试函数也可以像下面这样简单。

int main(int argc,char *argv[])
{
	   int num = atoi(argv[1]);
    fwrite("a",1,num,stdout);
		sleep(5);
	return 0;

以上是关于Linux系统编程-文件IO标准库IO刷新缓冲模式的主要内容,如果未能解决你的问题,请参考以下文章

Linux应用开发:标准IO库(下)

标准文件IO详解---文件IO操作和标准文件IO操作的区别

Android C++系列:Linux文件IO操作

系统调用与标准IO库区别

系统调用与标准IO库区别

C 标准库IO缓冲区和内核缓冲区的区别