OS interface#1439
Conversation
wks
left a comment
There was a problem hiding this comment.
Some High-level comments:
windows crate or windows-sys crate
There is a windows crate and it claims to be more idiomatic for Rust than windows-sys (for example, windows uses Option<*mut XxxxxXxxType> where windows-sys uses raw *mut XxxxxXxxType if it is nullable). Personally I think we only call a limited number of Windows API functions, so it doesn't really matter.
Granularity or reserving and committing
According to the documentation VirtualAlloc,
- The granularity of reserving and the granularity of committing may be different, and
- They are not constants, but can be queried using the
GetSystemInfo()function.
We don't have to fix it in this PR because Memory::dzmmap maps the exactly memory range the user requested, which is usually a chunk. But this is a reminder that we should make a related PR to remove the hard-coded 4K page size because MacOS and Android use different page sizes, too.
| while addr < end { | ||
| let mut mbi: MEMORY_BASIC_INFORMATION = std::mem::zeroed(); | ||
| let q = VirtualQuery( | ||
| addr as *const _, | ||
| &mut mbi, | ||
| std::mem::size_of::<MEMORY_BASIC_INFORMATION>(), | ||
| ); | ||
| if q == 0 { | ||
| return Err(io::Error::last_os_error()); | ||
| } | ||
|
|
||
| let region_base = mbi.BaseAddress as *mut u8; | ||
| let region_size = mbi.RegionSize; | ||
| let region_end = region_base.add(region_size); | ||
|
|
||
| // Calculate the intersection of [addr, end) and [region_base, region_end) | ||
| let _sub_begin = if addr > region_base { | ||
| addr | ||
| } else { | ||
| region_base | ||
| }; | ||
| let _sub_end = if end < region_end { end } else { region_end }; | ||
|
|
||
| match mbi.State { | ||
| MEM_FREE => saw_free = true, | ||
| MEM_RESERVE => saw_reserved = true, | ||
| MEM_COMMIT => saw_committed = true, | ||
| _ => { | ||
| return Err(io::Error::other("Unexpected memory state in mmap_fixed")); | ||
| } | ||
| } | ||
|
|
||
| // Jump to the next region (VirtualQuery always returns "continuous regions with the same attributes") | ||
| addr = region_end; | ||
| } | ||
|
|
||
| // 1. All FREE: make a new mapping in the region | ||
| // 2. All RESERVE/COMMIT: treat as an existing mapping, can just COMMIT or succeed directly | ||
| // 3. MIX of FREE + others: not allowed (semantically similar to MAP_FIXED_NOREPLACE) | ||
| if saw_free && (saw_reserved || saw_committed) { | ||
| return Err(io::Error::from_raw_os_error( | ||
| windows_sys::Win32::Foundation::ERROR_INVALID_ADDRESS as i32, | ||
| )); | ||
| } | ||
|
|
||
| if saw_free && !saw_reserved && !saw_committed { | ||
| // All FREE: make a new mapping in the region | ||
| let mut allocation_type = MEM_RESERVE; | ||
| if commit { | ||
| allocation_type |= MEM_COMMIT; | ||
| } | ||
|
|
||
| let res = VirtualAlloc( | ||
| ptr as *mut _, | ||
| size, | ||
| allocation_type, | ||
| strategy.prot.get_native_flags(), | ||
| ); | ||
| if res.is_null() { | ||
| return Err(io::Error::last_os_error()); | ||
| } | ||
|
|
||
| Ok(start) | ||
| } else { |
There was a problem hiding this comment.
Despite all the VirtualQuery checks we do, the subsequent VirtualAlloc may still fail in multi-threaded programs due to TOCTOU. And it takes time if the given region contains multiple mmap entries. But it is useful for sanity check.
I suggest we guard all the checks with a feature or debug_assertion so that we don't do them in production. (It's even better if we can extract all the sanity check parts to a separate function so that we can reuse it.) In production, we only call VirtualAlloc. And we can port the sanity check to Unix-like systems by parsing /proc/self/maps (or using a third-party crate for parsing it), but that still has the TICTOU problem and can only serve debug purposes.
Ideally, we should reserve the region of memory for metadata and the heap so that we won't need such checks.
| // If decommit fails, we try to release the memory. This might happen if the memory was | ||
| // only reserved. | ||
| let res_release = unsafe { VirtualFree(start.to_mut_ptr(), 0, MEM_RELEASE) }; | ||
| if res_release == 0 { | ||
| Err(std::io::Error::last_os_error()) | ||
| } else { | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
We shouldn't use MEM_RELEASE.
MEM_RELEASEis supposed to be paired withVirualAllocand be used as if they were malloc-free pairs. The doc says "If you specify this value (MEM_RELEASE), dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function when the region is reserved. The function fails if either of these conditions is not met." Obviously this is completely different from how we usemunmap.- It will remove the reserved state. We don't do it, just like we don't "un-quarantine" memory in Unix-like systems.
Instead if MEM_DECOMMIT fails, we should return failure.
| // If decommit fails, we try to release the memory. This might happen if the memory was | |
| // only reserved. | |
| let res_release = unsafe { VirtualFree(start.to_mut_ptr(), 0, MEM_RELEASE) }; | |
| if res_release == 0 { | |
| Err(std::io::Error::last_os_error()) | |
| } else { | |
| Ok(()) | |
| } | |
| Err(std::io::Error::last_os_error()) |
|
I don't intend to include Windows-related changes in this PR for merging. See the PR description. This PR is only about OS interface refactoring. I will keep Windows-related comments unresolved, and will copy them to the Windows support PR. |
wks
left a comment
There was a problem hiding this comment.
We should ensure that all text files in mmtk-core use LF (instead of CR LF or CR-only) line ends and ensure that the last line always ends with an LF. By doing so, we can avoid the complexity of handling line ending in CI scripts.
|
We also removed |
ad3c6a7 to
474d60a
Compare
Just to clarify, this PR doesn't remove |
|
|
||
| /// Get the process ID as a string. | ||
| /// Fallback: This is only used for debugging. For unimplemented cases, this function can return a placeholder Ok value. | ||
| fn get_process_id() -> Result<String>; |
There was a problem hiding this comment.
Returning the process ID as string is not as useful as returning the raw value. If some other parts of MMTk-core is OS-specific, they may use the raw integer value.
We can let each OS define its PID type because they may be different.
pub trait OSProcess {
type ProcessIDType: Display;
type ThreadIDType: Display;
fn get_process_id() -> Result<Self::ProcessIDType>;
fn get_thread_id() -> Result<Self::ThreadIDType>;
}On Linux, both PID and TID have the pid_t type whch is an alias of i32.
impl OSProcess for Linux {
type ProcessIDType = libc::pid_t;
type ThreadIDType = libc::pid_t;
fn get_process_id() -> Result<Self::ProcessIDType> {
Ok(unsafe { libc::getpid() })
}
fn get_thread_id() -> Result<Self::ThreadIDType> {
Ok(unsafe { libc::gettid() })
}
}On Windows, both PID and TID are DWORD (u32). MacOS may need to use pthread_threadid_np for TID. (see https://stackoverflow.com/questions/19247228/what-is-thread-id-of-posix-on-mac-osx)
Because both the PID type and the TID type are required to implement Display, debug_process_thread_id can still directly use them in formatting.
There was a problem hiding this comment.
I made the change. I think just requiring Display for pid/tid won't sufficient. I also added Eq and Copy so we can do equity compare and pass by value. We may need to expand this requirement in the future.
If some other parts of MMTk-core is OS-specific, they may use the raw integer value.
I don't think OS-specific code really should use the OS interface, as the OS interface is for non OS specific code. OS specific code can always directly call getpid etc.
| } | ||
|
|
||
| pub fn get_thread_id() -> Result<ThreadIDType> { | ||
| Ok(unsafe { libc::pthread_self() }) |
There was a problem hiding this comment.
Linux has the gettid() system call, available in the libc crate in Rust, although it doesn't have a C language wrapper in libc. I think it is better than pthread_self on Linux because that is what Linux users call "TID", and it can be shown by the command ps -efL or in the top utility by pressing the 'H' key. It is confusing to call the return value of pthread_self "TID" on Linux.
Android should be able to use gettid(), too.
There was a problem hiding this comment.
The thread ID here is for MMTk. It doesn't matter if it is the pthread or the tid, as long as it can uniquely identify a thread for MMTk.
MacOS does not have gettid, while all those systems have pthread pointers. That's why we used pthread_self.
There was a problem hiding this comment.
Well, one important use case is GDB and rr. But GDB shows both the "Thread ID" (pthread_self()) and the LWP (gettid()). So it should be OK to use pthread_self as the "thread ID".
|
@wks I have addressed the comments above. Can you review again? |
| use crate::util::Address; | ||
| use crate::vm::VMBinding; | ||
|
|
||
| /// Allocate with alignment. This also guarantees the memory is zero initialized. |
There was a problem hiding this comment.
Why is this comment removed? The function still guarantees the memory is zero initialized.
There was a problem hiding this comment.
That was unintended. I removed some windows related cfgs there. I added it back.
This PR addresses #1420.
This PR is based on top of #1418, and includes all the changes for Windows (for testing purpose). It is likely that Windows support will be removed from this PR, and will be merged separately.
This PR does not try to refactor our malloc interface -- I am not sure if malloc should be included in the OS interface or not.
This PR consolidates the current multiple mmap functions (such as dzmmap, mmap_fixed, mmap_noreplace, mmap_noreserve, etc), and use
MmapStrategyto specify the expected mmap behavior.