Linux 五种 IO 模型详解:从阻塞 IO 到非阻塞、信号驱动与异步 IO
网络编程里谈 IO,很多人一开始会把注意力放在 read、recv、recvfrom 这些函数上:调用它们,拿到数据,然后处理业务。真正写服务端程序时才会发现,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 为例,如果内核缓冲区没有数据,调用会返回失败,并设置类似 EWOULDBLOCK 或 EAGAIN 的错误码。应用程序可以稍后再试。
内核 应用程序 内核 应用程序 #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 |
获取或设置记录锁 |
设置非阻塞通常分两步:
- 用
F_GETFL取出原来的文件状态标志; - 在原有标志基础上追加
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 不一定代表程序出错。遇到 EAGAIN 或 EWOULDBLOCK 时,含义通常是"现在没有数据可读,请稍后再试"。这类返回值需要按正常分支处理。
七、信号驱动 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 源,等哪个就绪就处理哪个。
需要注意的是,多路转接本身并不负责把数据拷贝到用户空间。它只告诉应用程序哪些描述符可以读写,真正读取数据仍然要调用 read、recv、recvfrom 等函数。
补充说明:继续深入网络服务端编程时,通常会进一步学习
select、poll、epoll等接口。它们都服务于"同时管理多个 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 就是严重错误吗
在非阻塞模式下不是。errno 为 EAGAIN 或 EWOULDBLOCK 时,通常表示当前没有数据可读。程序应该稍后再试,而不是直接退出。
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 则把完成通知推进到更彻底的层次。
把这条主线理解透,再去看 select、poll、epoll、事件循环、协程调度,很多概念就不再是孤立的名词,而是围绕"如何更好地等待 IO"展开的不同方案。