Linux 标准 C 文件 I/O 管理

😊

1 标准 I/O 库

C 标准库中提供了标准 I/O 库(简称 stdio),它实现了跨平台的用户缓冲解决方案。 这个标准 I/O 库使用简单,且功能强大。

理论上,使用 ANSI C 标准编写的程序,是跨平台的,Windows 和 Linux 都遵守这一标准。在 Windows 平台使用 ANSI C 标准编写的代码可以不加修改拿到 Linux 平台下编译和运行。POSIX 标准编写的代码,是类 UNIX 系统遵循的标准,理论上使用这一标准编写的代码可以不加修改,移植到其他类 UNIX 系统上重新编译、运行。

ANSI C 标准函数是在 POSIX 标准函数的上层抽象,大部分 ANSI C 标准的函数都需要调用 POSIX 标准函数进行系统调用进入内核。

ANSI C 标准、POSIX 标准编写的程序区别:

类别 ANSI C 标准 POSIX 标准
是否使用应用层缓冲区
文件操作的接口 文件流(FILE 结构) 文件描述符
是否必须进行系统调用

ANSI C 标准文件管理函数围绕文件流进行操作,而 POSIX 标准文件管理函数围绕文件描述符进行操作。

1.1 文件流

标准 I/O 程序集并不是直接操作文件描述符。相反,它们通过唯一标识符,即文件指针(fle pointer)来操作文件。在 C 标准库里,文件指针和文件描述符一一映射(对应)。文件指针为 FILE 的指针表示,定义在 <stdio.h> 中。

在标准 I/O 中,打开的文件称为“流”(stream)。流可以被打开用来读(输入流)、 写(输出流)或者二者兼有(输人/输出流)。——《Linux系统编程 第二版 3.2》

在 Linux 系统中,系统默认为每个进程开放三个流:标准输入流、标准输出流、标准错误输出流。分别对应三个文件:/dev/stdin/dev/stdout/dev/stderr。这三个标准流都是文件流,文件流需和一个文件进行关联。每个进程默认从标准输入流中读取数据,向标准输出流中输出信息,将错误信息输出到标准错误输出流。这三个流指针及其宏定义如下:

1
2
3
4
5
6
7
8
9
10
/*  /usr/include/stdio.h  */ 
/* 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

对于一个程序,常见的输入流有:键盘、文件、管道(pipe);常见的输出流有:显示器、文件、管道。

文件流:程序对文件的读写通过文件流来完成,即通过文件流指针 FILE 来实现。当使用 ANSI 库函数 fopen 函数打开一个文件后,返回一个文件流指针 FILE ,该指针和打开的文件进行关联,所有针对文件的读写操作都通过该文件流指针来完成

文件流指针是一个 FILE 结构,其成员描述了该文件流的起始/结束地址、使用的缓冲区类型、缓冲区的大小等。如下:

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
/*  /usr/include/stdio.h  */ 
typedef struct _IO_FILE FILE;

/* /usr/include/libio.h */
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
// 高 2 字节为 _IO_MAGIC,低 2 字节为 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; /* 文件描述符 */
...
};

关于文件流 FILE 成员具体解释请参考FILE结构体及漏洞利用方法

如下代码,可以看到,标准 stdinstdoutstderr 都是文件流指针。由于 ANSI C 标准要求这三个标准流都是宏定义,所以后面定义了同名宏。

1
2
3
4
5
6
7
8
9
10
/*  /usr/include/stdio.h  */ 
/* 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

参考资料:

  • 《Linux系统编程 第二版 3.2》
  • 《Unix 环境高级编程 第二版 第五章》
  • 《Linux 高级程序设计 第三版 第四章》
  • 《UNIX系统编程手册(上册)第十三章》

1.2 缓冲区介绍

根据数据的存储方式,可以将文件分为文本文件和二进制文件:

  • 文本文件:ASCII 文件,每个字节存放一个 ASCII 码字符。便于对字符操作,存储量大,速度慢,文件以 EOF 结束。
  • 二进制文件:数据按照其在内存中的格式存储在磁盘文件。速度快,便于存放中间结果,如日志、临时文件(Windows 下 ETW 消费者拿到的内容、内存换页文件 C:\pagefile.sys)。

Linux 系统内核 I/O 操作和标准 C 语言库 I/O 函数(应用层)在操作磁盘文件时都会对文件进行缓冲。前者使用的缓冲区称为内核缓冲区,后者使用的缓冲区称为应用层文件缓冲区

POSIX 标准下的 read/write 函数对文件进行读写时,并不会直接发起磁盘访问。而是在应用层用户空间缓冲区和内核缓冲区高速缓存进行数据复制传输,所以每次 read/write 都会引发系统调用,带来线程在应用层和内核层的切换,消耗性能。使用 read/write 函数后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘。

为了减少每次文件数据读写引发的系统调用,标准 C 语言函数库的 I/O 函数提供了应用层的缓冲区(也叫做 stdio 缓冲区),该缓冲区位于用户态内存区。以下三种情况,stdio 库才会调用 write 进行系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)

  • 缓冲区填满时。
  • 使用 fflush 刷新应用层缓冲区时。
  • 当关闭流时,即调用 fclose 函数。

下图概括了 stdio 函数库和内核所采用的缓冲(针对输出文件),以及对各种缓冲类型的控制机制。从图中自上而下,首先是通过 stdio 库将用户数据传递到 stdio 缓冲区,该缓冲区位于用户态内存区。当缓冲区填满时,stdio 库会调用 write 系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)。最终,内核发起磁盘操作,将数据传递到磁盘。

30.png

1.3 I/O 标准缓冲区类型

本文关注应用层的文件读写缓冲区,即标准 C 语言库使用的缓冲区,内核缓冲区后续学习启动时再分析。

用户缓冲 I/O 实在用户空间而非内核空间完成(在内核中,所有文件操作都是基于块大小来执行),在应用层,对文件的访问可分为带缓冲区的文件操作和非缓冲文件操作。标准 I/O 提供了 3 种类型的缓冲区:全缓冲区、行缓冲区和无缓冲区。

缓冲类型 说明
全缓冲 1、文件读写是基于用户缓冲区完成的。可以为打开的文件申请一块内存作为文件读写缓冲区(标准 C 库函数 sethuf 默认大小为 BUFSIZ,定义在 stdio.h)。
2、一般在对磁盘上的文件读写时,使用全缓冲。
3、当用户缓冲区满、调用 fflush、或者 fclose 关闭文件流时,才会调用到 read/write 将用户缓冲区数据读写到内核缓冲区。
行缓冲 1、数据读写大小单位是一行,其大小在 libio.h 中定义,CentOS 7.x 中为 1024 字节,大小固定。
2、一般用于终端的输入输出。
3、当遇到换行符或者缓冲区满时,才会引发缓冲区的刷新。
无缓冲 1、数据读写不带缓冲区,标准 C 库函数对数据读写时不进行缓冲。比如使用 fopen 函数打开文件流,如果不使用 setbuf/setvbuf 等函数手工指定缓冲区的话,数据读写是不知用用户缓冲区的。
2、标准错误输出流 stderr 是不带缓冲区的,每次都会引发 I/O 操作,导致系统调用。

ISO C要求下列缓冲特征:

  • 当且晋档标准输入输出不涉及交互式设备时,才会使用全缓冲。
  • 标准出错绝对不是全缓冲。

在很多系统中默认:

  • 磁盘文件使用全缓冲
  • 涉及终端设备使用行缓冲
  • 标准错误输出流无缓冲

FILE->_flags 高 2 字节为 magic,低 2 字节才当做 flag 使用,表示 I/O 缓冲类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* /usr/include/libio.h
Magic numbers and bits for the _flags field.
The magic numbers use the high-order bits of _flags;
the remaining bits are available for variable flags.
Note: The magic numbers must all be negative if stdio
emulation is desired. */

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
...

主要关注:

  • #define _IO_UNBUFFERED 2:无缓冲区。
  • #define _IO_LINE_BUF 0x200:使用行缓冲区。
1
2
3
4
5
6
7
8
9
if(stdin->_flags & _IO_UNBUFFERED)
{
// 是无缓冲
}
else if(stdin->_flags & _IO_LINE_BUF)
{
// 是行缓冲
}
else // 全缓冲

如果没有匹配这两中缓冲类型那就是全缓冲(libio.h 中没有定义全缓冲的类型)

1.4 设置缓冲区

打开流后,必须要在使用其他任何 stdio 函数之前调用 setbuf/setvbuf 等函数设置好缓冲区,不同的缓冲区类型将会影响流操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*   /usr/include/stdio.h   */
void setbuf(
FILE* stream, // 文件流指针
char* buffer // 指向一个准备好的的缓冲区,缓冲区大小为 NULL 时表示关闭缓冲区
);
void setlinebuf(FILE* stream);
void setbuffer(FILE* stream, char* buffer, size_t size);

int setvbuf(
FILE* stream, // 文件流指针
char* buffer, // 指向一个准备好的的缓冲区,缓冲区大小为参数 4
int mode, // 缓冲区类型
size_t size // 缓冲区大小
);

关于 setvbuf 函数的参数:

参数 3 mode:指定缓冲区的类型。

1
2
3
4
5
/*  /usr/include/stdio.h  */
/* The possibilities for the third argument to 'setvbuf'. */
#define _IOFBF 0 /* 全缓冲 */
#define _IOLBF 1 /* 行缓冲 */
#define _IONBF 2 /* 无缓冲 */
缓冲类型 说明
_IONBF(2) 1、无缓冲,每个 stdio 库函数将立即调用 write 或者 read 进行系统调用。
2、此时将会忽略 buffersize 两个参数(不管被指定为多少都不会使用)。
3、无缓冲时,缓冲区的大小为 1 byte
_IOLBF(1) 行缓冲,此时必须指定 buffersize 两个参数。
_IOFBF(0) 全缓冲。
1、buffer != NULL,此时 read/write 使用该缓冲区进行数据读写,若 size != 0则会使用指定 size 大小的缓冲区;如果 size == 0无缓冲区(此时缓冲区大小为 1 byte)。
2、buffer == NULL,不管 size 是多少,系统都会默认分配一块大小为 BUFSIZ/2 = 0x1000 ß的缓冲区给该文件流使用。

上表中 _IOFBF(0) 全缓冲下使用的 BUFSIZ 的定义:

1
2
3
4
5
6
7
8
/*  /usr/include/stdio.h  */
#define BUFSIZ _IO_BUFSIZ

/* /usr/include/libio.h */
#define _IO_BUFSIZ _G_BUFSIZ

/* /usr/include/_G_config.h */
#define _G_BUFSIZ 8192 // 0x2000

设置缓冲区的注意事项:

  1. 打开文件得到文件流后,必须在调用任何其他 stdio 函数之前调用 setbuf/setvbuf 等函数设置好缓冲区,不同的缓冲区类型将会影响流操作。
  2. 由于文件读写缓冲区是在整个程序中使用的,在使用自定义指定缓冲区的过程中,应该在堆中为该缓冲区分配一块空间(使用 malloc 等函数),而不应是分配在栈上的函数本地变量。否则,函数返回时将销毁其栈帧,从而导致混乱。而 setvbuf(fp, NULL, _IOFBF, 0) 方式则不关心该问题,系统维护缓冲区。

示例:

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
#include <stdio.h>

int main(int argc, char* argv[], char* envp[])
{
char file_load[] = "/home/alvin/teatmain.c";
FILE* fp = NULL;
char* fbuffer = NULL;

if((fp = fopen(file_load, "a+")) == NULL)
{
printf("[*] Failed open the file.\n");
return 0;
}

if((fbuffer = (char*)malloc(BUFSIZ*2)) == NULL)
{
printf("[*] Failed malloc the buffer.\n");
return 0;
}
memset(fbuffer, 0, BUFSIZ*2);

setvbuf(fp, fbuffer, _IOFBF, BUFSIZ);

printf("fp = %p, *fp = %llx, fp->_flags = %x\n", fp, *fp, fp->_flags);
printf("fp->_IO_read_ptr = %llx, fp->_IO_read_base = %llx, fp->_IO_read_end = %llx\n", \
fp->_IO_read_ptr, fp->_IO_read_base, fp->_IO_read_end);
printf("fp->_IO_write_ptr = %llx, fp->_IO_write_base = %llx, fp->_IO_write_end = %llx\n", \
fp->_IO_write_ptr, fp->_IO_write_base, fp->_IO_write_end);
printf("fp->_IO_buf_base = %llx, fp->_IO_buf_end = %llx\n", fp->_IO_buf_base, fp->_IO_buf_end);
printf("The io buffer size = 0x%llx byte(s)\n", fp->_IO_buf_end - fp->_IO_buf_base);

free(fbuffer);
fbuffer = NULL;
fclose(fp);
return 0;
}

1.5 缓冲区刷新

在章节 1.3 中已经提过以下行为会导致缓冲区的刷新:

  • 全缓冲:当缓冲区写满、调用 fflush 函数、 调用 fclose 函数。
  • 行缓冲:当缓冲区写满、调用 fflush 函数、 调用 fclose 函数、当遇到换行符 \n

以上行为,会导致对应类型的缓冲区进行刷新,所谓的刷新,即表示强制将用户缓冲区(stdio 标准缓冲区)中的数据,通过调用 write 函数传送给内核缓冲区。

关于 fflush 函数:

1
2
/*   /usr/include/stdio.h   */
int fflush(FILE* stream); // 成功则返回 0,否则返回 EOF,并设置 errno

当参数 stream != NULL 时,刷新指定流用户缓冲区,当 stream == NULL,刷新所有流使用的 stdio 缓冲区

举例说明:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdbool.h>

bool open_file(char* filename, FILE** fpp)
{
FILE* fp_temp = NULL;

if((fp_temp = fopen(filename, "a+")) == NULL)
{
perror("[*] Fault");
return false;
}

*fpp = fp_temp;
return fp_temp;
}

bool set_no_buffer(char* buffer, const char* msg1)
{
char file_load[] = "/home/alvin/program/no_buf.txt";
FILE* fp = NULL;

// Open the file
if(!open_file(file_load, &fp))
{
printf("[*] Failed open the file.\n");
return -1;
}

setvbuf(fp, NULL, _IONBF, 0);
fwrite(msg1, 7, 1, fp);

printf("-----------------------\n");
printf("test NO buffer, buffer = %s\n", buffer);
printf("Please open the `no_buf.txt` to check TEXT.\n");
printf("-----------------------\n");

printf("Please enter to continue.\n");
getchar();
fclose(fp);

return true;
}

bool set_line_buffer(char* buffer, const char* msg2, int len)
{
char file_load[] = "/home/alvin/program/line_buf.txt";
FILE* fp = NULL;

// Open the file
if(!open_file(file_load, &fp))
{
printf("[*] Failed open the file.\n");
return -1;
}

setvbuf(fp, buffer, _IOLBF, 128);
fwrite(msg2, len, 1, fp);

printf("-----------------------\n");
printf("test LINE buffer, buffer = %s\n", buffer);
printf("Please open the `line_buf.txt` to check TEXT.\n");
printf("-----------------------\n");

printf("Please enter to continue.\n");
getchar();
fclose(fp);

return true;
}
int main(int argc, char* argv[], char* envp[])
{
char* fbuffer = NULL;
char msg1[] = "Hello world";
char msg2[] = "Hello\nworld";

// malloc the buffer
if((fbuffer = (char*)malloc(BUFSIZ)) == NULL)
{
printf("[*] Failed malloc the buffer.\n");
return 0;
}
memset(fbuffer, 0, BUFSIZ);

// test no buffer
set_no_buffer(fbuffer, msg1);

// test line buffer
set_line_buffer(fbuffer, msg2, sizeof(msg2));

free(fbuffer);
fbuffer = NULL;
return 0;
}

2 文件操作

2.1 fopen/fclose

打开的文件称为,在对文件进行操作之前,需要将它和流联系在一起。对文件的操作实际上是对流的操作。打开文件得到流是通过 fopen 函数来完成的:

1
FILE* fopen(const char* filename, const char* mode);

返回值:执行成功返回文件流指针,执行失败返回 NULL,并有相关的 errno 也会进行设置。

  • 参数一为要打开文件的路径,可以使相对路径或绝对路径。
  • 参数二为打开文件的模式,如下图。参数 mode 必须是一个字符串,而不是字符,所以必须使用双引号

31.png

注意:Windows 下打开二进制文件需要指定 b,但是在类 Unix 系统下不区分普通文件和二进制文件,所以在 Linux 下 b 实际上无意义。Linux 下不会像 Windows 那样区分普通文件和二进制文件,Linux 下把所有文件都当做二进制文件。

关闭流使用 fclose 函数,关闭指定的流之前,会将缓冲区的数据刷新到内核缓冲区,随之写到文件中。

1
2
3
4
/*   /usr/include/stdio.h   */
int fclose(FILE* stream); // 成功则返回 0,否则返回 EOF
int fcloseall(void); // 关闭所有的流,包括标准输入输出流,始终返回 0
// 流被关闭前,都会将缓冲区内容进行刷新

2.2 读写文件流

标准 C 函数库提供了三种对文件流读写的方式:字符、行、块。

  1. 字符读写。每次从缓冲区中读写一个字符,可使用 fgetc/getcfputc/putc 函数。
  2. 行读写。每次从流中读写一行数据(遇到换行符终止,也算一行数据),可使用 fgetsfputs/puts 函数。
  3. 块读写。每次按指定块大小对流数据进行读写,块的大小没有相关限制,可使用 freadfwrite 函数。

一、 字符读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int fgetc(FILE* fp); // 函数
int getc(FILE* fp); // 宏, #define getc(_fp) _IO_getc(_fp)
返回值:
成功:返回读取到字符对应的 ASCII
失败:返回 EOF(-1),表示读取失败或读到文件结束

int fputc(int c, FILE* fp); // 函数
int putc(int c, FILE* fp); // 宏, #define putc(_ch, _fp) _IO_putc(_ch, _fp)
返回值:
成功:返回写入的字符,即参数 1
失败:返回 -1

向标准输入输出流读写(stdin/stdout):
int getchar(void); // 返回值,成功:返回读取到字符对应的 ASCII
int putchar(int c); // 返回值,成功:返回写入的字符,即参数

二、行读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

char *fgets (char *__restrict __s, int __n, FILE *__restrict __stream);
参数说明:
__s:从数据从文件流中,读取到该指针指向的地址
__n:计划读取字符的个数,最后包含 \0。如果指定为n==10,则只会读取到可用的9个字符,最后一个字符是 \0
返回值:
成功:返回 __s 的地址
失败:返回 NULL。(如果达到文件结尾或出错),此时会设置 errno 值,可以使用 perror 函数查看原因。

int fputs (const char *__restrict __s, FILE *__restrict __stream);
参数说明:
__s:从该指针指向的地址的一行数据,写入到文件流中
返回值:
成功:返回非负整数(一般返回1)
失败:返回 -1

向标准输入输出流读写(stdin/stdout):
char* gets(char* buf); // 从标准输入流读取数据到 buf 中
int puts(char* buf); // 将 buf 指向的数据输出到标准输出流 stdout
  • 读取:fgets 函数将字符从 stream 读入 s 所指向的内存单元,直到读取 n-1 字符(最后一个字符是换行符)、换行符或遇到文件结束标志 EOF 为止,并将 s 最后一个空间置为 NULL
  • 写入:fputs 函数将 s 指向的数据(以 \0 结尾字符串)写入文件中,但是不会将 sNULL(空字符)写入到文件。基于这一特点,这两个函数不能用来操作二进制文件,因为二进制文件中包含 0 的可能性很大。

三、块读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

size_t fread (void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __stream);
参数说明:
__ptr:读取的数据存放到该指针指向的内存中
__size:每个块的大小(字节)
__n:读取几个块
__stream:目标文件流
返回值:
成功:成功读取到的块的个数(注意不是读取到的字节数)。成功读取数据大小 = n*size。
失败:返回值小于参数 __n,此时应该使用 ferror、feof 函数查看具体原因。

size_t fwrite (const void *__restrict __ptr, size_t __size, size_t __n, FILE *__restrict __s);
参数说明:
__ptr:将该指针执行的数据,写入到目标文件流 __s 中。
__size:每个块的大小(字节)
__n:写入几个块
返回值:
成功:成功写入的块的个数
失败:返回 -1

关于 size_t 类型:

1
2
3
4
5
6
7
8
// https://elixir.bootlin.com/linux/v3.10.108/source/include/linux/types.h#L54 
// /usr/include/asm-generic/posix_types.h
// https://blog.csdn.net/tcjy1000/article/details/128990382

Most 32 bit architectures use "unsigned int" size_t,
and all 64 bit architectures use "unsigned long" size_t.
Most 32 bit architectures use "int" ssize_t,
and all 64 bit architectures use "long" ssize_t.

2.3 ferror/feof

当使用 fgetc/fread 等函数读取数据失败时,这些函数返回失败信息做的很不友好,并不能直接返回失败的原因是因为文件读取错误还是读到了文件结束符 EOF。所以只能使用 ferrorfeof 函数来判断是出现错误还是达到文件结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

// ferror 函数
int ferror(FILE* fp);
返回值:
0 值:文件流存在错误
0 :不是文件流错误

// feof 函数
int feof(FILE* fp);
返回值:
1:读到文件结束符
0:没有读到文件结束符

// fclear 函数:清除错误标志和文件结束标志
void fclear(FILE* fp); // 无返回值

源码参考 glibc/libio/ferror.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int _IO_ferror (FILE *fp)
{
int result;
CHECK_FILE (fp, EOF);
if (!_IO_need_lock (fp))
return _IO_ferror_unlocked (fp);
_IO_flockfile (fp);
result = _IO_ferror_unlocked (fp);
_IO_funlockfile (fp);
return result;
}

weak_alias (_IO_ferror, ferror)

#ifndef _IO_MTSAFE_IO
#undef ferror_unlocked
weak_alias (_IO_ferror, ferror_unlocked)
#endif

// libio.h
#define _IO_feof_unlocked(__fp) (((__fp)->_flags & _IO_EOF_SEEN) != 0)
#define _IO_ferror_unlocked(__fp) (((__fp)->_flags & _IO_ERR_SEEN) != 0)
  1. 可以看到在 glibc 库的 ferror.c 文件中,会将 _IO_ferror 重命名为 ferror 函数。
  2. _IO_ferror 函数中,重点关注 _IO_ferror_unlocked 函数。
  3. 如果是文件流错误,即文件流 flags 存在 _IO_ERR_SEEN 标志,则会返回 true(1),否则返回 false(0)

关于 feof 同理可以查看 _IO_feof 来查看是否在存在 _IO_EOF_SEEN 标志。

clearerr 也只是用来清除 _IO_ERR_SEEN_IO_EOF_SEEN 标志的。

1
#define _IO_clearerr(FP) ((FP)->_flags &= ~(_IO_ERR_SEEN|_IO_EOF_SEEN))

2.4 文件流定位

在对文件流进行操作时,有一个指针指向流的当前读写位置,如果希望从特殊位置读写, 则需要通过函数修改当前读写位置。

每次读写文件时,都要先考虑当前文件流指针的位置,才能正确读写目标数据。注意这里描述的文件流指针并不是 FILE* 指针

举例:如果使用 fwrite("123", 3, 1, fp) 向目标文件写入 3 个字符(Unicode 编码),此时文件流指针指向位置为 6(以字节为单位)。如果此时使用 fread(buf, 3, 1, fp) 读取刚才写入的数据是读取不到的,就是因为文件流指针的指向位置导致的。

  1. ftell 函数:返回当前文件流指针的位置(以字节为单位)。

    1
    2
    3
    4
    5
    /* /usr/include/stdio.h  */
    extern long int ftell (FILE *__stream) __wur;
    返回值:
    成功:返回当前指针位置距离文件开始的字节数
    失败:返回 -1
  2. fseek 函数:修改当前读写位置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* /usr/include/stdio.h  */
    extern int fseek (FILE *__stream, long int __off, int __whence);
    参数说明:
    __stream:文件流指针
    __off:以参数 __whence 为基准的偏移量
    __whence:要修改的基准位置
    返回值:
    成功:返回 0
    失败:返回 -1

    关于 __whence 基准位置的定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* /usr/include/stdio.h  */
    /* The possibilities for the third argument to `fseek'.
    These values should not be changed. */
    #define SEEK_SET 0 /* Seek from beginning of file. */ 从文件开始位置
    #define SEEK_CUR 1 /* Seek from current position. */ 从当前位置
    #define SEEK_END 2 /* Seek from end of file. */ 从文件结束位置
    #ifdef __USE_GNU
    # define SEEK_DATA 3 /* Seek to next data. */
    # define SEEK_HOLE 4 /* Seek to next hole. */
    #endif
  3. rewind 函数:将读写指针移动到文件开头。等价于 fseek(fp, 0, SEEK_SET)

    1
    2
    /* /usr/include/stdio.h  */
    extern void rewind (FILE *__stream);

如果中间没有 fseekfsetposrewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出(Unix环境高级编程 第二版 p113)。

3 格式化输入输出

因为数据在磁盘存储格式和人最易识别的格式不尽一致,格式化输入/输出,使数据按指定的格式(字符、某类型数据)输入,或者按需求输出人能够识别的数据。

3.1 格式输出

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
__BEGIN_NAMESPACE_STD
/* Write formatted output to STREAM.
This function is a possible cancellation point and therefore not marked with __THROW. */
extern int fprintf (FILE *__restrict __stream, const char *__restrict __format, ...);

/* Write formatted output to stdout.
This function is a possible cancellation point and therefore not marked with __THROW. */
extern int printf (const char *__restrict __format, ...);

/* Write formatted output to S. */
extern int sprintf (char *__restrict __s, const char *__restrict __format, ...) __THROWNL;

/* Write formatted output to S from argument list ARG.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int vfprintf (FILE *__restrict __s, const char *__restrict __format, _G_va_list __arg);

/* Write formatted output to stdout from argument list ARG.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int vprintf (const char *__restrict __format, _G_va_list __arg);

/* Write formatted output to S from argument list ARG. */
extern int vsprintf (char *__restrict __s, const char *__restrict __format, _G_va_list __arg) __THROWNL;
__END_NAMESPACE_STD


#if defined __USE_BSD || defined __USE_ISOC99 || defined __USE_UNIX98
__BEGIN_NAMESPACE_C99
/* Maximum chars of output to write in MAXLEN. */
extern int snprintf (char *__restrict __s, size_t __maxlen,
const char *__restrict __format, ...)
__THROWNL __attribute__ ((__format__ (__printf__, 3, 4)));

extern int vsnprintf (char *__restrict __s, size_t __maxlen,
const char *__restrict __format, _G_va_list __arg)
__THROWNL __attribute__ ((__format__ (__printf__, 3, 0)));
__END_NAMESPACE_C99
#endif
  1. printf 将格式化数据写到标准输出,fprintf 写至指定的流,sprintf 将格式化的字符送入数组 __s 中。sprintf 在该数组的尾端自动加一个 null 字节,但该字节不包括在返回值中。
  2. 注意,sprintf 函数可能会造成由 __s 指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。为了解决这种缓冲区溢出问题,引入了 snprintf 函数。在该函数中,缓冲区长度是一个显式参数,超过缓冲区尾端写的任何字符都会被丢弃。如果缓冲区是够大,snprintf 函数就会返回写入缓冲区的字符数。与 sprintf 相同,该返回值不包括结尾的 null 字节。若 snprintf 函数返回小于缓冲区长度 n 的正值,那么没有截短输出。若发生了一个编码错误, snprintf 则返回负值。
  3. 还有 4 种 printf 族的变体类似于上面的 4 种,但是可变参数表(…〉代换成了 argvprintfvfprintfvsprintfvsnprintf

格式输出函数的格式为:

1
%[flags] [fldwidth] [precision] [lenmodifier] convtype
字段 解释
flags 32.png
fldwidth 宽度。转换的最小字段宽度。如果转换得到的字符较少,则用空格填充它。字段宽度是一个非负十进制数,或是一个星号(*)。
precision 精度。整型转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后的最大字符数。精度是一个句点(.),后接一个可选的非负十进制整数或一个星号(*)。
宽度和精度字段两者皆可为 *
lenmodifier 参数长度,其可能的取值示于表 5-6 中。
33.png
convtype 可选的。它控制如何解释参数。表5-7中列出了各种转换类型。
34.png

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(int argc, char* argv[], char* enpv[])
{
char buffer[128] = {0};
float num_f = 12345.6789;
int num = 12345;

snprintf(buffer, sizeof(buffer), "% 8.3lf", num_f);
printf("%s\n", buffer);

memset(buffer, 0, sizeof(buffer));
snprintf(buffer, sizeof(buffer), "%08d", num);
printf("%s\n", buffer);

getchar();

return 0;
}

// 输出
12345.679
00012345

3.2 格式输入

执行格式化输人处理的是三个 scanf 函数。

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
__BEGIN_NAMESPACE_STD
/* Read formatted input from STREAM.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...) __wur;

/* Read formatted input from stdin.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int scanf (const char *__restrict __format, ...) __wur;

/* Read formatted input from S. */
extern int sscanf (const char *__restrict __s, const char *__restrict __format, ...) __THROW;
__END_NAMESPACE_STD

#ifdef __USE_ISOC99
__BEGIN_NAMESPACE_C99
/* Read formatted input from S into argument list ARG.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int vfscanf (FILE *__restrict __s, const char *__restrict __format, _G_va_list __arg)
__attribute__ ((__format__ (__scanf__, 2, 0))) __wur;

/* Read formatted input from stdin into argument list ARG.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int vscanf (const char *__restrict __format, _G_va_list __arg)
__attribute__ ((__format__ (__scanf__, 1, 0))) __wur;

/* Read formatted input from S into argument list ARG. */
extern int vsscanf (const char *__restrict __s, const char *__restrict __format, _G_va_list __arg)
  1. scanf 族用于分析输人宇符串,并将字符序列转换成指定类型的变量。格式之后的各参数 包含了变量的地址,以用转换结果初始化这些变量。
  2. 格式说明控制如何转换参数,以便对它们赋值。转换说明以 字符开始。除转换说明和空 白字符外,格式字符串中的其他字符必须与输入匹配。若有一个字符不匹配,则停止后续处理, 不再读输入的其余部分。

格式输入函数的格式为:

1
%[*] [fldwidth] [lenmodifier] convtype
字段 解释
* 可选的前导星号(*)用于抑制转换。按照转换说明的其余部分对输入进行转换,但转换结果并不存放在参数中。
fldwidth 最大宽度(即最大字符数)。
lenmodifier 要用转换结果初始化的参数大小。
convtype 类似于 printf 族的转换类型字段,但两者之间还有些差别。表 5-8 列出了 scanf 函数族支持的转换类型。
35.png

scanf 家族比 printf 家族好用的一个地方是,可以使用正则表达式来格式化输入。

举例如下:

1
2
3
4
5
6
7
8
// 取仅包含 0 到 9 和小写字母的字符串
sscanf("123456qwerrtFGY", "%[0-9a-z]", buffer);

// 取遇到大写字母为止的字符串
sscanf("123456qwerrtFGY", "%[^A-Z]", buffer);

// 取长度为 4 字节的字符串
sscanf("123456", "%4s", buffer):