Fachprojekt „Systemsoftwaretechnik”

03 - Der eigene Treiber

Alexander Krause

11.06.2024
🚀 by Decker

Wie baue ich ein neues Feature ein?

  • Grundstruktur anlegen
  • Konfiguration erstellen
  • Code schreiben

Grundstruktur

Was benötige ich alles für meinen Treiber?

gen/dirlist.svg
  • Verzeichnis: fs/ext4
  • Beschreibung, was gebaut werden soll: fs/ext4/Kconfig
  • Beschreibung, was alles dazugehört: fs/ext4/Makefile
  • Natürlich den Quellcode selbst: fs/ext4/code.c

Wie baue ich ein neues Feature ein?

  • Grundstruktur anlegen ✅
  • Konfiguration erstellen
  • Code schreiben

Konfiguration

Kconfig (Wdh.)

fig/menuconfig.png
  • Konfigurationssprache des Linux-Kerns
  • Verwaltet Konfigurationsoptionen zur Übersetzungszeit
  • Berücksichtigt Abhängigkeiten und setzt diese durch
  • Bedeutung der Symbole
    • < > keine Abhängigkeiten
    • [ ] kann einkompiliert (y) werden oder nicht (n)
    • { } als Modul (m) oder einkompiliert (y) benötigt
    • - - einkompiliert (y) benötigt

Kconfig unter der Haube

config EXT4_FS
    tristate "The Extended 4 (ext4) filesystem"
    select JBD2
    select CRC16
    select CRYPTO
    select CRYPTO_CRC32C
    select FS_IOMAP
    select FS_ENCRYPTION_ALGS if FS_ENCRYPTION
    help
      This is the next generation of the ext3 filesystem.
      [...]
      If unsure, say N.

(Aus fs/ext4/Kconfig)

  • config leitet eine Konfigurationsoption ein, dahinter der Bezeichner der Option
  • tristate bezeichnet den Typ der Option (tristate, bool, int, string, …)
  • select wählt zusätzliche Optionen, wenn diese Option aktiviert wird
  • help ist selbst erklärend
  • Fehlt: depends → Beschreibt Abhängigkeiten zu anderen Optionen
  • Konfigurationsoptionen stehen sowohl im Quellcode als auch im Makefile mit dem Prefix CONFIG_ zur Verfügung

Von der Konfiguration zum Quellcode – Grundlagen

  • Makefile-Variablen legen Art der Übersetzung fest (siehe 3. in Dokumentation)
    • obj-y += foo.o: Übersetze Datei foo.c immer
    • obj-m += foo.o: Übersetze Datei foo.c, wenn die Modulunterstützung aktiv ist
  • Konfigurierbare Übersetzung
    • obj-$(CONFIG_FOO) += foo.o
    • Übbersetze foo.c, wenn Konfigurationsoption CONFIG_FOO entweder y oder m ist
  • Binde Unterverzeichnisse ein
obj-$(CONFIG_EXT4_FS)       += ext4/

(Aus fs/Makefile)

Von der Konfiguration zum Quellcode – Makefile

# SPDX-License-Identifier: GPL-2.0
#
# Makefile for the linux ext4-filesystem routines.
#

obj-$(CONFIG_EXT4_FS) += ext4.o

ext4-y  := balloc.o bitmap.o block_validity.o dir.o ext4_jbd2.o extents.o \
        extents_status.o file.o fsmap.o fsync.o hash.o ialloc.o \
        indirect.o inline.o inode.o ioctl.o mballoc.o migrate.o \
        mmp.o move_extent.o namei.o page-io.o readpage.o resize.o \
        super.o symlink.o sysfs.o xattr.o xattr_hurd.o xattr_trusted.o \
        xattr_user.o fast_commit.o orphan.o

ext4-$(CONFIG_EXT4_FS_POSIX_ACL)    += acl.o
# [....]
obj-$(CONFIG_EXT4_KUNIT_TESTS)      += ext4-inode-test.o
# [....]

(Aus fs/ext4/Makefile)

  • Modul ext4.o besteht aus mehreren Quellcodedateien
  • Umweg über separate Variable gemäßg Modulname: ext4-y :=
  • Syntax ist genauso wie bereits erwähnt

Wie baue ich ein neues Feature ein?

  • Grundstruktur anlegen ✅
  • Konfiguration erstellen ✅
  • Code schreiben

Gerätetreiber

Von Systemaufruf zum Treiber

char buf[42];

int fd = open("/dev/the-universe", O_RDWR);
if (read(fd, buf, 42)) {
    // ...
}
(gdb) bt
#0  universe_read (file=0xffff88800453b700, 
    buf=0x7f9b78c4a000 <error: Cannot access memory at address 0x7f9b78c4a000>, 
    count=42, ppos=0xffffc90000507ef0) at drivers/sst/sst_chrdev.c:25
#1  0xffffffff812a5579 in vfs_read (file=file@entry=0xffff88800453b700, 
    buf=buf@entry=0x7f9b78c4a000 <error: Cannot access memory at address 0x7f9b78c4a000>, 
    count=count@entry=131072, pos=pos@entry=0xffffc90000507ef0) at fs/read_write.c:468
#2  0xffffffff812a60e3 in ksys_read (fd=<optimized out>, 
    buf=0x7f9b78c4a000 <error: Cannot access memory at address 0x7f9b78c4a000>, 
    count=131072) at fs/read_write.c:613
#3  0xffffffff812a6179 in __do_sys_read (count=<optimized out>, buf=<optimized out>, 
    fd=<optimized out>) at fs/read_write.c:623
#4  __se_sys_read (count=<optimized out>, buf=<optimized out>, fd=<optimized out>)
    at fs/read_write.c:621
#5  __x64_sys_read (regs=<optimized out>) at fs/read_write.c:621
#6  0xffffffff81b1b055 in do_syscall_x64 (nr=<optimized out>, regs=0xffffc90000507f58)
    at arch/x86/entry/common.c:50
#7  do_syscall_64 (regs=0xffffc90000507f58, nr=<optimized out>)
    at arch/x86/entry/common.c:80
#8  0xffffffff81c0006a in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:120

Zustellen von Systemaufrufen zu Treibern

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;

    if (!(file->f_mode & FMODE_READ))
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_READ))
        return -EINVAL;
    if (unlikely(!access_ok(buf, count)))
        return -EFAULT;

    //  [...]
    if (file->f_op->read)
        ret = file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter)
        ret = new_sync_read(file, buf, count, pos);
    else
        ret = -EINVAL;
    if (ret > 0) {
        fsnotify_access(file);
        add_rchar(current, ret);
    }
    inc_syscr(current);
    return ret; 
}

(Aus fs/read_write.c:468)

Polymorphie in C 😖

  • UNIX-Paradigma
    • „Alles ist eine Datei”
    • Dateioperationen definieren Interaktionen: open, read, write, lseek, …, close
  • Schnittstellendefinition über Funktionszeiger
  • Definition der Schnittstelle in struct file_operations:
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    // [...]
} __randomize_layout;

(Aus include/linux/fs.h:2109)

Was bedeuten die Parameter beim read-Systemaufruf?

Datenstruktur hinter Dateideskriptor – struct file

struct file {
    // [...]
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;
    // [...]
	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t			f_pos;
    // [...]
   /* needed for tty driver, and maybe others */
	void			*private_data;
    // [...]
} __randomize_layout
  __attribute__((aligned(4)));	/* lest something weird decides that 2 is OK */

(Aus include/linux/fs.h:940)

  • Repräsentiert im Kern eine geöffnete Datei
  • Weitere Informationen in „Linux Device Driver” (Buchseite 66 [1])

Die Programmierschnittstelle (API)

Ausgaben

  • Grundsätzliches Vorgehen ist wie bei printf() (Formatstring, Argumente, …)
  • Pendant im Kernel heißt printk(), Semantik ist aber gleich!
  • Makros für das passende Log-Level: pr_err, pr_info, pr_debug, …
  • printk_ratelimited() 🤔
  • Definition und (etwas) Dokumentation in include/linux/printk.h:434
  • Ergänzungen zu pr_debug:
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/printk.h>
#define sst_debug(format, ...) pr_debug("(file:%s,line:%d,pid:%d): " format,       \
            __FILE__, __LINE__,  current->pid, ##__VA_ARGS__)

Dynamische Speicherverwaltung (im Linux-Kern)

  • vmalloc/vfree
    • Allokation von virtuell adjazentem Speicher
    • Bekommt Größe übergeben
  • kmalloc/kfree
    • Allokation von physikalisch adjazentem Speicher
    • Bekommt Größe und Flags übergeben – siehe Dokumentation
  • kmalloc-Flags
    • GFP_KERNEL: „Allocate normal kernel ram. May sleep.”
    • GFP_NOWAIT: „Allocation will not sleep.”
    • GFP_ATOMIC: „Allocation will not sleep. May use emergency pools.”

Kernel-Threads

  • Kernel-Threads sind „standard processes that exist solely in kernel-space” (Buchseite 35, [2])
  • Verhalten sich wie normale Prozesse: unterliegen Ablaufplanung und Präemption
  • Wichtiger Unterschied: verfügen über keinen keinen eigenen Adressraum, laufen im Adressraum des Kerns
  • Erzeugung läuft intern auch über Systemaufruf clone(), wie beim Systemaufruf fork()
  • Bei Erzeugung nicht lauffähig; müssen explizit gestartet werden

Kernel-Threads – API

  • Thread erstellen:

    struct *task_struct kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[],...)

  • Erstellten Thread aufwecken:

    int wake_up_process(struct task_struct *tsk)

  • Warten bis der Thread sich beendet:

    int kthread_stop(struct task_struct *tsk)

  • Soll der Thread anhalten?

    bool kthread_should_stop(void)

Weitere Funktionen und „Dokumentation” finden sich in include/linux/kthread.h

Kernel-Threads – Beispiel

#include <linux/kthread.h> 

char *text = "Hello from the other side!";
struct task_struct *faden;

static int work_fn(void *arg) {
    char *text = (char*)arg;

    while (!kthread_should_stop()) {
        printk("Working: %s\n", text);
        ssleep(1);
    }
    return 0;
}

static init __init module_init(void) {
    faden = kthread_create(work_fn, (void*)text, "ein-name");
    if (IS_ERR(faden)) {
        pr_err("Error\n");
    }
    return 0;

    wake_up_process(faden);
    return 0;
}

// kthread_stop(faden);

Synchronisation

  • Ganze Code-Abschnitte
    • (RW-)Spinlocks
    • Mutex
    • (RW-)Semaphore
    • Sequential Locks
    • Abschalten von Präemption/Unterbrechungen
  • Auf Instruktionsebene
    • Atomare Operationen
    • Barrieren
  • Sonstige
    • Completion Variables

(Siehe Kapitel 10 in „Linux Kernel Development” [2])

Synchronisation – API

  • Spinlocks
    • spin_lock/spin_unlock
    • Abschalten der Softirqs bzw. Unterbrechungen mit Suffix: _bh, _irq, _irqsave
  • Mutex/Semaphore
    • down/up (Besser: down_interruptible)
    • mutex_lock/mutex_unlock
  • Vergleich zwischen Spinlocks und Mutex/Semaphore in Kap. 10, Seite 197 [2]
  • Verwendung eines Semaphores siehe drivers/sst/sst_common.c in Zeile 146 bzw. 171)

Kernel Library

Bounded Buffer

  • Dürft Ihr gerne verwenden.
  • Implementierung liegt in drivers/sst/boundedbuffer.{c,h}
// Größe beträgt 20 Elemente
#define BOUNDEDBUFFER_SIZE 20
// Gespeichert werden Zeiger auf einen Char
#define BOUNDEDBUFFER_STORAGE_TYPE char*
#include "boundedbuffer.h

// [...]

struct boundedbuffer foo;

Fehlerbehandlungen

  • Beachtet immer den Rückgabewert einer Funktion
  • Behandelt immer den Fehlerfall
  • Macht bereits abgeschlossenen Teilschritte immer rückgängig
  • Fehler sind im Kern sind besonders fatal

→ Im Zweifel eine Ausgabe und einen Fehlercode zurückgegeben

int init_pferd(void) {
    foo = alloc_foo();
    
    spin_lock(&a_lock);
    if (!init_bar()) {
        // Was muss hier alles passieren?
    }
    spin_unlock(&a_lock);

    return 0;
}

Wie baue ich ein neues Feature ein?

  • Grundstruktur anlegen ✅
  • Konfiguration erstellen ✅
  • Code schreiben ✅

Dokumentation

Dokumentation

  • Die Bücher habe ich alle im Büro liegen. 😎

Referenzen

[1]
J. Corbet, A. Rubini, und G. Kroah-Hartman, Linux Device Drivers, 3rd Edition. O’Reilly Media, Inc., 2005.
[2]
R. Love, Linux Kernel Development, 3rd Aufl. Addison-Wesley, 2010.
[3]
D. P. Bovet und M. Cesati, Understanding The Linux Kernel, 3rd Aufl. O’Reilly Media Inc., 2005.