פרויקט סיכום - כתיבת דרייבר תו פשוט¶
מבוא¶
הגענו לפרויקט הסיכום של סעיף 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
הנחיות¶
- הגדירו את פקודות ה-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
-
ממשו פונקציית
kvstore_ioctlשמטפלת בפקודות האלה -
הוסיפו
.unlocked_ioctl = kvstore_ioctlל-file_operations -
כתבו תוכנית 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.
הנחיות¶
- הוסיפו mutex (לא spinlock - כי אנחנו עשויים לישון ב-kmalloc):
-
עטפו כל גישה לרשימה ב-mutex_lock/mutex_unlock
-
חשבו: האם mutex_lock צריך להיות בתוך read, write, ו-ioctl?
-
בדיקה - הריצו מספר תהליכים במקביל:
# ב-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 שמציג סטטיסטיקות על המאגר.
הנחיות¶
- השתמשו ב-
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);
- בדיקה:
רמזים¶
seq_fileAPI מטפל אוטומטית בפיצול פלט ארוך למספר קריאות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 (מתקדם!)
בהצלחה!