UDP 与 Unix 域套接字
主线位置:和 TCP 一样在两条主线的两端。这些类型造出注册主线起点的那个非阻塞 fd,它们的读写方法是事件主线的最后一步。注册、事件本身和 TCP 没有区别;不同的是建 fd 的方式(
socket(SOCK_DGRAM)、AF_UNIX、socketpair、pipe2)和读写的单位(数据报不能只读一半)。
flowchart TB udp["UdpSocket::bind"] --> ns["new_socket"] ul["UnixListener::bind"] --> ns us["UnixStream::connect"] --> ns ud["UnixDatagram::bind"] --> ns pairs["UnixStream::pair 等"] --> sp["socketpair"] pnew["pipe::new"] --> p2["pipe2"] ns --> fd["非阻塞 + CLOEXEC 的 fd"] sp --> fd p2 --> fd fd --> iosrc["IoSource 包住 std 类型"] iosrc --> doio["读写经过 do_io"]
代码:net/udp.rs(780 行)、net/uds/ 三个文件(约 0.7k 行)、sys/unix/udp.rs(36 行)、sys/unix/uds/(约 0.4k 行)、sys/unix/pipe.rs(622 行)。公开文件里一大半是文档和示例。
结构和 TCP 完全一样:每个类型一个 inner: IoSource<std 类型> 字段,Deref 到 std,读写经过 do_io。这些在 TCP 和 Source 与 IoSource 讲过,本页只讲各类型自己的部分。除了 pair 用的 socketpair,Unix 上的 socket 都从 new_socket 出生,Linux 上一次 socket(2) 带 SOCK_NONBLOCK|SOCK_CLOEXEC,macOS 上补 SO_NOSIGPIPE 和两次 fcntl,逐行解释见 TCP 一页。
UdpSocket
UdpSocket::bind 调 sys::udp::bind,只有两步:
pub fn bind(addr: SocketAddr) -> io::Result<net::UdpSocket> {
let fd = new_ip_socket(addr, libc::SOCK_DGRAM)?;
let socket = unsafe { net::UdpSocket::from_raw_fd(fd) };
let (raw_addr, raw_addr_length) = socket_addr(&addr);
syscall!(bind(fd, raw_addr.as_ptr(), raw_addr_length))?;
Ok(socket)
}
和 TcpListener::bind 比,没有 SO_REUSEADDR,也没有 listen。方法分两类:
| 方法 | 实现 | 说明 |
|---|---|---|
send_to、recv_from、peek_from | do_io + std | 每次一个数据报 |
send、recv、peek | do_io + std | 先 connect 过才能用 |
connect | self.inner.connect(addr),std | 不经过 do_io |
广播、组播、TTL、take_error | std | 选项 |
only_v6 | mio 自己 getsockopt(IPV6_V6ONLY) |
UDP 的 connect 不握手,只是在内核里记下默认的对端地址:之后 send 发往这里,recv 只收来自这里的数据报。它不会阻塞,所以 mio 直接用 std 的版本,不像 TCP 那样要自己写一个非阻塞的 connect。文档提到它也「may return a WouldBlock」,原因同 TCP:「there are insufficient entries in the routing cache」。
数据报只能读一次。net 模块的文档专门写了缓冲区太小时的平台差异:Linux、FreeBSD、macOS 这些 Unix「will simply fill the buffer and return the amount of bytes written」,剩下的部分丢掉;Windows 返回 WSAEMSGSIZE。「Mio does not change the value (either ok or error) returned by the OS」,建议总是用足够大的缓冲区。
实验:三个数据报,一个事件
src/bin/udp.rs 有三种情况,每种用一个新的 Poll:
a连发三个数据报给b,b只poll一次,然后recv_from到WouldBlock;a.connect(d)之后send,d先peek再recv;c连到一个没人监听的 UDP 端口(bind后立刻 drop 的 std socket),send一个数据报,c只注册了READABLE。
use std::io::ErrorKind;
use std::time::Duration;
use exp_mio_net::{bools, raw};
use mio::net::UdpSocket;
use mio::{Events, Interest, Poll, Token};
fn show(poll: &mut Poll, events: &mut Events) -> std::io::Result<()> {
poll.poll(events, Some(Duration::from_secs(1)))?;
for e in events.iter() {
println!("event: {} => {}", raw(e), bools(e));
}
Ok(())
}
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let mut events = Events::with_capacity(8);
let mut buf = [0u8; 64];
// 1. three datagrams before one poll
let mut poll = Poll::new()?;
let a = UdpSocket::bind("127.0.0.1:0".parse().unwrap())?;
let mut b = UdpSocket::bind("127.0.0.1:0".parse().unwrap())?;
poll.registry().register(&mut b, Token(1), Interest::READABLE)?;
for msg in [&b"one"[..], b"two", b"three"] {
a.send_to(msg, b.local_addr()?)?;
}
show(&mut poll, &mut events)?;
loop {
match b.recv_from(&mut buf) {
Ok((n, from)) => println!("recv_from: {:?} from a: {}", String::from_utf8_lossy(&buf[..n]), from == a.local_addr()?),
Err(e) if e.kind() == ErrorKind::WouldBlock => {
println!("recv_from: WouldBlock");
break;
}
Err(e) => return Err(e),
}
}
// 2. connect + send, peek then recv
let mut poll = Poll::new()?;
let mut d = UdpSocket::bind("127.0.0.1:0".parse().unwrap())?;
poll.registry().register(&mut d, Token(2), Interest::READABLE)?;
a.connect(d.local_addr()?)?;
a.send(b"hi")?;
show(&mut poll, &mut events)?;
let n = d.peek(&mut buf)?;
println!("peek: {:?}", String::from_utf8_lossy(&buf[..n]));
let n = d.recv(&mut buf)?;
println!("recv: {:?}", String::from_utf8_lossy(&buf[..n]));
println!("recv again: {:?}", d.recv(&mut buf).map_err(|e| e.kind()));
// 3. connected socket, peer port closed
let mut poll = Poll::new()?;
let closed = std::net::UdpSocket::bind("127.0.0.1:0")?.local_addr()?;
let mut c = UdpSocket::bind("127.0.0.1:0".parse().unwrap())?;
c.connect(closed)?;
poll.registry().register(&mut c, Token(3), Interest::READABLE)?;
c.send(b"anyone?")?;
show(&mut poll, &mut events)?;
println!("take_error = {:?}", c.take_error()?);
println!("recv = {:?}", c.recv(&mut buf).map_err(|e| e.kind()));
Ok(())
}
第 3 种情况留给页末的动脑筋,这里先贴前两种的输出(各平台输出的前 9 行)。M2 Max、macOS、rustc 1.99.0:
os=macos
event: filter=READ flags=CLEAR fflags=0 data=3 udata=0x1 => readable
recv_from: "one" from a: true
recv_from: "two" from a: true
recv_from: "three" from a: true
recv_from: WouldBlock
event: filter=READ flags=CLEAR fflags=0 data=2 udata=0x2 => readable
peek: "hi"
recv: "hi"
recv again: Err(WouldBlock)
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0:
os=linux
event: events=IN data=0x1 => readable
recv_from: "one" from a: true
recv_from: "two" from a: true
recv_from: "three" from a: true
recv_from: WouldBlock
event: events=IN data=0x2 => readable
peek: "hi"
recv: "hi"
recv again: Err(WouldBlock)
Linux 跑了 3 次,输出逐字节相同。macOS 跑了 10 次,只有第 2 行的 data 不同:7 次是 data=3,3 次是 data=6,其余各行相同。EVFILT_READ 的 data 是 kqueue 报告的可读字节数,3 是第一个数据报 one 的长度,6 是 one 加 two:取事件那一刻,后面的数据报不一定都到了。不管哪种,事件都只有一个,recv_from 循环都读到了全部三个。
Linux 上前两种情况的系统调用(strace -e trace=socket,bind,connect,epoll_ctl,epoll_pwait,recvfrom,sendto,getsockopt,write 输出的前 32 行):
4 write(1, "os=linux\n", 9) = 9
4 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 4
4 bind(4, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5
4 bind(5, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 epoll_ctl(3, EPOLL_CTL_ADD, 5, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = 0
4 sendto(4, "one", 3, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(51232), sin_addr=inet_addr("127.0.0.1")}, 16) = 3
4 sendto(4, "two", 3, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(51232), sin_addr=inet_addr("127.0.0.1")}, 16) = 3
4 sendto(4, "three", 5, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(51232), sin_addr=inet_addr("127.0.0.1")}, 16) = 5
4 epoll_pwait(3, [{events=EPOLLIN, data=0x1}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x1 => re"..., 39) = 39
4 recvfrom(5, "one", 64, 0, {sa_family=AF_INET, sin_port=htons(56327), sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 3
4 write(1, "recv_from: \"one\" from a: true\n", 30) = 30
4 recvfrom(5, "two", 64, 0, {sa_family=AF_INET, sin_port=htons(56327), sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 3
4 write(1, "recv_from: \"two\" from a: true\n", 30) = 30
4 recvfrom(5, "three", 64, 0, {sa_family=AF_INET, sin_port=htons(56327), sin_addr=inet_addr("127.0.0.1")}, [128 => 16]) = 5
4 write(1, "recv_from: \"three\" from a: true\n", 32) = 32
4 recvfrom(5, 0xffffdcfa4fc0, 64, 0, 0xffffdcfa4ef8, [128]) = -1 EAGAIN (Resource temporarily unavailable)
4 write(1, "recv_from: WouldBlock\n", 22) = 22
4 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 7
4 bind(7, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 epoll_ctl(6, EPOLL_CTL_ADD, 7, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x2}) = 0
4 connect(4, {sa_family=AF_INET, sin_port=htons(40476), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 sendto(4, "hi", 2, MSG_NOSIGNAL, NULL, 0) = 2
4 epoll_pwait(6, [{events=EPOLLIN, data=0x2}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x2 => re"..., 39) = 39
4 recvfrom(7, "hi", 64, MSG_PEEK, NULL, NULL) = 2
4 write(1, "peek: \"hi\"\n", 11) = 11
4 recvfrom(7, "hi", 64, 0, NULL, NULL) = 2
4 write(1, "recv: \"hi\"\n", 11) = 11
4 recvfrom(7, 0xffffdcfa4fc0, 64, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
4 write(1, "recv again: Err(WouldBlock)\n", 28) = 28
- 第 2 到 5 行:
bind就是socket(SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK)加bind,没有setsockopt。 - 第 7 到 10 行:三个数据报都在
epoll_pwait之前发出,epoll_pwait只返回一个事件。边沿触发只报「从不可读变成可读」那一次,之后再来的数据报不会让它再变一次。所以recv_from必须循环到EAGAIN(第 18 行);只读一个就回去poll,另外两个会一直留在接收队列里,直到下一个数据报到达再触发一次事件。 - 第 23 行:UDP 的
connect立刻返回 0,没有EINPROGRESS。之后的send是sendto(..., NULL, 0),不带地址。 - 第 27 行:
peek是带MSG_PEEK的recvfrom,数据留在队列里,所以第 29 行的recv读到同样的hi。peek也经过do_io:它可能返回WouldBlock,在poll(2)后备上这时要重新武装,TCP 的peek曾经漏掉这一步(见 TCP 的冷知识)。
第 3 种情况:c 已经 connect 到一个没人监听的本机端口,只注册了 READABLE,发出一个数据报后 poll 一次。没有任何数据报会到达 c。Linux 和 macOS 上,这次 poll 返回事件吗?如果返回,is_readable() 是真还是假?take_error() 又返回什么?
Unix 域套接字
三个类型:UnixListener、UnixStream、UnixDatagram,只在 cfg(unix) 上存在。mio 自己写的是 sys/unix/uds/ 下的四件事:把地址转成 sockaddr_un、建 socket 并 bind / connect、accept、socketpair。
地址:std 的 SocketAddr
每个按路径的构造函数都有一个 _addr 版本,路径版只是先转成 std::os::unix::net::SocketAddr:
pub fn bind<P: AsRef<Path>>(path: P) -> io::Result<UnixListener> {
let addr = SocketAddr::from_pathname(path)?;
UnixListener::bind_addr(&addr)
}
unix_addr 把它填进一个清零的 sockaddr_un。地址有三种:路径名、Linux 和 Android 上的抽象名字(sun_path 以一个 0 字节开头,不对应文件系统里的文件)、无名地址(空)。长度的算法:
let mut addrlen = path_offset(&sockaddr) + addr.len();
// +1 for null byte at the end of the path, not needed for abstract
// namespaces (which start with a null byte).
match addr.first() {
Some(&0) | None => {}
Some(_) => addrlen += 1,
}
路径名算上结尾的 0,抽象名字不算。下面实验的 strace 里,/tmp/mio-uds-4/l.sock 是 21 个字节,bind 的长度是 24(2 字节的 sun_family 加 21 加 1);抽象名字 mio-demo 是 8 个字节,长度是 11(2 加开头的 0 加 8)。
from_pathname 只接受路径,所以抽象名字只能用 _addr 版本,由 SocketAddr::from_abstract_name(Linux 上在 std::os::linux::net::SocketAddrExt 里)构造。
0.8 有一个 mio::net::SocketAddr,UnixListener::accept、local_addr 等返回它。#1527(issue)提出换成 std 的版本:「It's the same type. It's only implemented by us because we can't create std::os::unix::net::SocketAddr.」CHANGELOG 1.0 开头说,MSRV 提到 1.70 之后,mio 才能实现 I/O safety trait 并「replace SocketAddr with the version found in the standard library」。
替换在 #1749(PR)里完成,1.0.0 发布。提交说明:「With this change the path based API to create the sockets ({UnixDatagram,UnixListener}::bind and UnixStream::connect) will no longer work with abstract namespaces. For that the _addr variant must be used」,同时新增了 UnixDatagram::bind_addr。CHANGELOG 1.0 的「Removed」一节给这条改动挂的链接是 #1760,那是一个 Windows named pipe 的 PR;按 GitHub 上提交和 PR 的关联,Replace Unix SocketAddr with std version(72a80cb)属于 #1749。
同一个 PR 里还有一个提交「Work around Darwn (macOS) weirdness」:「It seems to return a length of 16 and an all zero address for unnamed Unix addresses.」它就是下一节 accept 里那个 if sockaddr.sun_path[0] == 0 判断。
UnixListener:bind 与 accept
bind_addr 是 new_socket(AF_UNIX, SOCK_STREAM)、bind、listen。backlog 也和 std 一致,但抄的是 std UnixListener 的值:Linux、FreeBSD、OpenBSD 和 Apple 平台上是 -1,Windows、Redox、ESP-IDF、Horizon 上是 128,其他平台 libc::SOMAXCONN。和 TCP 的 128 不同,见 TCP 的冷知识。没有 SO_REUSEADDR。
accept 的前半段和 TCP 的一样分两条路:Linux 用 accept4(SOCK_NONBLOCK | SOCK_CLOEXEC),macOS 用 accept 加 FD_CLOEXEC。后半段把内核填回来的 sockaddr_un 转成 SocketAddr:
#[allow(unused_mut)] // See below.
let mut path_len = socklen as usize - path_offset(&sockaddr);
// On FreeBSD and Darwin, it returns a length of 14/16, but an unnamed (all
// zero) address. Map that to a length of 0 to match other OS.
if sockaddr.sun_path[0] == 0 {
path_len = 0;
}
// SAFETY: going from i8 to u8 is fine in this context.
let mut path =
unsafe { &*(&sockaddr.sun_path[..path_len] as *const [libc::c_char] as *const [u8]) };
// Remove last null as `SocketAddr::from_pathname` doesn't accept it.
if let Some(0) = path.last() {
path = &path[..path.len() - 1];
}
let address = SocketAddr::from_pathname(Path::new(OsStr::from_bytes(path)))?;
Ok((socket, address))
客户端通常不 bind,连上来的是无名地址。Linux 对无名地址只填 sun_family,长度是 2;注释说 FreeBSD 和 Darwin 返回 14 或 16 的长度,sun_path 却全是 0。这段代码要两边都转成空路径:std 的 from_pathname 碰到含 0 字节的路径会报错(「paths must not contain interior null bytes」),空路径则得到一个无名地址。sun_path[0] == 0 时把长度归零,就是在处理 macOS 这种情况。后面去掉结尾 0 的那两行是 1.0.1 的 #1817(PR,「Remove last null from UDS address」)加的,处理带结尾 0 的路径名地址。
macOS 的这个行为用一个不经过 mio、直接调 libc::accept 的小程序 src/bin/uds_accept_len.rs 验证:
use std::os::fd::AsRawFd;
// What does a raw accept(2) on a Unix listener put into sockaddr_un for an
// unbound (unnamed) client? This is what mio's uds::listener::accept has to parse.
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let path = std::env::temp_dir().join(format!("mio-len-{}.sock", std::process::id()));
let listener = std::os::unix::net::UnixListener::bind(&path)?;
let _client = std::os::unix::net::UnixStream::connect(&path)?;
let mut sa: libc::sockaddr_un = unsafe { std::mem::zeroed() };
let mut len = size_of::<libc::sockaddr_un>() as libc::socklen_t;
println!("socklen in = {len}");
let fd = unsafe { libc::accept(listener.as_raw_fd(), &mut sa as *mut _ as *mut libc::sockaddr, &mut len) };
assert!(fd >= 0);
let offset = (&sa.sun_path as *const _ as usize) - (&sa as *const _ as usize);
println!("socklen out = {len}, path offset = {offset}, sun_path[0] = {}", sa.sun_path[0]);
unsafe { libc::close(fd) };
std::fs::remove_file(&path)?;
Ok(())
}
macOS 和 Linux 各跑 3 次,输出相同:
os=macos
socklen in = 106
socklen out = 16, path offset = 2, sun_path[0] = 0
os=linux
socklen in = 110
socklen out = 2, path offset = 2, sun_path[0] = 0
macOS 返回 16,减去偏移 2 是 14 个 0 字节。没有那个判断,这 14 个 0 去掉结尾一个后还剩 13 个,交给 from_pathname 就是一个错误,macOS 上每次 accept 一个没 bind 的客户端都会失败。
UnixStream:connect 与 pair
connect_addr 和 TCP 的 connect 一样,把 EINPROGRESS 当成成功:
pub(crate) fn connect_addr(address: &SocketAddr) -> io::Result<net::UnixStream> {
let fd = new_socket(libc::AF_UNIX, libc::SOCK_STREAM)?;
let socket = unsafe { net::UnixStream::from_raw_fd(fd) };
let (unix_address, addrlen) = unix_addr(address);
let sockaddr = &unix_address as *const libc::sockaddr_un as *const libc::sockaddr;
match syscall!(connect(fd, sockaddr, addrlen)) {
Ok(_) => {}
Err(ref err) if err.raw_os_error() == Some(libc::EINPROGRESS) => {}
Err(e) => return Err(e),
}
Ok(socket)
}
但 Unix 域套接字的 connect 不需要等网络。下面实验的 strace 里,connect 直接返回 0,peer_addr 立刻就有值。连不上时(路径不存在、对方 backlog 满了)它直接返回错误,不会先返回 EINPROGRESS 再在可写事件里报。
0.8.2 及以前,这里匹配的是 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {},EAGAIN 也被吞掉,函数返回一个其实没连上的 UnixStream。#1560(issue,「UnixStream::connect() returns unconnected socket on Linux」)说明:「This happens when the server's backlog is full. In this situation, on linux, connect() returns EAGAIN, which means that you have to call connect() again at a later time.」修复 #1564(PR)引用 Linux 手册「EAGAIN For nonblocking UNIX domain sockets, the socket is nonblocking, and the connection cannot be completed immediately.」,提交说明是「Previously the code incorrectly assumed that the connection was now in progress, which is incorrect. If that were the case EINPROGRESS would/should be returned by the OS」,改成现在只放过 EINPROGRESS,0.8.3 发布。UnixStream::connect 的文档从此写着「This may return a WouldBlock in which case the socket connection cannot be completed immediately. Usually it means the backlog is full.」
现在的行为可以直接复现。src/bin/uds_backlog.rs 把 listener 的 backlog 用 libc::listen(fd, 0) 改小,从不 accept,一直 connect 到第一次失败:
use std::os::fd::AsRawFd;
use mio::net::{UnixListener, UnixStream};
// Shrink the listener's backlog to 0, never accept, connect until the first failure.
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let path = std::env::temp_dir().join(format!("mio-bl-{}.sock", std::process::id()));
let listener = UnixListener::bind(&path)?;
assert_eq!(unsafe { libc::listen(listener.as_raw_fd(), 0) }, 0);
let mut keep = Vec::new();
for i in 1..=1000 {
match UnixStream::connect(&path) {
Ok(s) => keep.push(s),
Err(e) => {
println!("connect #{i}: Err({:?}) {e}", e.kind());
break;
}
}
}
println!("{} connect(s) returned Ok", keep.len());
std::fs::remove_file(&path)?;
Ok(())
}
Linux(3 次相同):
os=linux
connect #2: Err(WouldBlock) Resource temporarily unavailable (os error 11)
1 connect(s) returned Ok
strace 里是 listen(3, -1)(mio 的 bind)、listen(3, 0)(实验改的),然后第二次 connect 返回 EAGAIN:
4 listen(3, -1) = 0
4 listen(3, 0) = 0
4 connect(4, {sa_family=AF_UNIX, sun_path="/tmp/mio-bl-4.sock"}, 21) = 0
4 connect(5, {sa_family=AF_UNIX, sun_path="/tmp/mio-bl-4.sock"}, 21) = -1 EAGAIN (Resource temporarily unavailable)
4 +++ exited with 0 +++
macOS(3 次相同):
os=macos
connect #129: Err(ConnectionRefused) Connection refused (os error 61)
128 connect(s) returned Ok
macOS 上前 128 次都成功(这台机器的 kern.ipc.somaxconn 也是 128),第 129 次失败,错误是 ConnectionRefused,不是 WouldBlock。所以「backlog 满」在两个平台上是两种错误;两种都由 connect 直接返回,EINPROGRESS 之外的错误 mio 都原样交出来。
UnixStream::pair 和 UnixDatagram::pair 共用 pair:Linux 上 socketpair(AF_UNIX, type | SOCK_NONBLOCK | SOCK_CLOEXEC);macOS 上普通的 socketpair,再对两个 fd 各做两次 fcntl。注释解释了为什么先把两个 fd 包成 std 类型再 fcntl:「If a fnctl fails after the sockets have been created, the file descriptors will leak. Creating pair above ensures that if there is an error, the file descriptors are closed.」这条路不经过 new_socket,所以 macOS 上 pair 出来的 socket 没有设 SO_NOSIGPIPE。
UnixDatagram
bind_addr 是 unbound(new_socket(AF_UNIX, SOCK_DGRAM))加一次 bind。connect 和 UDP 一样直接用 std 的版本,只记下默认对端。recv_from 和 send_to 经过 do_io 转给 std,recv_from 返回的是 std 的 SocketAddr:发送方没 bind 时是无名地址,bind 过时是它的路径。
实验:三种 Unix 域套接字各走一轮
src/bin/uds.rs 在临时目录里依次做三件事:UnixListener 加 UnixStream,accept 一次、收一句话;一个 bind 过的 UnixDatagram 分别收一个没 bind 和一个 bind 过的发送方的数据报;Linux 上再用抽象名字建一个 listener:
use std::io::{Read, Write};
use std::time::Duration;
use exp_mio_net::{bools, raw};
use mio::net::{UnixDatagram, UnixListener, UnixStream};
use mio::{Events, Interest, Poll, Token};
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let dir = std::env::temp_dir().join(format!("mio-uds-{}", std::process::id()));
std::fs::create_dir(&dir)?;
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(8);
// 1. listener + stream
let path = dir.join("l.sock");
let mut listener = UnixListener::bind(&path)?;
poll.registry().register(&mut listener, Token(0), Interest::READABLE)?;
let mut client = UnixStream::connect(&path)?;
println!("client peer_addr = {:?}", client.peer_addr()?.as_pathname().map(|p| p.file_name().unwrap().to_owned()));
poll.poll(&mut events, Some(Duration::from_secs(1)))?;
for e in &events {
println!("event: {} => {}", raw(e), bools(e));
}
let (mut server, addr) = listener.accept()?;
println!("accept addr = {addr:?}, is_unnamed = {}", addr.is_unnamed());
println!("accept again = {:?}", listener.accept().map(|_| ()).map_err(|e| e.kind()));
poll.registry().register(&mut server, Token(1), Interest::READABLE)?;
client.write_all(b"ping")?;
poll.poll(&mut events, Some(Duration::from_secs(1)))?;
for e in &events {
println!("event: {} => {}", raw(e), bools(e));
}
let mut buf = [0u8; 64];
let n = server.read(&mut buf)?;
println!("server read {:?}", String::from_utf8_lossy(&buf[..n]));
// 2. datagram: bound receiver, one unbound and one bound sender
let rpath = dir.join("r.sock");
let mut r = UnixDatagram::bind(&rpath)?;
poll.registry().register(&mut r, Token(2), Interest::READABLE)?;
let unbound = UnixDatagram::unbound()?;
let bound = UnixDatagram::bind(dir.join("s.sock"))?;
unbound.send_to(b"from unbound", &rpath)?;
bound.send_to(b"from bound", &rpath)?;
poll.poll(&mut events, Some(Duration::from_secs(1)))?;
for e in &events {
println!("event: {} => {}", raw(e), bools(e));
}
while let Ok((n, from)) = r.recv_from(&mut buf) {
println!(
"recv_from {:?}: unnamed={} path={:?}",
String::from_utf8_lossy(&buf[..n]),
from.is_unnamed(),
from.as_pathname().map(|p| p.file_name().unwrap().to_owned())
);
}
// 3. Linux only: abstract namespace through bind_addr
#[cfg(target_os = "linux")]
{
use std::os::linux::net::SocketAddrExt;
let addr = std::os::unix::net::SocketAddr::from_abstract_name(b"mio-demo")?;
let l = UnixListener::bind_addr(&addr)?;
println!("abstract local_addr = {:?}", l.local_addr()?);
}
std::fs::remove_dir_all(&dir)?;
Ok(())
}
M2 Max、macOS、rustc 1.99.0:
os=macos
client peer_addr = Some("l.sock")
event: filter=READ flags=CLEAR fflags=0 data=1 udata=0x0 => readable
accept addr = (unnamed), is_unnamed = true
accept again = Err(WouldBlock)
event: filter=READ flags=CLEAR fflags=0 data=4 udata=0x1 => readable
server read "ping"
event: filter=READ flags=CLEAR fflags=0 data=22 udata=0x2 => readable
recv_from "from unbound": unnamed=true path=None
recv_from "from bound": unnamed=false path=Some("s.sock")
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0:
os=linux
client peer_addr = Some("l.sock")
event: events=IN data=0x0 => readable
accept addr = (unnamed), is_unnamed = true
accept again = Err(WouldBlock)
event: events=IN data=0x1 => readable
server read "ping"
event: events=IN data=0x2 => readable
recv_from "from unbound": unnamed=true path=None
recv_from "from bound": unnamed=false path=Some("s.sock")
abstract local_addr = "mio-demo" (abstract)
两个平台各跑 3 次,输出逐字节相同。Linux 上的系统调用(strace -e trace=socket,bind,listen,connect,accept4,epoll_ctl,epoll_pwait,recvfrom,sendto,write):
4 write(1, "os=linux\n", 9) = 9
4 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 4
4 bind(4, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/l.sock"}, 24) = 0
4 listen(4, -1) = 0
4 epoll_ctl(3, EPOLL_CTL_ADD, 4, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0}) = 0
4 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 5
4 connect(5, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/l.sock"}, 24) = 0
4 write(1, "client peer_addr = Some(\"l.sock\""..., 34) = 34
4 epoll_pwait(3, [{events=EPOLLIN, data=0}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x0 => re"..., 39) = 39
4 accept4(4, {sa_family=AF_UNIX}, [110 => 2], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6
4 write(1, "accept addr = (unnamed), is_unna"..., 43) = 43
4 accept4(4, 0xffffd9088cc0, [110], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
4 write(1, "accept again = Err(WouldBlock)\n", 31) = 31
4 epoll_ctl(3, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = 0
4 sendto(5, "ping", 4, MSG_NOSIGNAL, NULL, 0) = 4
4 epoll_pwait(3, [{events=EPOLLIN, data=0x1}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x1 => re"..., 39) = 39
4 recvfrom(6, "ping", 64, 0, NULL, NULL) = 4
4 write(1, "server read \"ping\"\n", 19) = 19
4 socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 7
4 bind(7, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/r.sock"}, 24) = 0
4 epoll_ctl(3, EPOLL_CTL_ADD, 7, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x2}) = 0
4 socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 8
4 socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 9
4 bind(9, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/s.sock"}, 24) = 0
4 sendto(8, "from unbound", 12, MSG_NOSIGNAL, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/r.sock"}, 24) = 12
4 sendto(9, "from bound", 10, MSG_NOSIGNAL, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/r.sock"}, 24) = 10
4 epoll_pwait(3, [{events=EPOLLIN, data=0x2}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x2 => re"..., 39) = 39
4 recvfrom(7, "from unbound", 64, 0, 0xffffd9088d30, [110 => 0]) = 12
4 write(1, "recv_from \"from unbound\": unname"..., 49) = 49
4 recvfrom(7, "from bound", 64, 0, {sa_family=AF_UNIX, sun_path="/tmp/mio-uds-4/s.sock"}, [110 => 24]) = 10
4 write(1, "recv_from \"from bound\": unnamed="..., 58) = 58
4 recvfrom(7, 0xffffd9088ee0, 64, 0, 0xffffd9088d30, [110]) = -1 EAGAIN (Resource temporarily unavailable)
4 socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 10
4 bind(10, {sa_family=AF_UNIX, sun_path=@"mio-demo"}, 11) = 0
4 listen(10, -1) = 0
4 write(1, "abstract local_addr = \"mio-demo\""..., 44) = 44
4 +++ exited with 0 +++
- 第 4、38 行:Unix 域 listener 的 backlog 是 -1,TCP 是 128。
- 第 7 行:
connect直接返回 0,不是EINPROGRESS;第 8 行peer_addr马上有值。 - 第 11 行:客户端没
bind,accept4填回来的长度是 2,只有sun_family。mio 把它转成无名地址,对应输出里的(unnamed)。macOS 上内核填的是 16 字节的全 0,转出来也是(unnamed),上一节的那个判断起了作用。 - 第 31、33 行:数据报的来源地址。没
bind的发送方,内核填回的长度是 0;bind过的,填的是它的路径。 - 第 37 行:抽象名字,strace 用
@表示开头的 0 字节,长度 11 里没有结尾的 0。 - macOS 上的
data:listener 的data=1是待accept的连接数,数据报那次data=22正好是两个数据报的字节数之和(12 加 10),两个数据报一个事件。
unix::pipe
pipe::new 要开 os-ext feature,路径是 mio::unix::pipe(lib.rs 里把 sys::pipe 的 new、Sender、Receiver 重新导出)。它调 new_raw:Linux 等平台用 pipe2(fds, O_CLOEXEC | O_NONBLOCK);macOS 等没有 pipe2 的平台用 pipe,再对两个 fd 各做 F_SETFL O_NONBLOCK 和 F_SETFD FD_CLOEXEC,任何一次失败就把两个 fd 都关掉。new_raw 不受 os-ext 控制,因为 pipe 版的 Waker 也用它(见 Waker)。
pub fn new() -> io::Result<(Sender, Receiver)> {
let fds = new_raw()?;
// SAFETY: `new_raw` initialised the `fds` above.
let r = unsafe { Receiver::from_raw_fd(fds[0]) };
let w = unsafe { Sender::from_raw_fd(fds[1]) };
Ok((w, r))
}
fds[0] 是读端、fds[1] 是写端,返回的顺序是 (Sender, Receiver)。Sender 和 Receiver 的字段都是 inner: IoSource<File>:std 没有专门的 pipe 类型,用 File 承载 fd。Sender 只实现 Write,Receiver 只实现 Read,都经过 do_io。文档写了关闭的信号:Receiver 的事件 is_read_closed() 为真表示写端没了,Sender 的事件 is_write_closed() 为真表示读端没了;两个平台上具体是哪些位,见 Token、Interest 与 Event 的实验第 7、8 项。
pipe 和 socket 有一处不同:Sender 和 Receiver 有自己的 set_nonblocking。原因在 From<ChildStdin>、From<ChildStdout>、From<ChildStderr> 这三个转换上,文档注明「The underlying pipe is not set to non-blocking.」子进程的 pipe 是 std 建的,阻塞;File 又没有 set_nonblocking 方法,所以 mio 补了一个,在 Linux 和 macOS 上是一次 ioctl(FIONBIO)(AIX、illumos、Solaris、Vita 上改用 fcntl 读改写 O_NONBLOCK)。
问: TcpStream::from_std 也会拿到阻塞的 socket,为什么 mio 不给 TcpStream 加一个 set_nonblocking,而只给 pipe 加?
答: socket 在转换之前就有办法设:std::net::TcpStream、UnixStream 等都自带 set_nonblocking,from_std 的文档让调用方先调它(见 TCP 的 from_std 实验)。pipe 这边,ChildStdout 转进来之前没有对应的 std 方法,转进来之后内部是 File,也没有。不在 mio 这一侧补一个,用户只能自己 fcntl。两边的约定其实一样:凡是从外面接进来的 fd,非阻塞都由用户负责,mio 只在自己建的 fd 上保证非阻塞。
实验:一个 pipe,一个子进程
src/bin/pipe.rs 先用 pipe::new 写一次、读一次;再起一个 /bin/echo 子进程,把它的 stdout 转成 Receiver,读回 set_nonblocking(true) 前后的标志,等子进程退出后读到 EOF:
use std::io::{ErrorKind, Read, Write};
use std::os::fd::AsRawFd;
use std::process::{Command, Stdio};
use std::time::Duration;
use exp_mio_net::{bools, fd_flags, raw};
use mio::unix::pipe;
use mio::{Events, Interest, Poll, Token};
fn main() -> std::io::Result<()> {
println!("os={}", std::env::consts::OS);
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(8);
// 1. pipe::new: one write, one readable event
let (mut tx, mut rx) = pipe::new()?;
println!("tx: {} rx: {}", fd_flags(tx.as_raw_fd()), fd_flags(rx.as_raw_fd()));
poll.registry().register(&mut rx, Token(0), Interest::READABLE)?;
tx.write_all(b"hello")?;
poll.poll(&mut events, Some(Duration::from_secs(1)))?;
for e in &events {
println!("event: {} => {}", raw(e), bools(e));
}
let mut buf = [0u8; 64];
println!("read = {:?}", rx.read(&mut buf));
println!("read again = {:?}", rx.read(&mut buf).map_err(|e| e.kind()));
// 2. a child's stdout: blocking until set_nonblocking(true)
let mut child = Command::new("/bin/echo").arg("from child").stdout(Stdio::piped()).spawn()?;
let mut out = pipe::Receiver::from(child.stdout.take().unwrap());
println!("child stdout before: {}", fd_flags(out.as_raw_fd()));
out.set_nonblocking(true)?;
println!("child stdout after: {}", fd_flags(out.as_raw_fd()));
poll.registry().register(&mut out, Token(1), Interest::READABLE)?;
child.wait()?;
poll.poll(&mut events, Some(Duration::from_secs(1)))?;
for e in &events {
println!("event: {} => {}", raw(e), bools(e));
}
loop {
match out.read(&mut buf) {
Ok(0) => {
println!("read = Ok(0): EOF");
break;
}
Ok(n) => println!("read {:?}", String::from_utf8_lossy(&buf[..n])),
Err(e) if e.kind() == ErrorKind::WouldBlock => {
println!("read = WouldBlock");
break;
}
Err(e) => return Err(e),
}
}
Ok(())
}
M2 Max、macOS、rustc 1.99.0:
os=macos
tx: nonblock=true cloexec=true rx: nonblock=true cloexec=true
event: filter=READ flags=CLEAR fflags=0 data=5 udata=0x0 => readable
read = Ok(5)
read again = Err(WouldBlock)
child stdout before: nonblock=false cloexec=true
child stdout after: nonblock=true cloexec=true
event: filter=READ flags=EOF|CLEAR fflags=0 data=11 udata=0x1 => readable read_closed
read "from child\n"
read = Ok(0): EOF
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0:
os=linux
tx: nonblock=true cloexec=true rx: nonblock=true cloexec=true
event: events=IN data=0x0 => readable
read = Ok(5)
read again = Err(WouldBlock)
child stdout before: nonblock=false cloexec=true
child stdout after: nonblock=true cloexec=true
event: events=IN|HUP data=0x1 => readable read_closed write_closed
read "from child\n"
read = Ok(0): EOF
两个平台各跑 3 次,输出逐字节相同。Linux 上的系统调用(strace -f -e trace=pipe2,ioctl,epoll_ctl,epoll_pwait,read,write;5 是子进程):
4 write(1, "os=linux\n", 9) = 9
4 pipe2([4, 5], O_NONBLOCK|O_CLOEXEC) = 0
4 write(1, "tx: nonblock=true cloexec=true "..., 63) = 63
4 epoll_ctl(3, EPOLL_CTL_ADD, 4, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0}) = 0
4 write(5, "hello", 5) = 5
4 epoll_pwait(3, [{events=EPOLLIN, data=0}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN data=0x0 => re"..., 39) = 39
4 read(4, "hello", 64) = 5
4 write(1, "read = Ok(5)\n", 13) = 13
4 read(4, 0xffffcfe06880, 64) = -1 EAGAIN (Resource temporarily unavailable)
4 write(1, "read again = Err(WouldBlock)\n", 29) = 29
4 pipe2([6, 7], O_CLOEXEC) = 0
4 pipe2([8, 9], O_CLOEXEC) = 0
4 read(8, "", 4) = 0
4 write(1, "child stdout before: nonblock=fa"..., 49) = 49
4 ioctl(6, FIONBIO, [1]) = 0
4 write(1, "child stdout after: nonblock=tr"..., 48) = 48
4 epoll_ctl(3, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x1}) = 0
5 write(1, "from child\n", 11) = 11
5 +++ exited with 0 +++
4 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=5, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
4 epoll_pwait(3, [{events=EPOLLIN|EPOLLHUP, data=0x1}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=IN|HUP data=0x1 ="..., 68) = 68
4 read(6, "from child\n", 64) = 11
4 write(1, "read \"from child\\n\"\n", 20) = 20
4 read(6, "", 64) = 0
4 write(1, "read = Ok(0): EOF\n", 18) = 18
4 +++ exited with 0 +++
- 第 2 行:
pipe::new是一次pipe2(O_NONBLOCK|O_CLOEXEC),读端 4、写端 5。pipe 的读写是普通的read/write(第 5、8 行),不是 socket 的recvfrom/sendto。 - 第 12 行:std 给子进程建的 stdout pipe 只带
O_CLOEXEC,所以before是nonblock=false。第 13、14 行是 std 的spawn内部用的另一个 pipe,和本实验无关。 - 第 16 行:
set_nonblocking(true)是ioctl(FIONBIO)。 - 第 22 行:子进程写完退出,写端关闭,事件是
EPOLLIN|EPOLLHUP:既有数据可读,又已关闭。读循环先读到 11 字节,再读到 0。macOS 上同样一个事件,EVFILT_READ带EV_EOF、data=11。
文档里还有一句关于注销:「Both Sender and Receiver will deregister themselves when dropped, iff the file descriptors are not duplicated (via [dup(2)]).」mio 的 pipe 类型并没有 Drop 实现,这里说的是关闭 fd 时内核自动移除;fd 被 dup 过就不会移除,和 socket 的情况一样,见 Poll 与 Registry。
动脑筋答案
UDP 实验第 3 种情况的输出(各平台输出的最后 3 行,和前面贴的 9 行出自同一次运行)。M2 Max、macOS、rustc 1.99.0:
event: filter=READ flags=CLEAR fflags=0 data=0 udata=0x3 => readable
take_error = Some(Os { code: 61, kind: ConnectionRefused, message: "Connection refused" })
recv = Err(WouldBlock)
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0:
event: events=ERR data=0x3 => error write_closed
take_error = Some(Os { code: 111, kind: ConnectionRefused, message: "Connection refused" })
recv = Err(WouldBlock)
对应的 strace(Linux 输出的最后 14 行):
4 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 9
4 bind(9, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 9
4 bind(9, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 connect(9, {sa_family=AF_INET, sin_port=htons(45515), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
4 epoll_ctl(8, EPOLL_CTL_ADD, 9, {events=EPOLLIN|EPOLLRDHUP|EPOLLET, data=0x3}) = 0
4 sendto(9, "anyone?", 7, MSG_NOSIGNAL, NULL, 0) = 7
4 epoll_pwait(8, [{events=EPOLLERR, data=0x3}], 8, 1000, NULL, 8) = 1
4 write(1, "event: events=ERR data=0x3 => e"..., 50) = 50
4 getsockopt(9, SOL_SOCKET, SO_ERROR, [ECONNREFUSED], [4]) = 0
4 write(1, "take_error = Some(Os { code: 111"..., 92) = 92
4 recvfrom(9, 0xffffdcfa4fc0, 64, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
4 write(1, "recv = Err(WouldBlock)\n", 23) = 23
4 +++ exited with 0 +++
Linux 跑了 3 次、macOS 跑了 10 次,这三行都相同。
- 两个平台都返回一个事件。 没有数据报到达,但内核在这个
connect过的 socket 上记下了一个错误ECONNREFUSED(strace 第 10 行SO_ERROR里的值),这就是一次可报告的状态变化。(第 1 行的socket是那个bind后立刻 drop 的 std socket,它关掉之后 fd 9 被 mio 的 socket 复用;close没在跟踪列表里。) - Linux 上
is_readable()是假。 事件只有EPOLLERR,没有EPOLLIN:没有数据报可读。bools列出的是error write_closed,write_closed来自 epoll 的is_write_closed里「恰好只有EPOLLERR」那一支(见 Token、Interest 与 Event)。注册时只要了READABLE,EPOLLERR照样报,因为内核总是报告它。 - macOS 上
is_readable()是真。 kqueue 给出一个data=0、没有EV_EOF的EVFILT_READ,所以is_error()是假,bools里只有readable。 take_error两边都是ConnectionRefused(Linux 111,macOS 61),recv两边都是WouldBlock:错误已经被take_error取走,队列里也没有数据报。
这又是一个只在 is_readable() 时才去处理的代码会出错的例子:Linux 上它永远不会看这个 socket,也就不会知道对端不在。处理事件时要把 is_error() 也当成「去看一眼」的理由,用 take_error 把错误取出来。
小结
UdpSocket::bind是socket(SOCK_DGRAM|SOCK_NONBLOCK|SOCK_CLOEXEC)加bind,没有SO_REUSEADDR。收发经过do_io;UDP 的connect不阻塞,直接用 std 的版本。- 一个可读事件可能对应多个数据报:实测三个数据报一个事件,
recv_from要循环到WouldBlock。缓冲区太小时 Unix 截断、Windows 报WSAEMSGSIZE,mio 原样交出。 connect过的 UDP socket 发往关闭端口:Linux 只报EPOLLERR(is_readable()为假),macOS 报一个EVFILT_READ;take_error两边都是ConnectionRefused。- Unix 域地址从 1.0 起用 std 的
SocketAddr(#1749,CHANGELOG 误链为 #1760),抽象名字只能走_addr版本。accept要把 macOS 返回的 16 字节全 0 地址转成无名地址;实测 macOSsocklen16、Linux 2。 UnixListener的 backlog 是 -1(TCP 是 128)。UnixStream::connect只放过EINPROGRESS(#1564,0.8.3),实测 backlog 满时 Linux 返回WouldBlock,macOS 在第 129 次返回ConnectionRefused。pair用socketpair,不经过new_socket。pipe::new是pipe2(O_NONBLOCK|O_CLOEXEC)(macOS 上pipe加四次fcntl),返回(Sender, Receiver),内部是IoSource<File>。从ChildStdout等转来的 pipe 是阻塞的,要调set_nonblocking(true),即ioctl(FIONBIO)。