feat: implement sbrk and pipe syscalls#14
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request implements two important syscalls for the rCore tutorial OS:
Purpose: Add sbrk (dynamic heap management) and pipe (inter-process communication) syscalls to support more advanced process features across chapters 4-8.
Changes:
- Implemented sbrk syscall (ch4-ch7) with heap management using
heap_bottomandprogram_brkfields, and addedunmap()method for memory deallocation - Implemented pipe syscall (ch7-ch8) using a ring buffer-based pipe with 32-byte capacity, modified FileHandle to support both files and pipes, and changed fd_table to use
Arc<FileHandle>for sharing - Added corresponding test cases and updated test configurations
Reviewed changes
Copilot reviewed 22 out of 27 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| tg-kernel-vm/src/space/mod.rs | Added unmap() method to remove virtual memory mappings |
| tg-kernel-vm/Cargo.toml | Version bump to 0.1.0-preview.2 |
| tg-easy-fs/src/pipe.rs | New file implementing PipeRingBuffer with 32-byte ring buffer |
| tg-easy-fs/src/file.rs | Extended FileHandle to support both files and pipes, changed offset to Cell for interior mutability |
| tg-easy-fs/src/lib.rs | Exported pipe module and make_pipe function |
| tg-easy-fs/Cargo.toml | Version bump to 0.1.0-preview.2 |
| tg-easy-fs/README.md | Added documentation for pipe support |
| tg-syscall/src/user.rs | Added sbrk() and pipe() syscall wrappers |
| tg-syscall/src/kernel/mod.rs | Added sbrk and pipe handler registration |
| ch4/src/process.rs | Added heap_bottom, program_brk fields and change_program_brk() method |
| ch4/src/main.rs | Added sbrk syscall handler |
| ch5/src/process.rs | Added heap management fields and implementation |
| ch5/src/main.rs | Added sbrk syscall handler |
| ch6/src/process.rs | Added heap management fields and implementation |
| ch6/src/main.rs | Added sbrk syscall handler, removed mut from file lock |
| ch7/src/process.rs | Added heap management and changed fd_table to use Arc |
| ch7/src/main.rs | Added sbrk and pipe syscall handlers, updated file operations |
| ch8/src/process.rs | Changed fd_table to use Arc for pipe sharing |
| ch8/src/main.rs | Added pipe syscall handler, updated file operations |
| user/src/lib.rs | Added pipe_read() and pipe_write() helper functions |
| user/src/bin/sbrk.rs | Test case for sbrk syscall |
| user/src/bin/pipetest.rs | Basic pipe functionality test |
| user/src/bin/pipe_large_test.rs | Large data transfer test for pipes |
| user/src/bin/ch6_usertest.rs | Added sbrk test to ch6 test suite |
| user/src/bin/ch8_usertest.rs | Added pipetest to ch8 test suite |
| user/cases.toml | Updated test configurations for all chapters |
| Cargo.toml | Updated version dependencies |
Comments suppressed due to low confidence (1)
ch8/src/process.rs:51
- The Process struct in ch8 is missing the heap_bottom and program_brk fields that were added in ch4-ch7. This means the sbrk syscall won't work properly in ch8, as there's no place to store or track the heap state. These fields need to be added to the Process struct and properly initialized in from_elf and copied in fork.
/// 进程。
pub struct Process {
/// 不可变
pub pid: ProcId,
/// 可变
pub address_space: AddressSpace<Sv39, Sv39Manager>,
/// 文件描述符表
pub fd_table: Vec<Option<Mutex<Arc<FileHandle>>>>,
/// 信号模块
pub signal: Box<dyn Signal>,
/// 分配的锁以及信号量
pub semaphore_list: Vec<Option<Arc<Semaphore>>>,
pub mutex_list: Vec<Option<Arc<dyn MutexTrait>>>,
pub condvar_list: Vec<Option<Arc<Condvar>>>,
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
| } | ||
| vpn = vpn + 1; | ||
| } |
There was a problem hiding this comment.
The unmap implementation clears page table entries but does not perform a TLB flush (sfence.vma). After unmapping pages, the TLB may still contain stale mappings, which could cause the CPU to access freed physical pages. A TLB flush should be added after modifying the page tables.
| } | |
| } | |
| // 在修改页表后刷新 TLB,防止使用已失效的映射 | |
| unsafe { | |
| core::arch::asm!("sfence.vma"); | |
| } |
| let read_fd = current.fd_table.len(); | ||
| current.fd_table.push(Some(Mutex::new(read_end))); | ||
| let write_fd = current.fd_table.len(); | ||
| current.fd_table.push(Some(Mutex::new(write_end))); | ||
| // 将 read_fd 写入 pipe[0] | ||
| if let Some(mut ptr) = current | ||
| .address_space | ||
| .translate::<usize>(VAddr::new(pipe), WRITEABLE) | ||
| { | ||
| unsafe { *ptr.as_mut() = read_fd }; | ||
| } else { | ||
| return -1; | ||
| } | ||
| // 将 write_fd 写入 pipe[1] | ||
| if let Some(mut ptr) = current | ||
| .address_space | ||
| .translate::<usize>(VAddr::new(pipe + core::mem::size_of::<usize>()), WRITEABLE) | ||
| { | ||
| unsafe { *ptr.as_mut() = write_fd }; | ||
| } else { | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
If the first address translation succeeds (line 441-445) but the second fails (line 450-457), the function returns -1 but the pipe file descriptors have already been added to the fd_table. This leaves the process with a partially-created pipe (only the read end in the fd_table) that cannot be used properly. Consider either removing the added file descriptors on error, or performing both translations before adding any file descriptors.
| let read_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(read_end))); | |
| let write_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(write_end))); | |
| // 将 read_fd 写入 pipe[0] | |
| if let Some(mut ptr) = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe), WRITEABLE) | |
| { | |
| unsafe { *ptr.as_mut() = read_fd }; | |
| } else { | |
| return -1; | |
| } | |
| // 将 write_fd 写入 pipe[1] | |
| if let Some(mut ptr) = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe + core::mem::size_of::<usize>()), WRITEABLE) | |
| { | |
| unsafe { *ptr.as_mut() = write_fd }; | |
| } else { | |
| return -1; | |
| } | |
| // 计算即将分配的文件描述符编号,但暂时不修改 fd_table | |
| let read_fd = current.fd_table.len(); | |
| let write_fd = read_fd + 1; | |
| // 先进行两次地址转换,若失败则不对 fd_table 做任何修改 | |
| let mut ptr0 = if let Some(ptr) = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe), WRITEABLE) | |
| { | |
| ptr | |
| } else { | |
| return -1; | |
| }; | |
| let mut ptr1 = if let Some(ptr) = current | |
| .address_space | |
| .translate::<usize>( | |
| VAddr::new(pipe + core::mem::size_of::<usize>()), | |
| WRITEABLE, | |
| ) | |
| { | |
| ptr | |
| } else { | |
| return -1; | |
| }; | |
| // 地址转换均成功后再向 fd_table 中添加管道端点 | |
| current.fd_table.push(Some(Mutex::new(read_end))); | |
| current.fd_table.push(Some(Mutex::new(write_end))); | |
| // 将 read_fd 写入 pipe[0] | |
| unsafe { *ptr0.as_mut() = read_fd }; | |
| // 将 write_fd 写入 pipe[1] | |
| unsafe { *ptr1.as_mut() = write_fd }; |
|
|
||
| /// 所有写端是否都已关闭 | ||
| pub fn all_write_ends_closed(&self) -> bool { | ||
| self.write_end.as_ref().unwrap().upgrade().is_none() |
There was a problem hiding this comment.
The method all_write_ends_closed() will panic with unwrap() if write_end is None. This can happen if set_write_end() is never called. Consider handling this case gracefully or documenting the invariant that set_write_end must be called before all_write_ends_closed.
| self.write_end.as_ref().unwrap().upgrade().is_none() | |
| match self.write_end.as_ref() { | |
| Some(weak) => weak.upgrade().is_none(), | |
| None => false, | |
| } |
| /// 取消指定 VPN 范围的映射 | ||
| pub fn unmap(&mut self, range: Range<VPN<Meta>>) { | ||
| // 从 areas 中移除该范围(可能需要拆分现有区域) | ||
| let mut new_areas = Vec::new(); | ||
| for area in self.areas.drain(..) { | ||
| if area.end <= range.start || area.start >= range.end { | ||
| // 不重叠,保留原区域 | ||
| new_areas.push(area); | ||
| } else { | ||
| // 有重叠,需要拆分 | ||
| if area.start < range.start { | ||
| new_areas.push(area.start..range.start); | ||
| } | ||
| if area.end > range.end { | ||
| new_areas.push(range.end..area.end); | ||
| } | ||
| } | ||
| } | ||
| self.areas = new_areas; | ||
|
|
||
| // 清除页表项(将 PTE 设为无效,即写入 0) | ||
| let mut vpn = range.start; | ||
| while vpn < range.end { | ||
| // 使用 visitor 找到 PTE 并清除 | ||
| if let Some(pte_ptr) = self.find_pte_mut(vpn) { | ||
| unsafe { | ||
| core::ptr::write_bytes( | ||
| pte_ptr as *mut u8, | ||
| 0, | ||
| core::mem::size_of::<page_table::Pte<Meta>>(), | ||
| ) | ||
| }; | ||
| } | ||
| vpn = vpn + 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
The unmap implementation does not free the allocated physical pages. It only clears the page table entries, but the physical memory remains allocated. This causes a memory leak. The implementation should call page_manager.deallocate() or similar to free the physical pages before clearing the PTEs.
| /// 写入一个字节 | ||
| pub fn write_byte(&mut self, byte: u8) { | ||
| self.status = RingBufferStatus::Normal; | ||
| self.arr[self.tail] = byte; | ||
| self.tail = (self.tail + 1) % RING_BUFFER_SIZE; | ||
| if self.tail == self.head { | ||
| self.status = RingBufferStatus::Full; | ||
| } | ||
| } | ||
|
|
||
| /// 读取一个字节 | ||
| pub fn read_byte(&mut self) -> u8 { | ||
| self.status = RingBufferStatus::Normal; | ||
| let c = self.arr[self.head]; | ||
| self.head = (self.head + 1) % RING_BUFFER_SIZE; | ||
| if self.head == self.tail { | ||
| self.status = RingBufferStatus::Empty; | ||
| } | ||
| c | ||
| } |
There was a problem hiding this comment.
The write_byte and read_byte methods do not check if the buffer is full or empty before writing/reading. If write_byte is called when the buffer is full, it will overwrite data. If read_byte is called when the buffer is empty, it will return stale data. The caller is responsible for checking available_write/available_read first, but this could lead to bugs if the API is misused. Consider adding assertions or returning Option/Result to enforce correct usage.
| if self.pipe { | ||
| assert!(self.writable() && self.buffer.is_some()); | ||
| let want_to_write = buf.len(); | ||
| let mut buf_iter = buf.into_iter(); | ||
| let mut already_write = 0usize; | ||
| let mut ring_buffer = self.buffer.as_ref().unwrap().lock(); | ||
| let loop_write = ring_buffer.available_write(); | ||
| if loop_write == 0 { | ||
| return -2; // 缓冲区满,需等待 | ||
| } | ||
| // 写入尽可能多的数据 | ||
| for _ in 0..loop_write { | ||
| if let Some(byte_ref) = buf_iter.next() { | ||
| ring_buffer.write_byte(unsafe { *byte_ref }); | ||
| already_write += 1; | ||
| if already_write == want_to_write { | ||
| return want_to_write as _; | ||
| } | ||
| } else { | ||
| return already_write as _; | ||
| } | ||
| } | ||
| total_write_size as _ | ||
| // 缓冲区写满但还没写完,返回已写入的字节数 | ||
| already_write as _ |
There was a problem hiding this comment.
The pipe write implementation does not check if all read ends are closed. In typical Unix semantics, writing to a pipe with no readers should cause a SIGPIPE signal or return an error (EPIPE). The current implementation will succeed even when no readers exist, which deviates from standard pipe behavior and may lead to unexpected behavior.
| let (read_end, write_end) = make_pipe(); | ||
| let read_fd = current.fd_table.len(); | ||
| current.fd_table.push(Some(Mutex::new(read_end))); | ||
| let write_fd = current.fd_table.len(); | ||
| current.fd_table.push(Some(Mutex::new(write_end))); | ||
| // 将 read_fd 写入 pipe[0] | ||
| if let Some(mut ptr) = current | ||
| .address_space | ||
| .translate::<usize>(VAddr::new(pipe), WRITEABLE) | ||
| { | ||
| unsafe { *ptr.as_mut() = read_fd }; | ||
| } else { | ||
| return -1; | ||
| } | ||
| // 将 write_fd 写入 pipe[1] | ||
| if let Some(mut ptr) = current | ||
| .address_space | ||
| .translate::<usize>(VAddr::new(pipe + core::mem::size_of::<usize>()), WRITEABLE) | ||
| { | ||
| unsafe { *ptr.as_mut() = write_fd }; | ||
| } else { | ||
| return -1; |
There was a problem hiding this comment.
If the first address translation succeeds but the second fails, the function returns -1 but the pipe file descriptors have already been added to the fd_table. This leaves the process with a partially-created pipe (only the read end in the fd_table) that cannot be used properly. Consider either removing the added file descriptors on error, or performing both translations before adding any file descriptors.
| let (read_end, write_end) = make_pipe(); | |
| let read_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(read_end))); | |
| let write_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(write_end))); | |
| // 将 read_fd 写入 pipe[0] | |
| if let Some(mut ptr) = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe), WRITEABLE) | |
| { | |
| unsafe { *ptr.as_mut() = read_fd }; | |
| } else { | |
| return -1; | |
| } | |
| // 将 write_fd 写入 pipe[1] | |
| if let Some(mut ptr) = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe + core::mem::size_of::<usize>()), WRITEABLE) | |
| { | |
| unsafe { *ptr.as_mut() = write_fd }; | |
| } else { | |
| return -1; | |
| // First, translate both user-space addresses. Only proceed if both succeed. | |
| let read_ptr_opt = current | |
| .address_space | |
| .translate::<usize>(VAddr::new(pipe), WRITEABLE); | |
| let write_ptr_opt = current | |
| .address_space | |
| .translate::<usize>( | |
| VAddr::new(pipe + core::mem::size_of::<usize>()), | |
| WRITEABLE, | |
| ); | |
| if read_ptr_opt.is_none() || write_ptr_opt.is_none() { | |
| return -1; | |
| } | |
| let (read_end, write_end) = make_pipe(); | |
| let read_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(read_end))); | |
| let write_fd = current.fd_table.len(); | |
| current.fd_table.push(Some(Mutex::new(write_end))); | |
| // 将 read_fd 写入 pipe[0] | |
| if let Some(mut ptr) = read_ptr_opt { | |
| unsafe { *ptr.as_mut() = read_fd }; | |
| } | |
| // 将 write_fd 写入 pipe[1] | |
| if let Some(mut ptr) = write_ptr_opt { | |
| unsafe { *ptr.as_mut() = write_fd }; |
| use alloc::sync::{Arc, Weak}; | ||
| use spin::Mutex; | ||
|
|
||
| const RING_BUFFER_SIZE: usize = 32; |
There was a problem hiding this comment.
The RING_BUFFER_SIZE is set to 32 bytes, but according to the PR description, it should be 32KB (32 * 1024 bytes). This will severely limit the pipe capacity and may cause frequent blocking.
| const RING_BUFFER_SIZE: usize = 32; | |
| const RING_BUFFER_SIZE: usize = 32 * 1024; |
| /// 缓冲区快照,用于恢复状态 | ||
| #[derive(Copy, Clone)] | ||
| pub struct BufferSnapshot { | ||
| head: usize, | ||
| tail: usize, | ||
| status: RingBufferStatus, | ||
| } | ||
|
|
||
| /// 管道环形缓冲区 | ||
| pub struct PipeRingBuffer { | ||
| arr: [u8; RING_BUFFER_SIZE], | ||
| head: usize, | ||
| tail: usize, | ||
| status: RingBufferStatus, | ||
| write_end: Option<Weak<FileHandle>>, | ||
| } | ||
|
|
||
| impl PipeRingBuffer { | ||
| /// 创建一个管道环形缓冲区 | ||
| pub fn new() -> Self { | ||
| Self { | ||
| arr: [0; RING_BUFFER_SIZE], | ||
| head: 0, | ||
| tail: 0, | ||
| status: RingBufferStatus::Empty, | ||
| write_end: None, | ||
| } | ||
| } | ||
|
|
||
| /// 设置写端 | ||
| pub fn set_write_end(&mut self, write_end: &Arc<FileHandle>) { | ||
| self.write_end = Some(Arc::downgrade(write_end)); | ||
| } | ||
|
|
||
| /// 写入一个字节 | ||
| pub fn write_byte(&mut self, byte: u8) { | ||
| self.status = RingBufferStatus::Normal; | ||
| self.arr[self.tail] = byte; | ||
| self.tail = (self.tail + 1) % RING_BUFFER_SIZE; | ||
| if self.tail == self.head { | ||
| self.status = RingBufferStatus::Full; | ||
| } | ||
| } | ||
|
|
||
| /// 读取一个字节 | ||
| pub fn read_byte(&mut self) -> u8 { | ||
| self.status = RingBufferStatus::Normal; | ||
| let c = self.arr[self.head]; | ||
| self.head = (self.head + 1) % RING_BUFFER_SIZE; | ||
| if self.head == self.tail { | ||
| self.status = RingBufferStatus::Empty; | ||
| } | ||
| c | ||
| } | ||
|
|
||
| /// 可读取的字节数 | ||
| pub fn available_read(&self) -> usize { | ||
| if self.status == RingBufferStatus::Empty { | ||
| 0 | ||
| } else if self.tail > self.head { | ||
| self.tail - self.head | ||
| } else { | ||
| self.tail + RING_BUFFER_SIZE - self.head | ||
| } | ||
| } | ||
|
|
||
| /// 可写入的字节数 | ||
| pub fn available_write(&self) -> usize { | ||
| if self.status == RingBufferStatus::Full { | ||
| 0 | ||
| } else { | ||
| RING_BUFFER_SIZE - self.available_read() | ||
| } | ||
| } | ||
|
|
||
| /// 所有写端是否都已关闭 | ||
| pub fn all_write_ends_closed(&self) -> bool { | ||
| self.write_end.as_ref().unwrap().upgrade().is_none() | ||
| } | ||
|
|
||
| /// 保存当前状态快照 | ||
| pub fn save_state(&self) -> BufferSnapshot { | ||
| BufferSnapshot { | ||
| head: self.head, | ||
| tail: self.tail, | ||
| status: self.status, | ||
| } | ||
| } | ||
|
|
||
| /// 恢复到之前的状态 | ||
| pub fn restore_state(&mut self, snapshot: BufferSnapshot) { | ||
| self.head = snapshot.head; | ||
| self.tail = snapshot.tail; | ||
| self.status = snapshot.status; | ||
| } |
There was a problem hiding this comment.
The BufferSnapshot struct and its related methods (save_state, restore_state) are defined but never used in the codebase. This appears to be dead code that should either be removed or used for some intended purpose.
sbrk syscall (ch4-ch7): Added
heap_bottomandprogram_brkfields toProcess, implementedchange_program_brk()for dynamic heap management, addedunmap()inAddressSpaceoftg-kernel-vm.pipe syscall (ch7-ch8): Created
PipeRingBuffer(32KB ring buffer) intg-easy-fs/pipe.rs, extendedFileHandleto support pipes, changed fd_table toMutex<Arc<FileHandle>>for sharing,addedpipe_readandpipe_writemethods inuser_libupdate related test cases