标准 I/O(C 库stdio.h)封装系统调用,核心对象:FILE * 文件指针,工作在用户空间。 系统调用(open/read/write)直接操作内核;标准 I/O 在用户层增加缓冲区,用来减少频繁系统调用,提升 IO 效率。Linux系统下,默认打开了三个文件流,分别为stdin、stdout、stderr,文件流结构体成员如下,文件流fd编号固定为stdin 固定为 0,stdout=1,stderr=2。
struct _IO_FILE {
int _flags; /* 状态标志位 */
char* _IO_read_ptr; /* 当前读缓冲区指针 */
char* _IO_read_end; /* 读缓冲区有效数据末尾 */
char* _IO_read_base; /* 读缓冲区起始地址 */
char* _IO_write_base; /* 写缓冲区起始 */
char* _IO_write_ptr; /* 当前写位置 */
char* _IO_write_end; /* 写缓冲区上限 */
char* _IO_buf_base; /* 整个缓冲区起始 */
char* _IO_buf_end; /* 整个缓冲区末尾 */
char *_IO_save_base; /* ungetc回退字符缓冲区 */
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain; /* 进程内所有FILE链表 */
int _fileno; // ★核心:关联的文件描述符!stdin->_fileno == 0
int _flags2;
_IO_off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1]; // 内置短小缓冲区(无malloc时先用它)
_IO_lock_t *_lock; // 线程锁,fopen默认带锁
#if _IO_OLD_IO_FILE
_IO_off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeresult;
int _freeresult_flags;
void *unused2;
#endif
};
typedef struct _IO_FILE FILE;
缓冲区:
1. 行缓冲(Line Buffered)
✅ 默认对象:stdout(终端屏幕输出) ✅ 缓冲区大小:1024 字节(1k) ✅ 刷新条件(满足任意一条就把缓冲数据写入内核)
- 输出遇到换行符
\n立即刷新 - 缓冲区写满 (1024B) 自动刷新
- 程序正常结束自动刷新
- 主动调用
fflush(stdout);强制刷新
典型例子:printf("hello"); 不加\n,内容会留在缓冲区,屏幕暂时看不到。
2. 全缓冲(Fully Buffered)
✅ 默认对象:普通磁盘文件(fopen 打开的文件) ✅ 缓冲区大小:4096 字节(4k) ✅ 刷新条件
- 缓冲区写满 (4096B) 自动刷新
- 程序正常结束自动刷新
- 主动调用
fflush(fp);强制刷新
只有终端设备默认行缓冲;普通文件默认全缓冲。
3. 无缓冲(Unbuffered)
✅ 默认对象:stderr(标准错误) ✅ 缓冲区:0,不缓存任何数据 ✅ 特性:调用输出函数直接发送到内核 ,没有缓存。 用途:错误信息优先实时打印,不会被缓冲区滞留。 示例:fprintf(stderr, "打开文件失败!\n");
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
返回值为读取了多少个字节, @ptr为读取数据的缓冲区 @size为每个数据的数据宽度
@nmemb为期望读取多少个数据 @stream为目标数据流,从该处读取数据
size_t fwrite(const void *ptr, size_t size, size_t nmemb,
FILE *stream);
返回值为写入了多少个字节, @ptr为写入数据的缓冲区 @size为写入的每个数据的数据宽度
@nmemb为期望下入多少个数据 @stream为目标数据流,从ptr处读取数据写入到该文件中
On success, fread() and fwrite() return the number of
items read or written. This number equals the number
of bytes transferred only when size is 1. If an error
occurs, or the end of the file
is reached, the return value is a short item count (or zero).
成功时,fread() 和 fwrite() 返回读取或写入的项目数量。
当 size 为 1 时,该数量等于传输的字节数。如果发生错误或到达文
件末尾,返回值为一个短整型的项目计数(或零)。
练习:输入学生类型数据并打印
-
定义学生结构体
stu_t,初始化包含 3 条学生数据的结构体数组 s。 -
以
w+模式打开 stu.txt,打开失败打印错误信息并退出。 -
调用
fwrite将 3 个结构体二进制原始数据写入文件。 -
fflush刷新缓冲区,fseek把文件读写指针重置到文件起始位置。 -
定义结构体数组 s1,使用
fread从文件读取 3 个结构体存入 s1。 -
输出 fread 返回值(成功读到的结构体个数,正常为 3)。
-
循环遍历 s1 数组,打印所有读取到的学生姓名、学号、分数。
-
关闭文件,程序结束。
#include <stdio.h>
// 定义学生结构体类型
typedef struct stu
{
char name[20]; // 姓名
int sno; // 学号
float score; // 分数
}stu_t;int main(void)
{
// 定义结构体数组,存放3名学生信息
stu_t s[3] = {
{"Tom",110,99.5},
{"Jerry",111,89.8},
{"XiaoMing",112,100}
};// 读写模式打开stu.txt,w+:文件不存在创建,存在则清空 FILE *fp = fopen("stu.txt","w+"); if (fp == NULL) { perror("fopen fail"); return -1; } // 将3个结构体数据以二进制形式写入文件 // 参数:数据首地址、单个块大小、块数量、文件流 int ret = fwrite(s,sizeof(stu_t),3,fp); fflush(fp); // 刷新缓冲区,确保数据写入磁盘 fseek(fp,0,SEEK_SET); // 文件指针偏移到文件开头 stu_t s1[3]; // 从文件读取3个结构体数据到s1数组 ret = fread(s1,sizeof(stu_t),3,fp); perror("fread fail"); printf("ret = %d\n",ret); // 打印实际读到的结构体块数量 int i = 0; // 循环打印读取出来的学生信息 for (i = 0; i < 3; ++i) { printf("name : %s\n",s1[i].name); printf("sno : %d\n",s1[i].sno); printf("score : %.2f\n",s1[i].score); } fclose(fp); return 0;}
其中用到了fflush和fseek函数,需要注意的是,我们在写入操作的时候,由于使用的是标准IO函数,是带有缓冲区的,写入数据未满,也未有换行符,数据并不会立刻写入文件,因此需要使用fflush强制刷新该流的缓冲区,才能确保数据写入,使用fseek是因为在写入过程中,文件流指针进行了偏移写入的操作,如果此时直接进行操作读取,会造成从末尾开始读,读不到任何数据,因此需要调用fseek修改文件流指针偏移量。
int fflush(FILE *stream);
返回值为整型,输入参数为文件流的文件指针
如果给入参数为NULL则会刷新所有已经打开的流的缓冲区
成功刷新时返回值为0,失败返回EOF为-1,并设置error为错误值
int fseek(FILE *stream, long offset, int whence);
新的位置以字节为单位,是通过将 offset 字节加到 whence 指定的位置上得到的。
如果 whence 被设置为 SEEK_SET、SEEK_CUR 或 SEEK_END,
则偏移量分别相对于文件开头、当前位置指针或文件末尾
long ftell(FILE *stream);
返回当前位置距离原始位置偏移量是多少以字节为单位
void rewind(FILE *stream);
rewind() 函数将指向 stream 指向的流的文件位置指针设置为文件开头。
它等同于:(void) fseek(stream, 0L, SEEK_SET)
成功完成后,fgetpos()、fseek() 和 fsetpos() 返回 0,
ftell() 返回当前偏移量。否则,返回 -1 并将 errno 设置为错误码以指示错误。
练习:使用fseek创建空洞文件,模拟下载抢占磁盘空间
-
校验命令行参数,参数数量不对打印使用提示并退出。
-
以只读模式打开传入的源文件,打开失败直接退出。
-
使用
fseek跳到源文件末尾,ftell获取源文件总大小。 -
创建
downloading.txt,通过fseek将指针偏移至文件长度倒数第 1 字节,执行fputc写入一字节,创建空洞文件预先抢占磁盘空间。 -
刷新缓冲区,随后将源文件、目标文件读写指针都重置到文件头部。
-
循环调用
fread读取源文件内容,使用fwrite写入目标文件;每次写入后刷新缓冲区。 -
fread返回 0 代表源文件读取完成,跳出循环。 -
关闭两个文件流,程序结束。
#include<stdio.h>
int main(int argc, const char *argv[])
{
if(argc != 2)
{
printf("Usage\n");
return -1;
}
//打开预下载文件
FILE *fp_s = fopen(argv[1],"r");if(fp_s == NULL) { printf("fopen fali\n"); return -1; } //获取预下载文件的大小 fseek(fp_s,0,SEEK_END); long len = ftell(fp_s); //创建下载到本地的文件的文件名并打开文件流 FILE *fp_target = fopen("downloading.txt","w+"); //抢占存储空间,偏移文件指针 fseek(fp_target,len-1,SEEK_SET); //需要在便宜之后进行一次写入操作,才能成功创建空洞文件 fputc('\0',fp_target); fflush(fp_target); char buf[1024] = {}; //将需要读取的文件和需要写入的文件的偏移量都清零 fseek(fp_s,0,SEEK_SET); fseek(fp_target,0,SEEK_SET); //循环读取并向目标文件写入 while(1) { //读取需要下载的文件存入缓冲区 int ret = fread(buf,sizeof(char),sizeof(buf),fp_s); //写入数据到本地文件流中 fwrite(buf,sizeof(char),ret,fp_target); fflush(fp_target); //如果返回值为0则代表已经读取完成整个文件 if(ret == 0) break; } fclose(fp_s); fclose(fp_target); return 0;}
练习:使用fread和fwrite、fseek实现cp命令的效果
-
校验命令行参数,参数不等于 3 打印使用提示并退出。
-
分别以只读、只写模式打开源文件与目标文件,任一打开失败打印错误信息退出。
-
fseek 将源文件指针移至末尾,ftell 获取源文件字节大小 len 并打印。
-
将目标文件指针移动到 len-1 位置,写入一个空字符,生成稀疏空洞文件预先占用空间;调用 fflush 刷新缓冲区。
-
rewind 将源文件、目标文件读写指针重置到文件起始位置。
-
使用变长数组 buf len 开辟缓冲区,一次性读取整个源文件内容,再一次性写入目标文件。
-
关闭两个文件流,程序结束。
#include <stdio.h>
#include <fcntl.h>// 运行方式:./a.out 1.txt 2.txt
int main(int argc, const char *argv[])
{
// 判断命令行参数,必须传入源文件和目标文件
if (argc != 3)
{
printf("Usage: %s\n",argv[0]);
return -1;
}// 只读打开源文件,只写创建目标文件 FILE *fp_s = fopen(argv[1],"r"); FILE *fp_d = fopen(argv[2],"w"); // 任意一个文件打开失败则退出 if (fp_s == NULL || fp_d == NULL) { perror("fopen fail"); return -1; } // 1.移动源文件指针到末尾 fseek(fp_s,0,SEEK_END); // 获取源文件总字节长度 long len = ftell(fp_s); printf("len = %ld\n",len); // 2.构造空洞文件,预先抢占磁盘空间 fseek(fp_d,len-1,SEEK_SET); // 目标文件指针偏移到倒数第一个字节 fputc('\0',fp_d); // 末尾写入1字节,确定文件逻辑长度 fflush(fp_d); // 刷新缓冲区,确保文件大小生效 rewind(fp_s); // 源文件指针回到文件开头 rewind(fp_d); // 目标文件指针回到文件开头 char buf[len]; // VLA变长数组,根据文件大小开辟缓冲区 fread(buf,sizeof(buf),1,fp_s); // 一次性读取整个源文件到buf fwrite(buf,sizeof(buf),1,fp_d); // 一次性把全部数据写入目标文件 fclose(fp_s); fclose(fp_d); return 0;}
缓存区空间buf本代码是开在系统栈上的,仅适用于小型文件拷贝,若文件过大,会导致栈溢出,因此在拷贝大文件时,需要将buf空间malloc在堆上更为合适,并在拷贝完成后释放内存。
Linux 系统调用 IO
核心:open read write close lseek 属于内核提供系统调用,操作对象:文件描述符 int fd
区分: ✅ 系统 IO:系统调用,无用户缓冲区 ✅ 标准 IO (fopen/fread):C 库,封装系统 IO,自带用户层缓冲区
1. open () 打开 / 创建文件
flag为访问模式,必须选择O_RDONLY / O_WRONLY / O_RDWR中的一个
含义为只读、只写、读写三种权限。除必选的flag以外其他:
O_EXCL
一般搭配 O_CREAT:O_CREAT | O_EXCL 作用:如果文件已存在,open 直接失败,返回 -1。 用途:原子创建文件,防止多进程竞争创建,实现简易互斥锁。 ⚠️ 只对普通文件有效,不能用于管道、设备。
O_TRUNC
打开文件时,将文件长度截断为 0,清空原有内容。 前提:文件必须可写(O_WRONLY / O_RDWR)。 典型用法:覆盖写文件
O_APPEND
追加模式。 每次 write 写入前,内核自动把文件偏移置到文件末尾。
O_NONBLOCK(等价 O_NDELAY)
以非阻塞方式打开文件。 适用对象:管道、FIFO、socket、字符设备(串口、终端)。 普通磁盘文件设置 O_NONBLOCK 无效。
O_SYNC
写操作完成前,数据 + 文件元数据(大小、修改时间)全部同步刷到磁盘。 write 返回时磁盘上已经持久化,速度很慢。
O_DSYNC
只同步文件数据内容,不需要同步元数据(如修改时间)。 性能比 O_SYNC 略好。
O_RSYNC
配合读操作:等待所有挂起的写操作完成后再执行 read。Linux 实现较弱,极少使用。
多个flag操作符可以用或的符号"|"进行合并操作
int open(const char *path, int flags);
若包含了O_CREAT则代表,会在传入的path参数不存在时创建文件
新文件的权限根据mode参数进行配置
mode 最终权限 = mode & ~umask(umask为八进制的002)
int open(const char *path, int flags, mode_t mode);
2. read --- 读取文件
ssize_t read(int fd, void *buf, size_t count);
返回值一般为读取到的字节数
fd为读取的文件标识符
buf为存放读取数据的缓冲区
count表示期望调用一次函数读取的字节数
3. write --- 写入文件
ssize_t write(int fd, const void *buf, size_t count);
返回值 fd:open 返回的文件描述符
≥0:实际写入字节数 buf:要写入数据缓冲区
-1:出错 count:期望写入字节数
4. close --- 关闭文件描述符
int close(int fd);
成功返回 0,失败 -1
练习:使用系统调用函数实现cat命令功能
-
校验命令行参数,参数数量不为 2,打印使用提示并退出。
-
使用系统调用 open 以只读模式打开指定文件,得到文件描述符 fd;打开失败打印错误信息并退出。
-
定义缓冲区 buf,进入循环。
-
调用 read 读取文件内容,ret 为实际读到的字节数;ret 等于 0 代表到达文件末尾,跳出循环。
-
调用 write 将本次读到的数据输出到标准输出(终端)。
-
循环结束,调用 close 关闭文件描述符,程序结束。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, const char *argv[])
{
//校验命令行参数,需要传入待读取文件名
if (argc != 2)
{
printf("Usage: %s\n",argv[0]);
return -1;
}//系统调用open,只读方式打开文件,返回文件描述符fd int fd = open(argv[1],O_RDONLY); if (fd < 0) //打开失败返回-1 { perror("open fail"); return -1; } char buf[1024]; //读写缓冲区 int ret = 0; while (1) { //从fd对应的文件读取数据到buf,最多读取1024字节 ret = read(fd,buf,sizeof(buf)); if (ret == 0) //读到文件末尾 break; //向标准输出(文件描述符1)写入读到的数据 write(1,buf,ret); } close(fd); //关闭文件描述符,释放资源 return 0;}
任何进程运行起来,内核预先分配 3 个文件描述符:
- fd = 0:标准输入 stdin
- fd = 1:标准输出 stdout
- fd = 2:标准错误 stderr
但是若此时,手动使用close关闭stdin或者stdou的进程,再次调用open的时候,会根据最小原则,分配编号。