五、文件描述符与FILE指针的关系
5.1 转换关系
文件描述符与FILE指针可以相互转换:
#include// 从文件描述符创建FILE指针FILE *fdopen(int fd, constchar *mode);// 从FILE指针获取文件描述符intfileno(FILE *stream);
**示例代码**:
#include #include #include int main() { // 使用系统调用打开文件 int fd = open("test.txt", O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("open failed"); return 1; } // 将文件描述符转换为FILE指针 FILE *fp = fdopen(fd, "w"); if (fp == NULL) { perror("fdopen failed"); close(fd); return 1; } fprintf(fp, "Hello, fdopen!"); // 获取文件描述符 int fd2 = fileno(fp); printf("文件描述符: %d", fd2); fclose(fp); // 关闭FILE指针,也会关闭文件描述符 return 0;}
六、缓冲机制
6.1 缓冲类型
标准IO库提供三种缓冲模式:
6.2 缓冲控制函数
#include// 设置缓冲模式intsetvbuf(FILE *stream, char *buf, int mode, size_t size);// 刷新缓冲区intfflush(FILE *stream);
**示例代码**:
#include int main() { // 设置标准输出为无缓冲 setvbuf(stdout, NULL, _IONBF, 0); printf("这是无缓冲输出"); // 刷新缓冲区 fflush(stdout); return 0;}
七、文件定位
7.1 lseek函数
**运行环境**:Linux系统
**实操目标**:移动文件读写位置
**函数原型**:
#include#includeoff_tlseek(int fd, off_t offset, int whence);
**参数说明**:
- `fd`:文件描述符
- `offset`:偏移量
- `whence`:基准位置
- `SEEK_SET`:文件开头
- `SEEK_CUR`:当前位置
- `SEEK_END`:文件末尾
**返回值**:
- 成功:返回新的文件位置
- 失败:返回-1
**示例代码**:
#include #include #include int main() { int fd = open("test.txt", O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("open failed"); return 1; } // 写入数据 write(fd, "Hello", 5); // 移动到文件开头 lseek(fd, 0, SEEK_SET); // 读取数据 char buf[10]; read(fd, buf, 5); buf[5] = ''; printf("读取: %s", buf); // 获取文件大小 off_t size = lseek(fd, 0, SEEK_END); printf("文件大小: %ld 字节", size); close(fd); return 0;}
**关键参数讲解**:
- `lseek`可以超出文件末尾,形成"文件空洞"
- 文件空洞不占用磁盘空间,但会计入文件大小
**高频故障提示**:
- 在只读模式下使用lseek可能导致错误
- 某些设备(如终端)不支持lseek
7.2 fseek函数
**函数原型**:
#includeintfseek(FILE *stream, long offset, int whence);
**参数说明**:
- `stream`:FILE指针
- `offset`:偏移量
- `whence`:基准位置
- `SEEK_SET`:文件开头
- `SEEK_CUR`:当前位置
- `SEEK_END`:文件末尾
**示例代码**:
#include int main() { FILE *fp = fopen("test.txt", "r+"); if (fp == NULL) { perror("fopen failed"); return 1; } // 移动到文件开头 fseek(fp, 0, SEEK_SET); // 获取当前位置 long pos = ftell(fp); printf("当前位置: %ld", pos); // 移动到文件末尾 fseek(fp, 0, SEEK_END); // 获取文件大小 pos = ftell(fp); printf("文件大小: %ld 字节", pos); fclose(fp); return 0;}
八、IO重定向
8.1 dup和dup2函数
**函数原型**:
#includeintdup(int oldfd);intdup2(int oldfd, int newfd);
**功能**:
- `dup`:复制文件描述符,返回新的最小可用文件描述符
- `dup2`:复制文件描述符到指定的文件描述符
**示例代码**:
#include #include #include int main() { // 打开文件 int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open failed"); return 1; } // 重定向标准输出到文件 dup2(fd, STDOUT_FILENO); // 关闭原文件描述符 close(fd); // 这里的printf会输出到文件 printf("这行文字会被写入文件"); return 0;}
**关键参数讲解**:
- `dup2`会先关闭newfd(如果newfd已打开)
- 重定向后,原文件描述符和新文件描述符指向同一个文件表项
**高频故障提示**:
- 忘记关闭原文件描述符会导致资源泄漏
- 重定向标准错误会影响错误信息的输出
九、阻塞与非阻塞IO
9.1 阻塞IO
默认情况下,文件描述符是阻塞的。对于某些操作(如读取终端、网络数据),如果数据不可用,进程会阻塞等待。
**示例代码**:
#include #include #include int main() { // 默认是阻塞模式 char buf[10]; ssize_t bytes_read = read(STDIN_FILENO, buf, 10); if (bytes_read > 0) { printf("读取了 %zd 字节", bytes_read); } return 0;}
9.2 非阻塞IO
通过设置文件描述符为非阻塞模式,IO操作会立即返回,如果数据不可用,返回错误并设置errno为EAGAIN或EWOULDBLOCK。
**示例代码**:
#include #include #include #include int main() { // 设置标准输入为非阻塞模式 int flags = fcntl(STDIN_FILENO, F_GETFL); fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); char buf[10]; ssize_t bytes_read = read(STDIN_FILENO, buf, 10); if (bytes_read < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("没有数据可读"); } else { perror("read failed"); } } else { printf("读取了 %zd 字节", bytes_read); } return 0;}
**关键参数讲解**:
- `fcntl`:文件控制函数,用于设置文件描述符属性
- `F_GETFL`:获取文件状态标志
- `F_SETFL`:设置文件状态标志
**高频故障提示**:
- 非阻塞IO需要循环检查或使用select/poll/epoll
- 忘记处理EAGAIN错误会导致逻辑错误
十、实战:实现文件复制程序
10.1 使用系统调用实现
**运行环境**:Linux系统,GCC编译器
**实操目标**:实现文件复制功能
**前置准备**:安装gcc编译器
# 安装gccsudo apt-get updatesudo apt-get install -y gcc
**示例代码**:
#include #include #include #include #define BUFFER_SIZE 4096int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "用法: %s <源文件> <目标文件>", argv[0]); return 1; } // 打开源文件 int src_fd = open(argv[1], O_RDONLY); if (src_fd < 0) { perror("打开源文件失败"); return 1; } // 获取源文件权限 struct stat st; if (fstat(src_fd, &st) < 0) { perror("获取文件信息失败"); close(src_fd); return 1; } // 创建目标文件 int dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, st.st_mode); if (dst_fd < 0) { perror("创建目标文件失败"); close(src_fd); return 1; } // 复制文件内容 char buffer[BUFFER_SIZE]; ssize_t bytes_read; while ((bytes_read = read(src_fd, buffer, BUFFER_SIZE)) > 0) { ssize_t bytes_written = write(dst_fd, buffer, bytes_read); if (bytes_written != bytes_read) { perror("写入失败"); break; } } if (bytes_read < 0) { perror("读取失败"); } // 关闭文件 close(src_fd); close(dst_fd); printf("文件复制完成"); return 0;}
**编译运行**:
gcc file_copy.c -o file_copy./file_copy source.txt destination.txt
**关键参数讲解**:
- `fstat`:获取文件状态信息
- `st.st_mode`:文件权限模式
- 使用4KB缓冲区提高效率
**高频故障提示**:
- 必须检查每次read和write的返回值
- 磁盘空间不足会导致写入失败
- 权限不足会导致创建文件失败
10.2 使用标准IO库实现
**示例代码**:
#include #include #define BUFFER_SIZE 4096int main(int argc, char *argv[]) { if(argc != 3) { fprintf(stderr, "用法: %s <源文件> <目标文件>", argv[0]); return 1; } // 打开源文件 FILE *src_fp = fopen(argv[1], "rb"); if(src_fp == NULL) { perror("打开源文件失败"); return 1; } // 创建目标文件 FILE *dst_fp = fopen(argv[2], "wb"); if(dst_fp == NULL) { perror("创建目标文件失败"); fclose(src_fp); return 1; } // 复制文件内容 char buffer[BUFFER_SIZE]; size_t items_read; while((items_read = fread(buffer, 1, BUFFER_SIZE, src_fp)) > 0) { size_t items_written = fwrite(buffer, 1, items_read, dst_fp); if(items_written != items_read) { perror("写入失败"); break; } } // 关闭文件 fclose(src_fp); fclose(dst_fp); printf("文件复制完成"); return 0;}
**编译运行**:
gcc file_copy_stdio.c -o file_copy_stdio./file_copy_stdio source.txt destination.txt
十一、总结与思考
11.1 核心知识点回顾
1. **文件描述符**:内核为打开的文件分配的非负整数索引
2. **系统调用IO**:open、read、write、close等底层接口
3. **标准IO库**:fopen、fread、fwrite等高层接口
4. **缓冲机制**:全缓冲、行缓冲、无缓冲
5. **文件定位**:lseek、fseek移动读写位置
6. **IO重定向**:dup、dup2重定向标准输入输出
7. **阻塞与非阻塞**:控制IO操作的行为模式
11.2 设计思想总结
- **抽象统一**:将各种设备统一抽象为文件接口
- **分层设计**:硬件层、内核层、标准库层、应用层
- **缓冲优化**:减少系统调用次数,提高效率
- **索引映射**:通过文件描述符快速定位资源
11.3 实践建议
1. 理解文件描述符与FILE指针的区别和联系
2. 掌握系统调用IO和标准IO库的使用场景
3. 注意检查所有IO函数的返回值
4. 合理使用缓冲机制提高性能
5. 注意资源管理,及时关闭文件描述符
11.4 性能优化技巧
1. **缓冲区大小**:选择合适的缓冲区大小(通常4KB-64KB)
2. **批量操作**:尽量减少IO操作次数
3. **内存映射**:对于大文件,考虑使用mmap
4. **异步IO**:对于高性能场景,使用异步IO