Skip to main content

zenith_linux/
syscalls.rs

1//! Linux 高性能零拷贝/批量系统调用封装
2//!
3//! 本模块集中收敛与「全链路最高性能最低消耗」相关的 Linux 内核系统调用封装:
4//!
5//! - `splice(2)`:在两个 fd 之间内核态零拷贝移动数据(pipe ↔ socket)
6//! - `sendfile(2)`:从文件 fd 直接发送到 socket fd,绕过用户态缓冲区
7//! - `recvmmsg(2)`:单次系统调用接收多个 UDP/UDP-Lite 数据报,降低系统调用开销
8//! - `sendmmsg(2)`:单次系统调用发送多个 UDP/UDP-Lite 数据报
9//! - `pipe2(2)`:创建用于 splice 中继的管道对
10//!
11//! # 设计原则(规范 §1.1 / §6.1)
12//! - **Fail-Closed**:任何系统调用失败立即返回错误,绝不静默降级
13//! - **零堆分配热路径**:批量数组使用栈上数组 `[libc::mmsghdr; N]`
14//! - **checked 算术**:所有偏移、长度计算使用 `checked_*` 防止溢出
15//! - **unsafe 隔离**:所有 unsafe 集中于本文件,附 `// SAFETY:` 注释
16//! - **错误语义完整**:errno → `LinuxError::Syscall`,保留 syscall 名与 errno
17//!
18//! # 极致性能
19//! - 单次 `splice` 可传输最多 2^31-1 字节(Linux 限制)
20//! - `recvmmsg/sendmmsg` 单次最多 64 个数据报(`UIO_MAXIOV`)
21//! - 与 AF_XDP 路径互补:内核套接字场景的最优解
22
23#![allow(unsafe_code)]
24
25use crate::error::{LinuxError, Result};
26
27/// 批量收发最大数据报数(与 `UIO_MAXIOV` 对齐,glibc 上限)
28pub const MAX_BATCH_DATAGRAMS: usize = 64;
29
30/// splice(2) 标志位
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(u32)]
33pub enum SpliceFlags {
34    /// 默认(无标志)
35    None = 0,
36    /// 移动数据(仅引用计数迁移,零拷贝)
37    Move = libc::SPLICE_F_MOVE,
38    /// 非阻塞
39    NonBlock = libc::SPLICE_F_NONBLOCK,
40    /// 不阻塞,返回 EAGAIN
41    More = libc::SPLICE_F_MORE,
42}
43
44impl SpliceFlags {
45    /// 合并为原始 u32 标志
46    #[inline]
47    pub fn bits(self) -> u32 {
48        self as u32
49    }
50}
51
52/// splice(2) 在两个 fd 之间内核态零拷贝移动数据。
53///
54/// 至少一个 fd 必须是管道(pipe)。常用于 socket ↔ pipe ↔ socket 中继。
55///
56/// # 参数
57/// * `fd_in` - 输入 fd(socket / file / pipe)
58/// * `off_in` - 输入偏移指针,None 表示使用当前文件偏移
59/// * `fd_out` - 输出 fd
60/// * `off_out` - 输出偏移指针,None 表示使用当前文件偏移
61/// * `len` - 最多移动的字节数(上限 2^31-1)
62/// * `flags` - splice 标志
63///
64/// # 返回
65/// 实际移动的字节数(0 表示 EOF)
66///
67/// # 错误
68/// - `LinuxError::Syscall { "splice", errno }` - 系统调用失败
69pub fn splice(
70    fd_in: i32,
71    off_in: Option<&mut i64>,
72    fd_out: i32,
73    off_out: Option<&mut i64>,
74    len: usize,
75    flags: u32,
76) -> Result<usize> {
77    // Linux splice 长度上限为 2^31-1(splice 内部用 ssize_t)
78    let safe_len = len.min(0x7FFF_FFFFusize);
79
80    let off_in_ptr = off_in.map_or(std::ptr::null_mut(), |p| p as *mut i64);
81    let off_out_ptr = off_out.map_or(std::ptr::null_mut(), |p| p as *mut i64);
82
83    // SAFETY: splice(2) 系统调用。fd_in/fd_out 必须是有效的打开文件描述符,
84    // 至少一个为管道。off_in/off_out 可空,非空时指向 64 位有符号偏移。
85    // safe_len 已被钳制到 Linux 上限内。flags 直接传递内核。
86    let ret = unsafe {
87        libc::splice(
88            fd_in,
89            off_in_ptr,
90            fd_out,
91            off_out_ptr,
92            safe_len,
93            flags,
94        )
95    };
96
97    if ret < 0 {
98        Err(LinuxError::Syscall {
99            syscall: "splice",
100            errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
101        })
102    } else {
103        Ok(ret as usize)
104    }
105}
106
107/// sendfile(2) 从文件 fd 直接发送到 socket fd,绕过用户态缓冲区。
108///
109/// 适用于静态文件服务(HTTP 静态资源、缓存持久化等)。
110///
111/// # 参数
112/// * `out_fd` - 输出 fd(必须是 socket 或类似可写 fd)
113/// * `in_fd` - 输入 fd(必须是支持 sendfile 的文件 fd,不能是 socket)
114/// * `offset` - 输入偏移指针,None 表示使用当前文件偏移
115/// * `count` - 最多发送的字节数
116///
117/// # 返回
118/// 实际发送的字节数(0 表示 EOF)
119pub fn sendfile(
120    out_fd: i32,
121    in_fd: i32,
122    offset: Option<&mut i64>,
123    count: usize,
124) -> Result<usize> {
125    let off_ptr = offset.map_or(std::ptr::null_mut(), |p| p as *mut i64);
126
127    // SAFETY: sendfile(2) 系统调用。out_fd 通常是 socket,in_fd 必须是支持
128    // mmap 的文件(如普通文件、块设备)。off_ptr 可空。count 不超过 SSIZE_MAX。
129    let ret = unsafe { libc::sendfile(out_fd, in_fd, off_ptr, count.min(0x7FFF_FFFFusize)) };
130
131    if ret < 0 {
132        Err(LinuxError::Syscall {
133            syscall: "sendfile",
134            errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
135        })
136    } else {
137        Ok(ret as usize)
138    }
139}
140
141/// pipe2(2) 创建管道对,用于 splice 中继。
142///
143/// # 参数
144/// * `flags` - 管道标志(如 `O_NONBLOCK`)
145///
146/// # 返回
147/// (read_fd, write_fd)
148pub fn pipe2(flags: i32) -> Result<(i32, i32)> {
149    let mut fds = [0i32; 2];
150
151    // SAFETY: pipe2(2) 系统调用。fds 数组长度为 2,flags 直接传递内核。
152    let ret = unsafe { libc::pipe2(fds.as_mut_ptr(), flags) };
153
154    if ret < 0 {
155        Err(LinuxError::Syscall {
156            syscall: "pipe2",
157            errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
158        })
159    } else {
160        Ok((fds[0], fds[1]))
161    }
162}
163
164/// recvmmsg(2) 批量接收多个数据报(UDP/UDP-Lite)。
165///
166/// 单次系统调用最多接收 `bufs.len()` 个数据报(上限 64),显著降低 PPS 场景系统调用开销。
167///
168/// # 参数
169/// * `fd` - UDP socket fd
170/// * `bufs` - 接收缓冲区切片(每个元素对应一个数据报)
171/// * `flags` - recvmsg 标志(如 `MSG_DONTWAIT`)
172///
173/// # 返回
174/// 实际接收的数据报数量(0 表示无数据)
175pub fn recvmmsg(bufs: &mut [&mut [u8]], fd: i32, flags: i32) -> Result<usize> {
176    let count = bufs.len().min(MAX_BATCH_DATAGRAMS);
177    if count == 0 {
178        return Ok(0);
179    }
180
181    // 栈上预分配 mmsghdr 数组,零堆分配
182    let mut msgs = [libc::mmsghdr {
183        msg_hdr: libc::msghdr {
184            msg_name: std::ptr::null_mut(),
185            msg_namelen: 0,
186            msg_iov: std::ptr::null_mut(),
187            msg_iovlen: 0,
188            msg_control: std::ptr::null_mut(),
189            msg_controllen: 0,
190            msg_flags: 0,
191        },
192        msg_len: 0,
193    }; MAX_BATCH_DATAGRAMS];
194
195    // 栈上预分配 iovec 数组
196    let mut iovs = [libc::iovec {
197        iov_base: std::ptr::null_mut(),
198        iov_len: 0,
199    }; MAX_BATCH_DATAGRAMS];
200
201    // 填充 iovs 指向用户缓冲区
202    for i in 0..count {
203        iovs[i] = libc::iovec {
204            iov_base: bufs[i].as_mut_ptr() as *mut std::ffi::c_void,
205            iov_len: bufs[i].len(),
206        };
207        msgs[i].msg_hdr.msg_iov = &mut iovs[i];
208        msgs[i].msg_hdr.msg_iovlen = 1;
209    }
210
211    // SAFETY: recvmmsg(2) 系统调用。msgs 数组长度 ≥ count,每个 msg_hdr.msg_iov
212    // 指向一个有效的 iovec,iovec 指向用户提供的可写缓冲区。
213    // timeout 传空表示阻塞(或受 flags 中 MSG_DONTWAIT 控制)。
214    let ret = unsafe {
215        libc::recvmmsg(
216            fd,
217            msgs.as_mut_ptr(),
218            count as u32,
219            flags,
220            std::ptr::null_mut(),
221        )
222    };
223
224    if ret < 0 {
225        Err(LinuxError::Syscall {
226            syscall: "recvmmsg",
227            errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
228        })
229    } else {
230        Ok(ret as usize)
231    }
232}
233
234/// sendmmsg(2) 批量发送多个数据报(UDP/UDP-Lite)。
235///
236/// 单次系统调用最多发送 `bufs.len()` 个数据报(上限 64)。
237///
238/// # 参数
239/// * `fd` - UDP socket fd
240/// * `bufs` - 发送缓冲区切片(每个元素对应一个数据报)
241/// * `dest` - 目标地址(所有数据报发往同一地址)
242/// * `flags` - sendmsg 标志(如 `MSG_DONTWAIT`)
243///
244/// # 返回
245/// 实际发送的数据报数量
246pub fn sendmmsg(bufs: &[&[u8]], fd: i32, dest: &libc::sockaddr_storage, flags: i32) -> Result<usize> {
247    let count = bufs.len().min(MAX_BATCH_DATAGRAMS);
248    if count == 0 {
249        return Ok(0);
250    }
251
252    let mut msgs = [libc::mmsghdr {
253        msg_hdr: libc::msghdr {
254            msg_name: std::ptr::null_mut(),
255            msg_namelen: 0,
256            msg_iov: std::ptr::null_mut(),
257            msg_iovlen: 0,
258            msg_control: std::ptr::null_mut(),
259            msg_controllen: 0,
260            msg_flags: 0,
261        },
262        msg_len: 0,
263    }; MAX_BATCH_DATAGRAMS];
264
265    let mut iovs = [libc::iovec {
266        iov_base: std::ptr::null_mut(),
267        iov_len: 0,
268    }; MAX_BATCH_DATAGRAMS];
269
270    // 目标地址转换为原始字节指针(所有数据报共用同一目标)
271    let dest_ptr = dest as *const libc::sockaddr_storage as *mut std::ffi::c_void;
272    let dest_len = std::mem::size_of::<libc::sockaddr_storage>() as u32;
273
274    for i in 0..count {
275        iovs[i] = libc::iovec {
276            iov_base: bufs[i].as_ptr() as *mut std::ffi::c_void,
277            iov_len: bufs[i].len(),
278        };
279        msgs[i].msg_hdr.msg_name = dest_ptr;
280        msgs[i].msg_hdr.msg_namelen = dest_len;
281        msgs[i].msg_hdr.msg_iov = &mut iovs[i];
282        msgs[i].msg_hdr.msg_iovlen = 1;
283    }
284
285    // SAFETY: sendmmsg(2) 系统调用。msgs 数组长度 ≥ count,每个 msg_hdr.msg_iov
286    // 指向一个有效的 iovec,iovec 指向用户提供的只读缓冲区。
287    // dest_ptr 指向有效的 sockaddr_storage,生命周期由调用者保证。
288    let ret = unsafe {
289        libc::sendmmsg(
290            fd,
291            msgs.as_mut_ptr(),
292            count as u32,
293            flags,
294        )
295    };
296
297    if ret < 0 {
298        Err(LinuxError::Syscall {
299            syscall: "sendmmsg",
300            errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
301        })
302    } else {
303        Ok(ret as usize)
304    }
305}
306
307/// TCP 双向中继:使用 splice(2) 实现内核态零拷贝双向转发。
308///
309/// 创建两条管道,分别中继 `client → upstream` 和 `upstream → client`。
310/// 全程数据不进入用户态缓冲区,达到内核零拷贝极限性能。
311///
312/// 采用双线程模型(每方向一线程),避免单线程中一个方向阻塞导致另一方向饥饿。
313/// 任一方向 EOF 即关闭对端 socket(shutdown(SHUT_WR)),触发另一方向也快速退出。
314///
315/// # 参数
316/// * `client_fd` - 客户端 socket fd
317/// * `upstream_fd` - 上游 socket fd
318/// * `pipe_buf_size` - 管道缓冲区大小(建议 65536)
319///
320/// # 返回
321/// `(c2u_bytes, u2c_bytes)`:分方向字节数
322/// (c2u = client→upstream,u2c = upstream→client),便于调用方分账 rx/tx
323///
324/// # 错误
325/// - `LinuxError::Syscall { "pipe2", .. }` - 管道创建失败
326/// - `LinuxError::Syscall { "splice", .. }` - splice 失败(含对端关闭 EPIPE/ECONNRESET)
327pub fn splice_bidirectional(
328    client_fd: i32,
329    upstream_fd: i32,
330    pipe_buf_size: usize,
331) -> Result<(usize, usize)> {
332    // pipe_buf_size 为 0 时 splice 每次传输 0 字节,导致无限空转
333    if pipe_buf_size == 0 {
334        return Err(LinuxError::InsufficientResources(
335            "pipe_buf_size 不能为 0(会导致无限 0 字节 splice 调用)".to_string(),
336        ));
337    }
338
339    // 阻塞管道:让 splice 在管道满/空时自然阻塞,避免 EAGAIN 自旋
340    let (c2u_read, c2u_write) = pipe2(0)?;
341    let (u2c_read, u2c_write) = pipe2(0)?;
342    // 全部 pipe fd 纳入 RAII 守卫:spawn 失败 / relay 异常 / join 失败等任何
343    // 提前返回路径均闭环回收;spawn 成功的方向写端所有权移交 relay 线程
344    // (relay 各退出路径统一 close),守卫 into_raw 摘除防止 double-close。
345    let c2u_read_guard = FdGuard::new(c2u_read);
346    let mut c2u_write_guard = Some(FdGuard::new(c2u_write));
347    let u2c_read_guard = FdGuard::new(u2c_read);
348    let mut u2c_write_guard = Some(FdGuard::new(u2c_write));
349
350    // 单向 splice 中继:src → pipe(w) → pipe(r) → dst,直到任一端 EOF
351    // pipe_w 所有权归本闭包:所有退出路径(EOF/EPIPE/错误)都负责 close 一次
352    let relay = |src_fd: i32, pipe_r: i32, pipe_w: i32, dst_fd: i32, p_size: usize| -> std::result::Result<usize, LinuxError> {
353        let mut total = 0usize;
354        loop {
355            // src → pipe 写端
356            let n = match splice(src_fd, None, pipe_w, None, p_size, libc::SPLICE_F_MOVE) {
357                Ok(n) => n,
358                Err(LinuxError::Syscall { syscall: _, errno }) if errno == libc::EPIPE => {
359                    // dst 已关闭写端(对端中继已 EOF),停止本方向
360                    // 关闭 pipe 写端防止 fd 泄漏(读端由主函数统一清理)
361                    // SAFETY: close(2) 关闭写端(本路径唯一一次)
362                    unsafe { libc::close(pipe_w); }
363                    return Ok(total);
364                }
365                Err(e) => {
366                    // 错误路径同样回收写端,杜绝 fd 泄漏
367                    // SAFETY: close(2) 关闭写端(本路径唯一一次)
368                    unsafe { libc::close(pipe_w); }
369                    return Err(e);
370                }
371            };
372            if n == 0 {
373                // src EOF:关闭 pipe 写端,使 pipe 读端 splice 收到 0
374                // SAFETY: close(2) 关闭写端,触发读端 EOF(本路径唯一一次)
375                unsafe { libc::close(pipe_w); }
376                // 排空管道剩余数据到 dst
377                loop {
378                    let m = splice(pipe_r, None, dst_fd, None, p_size, libc::SPLICE_F_MOVE)?;
379                    if m == 0 {
380                        break;
381                    }
382                    total = total.checked_add(m).ok_or_else(|| LinuxError::InsufficientResources(
383                        "splice byte count overflow".to_string()
384                    ))?;
385                }
386                // 通知 dst 没有更多数据
387                // SAFETY: shutdown(2) 语义安全,EBADF/ENOTCONN 忽略
388                unsafe { let _ = libc::shutdown(dst_fd, libc::SHUT_WR); }
389                return Ok(total);
390            }
391            total = total.checked_add(n).ok_or_else(|| LinuxError::InsufficientResources(
392                "splice byte count overflow".to_string()
393            ))?;
394            // pipe 读端 → dst
395            let m = splice(pipe_r, None, dst_fd, None, n, libc::SPLICE_F_MOVE)?;
396            // 管道内 splice 必须保持字节守恒
397            debug_assert_eq!(m, n, "splice through pipe must preserve byte count");
398        }
399    };
400
401    // 双线程并行:互不阻塞。spawn 成功后写端所有权移交对应 relay 线程
402    let t1 = match std::thread::Builder::new()
403        .name("zenith-splice-c2u".to_string())
404        .spawn(move || relay(client_fd, c2u_read, c2u_write, upstream_fd, pipe_buf_size))
405    {
406        Ok(t) => {
407            if let Some(g) = c2u_write_guard.take() {
408                let _ = g.into_raw();
409            }
410            t
411        }
412        Err(e) => {
413            // spawn 失败:无任何线程持有 fd,守卫随作用域闭环回收全部 4 个 fd
414            return Err(LinuxError::InsufficientResources(format!(
415                "spawn c2u thread failed: {e}"
416            )));
417        }
418    };
419
420    let t2 = match std::thread::Builder::new()
421        .name("zenith-splice-u2c".to_string())
422        .spawn(move || relay(upstream_fd, u2c_read, u2c_write, client_fd, pipe_buf_size))
423    {
424        Ok(t) => {
425            if let Some(g) = u2c_write_guard.take() {
426                let _ = g.into_raw();
427            }
428            t
429        }
430        Err(e) => {
431            // t1 正在对已移交的 client_fd/upstream_fd 做 splice:先 shutdown
432            // 两端 socket 让 t1 的 splice 返回 EOF/EPIPE 退出,join 回收线程
433            // 后再返回 Err(c2u 写端由 t1 闭环,其余 fd 随守卫闭环)。
434            // SAFETY: shutdown(2) 语义安全;fd 已关闭/未连接时的 EBADF/ENOTCONN 忽略。
435            unsafe {
436                let _ = libc::shutdown(client_fd, libc::SHUT_RDWR);
437                let _ = libc::shutdown(upstream_fd, libc::SHUT_RDWR);
438            }
439            let _ = t1.join();
440            return Err(LinuxError::InsufficientResources(format!(
441                "spawn u2c thread failed: {e}"
442            )));
443        }
444    };
445
446    let r1 = t1.join().map_err(|_| LinuxError::InsufficientResources(
447        "c2u relay thread panicked".to_string()
448    ))??;
449    let r2 = t2.join().map_err(|_| LinuxError::InsufficientResources(
450        "u2c relay thread panicked".to_string()
451    ))??;
452
453    // pipe 写端由 relay 线程在各自退出路径闭环关闭;读端与(spawn 失败路径的)
454    // 未移交写端随 FdGuard 在函数返回时回收——全程无泄漏分支
455    let _ = (c2u_read_guard, u2c_read_guard);
456
457    // r1 = c2u 字节(client→upstream),r2 = u2c 字节(upstream→client)分方向返回
458    Ok((r1, r2))
459}
460
461/// 将系统 fd 包装为 RAII 守卫,确保 close(2) 被调用。
462#[derive(Debug)]
463pub struct FdGuard {
464    fd: i32,
465    closed: bool,
466}
467
468impl FdGuard {
469    /// 创建 fd 守卫。`fd` 必须是有效的打开文件描述符。
470    #[inline]
471    pub fn new(fd: i32) -> Self {
472        Self { fd, closed: false }
473    }
474
475    /// 获取原始 fd。
476    #[inline]
477    pub fn as_raw(&self) -> i32 {
478        self.fd
479    }
480
481    /// 取出 fd 的所有权(不再由 Drop 关闭)。
482    #[inline]
483    pub fn into_raw(mut self) -> i32 {
484        self.closed = true;
485        self.fd
486    }
487}
488
489impl Drop for FdGuard {
490    fn drop(&mut self) {
491        if !self.closed && self.fd >= 0 {
492            // SAFETY: close(2) 释放 fd。忽略 EBADF(已被其他途径关闭)
493            unsafe { let _ = libc::close(self.fd); }
494        }
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_splice_flags_bits() {
504        assert_eq!(SpliceFlags::None.bits(), 0);
505        assert_eq!(SpliceFlags::Move.bits(), libc::SPLICE_F_MOVE);
506        assert_eq!(SpliceFlags::NonBlock.bits(), libc::SPLICE_F_NONBLOCK);
507        assert_eq!(SpliceFlags::More.bits(), libc::SPLICE_F_MORE);
508    }
509
510    #[test]
511    fn test_pipe2_create() {
512        // pipe2(O_NONBLOCK) 应创建两个非阻塞 fd
513        let (r, w) = pipe2(libc::O_NONBLOCK).expect("pipe2");
514        assert!(r >= 0);
515        assert!(w >= 0);
516        assert_ne!(r, w);
517        // SAFETY: close(2) 清理 fd
518        unsafe {
519            libc::close(r);
520            libc::close(w);
521        }
522    }
523
524    #[test]
525    fn test_pipe2_invalid_flags() {
526        // 不应 panic,应返回错误
527        let result = pipe2(0x7FFF_FFFF);
528        // pipe2 对未知标志可能返回 EINVAL 或忽略,不强制断言
529        let _ = result;
530    }
531
532    #[test]
533    fn test_splice_eof_pipe() {
534        // 写端关闭后,splice 应返回 0(EOF)
535        let (r, w) = pipe2(0).expect("pipe2");
536        // SAFETY: close 写端,触发读端 EOF
537        unsafe { libc::close(w); }
538        let n = splice(r, None, -1, None, 1024, 0);
539        // fd_out = -1 应失败
540        assert!(n.is_err());
541        unsafe { libc::close(r); }
542    }
543
544    #[test]
545    fn test_fd_guard_closes() {
546        let (r, w) = pipe2(0).expect("pipe2");
547        unsafe { libc::close(w); }
548        {
549            let _guard = FdGuard::new(r);
550            // guard drop 时关闭 r
551        }
552        // 再次 close 应返回 EBADF(已关闭),但我们不暴露 close 结果
553    }
554
555    #[test]
556    fn test_fd_guard_into_raw() {
557        let (r, w) = pipe2(0).expect("pipe2");
558        unsafe { libc::close(w); }
559        let guard = FdGuard::new(r);
560        let raw = guard.into_raw();
561        assert_eq!(raw, r);
562        // 需要手动 close
563        unsafe { libc::close(r); }
564    }
565
566    #[test]
567    fn test_recvmmsg_empty_bufs() {
568        // 空缓冲区切片应返回 Ok(0)
569        let mut bufs: [&mut [u8]; 0] = [];
570        let result = recvmmsg(&mut bufs, -1, 0);
571        assert_eq!(result.unwrap(), 0);
572    }
573
574    #[test]
575    fn test_sendmmsg_empty_bufs() {
576        let bufs: [&[u8]; 0] = [];
577        // SAFETY: sockaddr_storage 全零是合法的零初始化(ss_family=AF_UNSPEC)
578        let dest: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
579        let result = sendmmsg(&bufs, -1, &dest, 0);
580        assert_eq!(result.unwrap(), 0);
581    }
582
583    #[test]
584    fn test_sendfile_invalid_fd() {
585        let result = sendfile(-1, -1, None, 1024);
586        assert!(result.is_err());
587        let err = result.unwrap_err();
588        match err {
589            LinuxError::Syscall { syscall, .. } => assert_eq!(syscall, "sendfile"),
590            _ => panic!("expected Syscall error"),
591        }
592    }
593
594    #[test]
595    fn test_splice_invalid_fd() {
596        let result = splice(-1, None, -1, None, 1024, 0);
597        assert!(result.is_err());
598        let err = result.unwrap_err();
599        match err {
600            LinuxError::Syscall { syscall, .. } => assert_eq!(syscall, "splice"),
601            _ => panic!("expected Syscall error"),
602        }
603    }
604
605    #[test]
606    fn test_max_batch_datagrams_constant() {
607        // 与 UIO_MAXIOV 对齐,glibc 上限
608        assert_eq!(MAX_BATCH_DATAGRAMS, 64);
609        const { assert!(MAX_BATCH_DATAGRAMS > 0) };
610    }
611
612    /// 枚举当前进程打开的 pipe fd 的 inode 标识(/proc/self/fd 符号链接目标)
613    fn open_pipe_inodes() -> std::collections::HashSet<String> {
614        let mut set = std::collections::HashSet::new();
615        if let Ok(dir) = std::fs::read_dir("/proc/self/fd") {
616            for entry in dir.flatten() {
617                if let Ok(target) = std::fs::read_link(entry.path()) {
618                    let s = target.to_string_lossy().into_owned();
619                    if s.starts_with("pipe:[") {
620                        set.insert(s);
621                    }
622                }
623            }
624        }
625        set
626    }
627
628    /// 真实回环 TCP 的 splice 双向中继集成测试:
629    /// echo-server ↔ splice_bidirectional(client, upstream) ↔ 半关闭客户端
630    ///
631    /// 验证(EOF/半关闭语义):
632    /// 1. 字节数守恒:c2u/u2c 分方向计数 == 负载长度,echo 内容逐字节一致
633    /// 2. 半关闭传播:客户端 shutdown(Wr) → relay c2u EOF → upstream 半关闭 →
634    ///    echo 收口 → relay u2c EOF → 客户端读到 EOF(0)
635    /// 3. 无泄漏:relay 结束后全部 pipe fd 关闭(/proc/self/fd 对比)
636    #[test]
637    fn test_splice_bidirectional_tcp_loopback() {
638        use std::io::{Read, Write};
639        use std::net::{Shutdown, TcpListener, TcpStream};
640        use std::os::unix::io::AsRawFd;
641
642        let pipe_baseline = open_pipe_inodes();
643
644        // echo 服务端:读到的数据原样写回;客户端半关闭(EOF)后自身半关闭写侧
645        let echo_listener = match TcpListener::bind("127.0.0.1:0") {
646            Ok(l) => l,
647            Err(_) => return, // 极端环境无回环网络栈,跳过
648        };
649        let echo_addr = echo_listener.local_addr().unwrap();
650        let echo_thread = std::thread::spawn(move || -> usize {
651            let (mut s, _) = echo_listener.accept().unwrap();
652            let mut buf = [0u8; 4096];
653            let mut echoed = 0usize;
654            loop {
655                match s.read(&mut buf) {
656                    Ok(0) | Err(_) => break, // EOF:上游半关闭已传播
657                    Ok(n) => {
658                        s.write_all(&buf[..n]).unwrap();
659                        echoed += n;
660                    }
661                }
662            }
663            let _ = s.shutdown(Shutdown::Write); // echo 侧半关闭,触发 relay U2C EOF
664            echoed
665        });
666
667        // relay 两端:upstream 侧主动连 echo;client 侧接受测试驱动连接
668        let proxy_listener = TcpListener::bind("127.0.0.1:0").unwrap();
669        let proxy_addr = proxy_listener.local_addr().unwrap();
670        let upstream = TcpStream::connect(echo_addr).unwrap();
671        let mut client = TcpStream::connect(proxy_addr).unwrap();
672        let (proxy_side, _) = proxy_listener.accept().unwrap();
673
674        let proxy_fd = proxy_side.as_raw_fd();
675        let upstream_fd = upstream.as_raw_fd();
676        let relay_thread = std::thread::spawn(move || {
677            splice_bidirectional(proxy_fd, upstream_fd, 16384)
678        });
679
680        // 测试负载:64KB(pipe_buf 16384 → 多轮 splice),随后半关闭写侧
681        let payload: Vec<u8> = (0..65536u32).map(|i| (i % 251) as u8).collect();
682        client.write_all(&payload).unwrap();
683        client.shutdown(Shutdown::Write).unwrap();
684
685        // 读回全部 echo,直至 EOF(半关闭传播链的最终证据)
686        let mut got = Vec::with_capacity(payload.len());
687        let mut tmp = [0u8; 8192];
688        loop {
689            match client.read(&mut tmp) {
690                Ok(0) => break, // EOF:relay U2C 完成并传播了 client 侧半关闭
691                Ok(n) => got.extend_from_slice(&tmp[..n]),
692                Err(e) => panic!("client read failed: {e}"),
693            }
694        }
695        assert_eq!(got.len(), payload.len(), "echo 字节数必须守恒");
696        assert!(got == payload, "echo 内容必须逐字节一致");
697
698        // echo 侧读到的字节数 == 负载长度
699        let echoed = echo_thread.join().unwrap();
700        assert_eq!(echoed, payload.len(), "echo 服务端必须读满全部负载");
701
702        // relay 双向 EOF 后自然结束:分方向字节数核对
703        let (c2u, u2c) = relay_thread.join().unwrap().unwrap();
704        assert_eq!(c2u, payload.len(), "c2u 方向字节数必须守恒");
705        assert_eq!(u2c, payload.len(), "u2c 方向字节数必须守恒");
706
707        // 显式断开测试侧 socket,释放其 fd 后再做泄漏判定
708        drop(client);
709        drop(proxy_side);
710        drop(upstream);
711        drop(proxy_listener);
712
713        // 无泄漏判定:relay 的 4 个 pipe fd 必须全部关闭。
714        // 并发测试进程中其他用例的瞬时 pipe 会造成抖动:短暂重试等待其消失;
715        // 泄漏的 pipe fd 不会自行消失,重试窗口后仍存在即判失败。
716        let mut extra = Vec::new();
717        for _ in 0..100 {
718            extra = open_pipe_inodes()
719                .difference(&pipe_baseline)
720                .cloned()
721                .collect::<Vec<_>>();
722            if extra.is_empty() {
723                break;
724            }
725            std::thread::sleep(std::time::Duration::from_millis(10));
726        }
727        assert!(extra.is_empty(), "splice_bidirectional 泄漏 pipe fd: {extra:?}");
728    }
729}