Skip to content

Commit 5ad94c4

Browse files
committed
feat(fd): add pipe for inter-process communication
Add a unidirectional in-kernel pipe: a read end (PipeReceiver) and a write end (PipeSender) sharing a bounded ring buffer. Both ends are ordinary Fd objects, so fork clones the Arc-wrapped endpoints into the child's object map and a pipe created before the fork lets parent and child communicate. Closing the last write end surfaces EOF to readers; closing the last read end surfaces EPIPE to writers. Blocking and O_NONBLOCK semantics are supported, plus poll for readiness. Expose it via fd::pipe() and the sys_pipe(pipefd[2]) system call (SYSNO_PIPE = 58).
1 parent 44e7cab commit 5ad94c4

5 files changed

Lines changed: 330 additions & 0 deletions

File tree

src/fd/delegate.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use core::mem::MaybeUninit;
77
use delegate::delegate;
88

99
use crate::fd::eventfd::EventFd;
10+
#[cfg(feature = "common-os")]
11+
use crate::fd::pipe::{PipeReceiver, PipeSender};
1012
use crate::fd::random_file::RandomFile;
1113
#[cfg(feature = "tcp")]
1214
use crate::fd::socket::tcp;
@@ -39,6 +41,10 @@ pub(crate) enum Fd {
3941
#[cfg(feature = "uhyve")]
4042
UhyveStderr(UhyveStderr),
4143
EventFd(EventFd),
44+
#[cfg(feature = "common-os")]
45+
PipeReceiver(PipeReceiver),
46+
#[cfg(feature = "common-os")]
47+
PipeSender(PipeSender),
4248
#[cfg(feature = "tcp")]
4349
TcpSocket(tcp::Socket),
4450
#[cfg(feature = "udp")]
@@ -89,6 +95,10 @@ fd_from! {
8995
#[cfg(feature = "uhyve")]
9096
UhyveStderr(UhyveStderr),
9197
EventFd(EventFd),
98+
#[cfg(feature = "common-os")]
99+
PipeReceiver(PipeReceiver),
100+
#[cfg(feature = "common-os")]
101+
PipeSender(PipeSender),
92102
#[cfg(feature = "tcp")]
93103
TcpSocket(tcp::Socket),
94104
#[cfg(feature = "udp")]
@@ -123,6 +133,10 @@ impl ObjectInterface for Fd {
123133
#[cfg(feature = "uhyve")]
124134
Self::UhyveStderr(fd) => fd,
125135
Self::EventFd(fd) => fd,
136+
#[cfg(feature = "common-os")]
137+
Self::PipeReceiver(fd) => fd,
138+
#[cfg(feature = "common-os")]
139+
Self::PipeSender(fd) => fd,
126140
#[cfg(feature = "tcp")]
127141
Self::TcpSocket(fd) => fd,
128142
#[cfg(feature = "udp")]

src/fd/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use crate::io;
2121

2222
mod delegate;
2323
mod eventfd;
24+
#[cfg(feature = "common-os")]
25+
pub(crate) mod pipe;
2426
pub(crate) mod random_file;
2527
#[cfg(any(feature = "net", feature = "virtio-vsock"))]
2628
pub(crate) mod socket;
@@ -460,6 +462,29 @@ pub fn eventfd(initval: u64, flags: EventFlags) -> io::Result<RawFd> {
460462
Ok(fd)
461463
}
462464

465+
/// Create a unidirectional pipe.
466+
///
467+
/// Returns a pair of file descriptors `(read_fd, write_fd)` referring to
468+
/// the read and write ends of a fresh in-kernel pipe. Bytes written to
469+
/// `write_fd` can be read — in order — from `read_fd`. Because both ends
470+
/// are inherited across [`fork`](crate::scheduler::fork), a pipe set up
471+
/// before forking lets a parent and child process communicate.
472+
pub(crate) fn pipe() -> io::Result<(RawFd, RawFd)> {
473+
let (receiver, sender) = pipe::pipe();
474+
475+
let read_fd = insert_object(Arc::new(async_lock::RwLock::new(receiver.into())))?;
476+
let write_fd = match insert_object(Arc::new(async_lock::RwLock::new(sender.into()))) {
477+
Ok(fd) => fd,
478+
Err(e) => {
479+
// Undo the read end so we don't leak a descriptor on failure.
480+
drop(remove_object(read_fd));
481+
return Err(e);
482+
}
483+
};
484+
485+
Ok((read_fd, write_fd))
486+
}
487+
463488
pub(crate) fn get_object(fd: RawFd) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
464489
core_scheduler().get_object(fd)
465490
}

src/fd/pipe.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
//! An in-kernel, unidirectional byte pipe.
2+
//!
3+
//! A pipe couples a read end ([`PipeReceiver`]) and a write end
4+
//! ([`PipeSender`]) through a single shared, bounded ring buffer. Both
5+
//! endpoints are ordinary [`Fd`](crate::fd::Fd) objects, so they live
6+
//! behind `Arc<RwLock<Fd>>` in the per-process object map. `fork` clones
7+
//! those `Arc`s into the child's object map, which means a pipe created
8+
//! before the fork is transparently shared between parent and child —
9+
//! the classic Unix way for two processes to communicate.
10+
//!
11+
//! Lifetime of the endpoints is tracked by the `Arc` refcount of the
12+
//! shared state: once the last reference to an endpoint is dropped, its
13+
//! `Drop` impl marks the corresponding side as closed and wakes the
14+
//! opposite side. A reader then observes end-of-file (`read` returns 0),
15+
//! a writer observes a broken pipe ([`Errno::Pipe`]).
16+
17+
use alloc::collections::vec_deque::VecDeque;
18+
use alloc::sync::Arc;
19+
use alloc::vec::Vec;
20+
use core::future;
21+
use core::task::{Poll, Waker};
22+
23+
use hermit_sync::InterruptTicketMutex;
24+
25+
use crate::errno::Errno;
26+
use crate::fd::{ObjectInterface, PollEvent, StatusFlags};
27+
use crate::io;
28+
29+
/// Capacity of the pipe's ring buffer in bytes.
30+
///
31+
/// Mirrors the 64 KiB default Linux gives a pipe. A write blocks (or, in
32+
/// non-blocking mode, returns [`Errno::Again`]) once the buffer is full.
33+
const PIPE_CAPACITY: usize = 64 * 1024;
34+
35+
/// State shared between the two endpoints of a pipe.
36+
#[derive(Debug)]
37+
struct PipeState {
38+
/// FIFO byte buffer, capped at [`PIPE_CAPACITY`].
39+
buffer: VecDeque<u8>,
40+
/// Set once every read end has been dropped.
41+
reader_closed: bool,
42+
/// Set once every write end has been dropped.
43+
writer_closed: bool,
44+
/// Tasks blocked in `read`/readable `poll`.
45+
read_wakers: Vec<Waker>,
46+
/// Tasks blocked in `write`/writable `poll`.
47+
write_wakers: Vec<Waker>,
48+
}
49+
50+
impl PipeState {
51+
fn new() -> Self {
52+
Self {
53+
buffer: VecDeque::new(),
54+
reader_closed: false,
55+
writer_closed: false,
56+
read_wakers: Vec::new(),
57+
write_wakers: Vec::new(),
58+
}
59+
}
60+
61+
/// Wake everyone waiting for the pipe to become readable.
62+
///
63+
/// All wakers are drained and woken: readability is level-triggered
64+
/// and the `poll`-based fd multiplexing re-registers a fresh waker on
65+
/// each poll, so the queue accumulates stale wakers — waking only one
66+
/// could wake a dead waker and miss the live one.
67+
fn wake_readers(&mut self) {
68+
for waker in self.read_wakers.drain(..) {
69+
waker.wake();
70+
}
71+
}
72+
73+
/// Wake everyone waiting for the pipe to become writable.
74+
fn wake_writers(&mut self) {
75+
for waker in self.write_wakers.drain(..) {
76+
waker.wake();
77+
}
78+
}
79+
}
80+
81+
/// Allocate a fresh pipe, returning its read and write endpoints.
82+
pub(crate) fn pipe() -> (PipeReceiver, PipeSender) {
83+
let state = Arc::new(InterruptTicketMutex::new(PipeState::new()));
84+
(
85+
PipeReceiver {
86+
state: state.clone(),
87+
status_flags: StatusFlags::empty(),
88+
},
89+
PipeSender {
90+
state,
91+
status_flags: StatusFlags::empty(),
92+
},
93+
)
94+
}
95+
96+
/// The read end of a pipe.
97+
#[derive(Debug)]
98+
pub(crate) struct PipeReceiver {
99+
state: Arc<InterruptTicketMutex<PipeState>>,
100+
status_flags: StatusFlags,
101+
}
102+
103+
impl Drop for PipeReceiver {
104+
fn drop(&mut self) {
105+
let mut state = self.state.lock();
106+
state.reader_closed = true;
107+
// A blocked writer must return `EPIPE` now that nobody reads.
108+
state.wake_writers();
109+
}
110+
}
111+
112+
impl ObjectInterface for PipeReceiver {
113+
async fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
114+
let nonblock = self.status_flags.contains(StatusFlags::O_NONBLOCK);
115+
116+
future::poll_fn(|cx| {
117+
let mut state = self.state.lock();
118+
119+
if !state.buffer.is_empty() {
120+
let len = buf.len().min(state.buffer.len());
121+
for byte in buf.iter_mut().take(len) {
122+
*byte = state.buffer.pop_front().unwrap();
123+
}
124+
// Freed buffer space: a blocked writer can make progress.
125+
state.wake_writers();
126+
Poll::Ready(Ok(len))
127+
} else if state.writer_closed {
128+
// End of file: all write ends are gone.
129+
Poll::Ready(Ok(0))
130+
} else if nonblock {
131+
Poll::Ready(Err(Errno::Again))
132+
} else {
133+
state.read_wakers.push(cx.waker().clone());
134+
Poll::Pending
135+
}
136+
})
137+
.await
138+
}
139+
140+
async fn poll(&self, event: PollEvent) -> io::Result<PollEvent> {
141+
future::poll_fn(|cx| {
142+
let mut state = self.state.lock();
143+
144+
let mut available = PollEvent::empty();
145+
if !state.buffer.is_empty() {
146+
available.insert(PollEvent::POLLIN | PollEvent::POLLRDNORM);
147+
}
148+
if state.writer_closed {
149+
// EOF is reported as a readable, non-error condition.
150+
available.insert(PollEvent::POLLIN | PollEvent::POLLRDNORM | PollEvent::POLLHUP);
151+
}
152+
153+
let ret = event & available;
154+
if ret.is_empty() && !state.writer_closed {
155+
state.read_wakers.push(cx.waker().clone());
156+
Poll::Pending
157+
} else {
158+
Poll::Ready(Ok(ret))
159+
}
160+
})
161+
.await
162+
}
163+
164+
async fn status_flags(&self) -> io::Result<StatusFlags> {
165+
Ok(self.status_flags)
166+
}
167+
168+
async fn set_status_flags(&mut self, status_flags: StatusFlags) -> io::Result<()> {
169+
self.status_flags = status_flags;
170+
Ok(())
171+
}
172+
}
173+
174+
/// The write end of a pipe.
175+
#[derive(Debug)]
176+
pub(crate) struct PipeSender {
177+
state: Arc<InterruptTicketMutex<PipeState>>,
178+
status_flags: StatusFlags,
179+
}
180+
181+
impl Drop for PipeSender {
182+
fn drop(&mut self) {
183+
let mut state = self.state.lock();
184+
state.writer_closed = true;
185+
// A blocked reader must observe end-of-file now.
186+
state.wake_readers();
187+
}
188+
}
189+
190+
impl ObjectInterface for PipeSender {
191+
async fn write(&self, buf: &[u8]) -> io::Result<usize> {
192+
if buf.is_empty() {
193+
return Ok(0);
194+
}
195+
let nonblock = self.status_flags.contains(StatusFlags::O_NONBLOCK);
196+
197+
future::poll_fn(|cx| {
198+
let mut state = self.state.lock();
199+
200+
if state.reader_closed {
201+
// Writing to a pipe with no readers is a broken pipe.
202+
return Poll::Ready(Err(Errno::Pipe));
203+
}
204+
205+
let free = PIPE_CAPACITY - state.buffer.len();
206+
if free > 0 {
207+
let len = buf.len().min(free);
208+
state.buffer.extend(buf[..len].iter().copied());
209+
// New data: a blocked reader can make progress.
210+
state.wake_readers();
211+
Poll::Ready(Ok(len))
212+
} else if nonblock {
213+
Poll::Ready(Err(Errno::Again))
214+
} else {
215+
state.write_wakers.push(cx.waker().clone());
216+
Poll::Pending
217+
}
218+
})
219+
.await
220+
}
221+
222+
async fn poll(&self, event: PollEvent) -> io::Result<PollEvent> {
223+
future::poll_fn(|cx| {
224+
let mut state = self.state.lock();
225+
226+
let mut available = PollEvent::empty();
227+
if state.reader_closed {
228+
// A vanished reader is an error/hangup condition for the writer.
229+
available.insert(PollEvent::POLLERR);
230+
} else if state.buffer.len() < PIPE_CAPACITY {
231+
available.insert(PollEvent::POLLOUT | PollEvent::POLLWRNORM);
232+
}
233+
234+
let ret = event & available;
235+
if ret.is_empty() && !state.reader_closed {
236+
state.write_wakers.push(cx.waker().clone());
237+
Poll::Pending
238+
} else {
239+
Poll::Ready(Ok(ret))
240+
}
241+
})
242+
.await
243+
}
244+
245+
async fn status_flags(&self) -> io::Result<StatusFlags> {
246+
Ok(self.status_flags)
247+
}
248+
249+
async fn set_status_flags(&mut self, status_flags: StatusFlags) -> io::Result<()> {
250+
self.status_flags = status_flags;
251+
Ok(())
252+
}
253+
}

src/syscalls/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,41 @@ pub extern "C" fn sys_eventfd(initval: u64, flags: i16) -> i32 {
887887
fd::eventfd(initval, flags).unwrap_or_else(|e| -i32::from(e))
888888
}
889889

890+
/// Create a unidirectional pipe.
891+
///
892+
/// On success two file descriptors are stored in the caller-provided
893+
/// array `pipefd`: `pipefd[0]` refers to the read end, `pipefd[1]` to the
894+
/// write end. The descriptors are inherited across `fork`, so a pipe set
895+
/// up beforehand can be used to communicate between parent and child.
896+
///
897+
/// Returns `0` on success or a negative error number on failure.
898+
#[cfg(feature = "common-os")]
899+
#[hermit_macro::system(errno)]
900+
#[unsafe(no_mangle)]
901+
pub unsafe extern "C" fn sys_pipe(pipefd: *mut RawFd) -> i32 {
902+
if pipefd.is_null() {
903+
return -i32::from(Errno::Inval);
904+
}
905+
906+
match fd::pipe() {
907+
Ok((read_fd, write_fd)) => {
908+
unsafe {
909+
pipefd.write(read_fd);
910+
pipefd.add(1).write(write_fd);
911+
}
912+
0
913+
}
914+
Err(e) => -i32::from(e),
915+
}
916+
}
917+
918+
#[cfg(not(feature = "common-os"))]
919+
#[hermit_macro::system(errno)]
920+
#[unsafe(no_mangle)]
921+
pub unsafe extern "C" fn sys_pipe(pipefd: *mut RawFd) -> i32 {
922+
-i32::from(Errno::Nosys)
923+
}
924+
890925
#[hermit_macro::system]
891926
#[unsafe(no_mangle)]
892927
pub extern "C" fn sys_image_start_addr() -> usize {

src/syscalls/table.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ const SYSNO_EXEC: usize = 56;
141141
/// number of the system call `mmap`
142142
const SYSNO_MMAP: usize = 57;
143143

144+
const SYSNO_PIPE: usize = 58;
145+
144146
/// Total number of system calls
145147
pub(crate) const NO_SYSCALLS: usize = 64;
146148

@@ -269,6 +271,7 @@ impl SyscallTable {
269271
table.handle[SYSNO_GET_DENTS64] = sys_getdents64 as *const _;
270272
table.handle[SYSNO_EXEC] = sys_exec as *const _;
271273
table.handle[SYSNO_MMAP] = sys_mmap as *const _;
274+
table.handle[SYSNO_PIPE] = sys_pipe as *const _;
272275

273276
table
274277
}

0 commit comments

Comments
 (0)