day19 C 语言标准 IO 机制
题目要求与程序实现
编写一个C程序,持续向文件 test.txt
中追加写入带序号和时间戳的日志行,每秒一行。程序需支持:
- 追加写入,不覆盖原有内容
- 序号自动接续上次最后的编号
- 按
Ctrl+C
可中断程序 - 再次运行时能正确读取已有行数并继续编号
完整代码实现(含注释)
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/**
* 函数:get_line
* 功能:统计文件当前总行数
* 参数:FILE *fp - 文件指针
* 返回值:文件行数
* 说明:使用 fgets 逐行读取,通过判断每行末尾是否为换行符 '\n' 来计数
*/
int get_line(FILE *fp)
{
char buf[1024];
int cnt = 0;
// 重置文件位置到开头
fseek(fp, 0, SEEK_SET);
while (fgets(buf, sizeof(buf), fp) != NULL)
{
// 判断读取的字符串是否以换行符结尾(说明是一整行)
if (buf[strlen(buf) - 1] == '\n')
cnt++;
}
return cnt;
}
/**
* 函数:do_log
* 功能:循环写入日志数据
* 参数:FILE *fp - 文件指针
* 说明:
* 1. 先调用 get_line 获取当前文件行数,作为起始序号
* 2. 每隔1秒写入一行 "序号, YYYY-MM-DD HH:MM:SS"
* 3. 使用 time() 和 localtime() 获取本地时间
* 4. 调用 fflush 强制刷新缓冲区,确保数据立即写入磁盘
*/
void do_log(FILE *fp)
{
// 1. 统计当前文件已有行数,决定起始序号
int line = get_line(fp);
printf("lines = %d\n", line);
// 2. 开始无限循环写入日志
time_t t;
while (1)
{
time(&t); // 获取当前时间(UTC秒数)
struct tm *ptm = localtime(&t); // 转换为本地时间结构体
// 格式化输出到标准输出和文件
fprintf(stdout, "%d,%04d-%02d-%02d %02d:%02d:%02d\n",
line,
ptm->tm_year + 1900, // 年份需加上1900
ptm->tm_mon + 1, // 月份从0开始,需+1
ptm->tm_mday, // 日期
ptm->tm_hour, // 小时
ptm->tm_min, // 分钟
ptm->tm_sec); // 秒
fprintf(fp, "%d,%04d-%02d-%02d %02d:%02d:%02d\n",
line,
ptm->tm_year + 1900,
ptm->tm_mon + 1,
ptm->tm_mday,
ptm->tm_hour,
ptm->tm_min,
ptm->tm_sec);
fflush(fp); // 强制刷新文件缓冲区,确保写入磁盘
line++; // 序号递增
sleep(1); // 程序休眠1秒
}
}
/**
* 主函数
* 功能:解析命令行参数,打开文件,启动日志写入
* 参数:argc - 参数个数,argv - 参数数组
* 运行方式:./a.out test.txt
*/
int main(int argc, const char *argv[])
{
if (argc != 2)
{
printf("Usage: %s <filename>\n", argv[0]);
return -1;
}
// 以 "a+" 模式打开文件:可读可写,追加写入,文件不存在则创建
FILE *fp = fopen(argv[1], "a+");
if (fp == NULL)
{
perror("fopen fail");
return -1;
}
do_log(fp); // 启动日志写入循环
fclose(fp); // 关闭文件(实际不会执行到此处,因循环无限)
return 0;
}
理想运行结果示例
首次运行程序后,test.txt
内容如下:
1,2007-07-30 15:16:42
2,2007-07-30 15:16:43
3,2007-07-30 15:16:44
中断后再次运行,内容追加为:
1,2007-07-30 15:16:42
2,2007-07-30 15:16:43
3,2007-07-30 15:16:44
4,2007-07-30 15:19:02
5,2007-07-30 15:19:03
6,2007-07-30 15:19:04
提示:程序会持续输出类似内容,直到用户按下
Ctrl+C
中断。
标准IO机制深入解析
用户空间与内核空间交互模型
[用户空间]
printf("hello world!\n");
| //缓存 --> ["hello world!\n"] //
|
---------[系统调用]---------------------------------
[内核空间]
|
----->[屏幕]
----------------------------------------------------
标准IO基本概念
- 标准输入(stdin):默认设备为键盘
/dev/input
- 标准输出(stdout):默认设备为显示器
- 标准错误(stderr):用于输出错误信息
在Linux中,所有IO操作本质上都是对文件的操作。标准IO是对底层文件IO的封装,具有良好的可移植性。
标准IO的优势与原理
标准IO库由Dennis Ritchie于1975年编写,基于Mike Lesk的可移植IO库改进而来。50年来基本未变,稳定可靠。
标准IO处理的关键细节:
- 自动管理缓冲区分配
- 优化读写块长度
- 封装系统调用,内部使用文件描述符
✅ 优点:使用方便,无需手动优化块大小
⚠️ 注意:不同系统实现可能存在差异,不能完全保证跨平台兼容性
缓冲机制分类
类型 | 缓冲大小 | 典型用途 | 刷新条件 |
---|---|---|---|
行缓冲 | 1KB | 终端交互(stdout) | 换行 \n 、缓冲区满、程序结束、fflush |
全缓冲 | 4KB | 普通文件读写 | 缓冲区满、程序结束、fflush |
无缓冲 | 0KB | 错误输出(stderr) | 直接输出,不缓存 |
设计原则:
- 与终端关联 → 行缓冲
- 普通文件 → 全缓冲
- 错误处理 → 无缓冲
缓冲区信息查看代码
#include <stdio.h>
int main(void)
{
// 输出标准流的文件描述符
printf("stdin fileno = %d\n", stdin->_fileno);
printf("stdout fileno = %d\n", stdout->_fileno);
printf("stderr fileno = %d\n", stderr->_fileno);
getchar(); // 等待用户输入
// 输出各标准流缓冲区大小
printf("stdin buffer size = %ld\n", stdin->_IO_buf_end - stdin->_IO_buf_base);
printf("stdout buffer size = %ld\n", stdout->_IO_buf_end - stdout->_IO_buf_base);
printf("stderr buffer size = %ld\n", stderr->_IO_buf_end - stderr->_IO_buf_base);
return 0;
}
理想输出示例:
stdin fileno = 0
stdout fileno = 1
stderr fileno = 2
stdin buffer size = 1024
stdout buffer size = 1024
stderr buffer size = 0
文件定位函数
fseek
— 定位文件指针
int fseek(FILE *stream, long offset, int whence);
- 功能:将文件指针定位到指定位置
- 参数:
stream
:文件指针offset
:偏移量(可正可负)whence
:参考点(SEEK_SET
,SEEK_CUR
,SEEK_END
)
- 返回值:成功返回0,失败返回-1并设置
errno
示例:
fseek(fp, 0, SEEK_SET);
→ 定位到文件开头fseek(fp, 0, SEEK_END);
→ 定位到文件末尾fseek(fp, 100, SEEK_SET);
→ 从头偏移100字节
注意:允许偏移超出文件范围,形成“空洞”,但需一次写操作才能真正扩展文件。
创建空洞文件示例
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("hole.txt", "w");
if (fp == NULL)
{
perror("fopen fail");
return -1;
}
int n = 0;
scanf("%d", &n);
fseek(fp, n - 1, SEEK_SET); // 定位到目标位置前一个字节
fputc('\0', fp); // 写入一个空字符,创建空洞
fclose(fp);
return 0;
}
效果:创建大小为
n
字节的空洞文件,中间填充\0
模拟云盘下载:复制文件并创建空洞
#include <stdio.h>
int main(int argc, const char* argv[])
{
if (argc != 3)
{
printf("Usage: %s <src> <dest>\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;
}
// 获取源文件大小
fseek(fp_s, 0, SEEK_END);
long len = ftell(fp_s);
printf("len = %ld\n", len);
// 创建同大小空洞文件
fseek(fp_d, len - 1, SEEK_SET);
fputc('\0', fp_d);
fflush(fp_d);
// 重置指针,开始复制
rewind(fp_s);
rewind(fp_d);
int ret = 0;
while ((ret = fgetc(fp_s)) != EOF)
{
fputc(ret, fp_d);
}
fclose(fp_s);
fclose(fp_d);
return 0;
}
理想效果:成功复制文件,目标文件大小与源一致,内容完全相同。
标准IO vs 文件IO(系统调用)
对比项 | 标准IO(库函数) | 文件IO(系统调用) |
---|---|---|
操作对象 | FILE * 流指针 |
文件描述符 int fd |
打开函数 | fopen() |
open() |
读写函数 | fgetc/fgets/fread 等 |
read()/write() |
关闭函数 | fclose() |
close() |
定位函数 | fseek()/ftell()/rewind() |
lseek() |
缓冲机制 | 有(行/全/无缓冲) | 无(直接系统调用) |
可移植性 | 高 | 依赖系统 |
性能 | 高效(减少系统调用) | 低效(每次调用都进内核) |
关系:标准IO库函数最终调用系统调用来实现功能。
open
系统调用详解
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- flags:
- 必选:
O_RDONLY
,O_WRONLY
,O_RDWR
- 可选:
O_APPEND
,O_CREAT
,O_TRUNC
- 必选:
- mode:创建文件权限(如
0666
),仅在含O_CREAT
时需要 - 返回值:成功返回文件描述符(>=3),失败返回-1
等价关系:
fopen("1.txt","r")
→open("1.txt", O_RDONLY)
fopen("1.txt","w")
→open("1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666)
文件权限与 umask
实际文件权限 = mode & ~umask
例如:
mode: 0666 (rw-rw-rw-)
umask: 0022 (----w--w-)
~umask:111101101
结果:0644 (rw-r--r--)
read
与 write
系统调用
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
- read:从文件描述符读取数据到缓冲区
- write:将缓冲区数据写入文件描述符
注意:
read
读取的数据若用于字符串处理,需手动添加\0
结尾。
使用 read/write
实现 cat
功能
#include<stdio.h>
#include<unistd.h>
int main(void)
{
char buf[1024];
int ret = read(0, buf, sizeof(buf)); // 从stdin读
write(1, buf, ret); // 写入stdout
return 0;
}
运行示例:
$ ./a.out
hello
ret = 6 buf = hello
lseek
定位文件偏移
off_t lseek(int fd, off_t offset, int whence);
- 功能:移动文件读写位置
- 常用操作:
lseek(fd, 0, SEEK_SET);
→ 定位开头lseek(fd, 0, SEEK_END);
→ 定位末尾off_t len = lseek(fd, 0, SEEK_END);
→ 获取文件大小
文件描述符与流指针转换
int fileno(FILE *stream);
→ 将FILE*
转为fd
FILE *fdopen(int fd, const char *mode);
→ 将fd
转为FILE*
int fd = open("1.txt", O_WRONLY);
FILE *fp = fdopen(fd, "w"); // 关联FILE*指针
BMP图像合并示例(作业)
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
FILE *fp1 = fopen("1.bmp","r");
FILE *fp2 = fopen("2.bmp","r");
FILE *fp3 = fopen("3.bmp","w");
if(fp1==NULL || fp2==NULL || fp3==NULL)
{
printf("No open bmp!\n");
return -1;
}
char head[54];
fread(head,1,54,fp1); // 读取BMP头
fwrite(head,1,54,fp3); // 写入新文件头
fseek(fp2,54,SEEK_SET); // 跳过第二张图头
for(int i=0; i<600; ++i)
{
char row1[2400], row2[2400], row3[2400];
fread(row1,1,2400,fp1);
fread(row2,1,2400,fp2);
for(int j=0; j<800; ++j)
{
int idx = j*3;
char r1=row1[idx], g1=row1[idx+1], b1=row1[idx+2];
char r2=row2[idx], g2=row2[idx+1], b2=row2[idx+2];
if(r1==r2 && g1==g2 && b1==b2)
{
row3[idx] = r1; row3[idx+1] = g1; row3[idx+2] = b1;
}
else
{
row3[idx] = (r1+r2)/2;
row3[idx+1] = (g1+g2)/2;
row3[idx+2] = (b1+b2)/2;
}
}
fwrite(row3,1,2400,fp3);
}
fclose(fp1); fclose(fp2); fclose(fp3);
printf("Output file:3.bmp\n");
return 0;
}
功能:合并两张600×800的BMP图片,相同像素保留,不同则取平均值。
:这是 标准IO(C语言标准输入输出库)在 Linux 系统中的工作原理示意图,涉及以下实体和概念:
- 标准IO组件:stdin(标准输入)、stdout(标准输出)、stderr(标准错误输出 ),通过FILE *fp 指针操作,对应结构体 struct IO_FILE,包含缓存、文件描述符(fileNo ,关联内核文件操作)。
- 空间划分:分用户空间、内核空间,标准IO在用户空间,通过“系统调用”和内核交互。
- 内核文件管理结构:
- 文件描述符表:用 fd(如 0、1、2 分别对应标准输入、输出、错误)索引,关联文件操作。
- 文件表项:存文件状态标志、当前偏移量、v - node 指针、引用计数等,管理文件读写状态。
- vnode 节点:含 v - node、i - node 节点信息、文件长度等,最终关联磁盘文件,是用户操作和实际存储的桥梁 。
整体展示了标准IO从用户调用到内核操作文件(磁盘)的流程,体现缓存、系统调用封装、多结构协作管理文件IO的机制。