לדלג לתוכן

פרויקט סיכום - כתיבת דרייבר תו פשוט

מבוא

הגענו לפרויקט הסיכום של סעיף 6 - הקרנל של לינוקס. בפרויקט הזה נכתוב דרייבר character device שמממש מאגר key-value פשוט בזיכרון הקרנל, נגיש מ-user space.

הפרויקט הזה מאגד את כל מה שלמדנו:
- מודולי קרנל (סעיף 6.1) - כתיבה, טעינה ופריקה של מודולים
- VFS ומערכת הקבצים (סעיף 6.4) - interface file_operations
- ניהול זיכרון (סעיף 6.6) - kmalloc, kfree
- סנכרון (סעיף 6.10) - הגנה על מבני נתונים משותפים
- דיבוג (סעיף 6.12) - printk ו-dmesg

חשוב: פיתוח קרנל יכול לגרום לקריסת מערכת. תמיד עבדו בתוך VM (QEMU, VirtualBox, או VMware). לעולם אל תנסו קוד קרנל על המכונה הראשית שלכם.


רקע - character devices

ב-Linux, character device הוא סוג של קובץ מיוחד שמייצג התקן שניגשים אליו תו-אחר-תו (בניגוד ל-block device שניגשים אליו בבלוקים). דוגמאות: /dev/null, /dev/zero, /dev/random.

כשתוכנית קוראת/כותבת מ-character device, הקרנל קורא לפונקציות שהדרייבר רשם - read, write, open, release, ioctl.


שלב 1 - מודול בסיסי

המטרה

כתבו מודול קרנל שרושם character device חדש. כרגע הוא לא עושה כלום מעבר לרישום ולהדפסה ל-kernel log כשפותחים וסוגרים אותו.

מה לממש

  • פונקציית init שרושמת character device
  • פונקציית exit שמבטלת את הרישום
  • פונקציות open ו-release שמדפיסות ל-kernel log

בדיקה

# compile:
make

# load:
sudo insmod kvstore.ko
dmesg | tail -3    # a message about successful registration should appear

# create the device file:
# (the major number appears in dmesg)
sudo mknod /dev/kvstore c <major> 0
sudo chmod 666 /dev/kvstore

# test:
cat /dev/kvstore      # a message should appear in dmesg
echo "test" > /dev/kvstore  # same

# unload:
sudo rmmod kvstore
dmesg | tail -3    # an unload message should appear

שלב 2 - קריאה וכתיבה

המטרה

הוסיפו יכולת אחסון: כתיבה ל-device שומרת זוגות key=value, וקריאה מחזירה את כולם.

מה לממש

  • write: מקבל מחרוזת בפורמט key=value\n, מנתח אותה, ושומר ברשימה מקושרת
  • read: מחזיר את כל הזוגות בפורמט key=value\n
  • שימוש ב-kernel linked list API (list_head)
  • הallocation זיכרון עם kmalloc ושחרור עם kfree

בדיקה

# write:
echo "name=linux" > /dev/kvstore
echo "version=5.15" > /dev/kvstore
echo "author=torvalds" > /dev/kvstore

# read:
cat /dev/kvstore
# should display:
# name=linux
# version=5.15
# author=torvalds

מימוש מלא - שלבים 1+2

הנה המימוש המלא עד שלב 2 (כולל שלב 1):

// kvstore.c - kernel key-value store character device driver

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/list.h>
#include <linux/string.h>

#define DEVICE_NAME "kvstore"
#define MAX_KEY_LEN 64
#define MAX_VAL_LEN 256

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Student");
MODULE_DESCRIPTION("Simple kernel key-value store");

// --- data structures ---

struct kv_entry {
    char key[MAX_KEY_LEN];
    char value[MAX_VAL_LEN];
    struct list_head list;
};

static LIST_HEAD(kv_list);         // the linked list of all pairs
static int entry_count = 0;

// --- device registration ---

static int major_number;
static struct class *kvstore_class;
static struct cdev kvstore_cdev;

// --- file_operations functions ---

static int kvstore_open(struct inode *inode, struct file *file)
{
    pr_info("kvstore: device opened\n");
    return 0;
}

static int kvstore_release(struct inode *inode, struct file *file)
{
    pr_info("kvstore: device closed\n");
    return 0;
}

// find an entry by key (returns NULL if not found)
static struct kv_entry *find_entry(const char *key)
{
    struct kv_entry *entry;

    list_for_each_entry(entry, &kv_list, list) {
        if (strcmp(entry->key, key) == 0)
            return entry;
    }
    return NULL;
}

// write - parses "key=value\n" and stores it
static ssize_t kvstore_write(struct file *file, const char __user *buf,
                              size_t count, loff_t *pos)
{
    char kbuf[MAX_KEY_LEN + MAX_VAL_LEN + 2];  // key=value\n\0
    char *eq_pos;
    struct kv_entry *entry;
    size_t len;

    // limit the size
    len = min(count, sizeof(kbuf) - 1);

    // copy from user space
    if (copy_from_user(kbuf, buf, len))
        return -EFAULT;

    kbuf[len] = '\0';

    // remove the newline if present
    if (len > 0 && kbuf[len - 1] == '\n')
        kbuf[len - 1] = '\0';

    // find '='
    eq_pos = strchr(kbuf, '=');
    if (!eq_pos) {
        pr_err("kvstore: invalid format, expected key=value\n");
        return -EINVAL;
    }

    *eq_pos = '\0';    // separates key from value
    eq_pos++;           // eq_pos now points to the value

    // check lengths
    if (strlen(kbuf) == 0 || strlen(kbuf) >= MAX_KEY_LEN) {
        pr_err("kvstore: key too long or empty\n");
        return -EINVAL;
    }
    if (strlen(eq_pos) >= MAX_VAL_LEN) {
        pr_err("kvstore: value too long\n");
        return -EINVAL;
    }

    // check if the key already exists - update
    entry = find_entry(kbuf);
    if (entry) {
        strscpy(entry->value, eq_pos, MAX_VAL_LEN);
        pr_info("kvstore: updated key '%s'\n", kbuf);
        return count;
    }

    // new key - allocate a new entry
    entry = kmalloc(sizeof(*entry), GFP_KERNEL);
    if (!entry)
        return -ENOMEM;

    strscpy(entry->key, kbuf, MAX_KEY_LEN);
    strscpy(entry->value, eq_pos, MAX_VAL_LEN);
    list_add_tail(&entry->list, &kv_list);
    entry_count++;

    pr_info("kvstore: added key '%s' = '%s' (total: %d)\n",
            entry->key, entry->value, entry_count);

    return count;
}

// read - returns all the pairs
static ssize_t kvstore_read(struct file *file, char __user *buf,
                             size_t count, loff_t *pos)
{
    struct kv_entry *entry;
    char *kbuf;
    size_t total_len = 0;
    size_t remaining;
    int ret;

    // calculate the total size
    list_for_each_entry(entry, &kv_list, list) {
        total_len += strlen(entry->key) + 1 + strlen(entry->value) + 1;
        // key=value\n
    }

    if (total_len == 0 || *pos >= total_len)
        return 0;   // no data, or we've already read everything

    // allocate a temporary buffer
    kbuf = kmalloc(total_len + 1, GFP_KERNEL);
    if (!kbuf)
        return -ENOMEM;

    kbuf[0] = '\0';

    // build the string
    list_for_each_entry(entry, &kv_list, list) {
        strcat(kbuf, entry->key);
        strcat(kbuf, "=");
        strcat(kbuf, entry->value);
        strcat(kbuf, "\n");
    }

    // copy to user space
    remaining = total_len - *pos;
    if (remaining > count)
        remaining = count;

    ret = copy_to_user(buf, kbuf + *pos, remaining);
    kfree(kbuf);

    if (ret)
        return -EFAULT;

    *pos += remaining;
    return remaining;
}

// --- registration ---

static const struct file_operations kvstore_fops = {
    .owner = THIS_MODULE,
    .open = kvstore_open,
    .release = kvstore_release,
    .read = kvstore_read,
    .write = kvstore_write,
};

static int __init kvstore_init(void)
{
    dev_t dev;
    int ret;

    // allocate a major number
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        pr_err("kvstore: failed to allocate major number\n");
        return ret;
    }
    major_number = MAJOR(dev);

    // create the cdev
    cdev_init(&kvstore_cdev, &kvstore_fops);
    kvstore_cdev.owner = THIS_MODULE;
    ret = cdev_add(&kvstore_cdev, dev, 1);
    if (ret < 0) {
        unregister_chrdev_region(dev, 1);
        pr_err("kvstore: failed to add cdev\n");
        return ret;
    }

    // create the class and device (automatically creates /dev/kvstore)
    kvstore_class = class_create(DEVICE_NAME);
    if (IS_ERR(kvstore_class)) {
        cdev_del(&kvstore_cdev);
        unregister_chrdev_region(dev, 1);
        pr_err("kvstore: failed to create class\n");
        return PTR_ERR(kvstore_class);
    }

    if (IS_ERR(device_create(kvstore_class, NULL, dev, NULL, DEVICE_NAME))) {
        class_destroy(kvstore_class);
        cdev_del(&kvstore_cdev);
        unregister_chrdev_region(dev, 1);
        pr_err("kvstore: failed to create device\n");
        return -1;
    }

    pr_info("kvstore: loaded (major=%d)\n", major_number);
    return 0;
}

static void __exit kvstore_exit(void)
{
    struct kv_entry *entry, *tmp;
    dev_t dev = MKDEV(major_number, 0);

    // free all the entries
    list_for_each_entry_safe(entry, tmp, &kv_list, list) {
        list_del(&entry->list);
        kfree(entry);
    }

    device_destroy(kvstore_class, dev);
    class_destroy(kvstore_class);
    cdev_del(&kvstore_cdev);
    unregister_chrdev_region(dev, 1);

    pr_info("kvstore: unloaded\n");
}

module_init(kvstore_init);
module_exit(kvstore_exit);

Makefile

obj-m += kvstore.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

שלב 3 - ioctl

המטרה

הוסיפו פקודות ioctl שמאפשרות:
- KVSTORE_CLEAR - מחיקת כל הערכים
- KVSTORE_COUNT - החזרת מספר הערכים
- KVSTORE_DELETE - מחיקת ערך לפי key

הנחיות

  1. הגדירו את פקודות ה-ioctl בקובץ header משותף (לקרנל ול-user space):
// kvstore_ioctl.h
#ifndef KVSTORE_IOCTL_H
#define KVSTORE_IOCTL_H

#include <linux/ioctl.h>

#define KVSTORE_MAGIC 'K'
#define KVSTORE_CLEAR   _IO(KVSTORE_MAGIC, 0)
#define KVSTORE_COUNT   _IOR(KVSTORE_MAGIC, 1, int)
#define KVSTORE_DELETE  _IOW(KVSTORE_MAGIC, 2, char[64])

#endif
  1. ממשו פונקציית kvstore_ioctl שמטפלת בפקודות האלה

  2. הוסיפו .unlocked_ioctl = kvstore_ioctl ל-file_operations

  3. כתבו תוכנית user space לבדיקה:

// test_ioctl.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "kvstore_ioctl.h"

int main(void)
{
    int fd = open("/dev/kvstore", O_RDWR);
    int count;

    // count:
    ioctl(fd, KVSTORE_COUNT, &count);
    printf("Entries: %d\n", count);

    // delete by key:
    char key[64] = "name";
    ioctl(fd, KVSTORE_DELETE, key);

    // clear everything:
    ioctl(fd, KVSTORE_CLEAR);

    close(fd);
    return 0;
}

רמזים

  • copy_from_user / copy_to_user נדרשים גם ב-ioctl (הארגומנט מגיע מ-user space)
  • אל תשכחו לעדכן את entry_count במחיקה
  • השתמשו ב-list_for_each_entry_safe כשמוחקים מתוך לולאה (כי list_del משנה את ה-list)

שלב 4 - סנכרון

המטרה

הגנו על מבנה הנתונים כך שמספר processes יוכלו לקרוא/לכתוב בו-זמנית בלי corruption.

הנחיות

  1. הוסיפו mutex (לא spinlock - כי אנחנו עשויים לישון ב-kmalloc):
static DEFINE_MUTEX(kv_mutex);
  1. עטפו כל גישה לרשימה ב-mutex_lock/mutex_unlock

  2. חשבו: האם mutex_lock צריך להיות בתוך read, write, ו-ioctl?

  3. בדיקה - הריצו מספר processes במקביל:

# in one terminal:
for i in $(seq 1 100); do echo "key$i=value$i" > /dev/kvstore; done

# in a second terminal (in parallel):
for i in $(seq 100 200); do echo "key$i=value$i" > /dev/kvstore; done

# test:
cat /dev/kvstore | wc -l   # should be 200

רמזים

  • אם allocation הזיכרון (kmalloc) נמצאת בתוך הנעילה, השתמשו ב-GFP_KERNEL (מותר כי mutex מאפשר שינה)
  • אפשר גם להקצות לפני הנעילה ואז רק את ההכנסה לרשימה לעשות בתוך הנעילה
  • שיפור: אפשר להשתמש ב-rw_semaphore כדי לאפשר מספר קוראים במקביל

שלב 5 (בונוס) - interface procfs

המטרה

צרו קובץ /proc/kvstore שמציג סטטיסטיקות על המאגר.

הנחיות

  1. השתמשו ב-proc_create ליצירת הקובץ:
#include <linux/proc_fs.h>
#include <linux/seq_file.h>

static int kvstore_proc_show(struct seq_file *m, void *v)
{
    mutex_lock(&kv_mutex);
    seq_printf(m, "Entries: %d\n", entry_count);
    // add: total memory in use, the longest key, etc.
    mutex_unlock(&kv_mutex);
    return 0;
}

static int kvstore_proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, kvstore_proc_show, NULL);
}

static const struct proc_ops kvstore_proc_ops = {
    .proc_open = kvstore_proc_open,
    .proc_read = seq_read,
    .proc_lseek = seq_lseek,
    .proc_release = single_release,
};

// in init:
proc_create("kvstore", 0444, NULL, &kvstore_proc_ops);

// in exit:
remove_proc_entry("kvstore", NULL);
  1. בדיקה:
cat /proc/kvstore
# Entries: 42
# Memory: 12600 bytes

רמזים

  • seq_file API מטפל אוטומטית בפיצול פלט ארוך למספר קריאות
  • proc_ops (מ-kernel 5.6) מחליף את file_operations ל-procfs
  • אל תשכחו את ה-cleanup ב-exit (remove_proc_entry)

הערות סיום

בטיחות

  • תמיד עבדו ב-VM. בטעות בקוד קרנל = reboot. בשגיאה חמורה = אובדן נתונים.
  • הפעילו CONFIG_DEBUG_INFO ו-CONFIG_PROVE_LOCKING (lockdep) בקרנל שלכם
  • בדקו עם dmesg אחרי כל שינוי - הודעות שגיאה מופיעות שם

כלים מומלצים

  • QEMU - הריצו קרנל ב-VM קלה ומהירה. אפשר גם לחבר GDB לדיבוג.
  • VirtualBox/VMware - אם QEMU מורכב מדי, VM רגילה עובדת מצוין

הרחבות אפשריות

אם סיימתם את כל 5 השלבים ורוצים אתגר נוסף:

  • הוסיפו TTL (Time To Live) לערכים - ערך שפג תוקפו נמחק אוטומטית
  • הוסיפו גבול למספר הערכים ולזיכרון הכולל
  • ממשו seek כדי לאפשר קריאה חלקית
  • הוסיפו interface sysfs במקום (או בנוסף ל-) procfs
  • ממשו mmap כדי לאפשר גישה ישירה לנתונים מ-user space (מתקדם!)

בהצלחה!