参考引用
为什么需要标准 I/O 库?直接使用文件 I/O 系统调用不是更好吗?
- 设计库函数是为了提供比底层系统调用更为方便、好用的调用接口,虽然标准 I/O 构建于文件 I/O 之上,但标准 I/O 却有它自己的优势
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO1 /* Standard output. */
#define STDERR_FILENO2 /* Standard error output. */
// struct _IO_FILE 结构体就是 FILE 结构体,使用 typedef 进行了重命名
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. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
#include <stdio.h>
// path:参数 path 指向文件路径,可以是绝对路径或相对路径
// mode:参数 mode 指定了对该文件的读写权限,是一个字符串
// 返回值:调用成功返回一个指向 FILE 类型对象的指针(FILE *)
FILE *fopen(const char *path, const char *mode);
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)
#include <stdio.h>
// 参数 stream 为 FILE 类型指针,调用成功返回 0;失败将返回 EOF(也就是-1)
int fclose(FILE *stream);
#include <stdio.h>
/*
ptr:fread() 将读取到的数据存放在参数 ptr 指向的缓冲区中
size:fread() 从文件读取 nmemb 个数据项,每一个数据项大小为 size 个字节,所以总共读取 nmemb * size 个字节数据
nmemb:参数 nmemb 指定了读取数据项的个数
stream:FILE 指针
*/
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
/*
ptr:将参数 ptr 指向的缓冲区中的数据写入到文件中
size:参数 size 指定了每个数据项的字节大小,与 fread() 函数的 size 参数意义相同
nmemb:参数 nmemb 指定了写入的数据项个数,与 fread() 函数的 nmemb 参数意义相同
stream:FILE 指针
*/
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buff[] = "Hello World!\n";
FILE *fp = NULL;
if ((fp = fopen("./test_file", "w")) == NULL) {
perror("fopen error");
exit(-1);
}
printf("open success\n");
if (fwrite(buff, 1, sizeof(buff), fp) < sizeof(buff)) {
printf("fwrite error\n");
fclose(fp);
exit(-1);
}
printf("write success\n");
fclose(fp);
exit(0);
}
$ gcc fw.c -o fw
$ ./fw
open success
write success
$ cat test_file
Hello World!
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buf[50] = {0};
FILE *fp = NULL;
int size;
/* 只读方式打开文件 */
if ((fp = fopen("./test_file", "r")) == NULL) {
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 读取 12 * 1=12 个字节的数据 */
if ((size = fread(buf, 1, 12, fp)) < 12) {
if (ferror(fp)) { //使用 ferror 判断是否是发生错误
printf("fread error\n");
fclose(fp);
exit(-1);
}
/* 如果未发生错误则意味着已经到达了文件末尾 */
}
printf("成功读取%d 个字节数据: %s\n", size, buf);
/* 关闭文件 */
fclose(fp);
exit(0);
}
$ gcc fr.c -o fr
$ ./fr
文件打开成功!
成功读取12 个字节数据: Hello World!
$ cat test_file
Hello World!
#include <stdio.h>
// stream:FILE 指针
// offset:与 lseek() 函数的 offset 参数意义相同
// whence:与 lseek() 函数的 whence 参数意义相同
int fseek(FILE *stream, long offset, int whence);
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = NULL;
char rd_buf[100] = {0};
char wr_buf[] = "www.baidu.com\n";
int ret;
/* 打开文件 */
if ((fp = fopen("./test_file", "w+")) == NULL) {
perror("fopen error");
exit(-1);
}
printf("open seccess\n");
/* 写文件 */
if (fwrite(wr_buf, 1, sizeof(wr_buf), fp) < sizeof(wr_buf)) {
printf("fwrite error\n");
fclose(fp);
exit(-1);
}
printf("write success\n");
/* 将读写位置移动到文件头部 */
if (fseek(fp, 0, SEEK_SET) < 0) {
perror("fseek error\n");
fclose(fp);
exit(-1);
}
/* 读文件 */
if ((ret = fread(rd_buf, 1, sizeof(wr_buf), fp)) < sizeof(wr_buf)) {
printf("fread error\n");
fclose(fp);
exit(-1);
}
printf("成功读取 %d 个字节数据: %s\n", ret, rd_buf);
/* 关闭文件 */
fclose(fp);
exit(0);
}
$ gcc fseek.c -o fseek
$ ./fseek
open seccess
write success
成功读取 14 个字节数据: www.baidu.com
#include <stdio.h>
// 参数 stream 指向对应的文件,函数调用成功将返回当前读写位置偏移量;调用失败将返回 -1
long ftell(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = NULL;
int ret;
/* 打开文件 */
if ((fp = fopen("./testApp.c", "r")) == NULL) {
perror("fopen error");
exit(-1);
}
printf("open success\n");
/* 将读写位置移动到文件末尾 */
if (fseek(fp, 0, SEEK_END) < 0>) {
perror("fseek error");
fclose(fp);
exit(-1);
}
/* 获取当前位置偏移量,也就得到了 testApp.c 整个文件的大小 */
if ((ret = ftell(fp)) < 0) {
perror("ftell error");
fclose(fp);
exit(-1);
}
printf("file size: %d\n", ret);
/* 关闭文件 */
fclose(fp);
exit(0);
}
$ gcc ftell.c -o ftell
$ ./ftell
open success
file size: 13
#include <stdio.h>
int feof(FILE *stream);
// 当文件的读写位置移动到了文件末尾时,end-of-file 标志将会被设置
if (feof(file)) {
/* 到达文件末尾 */
}
else {
/* 未到达文件末尾 */
}
#include <stdio.h>
int ferror(FILE *stream);
// 当对文件的 I/O 操作发生错误时,错误标志将会被设置
if (ferror(file)) {
/* 发生错误 */
}
else {
/* 未发生错误 */
}
#include <stdio.h>
void clearerr(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = NULL;
char buf[20] = {0};
/* 打开文件 */
if ((fp = fopen("./testApp.c", "r")) == NULL) {
perror("fopen error");
exit(-1);
}
printf("文件打开成功!\n");
/* 将读写位置移动到文件末尾 */
if (fseek(fp, 0, SEEK_END) < 0) {
perror("fseek error");
fclose(fp);
exit(-1);
}
/* 读文件 */
if (fread(buf, 1, 10, fp) < 10) {
if (feof(fp))
printf("end-of-file 标志被设置, 已到文件末尾!\n");
clearerr(fp); // 清除标志
}
/* 关闭文件 */
fclose(fp);
exit(0);
}
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *buf, const char *format, ...);
int snprintf(char *buf, size_t size, const char *format, ...);
printf("Hello World!\n");
printf("%d\n", 5);
fprintf(stderr, "Hello World!\n");
fprintf(stderr, "%d\n", 5);
dprintf(STDERR_FILENO, "Hello World!\n");
dprintf(STDERR_FILENO, "%d\n", 5);
char buf[100];
sprintf(buf, "Hello World!\n");
// 一般会使用这个函数进行格式化转换,并将转换后的字符串存放在缓冲区中
// 如:将数字 100 转换为字符串 "100",将转换后得到的字符串存放在 buf 中
// sprintf() 函数会在字符串尾端自动加上一个字符串终止字符 '\0'
char buf[20] = {0};
sprintf(buf, "%d", 100);
sprintf() 函数可能会造成由参数 buf 指定的缓冲区溢出,因为缓冲区溢出会造成程序不稳定甚至安全隐患,为解决这个问题,引入 snprintf() 函数,在该函数中,使用参数 size 显式的指定缓冲区的大小
- 如果写入到缓冲区的字节数大于参数 size 指定的大小,超出的部分将会被丢弃
- 如果缓冲区空间足够大,snprintf() 函数就会返回写入到缓冲区的字符数
printf("转换说明 1 转换说明 2 转换说明 3", arg1, arg2, arg3);
/*
flags:标志,用于规定输出样式,可包含 0 个或多个标志
width:输出最小宽度,表示转换后输出字符串的最小宽度
precision:精度,前面有一个点号 "."
length:长度修饰符
type:转换类型,指定待转换数据的类型
*/
%[flags][width][.precision][length]type
printf("%hd\n", 12345); // 将数据以 short int 类型进行转换
printf("%ld\n", 12345); // 将数据以 long int 类型进行转换
printf("%lld\n", 12345); // 将数据以 long long int 类型进行转换
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
int a, b, c;
fscanf(stdin, "%d %d %d", &a, &b, &c);
char *str = "5454 hello";
char buf[10];
int a;
sscanf(str, "%d %s", &a, buf);
/*
width:最大字符宽度
length:长度修饰符,与格式化输出函数的 format 相同
type:指定输入数据的类型
*/
%[*][width][length]type
%[m][width][length]type
char *buf;
scanf("%ms", &buf);
......
free(buf);
scanf("%4s", buf); // 匹配字符串,字符串长度不超过 4 个字符
// 用户输入 abcdefg,按回车,那么只能将 adcd 作为一个字符串存储在 buf 数组中
scanf("%hd", var); // 匹配 short int 类型数据
scanf("%hhd", var); // 匹配 signed char 类型数据
scanf("%ld", var); // 匹配 long int 类型数据
scanf("%f", var); // 匹配 float 类型数据
scanf("%lf", var); // 匹配 double 类型数据
scanf("%Lf", var); // 匹配 long double 类型数据
write(fd, "Hello", 5); // 写入 5 个字节数据
对于读文件而言亦是如此,内核会从磁盘设备中读取文件数据并存储到内核缓冲区中,当调用 read() 读取数据时,read() 调用将从内核缓冲区中读取数据,直至把缓冲区中的数据读完,这时,内核会将文件的下一段内容读入到内核缓冲区中进行缓存,把这个内核缓冲区就称为文件 I/O 的内核缓冲
文件 I/O 的内核缓冲区自然是越大越好,内核会分配尽可能多的内核来作为文件 I/O 的内核缓冲区,但受限于物理内存的总量,操作越大的文件也要依赖于更大空间的内核缓冲区
fsync() 函数
#include <unistd.h>
int fsync(int fd);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 4096
#define READ_FILE "./rfile"
#define WRITE_FILE "./wfile"
static char buf[BUF_SIZE];
int main(void) {
int rfd, wfd;
size_t size;
/* 打开源文件 */
rfd = open(READ_FILE, O_RDONLY);
if (rfd < 0) {
perror("open error");
exit(-1);
}
/* 打开目标文件 */
wfd = open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (wfd < 0) {
perror("open error");
exit(-1);
}
/* 拷贝数据 */
while((size = read(rfd, buf, BUF_SIZE)) > 0)
write(wfd, buf, size);
/* 对目标文件执行 fsync 同步 */
fsync(wfd);
/* 关闭文件退出程序 */
close(rfd);
close(wfd);
exit(0);
}
fdatasync() 函数
#include <unistd.h>
int fdatasync(int fd);
sync() 函数
#include <unistd.h>
void sync(void);
fd = open(filepath, O_WRONLY | O_SYNC);
在程序中频繁调用 fsync()、fdatasync()、sync()(或者调用 open 时指定 O_DSYNC 或 O_SYNC 标志)对性能的影响极大,大部分的应用程序是没有这种需求的
Linux 允许应用程序在执行文件 I/O 操作时绕过内核缓冲区,从用户空间直接将数据传递到文件或磁盘设备,把这种操作也称为直接 I/O(direct I/O)或裸 I/O(raw I/O)
fd = open(filepath, O_WRONLY | O_DIRECT);
因为直接 I/O 涉及到对磁盘设备的直接访问,所以在执行直接 I/O 时,必须要遵守三个对齐限制要求
以上所说的块大小指的是磁盘设备的物理块大小,常见的块大小包括 512 字节、1024 字节、2048 以及 4096 字节,如何确定磁盘分区的块大小?通常,Ubuntu 系统的根文件系统挂载在 /dev/sda1 磁盘分区下
$ sudo tune2fs -l /dev/sda1 | grep "Block size"
Block size: 4096
调用 setvbuf() 库函数可以对文件的 stdio 缓冲区进行设置,如:缓冲区的缓冲模式、缓冲区的大小、起始地址等
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
stream
buf
mode:用于指定缓冲区的缓冲类型
size:指定缓冲区的大小
返回值:成功返回 0,失败将返回一个非 0 值,并设置 errno 指示错误原因
当 stdio 缓冲区中的数据被刷入到内核缓冲区或被读取之后,这些数据就不会存在于缓冲区中了,数据被刷入了内核缓冲区或被读走了
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
// 要么将 buf 设置为 NULL 以表示无缓冲
// 要么指向由调用者分配的 BUFSIZ 个字节大小的缓冲区(BUFSIZ 定义于头文件 <stdio.h> 中,该值通常为 8192)
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Hello World!\n");
printf("Hello World!");
for ( ; ; )
sleep(1);
}
$ gcc io2.c -o io2
$ ./io2
Hello World! # 只有第一个 printf() 打印的信息显示出来了,第二个并没有显示出来
这就是 stdio 缓冲的问题,标准输出默认采用的是行缓冲模式,printf() 输出的字符串写入到了标准输出的 stdio 缓冲区中,只有输出换行符时(不考虑缓冲区填满的情况)才会将这一行数据刷入到内核缓冲区,也就是写入标准输出文件(终端设备)
- 第一个 printf 包含了换行符,所以已经刷入了内核缓冲区
- 第二个 printf 没有包含换行符,所以输出的 “Hello World!” 还缓存在 stdio 缓冲区中,需要等待一个换行符才可输出到终端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
/* 将标准输出设置为无缓冲模式 */
if (setvbuf(stdout, NULL, _IONBF, 0)) {
perror("setvbuf error");
exit(0);
}
printf("Hello World!\n");
printf("Hello World!");
for ( ; ; )
sleep(1);
}
$ gcc io3.c -o io3
$ ./io3
Hello World!
Hello World!
#include <stdio.h>
// 参数 stream 指定需要进行强制刷新的文件,如果该参数设置为 NULL,则表示刷新所有的 stdio 缓冲区
int fflush(FILE *stream);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Hello World!\n");
printf("Hello World!");
fflush(stdout); // 刷新标准输出 stdio 缓冲区
for ( ; ; )
sleep(1);
}
$ gcc io4.c -o io4
$ ./io4
Hello World!
Hello World!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Hello World!\n");
printf("Hello World!");
fclose(stdout); // 关闭标准输出
for ( ; ; )
sleep(1);
}
上面的测试程序中,在最后都使用了一个 for 死循环,让程序处于休眠状态无法退出,为什么要这样做呢?原因在于程序退出时也会自动刷新 stdio 缓冲区,这样的话就会影响到测试结果,下面去掉 for 死循环,让程序结束
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("Hello World!\n");
printf("Hello World!");
}
$ gcc io5.c -o io5
$ ./io5
Hello World!
Hello World! $
如果使用 exit()、return 或像上述示例代码一样不显式调用相关函数或执行 return 语句来结束程序,这些情况下程序终止时会自动刷新 stdio 缓冲区,但是如果使用 _exit 或 _Exit() 终止程序则不会自动刷新 stdio 缓冲区
#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
当混合使用文件 I/O 和标准 I/O 时,需要特别注意缓冲的问题
- 文件 I/O 会直接将数据写入到内核缓冲区进行高速缓存
- 标准 I/O 则会将数据写入到 stdio 缓冲区,之后再调用 write() 将 stdio 缓冲区中的数据写入到内核缓冲区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("print"); // 缺换行符 "\n"
write(STDOUT_FILENO, "write\n", 6);
exit(0);
}
$ gcc test.c -o test
$ ./test
write # 先输出了 "write" 字符串信息,接着再输出了 "print" 字符串信息
print $