【APUE】Chapter5 Standard I/O Library

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中调用了readwrite这样的calls。而standard I/O的设计目的,有一条就是尽量减小调用readwrite的次数。

  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\n", stdout);
    if(getchar()==EOF)
        err_sys("getchar error");
    fputs("one line to standard error\n",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\n", 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\n", tmpnam(NULL));

    tmpnam(name);
    printf("%s\n",name);

    fp = tmpfile();
    fputs("one line of output\n",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\n", template);
13     close(fd);
14
15     if (stat(template,&sbuf)<0) {
16         if(errno==ENOENT)
17             printf("file doesn‘t exist\n");
18         else
19             err_sys("stat failed");
20     }
21     else
22     {
23         printf("file exists\n");
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..\n");
34     make_temp(good_template);
35     printf("trying to create second temp file...\n");
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]==‘\0‘?‘#‘:buf[i]);
 9     printf("\n");
10 }
11
12 int pr_offset(FILE *fp)
13 {
14     printf("    memeory stream offset: %ld\n",ftell(fp));
15 }
16
17 int main()
18 {
19     FILE *fp;
20     char buf[BSZ];
21
22     memset(buf, ‘a‘, BSZ-2);
23     buf[BSZ-2]=‘\0‘;
24     buf[BSZ-1] = ‘X‘;
25     printf("befor fmemopen initial buffer contents: ");pr_print(buf);
26     fp = fmemopen(buf, BSZ, "w+");
27     pr_offset(fp);
28     printf("after fmemopen initial buffer contents: ");pr_print(buf);
29     fprintf(fp, "hello, world"); /*此时fp标志在buf的起始位置*/
30     //pr_offset(fp);
31     printf("before flush: %s\n",buf);
32     fflush(fp);
33     printf("after fflush:");
34     pr_print(buf); /*本来buf的内容是aaaa...\0#,但是由于fprintf的操作hello world写到前面buf的前面几个位置,并且后面跟了一个\0*/
35     pr_offset(fp);
36     printf("len of string in buf = %ld\n", (long)strlen(buf));
37
38     memset(buf, ‘b‘, BSZ-2);
39     buf[BSZ-2] = ‘\0‘;
40     buf[BSZ-1] = ‘X‘;
41     fprintf(fp, "hello, world");
42     pr_offset(fp);
43     fseek(fp, 0, SEEK_SET);
44     printf("after fseek:");
45     pr_offset(fp);
46     pr_print(buf);
47     printf("len of string in buf = %ld\n", (long)strlen(buf));
48
49     memset(buf, ‘c‘, BSZ-2);
50     buf[BSZ-2] = ‘\0‘;
51     buf[BSZ-1] = ‘X‘;
52     fprintf(fp, "hello, world");
53     fclose(fp);
54     printf("after fclose:");
55     pr_print(buf);
56     printf("len of string in buf = %ld\n", (long)strlen(buf));
57
58     return(0);
59 }

    代码执行结果如下:

    

    上述代码在书上5.15的例子基础上做了修改,原因是更好的理解各种操作对实际内存数据的影响。

    这里有个点需要明确:printf函数在输出内存中字符串的时候,如何判断字符串结束了?首次遇见‘\0‘就认为字符串结束了

    下面按照执行顺序来分析为:

    1. memset()之后,以buf为起始地址的内存字符串都被赋值为‘a‘了。

    2. 执行fmemopen,将buf以w+的方式关联到一个FILE *类型变量fp上。这个时候,看书上P171的阐述,以‘w+’这种方式打开的时候,""truncate to 0 length and open for writing"。

      (1)为了验证书上这句话,我调用了DIY的函数pr_offset,来查看此时current file position(具体ftell函数,可以参考书上P158),stream offset确实是0。

      (2)那么stream offset是0意味着什么呢?我调用了DIY的函数pr_print函数,逐个输出buf开始的BSZ个字符:发现原来的首个字符‘a‘变成了‘\0‘,客观上在我实验的环境中就是这样。

    3. 执行fprintf函数,向fp指向的buf写“hello, world”12个byte。但是这个写并不会马上写进去:

      (1)执行fflush之前,buf的首个字符还是‘\0‘,因此用printf函数输出buf,自然什么都不会输出。

      (2)执行fflush之后,fp的内容被写进去了;调用pr_print函数可以看到"hello, world"被写进去了,并且紧接着还写进去了一个‘\0‘;此时fp的offset变成了12,而且用strlen函数去统计buf的长度也是12。由此可见fprintf的工作方式,而且可以看到strlen的实现也差不多是根据第一个遇见的‘\0‘来统计字符串长度的。

    4. 现在另起炉灶,把buf后面的BSZ长度的字符都设成‘b‘了。

      (1)还是调用fprintf往fp里面再写一次“hello, world”;注意,此时fp的offset是12,跟执行memset没有关系,此时执行过第二次写“hello, world”之后,fp的offset变成了24。

      (2)此时,执行fseek,将fp的offset移动到0,并用pr_offset函数验证。

      (3)再次输出buf起始的BSZ个字符,发现“hello, world"确实从第13个位置写到了第24个位置,并且在末尾跟上了一个‘\0‘,验证了上面的分析。

    5. 再次处理一下,把buf后面的BSZ长度的字符都设成‘c‘了。

      (1)先明确一点,此时fp的offset虽然恢复0了,但是fp关联的memory stream的amount of data还是24个字符,这一点比较关键。

      (2)此时还是fprintf往fp里面写"hello, world"12个字符,随后把fp关掉。

      (3)此时,再用pr_print逐个输出buf起始的BSZ个字符,发现了一个与之前不同的地方,即"hello, world"的后面没有在跟着一个‘\0‘。回想(1)中提到的,此时fp关联的memory steam的amount还是48,如果从0开始写12个byte,并不会改变整个memory stream的amount,因此后面就没有再跟着‘\0‘了,这也就解释了最后的一组输出。

    6. 如果我们将上述代码的30行的屏蔽符号去掉,在执行代码,会得到以下结果:

    

    这里只需要注意before flush后面的输出即可:为什么没有用fflush,还是刷出来了buf呢?

    我猜,这是因为ftell(fp)中有刷新fp的操作,所以相当于隐藏着调用了一个fflush了。

  这个memory streams一开始看的并不清晰,主要是不明确printf的实现原理,遇到‘\0‘不输出后面的type了。所以,掌握这些基础函数的原理对提高工作效率比较有帮助。

时间: 2024-10-27 07:02:32

【APUE】Chapter5 Standard I/O Library的相关文章

【APUE】孤儿进程与僵死进程

基本概念: 在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程.子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束. 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态. 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作. 僵尸进程:一个进程

【APUE】fork函数

#include <unisth.h> pid_t fork(void) fork函数被调用一次,返回两次.子进程的返回值是0,父进程的返回值是子进程的进程id. 子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本,子进程获得父进程数据空间.堆和栈的副本.注意:这是子进程所拥有的副本,父子进程并不共享这些存储空间部分.父子进程共享正文段 #include <stdio.h> #include <sys/types.h> #include <unis

【APUE】wait与waitpid函数

当一个进程终止时,内核就向其父进程发送SIGCHLD信号.因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知.父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数.对于这种信号的系统默认动作是忽略它. 调用wait或waitpid的进程发生的情况如下: 1.如果所有子进程都还在运行,则阻塞 2.如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回 3.如果它没有任何子进程,则立即出错返回 #include <sys/wait.h>

【APUE】进程基础

进程标识符:非负整数 ID为0的进程通常是是调度进程,常被称为交换进程.该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程 ID为1的进程是init进程,在自举过程结束时由内核调用,此进程负责在自举内核后启动一个UNIX系统,init通常读入与系统有关的初始化文件,并将系统引导到一个状态,init进程不会终止.它是普通的用户进程,以超级用户特权运行 常用函数: pid_t getpid(void)  返回调用进程的进程id pid_t getppid(void) 返回调用进程

【APUE】进程间通信之管道

管道是UNIX系统IPC最古老形式,并且所有UNIX系统都提供此种通信机制.管道由下面两种局限性: 1)历史上,它们是半双工的(即数据只能在一个方向上流动) 2)它们只能在具有公共祖先的进程之间使用.通常,一个管道由一个进程创建,然后该进程调用fork,此后父.子进程之间就可应用该管道 管道由调用pipe函数创建: #include <unistd.h> int pipe(int filedes[2]);//若成功则返回0,出错返回-1 注意:filedes[0]为读而打开,filedes[1

【APUE】用户态与内核态的区别

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态).此时处理器处于特权级最高的(0级)内核代码中 执行.当进程处于内核态时,执行的内核代码会使用当前进程的内核栈.每个进程都有自己的内核栈.当进程在执行用户自己的代码时,则称其处于用户运行态(用 户态).即此时处理器在特权级最低的(3级)用户代码中运行. 内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, intel cpu提供Ring0-Ring3三种级别的运行模式,Rin

【APUE】vim常用命令

转自:http://coolshell.cn/articles/5426.html 基本命令: i → Insert 模式,按 ESC 回到 Normal 模式. x → 删当前光标所在的一个字符. :wq → 存盘 + 退出 (:w 存盘, :q 退出)   (陈皓注::w 后可以跟文件名) dd → 删除当前行,并把删除的行存到剪贴板里 p → 粘贴剪贴板 简单的移动光标 0 → 数字零,到行头 ^ → 到本行第一个不是blank字符的位置(所谓blank字符就是空格,tab,换行,回车等)

【APUE】进程间通信之FIFO

FIFO也称为有名管道,它是一种文件类型.FIFO简单理解,就是它能把两个不相关的进程联系起来,FIFO就像一个公共通道,解决了不同进程之间的“代沟”.普通的无名管道只能让相关的进程进行沟通(比如父shell和子shell之间). 创建FIFO类似于创建文件 #include <sys/stat.h> int mkfifo(const char *pathname,mode_t mode); FIFO的出现,极好地解决了系统在应用过程中产生的大量的中间临时文件的问题.FIFO可以被shell调

【APUE】文件I/O

文件描述符 对于内核而言,所有打开的文件都通过文件描述符引用.文件描述符是一个非负整数. 按照惯例,UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误输出相关联. open函数:打开或创建一个文件 #include <fcntl.h> int open(const char *pathname,int oflag,...); pathname是要打开或创建文件的名字,oflag参数用来说明此函数的多个选项 create函数:创建文