【UNIX/Liux】标准I/O库【Part 1】

本文是笔者拜读《UNIX环境高级编程》第5章(标准I/O库)的学习笔记。本文的主要内容包括文件流、FILE指针、缓冲、读写流、各种I/O的效率。文中不仅包含书中的知识点,也包括笔者的理解。

UNIX为例,操作系统的体系结构如下图所示:
在这里插入图片描述
shell是一个特殊的应用程序,为运行其他应用程序提供接口。系统调用库函数是应用程序访问内核的接口,前面两章介绍的函数大都属于系统调用,如openreadwritelstat

库函数是对系统调用的封装,更便于用户使用。其中的标准I/O库由ISO C标准所定义,该标准库也移植到了UNIX之外的很多系统中。标准I/O库处理了很多细节,如缓冲区分配、以优化的块长度执行I/O等。这些处理使用户不必担心如何选择正确的块长度。

在使用man命令查询函数时,选项2表示系统调用,3表示库函数。

流和FILE对象

标准I/O是围绕(stream)而不是文件描述符进行的。当用标准I/O库打开或创建一个文件时,使一个流与一个文件相关联。

标准I/O文件流可用于单字节或多字节(宽)字符集。流的定向决定了所读写的字符是单字节(字节定向)还是多字节(宽定向)。一个流刚被创建时,没有定向。fwide函数用于设置流的定向,freopen函数清除一个流的定向。
在这里插入图片描述
如果mode参数值为负,将指定流设置为字节定向。
如果mode参数值为正,将指定流设置为宽定向。
如果mode值为0,不设置流的定向,返回标识该流定向的值。
fwide不改变已定向流的定向,且无出错返回。

fopen函数返回一个FILE对象的指针,FILE对象包含了标准I/O库为管理该流所需要的所有信息:文件描述符、流缓冲区地址、缓冲区长度、缓冲区中的字符数、出错标志等。我们称FILE*文件指针

在前面几篇博客里,笔者将文件偏移量也称为文件指针。笔者把系统调用和库函数里的相关概念搞混了,表示抱歉。

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

文件描述符文件指针说明
STDIN_FILENOstdin标准输入
STDOUT_FILENOstdout标准输出
STDERR_FILENOstderr标准错误

以上3个文件指针定义在了头文件<stdio.h>中。

缓冲

标准I/O库提供缓冲的目的是尽可能减少readwrite的调用次数(即减少访问内核的次数)。如下图所示,进程每次读/写磁盘时,首先访问缓冲区,如果无法完成期望的行为,才访问真正的磁盘。
在这里插入图片描述

标准I/O提供了3种类型的缓冲。

(1) 全缓冲。在标准I/O缓冲区被填满后才进行实际的I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。(调用malloc获得缓冲区)

冲洗(flush)说的是标准I/O缓冲区的写操作。函数fflush可以冲洗一个流(有的编译器不支持)。在标准I/O库方面,冲洗指的是将缓冲区里的内容写到磁盘(不管缓冲区有没有满);在终端驱动程序方面,冲洗表示丢弃已存储在缓冲区中的数据。

冲洗缓冲区:要么把缓冲区里的数据用掉,要么删掉。

(2) 行缓冲。在输入和输出中遇到换行符时,执行实际的I/O操作。标准输入/输出使用的是行缓冲。行缓冲的限制:

a.缓冲区的长度是固定的,只要填满了缓冲区,即使没遇到换行符,也会进行I/O操作。
b.任何时候只要通过标准I/O库要求从一个不带缓冲的流m,或者一个行缓冲的流n(从内核请求数据)得到数据,那么就会冲洗相应的行缓冲输出流。

在输入数据来到缓冲区前,要冲洗缓冲区中的输出数据。输入/输出共用一个缓冲区。

(3) 不带缓冲。标准I/O库不对字符进行缓冲存储。标准错误流stderr通常是不带缓冲的。

很多系统默认使用以下类型的缓冲:
(1) 标准错误流不带缓冲。
(2) 如果流(除了标准错误流)指向的是终端设备,则行缓冲的,否则全缓冲。

更换缓冲类型的函数:
在这里插入图片描述
成功返回0,出错返回非0.
setbuf打开或关闭缓冲机制。bufNULL时,关闭缓冲。buf指向一个长度为BUFSIZ的缓冲区时,设置为全缓冲或行缓冲。
使用setvbuf可以精确地说明缓冲类型,如果指定不带缓冲,则忽略bufsize,否则bufsize可选择地指定缓冲区的地址和长度。一般而言,应由系统选择缓冲区的长度并自动分配缓冲区。
在这里插入图片描述
fflush可强制冲洗一个流。此函数使该流所有未写的数据都被传送至内核。如果streamNULL,则冲洗所有输出流。
在这里插入图片描述

打开流

以下函数打开一个标准I/O流:
在这里插入图片描述
fopen:打开路径名为path的指定文件。

fdopen:从一个已有的文件描述符上打开文件,使一个文件指针和该描述符相关联。此函数常用于由创建管道网络通信通道函数返回的描述符,因为一开始只能直接使用文件描述符访问这些特殊文件。

freopen:在一个指定的流上打开一个指定的文件。如果流已经打开,则先关闭它。若流已经定向,则清除定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误流。

使streampath关联起来,并返回stream

mode参数指定了对流的读写方式,该参数和open函数的标志对应。

modeopen标志
rrbO_RDONLY
wwbO_WRONLY | O_CREAT | O_TRUNC
aabO_WRONLY | O_CREAT | O_APPEND
r+r+brb+O_RDWR
w+w+bwb+O_RDWR | O_CREAT | O_TRUNC
a+a+bab+O_RDWR | O_CREAT | O_APPEND

fopen函数区分了文本文件和二进制文件,而open函数将它们都看做普通文件。

使用fdopen时,因为文件已经被打开,所以以写方式打开文件时不截断文件,以追加方式打开时也不能创建该文件。

当以读和写方式打开一个文件时,具有以下限制:
(1)如果中间没有fflushfseekfsetposrewind,则在输出的后面不能直接跟随输入。
(2)如果中间没有fseekfsetposrewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。

说到底,是因为读和写共用一个缓冲区,要避免读和写数据混合存储在缓冲区里。

限制rwar+w+a+
文件必须已存在++
放弃文件以前的内容++
流可以读++++
流可以写+++++
流只可在尾端处写++

在指定wa类型创建一个新文件时,我们无法说明该文件的权限位。POSIX要求实现使用如下的权限位来创建文件:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH

默认权限?我们可以通过umask函数屏蔽一些权限。

fclose关闭一个打开的流。
在这里插入图片描述
在该流被关闭之前,会自动冲洗缓冲区的输出数据,缓冲区中的输入数据被丢弃。如果标准I/O库自动为该流分配了一个缓冲区,则释放缓冲区。

当一个进程正常终止时,所有缓冲区里的输出数据被冲洗,所有I/O流被关闭。
***例: ***
fopenfreopen的一般使用。

// test.c
#include <stdio.h>

int main() {
	FILE *fp = fopen("./t1.txt", "w+");
	fputs("hello t1.txt\n", fp);
	fp = freopen("./t2.txt", "w+", fp);
	fputs("hello t2.txt\n", fp);
	fclose(fp);
	return 0;
}

运行结果:
在这里插入图片描述

读和写流

一旦打开了流,则可在3种不同类型的非格式化I/O种进行选择,对其进行读、写操作。
(1)每次一个字符的I/O。一次读写一个字符,如fgetcfputc。如果流是带缓冲的,则标准I/O函数处理缓冲。
(2)每次一行的I/O。一次读写一行,以一个换行符终止,如fgetsfputs
(3)直接I/O,有时被称为二进制I/O。每次读写一定数量的数据,如freadfwrite
在这里插入图片描述
nmemb是数据项的个数,size是每个数据项的大小(单位是字节)。

以上的都是非格式化I/O,格式化I/O包括printfscanf

输入函数

在这里插入图片描述
getchar()相当于getc(stdin)
getc可以被实现为宏,fgetc不能实现为宏。这意味着:
(1) getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
(2) fgetc是一个函数,可以取到其地址。
(3) 调用函数fgetc所需的时间通常长于调用宏getc

调用fgets时,应说明能处理的最大行长。

从流中读取数据后,可以调用ungetc将字符再压送回流中。并没有将压送字符写到底层文件或设备中,只是将它写回了标准I/O缓冲区中。

调用一次ungetc相当于将文件偏移量前移了1位。

例:
测试ungetc的功能。

// test.c
#include <stdio.h>

int main() {
	FILE *fp = fopen("./t.txt", "a+");
	int c = fgetc(fp);
	printf("%c\n", c);
	c = fgetc(fp);
	printf("%c\n", c);
	c = ungetc(c, fp);
	c = fgetc(fp);
	printf("%c\n", c);
	fclose(fp);
	return 0;
}

运行结果如下:
在这里插入图片描述
getchargetcfgetc在返回一个字符时,将读到的unsigned char类型数据转换为int类型。常量EOF(-1)表示读出错或者是到了文件末尾,为了区分这两种不同的情况,需要调用ferrorfeof

每个流在FILE对象种维护了两个标志:出错标志;文件结束标志。调用clearerr可以清除这两个标志。
在这里插入图片描述

输出函数

在这里插入图片描述
输出函数与上面的输入函数对应。putchar(c)等同于fputc(c, stdout)putc可被实现为宏,fputc不能实现为宏。

每次一行I/O

fgetsgets提供每次读取一行的功能。gets从标准输入读,fgets从指定流读。
在这里插入图片描述
在这里插入图片描述
对于fgets,必须指定缓冲长度size,此函数一直读到'\n'为止,以'\0'结尾。如果该行包括最后一个换行符的字符数超过了size-1,则会读到一个不完整的行,对fgets的下一次调用会继续读该行。
gets不将'\n'存入缓冲区。不推荐使用gets,因为可能会引起缓冲区溢出。

fputsputs提供输出一行的功能。
fputsputs均将一个以'\0'作为终止符的字符串写到指定流或标准输出中,不打印'\0'本身。不同的是,puts会自动追加一个'\n',因此请尽量避免使用puts

I/O系统调用的API中,文件描述符通常是第一个参数。
在标准I/O库的API中,流指针通常是最后一个参数。

标准I/O的效率

下面三个程序将标准输入复制到了标准输出。
a. 每次一个字符 I/O

// copy1.c
#include <stdio.h>

int main() {
	int c;
	while ((c = fgetc(stdin)) != EOF) {
		if (fputc(c, stdout) != c) {
			perror("fputc error");
			return -1;
		}
	}
	return 0;
}

b. 每次一行字符 I/O

// copy2.c
#include <stdio.h>

#define N 1024

int main() {
	char buf[N];
	while (fgets(buf, N, stdin)) {
		if (!fputs(buf, stdout)) {
			perror("fputs error");
			return -1;
		}
	}
	return 0;
}

c. 直接系统调用 I/O

// copy3.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define N 4096

int main() {
	char buf[N];
	int wNum;
	int rNum;
	
	while ((rNum = read(STDIN_FILENO, buf, N)) > 0) {
		if ((wNum = write(STDOUT_FILENO, buf, rNum)) != rNum) {
			perror("write error");
			return -1;
		}
	}
	if (rNum == -1) {
		perror("read error");
	}
	return 0;
}

ab使用的是库函数,c使用的是系统调用。对测试结果进行分析:

(1) a的用户CPU时间最长,因为它每读一个字符都要执行一次循环;b次之,循环的次数至少和文件中'\n'的数量相当。c的最短,可以设置程序,使其每次最多读一个磁盘块大小的数据,循环次数最少。

(2) 三者的系统CPU时间几乎相同,因为所有这些程序对内核提出的读、写请求数基本相同。

(3) 三者的时钟时间差主要来源于用户CPU时间差等待I/O结束所消耗的时间差

(4) ab是带缓冲的I/O,缓冲区的大小是系统的默认值;而c不带缓冲。这就意味着,当c中的N被用户指定的很小时,效率会极低,因为每次I/O都要访问内核,而不是直接读写缓冲区。

综合来看,标准I/O库与readwrite相比并不慢很多。对大多数比较复杂的应用程序而言,用户CPU时间的主要部分是应用程序本身的各种数据处理,而不是I/O例程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值