17.22.2. scull

This kernel module is a port of scull example from LDD3. It was tested on LKMC e1834763088b8a7532b5fae800039de880471f2d + 1 with Linux kernel 6.8.12.

"Scull" is an acronym for "Simple Character Utility for Loading Localities". This expansion is mostly meaningless however, but there you are.

Source code:

Create the devices and test them:

scull.sh

scull creates several character devices.

The most "basic" one is /dev/scull0, which acts a bit as an in-memory file, except that it has weird quantizations applied to it so that you can’t append as normal and it doesn’t really look like a regular file. What it actually is more like is an object pool.

The original scull interface is very weird and would erase all data on write-only O_WRONLY, but not on read/write O_RDWR, which doesn’t make much sense:

int scull_open(struct inode *inode, struct file *filp) {
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
        scull_trim(dev); /* ignore errors */

We have modified that to the much more reasonable:

	if ((filp->f_flags & O_TRUNC)) {

The old weird truncation condition makes the code hard to test as there is no way to write to two different blocks like it and keep them both in memory, unless you are able to find a CLI tool that supports O_RDWR or you write a C program to test things.

With our new inferface, we can differentiate clear all vs don’t clear all in the usual manner, e.g. this clears:

echo asdf > /dev/scull0

but this doesn’t:

echo asdf >> /dev/scull0

The examples from our test should make its weird behavior clearer e.g.:

# Append starts writing from the start of the 4k block, not like the usual semantic.
printf asdf > "$f"
printf qw >> "$f"
[ "$(cat "$f")" = qwdf ]

# Overwrite first clears everything, then writes to start of 4k block.
printf asdf > /dev/${module}0
printf qw > /dev/${module}0
[ "$(cat "$f")" = qw ]

# Read from the middle
printf asdf > /dev/${module}0
[ "$(dd if="$f" bs=1 count=2 skip=2 status=none)" = df ]

# Write to the middle
printf asdf > /dev/${module}0
printf we | dd of="$f" bs=1 seek=1 conv=notrunc status=none
[ "$(cat "$f")" = aqwf ]

It is also worth noting that the implementation of scull is meant to be "readable" but not optimal:

kmalloc is not the most efficient way to allocate large areas of memory (see Chapter 8), so the implementation chosen for scull is not a particularly smart one. The source code for a smart implementation would be more difficult to read, and the aim of this section is to show read and write, not memory management. That’s why the code just uses kmalloc and kfree without resorting to allocation of whole pages, although that approach would be more efficient.

Another shortcoming of the example is that it uses mutexes, where rwsem would be the clearly superior choice.

This module was derived from https://github.com/martinezjavier/ldd3/tree/30f801cd0157e8dfb41193f471dc00d8ca10239f/scull which had already ported it to much more recent kernel versions for us. Ideally we should just use that repo as a submodule, but we were lazy to setup the buildroot properly for now, and decided to dump it all into a single file to start with.