Linux 五种 IO 模型详解

Linux 五种 IO 模型详解:从阻塞 IO 到非阻塞、信号驱动与异步 IO

网络编程里谈 IO,很多人一开始会把注意力放在 readrecvrecvfrom 这些函数上:调用它们,拿到数据,然后处理业务。真正写服务端程序时才会发现,IO 的难点往往不在"怎么读",而在"什么时候读""等待期间程序在干什么""一个进程能不能同时照看多个连接"。

理解五种 IO 模型,可以抓住一条主线:一次 IO 通常分成两个阶段:等待数据就绪,以及把数据从内核拷贝到用户空间
#mermaid-svg-wAcJlEXbLbv8fPwV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wAcJlEXbLbv8fPwV .error-icon{fill:#552222;}#mermaid-svg-wAcJlEXbLbv8fPwV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wAcJlEXbLbv8fPwV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wAcJlEXbLbv8fPwV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wAcJlEXbLbv8fPwV .marker.cross{stroke:#333333;}#mermaid-svg-wAcJlEXbLbv8fPwV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wAcJlEXbLbv8fPwV p{margin:0;}#mermaid-svg-wAcJlEXbLbv8fPwV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster-label text{fill:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster-label span{color:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster-label span p{background-color:transparent;}#mermaid-svg-wAcJlEXbLbv8fPwV .label text,#mermaid-svg-wAcJlEXbLbv8fPwV span{fill:#333;color:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV .node rect,#mermaid-svg-wAcJlEXbLbv8fPwV .node circle,#mermaid-svg-wAcJlEXbLbv8fPwV .node ellipse,#mermaid-svg-wAcJlEXbLbv8fPwV .node polygon,#mermaid-svg-wAcJlEXbLbv8fPwV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wAcJlEXbLbv8fPwV .rough-node .label text,#mermaid-svg-wAcJlEXbLbv8fPwV .node .label text,#mermaid-svg-wAcJlEXbLbv8fPwV .image-shape .label,#mermaid-svg-wAcJlEXbLbv8fPwV .icon-shape .label{text-anchor:middle;}#mermaid-svg-wAcJlEXbLbv8fPwV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wAcJlEXbLbv8fPwV .rough-node .label,#mermaid-svg-wAcJlEXbLbv8fPwV .node .label,#mermaid-svg-wAcJlEXbLbv8fPwV .image-shape .label,#mermaid-svg-wAcJlEXbLbv8fPwV .icon-shape .label{text-align:center;}#mermaid-svg-wAcJlEXbLbv8fPwV .node.clickable{cursor:pointer;}#mermaid-svg-wAcJlEXbLbv8fPwV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wAcJlEXbLbv8fPwV .arrowheadPath{fill:#333333;}#mermaid-svg-wAcJlEXbLbv8fPwV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wAcJlEXbLbv8fPwV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wAcJlEXbLbv8fPwV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wAcJlEXbLbv8fPwV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wAcJlEXbLbv8fPwV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wAcJlEXbLbv8fPwV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster text{fill:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV .cluster span{color:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wAcJlEXbLbv8fPwV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wAcJlEXbLbv8fPwV rect.text{fill:none;stroke-width:0;}#mermaid-svg-wAcJlEXbLbv8fPwV .icon-shape,#mermaid-svg-wAcJlEXbLbv8fPwV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wAcJlEXbLbv8fPwV .icon-shape p,#mermaid-svg-wAcJlEXbLbv8fPwV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wAcJlEXbLbv8fPwV .icon-shape .label rect,#mermaid-svg-wAcJlEXbLbv8fPwV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wAcJlEXbLbv8fPwV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wAcJlEXbLbv8fPwV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wAcJlEXbLbv8fPwV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用程序发起 IO 调用
阶段一:等待数据就绪
阶段二:内核数据拷贝到用户空间
应用程序拿到数据并继续执行

对网络读取来说,数据会先到达内核缓冲区。应用层代码想拿到这些数据,需要系统调用进入内核。如果数据还没来,程序可能一直等,也可能马上返回,也可能先去做别的事情,等内核通知。这些选择就形成了不同的 IO 模型。

一、为什么要关心 IO 模型

一个朴素的网络程序经常长这样:

c 复制代码
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);

这行代码看起来只是"收数据",内部却可能经历较长等待:

  • 远端还没有发送数据,本机内核缓冲区为空;
  • 数据已经到达内核,但还需要拷贝到用户提供的 buffer
  • 当前调用者在等待期间可能完全无法继续执行后续逻辑。

如果程序只处理一个连接,阻塞等待通常可以接受。可一旦面对多个连接、多个文件描述符,或者要求程序在等待输入时还要做其他任务,IO 模型的选择就会直接影响程序结构和性能。

这里需要特别注意:提高 IO 效率,很多时候不是让拷贝本身变快,而是减少无意义的等待,或者让等待变得更可控

二、IO 的两个阶段:等待与拷贝

以网络读取为例,一次读取并不是应用程序直接从网卡里拿数据,而是经过内核缓冲区:

text 复制代码
网络数据到达
    ↓
内核接收缓冲区
    ↓
系统调用读取
    ↓
用户空间 buffer

所以一次读取可以拆成两个动作:

阶段 含义 常见耗时原因
等待数据就绪 等待内核缓冲区里有可读数据 对端未发送、网络延迟、连接空闲
数据拷贝 将内核缓冲区中的数据复制到用户空间 数据量、内存拷贝成本

五种 IO 模型主要区别就在于:等待阶段是否阻塞调用者,拷贝阶段由谁负责,什么时候通知应用程序

三、五种 IO 模型总览

IO 模型 等待数据期间 数据拷贝期间 通知时机 典型特点
阻塞 IO 调用者阻塞 调用者继续阻塞 调用返回时数据已拷贝完成 最直观,代码简单
非阻塞 IO 立即返回 数据就绪后调用者阻塞等待拷贝 每次调用立即给结果 需要反复轮询
信号驱动 IO 调用者可继续执行 收到信号后再发起读取,拷贝时阻塞 数据就绪时通知 减少等待阶段的阻塞
IO 多路转接 阻塞在统一等待接口上 就绪后再调用读取,拷贝时阻塞 一个或多个描述符就绪 适合同时管理多个描述符
异步 IO 调用者不阻塞 内核完成拷贝 拷贝完成后通知 等待和拷贝都交给内核处理

下面逐个拆开看。

四、阻塞 IO:最容易理解,也最容易卡住

阻塞 IO 是 socket 默认的工作方式。应用程序调用 recvfrom 之后,如果内核中还没有准备好数据,当前执行流会进入等待状态。数据到达后,内核继续把数据拷贝到用户空间,拷贝完成后系统调用返回。

流程可以理解为:
内核 应用程序 内核 应用程序 #mermaid-svg-GKvf4XRnXK9huv8h{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GKvf4XRnXK9huv8h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GKvf4XRnXK9huv8h .error-icon{fill:#552222;}#mermaid-svg-GKvf4XRnXK9huv8h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GKvf4XRnXK9huv8h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GKvf4XRnXK9huv8h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GKvf4XRnXK9huv8h .marker.cross{stroke:#333333;}#mermaid-svg-GKvf4XRnXK9huv8h svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GKvf4XRnXK9huv8h p{margin:0;}#mermaid-svg-GKvf4XRnXK9huv8h .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GKvf4XRnXK9huv8h text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GKvf4XRnXK9huv8h .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GKvf4XRnXK9huv8h .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GKvf4XRnXK9huv8h .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GKvf4XRnXK9huv8h .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GKvf4XRnXK9huv8h #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GKvf4XRnXK9huv8h .sequenceNumber{fill:white;}#mermaid-svg-GKvf4XRnXK9huv8h #sequencenumber{fill:#333;}#mermaid-svg-GKvf4XRnXK9huv8h #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GKvf4XRnXK9huv8h .messageText{fill:#333;stroke:none;}#mermaid-svg-GKvf4XRnXK9huv8h .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GKvf4XRnXK9huv8h .labelText,#mermaid-svg-GKvf4XRnXK9huv8h .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GKvf4XRnXK9huv8h .loopText,#mermaid-svg-GKvf4XRnXK9huv8h .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GKvf4XRnXK9huv8h .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GKvf4XRnXK9huv8h .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GKvf4XRnXK9huv8h .noteText,#mermaid-svg-GKvf4XRnXK9huv8h .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GKvf4XRnXK9huv8h .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GKvf4XRnXK9huv8h .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GKvf4XRnXK9huv8h .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GKvf4XRnXK9huv8h .actorPopupMenu{position:absolute;}#mermaid-svg-GKvf4XRnXK9huv8h .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GKvf4XRnXK9huv8h .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GKvf4XRnXK9huv8h .actor-man circle,#mermaid-svg-GKvf4XRnXK9huv8h line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GKvf4XRnXK9huv8h :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} recvfrom()数据未就绪,调用者等待数据到达内核缓冲区拷贝数据到用户空间recvfrom() 返回

阻塞 IO 的优势是代码简单,符合直觉:调用函数,等结果,拿到结果继续执行。缺点也很明显:如果数据一直不来,调用者就一直停在那里。

它适合初学阶段、单连接程序、简单命令行工具,也常见于"一个线程处理一个任务"的模型。但在高并发网络服务中,如果大量线程都阻塞在 IO 上,线程调度和资源占用会变得很重。

五、非阻塞 IO:不等数据,但要自己反复问

非阻塞 IO 的核心特点是:当数据没有准备好时,系统调用不会让调用者一直睡眠,而是立即返回错误状态

recvfrom 为例,如果内核缓冲区没有数据,调用会返回失败,并设置类似 EWOULDBLOCKEAGAIN 的错误码。应用程序可以稍后再试。
内核 应用程序 内核 应用程序 #mermaid-svg-2sE3bkVa6aj6D0bU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2sE3bkVa6aj6D0bU .error-icon{fill:#552222;}#mermaid-svg-2sE3bkVa6aj6D0bU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2sE3bkVa6aj6D0bU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2sE3bkVa6aj6D0bU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2sE3bkVa6aj6D0bU .marker.cross{stroke:#333333;}#mermaid-svg-2sE3bkVa6aj6D0bU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2sE3bkVa6aj6D0bU p{margin:0;}#mermaid-svg-2sE3bkVa6aj6D0bU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2sE3bkVa6aj6D0bU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2sE3bkVa6aj6D0bU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-2sE3bkVa6aj6D0bU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-2sE3bkVa6aj6D0bU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-2sE3bkVa6aj6D0bU .sequenceNumber{fill:white;}#mermaid-svg-2sE3bkVa6aj6D0bU #sequencenumber{fill:#333;}#mermaid-svg-2sE3bkVa6aj6D0bU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-2sE3bkVa6aj6D0bU .messageText{fill:#333;stroke:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2sE3bkVa6aj6D0bU .labelText,#mermaid-svg-2sE3bkVa6aj6D0bU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .loopText,#mermaid-svg-2sE3bkVa6aj6D0bU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-2sE3bkVa6aj6D0bU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-2sE3bkVa6aj6D0bU .noteText,#mermaid-svg-2sE3bkVa6aj6D0bU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-2sE3bkVa6aj6D0bU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2sE3bkVa6aj6D0bU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2sE3bkVa6aj6D0bU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-2sE3bkVa6aj6D0bU .actorPopupMenu{position:absolute;}#mermaid-svg-2sE3bkVa6aj6D0bU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-2sE3bkVa6aj6D0bU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-2sE3bkVa6aj6D0bU .actor-man circle,#mermaid-svg-2sE3bkVa6aj6D0bU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-2sE3bkVa6aj6D0bU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} recvfrom()数据未就绪,立即返回 EWOULDBLOCKrecvfrom()数据仍未就绪,继续返回recvfrom()数据已就绪拷贝数据recvfrom() 成功返回

这种模型不会让调用者卡在一次系统调用里,程序可以在等待期间做别的事情。但它也带来一个问题:如果只是简单地写一个死循环不断调用 recvfrom,就会形成轮询,CPU 可能被大量无效检查消耗掉。

很多初学者会误以为"非阻塞一定更高效"。实际上,非阻塞只是把等待的控制权交给应用程序。如果应用程序没有合理安排重试时机,效率未必更好。

六、用 fcntl 设置非阻塞

Linux 中可以通过 fcntl 修改文件描述符的属性。它的函数原型如下:

c 复制代码
int fcntl(int fd, int cmd, ...);

其中:

  • fd 是文件描述符;
  • cmd 表示要执行的操作;
  • 第三个参数根据 cmd 的不同而变化。

常用操作可以整理成下面这张表:

操作 含义
F_DUPFD 复制文件描述符
F_GETFD / F_SETFD 获取或设置文件描述符标志
F_GETFL / F_SETFL 获取或设置文件状态标志
F_GETOWN / F_SETOWN 获取或设置异步 IO 所有权
F_GETLK / F_SETLK / F_SETLKW 获取或设置记录锁

设置非阻塞通常分两步:

  1. F_GETFL 取出原来的文件状态标志;
  2. 在原有标志基础上追加 O_NONBLOCK,再用 F_SETFL 写回。

补充说明:下面的示例用于帮助理解,重点演示如何把标准输入设置为非阻塞,以及非阻塞读取失败时应该如何判断原因。

c 复制代码
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL);
    if (flags < 0) {
        perror("fcntl F_GETFL");
        return -1;
    }

    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl F_SETFL");
        return -1;
    }

    return 0;
}

int main(void) {
    if (set_nonblock(STDIN_FILENO) < 0) {
        return 1;
    }

    while (1) {
        char buffer[1024] = {0};
        ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);

        if (n > 0) {
            printf("input: %s", buffer);
            continue;
        }

        if (n == 0) {
            printf("stdin closed\n");
            break;
        }

        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("no data, do something else...\n");
            sleep(1);
            continue;
        }

        printf("read error: %s\n", strerror(errno));
        break;
    }

    return 0;
}

编译运行:

bash 复制代码
gcc nonblock_stdin.c -o nonblock_stdin
./nonblock_stdin

可能看到的输出:

text 复制代码
no data, do something else...
no data, do something else...
hello
input: hello

这段代码的关键不在 read,而在这两行:

c 复制代码
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

F_GETFL 先读取原有状态,flags | O_NONBLOCK 是在保留原有设置的基础上增加非阻塞能力。如果直接覆盖状态,可能会把原来的访问模式或其他标志弄丢。

另外,非阻塞模式下 read 返回 -1 不一定代表程序出错。遇到 EAGAINEWOULDBLOCK 时,含义通常是"现在没有数据可读,请稍后再试"。这类返回值需要按正常分支处理。

七、信号驱动 IO:数据好了再提醒我

信号驱动 IO 的思路是:应用程序先告诉内核,"这个描述符有数据时通知我"。注册完成后,应用程序可以继续执行其他逻辑。当数据到达内核缓冲区后,内核通过 SIGIO 通知应用程序。应用程序收到信号后,再调用读取函数把数据取出来。
内核 应用程序 内核 应用程序 #mermaid-svg-87mYtSiyYmVTCj8n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-87mYtSiyYmVTCj8n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-87mYtSiyYmVTCj8n .error-icon{fill:#552222;}#mermaid-svg-87mYtSiyYmVTCj8n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-87mYtSiyYmVTCj8n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-87mYtSiyYmVTCj8n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-87mYtSiyYmVTCj8n .marker.cross{stroke:#333333;}#mermaid-svg-87mYtSiyYmVTCj8n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-87mYtSiyYmVTCj8n p{margin:0;}#mermaid-svg-87mYtSiyYmVTCj8n .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-87mYtSiyYmVTCj8n text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-87mYtSiyYmVTCj8n .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-87mYtSiyYmVTCj8n .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-87mYtSiyYmVTCj8n .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-87mYtSiyYmVTCj8n .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-87mYtSiyYmVTCj8n #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-87mYtSiyYmVTCj8n .sequenceNumber{fill:white;}#mermaid-svg-87mYtSiyYmVTCj8n #sequencenumber{fill:#333;}#mermaid-svg-87mYtSiyYmVTCj8n #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-87mYtSiyYmVTCj8n .messageText{fill:#333;stroke:none;}#mermaid-svg-87mYtSiyYmVTCj8n .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-87mYtSiyYmVTCj8n .labelText,#mermaid-svg-87mYtSiyYmVTCj8n .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-87mYtSiyYmVTCj8n .loopText,#mermaid-svg-87mYtSiyYmVTCj8n .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-87mYtSiyYmVTCj8n .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-87mYtSiyYmVTCj8n .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-87mYtSiyYmVTCj8n .noteText,#mermaid-svg-87mYtSiyYmVTCj8n .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-87mYtSiyYmVTCj8n .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-87mYtSiyYmVTCj8n .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-87mYtSiyYmVTCj8n .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-87mYtSiyYmVTCj8n .actorPopupMenu{position:absolute;}#mermaid-svg-87mYtSiyYmVTCj8n .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-87mYtSiyYmVTCj8n .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-87mYtSiyYmVTCj8n .actor-man circle,#mermaid-svg-87mYtSiyYmVTCj8n line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-87mYtSiyYmVTCj8n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注册 SIGIO 相关处理注册返回,应用继续执行数据到达发送 SIGIOrecvfrom()拷贝数据并返回

它和非阻塞轮询相比,减少了"反复问内核数据来没来"的动作。应用程序不用不停调用 recvfrom,而是等内核在数据就绪时通知。

但这里有一个容易混淆的点:信号驱动 IO 通知的是数据已经就绪,不代表数据已经被拷贝到用户空间 。收到 SIGIO 后,应用程序还需要再次调用读取函数;在数据拷贝阶段,调用者仍然可能阻塞。

八、IO 多路转接:一个入口等待多个描述符

IO 多路转接也常被称为 IO 多路复用。它解决的问题是:一个进程如何同时等待多个文件描述符的就绪事件。

如果不用多路转接,一个进程要么阻塞在某一个 recvfrom 上,要么用非阻塞方式反复检查多个描述符。前者顾不上其他连接,后者容易浪费 CPU。多路转接提供了一个统一等待入口,例如典型的 select 模型:
#mermaid-svg-iFNTIfer6AQxOnnJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iFNTIfer6AQxOnnJ .error-icon{fill:#552222;}#mermaid-svg-iFNTIfer6AQxOnnJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iFNTIfer6AQxOnnJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iFNTIfer6AQxOnnJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iFNTIfer6AQxOnnJ .marker.cross{stroke:#333333;}#mermaid-svg-iFNTIfer6AQxOnnJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iFNTIfer6AQxOnnJ p{margin:0;}#mermaid-svg-iFNTIfer6AQxOnnJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster-label text{fill:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster-label span{color:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster-label span p{background-color:transparent;}#mermaid-svg-iFNTIfer6AQxOnnJ .label text,#mermaid-svg-iFNTIfer6AQxOnnJ span{fill:#333;color:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ .node rect,#mermaid-svg-iFNTIfer6AQxOnnJ .node circle,#mermaid-svg-iFNTIfer6AQxOnnJ .node ellipse,#mermaid-svg-iFNTIfer6AQxOnnJ .node polygon,#mermaid-svg-iFNTIfer6AQxOnnJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iFNTIfer6AQxOnnJ .rough-node .label text,#mermaid-svg-iFNTIfer6AQxOnnJ .node .label text,#mermaid-svg-iFNTIfer6AQxOnnJ .image-shape .label,#mermaid-svg-iFNTIfer6AQxOnnJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-iFNTIfer6AQxOnnJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iFNTIfer6AQxOnnJ .rough-node .label,#mermaid-svg-iFNTIfer6AQxOnnJ .node .label,#mermaid-svg-iFNTIfer6AQxOnnJ .image-shape .label,#mermaid-svg-iFNTIfer6AQxOnnJ .icon-shape .label{text-align:center;}#mermaid-svg-iFNTIfer6AQxOnnJ .node.clickable{cursor:pointer;}#mermaid-svg-iFNTIfer6AQxOnnJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iFNTIfer6AQxOnnJ .arrowheadPath{fill:#333333;}#mermaid-svg-iFNTIfer6AQxOnnJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iFNTIfer6AQxOnnJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iFNTIfer6AQxOnnJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iFNTIfer6AQxOnnJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iFNTIfer6AQxOnnJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iFNTIfer6AQxOnnJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster text{fill:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ .cluster span{color:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iFNTIfer6AQxOnnJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iFNTIfer6AQxOnnJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-iFNTIfer6AQxOnnJ .icon-shape,#mermaid-svg-iFNTIfer6AQxOnnJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iFNTIfer6AQxOnnJ .icon-shape p,#mermaid-svg-iFNTIfer6AQxOnnJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iFNTIfer6AQxOnnJ .icon-shape .label rect,#mermaid-svg-iFNTIfer6AQxOnnJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iFNTIfer6AQxOnnJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iFNTIfer6AQxOnnJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iFNTIfer6AQxOnnJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有

应用程序把多个 fd 交给 select
select 等待 fd 就绪
是否有 fd 就绪
返回就绪结果
应用程序调用 read/recvfrom 读取对应 fd

从现象上看,应用程序还是会阻塞,因为它会阻塞在 select 这类等待接口上。但它和阻塞在单个 recvfrom 上不一样:前者等待的是"一组描述符里任意一个或多个就绪",后者只盯着一个描述符。

这个差异非常关键。网络服务端往往需要同时处理监听 socket、多个客户端连接、标准输入、定时事件等。多路转接让程序可以用一个执行流管理多个 IO 源,等哪个就绪就处理哪个。

需要注意的是,多路转接本身并不负责把数据拷贝到用户空间。它只告诉应用程序哪些描述符可以读写,真正读取数据仍然要调用 readrecvrecvfrom 等函数。

补充说明:继续深入网络服务端编程时,通常会进一步学习 selectpollepoll 等接口。它们都服务于"同时管理多个 IO 事件"这个方向,但具体能力和使用方式不同。

九、异步 IO:完成后再告诉我

异步 IO 和前面几种模型最大的区别在于通知时机。

在信号驱动 IO 中,内核通知应用程序:"数据已经准备好了,你可以来读了。"

在异步 IO 中,应用程序发起异步读取请求后立即返回,之后等待内核把"等待数据"和"拷贝数据"都处理完。等用户空间缓冲区已经拿到数据后,内核再通知应用程序:"这次 IO 已经完成了。"
内核 应用程序 内核 应用程序 #mermaid-svg-PVGyNVF191IwydRT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PVGyNVF191IwydRT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PVGyNVF191IwydRT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PVGyNVF191IwydRT .error-icon{fill:#552222;}#mermaid-svg-PVGyNVF191IwydRT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PVGyNVF191IwydRT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PVGyNVF191IwydRT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PVGyNVF191IwydRT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PVGyNVF191IwydRT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PVGyNVF191IwydRT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PVGyNVF191IwydRT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PVGyNVF191IwydRT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PVGyNVF191IwydRT .marker.cross{stroke:#333333;}#mermaid-svg-PVGyNVF191IwydRT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PVGyNVF191IwydRT p{margin:0;}#mermaid-svg-PVGyNVF191IwydRT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PVGyNVF191IwydRT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-PVGyNVF191IwydRT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PVGyNVF191IwydRT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-PVGyNVF191IwydRT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-PVGyNVF191IwydRT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-PVGyNVF191IwydRT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-PVGyNVF191IwydRT .sequenceNumber{fill:white;}#mermaid-svg-PVGyNVF191IwydRT #sequencenumber{fill:#333;}#mermaid-svg-PVGyNVF191IwydRT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-PVGyNVF191IwydRT .messageText{fill:#333;stroke:none;}#mermaid-svg-PVGyNVF191IwydRT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PVGyNVF191IwydRT .labelText,#mermaid-svg-PVGyNVF191IwydRT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-PVGyNVF191IwydRT .loopText,#mermaid-svg-PVGyNVF191IwydRT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-PVGyNVF191IwydRT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PVGyNVF191IwydRT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-PVGyNVF191IwydRT .noteText,#mermaid-svg-PVGyNVF191IwydRT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-PVGyNVF191IwydRT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PVGyNVF191IwydRT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PVGyNVF191IwydRT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PVGyNVF191IwydRT .actorPopupMenu{position:absolute;}#mermaid-svg-PVGyNVF191IwydRT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-PVGyNVF191IwydRT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PVGyNVF191IwydRT .actor-man circle,#mermaid-svg-PVGyNVF191IwydRT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-PVGyNVF191IwydRT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} aio_read()立即返回等待数据就绪拷贝数据到用户空间通知 IO 完成

所以异步 IO 的"异步"不只是等待阶段不阻塞,而是连数据拷贝阶段也不需要应用程序同步等待。应用程序提交请求后可以继续做其他事情,等收到完成通知后再处理结果。

很多人容易把 IO 多路转接和异步 IO 混为一谈。判断标准很简单:如果就绪后还要自己调用读取函数把数据取走,它就不是完整意义上的异步 IO;如果内核已经把数据拷贝完成后才通知应用程序,那才是异步 IO 的核心特征

十、同步、异步、阻塞、非阻塞不要混在一起

这组概念很容易绕。可以从两个维度区分:

维度 关注点 典型问题
阻塞 / 非阻塞 调用者在等待结果时会不会被挂起 当前线程还能不能继续执行
同步 / 异步 结果由谁推动、什么时候通知 调用返回时是否已经完成,后续是否通过通知或回调得知结果

阻塞和非阻塞描述的是"调用者的状态"。同步和异步描述的是"调用与结果之间的关系"。

举个直观例子:

  • 阻塞读取:调用 recvfrom 后一直等,直到数据拷贝完成;
  • 非阻塞读取:调用 recvfrom 后马上知道当前能不能读;
  • 信号驱动:数据就绪时通知应用程序,但数据还没自动进入用户缓冲区;
  • 异步 IO:提交请求后返回,内核完成拷贝后再通知。

还要特别区分:这里说的同步不是多线程里的互斥、条件变量那种"线程同步"。网络 IO 语境里的同步/异步,更强调调用者与被调用者之间的通信方式。

十一、常见问题与易错点

1. 非阻塞 IO 一定比阻塞 IO 快吗

不一定。非阻塞 IO 避免了线程卡在一次系统调用里,但如果程序用死循环频繁重试,会把 CPU 浪费在无效轮询上。非阻塞通常需要配合合理的事件管理方式,才更容易发挥价值。

2. read 返回 -1 就是严重错误吗

在非阻塞模式下不是。errnoEAGAINEWOULDBLOCK 时,通常表示当前没有数据可读。程序应该稍后再试,而不是直接退出。

3. IO 多路转接是不是异步 IO

不是。多路转接等待的是描述符"就绪",就绪后应用程序还要自己调用读取函数。异步 IO 的通知点是"数据拷贝已经完成"。

4. 信号驱动 IO 和异步 IO 的区别在哪里

信号驱动 IO 通知的是"可以开始读了";异步 IO 通知的是"已经读完了"。这句话基本可以抓住二者差异。

5. 阻塞 IO 有没有使用价值

当然有。阻塞 IO 简单、直观、容易调试,在简单程序或线程模型清晰的场景里非常实用。学习更复杂模型之前,先理解阻塞 IO 的执行过程反而更重要。

十二、扩展:高级 IO 能力都在解决什么

除了基础的 read / write / recv / send,Linux 里还有一些更高级的 IO 能力,例如:

能力 解决方向
非阻塞 IO 避免调用者长时间停在一次系统调用中
记录锁 控制多个进程对同一文件区域的访问
IO 多路转接 同时等待多个文件描述符的事件
readv / writev 分散读、集中写,减少多次调用
mmap 将文件映射到进程地址空间

这些能力背后的目标并不完全相同。有的控制等待方式,有的减少系统调用次数,有的改变文件访问方式。学习时不要急着把所有接口混在一起,而是先问清楚:它到底是在解决等待问题、拷贝问题,还是并发访问问题。

十三、总结

五种 IO 模型可以用"等待数据"和"数据拷贝"两件事串起来理解:

  • 阻塞 IO:等待和拷贝都让调用者停住;
  • 非阻塞 IO:数据没准备好就立即返回,需要应用程序重试;
  • 信号驱动 IO:数据就绪时通知应用程序,读取动作仍由应用程序发起;
  • IO 多路转接:用一个入口等待多个描述符就绪;
  • 异步 IO:等待和拷贝都由内核完成,完成后再通知应用程序。

真正写网络程序时,没有哪一种模型永远最好。简单程序用阻塞 IO 足够清晰;需要同时管理多个连接时,多路转接更合适;想让等待过程更可控,就会接触非阻塞和事件驱动;而异步 IO 则把完成通知推进到更彻底的层次。

把这条主线理解透,再去看 selectpollepoll、事件循环、协程调度,很多概念就不再是孤立的名词,而是围绕"如何更好地等待 IO"展开的不同方案。

相关推荐
IvorySQL1 小时前
IvorySQL HTAP 实时湖仓接入引擎
数据库·postgresql·开源·htap·ivorysql
谷雪_6581 小时前
Linux 网络命名空间:从内核原理到企业级容器网络架构全景实战
linux·网络·架构
忧郁的紫菜1 小时前
WCF+JSON+实体对象与WebService+DataSet效率大比拼
数据库·oracle·json
道友可好1 小时前
Claude Code 泄露源码里的 89 个秘密
前端·人工智能·后端
Tian_Hang2 小时前
Eclipse Ditto 的权限策略
java·服务器·前端·网络·ide·ubuntu·eclipse
IvorySQL2 小时前
PostgreSQL 技术日报|2026-07-05
数据库·postgresql
qq_163135752 小时前
Linux 【03- chgrp命令超详细教程】
linux
用户8356290780512 小时前
使用 Python 保护和取消保护 Excel 工作表和工作簿
后端·python
王老师青少年编程2 小时前
2026年6月GESP真题及题解(C++二级):完全平方数计数
c++·题解·真题·gesp·二级·2026年6月·完全平方数计数
zwenqiyu2 小时前
非线性字符串数据结构串讲
数据结构·c++·学习·算法