Linux编程笔记(一)——标准IO

综述

标准I/O提供的f系列I/O文件操作,如(fwrite,fread,fseek等)都是提供了有缓存的接口,如果数据需要送到像控制终端等交互设备,必须要设置fflush或者使用非缓存I/O。

而缓存可以减少系统调用的次数,增加效率。

这是标准I/O相对于系统调用的特点,也是优点。

而系统调用的I/O读写是无缓冲的,实时性高。

到这里我们还是没有说明白为什么标准 IO 吞吐量高,而系统 IO 实时性高。我再举个简单的栗子:门卫老大爷负责送信到邮局,他去一次邮局要花费 10 分钟的时间,而每次最多能送 20 封信,每当信件累计到 20 封的时候他就要动身去邮局了。但是当他收到一封加急的邮件时,就会立即去一趟邮局。系统 IO 就好比每收到一封信时都要去一趟邮局,所以实时性高。而标准 IO 就好比要攒够 20 封信才去一趟邮局,所以吞吐量高,因为用户把信件交到老大爷的手上时就会立即返回,响应速度快,用户体验更好。而我们使用 fflush(3) 之类的函数强制刷新缓冲的时候,就相当于是老大爷收到了一封加急信件需要立即去一趟邮局送信。

标准I/O

1.fopen(3)

1
2
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);

这是今天要学习的第一个函数,在操作文件之前,我们需要通过 fopen() 函数将文件打开,通过这个函数我们可以告诉操作系统我们要操作的是哪个文件,以及用什么样的方式操作这个文件。

参数列表:

path:要操作的文件路径。

mode:文件的打开方式,这个打开方式一共分为6种。

  r:以只读的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。

  r+:以读写的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。

  w:以只写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。

  w+:以读写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。

  a:以追加的方式打开文件,如果文件不存在则创建,且文件位置指针会被定位到文件最后一个有效字符的后面(EOF,end of the file)。

  a+:以读和追加的方式打开文件,如果文件不存在则创建,且读文件位置指针会被初始化到文件首,但是总是写入到最后一个有效字符的后面(EOF,end of the file)。

返回值:

  FILE 是一个由标准库定义的结构体,各位童鞋不要企图通过手动修改结构体里的内容来实现文件的操作,一定要通过标准库函数来操作文件。

  这个函数返回一个 FILE 类型的指针,它作为我们打开文件的凭据,后面所有对这个文件的操作都需要使用这个指针,而且使用之后一定不要忘记调用 fclose(3) 函数释放资源。

  如果该函数返回了一个指向 NULL 的指针,则表示文件打开失败了,可以通过 errno 获取到具体失败的原因。

2.fclose(3)

1
2
3
4
5
1 fclose - close a stream
2
3 #include <stdio.h>
4
5 int fclose(FILE *fp);

这个函数是与 fopen(3) 函数对应的,当我们使用完一个文件之后,需要调用 fclose(3) 函数释放相关的资源,否则会造成内存泄漏。当一个 FILE 指针被 fclose(3) 函数成功释放后,这个指针所指向的内容将不能再次被使用,如果需要再次打开文件还需要调用 fopen(3) 函数。

参数列表:

  fp:fopen(3) 函数的返回值作为参数传入即可。

3.fgets(3)

1
2
3
4
5
6
7
1 fgets - input of strings
2
3 #include <stdio.h>
4
5 int fgetc(FILE *stream);
6
7 char *fgets(char *s, int size, FILE *stream);

从输入流 stream 中读取一个字符串回填到 s 所指向的空间。

这里出现了一个 stream 的概念,这个 stream 是什么呢,它被成为“流”,其实就是操作系统对于可以像文件一样操作的东西的一种抽象。它并非像自然界的小河流水一样潺潺细流,而通常是要么没有数据,要么一下子来一坨数据。当然 stream 也未必一定就是文件,比如系统为每个进程默认打开的三个 stream:stdin、stdout、stderr,它们本身就不是文件,就是与文件有着相同的操作方式,所以同样被抽象成了“流”。

这个函数并没有解决 gets(3) 函数可能会导致的数组越界问题,而是通过牺牲了获取数据的正确性来保证程序不会出现数组越界的错误,实际上是掩盖了 gets(3) 的问题。

该函数遇到如下四种情况会返回:

  1.当读入的数据量达到 size - 1 时;

  2.当读取的字符遇到 \n 时;

  3.当读取的字符遇到 EOF 时;

  4.当读取遇到错误时;

并且它会在读取到的数据的最后面添加一个 \0 到 s 中。

返回值:

  成功时返回 s。

  返回 NULL 时表示出现了错误或者读到了 strem 的末尾(EOF)。

4.fread(3)、fwrite(3)

1
2
3
4
5
6
7
1 fread, fwrite - binary stream input/output
2
3 #include <stdio.h>
4
5 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
6
7 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

这两个函数使用得最频繁,用来读写 stream,通常是用来读写文件。

参数列表:

  ptr:fread(3) 将从 stream 中读取出来的数据回填到 ptr 所指向的位置;fwrite(3) 则将从 ptr 所只想的位置读取数据写入到 stream 中;

  size:要读取的每个对象所占用的字节数;

  nmemb:要读取出多少个对象;

  stream:数据来源或去向;

返回值:

  注意这两个函数的返回值表示的是成功读(写)的对象的个数,而不是字节数!

例如:

  read(buf, 1, 10, fp); // 读取 10 个对象,每个对象 1 个字节

  read(buf, 10, 1, fp); // 读取 1 个对象,每个对象 10 个字节

当数据量充足的时候,这两种方式是没有区别的。

但是!!当数据量少于 size 个字节的整倍数时,第二种方法的的最后一个对象会读取失败。比如数据只有 45 个字节,那么第二种方法的返回值为 4,因为它只能成功读取 4 个对象。

所以通常第一种方式读写数据使用得比较普遍。

5.atoi(3)

1
2
3
4
5
6
7
8
1 atoi, atol, atoll, atoq - convert a string to an integer
2
3 #include <stdlib.h>
4
5 int atoi(const char *nptr);
6 long atol(const char *nptr);
7 long long atoll(const char *nptr);
8 long long atoq(const char *nptr);

atoi(3) 函数族在这里提一下,主要是为了下面的 printf(3) 函数族做一个铺垫。

这些函数的作用是方便的将一个字符串形式的数字转换为对应的数字类型的数字。

上面这句话可能有点坳口,给你看个例子就懂了,下面是伪代码。

1
2
3
1 char *str = "123abc456";
2 int i = 0;
3 i = atoi(str);

i 的结果会变成 123。这些函数会转换一个字符串中地一个非有效数字前面的数字。如果很不幸这个字符串中的第一个字符就不是一个有效数字时,那么它们会返回 0。

6.printf(3)家族

1
2
3
4
5
6
7
8
1 printf, fprintf, sprintf, snprintf - formatted output conversion
2
3 #include <stdio.h>
4
5 int printf(const char *format, ...);
6 int fprintf(FILE *stream, const char *format, ...);//输出到文件流
7 int sprintf(char *str, const char *format, ...);//输出到字符串流
8 int snprintf(char *str, size_t size, const char *format, ...);

printf(3) 函数大家一定不会陌生了,应该从写 Hello World! 的时候就接触到了的吧,所以我也不多介绍了,主要介绍两个内容。

一个是面试常考的一个问题,用了这么久的 printf(3) 函数,大家有没有注意过它的返回值表示什么呢?

printf(3) 的返回值表示成功打印的有效字符数量,不包括 \0。

另一个要说的就是刚才我们提到了 atoi(3) 函数族,它们负责将字符串转换为数字,那么有没有什么函数可以将数字转换为字符串呢,其实通过 sprintf(3) 或 snprintf(3) 就可以了。

有了这两个函数,不仅可以方便的将数字转换为字符串,还可以将多个字符串任意拼接为一个完整的字符串。

这里直接讲解一下 snprintf(3) 函数。

参数列表:

  str:拼接之后的结果会回填到这个指针所指向的位置;

  size:size - 1 为回填到 str 中的最大长度,数据超过这个长度的部分则会被舍弃,然后会在拼接的字符串的尾部追加 \0;

  format:格式化字符串,用法与 printf(3) 相同,这里不再赘述;

  …:格式化字符串的参数,用法与 printf(3) 相同;

这个函数与 fputs(3) 一样,只是掩盖了 sprintf(3) 可能会导致的数组越界问题,通过牺牲数据的正确性来保证程序不会出现数组越界的错误。

7.scanf(3)

1
2
3
4
5
6
7
1 scanf, fscanf, sscanf - input format conversion
2
3 #include <stdio.h>
4
5 int scanf(const char *format, ...);
6 int fscanf(FILE *stream, const char *format, ...);
7 int sscanf(const char *str, const char *format, ...);

scanf(3) 函数族相信也不用过多的介绍了,这里唯一要强调的就是:scanf(3) 函数支持多种格式化参数,唯独 %s 是不能安全使用的,可能会导致数组越界,所以当需要接收用户输入的时候可以使用 fgets(3) 等函数来替代。

8.fseek(3)

1
2
3
4
5
6
7
8
9
1 fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream
2
3 #include <stdio.h>
4
5 int fseek(FILE *stream, long offset, int whence);
6
7 long ftell(FILE *stream);
8
9 void rewind(FILE *stream);

fseek(3) 函数族的函数用来控制和获取文件位置指针所在的位置,从而能够使我们灵活的读写文件。

介绍一下 fseek(3) 函数的参数列表:

  stream:这个已经不需要多介绍了吧,就是准备修改文件位置指针的文件流;

  offset:基于 whence 参数的偏移量;

  whence:相对于文件的哪里;有三个宏定义可以作为它的参数:SEEK_SET(文件首), SEEK_CUR(当前位置), or SEEK_END(文件尾);

返回值:

  成功返回 0;失败返回 -1,并且会设置 errno。

单独看参数列表也许你还有所疑惑,那么我写点简单的伪代码作为例子:

1
2
1 fseek(fp, -10, SEEK_CUR); // 从当前位置向前偏移10个字节。
2 fseek(fp, 2GB, SEEK_SET); // 可以制造一个空洞文件,如迅雷刚开始下载时产生的文件。

ftell(3) 函数以字节为单位获得文件指针的位置。

fseek(fp, 0, SEEK_END) + ftell(3) 可以计算出文件总字节大小。

还有一个值得大家注意的问题:

fseek(3) 和 ftell(3) 的参数和返回值使用了 long,所以取值范围为 -2GB ~ (2GB-1),而 ftell(3) 只能表示 2G-1 之内的文件大小,所以可以使用 fseeko(3) 和 ftello(3) 函数替代它们,但它们只是方言(SUSv2, POSIX.1-2001.)。

由于这两个函数比较古老,所以设计的时候认为 +-2GB 的取值范围已经足够用了,而没有意识到科技发展如此迅速的今天,2GB 大小的文件已经完全不能满足实际的需求了。

rewind(3) 函数将文件位置指针移动到文件起始位置,相当于:

1
1 (void) fseek(stream, 0L, SEEK_SET)

9.getline(3)

1
2
3
4
5
6
7
8
9
10
11
12
13
1 getline - delimited string input
2
3 #include <stdio.h>
4
5 ssize_t getline(char **lineptr, size_t *n, FILE *stream);
6
7 Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
8
9 getline():
10 Since glibc 2.10:
11 _POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700
12 Before glibc 2.10:
13 _GNU_SOURCE

这个函数是一个非常好用的函数,它能帮助我们一次获取一行数据,而无论这个数据有多长。

参数列表:
  lineptr:一个一级指针的地址,它会将读取到的数据填写到一级指针指向的位置,并将>该位置回填到该参数中。指针初始必须置为NULL,该函数会根据指针是否为 NULL 来决定是否需要分配新的内存。
  n:是由该函数回填的申请的内存缓冲区的总大小,长度初始必须置为0。
虽然很好用,但是各位童鞋别高兴得太早了,该函数仅支持 GNU 标准,所以是方言,大家还是自己封装一个备用吧。

另外,想要使用这个函数必须在编译的时候指定 -D_GNU_SOURCE 参数:

1
1 $> gcc -D_GNU_SOURCE

当然如果不想在编译的时候添加参数,也可以在引用头文件之前 #define _GNU_SOURCE,只是比较丑陋而已。

还有一个办法,是在 makefile 中配置 CFLAGS += -D_GNU_SOURCE,这样即省去了编译时手动写参数的麻烦,也避免了代码中的丑陋。

10.fflush(3)

1
2
#include <stdio.h>
int fflush(FILE *stream);

fflush(3) 函数的作用是刷新缓冲区,提到这个函数就要讲讲缓冲区了。

缓冲区的作用是为了合并系统调用,在上面讲 STDIO 与 SYSIO 的区别时大家已经看到什么是合并系统调用了。

Linux 系统中有三种缓冲形式:无缓冲、行缓冲和全缓冲。

无缓冲:需要立刻输出时使用,例如 stderr;

行缓冲:遇到换行符时进行刷新、缓冲区满了的时候刷新、强制刷新(fflush(3));而标准输出(stdout)是行缓冲,因为涉及到终端设备;

全缓冲:只有缓冲区满了的时候和强制刷新(fflush(3))时才会刷新,这是 Linux 默认的缓冲模式,但终端设备除外,终端设备使用行缓冲模式;

当数据被放入缓冲区的时候是不会通过系统调用(read(3)、write(3))送到内核中的,只有缓冲区被刷新的时候数据才会通过系统调用进入内核。而刷新缓冲区就是 fflush(3) 函数的作用。

fflush(3) 的参数是具体要刷新的流,当参数为 NULL 时会刷新所有的输出流。

例子

文件读

文件的I/O 操作包含在stdio.h中。

文件I/O操作中使用的句柄:

FILE *fp;

这个句柄通常叫文件指针,是一个透明的结构体,且不应该由开发人员直接访问。

读文件的例子,一个简单的more01程序实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/* more01.c - version 0.1 of more
* read and print 24 lines then pause for a few special commands
*/
#include <stdio.h>
#define PAGELEN 24
#define LINELEN 512
void do_more(FILE *);
int see_more();
int main( int ac , char *av[] )
{
FILE *fp;
if ( ac == 1 )
do_more( stdin );
else
while ( --ac )
if ( (fp = fopen( *++av , "r" )) != NULL )
{
do_more( fp ) ;
fclose( fp );
}
else
exit(1);
return 0;
}
void do_more( FILE *fp )
/*
* read PAGELEN lines, then call see_more() for further instructions
*/
{
char line[LINELEN];
int num_of_lines = 0;
int see_more(), reply;
while ( fgets( line, LINELEN, fp ) ){ /* more input */
if ( num_of_lines == PAGELEN ) { /* full screen? */
reply = see_more(); /* y: ask user */
if ( reply == 0 ) /* n: done */
break;
num_of_lines -= reply; /* reset count */
}
if ( fputs( line, stdout ) == EOF ) /* show line */
exit(1); /* or die */
num_of_lines++; /* count it */
}
}
int see_more()
/*
* print message, wait for response, return # of lines to advance
* q means no, space means yes, CR means one line
*/
{
int c;
printf("\033[7m more? \033[m"); /* reverse on a vt100 */
while( (c=getchar()) != EOF ) /* get response */
{
if ( c == 'q' ) /* q -> N */
return 0;
if ( c == ' ' ) /* ' ' => next page */
return PAGELEN; /* how many to show */
if ( c == '\n' ) /* Enter key => 1 line */
return 1;
}
return 0;
}

代码解读:

ac==1时do_more(stdin)是让标准输入流重定向进来的方法!

1
if(fputs(line,stdout)==EOF) //fputs将字符串char line[LINELEN]输出到标准输出中,顺便利用返回值进行检错

more01 如果用linux 管道命令“|”将ls /bin | ./more01输出重定向到more01,结果发现打印24行后没有停下来,这是为什么呢?

因为ls /bin | ./more01已经将more01的标准输入重定向到ls的标准输出了,这样more01从同一个数据流中读了数据,又读了用户输入,显然是有问题的。

解决办法:

从标准输入中读入要分页的数据,直接从键盘读取用户的输入。

具体如何做呢?

有一个文件/dev/tty,这是键盘和显示器的设备文件,向这个文件写相当于显示在用户屏幕上,读相当于从键盘获取用户的输入。即使程序的输入/输出被“<”或“>”重定向,程序还是可以通过这个文件与终端交换数据。

改进的more02:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/* more02.c - version 0.2 of more
* read and print 24 lines then pause for a few special commands
* feature of version 0.2: reads from /dev/tty for commands
*/
#include <stdio.h>
#define PAGELEN 24
#define LINELEN 512
void do_more(FILE *);
int see_more(FILE *);
int main( int ac , char *av[] )
{
FILE *fp;
if ( ac == 1 )
do_more( stdin );
else
while ( --ac )
if ( (fp = fopen( *++av , "r" )) != NULL )
{
do_more( fp ) ;
fclose( fp );
}
else
exit(1);
return 0;
}
void do_more( FILE *fp )
/*
* read PAGELEN lines, then call see_more() for further instructions
*/
{
char line[LINELEN];
int num_of_lines = 0;
int see_more(FILE *), reply;
FILE *fp_tty;
fp_tty = fopen( "/dev/tty", "r" ); /* NEW: cmd stream */
if ( fp_tty == NULL ) /* if open fails */
exit(1); /* no use in running */
while ( fgets( line, LINELEN, fp ) ){ /* more input */
if ( num_of_lines == PAGELEN ) { /* full screen? */
reply = see_more(fp_tty); /* NEW: pass FILE * */
if ( reply == 0 ) /* n: done */
break;
num_of_lines -= reply; /* reset count */
}
if ( fputs( line, stdout ) == EOF ) /* show line */
exit(1); /* or die */
num_of_lines++; /* count it */
}
}
int see_more(FILE *cmd) /* NEW: accepts arg */
/*
* print message, wait for response, return # of lines to advance
* q means no, space means yes, CR means one line
*/
{
int c;
printf("\033[7m more? \033[m"); /* reverse on a vt100 */
while( (c=getc(cmd)) != EOF ) /* NEW: reads from tty */
{
if ( c == 'q' ) /* q -> N */
return 0;
if ( c == ' ' ) /* ' ' => next page */
return PAGELEN; /* how many to show */
if ( c == '\n' ) /* Enter key => 1 line */
return 1;
}
return 0;
}

参考资料:

坚持原创技术分享,您的支持将鼓励我继续创作!