לדלג לתוכן

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

מבוא

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

הפרויקט הזה מאגד את כל מה שלמדנו:
- מודולי קרנל (סעיף 6.1) - כתיבה, טעינה ופריקה של מודולים
- VFS ומערכת הקבצים (סעיף 6.4) - ממשק 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

בדיקה

# קומפילציה:
make

# טעינה:
sudo insmod kvstore.ko
dmesg | tail -3    # אמורה להופיע הודעה על רישום מוצלח

# יצירת קובץ ההתקן:
# (המספר הראשי מופיע ב-dmesg)
sudo mknod /dev/kvstore c <major> 0
sudo chmod 666 /dev/kvstore

# בדיקה:
cat /dev/kvstore      # אמורה להופיע הודעה ב-dmesg
echo "test" > /dev/kvstore  # גם

# פריקה:
sudo rmmod kvstore
dmesg | tail -3    # אמורה להופיע הודעת פריקה

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

המטרה

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

מה לממש

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

בדיקה

# כתיבה:
echo "name=linux" > /dev/kvstore
echo "version=5.15" > /dev/kvstore
echo "author=torvalds" > /dev/kvstore

# קריאה:
cat /dev/kvstore
# אמור להציג:
# 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");

// --- מבני נתונים ---

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

static LIST_HEAD(kv_list);         // הרשימה המקושרת של כל הזוגות
static int entry_count = 0;

// --- רישום ההתקן ---

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

// --- פונקציות file_operations ---

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;
}

// חיפוש entry לפי key (מחזיר NULL אם לא נמצא)
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;
}

// כתיבה - מנתחת "key=value\n" ושומרת
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;

    // הגבלת גודל
    len = min(count, sizeof(kbuf) - 1);

    // העתקה מ-user space
    if (copy_from_user(kbuf, buf, len))
        return -EFAULT;

    kbuf[len] = '\0';

    // הסרת newline אם קיים
    if (len > 0 && kbuf[len - 1] == '\n')
        kbuf[len - 1] = '\0';

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

    *eq_pos = '\0';    // מפריד בין key ל-value
    eq_pos++;           // eq_pos מצביע עכשיו על ה-value

    // בדיקת אורכים
    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;
    }

    // בדיקה אם ה-key כבר קיים - עדכון
    entry = find_entry(kbuf);
    if (entry) {
        strscpy(entry->value, eq_pos, MAX_VAL_LEN);
        pr_info("kvstore: updated key '%s'\n", kbuf);
        return count;
    }

    // key חדש - הקצאת 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;
}

// קריאה - מחזירה את כל הזוגות
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;

    // חישוב גודל כולל
    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;   // אין נתונים או כבר קראנו הכל

    // הקצאת buffer זמני
    kbuf = kmalloc(total_len + 1, GFP_KERNEL);
    if (!kbuf)
        return -ENOMEM;

    kbuf[0] = '\0';

    // בניית המחרוזת
    list_for_each_entry(entry, &kv_list, list) {
        strcat(kbuf, entry->key);
        strcat(kbuf, "=");
        strcat(kbuf, entry->value);
        strcat(kbuf, "\n");
    }

    // העתקה ל-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;
}

// --- רישום ---

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;

    // הקצאת 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);

    // יצירת 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;
    }

    // יצירת class ו-device (מייצר /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);

    // שחרור כל ה-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;

    // ספירה:
    ioctl(fd, KVSTORE_COUNT, &count);
    printf("Entries: %d\n", count);

    // מחיקה לפי key:
    char key[64] = "name";
    ioctl(fd, KVSTORE_DELETE, key);

    // ניקוי הכל:
    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 - סנכרון

המטרה

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

הנחיות

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

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

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

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

# ב-terminal שני (במקביל):
for i in $(seq 100 200); do echo "key$i=value$i" > /dev/kvstore; done

# בדיקה:
cat /dev/kvstore | wc -l   # אמור להיות 200

רמזים

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

שלב 5 (בונוס) - ממשק 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);
    // הוסיפו: סך הזיכרון בשימוש, ה-key הארוך ביותר, וכו'
    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,
};

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

// ב-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 כדי לאפשר קריאה חלקית
  • הוסיפו ממשק sysfs במקום (או בנוסף ל-) procfs
  • ממשו mmap כדי לאפשר גישה ישירה לנתונים מ-user space (מתקדם!)

בהצלחה!