APUE读书笔记-第五章 标准I/O库

今天草草的把第四章结了,后面的内容分析的也不是很详细,就连书中的例子都没有怎么实验,还是等以后有机会吧。

从5.3节开始研究起吧,这一节主要谈了一个进程预定义的3个流,分别是标准输入、标准输出和标准错误,通过stdin、stdout、stderr引用。这里要和进程中的文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO相区分。

/* 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

5.4缓冲

标准I/O提供以下3种缓冲:

  1. 全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。
  2. 行缓冲。在这种情况下,当在输入和输出遇到换行符时,标准I/O库执行I/O操作。行缓冲允许一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。对于行缓冲又存在两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只有填满缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输入流。此处冲洗是指将缓冲区中的数据写入内核中。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
  3. 不带缓冲。标准I/O库不对字符进行缓冲存储。

对于上面提到的第二点可通过几个简单的实验进行验证,是通过fputc向标准输出输入数据,具体程序如下:

#include <stdio.h>
#include <unistd.h>

int main(void)
{
   char msg[] = "Hello world";
   int i = 0;

   while (msg[i])
   {
      fputc(msg[i], stdout); //将fputc函数改成printf效果相同
      sleep(1); //写入后程序挂起1s
      i++;
   }
   return 0;
}

运行结果:程序先不输出,最后统一输出msg,通过这个程序基本验证了行缓冲的特点,没有换行符或写满行缓冲区的情况下,不会执行I/O操作,同时也验证了标准I/O使用行缓冲模式。但这个实验又引出了一个问题那就是进行I/O操作的时机,在以上实验中我既没有输出换行符,同时也没有写满缓冲区,暂且现把这个疑问记下,学了后面的知识也许就能解答了。

标准I/O缓冲还具有以下惯例:

  1. 标准错误是不带缓冲的。
  2. 若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。

可通过以下函数更改缓冲类型。

#include <stdio.h>
extern void setbuf (FILE *__restrict __stream, char *__restrict __buf) __THROW;
extern int setvbuf (FILE *__restrict __stream, char *__restrict __buf,
            int __modes, size_t __n) __THROW;

以上函数须在流已被打开后使用。同时应在对该流执行任何一个其他操作之前调用。

setvbuf的功能比较明确,使用modes参数可以设定缓冲模式:

#include <stdio.h>
#define _IOFBF 0		/* Fully buffered.  */
#define _IOLBF 1		/* Line buffered.  */
#define _IONBF 2		/* No buffering.  */

强制冲洗一个流。

#include <stdio.h>
extern int fflush (FILE *__stream);

若fp为NULL,则此函数将导致所有输出流被冲洗。

5.5打开流

extern FILE *fopen (const char *__restrict __filename,
		    const char *__restrict __modes) __wur;
extern FILE *freopen (const char *__restrict __filename,
              const char *__restrict __modes,
              FILE *__restrict __stream) __wur;
extern FILE *fdopen (int __fd, const char *__modes) __THROW __wur;

上述三个函数的区别如下:

  1. fopen函数打开路径名为pathname的一个指定的文件。
  2. freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
  3. fdopen函数取一个已有的文件描述符(可通过open函数获得此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fopen使一个标准I/O流与该描述符相结合。

ISO规定type参数可以有15种不同的值。

  1. r或rb:为读而打开。但文件必须已经存在。(O_RDONLY)
  2. w或wb:把文件截断至0长,或为写而创建。文件之前的内容会被删除。(O_WRONLY|O_CREAT|O_TRUNC)
  3. a或ab:追加;为在文件尾写而打开,或为写而打开。(O_WRONLY|O_CREAT|O_APPEND)
  4. r+或r+b或rb+:为读和写而打开。流只可在尾端处写。(O_RDWR)
  5. w+或w+b或wb+:把文件截断至0长,或为读和写而打开。(O_RDWR|O_CREAT|O_TRUNC)
  6. a+或a+b或ab+:为在文件尾读和写而打开或创建。(O_RDWR|O_CREAT|O_APPEND)

有关于type解释请见:http://www.cnblogs.com/emanlee/p/4418163.html

由于内核不区分文本文件和二进制文件。所以b作为type的一部分实际上并无作用。

对于fdopen函数,若描述符已被打开,则fdopen为写而打开并不截断该文件。另外,标准I/O追加写方式(a或a+)也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。

在使用w或a类型创建一个新文件时,POSIX.1要求使用如下权限来创建文件:

S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH

可通过unmask值来更改权限。

按照系统默认,流打开时是全缓冲的。若流引用终端设备,则该设备是行缓冲的。

调用fclose可关闭一个打开的流。

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

在该文件被关闭前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。

当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。这也解释了我们之前留下的疑问,缓冲区中数据冲洗的时机——进程正常终止时,根据我们之前学习到的知识,应该是在main函数开始之前注册了某个析构函数,这个析构函数的功能就是冲洗缓冲区。

5.6 读和写流

标准I/O提供三种方式进行非格式化读、写操作。

  1. 每次一个字符的I/O。
  2. 每次一行的I/O。每行都以一个换行符终止。
  3. 直接I/O。

每次一个字符的I/O。由于标准I/O采用行缓冲模式,所以只有出现换行符或写满一行的情况下才会冲洗缓冲区,此时数据已经写入缓冲区中。输入函数如下:

#include <stdio.h>
extern int fgetc (FILE *__stream);
extern int getc (FILE *__stream);
extern int getchar (void); 函数getchar()等同于getc(stdin)

getc的具体实现为:

#include <stdio.h>
#define getc(_fp) _IO_getc (_fp)

使用int作为返回值的原因是:返回所有可能的字符值在加上一个已出错或已达到文件尾端的指示值。我也查了以下仅是ascii码表就已经包含128个符号,因此使用char作为返回数据远远不够。

EOF的定义为

#include <stdio.h>
#ifndef EOF
# define EOF (-1)
#endif

对于到达文件尾端或出错都会返回EOF,所以为了进一步区分这两种情况,引入两个函数:

#include <stdio.h>
extern int feof (FILE *__stream) __THROW __wur; //若以达到文件尾端,则返回非0,否则返回0
extern int ferror (FILE *__stream) __THROW __wur; //若输入出错,则返回非0,否则返回0

为每个流在FILE对象中维护了两个标志。

  1. 出错标志。
  2. 文件结束标志。

由于feof仅检查文件结束标志位是否被置位,文件结束标志是由当前时间之前的最后一次相关操作(包括读、seek等操作)设置的。因此在fgets函数前调用feof还无法判断文件流是否到达尾部。有关于这个问题的解决方法请见:http://book.51cto.com/art/201311/419432.htm

调用clearerr可以清除这两个标志。

#include <stdio.h>
extern void clearerr (FILE *__stream) __THROW;

从流中读取数据后,可以调用ungetc将字符再压送回流中。

#include <stdio.h>
extern int ungetc (int __c, FILE *__stream);

每次一个字符的I/O。输出函数:

#include <stdio.h>
extern int fputc (int __c, FILE *__stream);
extern int putc (int __c, FILE *__stream);
#define putc(_ch, _fp) _IO_putc (_ch, _fp)
extern int putchar (int __c);

5.7 每次一行I/O

5.6节主要分析每次一个字符的I/O,5.7节则介绍每次一行的I/O。

先来看输入函数:

#include <stdio.h>
extern char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream) //从指定流读
     __wur;
extern char *gets (char *__s) __wur __attribute_deprecated__; //从标准输入读

由于是标准I/O,所以一直读到下一个换行符为止,但不会超过n-1个字符。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n-1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。

先来看看对fgets的下一次调用会继续读改行这一点,通过一个验证一下。源码如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	FILE* fp;
	char* buf1 = (char*)malloc(4*sizeof(char));
	char* buf2 = (char*)malloc(7*sizeof(char));

	fp = fopen("./temp","r");

	fgets(buf1,4,fp);
	printf("%s\n",buf1);

	fgets(buf2,7,fp);
	printf("%s\n",buf2);

	free(buf1);
	free(buf2);

	return 0;
}

temp文件中的内容还是我们熟悉的“hello world”

运行结果如下:

hel
lo wor

根据我们之前学到的知识,fgets在没有读到换行符的情况下,会读取n-1个字符,buf1的长度为4,所以会读取三个字符,也就是“hel”,输出第一行也印证了这一知识点。

由于标准I/O使用行缓冲模式,根据实验的结果,我猜测fgets会一次性读取一定长度的数据填充到缓冲区中,每次调用fgets函数会从缓冲区中读取n-1个字符,直到数据被读取完。但如果数据中包含有换行符,则执行I/O操作,此处就是输出到屏幕。

由于缓冲区中的数据没有被全部读取,因此fgets会继续读该行。输出的第二行也验证了这一点。

或者不从源码的角度理解,仅从功能角度理解函数的特点,fgets一次就是读取一行,如果这其中包含有换行符,实际上就意味着数据不是一行而是两行。读取一行数据后,fgets将n-1个字符复制到用户缓冲区中,知道缓冲区中的数据被全部读取完。根据以上描述,我们自己都可以实现一个简单的fgets函数。函数的执行流程如下:

  1. 判断当前位置指针是否为0,若为0则直接跳转到4执行,否则顺序执行。
  2. 读取数据,若遇到"\n"则跳出循环,否则一直读取直到缓冲区满。
  3. 首先设置当前位置指针指向缓冲区的头部。
  4. 计算复制的字符数,若缓冲区中包含有足够的数据则复制用户指定字节数的数据,否则复制缓冲区中实际含有的数据。
  5. 将用户缓冲区的最后一位置为"\0"。
  6. 更新当前位置指针。

fgets函数的源码在此就不详细分析了,争取对fread的源码进行一个简单的分析。

输出函数,每次一行。

#include <stdio.h>
extern int fputs (const char *__restrict __s, FILE *__restrict __stream);
extern int puts (const char *__s);

puts输出后还会将一个换行符写到标准输出。

5.9二进制I/O

二进制I/O的功能主要就是读入或写入任意类型、任意字节的数据,其中的两个参数与MPI接口中包含的参数相类似。

函数原型类型:

#include <stdio.h>
extern size_t fread (void *__restrict __ptr, size_t __size,
		     size_t __n, FILE *__restrict __stream) __wur;
extern size_t fwrite (const void *__restrict __ptr, size_t __size,
		      size_t __n, FILE *__restrict __s);

返回读或写的对象数。对于写,如果返回值小于所要求的__n,则出错。可通过ferror检查。

5.12 实现细节

通过以下函数可以获得文件流指针对应的文件描述符。

#include <stdio.h>
extern int fileno (FILE *__stream) __THROW __wur;

有关于FILE结构的定义如下,位于libio.h中

struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

有关于临时文件与内存流的内容,现在就先不跟大家分享了,以后遇到的时候在详细研究吧。

关于书后习题第6题给大家分享一点我的见解

题目如下:打印的提示信息没有包含换行符,程序也没有调用fflush函数,请解释输出提示信息的原因是什么?

题目中给出了两点条件,逐条来分析。

“打印的提示信息没有包含换行符”,由于标准I/O使用行缓冲模式,所以没有换行符则不执行I/O操作。此处就是不输出提示信息。

“程序也没有调用fflush函数”,fflush函数的功能就是将流所有未写的数据都传送至内核。作为一种特殊情形,如若fp为NULL,则此函数将导致所有输出流被清洗。此处就是不输出提示信息。

以上两点的结果都是不输出提示信息,那么是什么原因导致提示信息的输出?

这是基于行缓冲的特点:从一个行缓冲的流得到输入数据,输出流就会被自动冲洗。此处就是每次调用fgets时标准输出设备将自动冲洗。

关于以上内容我们可以通过几个简单的实验进行验证。首先来看一个永远不会输出的源码:

#include <stdio.h>

int main()
{
	char output[] = "Hello world";
	printf("%s",output);
	while(1);
	return 0;
}

调用printf后,程序进入死循环,通过之前的知识可以了解到一个进程正常终止时,所有带未写缓冲数据的标准I/O流都被冲洗。由于此处进程一直未能正常终止,所以信息一直未能输出。

针对第一点做一点调整:

#include <stdio.h>

int main()
{
	char output[] = "Hello world\n";
	printf("%s",output);
	while(1);
	return 0;
}

此时程序可以正常输出,或者进行相同的调整:

#include <stdio.h>

int main()
{
	char output[] = "Hello world";
	printf("%s\n",output);
	while(1);
	return 0;
}

好了看完第一点,针对第二点作出调整:

#include <stdio.h>

int main()
{
	char output[] = "Hello world";
	printf("%s",output);
	fflush(stdout);
	while(1);
	return 0;
}

最后一种调整方法:

#include <stdio.h>

int main()
{
	char output[] = "Hello world";
	printf("%s",output);
	fgetc(stdin);
	while(1);
	return 0;
}

在标准输出后进行标准输入。

时间: 2024-10-16 14:03:03

APUE读书笔记-第五章 标准I/O库的相关文章

APUE学习笔记:第五章 标准I/O库

5.1 引言 标准I/O库处理很多细节,例如缓冲区分配,以优化长度执行I/O等.这些处理不必担心如何使用正确的块长度.这使得它便于用户使用,但是如果不较深入地了解I/O库函数的操作,也会带来一些问题 5.2 流和FILE对象 对于ASCII字符集,一个字符用一个字节表示.对于国际字符集,一个字符可用多个字节表示.标准I/O文件流可用于单字节或多字节字符集. 流的定向决定了所读.写的字符是单字节还是多字节的.当一个流最初被创建时,它并没有定向.如若在未定向的流上使用一个多字节I/O函数,则将该流的

《Linux内核设计与实现》读书笔记 第五章 系统调用

第五章系统调用 系统调用是用户进程与内核进行交互的接口.为了保护系统稳定可靠,避免应用程序恣意忘形. 5.1与内核通信 系统调用在用户空间进程和硬件设备间添加了一个中间层, 作用:为用户空间提供了一种硬件的抽象接口:保证了系统的稳定和安全,避免应用程序不正确使用硬件,窃取其他进程的资源,或做出危害系统的行为:为了实现多任务和虚拟内存. Linux提供的系统调用比大部分操作系统少得多. 5.2 API.POSIX.和C库 一个API定义了一组应用程序使用的编程接口.(API和系统调用不是一一对应)

APUE读书笔记-第三章 文件I/O

今天看得挺快的,一下子就把第二章看完了,不过第二章也确实看得不仔细,这一章其实在程序设计中还是非常重要的,因为这一章的内容决定了程序的可移植性. 好了,回到这一章的主题文件I/O. 3.2节主要对文件描述符的概念进行了简单的介绍.根据APUE:文件描述符是一个非负整数.当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符.我也简单地翻了一下LKD和<深入理解linux内核>,其中对于文件描述符的讲解不是很多,所以对于文件描述符也谈不出来太深入理解,各大家还是分享一篇blog吧.

《linux内核设计与实现》读书笔记第五章——系统调用

第5章 系统调用 操作系统提供接口主要是为了保证系统稳定可靠,避免应用程序恣意妄行. 5.1 与内核通信 系统调用在用户空间进程和硬件设备之间添加了一个中间层. 该层主要作用有三个: 为用户空间提供了一种硬件的抽象接口. 系统调用保证了系统的稳定和安全 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口. 5.2 API.POSIX和C库 1.API可以在各种不同的操作系统实现,给应用程序提供完全相同的接口,而它们本身在这些系统上的实现却可能迥异. 2.在Unix世界中

APUE读书笔记-第四章 文件和目录

到第四章了,不知什么时候才能把这本书看完,耽误的时间太多了. 第四章是在第三章的基础上,主要描述文件系统的其他性质和文件的性质. 4.2 stat.fstat.fstatat.lstat函数 首先来看看这四个函数的原型: #include <sys/stat.h> ///usr/include/x86_64-linux-gnu/sys/ int stat (const char *__restrict __file, struct stat *__restrict __buf) int fst

APUE第五章标准I/O库

使用标准IO库时,进程(或者是shell)自动打开并关联到程序运行窗口的标准输入输出流对象,为标准输入,标准输出,标准出错,这些流对象引用的文件,与不带缓冲的IO函数使用的文件描述符,它们关联的文件对是相同的,这些文件应该指的就是那些窗口,窗口在显示器上(显示器是文件),如果使用了重定向,那么所谓的文件,就是硬盘上的指定文件.也就是说,流对象(标准IO使用)与文件描述符(不带缓冲的IO使用),它们关联到相同的文件. 程序清单5-2用fgets和fputs将标准输入复制到标准输出 #include

apue读书笔记-第14章 高级IO

多路I/O转接 与select函数不同,poll不是为每个状态(可读性.可写性和异常状态)构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及其所关心的状态 readv和writev函数 作用:在一次函数调用中读.写多个非连续缓存区 总结:应当用尽量少的系统调用次数来完成任务.如果只写少量的数据,会发现自己复制数据然后使用一次write会比用writev更合算.但也可能发现,这样获得的性能提升并不值得,因为管理中间缓冲区会增加程序复杂度. readn和write

APUE读书笔记-第六章 系统数据文件和信息

昨天看完了,今天来看看第六章.感觉第六章的内容不是非常重要.简单看看吧 6.2 口令文件 口令文件其实就是/etc文件夹下的passwd文件,但处于安全性的考虑,我们无法直接读取它.就是通过直接限制权限的方式对其进行保护,passwd文件具体权限如下: -rw-r--r-- 1 root root 可以看到只有root用户具有读写权限,与root同组的用户与其他用户仅具有读权限. 不过为了解决以上问题,Linux中给出了一系列数据结构与函数帮助我们操纵口令文件,首先是关键数据结构,定义位于/in

《Python基础教程》 读书笔记 第五章(下)循环语句

5.5.1while循环 x=1 while x<=100: print x x+=1 确保用户输入了名字: name="" while not name: name=raw_input('please enter your name:') print 'hello,%s!'%name 5.5.2for循环 while语句非常灵活.它可以用来在任何条件为真的情况下重复执行一个代码块.一般情况下这样就够用了,但是有些时候还得量体裁衣.比如要为一个集合(序列和其他可迭代对象)的每个元