POSIX系统上read和write系统调用具体如何实现,其行为细节是什么?
摘要:关于UNIX和Linux的宣传语中,一切皆文件应该是最广为人知的一句。 不管是普通文件,还是硬件设备、管道、网络套接字,在Linux甚至还有信号和定时器都共享一套相似的api,大家可以用类似的代码完成各种不同的任务,大大简化了代码复杂度和学
关于UNIX和Linux的宣传语中,一切皆文件应该是最广为人知的一句。
不管是普通文件,还是硬件设备、管道、网络套接字,在Linux甚至还有信号和定时器都共享一套相似的api,大家可以用类似的代码完成各种不同的任务,大大简化了代码复杂度和学习成本。
当然这只是理想中的情况,现实是普通文件和硬件设备是两种完全不同的东西,普通文件和网络套接字尤其是UDP协议的那种更是风马牛不相及,强行把这些行为属性完全不同的事物整合进同一套api,导致了read/write/send/recv这几个系统调用的行为极其复杂,bug丛生,更是给很多新手带来了无尽的困扰。
而且由于系统差异和资料分散,这类问题就连求助于AI都很难得到有效解决。这也是我写这篇文章的原因。
进入正题之前我们先限定一下讨论范围和实验环境,因为这个主题太复杂了包罗万象是不可能的。
讨论范围:行为会基于POSIX 2008这版标准进行讨论,但也会加入一下Linux和macOS上特有的行为,这些会特别标注。
实验环境:Linux环境内核版本高于4.0即可,macOS 15及以上。
基础回顾之部分读部分写
有一些重要的概念会贯穿整个我们对系统调用行为的讨论,这里必须先介绍一下。
我们先来看看接下来要说的系统调用长什么样:
#include <sys/types.h>
#include <unistd.h>
// 从文件描述符里读数据
ssize_t read(int fd, void *buf, size_t nbyte);
// 向文件描述符里写数据
ssize_t write(int fd, const void *buf, size_t nbyte);
// 从套接字中读取数据,不可用于套接字以外
ssize_t recv(int sockfd, void *buf, size_t nbyte, int flags);
// 向套接字写入数据,不可用于套接字以外
ssize_t send(int sockfd, const void *buf, size_t nbyte, int flags);
他们长得很像,核心逻辑也差不多——围绕一块nbyte长度的缓冲区进行操作,把数据从缓冲区写入描述符,或者从描述符里读取数据填进缓冲区。这些系统调用是文件和网络io的核心。
通常读取类的系统调用会尽可能多地读入数据直到填满缓冲区,而写入类的系统调用则会尽可能把缓冲区里所有的数据写入描述符。
然而现实是POSIX除了少数操作之外并没有规定读写操作不能被打断,因此经常会出现读或者写了一半时操作被中断的情况:
进程收到了信号,导致系统调用中断,当然一部分系统会在中断后自动重启系统调用,但这个行为是可配置且有系统差异的,所以我们不能忽略这种中断场景
网络套接字的缓冲区中只有少量数据可读/少量空间可写,系统调用在一些情况下中止并返回
读写中遇到错误,比如网络中断、硬盘故障等
这些情况会导致缓冲区里的数据只有一部分被写入目标或者只从目标中读取了一部分数据没能填满缓冲区,简单的说就是调用返回的值比nbyte小且没有设置errno,我们把这些情况统一叫做部分读和部分写,英文叫short read/write或者partial read/write。
这不是bug,而是需要处理的正常的系统行为,尤其是在非阻塞io中。不同类型的对象在这方面有很大的行为差异,这也是本文下面要讨论的内容。
普通文件上的读写行为
普通文件是指在你硬盘里的那些文本文件、程序代码、音乐、图片、视频、PPT之类的东西。这些统称regular files。
普通文件上没有非阻塞io,且无法被poll、select监听。bsd系统上的kqueue对普通文件做了扩展,但这不属于POSIX规范且超出了讨论范围,我们就不提了。
虽然普通文件特性少,也因此read和write在它们上的行为更直观,也更符合预期。
read的行为:
几乎总是阻塞到填满缓冲区
文件可读取内容比缓冲区小的时候会把文件中剩下可读的数据全部读取,然后返回,这是返回值小于缓冲区大小
读取过程中可以被中断
如果读取出错了,则返回值是-1,errno会被设置,缓冲区里很可能会有垃圾数据
如果返回0(EOF,end-of-file),说明文件所有内容已经读取完毕,这也是正常情况,errno不会被设置
从POSIX标准和Linux的文档上来看,read是会有部分读存在的,然而标准是标准实现是实现,现实情况是不管是macOS的APFS上还是Linux上常见的文件系统,read一但准备工作完成就不可被信号中断,因此部分读无法发生。
