04 - Reading and Writing
Objectives
- Understand how to transfer data between the kernel and userspace through
readandwritefile operations, using the safeIovIterDestandIovIterSourcetypes.
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:
| Type | Allocator | Use |
|---|---|---|
KBox<T> | Kmalloc | Single owned value, physically contiguous |
VBox<T> | Vmalloc | Single large owned value |
KVBox<T> | KVmalloc | Single value, tries contiguous first |
KVec<T> | Kmalloc | Growing array, physically contiguous |
VVec<T> | Vmalloc | Large growing array |
KVVec<T> | KVmalloc | Growing 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:
| Flag | Can sleep? | Reclaim? | Atomic reserve? | When to use |
|---|---|---|---|---|
GFP_KERNEL | Yes | Yes — direct + background | No | Process context with no locks held. The default for open, ioctl, read_iter, write_iter. |
GFP_KERNEL_ACCOUNT | Yes | Yes | No | Same as GFP_KERNEL but charges the allocation to the calling process's kmemcg. Use for allocations triggered by untrusted userspace. |
GFP_NOWAIT | No | No | No | Process context where sleeping is undesirable. Fails frequently — only use when a fallback is available. |
GFP_ATOMIC | No | No | Yes | Interrupt 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.
| Flag | Effect |
|---|---|
__GFP_ZERO | Zeroes the memory before returning it. Equivalent to calloc. Example: GFP_KERNEL | __GFP_ZERO. |
__GFP_NOWARN | Suppresses the kernel's allocation-failure warning in the system log. Use when failure is expected and handled. |
__GFP_HIGHMEM | Allows 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
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 toread_iter. Data flows from the kernel to userspace: your driver writes into this destination.IovIterSource— passed towrite_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
| Method | What 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_ioctlin C receives astruct file *withprivate_dataon it; the Rust abstraction extractsprivate_databefore calling yourioctlhandler.read_iter/write_iterin C receive astruct kiocb *, which itself contains a pointer to thestruct file. The Rust abstraction therefore hands you the wholeKiocband 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.
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.
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 thaninput.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 readsppos, copies the appropriate sub-slice ofcontentsinto the iterator, advancesppos, and returns the number of bytes written. It handles the EOF case (returningOk(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)
}
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:
| Constant | Direction | Nr | Type |
|---|---|---|---|
IOCTL_SET_COUNTER | _IOW | 1 | u32 |
IOCTL_GET_COUNTER | _IOR | 2 | u32 |
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.
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.