Add real vfork logics

This commit is contained in:
Chen Chengjun 2025-04-03 14:16:17 +08:00 committed by Jianfeng Jiang
parent 43e43ca133
commit e4f07b76a0
8 changed files with 164 additions and 63 deletions

View File

@ -195,8 +195,18 @@ pub fn clone_child(
Ok(child_tid)
} else {
let child_process = clone_child_process(ctx, parent_context, clone_args)?;
if clone_args.flags.contains(CloneFlags::CLONE_VFORK) {
child_process.status().set_vfork_child(true);
}
child_process.run();
if child_process.status().is_vfork_child() {
let cond = || (!child_process.status().is_vfork_child()).then_some(());
let current = ctx.process;
current.children_wait_queue().wait_until(cond);
}
let child_pid = child_process.pid();
Ok(child_pid)
}
@ -435,8 +445,10 @@ fn clone_user_ctx(
// The return value of child thread is zero
child_context.set_syscall_ret(0);
if clone_flags.contains(CloneFlags::CLONE_VM) {
// if parent and child shares the same address space, a new stack must be specified.
if clone_flags.contains(CloneFlags::CLONE_VM) && !clone_flags.contains(CloneFlags::CLONE_VFORK)
{
// If parent and child shares the same address space and not in vfork situation,
// a new stack must be specified.
debug_assert!(new_sp != 0);
}
if new_sp != 0 {

View File

@ -14,6 +14,7 @@ use crate::{prelude::*, process::signal::signals::kernel::KernelSignal};
/// [`do_exit_group`]: crate::process::posix_thread::do_exit_group
pub(super) fn exit_process(thread_local: &ThreadLocal, current_process: &Process) {
current_process.status().set_zombie();
current_process.status().set_vfork_child(false);
// FIXME: This is obviously wrong in a number of ways, since different threads can have
// different file tables, and different processes can share the same file table.
@ -25,7 +26,7 @@ pub(super) fn exit_process(thread_local: &ThreadLocal, current_process: &Process
send_child_death_signal(current_process);
current_process.lock_root_vmar().clear();
current_process.lock_root_vmar().set_vmar(None);
}
/// Sends parent-death signals to the children.

View File

@ -26,8 +26,10 @@ pub use process::{
ExitCode, JobControl, Pgid, Pid, Process, ProcessBuilder, ProcessGroup, Session, Sid, Terminal,
};
pub use process_filter::ProcessFilter;
pub use process_vm::{MAX_ARGV_NUMBER, MAX_ARG_LEN, MAX_ENVP_NUMBER, MAX_ENV_LEN};
pub use program_loader::{check_executable_file, load_program_to_vm};
pub use process_vm::{
renew_vm_and_map, MAX_ARGV_NUMBER, MAX_ARG_LEN, MAX_ENVP_NUMBER, MAX_ENV_LEN,
};
pub use program_loader::{check_executable_file, ProgramToLoad};
pub use rlimit::ResourceType;
pub use term_status::TermStatus;
pub use wait::{wait_child_exit, WaitOptions};

View File

@ -9,7 +9,7 @@ use crate::{
thread_info::ThreadFsInfo,
},
prelude::*,
process::{process_vm::ProcessVm, program_loader::load_program_to_vm, Credentials, Process},
process::{process_vm::ProcessVm, program_loader::ProgramToLoad, Credentials, Process},
thread::{AsThread, Thread, Tid},
};
@ -48,7 +48,10 @@ pub fn create_posix_task_from_executable(
let fs_resolver = fs.resolver().read();
let fs_path = FsPath::new(AT_FDCWD, executable_path)?;
let elf_file = fs.resolver().read().lookup(&fs_path)?;
load_program_to_vm(process_vm, elf_file, argv, envp, &fs_resolver, 1)?
let program_to_load =
ProgramToLoad::build_from_file(elf_file, &fs_resolver, argv, envp, 1)?;
process_vm.clear_and_map();
program_to_load.load_to_vm(process_vm, &fs_resolver)?
};
let mut user_ctx = UserContext::default();

View File

@ -14,7 +14,7 @@ mod init_stack;
use aster_rights::Full;
pub use heap::Heap;
use ostd::sync::MutexGuard;
use ostd::{sync::MutexGuard, task::disable_preempt};
pub use self::{
heap::USER_HEAP_SIZE_LIMIT,
@ -83,9 +83,12 @@ impl ProcessVmarGuard<'_> {
self.inner.as_ref().unwrap()
}
/// Clears the VMAR of the binding process.
pub(super) fn clear(&mut self) {
*self.inner = None;
/// Sets a new VMAR for the binding process.
///
/// If the `new_vmar` is `None`, this method will remove the
/// current VMAR.
pub(super) fn set_vmar(&mut self, new_vmar: Option<Vmar<Full>>) {
*self.inner = new_vmar;
}
}
@ -160,10 +163,25 @@ impl ProcessVm {
&self.heap
}
/// Clears existing mappings and then maps stack and heap vmo.
pub(super) fn clear_and_map(&self) {
/// Clears existing mappings and then maps the heap VMO to the current VMAR.
pub fn clear_and_map(&self) {
let root_vmar = self.lock_root_vmar();
root_vmar.get().clear().unwrap();
self.heap.alloc_and_map_vm(&root_vmar.get()).unwrap();
}
}
/// Renews the [`ProcessVm`] of the current process and then maps the heap VMO to the new VMAR.
pub fn renew_vm_and_map(ctx: &Context) {
let process_vm = ctx.process.vm();
let mut root_vmar = process_vm.lock_root_vmar();
let new_vmar = Vmar::<Full>::new_root();
let guard = disable_preempt();
*ctx.thread_local.root_vmar().borrow_mut() = Some(new_vmar.dup().unwrap());
new_vmar.vm_space().activate();
root_vmar.set_vmar(Some(new_vmar));
drop(guard);
process_vm.heap.alloc_and_map_vm(root_vmar.get()).unwrap();
}

View File

@ -17,56 +17,89 @@ use crate::{
prelude::*,
};
/// Load an executable to root vmar, including loading programme image, preparing heap and stack,
/// initializing argv, envp and aux tables.
/// About recursion_limit: recursion limit is used to limit th recursion depth of shebang executables.
/// If the interpreter(the program behind #!) of shebang executable is also a shebang,
/// then it will trigger recursion. We will try to setup root vmar for the interpreter.
/// I guess for most cases, setting the recursion_limit as 1 should be enough.
/// because the interpreter is usually an elf binary(e.g., /bin/bash)
pub fn load_program_to_vm(
process_vm: &ProcessVm,
/// Represents an executable file that is ready to be loaded into memory and executed.
///
/// This struct encapsulates the ELF file to be executed along with its header data,
/// the `argv` and the `envp` which is required for the program execution.
pub struct ProgramToLoad {
elf_file: Dentry,
file_header: Box<[u8; PAGE_SIZE]>,
argv: Vec<CString>,
envp: Vec<CString>,
fs_resolver: &FsResolver,
recursion_limit: usize,
) -> Result<(String, ElfLoadInfo)> {
let abs_path = elf_file.abs_path();
let inode = elf_file.inode();
let file_header = {
// read the first page of file header
let mut file_header_buffer = Box::new([0u8; PAGE_SIZE]);
inode.read_bytes_at(0, &mut *file_header_buffer)?;
file_header_buffer
};
if let Some(mut new_argv) = parse_shebang_line(&*file_header)? {
if recursion_limit == 0 {
return_errno_with_message!(Errno::ELOOP, "the recursieve limit is reached");
}
new_argv.extend_from_slice(&argv);
let interpreter = {
let filename = new_argv[0].to_str()?.to_string();
let fs_path = FsPath::new(AT_FDCWD, &filename)?;
fs_resolver.lookup(&fs_path)?
}
impl ProgramToLoad {
/// Constructs a new `ProgramToLoad` from a file, handling shebang interpretation if needed.
///
/// About `recursion_limit`: recursion limit is used to limit th recursion depth of shebang executables.
/// If the interpreter(the program behind #!) of shebang executable is also a shebang,
/// then it will trigger recursion. We will try to setup root vmar for the interpreter.
/// I guess for most cases, setting the `recursion_limit` as 1 should be enough.
/// because the interpreter is usually an elf binary(e.g., /bin/bash)
pub fn build_from_file(
elf_file: Dentry,
fs_resolver: &FsResolver,
argv: Vec<CString>,
envp: Vec<CString>,
recursion_limit: usize,
) -> Result<Self> {
let inode = elf_file.inode();
let file_header = {
// read the first page of file header
let mut file_header_buffer = Box::new([0u8; PAGE_SIZE]);
inode.read_bytes_at(0, &mut *file_header_buffer)?;
file_header_buffer
};
check_executable_file(&interpreter)?;
return load_program_to_vm(
process_vm,
interpreter,
new_argv,
if let Some(mut new_argv) = parse_shebang_line(&*file_header)? {
if recursion_limit == 0 {
return_errno_with_message!(Errno::ELOOP, "the recursieve limit is reached");
}
new_argv.extend_from_slice(&argv);
let interpreter = {
let filename = new_argv[0].to_str()?.to_string();
let fs_path = FsPath::new(AT_FDCWD, &filename)?;
fs_resolver.lookup(&fs_path)?
};
check_executable_file(&interpreter)?;
return Self::build_from_file(
interpreter,
fs_resolver,
new_argv,
envp,
recursion_limit - 1,
);
}
Ok(Self {
elf_file,
file_header,
argv,
envp,
fs_resolver,
recursion_limit - 1,
);
})
}
process_vm.clear_and_map();
/// Loads the executable into the specified virtual memory space.
///
/// Returns a tuple containing:
/// 1. The absolute path of the loaded executable.
/// 2. Information about the ELF loading process.
pub fn load_to_vm(
self,
process_vm: &ProcessVm,
fs_resolver: &FsResolver,
) -> Result<(String, ElfLoadInfo)> {
let abs_path = self.elf_file.abs_path();
let elf_load_info = load_elf_to_vm(
process_vm,
&*self.file_header,
self.elf_file,
fs_resolver,
self.argv,
self.envp,
)?;
let elf_load_info =
load_elf_to_vm(process_vm, &*file_header, elf_file, fs_resolver, argv, envp)?;
Ok((abs_path, elf_load_info))
Ok((abs_path, elf_load_info))
}
}
pub fn check_executable_file(dentry: &Dentry) -> Result<()> {

View File

@ -10,10 +10,13 @@ use super::ExitCode;
///
/// This maintains:
/// 1. Whether the process is a zombie (i.e., all its threads have exited);
/// 2. The exit code of the process.
/// 2. Whether the process is the vfork child, which shares the user-space virtual memory
/// with its parent process;
/// 3. The exit code of the process.
#[derive(Debug)]
pub struct ProcessStatus {
is_zombie: AtomicBool,
is_vfork_child: AtomicBool,
exit_code: AtomicU32,
}
@ -21,6 +24,7 @@ impl Default for ProcessStatus {
fn default() -> Self {
Self {
is_zombie: AtomicBool::new(false),
is_vfork_child: AtomicBool::new(false),
exit_code: AtomicU32::new(0),
}
}
@ -44,6 +48,18 @@ impl ProcessStatus {
}
}
impl ProcessStatus {
/// Returns whether the process is the vfork child.
pub fn is_vfork_child(&self) -> bool {
self.is_vfork_child.load(Ordering::Acquire)
}
/// Sets whether the process is the vfork child.
pub fn set_vfork_child(&self, is_vfork_child: bool) {
self.is_vfork_child.store(is_vfork_child, Ordering::Release);
}
}
impl ProcessStatus {
/// Returns the exit code.
pub fn exit_code(&self) -> ExitCode {

View File

@ -15,8 +15,8 @@ use crate::{
},
prelude::*,
process::{
check_executable_file, load_program_to_vm, posix_thread::ThreadName, Credentials, Process,
MAX_ARGV_NUMBER, MAX_ARG_LEN, MAX_ENVP_NUMBER, MAX_ENV_LEN,
check_executable_file, posix_thread::ThreadName, renew_vm_and_map, Credentials, Process,
ProgramToLoad, MAX_ARGV_NUMBER, MAX_ARG_LEN, MAX_ENVP_NUMBER, MAX_ENV_LEN,
},
};
@ -118,11 +118,27 @@ fn do_execve(
drop(closed_files);
debug!("load program to root vmar");
let (new_executable_path, elf_load_info) = {
let fs_resolver = &*posix_thread.fs().resolver().read();
let process_vm = process.vm();
load_program_to_vm(process_vm, elf_file.clone(), argv, envp, fs_resolver, 1)?
};
let fs_resolver = &*posix_thread.fs().resolver().read();
let program_to_load =
ProgramToLoad::build_from_file(elf_file.clone(), fs_resolver, argv, envp, 1)?;
let process_vm = process.vm();
if process.status().is_vfork_child() {
renew_vm_and_map(ctx);
// Resumes the parent process.
process.status().set_vfork_child(false);
let parent = process.parent().lock().process().upgrade().unwrap();
parent.children_wait_queue().wake_all();
} else {
// FIXME: Currently, the efficiency of replacing the VMAR is lower than that
// of directly clearing the VMAR. Therefore, if not in vfork case we will only
// clear the VMAR.
process_vm.clear_and_map();
}
let (new_executable_path, elf_load_info) =
program_to_load.load_to_vm(process_vm, fs_resolver)?;
// After the program has been successfully loaded, the virtual memory of the current process
// is initialized. Hence, it is necessary to clear the previously recorded robust list.