A full-stack embedded Linux system built using Yocto (Kirkstone), targeting QEMU AArch64, integrating a custom Linux kernel character device driver, a multi-threaded TCP socket server, and a userspace-to-kernel ioctl interface.
Host Machine
┌──────────────────────────────────────────────────────────┐
│ bitbake / Yocto build system │
│ netcat / sockettest.sh / drivertest.sh │
│ │ │
│ ├── TCP :9000 ─────────────────────────────────┐ │
│ └── SSH :10022 (QEMU port-forwarded) ────────┐ │ │
└─────────────────────────────────────────────────────┼─┼──┘
│ │
┌─────────────────────────▼─▼───────────────────────────┐
│ QEMU (qemuarm64) — Yocto Linux Image │
│ │
│ aesdsocket (userspace daemon, port 9000) │
│ └── /dev/aesdchar (USE_AESD_CHAR_DEVICE=1) │
│ └── aesd-char-driver (kernel module) │
│ └── circular buffer (kernel) │
└───────────────────────────────────────────────────────┘
Custom Yocto layer providing:
- Kernel module recipe:
aesd-char-module— builds and installsaesd-char-driveras a loadable kernel module - Userspace application recipe:
aesd-assignments— builds and installsaesdsocketas a daemon with sysvinit integration - Image recipe:
core-image-aesd— produces a complete bootable image forqemuarm64
Build the full image:
./build.sh # sets up Yocto environment and runs bitbakeHost Machine
┌──────────────────────────────────────────────────────────────┐
│ nc / sockettest.sh │
│ connection 1 ──┐ │
│ connection 2 ──┤── TCP :9000 │
│ connection N ──┘ │
└──────────────────────────────┬───────────────────────────────┘
│ QEMU port forwarding
▼
┌──────────────────────────────────────────────────────────────┐
│ aesdsocket (daemon) │
│ │
│ main thread │
│ └── accept loop │
│ ├── connection 1 → pthread_create → worker thread 1 │
│ │ └── recv/write │
│ ├── connection 2 → pthread_create → worker thread 2 │
│ │ └── recv/write │
│ └── connection N → pthread_create → worker thread N │
│ └── recv/write │
│ │ │
│ file_mutex │
│ │ │
│ /dev/aesdchar │
│ │ │
└────────────────────────────────────┼─────────────────────────┘
│
kernel space
│
┌────────────────▼───────────────┐
│ aesd-char-driver │
│ circular buffer (10 max) │
│ mutex-protected ops │
└────────────────────────────────┘
A multi-threaded TCP daemon that:
- Listens on port 9000
- Handles concurrent clients using
pthread, one thread per connection - Accepts newline-delimited data packets per connection
- Stores received data in a backend (file or kernel device)
- Returns the full accumulated backend content to the client after each packet
Supports two storage backends, selected at compile time:
| Mode | Backend | Flag |
|---|---|---|
| Device mode (default) | /dev/aesdchar |
USE_AESD_CHAR_DEVICE=1 |
| File mode | /var/tmp/aesdsocketdata |
USE_AESD_CHAR_DEVICE=0 |
In device mode:
- Timestamp printing is disabled
- The
/dev/aesdcharnode is not removed on exit - File descriptor is opened per-connection (not at startup)
When a client sends AESDCHAR_IOCSEEKTO:X,Y (newline-terminated) over the socket:
- The string is not written to the device
X(write command index) andY(byte offset within that command) are parsedioctl(AESDCHAR_IOCSEEKTO)is issued to/dev/aesdcharon the same file descriptor- The driver repositions the read offset to entry
X, byteY - The device content from that offset is read back and returned to the client
- The same fd is used for both ioctl and read — closing/reopening would lose the offset
Example (from sockettest.sh):
# After writing swrite1..swrite10 to the device:
echo "AESDCHAR_IOCSEEKTO:0,2" | nc localhost 9000
# Returns: rite1\nswrite2\n...swrite10\n (entry 0, skipping first 2 bytes "sw")
echo "AESDCHAR_IOCSEEKTO:8,6" | nc localhost 9000
# Returns: 9\nswrite10\n (entry 8 "swrite9\n", skipping "swrite")Run with -d to daemonize:
aesdsocket -dDouble-forks, creates new session, redirects stdio to /dev/null, writes PID to /tmp/aesdsocket.pid. Handles SIGINT and SIGTERM gracefully — joins all worker threads before exit.
A Linux kernel module (aesd-char-module) implementing /dev/aesdchar with:
- Circular buffer storing up to 10 write entries (dynamically allocated per write)
- Mutex-protected read/write/ioctl operations
llseeksupport viageneric_file_llseek_sizefor absolute byte positioningioctlcommandAESDCHAR_IOCSEEKTOfor two-level seek: entry index + intra-entry offset
The driver appends each newline-terminated write as a new circular buffer entry. Reads stream content sequentially from the current file offset.
struct aesd_seekto {
uint32_t write_cmd; // circular buffer entry index (0-based)
uint32_t write_cmd_offset; // byte offset within that entry
};
#define AESDCHAR_IOCSEEKTO _IOW('k', 1, struct aesd_seekto)- One
pthreadworker thread per accepted TCP connection file_mutexserializes all device read/write/ioctl operations across threadslist_mutexprotects the TAILQ-based thread tracking list- Worker threads are tracked via a
TAILQlinked list; completed threads are joined and freed during the accept loop - On shutdown, all client sockets are
shutdown(SHUT_RDWR)to unblock blockedrecv()calls before joining
Tests run automatically via the Yocto CI pipeline using assignment-autotest:
./full-test.shDirectly writes to /dev/aesdchar and reads back using dd skip=N (byte-level seek):
# Writes write1..write10 to device, then:
dd if=/dev/aesdchar skip=2 bs=1 # skips first 2 bytes → "ite1\nwrite2\n..."
dd if=/dev/aesdchar skip=61 bs=1 # skips to entry 9 → "9\nwrite10\n"Tests the full stack end-to-end over TCP using nc:
- Sends
swrite1throughswrite10as separate connections, verifying cumulative readback after each write - Sends
AESDCHAR_IOCSEEKTO:0,2— expects readback from entry 0, byte 2 - Sends
AESDCHAR_IOCSEEKTO:8,6— expects readback from entry 8, byte 6
Each nc invocation is a separate TCP connection and thus a separate worker thread with its own file descriptor.
| Variable | Default | Description |
|---|---|---|
USE_AESD_CHAR_DEVICE |
1 |
1 = use /dev/aesdchar, 0 = use /var/tmp/aesdsocketdata |
MACHINE |
qemuarm64 |
Yocto target machine |
DISTRO |
poky |
Yocto distro (Kirkstone 4.0.x) |
| Kernel version | 5.15.x-yocto-standard |
Linux kernel for qemuarm64 |
On SIGINT or SIGTERM:
- Accept loop exits
- All active client sockets are shut down to unblock worker threads
- All worker threads are joined
- Listening socket is closed
/dev/aesdcharnode is preserved (device mode)/var/tmp/aesdsocketdatais removed (file mode only)- PID file is removed (daemon mode)
- Syslog:
Caught signal, exiting
- Single global circular buffer (10 entries max); oldest entry is overwritten when full
- No persistent storage across reboot or module reload
- No authentication or encryption on the TCP interface
- Ioctl only supports
AESDCHAR_IOCSEEKTO; no arbitrary seek beyond that
- Linux kernel character driver with
file_operations(read,write,llseek,ioctl) - Circular buffer with dynamic per-entry allocation in kernel space
- Userspace ioctl via
ioctl(2)syscall with custom command encoding (_IOW) - Multi-threaded TCP server with per-connection worker threads
- Compile-time backend switching via preprocessor flag
- POSIX signal-safe shutdown with
sig_atomic_tandsigaction - Yocto recipe authoring:
module.bbclass,update-rc.d,EXTRA_OEMAKE,SRCREVpinning