Linux系统编程----文件IO
一、文件描述符本质内核为进程打开的文件分配的非负整数索引是进程访问文件的唯一标识。分配规则优先使用当前最小未被使用的文件描述符编号。默认关联每个进程启动时默认打开 3 个文件描述符0标准输入stdin1标准输出stdout2标准错误stderr二、open 函数头文件#include fcntl.h函数原型int open(const char *pathname, int flags, mode_t mode);核心参数pathname: 要打开的文件路径字符串形式。flags: 指定打开文件的方式必须包含以下必选项之一标志含义O_RDONLY只读O_WRONLY只写O_RDWR读写此外还可以通过按位或组合以下常用附加标志标志含义O_APPEND追加模式每次写入都定位到文件末尾O_CREAT若文件不存在则创建需配合mode指定权限O_TRUNC若文件存在且以写方式打开则将其长度截断为0清空内容O_EXCL与O_CREAT一起使用时若文件已存在则返回错误防止竞争mode仅当使用了O_CREAT时第三个参数才有效用于指定新创建文件的访问权限八进制表示如0666。最终权限还会受到进程的umask影响实际权限 mode ~umask返回值成功返回一个文件描述符非负整数如3、4、5等。失败返回-1并设置errno可用perror或strerror查看错误原因。*open与fopen的对应关系标准IO的fopen实际上就是对open的系统调用进行了封装并增加了缓冲区管理。下面是常用fopen模式与open标志的对应关系fopen模式open标志组合rO_RDONLYrO_RDWRwO_WRONLY | O_CREAT | O_TRUNCwO_RDWR | O_CREAT | O_TRUNCaO_WRONLY | O_CREAT | O_APPENDaO_RDWR | O_CREAT | O_APPEND示例#include stdio.h #include fcntl.h #include unistd.h int main(int argc, const char *argv[]) { if (argc ! 2) { printf(Usage: %s filename\n,argv[0]); return -1; } // 打开文件只写模式 创建如果不存在 截断(如果文件已存在打开时会清空原有内容) // 0666权限设置为 rw-rw-rw- int fd open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666); if (fd 0) { perror(open fail); return -1; } printf(success\n); return 0; }三、read/write函数ssize_t read(int fd, void *buf, size_t count);功能 :从文件描述符fd指向的文件中读取最多count个字节的数据并存储到buf指向的缓冲区中。参数说明 :参数含义fd文件描述符由open返回buf指向缓冲区的指针用于存放读取的数据count请求读取的最大字节数返回值成功返回实际读取的字节数可能小于count如到达文件末尾或剩余数据不足。到达文件末尾返回0表示没有更多数据可读。失败返回-1并设置errno。注意read不保证一次能读到count字节因此通常需要循环读取直到满足需求或遇到文件结束。ssize_t write(int fd, const void *buf, size_t count);功能 :将buf指向的缓冲区中的count个字节写入到文件描述符fd指向的文件中。参数说明参数含义fd文件描述符buf指向要写入数据的缓冲区count要写入的字节数返回值成功返回实际写入的字节数通常等于count但可能小于如磁盘满或信号中断。失败返回-1并设置errno。示例 : 用read/write实现cp命令#include stdio.h #include fcntl.h #include unistd.h //./a.out src dest int main(int argc, const char *argv[]) { if (argc ! 3) { printf(Usage: %s src dest\n,argv[0]); return -1; } int fd_s open(argv[1],O_RDONLY); int fd_d open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666); if (fd_s 0 || fd_d 0) { perror(open fail); return -1; } //拷贝 char buf[100] {0}; int ret 0; while (1) { ret read(fd_s,buf,sizeof(buf)); if (ret 0) break; write(fd_d,buf,ret); } //关闭 close(fd_s); close(fd_d); return 0; }四、lseek函数off_t lseek(int fd, off_t offset, int whence);功能 :重新定位文件描述符fd关联的文件偏移量即下一次读写操作开始的位置。它不进行实际的读写操作只移动文件内部的位置指针。参数说明参数含义fd文件描述符由open返回offset偏移量单位字节可正可负whence参考起点决定offset的基准位置whence的取值宏含义说明SEEK_SET文件开头offset必须 ≥ 0SEEK_CUR当前位置offset可正可负SEEK_END文件末尾offset可正可负返回值成功返回从文件开头到新位置的字节数即新的文件偏移量。失败返回-1并设置errno如fd无效、whence非法等。常用 :off_t pos lseek(fd, 0, SEEK_SET); // 返回0文件开头 off_t end lseek(fd, 0, SEEK_END); // 返回文件大小字节数 off_t pos lseek(fd, 100, SEEK_CUR); // 从当前位置向后移动100字节 off_t pos lseek(fd, -50, SEEK_END); // 从文件末尾向前移动50字节获取文件大小 :off_t file_size lseek(fd, 0, SEEK_END); // 此时文件偏移量已变为文件末尾若需后续读写应重新定位 lseek(fd, 0, SEEK_SET); // 可选移回开头示例 :根据源文件的大小给目标文件创建一个一样大的空洞文件。#include stdio.h #include fcntl.h #include unistd.h //./a.out src dest int main(int argc, const char *argv[]) { if (argc ! 3) { printf(Usage: %s src dest\n,argv[0]); return -1; } int fd_s open(argv[1],O_RDONLY); int fd_d open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666); if (fd_s 0 || fd_d 0) { perror(open fail); return -1; } // 1. 获取源文件总大小把指针移到末尾返回值就是文件字节数 off_t len lseek(fd_s,0,SEEK_END); // 2. 把目标文件的指针 移动到 【文件大小 - 1】的位置 // 比如源文件是 100 字节就移动到第 99 个字节的位置 lseek(fd_d,len-1,SEEK_SET); // 3. 在这个位置写入 1 个空字节 write(fd_d,,1); return 0; }五、目录操作与文件状态获取5.1 opendir打开目录#include dirent.hDIR *opendir(const char *name);功能打开名为name的目录并返回一个目录流指针DIR*。参数name— 目录路径字符串。返回值成功返回DIR*指针失败返回NULL并设置errno。5.2 readdir读取目录项struct dirent *readdir(DIR *dirp);功能从目录流dirp中读取下一个目录项返回包含该目录项信息的结构体指针。参数dirp— 由opendir返回的目录流指针。返回值成功返回struct dirent*指针失败或读到目录末尾时返回NULL。struct dirent结构体不同系统略有差异但核心成员如下struct dirent { ino_t d_ino; /* inode号 */ off_t d_off; /* 偏移量非文件偏移 */ unsigned short d_reclen; /* 记录长度 */ unsigned char d_type; /* 文件类型某些文件系统可能不支持 */ char d_name[256]; /* 文件名以\0结尾 */ };d_name最重要的字段存储文件名。d_type可快速判断文件类型其值可以是宏含义DT_BLK块设备DT_CHR字符设备DT_DIR目录DT_FIFO命名管道DT_LNK符号链接DT_REG普通文件DT_SOCK套接字DT_UNKNOWN类型未知需用stat进一步判断5.3 closedir关闭目录流int closedir(DIR *dirp);功能关闭目录流释放资源。返回值成功返回0失败返回-1。⚠️注意一个目录流只能关闭一次多次关闭可能导致未定义行为。六、获取文件状态stat系列函数目录操作只能获得文件名和简单的文件类型d_type若需要文件的详细属性大小、权限、时间戳等必须使用stat、lstat或fstat。6.1 stat获取文件状态#include sys/stat.h#include unistd.hint stat(const char *pathname, struct stat *statbuf);功能获取pathname指定文件的属性存入statbuf指向的结构体。参数pathname文件路径。statbuf指向struct stat的指针用于接收信息。返回值成功返回0失败返回-1并设置errno。注意stat会跟随符号链接即获取链接指向的目标文件的信息。若需获取符号链接本身的信息使用lstat。6.2 struct stat 结构体struct stat { dev_t st_dev; /* 设备ID */ ino_t st_ino; /* inode号 */ mode_t st_mode; /* 文件类型和权限 */ nlink_t st_nlink; /* 硬链接数 */ uid_t st_uid; /* 所有者用户ID */ gid_t st_gid; /* 所有者组ID */ dev_t st_rdev; /* 特殊设备ID仅对设备文件有效 */ off_t st_size; /* 总大小字节 */ blksize_t st_blksize; /* 文件系统IO块大小 */ blkcnt_t st_blocks; /* 分配的512B块数 */ time_t st_atime; /* 最后访问时间 */ time_t st_mtime; /* 最后修改时间 */ time_t st_ctime; /* 最后状态改变时间 */ };七、解析文件类型与权限st_mode字段是一个位掩码同时包含了文件类型和权限信息。7.1 获取文件类型方法一使用S_IFMT掩码提取类型mode_t type st_mode S_IFMT; switch (type) { case S_IFREG: printf(普通文件); break; case S_IFDIR: printf(目录); break; case S_IFLNK: printf(符号链接); break; case S_IFCHR: printf(字符设备); break; case S_IFBLK: printf(块设备); break; case S_IFIFO: printf(FIFO); break; case S_IFSOCK: printf(套接字); break; default: printf(未知类型); break; }方法二使用预定义的宏更简洁宏含义S_ISREG(st_mode)是否为普通文件S_ISDIR(st_mode)是否为目录S_ISCHR(st_mode)是否为字符设备S_ISBLK(st_mode)是否为块设备S_ISFIFO(st_mode)是否为FIFOS_ISLNK(st_mode)是否为符号链接S_ISSOCK(st_mode)是否为套接字示例if (S_ISREG(st.st_mode)) printf(普通文件);7.2 获取文件权限st_mode的低9位或12位含setuid等表示权限。常用宏宏含义S_IRUSR/S_IRGRP/S_IROTH读权限S_IWUSR/S_IWGRP/S_IWOTH写权限S_IXUSR/S_IXGRP/S_IXOTH执行权限S_ISUID/S_ISGIDsetuid / setgid 位S_ISVTX粘滞位权限可像ls -l那样输出通常用掩码运算。八、标准IO与文件IO的互转在Linux系统编程中标准IOFILE*流和文件IO文件描述符两套接口各有优势标准IO提供缓冲和格式化方便文件IO更底层、更灵活。通过以下两个函数可以在两者之间自由转换。8.1 从文件描述符到流fdopenFILE *fdopen(int fd, const char *mode);功能将一个已打开的文件描述符转换为标准IO流FILE*。参数fd已打开的文件描述符。mode与fopen相同的模式字符串如r,w,a等必须与fd原有的访问模式兼容。返回值成功返回FILE*指针失败返回NULL。注意事项转换后对流的操作如fprintf、fgets会作用于原文件描述符。关闭流fclose时底层的文件描述符也会自动关闭。转换后不要再单独close(fd)避免重复关闭。8.2 从流到文件描述符filenoint fileno(FILE *stream);功能获取标准IO流底层关联的文件描述符。参数stream— 已打开的FILE*流。返回值成功返回文件描述符非负整数失败返回-1。注意事项获取的文件描述符可用于系统调用如read、write、lseek、fcntl等。不要手动关闭该文件描述符因为关闭流fclose时会自动关闭它。若同时使用标准IO和系统调用操作同一文件需注意缓冲问题必要时调用fflush确保数据一致混合使用示例 :// 用open创建文件转换为流再用fprintf格式化写入 int fd open(test.txt, O_WRONLY | O_CREAT, 0644); FILE *fp fdopen(fd, w); fprintf(fp, Hello, world!\n); fclose(fp); // 自动关闭底层fd // 用fopen打开文件获取fd再用lseek定位 FILE *fp2 fopen(data.txt, r); int fd2 fileno(fp2); off_t size lseek(fd2, 0, SEEK_END); printf(文件大小: %ld\n, size); fclose(fp2);四、总结fdopen文件描述符 → 标准IO流适合需要格式化输出或行缓冲的场景。fileno标准IO流 → 文件描述符适合需要系统调用精细控制的场景。两者配合既能享受标准IO的便捷又能利用系统调用的强大功能。关键点转换后只关闭流fclose不要重复关闭文件描述符混合使用时注意缓冲区同步。