ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++ open()和read()函数使用详解

C++ open()和read()函数使用详解 前言先说一件容易被忽略的事open()和read()不属于 C 标准库它们是 POSIXPortable Operating System Interface定义的系统调用接口声明在#include fcntl.h、#include unistd.h里。所以用 C 写文件操作和用open()/read()是两回事前者指std::ifstream那一套后者是直接调用内核。把程序从 Linux 拿到 Windows 上用 MSVC 编译open会直接报找不到标识符——MSVC 提供的是带下划线的_open、_read、_write、_close、_read声明在#include io.h中标志位也是_O_RDONLY、_O_CREAT这一套。本文以下内容全部以Linux glibc GCC 13、C17为准。第二个普遍误解是read(fd, buf, n)会读满n个字节。事实是对普通文件、管道、终端、套接字read()都可能只返回一部分这叫短读short read返回 0 只代表文件结束EOF返回 -1 才是失败。把请求了多少当成读到了多少是绝大多数文件复制、协议收发代码出 bug 的根源。本文讲三件事open()的标志位与返回值语义read()/write()的短读短写与EINTR重试该怎么写才正确以及它和 C 标准库流、C 的fread()之间该怎么选。一、open() 的参数、标志位与返回值POSIX 定义了两个重载形式int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);flags由访问模式和若干可选标志按位或组成。访问模式必须三选一标志含义常见误用O_RDONLY只读拿它返回的 fd 去write()必然EBADFO_WRONLY只写拿它去read()同样失败O_RDWR读写并发下最容易忘记配合O_APPEND可选标志里最常用的是这几个O_CREAT文件不存在就创建此时必须提供第三个参数 mode、O_EXCL与O_CREAT连用文件已存在则失败可用来实现原子地创建锁文件、O_TRUNC打开时截断为 0 字节、O_APPEND每次写之前把文件偏移原子地移到末尾、O_NONBLOCK非阻塞、O_CLOEXECexec系列函数执行时自动关闭该 fd。返回值成功时返回一个非负的、当前进程中最小的未使用文件描述符失败返回 -1 并设置errnoENOENT路径不存在、EACCES权限不足、EMFILE本进程 fd 用尽、EISDIR试图以写方式打开目录等。文件描述符是进程级资源进程退出时内核会自动回收但长期运行的服务必须显式close()否则迟早耗尽 fd。Linux 上单个进程的默认上限可以用getrlimit(RLIMIT_NOFILE, ...)查询。关于O_CLOEXEC它是 POSIX.1-2008 加入的Linux 2.6.23 之后支持。早期系统上要用fcntl(fd, F_SETFD, FD_CLOEXEC)补一刀。这一点在多线程程序里尤其重要——如果先open()再fcntl()两个调用之间恰好有别的线程fork()exec()子进程就会泄漏一个不该继承的 fd。二、read()/write() 的返回值语义两个函数的原型是ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);ssize_t是 POSIX 的有符号整型64 位 Linux 上是long它可以表示 -1所以绝不能用int或char去接返回值——用char接 200 会变成负数用char判断 -1更是永远不成立。read()返回值含义正确处理大于 0实际读到的字节数可能小于count累加偏移继续读0到达 EOF正常结束-1errno EINTR被信号打断没有数据丢失直接重试不要当错误-1errno EAGAIN非阻塞 fd 当前无数据交给事件循环等下次可读-1其他真错误EIO、EBADF、EFAULT…记录并放弃短读的成因值得逐条记住读普通文件时接近末尾自然会短读管道/FIFO 时只返回缓冲区里现有的量读终端时行模式下最多返回一行读套接字时一次只返回当前可读的报文量。所以任何我read一次就该拿到完整消息的假设都是错的应用层协议必须自带长度字段或分隔符。write()同样会短写partial write内核发送缓冲区满了就只写一部分。被信号打断时返回 -1 且errno EINTR。这里有个易错点——write返回 -1 且errno EAGAIN是暂时写不进去不是错误而write返回 0 在count 0时几乎不可能出现写循环里把它当失败处理最省心否则会死循环。下面是一个完整可编译的复制文件程序把重试 EINTR和循环处理短读短写都写对了#include fcntl.h #include unistd.h #include cerrno #include cstddef #include cstdio #include cstring namespace { // 把 len 字节全部写出。只重试 EINTR返回 false 表示真失败。 bool write_all(int fd, const char* data, std::size_t len) { std::size_t off 0; while (off len) { const ssize_t n ::write(fd, data off, len - off); if (n 0) { off static_caststd::size_t(n); // 短写只前进了 n 字节 continue; } if (n -1 errno EINTR) continue; // 被信号打断重试 return false; // 真错误或异常的 n 0 } return true; } // 从 in_fd 拷贝到 out_fd。返回 0 成功-1 失败errno 已设置。 int copy_fd(int in_fd, int out_fd) { char buf[64 * 1024]; for (;;) { const ssize_t n ::read(in_fd, buf, sizeof(buf)); if (n 0) { if (!write_all(out_fd, buf, static_caststd::size_t(n))) return -1; continue; } if (n 0) return 0; // EOF正常结束 if (errno EINTR) continue; // 被打断重试 return -1; // 真错误 } } } // namespace int main(int argc, char** argv) { if (argc ! 3) { std::fprintf(stderr, usage: %s src dst\n, argv[0]); return 2; } const int in_fd ::open(argv[1], O_RDONLY | O_CLOEXEC); if (in_fd -1) { std::fprintf(stderr, open %s: %s\n, argv[1], std::strerror(errno)); return 1; } // O_CREAT 时第三个参数 mode 必须给它只在本次真的新建了文件时生效 // 还会被进程的 umask 削掉若干权限位。 const int out_fd ::open(argv[2], O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); if (out_fd -1) { std::fprintf(stderr, open %s: %s\n, argv[2], std::strerror(errno)); ::close(in_fd); // 别忘了回收已打开的 fd return 1; } const int rc copy_fd(in_fd, out_fd); ::close(in_fd); // close() 也可能失败某些文件系统如 NFS把写入错误延迟到这里报告。 if (::close(out_fd) -1 rc 0) { std::fprintf(stderr, close: %s\n, std::strerror(errno)); return 1; } return rc 0 ? 0 : 1; }要注意std::strerror内部返回的是静态缓冲区不是线程安全的多线程程序里应该用 POSIX 的strerror_r或者干脆只打印errno的数值。另外这段代码里的char buf[64 * 1024]是栈上数组单线程没问题但如果把它放进线程函数就要留意线程栈大小pthread_attr_setstacksize或std::thread的默认栈Linux 上 glibc 默认 8 MB——64 KiB 不算多但若写成char buf[1024 * 1024]就有风险了。三、open()/read() 与标准库设施怎么选同一件事有三种做法各有适用面方案层次缓冲适用场景主要代价open/read/write直接系统调用无每次调用一次陷入内核需要精确控制、非阻塞 IO、epoll事件循环、sendfile要自己处理短读短写与EINTRfopen/fread/fwriteC 标准库有 FILE 缓冲用户态缓冲C 风格代码、按行/按块读与 fd 混用要小心缓冲std::ifstream/std::ofstreamC 标准库有 streambuf 缓冲用户态缓冲类型安全的格式化输入输出、RAII默认异常不抛出需手动检查状态判断标准很直接需要把 fd 交给select/poll/epoll做多路复用或者需要O_NONBLOCK、O_DIRECT、sendfile这类 fd 级能力时必须用open()只是读个配置文件、写个日志std::ofstream更省事且自动 RAII。ifstream与open()之间不要随意混用——fileno()POSIX 扩展非标准 C能拿到 FILE 或流背后的 fd但前提是流没有被缓冲走数据混用极易读到错位的内容。常见坑点#场景❌ 错误写法✅ 正确写法1接住read的返回值char n ::read(fd, buf, len);然后判断n -1ssize_t n ::read(fd, buf, len);用有符号的ssize_t2以为一次读满::read(fd, buf, 4096);直接当 4096 字节用循环读到n 0或用长度前缀协议3创建文件忘了 mode带了O_CREAT却只传两个实参第三个参数 mode 必须给例如06444把EINTR当错误if (n 0) { return -1; }if (n 0 errno EINTR) continue;5打开后不检查int fd ::open(p, O_RDONLY); ::read(fd, ...);先判断fd -1并处理errno6fd 泄漏出错路径return前忘了::close(in_fd)用 RAII 包装 fd或所有出口统一关7多进程追加写lseek(fd, 0, SEEK_END); write(fd, s, n);打开时加O_APPEND偏移移动与写在同一个原子操作内8混用 fd 与 FILE 缓冲fread之后紧接着::read(fd, ...)二选一要混用先fflush并注意FILE的读缓冲第 7 条特别值得展开lseek加write在单线程里看着没问题一旦多个进程或同一进程的多个线程共用同一个打开的文件描述两个调用之间的窗口就会让偏移互相覆盖最终数据互相踩踏。O_APPEND由内核保证定位到末尾和写入是原子的这是唯一正确的做法。总结要点结论出身open/read/write/close是 POSIX 接口不是 C 标准库Windows 上对应_open/_read等返回值铁律read用ssize_t接0 是 EOF-1 要区分EINTR、EAGAIN与真错误短读短写必须写循环任何一次读完/写完的假设都是 bugO_CREAT必须提供 mode 参数否则行为未定义实际权限还会被 umask 削减并发追加用O_APPEND不要lseekwrite选型要epoll/非阻塞/零拷贝就用 fd只要读写文件就用std::ifstream/std::ofstream一句话open()/read()的价值在于给你一个可以交给事件循环的、语义完全可控的句柄代价是所有边界情况都得自己兜住——返回值、errno、短读短写、fd 生命周期一个都不能省。
返回列表