5.1 Introduction
这章介绍的standard I/O都是ISOC标准的。用这些standard I/O可以不用考虑一些buffer allocation、I/O optimal-sized的细节,增加了易用性。但是也有一些问题。
5.2 Streams and FILE Objects
1. Chapter3中提到的I/O routines的核心是file descriptor;而在standard I/O背景下,相应的概念换成了stream。
2. standard I/O可以设置signle character和multi character的不同模式。
5.3 Standard Inpupt, Standard Output, and Standard Error
STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO 也在standard I/O中有定义。头文件在<stdio.h>中
5.4 Buffering
standard I/O中调用了read和write这样的calls。而standard I/O的设计目的,有一条就是尽量减小调用read和write的次数。
buffering的方式分为以下三种:
1. Fully buffered
把buffer写满了就往外输出。如果强制往外输出,可以调用fflush函数。
2. Line buffered
根绝是否出现newline character判断是否往外输出buffer的内容。
但是,有些时候Line buffered等不到newline character也会被动刷新输出:
(1)standard I/O留给Line buffer的容量写满了,等不到newline character出现就刷新输出。这个比较好理解,满了就输出了。
(2)当有来自unbuffered stream或line-buffered的input请求,会强制刷新输出。这个书上讲的没理解,具体看到例子再说。
3. Unbuffered
这个比较好理解,不用buffer直接输出。例子,一般的error stream都是unbuffered的方式。
另,介绍了一个函数fflush(FILE *fp) 当fp==NULL的时候,刷新全部的output stream。
5.5 Opening a Stream
记住俩函数:
fopen(const char *restrict pathname, const char *restrict type) (char *restrict意思是参数只能是受限制的字符串,这里只能是'w','r'这类的限制的几个字符串)
fclose(FILE *fp)
5.6 Reading and Writing a Stream
读单个字符的三个函数:
int getc(FILE *fp)
int fgetc(FILE *fp)
int getchar(void)
三个函数的返回值有如下几种情况:
(1)如果执行成功且没有到末尾,则返回next character
(2)如果执行到文件末尾了,则返回EOF
(3)如果出错了,则返回error
书上关于返回值有这样的陈述“These three functions return the next character as an unsigned char converted to an int”。
读的是字符为什么返回值要采用整数的形式?
因为,可以区分正常字符、文件末尾、出错三种情况。如果是正常字符,转换成int后都是正的;如果是EOF和error则都是负的。
有的系统上EOF和error都是-1,那么还怎么区分是出错了还是读到文件末尾了呢?
还有以下两个函数,判断到底是error为真,还是eof为真:
int ferror(FILE *fp)
int feof(FILE *fp)
如果是真的,则返回非零;否则,返回零。
5.7 Line-at-a-Time
char *fgets(char *restrict buf, int n, FILE *restrict fp)
一次读一行,最多读n-1个字符;超过n-1个字符的部分就被抹掉了。n-1的原因是buffer必须是以null结束的
int fputs(const char*restrict str, FILE *restrict fp)
注意是否用补上换行符。
5.8 Standard I/O Efficiency
比较两种读写模式的效率,读一个500M大小的文件,然后在写到同样的文件中;比较这一读一写的效率。
代码1:
1 #include "apue.h" 2 3 int main() 4 { 5 int c; 6 while ((c=getc(stdin))!=EOF) 7 if (putc(c, stdout)==EOF) 8 err_sys("output error"); 9 if (ferror(stdin)) 10 err_sys("input error"); 11 exit(0); 12 }
执行耗时如下:
代码2:
1 int main() 2 { 3 char buf[MAXLINE]; 4 while (fgets(buf, MAXLINE, stdin)!=NULL) 5 { 6 if (fputs(buf,stdout)==EOF) { 7 err_sys("output error"); 8 } 9 } 10 11 if (ferror(stdin)) { 12 err_sys("input error"); 13 } 14 exit(0); 15 }
执行耗时如下:
代码1是逐个读字符,代码2是逐行读字符。对比二者执行结果,可以获得以下的结论:
(1)对比二者的耗时,发现system time都差不多:这意味着kernel耗时都差不多。
(2)差别在于user time不一样:原因就在于fgetc fputc读写同样的字符串需要调用的系统read write次数多;而fgets fputs调用的read write次数少很多,所以real time上体现了效率提高了。
5.9 Binary I/O
size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)
size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp)
每次读写一个完整的数据结构:struct int float这样的内容。
这样binary I/O的关键就在于这样一个完整的数据结构占多少byte。
书上提醒,完整的数据结构到底占多少byte跟编译器complier有关,还跟不同的机器架构有关,即提醒此处可能有坑。
5.10 Positioning a Stream
long ftell(FILE *fp) :返回current file position indicator if OK, -1L on error
int fseek(FILE *fp, long offset, int whence) :操作stream;通过whence参数控制操作的方式;成功返回0,出错返回-1
void rewind(FILE *fp): stream移动到文件的beginning
类比Chapter3.6提出来的lseek()函数,lseek这个函数集成的功能比较多,可以直接得到fd对应的offset位置;而fseek由于只能返回01,因此需要单拎出来ftell这个函数来返回stream的当前位置
书上还介绍了两个ISO C standard的函数:
int fgetpos(FILE *restrict fp, fpos_t *restrict pos)
int fsetpos(FILE *fp, const fpos_t *pos)
书上对这两个函数给出的tips是便于porting to non-Unix systems (便于移植)
5.11 Formatted I/O
printf和scanf函数,用到再细看
5.12 Implementation Details
1. 给出了Unix System中,standard I/O最终还是调用了Chapter3中介绍的各种I/O routine来完成的。因此,每个standard I/O stream必然对应一个file descriptor。靠int fileno(FILE *fp)来实现这个功能。
2. 给出了一个简易的buffering three standard stream的实现,具体如下:
#include "apue.h" void pr_stdio(const char *, FILE *); int is_unbuffered(FILE *); int is_linebuffered(FILE *); int buffer_size(FILE *); int main() { FILE *fp; fputs("enter any character ", stdout); if(getchar()==EOF) err_sys("getchar error"); fputs("one line to standard error ",stderr); pr_stdio("stdin", stdin); pr_stdio("stdout", stdout); pr_stdio("stderr", stderr); if ((fp=fopen("/etc/passwd","r"))==NULL) { err_sys("fopen error"); } if (getc(fp)==EOF) err_sys("getc error"); pr_stdio("/etc/passwd",fp); exit(0); } void pr_stdio(const char *name, FILE *fp) { printf("stream = %s, ", name); if (is_unbuffered(fp)) { printf("unbuffered"); } else if (is_linebuffered(fp)) { printf("line buffered"); } else printf("fully buffered"); printf(", buffer size = %d ", buffer_size(fp)); } #if defined(_IO_UNBUFFERED) int is_unbuffered(FILE *fp) { return(fp->_flags & _IO_UNBUFFERED); } int is_linebuffered(FILE *fp) { return(fp->_flags & _IO_LINE_BUF); } int buffer_size(FILE *fp) { return(fp->_IO_buf_end - fp->_IO_buf_base); } #elif defined(__SNBF) int is_unbuffered(FILE *fp) { return(fp->_flags & __SNBF); } int is_linebuffered(FILE *fp) { return(fp->_flags & __SLBF); } int buffer_size(FILE *fp) { return(fp->_bf.size); } #elif defined(_IONBF) #ifdef _LP64 #define _flag __pad[4] #define _flag __pad[1] #define _base __pad[2] #endif int is_unbuffered(FILE *fp) { return(fp->_flags & _IONBF); } int is_linebuffered(FILE *fp) { return(fp->_flags & _IOLBF); } int buffer_size(FILE *fp) { #ifdef _LP64 return(fp->_base - fp->_ptr); #else return(BUFSIZ); #endif } #else #error unknown stdio implementation #endif
执行这个代码,结果如下:
(1)如果输入输出都是终端,则系统给出的buffer策略是line buffered
(2)如果输入输出都是文件,则系统给出的buffer策略是fully buffered
(3)如果是error输出,则默认是unbuffered策略
5.13 Temporary Files
1. 给了char *tmpnam(char *ptr) 和 FILE *tmpfile(void)的例子,使用方法。
#include <stdio.h> #include <stdlib.h> #define MAXLINE 4096 int main() { char name[L_tmpnam], line[MAXLINE]; FILE *fp; printf("%s ", tmpnam(NULL)); tmpnam(name); printf("%s ",name); fp = tmpfile(); fputs("one line of output ",fp); rewind(fp); fgets(line, sizeof(line), fp); fputs(line, stdout); exit(0); }
代码执行结果如下:
(1)前两个printf是讲tmpnam的用法,作用是生成个独一无二的文件名;并展示了参数是NULL和不是NULL都是怎么个用法。
(2)后面一段是先用tmpfile()生成一个临时文件,向文件中写一行,再把写进去的内容读出来输出到终端。这样证明确实产生了临时文件,并且随着程序结束,临时文件也没有了。
2. 介绍了更为安全的两个临时文件函数char *mkdtemp(char *template) 和 int mkstemp(char *template)
从紧上面1的代码执行结果中可以看到有个关于tmpnam的warning。这个warning的原因是什么呢?
tmpnam函数有个弊端:tmpnam产生独一无二的临时文件名并不是一个原子操作;可能有个time window,tmpnam产生文件名A的同时,另一个应用产生相同文件名的文件了。
因此,就需要引出mkstemp这样的函数,保证是原子操作,即函数产生的文件名是独一无二的。
首先得明确一下临时文件的产生过程:1. 生成一个独一无二的临时文件名 2.马上unlink了
为什么要这么做呢?kernel判断一个file是否可以被delete主要取决于两点:
1. link数是0了
2. 没有process在使用这个file
因此,按照上述临时文件的原理,先生成文件名,然后unlink,则等着当前的process结束,或放弃对file的占用,kernel就把file给delete了。
这里需要注意的是,unlink这个操作需要我们自己来完成。
看个例子:
1 #include "apue.h" 2 #include <errno.h> 3 4 void make_temp(char *template) 5 { 6 int fd; 7 struct stat sbuf; 8 9 if ((fd = mkstemp(template))<0) { 10 err_sys("can't create temp file"); 11 } 12 printf("temp name = %s ", template); 13 close(fd); 14 15 if (stat(template,&sbuf)<0) { 16 if(errno==ENOENT) 17 printf("file doesn't exist "); 18 else 19 err_sys("stat failed"); 20 } 21 else 22 { 23 printf("file exists "); 24 unlink(template); 25 } 26 } 27 28 int main() 29 { 30 char good_template[] = "/tmp/dirXXXXXX"; 31 char *bad_template = "/tmp/dirXXXXXX"; 32 33 printf("tring to crate first temp file.. "); 34 make_temp(good_template); 35 printf("trying to create second temp file... "); 36 make_temp(bad_template); 37 exit(0); 38 }
代码执行结果如下:
从这段代码中可以看到,临时文件确实是存在的(“file exists”可以说明),并且需要我们手工unlink。
这里还涉及到一个细节,常量字符串不能修改,如果把常量字符串送到mkstemp函数中,会报段错误。因为,常量字符串在只读segment上面,mkstemp函数要修改这个常量字符串就会报错。
5.14 Memory Streams
这个部分说的是:是否可以像读写文件一样操作内存中的一块区域。下面这个函数就帮助user实现了这样的功能。
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type)
直接看一段代码:
1 #include "apue.h" 2 3 #define BSZ 48 4 5 int pr_print(char buf[]) 6 { 7 int i; 8 for ( i=0; i<BSZ; ++i) printf("%c",buf[i]=='