C语言I/O
ANSI & POSIX
1. 定义与制定机构
-
ANSI:
- 全称:American National Standards Institute(美国国家标准协会)。
- 职责:制定广泛的国家标准,涵盖工业、技术、安全等多个领域,如编程语言(ANSI C/C++)、字符编码(如ANSI转义码)、文件格式等。
- 示例标准:ANSI C(C89/C90)、ANSI C++。
-
POSIX:
- 全称:Portable Operating System Interface(可移植操作系统接口)。
- 制定机构:由IEEE(电气电子工程师协会)制定,后成为国际标准(ISO/IEC 9945)。
- 目标:统一Unix-like操作系统的API,确保应用程序在不同系统(如Linux、macOS)间的可移植性。
2. 核心区别
维度 | ANSI | POSIX |
---|---|---|
适用范围 | 编程语言、工业标准等广泛领域。 | 操作系统接口和工具(如文件操作、进程管理)。 |
核心内容 | 定义语言语法、基础库(如printf )。 | 扩展系统级API(如fork() , pthread )。 |
依赖关系 | 独立标准,如ANSI C是编程基础。 | 依赖ANSI C实现接口,扩展系统功能。 |
应用场景 | 确保代码在编译器间的兼容性。 | 确保程序在Unix系统间的可移植性。 |
3. 功能示例
-
ANSI C:
- 标准库:
stdio.h
(fopen
,printf
)、stdlib.h
(malloc
)。 - 不涉及操作系统特性,如多线程或进程控制。
- 标准库:
-
POSIX:
- 系统调用:
open()
,read()
,fork()
,exec()
。 - 多线程支持:
pthread
库(pthread_create
)。 - Shell工具规范:
grep
,awk
的行为标准化。
- 系统调用:
4. 实际应用中的关联
- 互补性:
POSIX基于ANSI C实现系统接口。例如:// ANSI C的标准文件操作 FILE *file = fopen("test.txt", "r"); // POSIX的低级文件操作(依赖ANSI C但扩展功能) int fd = open("test.txt", O_RDONLY);
文件IO
与标准IO
的异同
1. 核心定义
-
文件I/O(低级I/O)
- 直接调用操作系统提供的原生接口(如POSIX的
open()
、read()
、write()
)。 - 无缓冲或用户自定义缓冲,每次操作直接与内核交互。
- 适用于需要精细控制底层行为的场景(如设备驱动、实时系统)。
- 直接调用操作系统提供的原生接口(如POSIX的
-
标准I/O(高级I/O)
- 基于ANSI C标准库(如
fopen()
、fread()
、fprintf()
)。 - 自带缓冲区管理(全缓冲/行缓冲/无缓冲),减少系统调用次数。
- 提供格式化输入输出、跨平台兼容性等高级功能。
- 基于ANSI C标准库(如
2. 主要差异
维度 | 文件I/O(系统级) | 标准I/O(标准库) |
---|---|---|
接口层级 | 底层系统调用(如POSIX API) | 高层库函数(封装系统调用) |
缓冲机制 | 无缓冲(或需用户手动管理) | 自动缓冲(默认全缓冲,终端设备行缓冲) |
性能 | 频繁系统调用,效率较低 | 缓冲减少系统调用,效率更高 |
可移植性 | 依赖操作系统(如Windows无原生POSIX支持) | 跨平台(ANSI C标准) |
功能扩展 | 支持更底层操作(如O_DIRECT 绕过页缓存) | 提供格式化输入输出(printf )、类型安全 |
错误处理 | 通过errno 和返回值判断错误 | 通过返回值(如NULL 、EOF )和ferror() |
3. 典型场景对比
(1) 文件操作示例
// 文件I/O(POSIX)
int fd = open("data.txt", O_RDWR | O_CREAT, 0644);
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
write(fd, "Hello", 5);
close(fd);
// 标准I/O(ANSI C)
FILE *fp = fopen("data.txt", "w+");
char buf[1024];
fread(buf, 1, sizeof(buf), fp);
fprintf(fp, "Hello");
fclose(fp);
(2) 缓冲机制差异
-
标准I/O:
- 写入数据时,默认先存入内存缓冲区,满后调用
write()
批量写入。 - 可通过
fflush()
强制刷新缓冲区。 - 风险:程序崩溃时,未刷新的数据可能丢失。
- 写入数据时,默认先存入内存缓冲区,满后调用
-
文件I/O:
- 每次
write()
直接触发系统调用,数据立即写入内核(不保证落盘,需fsync()
)。 - 适合实时性要求高的场景(如日志记录)。
- 每次
(3) 格式化输出能力
- 标准I/O支持复杂格式化:
fprintf(fp, "Value: %d, String: %s", 42, "text"); // 直接处理类型转换
- 文件I/O需手动格式化:
char str[100]; sprintf(str, "Value: %d", 42); // 先格式化到内存 write(fd, str, strlen(str));
4. 共同点
- 目标一致:均用于数据的输入输出操作。
- 底层依赖:标准I/O最终通过文件I/O实现(如
fwrite()
内部调用write()
)。 - 文件描述符关联:可通过
fileno(fp)
获取标准I/O的底层文件描述符。
标准IO
1. 文件流指针的本质
- 在 C 语言中,文件流指针(通常称为
FILE
指针)是操作文件的核心工具,它通过标准库<stdio.h>
提供了一系列函数来实现文件的读写和管理。以下是关键概念和使用方法的详细说明: FILE
结构体:文件流指针是一个指向FILE
结构体的指针,该结构体封装了文件的元数据(如文件位置、缓冲区状态等)。- 不透明性:
FILE
的内部结构是平台相关的,开发者无需直接操作其成员,应通过标准库函数间接管理。
2. 文件操作流程
(1) 打开文件:fopen
FILE *fopen(const char *filename, const char *mode);
- 功能:打开文件并返回关联的
FILE
指针。 - 模式:
"r"
:只读(文件必须存在)。"w"
:只写(创建新文件或清空已有文件)。"a"
:追加写(文件不存在则创建)。"r+"
:读写(文件必须存在)。"w+"
:读写(创建新文件或清空已有文件)。"b"
:二进制模式(如"rb"
,"wb+"
),需在模式字符串中添加。
(2) 关闭文件:fclose
int fclose(FILE *stream);
- 功能:关闭文件流,释放资源。
- 返回值:成功返回
0
,失败返回EOF
。
3. 读写操作
(1) 文本读写
int fprintf(FILE *stream, const char *format, ...); // 格式化写入
int fscanf(FILE *stream, const char *format, ...); // 格式化读取
char *fgets(char *str, int n, FILE *stream); // 读取一行文本
int fputs(const char *str, FILE *stream); // 写入字符串
(2) 二进制读写
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
- 参数:
ptr
:数据缓冲区。size
:单个元素的大小(如sizeof(int)
)。count
:元素数量。stream
:文件指针。
- 返回值:实际读/写的元素数量。
4. 文件定位
int fseek(FILE *stream, long offset, int origin); // 移动文件指针
long ftell(FILE *stream); // 获取当前指针位置
void rewind(FILE *stream); // 重置指针到文件开头
- 参数说明
stream
:指向FILE
结构体的指针(文件流)。offset
:偏移量(字节数),可以是正数(向后移动)或负数(向前移动)。origin
:基准位置,决定从何处开始计算偏移。取以下值之一:SEEK_SET
:文件开头。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
典型应用场景
(1) 随机访问文件内容
// 读取文件的第 10 个字节
fseek(fp, 9, SEEK_SET); // 索引从 0 开始
char c = fgetc(fp);
(2) 跳过固定长度的文件头
// 跳过文件头部 1024 字节
fseek(fp, 1024, SEEK_SET);
(3) 动态定位读写位置
// 在文件中追加数据后回到原位置
long pos = ftell(fp); // 记录当前位置
fseek(fp, 0, SEEK_END); // 移动到文件末尾
fputs("Appended data", fp);
fseek(fp, pos, SEEK_SET); // 回到原位置
大文件限制
offset
的类型为long
,在 32 位系统中可能无法处理超过 2GB 的文件。- 解决方案:
- 使用
fseeko
(POSIX 标准,offset
类型为off_t
)。 - 使用
_fseeki64
(Windows 特有,支持 64 位偏移)。
- 使用
错误处理
- 始终检查
fseek
的返回值:if (fseek(fp, offset, origin) != 0) { perror("fseek failed"); exit(EXIT_FAILURE); }
与 ftell
配合使用
ftell
返回当前指针位置(相对于文件开头的字节偏移量):long pos = ftell(fp); // 获取当前位置 fseek(fp, pos, SEEK_SET); // 回到该位置
4.1 fgetpos
和 fsetpos
对于更复杂的文件位置操作(尤其是跨平台或超大文件),建议使用 fgetpos
和 fsetpos
:
int fgetpos(FILE *stream, fpos_t *pos); // 获取位置
int fsetpos(FILE *stream, const fpos_t *pos); // 设置位置
- 优势:
fpos_t
类型可存储任意文件位置(包括多字节编码文件的位置)。- 适用于非二进制模式下的随机访问。
- 示例:
fpos_t pos; if (fgetpos(fp, &pos) != 0) { perror("fgetpos failed"); return; } if (fsetpos(fp, &pos) != 0) { perror("fsetpos failed"); return; }
5. 错误处理
- 检查文件是否成功打开:
FILE *fp = fopen("file.txt", "r"); if (fp == NULL) { perror("Error opening file"); exit(EXIT_FAILURE); }
- 检查读写错误:
if (ferror(fp)) { printf("Read/Write error occurred.\n"); }
- 清除错误标志:
clearerr(fp);
6. 缓冲与同步
- 手动刷新缓冲区:
fflush(fp); // 将缓冲区数据立即写入文件
- 缓冲模式设置:
setvbuf(fp, buffer, _IOFBF, 1024); // 全缓冲 setvbuf(fp, buffer, _IOLBF, 1024); // 行缓冲 setvbuf(fp, buffer, _IONBF, 1024); // 无缓冲
7. 注意事项
- 资源泄漏:务必使用
fclose
关闭文件,避免文件句柄泄漏。 - 二进制模式:在 Windows 中,文本模式会自动转换换行符(
\r\n
↔\n
),处理非文本文件时应使用二进制模式。 - 跨平台问题:路径分隔符在 Windows 中是
\
,在 Linux/macOS 中是/
,建议使用/
或\\
(转义)。 - 文件权限:确保程序对目标文件有读写权限。
文件IO
1. 打开与关闭文件
(1) open
- 打开文件
int open(const char *pathname, int flags, mode_t mode);
- 功能:打开或创建文件,返回文件描述符(
int
类型)。 - 参数:
-
pathname
:文件路径。 -
flags
:打开模式(必选O_RDONLY
,O_WRONLY
,O_RDWR
之一,可选O_CREAT
,O_APPEND
,O_TRUNC
等)。 -
-
mode
:文件权限(mode & ~umask)(仅在O_CREAT
时有效,如0644
)。
-
- 示例:
int fd = open("data.txt", O_RDWR | O_CREAT, 0644); if (fd == -1) { perror("open failed"); exit(EXIT_FAILURE); }
(2) close
- 关闭文件
int close(int fd);
- 功能:关闭文件描述符,释放资源。
- 返回值:成功返回
0
,失败返回-1
。
2. 读写操作
(1) read
- 读取数据
ssize_t read(int fd, void *buf, size_t count);
- 功能:从文件描述符
fd
读取count
字节到buf
。 - 返回值:
- 成功:返回实际读取的字节数(可能小于
count
)。 - 失败:返回
-1
,设置errno
。 - 文件末尾:返回
0
。
- 成功:返回实际读取的字节数(可能小于
(2) write
- 写入数据
ssize_t write(int fd, const void *buf, size_t count);
- 功能:将
buf
中的count
字节写入文件描述符fd
。 - 返回值:实际写入的字节数(可能小于
count
)。
示例:
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read failed");
}
char *data = "Hello, POSIX I/O";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write failed");
}
3. 文件定位
lseek
- 移动文件指针
off_t lseek(int fd, off_t offset, int whence);
- 功能:设置文件偏移量(类似
fseek
)。 - 参数:
whence
:基准位置(SEEK_SET
,SEEK_CUR
,SEEK_END
)。
- 返回值:新的文件偏移量(失败返回
-1
)。 - 示例:
off_t pos = lseek(fd, 0, SEEK_END); // 获取文件大小
4. 文件控制
fcntl
- 控制文件属性
int fcntl(int fd, int cmd, ... /* arg */);
- 功能:修改文件描述符属性(如获取/设置文件状态标志、文件锁等)。
- 常用命令:
F_GETFL
:获取文件状态标志。F_SETFL
:设置文件状态标志(如O_NONBLOCK
非阻塞模式)。
- 示例:
int flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 设置为非阻塞模式
5. 文件同步
(1) fsync
- 强制同步到磁盘
int fsync(int fd);
- 功能:将文件数据及元数据(如修改时间)刷入磁盘。
- 适用场景:确保数据持久化(如数据库操作)。
(2) fdatasync
- 仅同步数据
int fdatasync(int fd);
- 功能:仅同步数据(不强制更新元数据),性能优于
fsync
。
6. 文件内存映射
mmap
- 内存映射文件
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 功能:将文件映射到内存,直接通过指针访问文件内容。
- 优势:避免频繁的
read/write
调用,适合大文件随机访问。 - 示例:
int fd = open("data.bin", O_RDONLY); void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); // 映射后可以立即关闭文件描述符 // 使用 mapped 指针访问文件内容... munmap(mapped, file_size); // 解除映射
7. 原子操作
1. pread
- 原子性读取
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
- 参数:
fd
:文件描述符。buf
:存储读取数据的缓冲区。count
:要读取的字节数。offset
:读取的起始偏移量(相对于文件开头)。
- 返回值:
- 成功:返回实际读取的字节数。
- 失败:返回
-1
,并设置errno
。 - 文件末尾:返回
0
。
2. pwrite
- 原子性写入
#include <unistd.h>
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
- 参数:同上,但
buf
为待写入数据的缓冲区。 - 返回值:
- 成功:返回实际写入的字节数。
- 失败:返回
-1
,并设置errno
。
3.核心特性
1. 原子性操作
pread
和pwrite
的读写操作是原子的,即:- 不会与其他线程/进程的
pread
/pwrite
操作发生偏移量冲突。 - 不受文件指针位置影响,操作完成后文件指针仍保持原位置。
- 不会与其他线程/进程的
- 对比普通
read
/write
:// 非原子操作:需要 lseek + read/write lseek(fd, offset, SEEK_SET); read(fd, buf, count); // 其他线程可能在此间修改文件指针
2. 无状态性
- 操作完全依赖传入的
offset
,与文件描述符的当前偏移量无关。 - 适用于需要多次随机访问的场景,无需频繁调用
lseek
。
3. 线程安全
- 多线程环境下,无需加锁即可安全使用(前提是操作不重叠)。
4.使用示例
1. 读取文件的特定块
int fd = open("data.bin", O_RDONLY);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
char buffer[1024];
ssize_t bytes_read = pread(fd, buffer, sizeof(buffer), 4096); // 从第4096字节处读取1KB
if (bytes_read == -1) {
perror("pread failed");
}
close(fd);
2. 并发写入不同偏移量
// 线程1:写入文件头部
void *thread1(void *arg) {
int fd = *(int *)arg;
const char *data = "HEADER";
pwrite(fd, data, strlen(data), 0); // 写入偏移量0处
return NULL;
}
// 线程2:写入文件尾部
void *thread2(void *arg) {
int fd = *(int *)arg;
const char *data = "FOOTER";
off_t end = lseek(fd, 0, SEEK_END); // 获取文件末尾偏移量
pwrite(fd, data, strlen(data), end); // 写入末尾
return NULL;
}
4. 适用场景
1. 多线程/多进程并发访问
- 多个线程/进程同时读写文件的不同区域,无需锁保护文件指针。
2. 随机访问大文件
- 如数据库索引、日志文件解析等,直接跳转到指定位置读写。
3. 避免竞争条件
- 确保读写操作的偏移量精确性,防止其他操作修改文件指针。
8. 文件锁
flock
或 fcntl
- 文件锁定
// 使用 fcntl 实现文件锁
struct flock lock = {
.l_type = F_WRLCK, // 写锁
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0, // 锁定到文件末尾
};
fcntl(fd, F_SETLK, &lock); // 非阻塞加锁
fcntl(fd, F_SETLKW, &lock); // 阻塞加锁
9.注意事项
- 错误处理:所有 POSIX 函数在失败时返回
-1
,需检查errno
(通过perror
或strerror
)。 - 文件描述符限制:进程可打开的文件描述符数量有限(通过
ulimit -n
查看)。 - 大文件支持:使用
lseek64
和O_LARGEFILE
标志处理超过 2GB 的文件。 - 原子性:
O_APPEND
模式保证多进程追加写的原子性。