Skip to main content

04 - Reading and Writing

Objectives

  • Understand how to transfer data between the kernel and userspace through read and write file operations, using the safe IovIterDest and IovIterSource types.

Memory allocation

Every heap allocation in the kernel requires two decisions: which allocator to use (determining the physical layout of the memory) and which GFP flags to pass (describing the context in which the allocation is made and how hard the allocator should try to satisfy it). The Rust abstractions in kernel::alloc encode both choices in the type system, so an allocation made with the wrong allocator or the wrong flags is a compile-time error rather than a runtime surprise.

The allocators

The kernel::alloc::allocator module exposes three zero-sized marker structs, each implementing the Allocator trait.

Kmalloc is the general-purpose contiguous allocator. It is backed by the slab/SLUB allocator and guarantees that the returned memory is physically contiguous. This makes it suitable for allocations up to page size and, in most cases, the right default. DMA-capable devices can use Kmalloc-allocated memory directly without a bounce buffer, provided the allocation fits within the addressable range of the device.

Vmalloc allocates whole pages from the page allocator and maps them into a contiguous region of kernel virtual address space. The physical pages are not contiguous — they are merely made to appear contiguous through the page table. This makes Vmalloc suitable for large allocations (multi-megabyte buffers, firmware images, large lookup tables) where demanding physical contiguity would cause the allocation to fail under fragmentation. The minimum alignment of a Vmalloc allocation is one full page.

KVmalloc is a hybrid: it tries Kmalloc first and falls back to Vmalloc if the contiguous allocation fails. This is the right choice when you need something larger than a typical slab allocation but would prefer contiguous memory if available, without hard-failing if the system is fragmented.

Typed allocation helpers

The allocator is baked into collection and box types as a generic parameter. The type aliases in kernel::alloc pair each allocator with the standard containers:

TypeAllocatorUse
KBox<T>KmallocSingle owned value, physically contiguous
VBox<T>VmallocSingle large owned value
KVBox<T>KVmallocSingle value, tries contiguous first
KVec<T>KmallocGrowing array, physically contiguous
VVec<T>VmallocLarge growing array
KVVec<T>KVmallocGrowing array, tries contiguous first

Use KBox and KVec for the vast majority of driver allocations. Reach for the V- or KV-prefixed variants only when sizes are in the tens or hundreds of kilobytes, or when you have evidence that contiguous allocation is failing under memory pressure.

GFP flags

Every allocation also takes a Flags value from kernel::alloc::flags. These flags tell the allocator what it is allowed to do when memory is tight. The full reference is in the kernel's Memory Allocation Guide.

Base flags

Choose exactly one of these for every allocation:

FlagCan sleep?Reclaim?Atomic reserve?When to use
GFP_KERNELYesYes — direct + backgroundNoProcess context with no locks held. The default for open, ioctl, read_iter, write_iter.
GFP_KERNEL_ACCOUNTYesYesNoSame as GFP_KERNEL but charges the allocation to the calling process's kmemcg. Use for allocations triggered by untrusted userspace.
GFP_NOWAITNoNoNoProcess context where sleeping is undesirable. Fails frequently — only use when a fallback is available.
GFP_ATOMICNoNoYesInterrupt handlers, spinlock-held sections, any hard non-preemptive context. Never use in sleepable code.

Modifier flags

These are or-ed on top of a base flag to adjust behaviour. Do not use them standalone.

FlagEffect
__GFP_ZEROZeroes the memory before returning it. Equivalent to calloc. Example: GFP_KERNEL | __GFP_ZERO.
__GFP_NOWARNSuppresses the kernel's allocation-failure warning in the system log. Use when failure is expected and handled.
__GFP_HIGHMEMAllows placement in high memory (above the direct mapping). 32-bit systems only; incompatible with Kmalloc. Rarely needed in Rust driver code.

Putting it together

The rule of thumb is: use GFP_KERNEL unless you are in a context where sleeping is forbidden, in which case use GFP_ATOMIC. Add __GFP_ZERO if you need a clean buffer. Choose KBox/KVec for most allocations, and reach for the V- or KV-prefixed variants only when sizes grow large.

use kernel::alloc::flags::{GFP_KERNEL, GFP_ATOMIC, __GFP_ZERO};

// Normal process-context allocation — can sleep, will succeed.
let config = KBox::new(MyConfig::default(), GFP_KERNEL)?;

// Zeroed buffer — contiguous and guaranteed clean.
let buf = KVec::<u8>::with_capacity(4096, GFP_KERNEL | __GFP_ZERO)?;

// Inside an interrupt handler — cannot sleep, draw from atomic reserve.
let event = KBox::new(Event { kind: IRQ_FIRED }, GFP_ATOMIC)?;

// Large allocation — try kmalloc first, fall back to vmalloc if fragmented.
let firmware = KVBox::<[u8; 1024 * 1024]>::new_uninit(GFP_KERNEL)?;

Transferring data with UserSlice

When an IOCTL handler receives a _IOR, _IOW, or _IOWR command, the arg parameter is a raw userspace pointer to a buffer. You must never dereference it directly. Instead, use UserSlice from kernel::uaccess, which wraps the kernel's copy_from_user and copy_to_user APIs and guarantees that the address range is validated and that accesses stay within the user mapping.

All methods on UserSlice and its reader/writer types are safe. Accessing a bad or unmapped address returns EFAULT rather than causing undefined behaviour.

Construction

A UserSlice is created from the raw pointer and length passed to your IOCTL handler:

use kernel::uaccess::{UserPtr, UserSlice};

fn ioctl(
device: ...,
_file: &File,
cmd: u32,
arg: usize,
) -> Result<isize> {
match cmd {
MY_IOCTL => {
let user = UserSlice::new(
UserPtr::from_usize(arg),
core::mem::size_of::<u32>(),
);
// ...
}
_ => Err(ENOTTY),
}
}

Constructing a UserSlice performs no checks. Validity is only checked when you actually read or write. The arg from an IOCTL call is always a usize; pass it through UserPtr::from_usize to get the typed pointer UserSlice::new expects.

Obtaining readers and writers

UserSlice is consumed when you call one of its three factory methods. This design enforces that you cannot accidentally build multiple independent readers over the same memory, which is the primary source of TOCTOU bugs.

UserSlice::reader consumes the slice and returns a UserSliceReader for read-only access:

let reader = UserSlice::new(UserPtr::from_usize(arg), size).reader();

UserSlice::writer returns a UserSliceWriter for write-only access:

let mut writer = UserSlice::new(UserPtr::from_usize(arg), size).writer();

UserSlice::reader_writer returns both at once, sharing the same address range. Use this for _IOWR commands where you read the input and then write the result back into the same buffer:

let (reader, mut writer) = UserSlice::new(UserPtr::from_usize(arg), size).reader_writer();

Reading from userspace

Reading a typed value

UserSliceReader::read::<T> reads exactly size_of::<T>() bytes and returns a value of type T. The type must implement FromBytes, which is implemented for all plain-integer types (u8, u16, u32, u64, i32, etc.) and for types you annotate with #[derive(FromBytes)]:

// _IOW: userspace is sending us a u32
fn handle_set_value(arg: usize) -> Result<isize> {
let mut reader = UserSlice::new(
UserPtr::from_usize(arg),
core::mem::size_of::<u32>(),
).reader();

let value: u32 = reader.read()?;
pr_info!("received value: {}\n", value);
Ok(0)
}

Reading raw bytes

UserSliceReader::read_slice fills a &mut [u8] you provide:

let mut buf = [0u8; 64];
reader.read_slice(&mut buf)?;

UserSliceReader::read_all reads everything remaining in the reader and appends it to a KVec<u8>, allocating as needed:

let mut buf = KVec::new();
reader.read_all(&mut buf, GFP_KERNEL)?;

Reading a C string

UserSliceReader::strcpy_into_buf reads a NUL-terminated string from userspace into a kernel buffer and returns a &CStr:

let mut buf = [0u8; 256];
let name: &CStr = reader.strcpy_into_buf(&mut buf)?;

Writing to userspace

Writing a typed value

UserSliceWriter::write::<T> copies size_of::<T>() bytes to userspace. The type must implement AsBytes:

// _IOR: userspace is reading a u32 from us
fn handle_get_version(arg: usize) -> Result<isize> {
let mut writer = UserSlice::new(
UserPtr::from_usize(arg),
core::mem::size_of::<u32>(),
).writer();

let version: u32 = 1;
writer.write(&version)?;
Ok(0)
}

Writing raw bytes

UserSliceWriter::write_slice copies a &[u8] to userspace:

let response = b"hello\n";
writer.write_slice(response)?;

A complete _IOWR example

A read-write IOCTL reads data from userspace, transforms it, and writes the result back. Use reader_writer to get both handles over the same memory:

fn handle_transform(arg: usize) -> Result<isize> {
let size = core::mem::size_of::<u32>();
let (reader, mut writer) = UserSlice::new(
UserPtr::from_usize(arg),
size,
).reader_writer();

let input: u32 = reader.read()?;
let output: u32 = input.wrapping_mul(2);
writer.write(&output)?;
Ok(0)
}

TOCTOU safety

The API enforces that each byte of userspace memory is read at most once. Reading advances the reader's internal cursor, so subsequent reads start from the new position. You cannot reuse a UserSlice once a reader or writer has been created from it.

If you genuinely need to read the same address twice — for example, to validate the data before acting on it — use UserSliceReader::clone_reader to get a second reader at the same position. Be aware that the data may change between the two reads, so you must treat the second read as the authoritative copy and act on it, not on the first:

let reader = UserSlice::new(UserPtr::from_usize(arg), size).reader();
let validator = reader.clone_reader(); // reads from the same address

// validate using `validator`
// act using `reader` — NOT `validator`, which was already consumed by validation
warning

Do NOT construct two separate UserSlice instances over the same address and read from each. That is the classic TOCTOU bug. Use clone_reader when a second read is unavoidable, and always act on the last copy you read.

Read and write data

When userspace calls read() or write() on your device file, the kernel does not give you a raw pointer to the userspace buffer. Instead it hands you a struct iov_iter — a kernel abstraction that can represent many different kinds of buffer at once: a single flat userspace region, a scatter-gather list from readv()/ writev(), a kernel-space buffer, a pipe, and more. Your driver only needs to call one function regardless of which kind is actually in use.

The Rust bindings expose two typed wrappers around struct iov_iter, found in the kernel::iov module:

  • IovIterDest — passed to read_iter. Data flows from the kernel to userspace: your driver writes into this destination.
  • IovIterSource — passed to write_iter. Data flows from userspace to the kernel: your driver reads from this source.

The naming is from the kernel's point of view, which is the opposite of what userspace sees: a read() call in userspace creates an IovIterDest because the user's buffer is the destination for the kernel's data, while a write() call creates an IovIterSource because the user's buffer is the source of data for the kernel.

Both callbacks also receive a Kiocb argument. Kiocb holds the current file position (ppos) and a borrow of the per-open device data, making it the bridge between the IO operation and your device state.

Kiocb — the I/O control block

When the kernel dispatches a read_iter or write_iter call to your driver, it does not pass the file and position as separate arguments. Instead it wraps them together into a struct kiocb — the kernel I/O control block — and passes a pointer to that. The Rust abstraction for it is kernel::fs::Kiocb.

pub struct Kiocb<'a, T> { /* private fields */ }

The lifetime 'a ties the Kiocb to the underlying C struct, preventing it from outliving the kernel's stack frame. The type parameter T is the driver-specific private data type that was stored in the file — the same T that appears as Self::Ptr in your MiscDevice implementation.

Where you encounter it

Kiocb appears as the first argument of read_iter and write_iter in the MiscDevice trait:

fn read_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result<usize>;
fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize>;

These are the streaming I/O paths. Every time userspace calls read(2) or write(2) on your device file, the kernel routes it through read_iter/write_iter with a freshly constructed Kiocb.

Methods

MethodWhat it gives you
kiocb.file()The driver-specific data (T::Borrowed<'a>), equivalent to what open stored
kiocb.ki_pos()The current file position as an i64
kiocb.ki_pos_mut()A mutable reference to the file position, so you can advance it
kiocb.as_raw()The raw *mut kiocb pointer, for when you need to call a C helper directly

Reading and writing with ki_pos

For character devices it is common to use the position as a byte offset into some internal buffer. You are responsible for advancing ki_pos after each transfer so that sequential read calls make forward progress:

fn read_iter(
mut kiocb: Kiocb<'_, KBox<MyDevice>>,
iov: &mut IovIterDest<'_>,
) -> Result<usize> {
let device = kiocb.file();
let pos = kiocb.ki_pos() as usize;

// Build a response at the current position.
let data: &[u8] = device.buffer_at(pos)?;

// Copy bytes into userspace via the iov iterator.
let copied = iov.copy_to_iter(data)?;

// Advance the file position.
*kiocb.ki_pos_mut() += copied as i64;

Ok(copied)
}

For most misc devices that do not represent a seekable byte stream, the position is irrelevant and you can simply ignore it — userspace cannot meaningfully lseek a misc device anyway.

The file() accessor versus ioctl

You may notice that ioctl receives the driver data through a separate device argument, while read_iter/write_iter receive it through kiocb.file(). This is just an artifact of the C calling conventions for the two paths:

  • unlocked_ioctl in C receives a struct file * with private_data on it; the Rust abstraction extracts private_data before calling your ioctl handler.
  • read_iter/write_iter in C receive a struct kiocb *, which itself contains a pointer to the struct file. The Rust abstraction therefore hands you the whole Kiocb and lets you call .file() to retrieve your data.

Both give you the same driver-specific pointer; the difference is just how you get to it.

Current limitations

The Rust Kiocb abstraction is explicitly marked incomplete in the kernel source. At the moment it only exposes the file pointer and the ki_pos field. Fields used for asynchronous I/O (ki_complete, ki_flags, ki_ioprio, etc.) are not yet wrapped, so true async I/O from Rust drivers is not yet supported through this API.

note

Because Kiocb is !Send and !Sync, you cannot store it in a struct or send it across threads. It is a short-lived view into the kernel's call stack and must be consumed within the read_iter/write_iter call that received it.

Writing data to userspace with IovIterDest

IovIterDest is your tool for implementing read_iter. The two main methods are:

  • copy_to_iter(input: &[u8]) -> usize — copies a kernel byte slice into the userspace buffer. Returns the number of bytes actually written, which may be less than input.len() if the iterator is shorter than the data you are trying to write.
  • simple_read_from_buffer(ppos: &mut i64, contents: &[u8]) -> Result<usize> — a higher-level helper for the common case where your device exposes a fixed byte sequence (like a status string or a counter). It reads ppos, copies the appropriate sub-slice of contents into the iterator, advances ppos, and returns the number of bytes written. It handles the EOF case (returning Ok(0)) automatically.

The simplest possible read_iter implementation exposes a static string:

fn read_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result<usize> {
iov.simple_read_from_buffer(kiocb.ppos(), b"hello from kernel\n")
}

For cases where you need more control — for example, copying data from a dynamically sized kernel buffer — use copy_to_iter directly and update the position yourself:

fn read_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result<usize> {
let device = kiocb.device();
let ppos = kiocb.ppos();

let data = device.buffer.lock();

let pos = usize::try_from(*ppos).map_err(|_| EINVAL)?;
if pos >= data.len() {
return Ok(0); // EOF
}

let written = iov.copy_to_iter(&data[pos..]);
*ppos += written as i64;
Ok(written)
}

len() and is_empty() are available on IovIterDest to check how many bytes the caller has room for before committing to any copies, which is useful when building output in chunks.

Reading data from userspace with IovIterSource

IovIterSource is the mirror image, used in write_iter. The primary method is:

  • copy_from_iter(out: &mut [u8]) -> usize — copies bytes from the userspace buffer into a kernel slice. Returns the number of bytes actually read.

A simple write_iter that stores incoming bytes in a device buffer:

fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize> {
let device = kiocb.device();
let mut buf = [0u8; 256];

// Read however many bytes the iterator holds, up to our buffer size.
let n = iov.copy_from_iter(&mut buf);
if n == 0 {
return Ok(0);
}

device.buffer.lock().extend_from_slice(&buf[..n]);
Ok(n)
}

If you need a buffer whose size matches exactly what the caller is trying to write, check iov.len() first and allocate accordingly:

fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize> {
let incoming = iov.len();
if incoming == 0 {
return Ok(0);
}
if incoming > MAX_PAYLOAD {
return Err(EINVAL);
}

let mut buf = KVec::with_capacity(incoming, GFP_KERNEL)?;
buf.resize(incoming, 0, GFP_KERNEL)?;
let n = iov.copy_from_iter(&mut buf);

process(&buf[..n])?;
Ok(n)
}
Use the return value, not len()

iov.len() may over-estimate the available data. For example, if part of the userspace buffer spans a page that cannot be faulted in, the actual copy will stop short. Always use the return value of copy_from_iter — not iov.len() — as the authoritative byte count, and base your file position update on that value.

Registering the callbacks

Both read_iter and write_iter are opt-in. The #[vtable] macro only wires them into file_operations when you provide an implementation, so adding one does not affect the other:

#[vtable]
impl MiscDevice for MyDeviceInstance {
type Ptr = KBox<Self>;

fn open(_file: &File, _reg: &MiscDeviceRegistration<Self>) -> Result<KBox<Self>> {
Ok(KBox::new(MyDeviceInstance::new()?, GFP_KERNEL)?)
}

fn read_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterDest<'_>) -> Result<usize> {
// ...
}

fn write_iter(kiocb: Kiocb<'_, Self::Ptr>, iov: &mut IovIterSource<'_>) -> Result<usize> {
// ...
}
}

If neither method is provided, open() still works and userspace will receive EINVAL for any read() or write() attempt on the device file.

Exercises

The exercises below are split into two groups. The first group uses ioctl with UserSlice to exchange structured data with userspace. The second group uses the read_iter and write_iter callbacks with IovIter to implement streaming I/O.

All exercises build on the same misc device skeleton. Your device should store a small amount of mutable state — a u32 counter and a fixed-size byte buffer — protected by a Mutex inside an Arc that is shared between open instances.


Part 1 — ioctl and UserSlice

Exercise 1 — Echo a u32 back to userspace

Define an _IOWR command with magic byte 'J', number 0, and data type u32. When userspace calls the ioctl, read one u32 from the arg pointer, add one to it, and write the result back to the same pointer.

Use UserSlice::new(UserPtr::from_usize(arg), size_of::<u32>()) to construct the slice, then split it into a reader and writer.

After implementing it, verify with the generic ioctl tool:

ioctl /dev/mydevice -t J -n 0 -d read-write -s 4 -v 41
# should print back: 42
Hint

Call .reader() on the UserSlice to get a UserSliceReader, call .read::<u32>()? to get the value, then construct a new UserSlice at the same address (or use clone_reader before reading) and call .writer() followed by .write(&result)?.

You cannot split a single UserSlice into both a reader and writer simultaneously — you need two separate UserSlice instances at the same address, one for reading and one for writing.


Exercise 2 — Set and get the device counter

Define two commands:

ConstantDirectionNrType
IOCTL_SET_COUNTER_IOW1u32
IOCTL_GET_COUNTER_IOR2u32

IOCTL_SET_COUNTER reads a u32 from userspace and stores it in the shared counter. IOCTL_GET_COUNTER writes the current value of the counter out to userspace.

Test them in sequence:

ioctl /dev/mydevice -t J -n 1 -d write -s 4 -v 100
ioctl /dev/mydevice -t J -n 2 -d read -s 4
# should print: 100
Hint

For _IOR the kernel writes to the userspace pointer — the direction name describes the direction from userspace's perspective, not the kernel's. Use a UserSliceWriter obtained from UserSlice::new(...).writer() and call .write(&counter_value)?.


Exercise 3 — Transfer a fixed-size struct

Define a C-compatible struct with #[repr(C)]:

#[repr(C)]
struct Point {
x: i32,
y: i32,
}

Define an _IOWR command with number 3 and data type Point. When called, the kernel should read the Point from userspace, negate both coordinates, and write the result back.

warning

Point must implement kernel::uaccess::FromBytes for reading and kernel::uaccess::AsBytes for writing. Both are marker traits that assert the type has no invalid bit patterns and no padding. Add unsafe impl FromBytes for Point {} and unsafe impl AsBytes for Point {} only after verifying that the struct is fully repr(C) and has no padding between fields.

Hint

After implementing both marker traits, call reader.read::<Point>()? to get the struct from userspace in one call, and writer.write(&negated)? to send it back. The size passed to UserSlice::new should be size_of::<Point>().


Exercise 4 — Copy a variable-length byte buffer

Define an _IOW command with number 4 that accepts a pointer to userspace memory containing arbitrary bytes. Because variable-length buffers cannot be encoded in the IOCTL size field reliably, define the ioctl with a data size of u64 and pass a (pointer, length) pair encoded in a struct:

#[repr(C)]
struct UserBuf {
ptr: u64,
len: u64,
}

When called, read the UserBuf descriptor from arg, then construct a second UserSlice at ptr with length len (clamped to the internal buffer size), read all the bytes with .read_all(GFP_KERNEL)?, and store them in the device.

Hint

UserSliceReader::read_all(gfp) returns a KVec<u8> containing all the remaining bytes in the reader. Use the len field to guard against excessively large inputs before constructing the second UserSlice.


Exercise 5 — Reject unknown commands correctly

Add a catch-all arm to your match cmd block and return Err(ENOTTY).

Then verify that your device correctly rejects a bogus command:

ioctl /dev/mydevice -t J -n 99 -d none
# should fail with ENOTTY (errno 25)

Explain why ENOTTY is the correct error for an unrecognised ioctl rather than EINVAL or ENOSYS.


Part 2 — read_iter, write_iter, and IovIter

For this section, implement the read_iter and write_iter callbacks in your MiscDevice. The device should behave like a simple pipe: bytes written with write are stored in an internal ring buffer, and bytes read with read are consumed from the front of that buffer. Keep things simple and use a fixed-size [u8; 256] buffer with a length counter inside the Mutex.


Exercise 6 — Implement write_iter

Implement write_iter so that calling write(2) on your device stores bytes into the internal buffer. The handler receives an IovIterSource, which represents data coming from userspace.

Use iov.copy_from_iter(buf_slice)? to pull bytes from the iterator into a kernel buffer, then append them to the device's internal storage. Return the number of bytes actually written.

echo -n "hello" > /dev/mydevice
Hint

IovIterSource::copy_from_iter takes a &mut [u8] and fills it from the userspace iterator. The number of bytes actually copied is its return value — do not assume the slice was filled completely. Advance ki_pos accordingly.

The write_iter callback receives Kiocb<'_, Self::Ptr> as its first argument. Call kiocb.file() to get a borrow of your driver data and lock the mutex to access the buffer.


Exercise 7 — Implement read_iter

Implement read_iter so that calling read(2) on your device drains bytes from the front of the internal buffer into userspace. The handler receives an IovIterDest, which represents a destination in userspace.

Use iov.copy_to_iter(data_slice)? to push kernel bytes into the iterator. Return the number of bytes delivered. If the buffer is empty, return Ok(0) to signal EOF.

cat /dev/mydevice
# should print: hello
Hint

IovIterDest::copy_to_iter takes a &[u8] and copies it into userspace, returning the number of bytes actually transferred. After copying, remove the consumed bytes from your internal buffer (shift remaining bytes to the front, or use a proper ring buffer index).

Return Ok(0) — not an error — when there is nothing left to read. Returning Ok(0) is what signals end-of-file to userspace.


Exercise 8 — Enforce a buffer limit on write_iter

Extend write_iter so that it refuses to accept more bytes than the internal buffer can hold. If the buffer is full, return Err(ENOSPC). If only some bytes fit, accept those and return the count of accepted bytes rather than an error.

Test the boundary:

head -c 300 /dev/zero | tr '\0' 'A' > /dev/mydevice
# should fail or write only 256 bytes depending on how full the buffer is

Exercise 9 — Use simple_read_from_buffer

Replace the manual copy_to_iter call in read_iter with IovIterDest::simple_read_from_buffer. This helper takes a kernel byte slice and a mutable position offset, copies as many bytes as fit, advances the position, and returns the count — handling all the boundary arithmetic for you.

Observe how the position tracking in simple_read_from_buffer maps to the ki_pos field on the Kiocb. When would you use simple_read_from_buffer versus copy_to_iter directly?

Hint

simple_read_from_buffer(iov, data, pos) expects pos to be a &mut u64 tracking the read position within data. It copies data[*pos..] into iov and increments *pos by the number of bytes copied. It is best suited for read-only, seekable, fixed-size data (like a /proc-style file). For a consuming ring buffer, the manual approach from Exercise 7 is more natural.


Exercise 10 — Round-trip test

Write a program (in userspace, any language) that opens your device twice — once for writing and once for reading — writes a known payload through one file descriptor, reads it back through the other, and asserts that the two byte sequences match.

Then open a third file descriptor and verify that all three share the same underlying buffer (because the Arc<Mutex<...>> is shared across all open instances). Write through fd3 and read through fd1 to confirm.

This exercise does not require any kernel changes — it is purely a verification of the Arc-sharing design you established in the open callback.