-
Notifications
You must be signed in to change notification settings - Fork 120
Add RCU read bindings (#143) and each_process() iterator #250
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
//! Bindings to RCU (read-copy-update), a high-performance lockless | ||
//! synchronization system used by many kernel data structures. At the | ||
//! moment, only calling functions that perform RCU reads is supported. | ||
|
||
extern "C" { | ||
fn rcu_read_lock_helper(); | ||
fn rcu_read_unlock_helper(); | ||
} | ||
|
||
/// A guard representing an RCU read-side critical section. Its | ||
/// constructor calls `rcu_read_lock()` and its destructor calls | ||
/// `rcu_read_unlock()`. | ||
/// | ||
/// Within a read-side critical section (i.e., while at least one | ||
/// RcuReadGuard object is instantiated), objects behind RCU-protected | ||
/// pointers are guaranteed not to change, and so reading from them | ||
/// (after gaining a pointer with `rcu_dereference()`) is safe. | ||
/// | ||
/// It is an error (risk of deadlock, but not memory unsafety) to block | ||
/// or schedule while holding an RcuReadGuard. It is also an error | ||
/// (guaranteed deadlock) to call `synchronize_rcu()` while holding an | ||
/// RcuReadGuard. Holding multiple guards (i.e., nesting read-side | ||
/// critical sections) is safe. | ||
pub struct RcuReadGuard(()); | ||
|
||
#[allow(clippy::new_without_default)] | ||
impl RcuReadGuard { | ||
pub fn new() -> Self { | ||
unsafe { | ||
rcu_read_lock_helper(); | ||
} | ||
RcuReadGuard(()) | ||
} | ||
} | ||
|
||
impl Drop for RcuReadGuard { | ||
fn drop(&mut self) { | ||
unsafe { | ||
rcu_read_unlock_helper(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
//! APIs for interacting with the scheduler and with processes, | ||
//! corresponding to <linux/sched.h> and related header files. | ||
#![allow(improper_ctypes)] | ||
|
||
use core::ptr; | ||
|
||
use crate::bindings; | ||
use crate::rcu; | ||
|
||
extern "C" { | ||
fn task_lock_helper(p: *mut bindings::task_struct); | ||
fn task_unlock_helper(p: *mut bindings::task_struct); | ||
fn next_task_helper(p: *mut bindings::task_struct) -> *mut bindings::task_struct; | ||
} | ||
|
||
/// Represents a `struct task_struct *`. | ||
pub struct TaskStruct<'a>(&'a mut bindings::task_struct); | ||
|
||
impl TaskStruct<'_> { | ||
/// Returns the threadgroup ID (what userspace calls the process ID). | ||
pub fn tgid(&self) -> i32 { | ||
self.0.tgid | ||
} | ||
|
||
/// Returns the command name / process title. This is a short name, | ||
/// typically the base name of the command, and does not have the | ||
/// full path or arguments. It's a fixed-sized set of bytes, but by | ||
/// convention it's interpreted as NUL-terminated. | ||
pub fn comm(&mut self) -> [u8; bindings::TASK_COMM_LEN as usize] { | ||
let mut result = [0u8; bindings::TASK_COMM_LEN as usize]; | ||
unsafe { | ||
task_lock_helper(self.0); | ||
} | ||
// if only char were unsigned char | ||
for (src, dst) in self.0.comm.iter().zip(result.iter_mut()) { | ||
if *src == 0 { | ||
break; | ||
} | ||
*dst = *src as _; | ||
} | ||
unsafe { | ||
task_unlock_helper(self.0); | ||
} | ||
result | ||
} | ||
} | ||
|
||
/// Iterate over every process on the system. Returns only processes, | ||
/// i.e., thread group leaders. | ||
/// | ||
/// ``` | ||
/// let g = rcu::RcuReadGuard::new(); | ||
/// for p in each_process(&g) { | ||
/// println!("{:?}", p.comm()); | ||
/// } | ||
geofft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// ``` | ||
struct EachProcess<'g> { | ||
p: *mut bindings::task_struct, | ||
_g: &'g rcu::RcuReadGuard, | ||
} | ||
|
||
pub fn each_process(g: &rcu::RcuReadGuard) -> impl Iterator<Item = TaskStruct> { | ||
// unsafe is bogus here because we don't read it | ||
// https://github.com/rust-lang/rust/issues/74843 | ||
alex marked this conversation as resolved.
Show resolved
Hide resolved
|
||
EachProcess { | ||
p: unsafe { &mut bindings::init_task }, | ||
_g: g, | ||
} | ||
} | ||
|
||
impl<'g> Iterator for EachProcess<'g> { | ||
type Item = TaskStruct<'g>; | ||
|
||
fn next(&mut self) -> Option<TaskStruct<'g>> { | ||
// Safety: | ||
// - oldp is valid if not null, because it is either &init_task | ||
// (a static location) or updated by this function. | ||
// - next_task calls rcu_dereference internally, which is safe | ||
// because we hold self._g. | ||
// - The returned reference has lifetime 'g, which is valid | ||
// because self._g lives at least that long. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please note that holding RCU doesn't guarantee the task list to be unchanged, RCU read-side critical section here only guarantees that the task_struct we reference here won't get freed (in side the RCU read-side critical section). We use another lock "tasklist_lock" to protect the tasklist. |
||
let oldp = unsafe { self.p.as_mut()? }; | ||
self.p = unsafe { next_task_helper(self.p) }; | ||
if self.p == unsafe { &mut bindings::init_task } { | ||
self.p = ptr::null_mut(); | ||
} | ||
Some(TaskStruct(oldp)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "for-each-process-tests" | ||
version = "0.1.0" | ||
authors = ["Alex Gaynor <[email protected]>", "Geoffrey Thomas <[email protected]>"] | ||
edition = "2018" | ||
|
||
[lib] | ||
crate-type = ["staticlib"] | ||
test = false | ||
|
||
[features] | ||
default = ["linux-kernel-module"] | ||
|
||
[dependencies] | ||
linux-kernel-module = { path = "../..", optional = true } | ||
|
||
[dev-dependencies] | ||
kernel-module-testlib = { path = "../../testlib" } | ||
libc = "0.2.58" | ||
tempfile = "3" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
#![no_std] | ||
|
||
use linux_kernel_module::{self, println, rcu, sched}; | ||
|
||
struct ForEachProcessTestModule; | ||
|
||
impl linux_kernel_module::KernelModule for ForEachProcessTestModule { | ||
fn init() -> linux_kernel_module::KernelResult<Self> { | ||
let g = rcu::RcuReadGuard::new(); | ||
for mut p in sched::each_process(&g) { | ||
let comm = p.comm(); | ||
let comm_until_nul = comm.split(|c| *c == 0).next().unwrap(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will there be a null if the comm name uses up the full buffer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the buffer as returned by the kernel always includes at least one trailing NUL, but more importantly, split() always returns at least one element so we're fine either way. |
||
println!( | ||
"for-each-process: {:8} {}", | ||
p.tgid(), | ||
core::str::from_utf8(comm_until_nul).unwrap_or("[invalid UTF-8]") | ||
); | ||
} | ||
Ok(ForEachProcessTestModule) | ||
} | ||
} | ||
|
||
linux_kernel_module::kernel_module!( | ||
ForEachProcessTestModule, | ||
author: b"Fish in a Barrel Contributors", | ||
description: b"A module for testing EachProcess", | ||
license: b"GPL" | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
use std::fs::File; | ||
use std::io::Write; | ||
|
||
use kernel_module_testlib::{assert_dmesg_contains, with_kernel_module}; | ||
|
||
#[test] | ||
fn test_for_each_process() { | ||
File::create("/proc/self/comm") | ||
.unwrap() | ||
.write_all(b"areyouthere") | ||
.unwrap(); | ||
with_kernel_module(|| { | ||
assert_dmesg_contains(&[b"areyouthere"]); | ||
geofft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On thinking more... do we need a type that's backed by a fixed size array, but maintains a "how much of this is valid"
usize
and derefs to slice/mut slice?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. smallvec is basically this except that it falls back to Vec; I think we want one that just doesn't fall back.
While we're at it I also want to bury the i8/u8 nonsense under an abstraction too (i.e.
impl Deref<Target=[u8]>
for something constructed from an&[i8]
). Really what I think I want isOsStr
from libstd :(There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://crates.io/crates/arrayvec