Skip to content

Commit 838b8d5

Browse files
committed
Auto merge of rust-lang#3698 - folkertdev:sched-setaffinity, r=RalfJung
implement `libc::sched_setaffinity` on linux fixes rust-lang/miri#2749 the implementation, like `libc::sched_getaffinity`, just always returns `EINVAL`, which kind of simulates a device with zero cpus. I believe the idea is that callers of this function always do it to optimize, so they are likely to gracefully recover from this function returning an error. based on the libc crate, these functions are also available on android and freebsd (but not on macos or windows). So should the implementation of the `sched_*` functions just be copied to the android and freebsd shims?
2 parents d358f5d + d65e368 commit 838b8d5

File tree

13 files changed

+507
-37
lines changed

13 files changed

+507
-37
lines changed

src/tools/miri/src/bin/miri.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,9 @@ fn main() {
592592
let num_cpus = param
593593
.parse::<u32>()
594594
.unwrap_or_else(|err| show_error!("-Zmiri-num-cpus requires a `u32`: {}", err));
595+
if !(1..=miri::MAX_CPUS).contains(&usize::try_from(num_cpus).unwrap()) {
596+
show_error!("-Zmiri-num-cpus must be in the range 1..={}", miri::MAX_CPUS);
597+
}
595598
miri_config.num_cpus = num_cpus;
596599
} else if let Some(param) = arg.strip_prefix("-Zmiri-force-page-size=") {
597600
let page_size = param.parse::<u64>().unwrap_or_else(|err| {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use rustc_middle::ty::layout::LayoutOf;
2+
use rustc_target::abi::Endian;
3+
4+
use crate::*;
5+
6+
/// The maximum number of CPUs supported by miri.
7+
///
8+
/// This value is compatible with the libc `CPU_SETSIZE` constant and corresponds to the number
9+
/// of CPUs that a `cpu_set_t` can contain.
10+
///
11+
/// Real machines can have more CPUs than this number, and there exist APIs to set their affinity,
12+
/// but this is not currently supported by miri.
13+
pub const MAX_CPUS: usize = 1024;
14+
15+
/// A thread's CPU affinity mask determines the set of CPUs on which it is eligible to run.
16+
// the actual representation depends on the target's endianness and pointer width.
17+
// See CpuAffinityMask::set for details
18+
#[derive(Clone)]
19+
pub(crate) struct CpuAffinityMask([u8; Self::CPU_MASK_BYTES]);
20+
21+
impl CpuAffinityMask {
22+
pub(crate) const CPU_MASK_BYTES: usize = MAX_CPUS / 8;
23+
24+
pub fn new<'tcx>(cx: &impl LayoutOf<'tcx>, cpu_count: u32) -> Self {
25+
let mut this = Self([0; Self::CPU_MASK_BYTES]);
26+
27+
// the default affinity mask includes only the available CPUs
28+
for i in 0..cpu_count as usize {
29+
this.set(cx, i);
30+
}
31+
32+
this
33+
}
34+
35+
pub fn chunk_size<'tcx>(cx: &impl LayoutOf<'tcx>) -> u64 {
36+
// The actual representation of the CpuAffinityMask is [c_ulong; _].
37+
let ulong = helpers::path_ty_layout(cx, &["core", "ffi", "c_ulong"]);
38+
ulong.size.bytes()
39+
}
40+
41+
fn set<'tcx>(&mut self, cx: &impl LayoutOf<'tcx>, cpu: usize) {
42+
// we silently ignore CPUs that are out of bounds. This matches the behavior of
43+
// `sched_setaffinity` with a mask that specifies more than `CPU_SETSIZE` CPUs.
44+
if cpu >= MAX_CPUS {
45+
return;
46+
}
47+
48+
// The actual representation of the CpuAffinityMask is [c_ulong; _].
49+
// Within the array elements, we need to use the endianness of the target.
50+
let target = &cx.tcx().sess.target;
51+
match Self::chunk_size(cx) {
52+
4 => {
53+
let start = cpu / 32 * 4; // first byte of the correct u32
54+
let chunk = self.0[start..].first_chunk_mut::<4>().unwrap();
55+
let offset = cpu % 32;
56+
*chunk = match target.options.endian {
57+
Endian::Little => (u32::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
58+
Endian::Big => (u32::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
59+
};
60+
}
61+
8 => {
62+
let start = cpu / 64 * 8; // first byte of the correct u64
63+
let chunk = self.0[start..].first_chunk_mut::<8>().unwrap();
64+
let offset = cpu % 64;
65+
*chunk = match target.options.endian {
66+
Endian::Little => (u64::from_le_bytes(*chunk) | 1 << offset).to_le_bytes(),
67+
Endian::Big => (u64::from_be_bytes(*chunk) | 1 << offset).to_be_bytes(),
68+
};
69+
}
70+
other => bug!("chunk size not supported: {other}"),
71+
};
72+
}
73+
74+
pub fn as_slice(&self) -> &[u8] {
75+
self.0.as_slice()
76+
}
77+
78+
pub fn from_array<'tcx>(
79+
cx: &impl LayoutOf<'tcx>,
80+
cpu_count: u32,
81+
bytes: [u8; Self::CPU_MASK_BYTES],
82+
) -> Option<Self> {
83+
// mask by what CPUs are actually available
84+
let default = Self::new(cx, cpu_count);
85+
let masked = std::array::from_fn(|i| bytes[i] & default.0[i]);
86+
87+
// at least one thread must be set for the input to be valid
88+
masked.iter().any(|b| *b != 0).then_some(Self(masked))
89+
}
90+
}

src/tools/miri/src/concurrency/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod cpu_affinity;
12
pub mod data_race;
23
pub mod init_once;
34
mod range_object_map;

src/tools/miri/src/concurrency/thread.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
936936
// After this all accesses will be treated as occurring in the new thread.
937937
let old_thread_id = this.machine.threads.set_active_thread_id(new_thread_id);
938938

939+
// The child inherits its parent's cpu affinity.
940+
if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&old_thread_id).cloned() {
941+
this.machine.thread_cpu_affinity.insert(new_thread_id, cpuset);
942+
}
943+
939944
// Perform the function pointer load in the new thread frame.
940945
let instance = this.get_ptr_fn(start_routine)?.as_instance()?;
941946

src/tools/miri/src/eval.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ pub fn create_ecx<'tcx>(
282282
})?;
283283

284284
// Make sure we have MIR. We check MIR for some stable monomorphic function in libcore.
285-
let sentinel = ecx.try_resolve_path(&["core", "ascii", "escape_default"], Namespace::ValueNS);
285+
let sentinel =
286+
helpers::try_resolve_path(tcx, &["core", "ascii", "escape_default"], Namespace::ValueNS);
286287
if !matches!(sentinel, Some(s) if tcx.is_mir_available(s.def.def_id())) {
287288
tcx.dcx().fatal(
288289
"the current sysroot was built without `-Zalways-encode-mir`, or libcore seems missing. \

src/tools/miri/src/helpers.rs

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
1818
use rustc_middle::middle::dependency_format::Linkage;
1919
use rustc_middle::middle::exported_symbols::ExportedSymbol;
2020
use rustc_middle::mir;
21+
use rustc_middle::ty::layout::MaybeResult;
2122
use rustc_middle::ty::{
2223
self,
2324
layout::{LayoutOf, TyAndLayout},
@@ -159,6 +160,35 @@ fn try_resolve_did(tcx: TyCtxt<'_>, path: &[&str], namespace: Option<Namespace>)
159160
None
160161
}
161162

163+
/// Gets an instance for a path; fails gracefully if the path does not exist.
164+
pub fn try_resolve_path<'tcx>(
165+
tcx: TyCtxt<'tcx>,
166+
path: &[&str],
167+
namespace: Namespace,
168+
) -> Option<ty::Instance<'tcx>> {
169+
let did = try_resolve_did(tcx, path, Some(namespace))?;
170+
Some(ty::Instance::mono(tcx, did))
171+
}
172+
173+
/// Gets an instance for a path.
174+
#[track_caller]
175+
pub fn resolve_path<'tcx>(
176+
tcx: TyCtxt<'tcx>,
177+
path: &[&str],
178+
namespace: Namespace,
179+
) -> ty::Instance<'tcx> {
180+
try_resolve_path(tcx, path, namespace)
181+
.unwrap_or_else(|| panic!("failed to find required Rust item: {path:?}"))
182+
}
183+
184+
/// Gets the layout of a type at a path.
185+
#[track_caller]
186+
pub fn path_ty_layout<'tcx>(cx: &impl LayoutOf<'tcx>, path: &[&str]) -> TyAndLayout<'tcx> {
187+
let ty =
188+
resolve_path(cx.tcx(), path, Namespace::TypeNS).ty(cx.tcx(), ty::ParamEnv::reveal_all());
189+
cx.layout_of(ty).to_result().ok().unwrap()
190+
}
191+
162192
/// Call `f` for each exported symbol.
163193
pub fn iter_exported_symbols<'tcx>(
164194
tcx: TyCtxt<'tcx>,
@@ -259,23 +289,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
259289
try_resolve_did(*self.eval_context_ref().tcx, path, None).is_some()
260290
}
261291

262-
/// Gets an instance for a path; fails gracefully if the path does not exist.
263-
fn try_resolve_path(&self, path: &[&str], namespace: Namespace) -> Option<ty::Instance<'tcx>> {
264-
let tcx = self.eval_context_ref().tcx.tcx;
265-
let did = try_resolve_did(tcx, path, Some(namespace))?;
266-
Some(ty::Instance::mono(tcx, did))
267-
}
268-
269-
/// Gets an instance for a path.
270-
fn resolve_path(&self, path: &[&str], namespace: Namespace) -> ty::Instance<'tcx> {
271-
self.try_resolve_path(path, namespace)
272-
.unwrap_or_else(|| panic!("failed to find required Rust item: {path:?}"))
273-
}
274-
275292
/// Evaluates the scalar at the specified path.
276293
fn eval_path(&self, path: &[&str]) -> OpTy<'tcx> {
277294
let this = self.eval_context_ref();
278-
let instance = this.resolve_path(path, Namespace::ValueNS);
295+
let instance = resolve_path(*this.tcx, path, Namespace::ValueNS);
279296
// We don't give a span -- this isn't actually used directly by the program anyway.
280297
let const_val = this.eval_global(instance).unwrap_or_else(|err| {
281298
panic!("failed to evaluate required Rust item: {path:?}\n{err:?}")
@@ -344,19 +361,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
344361
"`libc` crate is not reliably available on Windows targets; Miri should not use it there"
345362
);
346363
}
347-
let ty = this
348-
.resolve_path(&["libc", name], Namespace::TypeNS)
349-
.ty(*this.tcx, ty::ParamEnv::reveal_all());
350-
this.layout_of(ty).unwrap()
364+
path_ty_layout(this, &["libc", name])
351365
}
352366

353367
/// Helper function to get the `TyAndLayout` of a `windows` type
354368
fn windows_ty_layout(&self, name: &str) -> TyAndLayout<'tcx> {
355369
let this = self.eval_context_ref();
356-
let ty = this
357-
.resolve_path(&["std", "sys", "pal", "windows", "c", name], Namespace::TypeNS)
358-
.ty(*this.tcx, ty::ParamEnv::reveal_all());
359-
this.layout_of(ty).unwrap()
370+
path_ty_layout(this, &["std", "sys", "pal", "windows", "c", name])
360371
}
361372

362373
/// Project to the given *named* field (which must be a struct or union type).

src/tools/miri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ pub use crate::borrow_tracker::{
129129
};
130130
pub use crate::clock::{Clock, Instant};
131131
pub use crate::concurrency::{
132+
cpu_affinity::MAX_CPUS,
132133
data_race::{AtomicFenceOrd, AtomicReadOrd, AtomicRwOrd, AtomicWriteOrd, EvalContextExt as _},
133134
init_once::{EvalContextExt as _, InitOnceId},
134135
sync::{CondvarId, EvalContextExt as _, MutexId, RwLockId, SynchronizationObjects},

src/tools/miri/src/machine.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use rustc_target::spec::abi::Abi;
3030

3131
use crate::{
3232
concurrency::{
33+
cpu_affinity::{self, CpuAffinityMask},
3334
data_race::{self, NaReadType, NaWriteType},
3435
weak_memory,
3536
},
@@ -471,6 +472,12 @@ pub struct MiriMachine<'tcx> {
471472

472473
/// The set of threads.
473474
pub(crate) threads: ThreadManager<'tcx>,
475+
476+
/// Stores which thread is eligible to run on which CPUs.
477+
/// This has no effect at all, it is just tracked to produce the correct result
478+
/// in `sched_getaffinity`
479+
pub(crate) thread_cpu_affinity: FxHashMap<ThreadId, CpuAffinityMask>,
480+
474481
/// The state of the primitive synchronization objects.
475482
pub(crate) sync: SynchronizationObjects,
476483

@@ -627,6 +634,18 @@ impl<'tcx> MiriMachine<'tcx> {
627634
let stack_addr = if tcx.pointer_size().bits() < 32 { page_size } else { page_size * 32 };
628635
let stack_size =
629636
if tcx.pointer_size().bits() < 32 { page_size * 4 } else { page_size * 16 };
637+
assert!(
638+
usize::try_from(config.num_cpus).unwrap() <= cpu_affinity::MAX_CPUS,
639+
"miri only supports up to {} CPUs, but {} were configured",
640+
cpu_affinity::MAX_CPUS,
641+
config.num_cpus
642+
);
643+
let threads = ThreadManager::default();
644+
let mut thread_cpu_affinity = FxHashMap::default();
645+
if matches!(&*tcx.sess.target.os, "linux" | "freebsd" | "android") {
646+
thread_cpu_affinity
647+
.insert(threads.active_thread(), CpuAffinityMask::new(&layout_cx, config.num_cpus));
648+
}
630649
MiriMachine {
631650
tcx,
632651
borrow_tracker,
@@ -644,7 +663,8 @@ impl<'tcx> MiriMachine<'tcx> {
644663
fds: shims::FdTable::new(config.mute_stdout_stderr),
645664
dirs: Default::default(),
646665
layouts,
647-
threads: ThreadManager::default(),
666+
threads,
667+
thread_cpu_affinity,
648668
sync: SynchronizationObjects::default(),
649669
static_roots: Vec::new(),
650670
profiler,
@@ -765,6 +785,7 @@ impl VisitProvenance for MiriMachine<'_> {
765785
#[rustfmt::skip]
766786
let MiriMachine {
767787
threads,
788+
thread_cpu_affinity: _,
768789
sync: _,
769790
tls,
770791
env_vars,

src/tools/miri/src/shims/unix/foreign_items.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ use std::str;
33

44
use rustc_middle::ty::layout::LayoutOf;
55
use rustc_span::Symbol;
6+
use rustc_target::abi::Size;
67
use rustc_target::spec::abi::Abi;
78

9+
use crate::concurrency::cpu_affinity::CpuAffinityMask;
810
use crate::shims::alloc::EvalContextExt as _;
911
use crate::shims::unix::*;
1012
use crate::*;
@@ -571,6 +573,99 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
571573
let result = this.nanosleep(req, rem)?;
572574
this.write_scalar(Scalar::from_i32(result), dest)?;
573575
}
576+
"sched_getaffinity" => {
577+
// Currently this function does not exist on all Unixes, e.g. on macOS.
578+
if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
579+
throw_unsup_format!(
580+
"`sched_getaffinity` is not supported on {}",
581+
this.tcx.sess.target.os
582+
);
583+
}
584+
585+
let [pid, cpusetsize, mask] =
586+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
587+
let pid = this.read_scalar(pid)?.to_u32()?;
588+
let cpusetsize = this.read_target_usize(cpusetsize)?;
589+
let mask = this.read_pointer(mask)?;
590+
591+
// TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
592+
let thread_id = match pid {
593+
0 => this.active_thread(),
594+
_ => throw_unsup_format!("`sched_getaffinity` is only supported with a pid of 0 (indicating the current thread)"),
595+
};
596+
597+
// The mask is stored in chunks, and the size must be a whole number of chunks.
598+
let chunk_size = CpuAffinityMask::chunk_size(this);
599+
600+
if this.ptr_is_null(mask)? {
601+
let einval = this.eval_libc("EFAULT");
602+
this.set_last_error(einval)?;
603+
this.write_scalar(Scalar::from_i32(-1), dest)?;
604+
} else if cpusetsize == 0 || cpusetsize.checked_rem(chunk_size).unwrap() != 0 {
605+
// we only copy whole chunks of size_of::<c_ulong>()
606+
let einval = this.eval_libc("EINVAL");
607+
this.set_last_error(einval)?;
608+
this.write_scalar(Scalar::from_i32(-1), dest)?;
609+
} else if let Some(cpuset) = this.machine.thread_cpu_affinity.get(&thread_id) {
610+
let cpuset = cpuset.clone();
611+
// we only copy whole chunks of size_of::<c_ulong>()
612+
let byte_count = Ord::min(cpuset.as_slice().len(), cpusetsize.try_into().unwrap());
613+
this.write_bytes_ptr(mask, cpuset.as_slice()[..byte_count].iter().copied())?;
614+
this.write_scalar(Scalar::from_i32(0), dest)?;
615+
} else {
616+
// The thread whose ID is pid could not be found
617+
let einval = this.eval_libc("ESRCH");
618+
this.set_last_error(einval)?;
619+
this.write_scalar(Scalar::from_i32(-1), dest)?;
620+
}
621+
}
622+
"sched_setaffinity" => {
623+
// Currently this function does not exist on all Unixes, e.g. on macOS.
624+
if !matches!(&*this.tcx.sess.target.os, "linux" | "freebsd" | "android") {
625+
throw_unsup_format!(
626+
"`sched_setaffinity` is not supported on {}",
627+
this.tcx.sess.target.os
628+
);
629+
}
630+
631+
let [pid, cpusetsize, mask] =
632+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
633+
let pid = this.read_scalar(pid)?.to_u32()?;
634+
let cpusetsize = this.read_target_usize(cpusetsize)?;
635+
let mask = this.read_pointer(mask)?;
636+
637+
// TODO: when https://github.com/rust-lang/miri/issues/3730 is fixed this should use its notion of tid/pid
638+
let thread_id = match pid {
639+
0 => this.active_thread(),
640+
_ => throw_unsup_format!("`sched_setaffinity` is only supported with a pid of 0 (indicating the current thread)"),
641+
};
642+
643+
if this.ptr_is_null(mask)? {
644+
let einval = this.eval_libc("EFAULT");
645+
this.set_last_error(einval)?;
646+
this.write_scalar(Scalar::from_i32(-1), dest)?;
647+
} else {
648+
// NOTE: cpusetsize might be smaller than `CpuAffinityMask::CPU_MASK_BYTES`.
649+
// Any unspecified bytes are treated as zero here (none of the CPUs are configured).
650+
// This is not exactly documented, so we assume that this is the behavior in practice.
651+
let bits_slice = this.read_bytes_ptr_strip_provenance(mask, Size::from_bytes(cpusetsize))?;
652+
// This ignores the bytes beyond `CpuAffinityMask::CPU_MASK_BYTES`
653+
let bits_array: [u8; CpuAffinityMask::CPU_MASK_BYTES] =
654+
std::array::from_fn(|i| bits_slice.get(i).copied().unwrap_or(0));
655+
match CpuAffinityMask::from_array(this, this.machine.num_cpus, bits_array) {
656+
Some(cpuset) => {
657+
this.machine.thread_cpu_affinity.insert(thread_id, cpuset);
658+
this.write_scalar(Scalar::from_i32(0), dest)?;
659+
}
660+
None => {
661+
// The intersection between the mask and the available CPUs was empty.
662+
let einval = this.eval_libc("EINVAL");
663+
this.set_last_error(einval)?;
664+
this.write_scalar(Scalar::from_i32(-1), dest)?;
665+
}
666+
}
667+
}
668+
}
574669

575670
// Miscellaneous
576671
"isatty" => {

0 commit comments

Comments
 (0)