לדלג לתוכן

דרייברים (device drivers) הם החלק הכי גדול בקוד המקור של הקרנל של לינוקס. יותר ממחצית מכל הקוד בקרנל הוא קוד של דרייברים. הסיבה פשוטה - יש המון סוגי חומרה בעולם, וכל אחד צריך קוד שיודע לדבר איתו.

דרייבר הוא הגשר בין החומרה לבין שאר הקרנל. הוא מתרגם בין הממשק הגנרי שהקרנל מספק (קריאה, כתיבה, פתיחה) לבין הפרטים הספציפיים של כל התקן חומרה.

דרייברים - device drivers

סוגי התקנים בלינוקס

לינוקס מסווגת התקנים לשלושה סוגים עיקריים:

התקני תו - character devices (c)

ניגשים אליהם כזרם של בתים - בית אחרי בית, בסדר רציף.

דוגמאות:
- /dev/tty - הטרמינל
- /dev/null - "חור שחור" שבולע כל מה שכותבים אליו
- /dev/random - מחולל מספרים אקראיים
- /dev/zero - מחזיר אפסים אינסופיים
- /dev/console - הקונסולה של המערכת

אפשר לעבוד איתם עם הsyscalls הרגילים: open, read, write, close.

התקני בלוק - block devices (b)

ניגשים אליהם בבלוקים בגודל קבוע (בדרך כלל 512 בתים או 4096 בתים). מאפשרים גישה אקראית - אפשר לקרוא מכל מקום, לא רק בסדר.

דוגמאות:
- /dev/sda - הדיסק הקשיח הראשון
- /dev/sda1 - המחיצה הראשונה בדיסק הראשון
- /dev/loop0 - התקן loop (קובץ שמתנהג כדיסק)
- /dev/nvme0n1 - דיסק NVMe

מערכות קבצים (ext4, xfs, btrfs) עובדות מעל התקני בלוק.

התקני רשת - network devices

סוג מיוחד - לא מופיעים ב/dev/. ניגשים אליהם דרך ממשק הsocket (שנלמד בפרק 6.9). כל כרטיס רשת מיוצג ע"י שם ממשק כמו eth0, wlan0, enp3s0.


מספרים ראשיים ומשניים - major and minor numbers

כל קובץ התקן ב/dev/ מזוהה על ידי שני מספרים:

  • מספר ראשי - major number: מזהה את הדרייבר שאחראי על ההתקן
  • מספר משני - minor number: מזהה את ההתקן הספציפי בתוך אותו דרייבר
ls -la /dev/sda /dev/sda1 /dev/sda2

פלט לדוגמה:

brw-rw---- 1 root disk 8, 0 Mar  8 10:00 /dev/sda
brw-rw---- 1 root disk 8, 1 Mar  8 10:00 /dev/sda1
brw-rw---- 1 root disk 8, 2 Mar  8 10:00 /dev/sda2

שימו לב:
- האות b בהתחלה אומרת שזה block device
- המספרים 8, 0 ו-8, 1 ו-8, 2 הם הmajor והminor
- כולם חולקים major 8 (הדרייבר של SCSI/SATA דיסקים) אבל כל מחיצה מקבלת minor שונה

דוגמה נוספת:

ls -la /dev/tty0 /dev/null /dev/random

crw--w---- 1 root tty  4, 0 Mar  8 10:00 /dev/tty0
crw-rw-rw- 1 root root 1, 3 Mar  8 10:00 /dev/null
crw-rw-rw- 1 root root 1, 8 Mar  8 10:00 /dev/random

כאן c בהתחלה אומרת character device.

איך הקרנל משתמש במספרים האלו?

כשאתם עושים open("/dev/sda", ...):
1. הקרנל קורא את הinode של /dev/sda ורואה שזה קובץ התקן עם major 8, minor 0
2. הקרנל מחפש בטבלה פנימית איזה דרייבר רשום למספר ראשי 8
3. הקרנל קורא לפונקציית open של אותו דרייבר
4. מעכשיו כל read/write/ioctl על הקובץ הזה יגיע ישר לדרייבר


מבנה דרייבר להתקן תו - character device driver

בפרק 6.5 למדנו על הVFS ועל המבנה file_operations - מערך של פוינטרים לפונקציות שכל מערכת קבצים ממלאת. דרייברים להתקני תו משתמשים באותו מנגנון בדיוק.

דרייבר להתקן תו צריך:
1. להגדיר struct file_operations עם הפונקציות שהוא תומך בהן
2. לרשום את עצמו בקרנל עם register_chrdev()
3. לבטל רישום כשהוא נפרק עם unregister_chrdev()

דוגמה מלאה - דרייבר התקן תו פשוט

הדרייבר הבא יוצר התקן תו שעובד כמו באפר - אפשר לכתוב אליו מידע ואז לקרוא אותו חזרה:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define BUF_SIZE 1024

static int major;
static char device_buffer[BUF_SIZE];
static int buffer_len = 0;

static ssize_t dev_read(struct file *f, char __user *buf, size_t len, loff_t *off) {
    if (*off >= buffer_len) return 0;
    if (len > buffer_len - *off) len = buffer_len - *off;
    if (copy_to_user(buf, device_buffer + *off, len)) return -EFAULT;
    *off += len;
    return len;
}

static ssize_t dev_write(struct file *f, const char __user *buf, size_t len, loff_t *off) {
    if (len > BUF_SIZE) len = BUF_SIZE;
    if (copy_from_user(device_buffer, buf, len)) return -EFAULT;
    buffer_len = len;
    return len;
}

static int dev_open(struct inode *i, struct file *f) {
    printk(KERN_INFO "mychardev: opened\n");
    return 0;
}

static int dev_release(struct inode *i, struct file *f) {
    printk(KERN_INFO "mychardev: closed\n");
    return 0;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = dev_read,
    .write = dev_write,
    .open = dev_open,
    .release = dev_release,
};

static int __init mychardev_init(void) {
    major = register_chrdev(0, DEVICE_NAME, &fops);
    printk(KERN_INFO "mychardev: registered with major %d\n", major);
    return 0;
}

static void __exit mychardev_exit(void) {
    unregister_chrdev(major, DEVICE_NAME);
    printk(KERN_INFO "mychardev: unregistered\n");
}

module_init(mychardev_init);
module_exit(mychardev_exit);
MODULE_LICENSE("GPL");

בואו נעבור על הנקודות החשובות:

הפונקציות של הדרייבר

dev_open - נקראת כשמישהו עושה open() על הקובץ של ההתקן. כאן אפשר לאתחל חומרה, לבדוק הרשאות, ולהכין משאבים.

dev_release - נקראת כשהקובץ נסגר (כל הfile descriptors שמצביעים אליו נסגרו). כאן משחררים משאבים.

dev_read - נקראת כשמישהו עושה read(). שימו לב לכמה דברים:
- הפרמטר buf הוא מטיפוס char __user * - זה פוינטר ל-user space, אסור לגשת אליו ישירות!
- חייבים להשתמש ב-copy_to_user() כדי להעתיק מידע מהקרנל ליוזר (מפרק 6.2 - הsyscall מבפנים)
- אם copy_to_user נכשלת, מחזירים -EFAULT

dev_write - אותו רעיון, רק בכיוון ההפוך. copy_from_user() מעתיקה מהיוזר לקרנל.

הפונקציות של המודול

module_init(mychardev_init) - מסמנת את הפונקציה שתרוץ כשטוענים את המודול (insmod)
module_exit(mychardev_exit) - מסמנת את הפונקציה שתרוץ כשמסירים את המודול (rmmod)

איך בודקים את הדרייבר?

# קומפילציה (צריך kernel headers מותקנים)
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules

# טעינת המודול
sudo insmod mychardev.ko

# הדרייבר מדפיס את הmajor שקיבל - נבדוק ב-dmesg
dmesg | tail -1
# mychardev: registered with major 240

# יצירת קובץ ההתקן ב/dev/
sudo mknod /dev/mychardev c 240 0

# עכשיו אפשר לכתוב ולקרוא!
echo "hello kernel" > /dev/mychardev
cat /dev/mychardev
# hello kernel

# הסרת המודול
sudo rmmod mychardev

ממשק הioctl

הsyscalls read ו-write מתאימים להעברת מידע, אבל לפעמים צריך לשלוט בהתקן בצורות שלא מתאימות לקריאה/כתיבה:

  • שינוי baud rate בפורט סריאלי
  • שאילתת גודל המסך בטרמינל
  • הפעלה/כיבוי של LED על ההתקן
  • שינוי הגדרות של כרטיס רשת

לשם כך יש את הsyscall ioctl (input/output control):

int ioctl(int fd, unsigned long request, ...);

הדרייבר מגדיר מספרי פקודות (request codes) ומפרש אותם בפונקציה unlocked_ioctl שהוא רושם בfile_operations:

static long dev_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
        case MY_IOCTL_RESET:
            buffer_len = 0;
            return 0;
        case MY_IOCTL_GET_LEN:
            return buffer_len;
        default:
            return -EINVAL;
    }
}

דוגמה מוכרת - כשתוכנה שואלת מה גודל חלון הטרמינל:

#include <sys/ioctl.h>
struct winsize ws;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
printf("rows: %d, cols: %d\n", ws.ws_row, ws.ws_col);

הפקודה TIOCGWINSZ מוגדרת על ידי דרייבר הTTY בקרנל.


מודל ההתקנים ו-sysfs

מודל ההתקנים - device model

בגרסאות ישנות של הקרנל, כל דרייבר פעל בצורה עצמאית. לא היה מנגנון מאורגן לייצג את הקשר בין חומרה, אפיקים ודרייברים.

מהקרנל 2.6 והלאה, לינוקס מיישמת מודל התקנים מאורגן שמכיל שלושה מרכיבים:
- אפיק - bus: PCI, USB, I2C, SPI ועוד. כל אפיק יודע לסרוק את ההתקנים שמחוברים אליו
- התקן - device: ייצוג של חומרה ספציפית שמחוברת לאפיק
- דרייבר - driver: קוד שיודע לעבוד עם התקן מסוים

הקרנל מנהל מנגנון התאמה (matching) - כשהתקן חדש מתגלה על אפיק, הקרנל מחפש דרייבר שמכריז שהוא יודע לטפל בהתקן הזה, וקורא לפונקציית probe של הדרייבר.

מערכת sysfs

מודל ההתקנים חשוף ליוזר דרך מערכת הקבצים /sys/ (sysfs). זו לא מערכת קבצים אמיתית - כמו /proc/, היא מיוצרת על ידי הקרנל בזמן ריצה:

# כל הclasses של התקנים
ls /sys/class/
# block  input  net  tty  sound  ...

# כל ההתקנים על אפיק PCI
ls /sys/bus/pci/devices/

# מידע על כרטיס הרשת
cat /sys/class/net/eth0/speed      # מהירות החיבור
cat /sys/class/net/eth0/address    # כתובת MAC

הדמון udev

בעבר, מנהל המערכת היה צריך ליצור ידנית קבצי התקנים ב/dev/ עם mknod. היום התהליך אוטומטי:

  1. הקרנל מזהה התקן חדש (למשל חיברתם USB flash)
  2. הקרנל שולח אירוע (uevent) ל-user space
  3. הדמון udev מקבל את האירוע
  4. udev יוצר את קובץ ההתקן המתאים ב/dev/ עם ההרשאות הנכונות
  5. udev יכול גם להריץ סקריפטים מותאמים אישית (rules)

דרייברי פלטפורמה ו-Device Tree

בעולם הPC, התקני חומרה יושבים על אפיקים שניתנים לסריקה - כמו PCI ו-USB. המעבד יכול לשאול את האפיק "מי מחובר אליך?" ולקבל רשימה.

אבל במערכות משובצות (embedded) - טלפונים, רספברי פאי, ראוטרים - רוב ההתקנים מחוברים ישירות למעבד דרך memory-mapped I/O. אין אפיק שאפשר לסרוק.

כדי שהקרנל ידע אילו התקנים קיימים ובאילו כתובות הם יושבים, משתמשים בDevice Tree - קובץ טקסט (בפורמט DTS) שמתאר את כל החומרה במערכת. הקובץ עובר קומפילציה לפורמט בינארי (DTB) ונטען לזיכרון על ידי הbootloader לפני שהקרנל עולה.

דרייברים שעובדים עם חומרה כזו נקראים platform drivers ומשתמשים בAPI ייעודי (platform_driver_register).


גישה ישירה לזכרון - DMA - Direct Memory Access

בדרך כלל, כדי להעביר מידע מהתקן חומרה לזיכרון, המעבד צריך לקרוא מההתקן ולכתוב לזיכרון - בית אחרי בית. זה בזבוז של זמן מעבד.

מנגנון DMA מאפשר להתקני חומרה לקרוא ולכתוב ישירות לזיכרון בלי מעורבות המעבד:

  1. המעבד אומר להתקן: "תעתיק 4KB לכתובת הזו בזיכרון"
  2. ההתקן מעתיק את המידע ישירות, המעבד חופשי לעשות דברים אחרים
  3. כשההתקן סיים, הוא שולח פסיקה (IRQ) למעבד

הקרנל מספק API לדרייברים שצריכים DMA:

// הקצאת זיכרון שמתאים לDMA (רציף פיזית, noncacheable)
void *dma_alloc_coherent(struct device *dev, size_t size,
                         dma_addr_t *dma_handle, gfp_t flag);

// מיפוי באפר רגיל לכתובת DMA
dma_addr_t dma_map_single(struct device *dev, void *ptr,
                          size_t size, enum dma_data_direction dir);

דוגמה קלאסית - כרטיסי רשת מודרניים משתמשים בDMA כדי להעתיק חבילות ישירות לזיכרון, בלי שהמעבד יצטרך להעתיק כל בית.


סיכום

  • דרייברים הם הגשר בין חומרה לקרנל, ומהווים את רוב קוד הקרנל
  • שלושה סוגי התקנים: תו (character), בלוק (block), ורשת (network)
  • כל התקן מזוהה על ידי major ו-minor numbers
  • דרייבר להתקן תו ממלא struct file_operations - אותו מנגנון כמו VFS מפרק 6.5
  • ioctl משמש לפקודות שלא מתאימות ל-read/write
  • מודל ההתקנים (bus/device/driver) חשוף דרך sysfs ב/sys/
  • udev יוצר אוטומטית קבצי התקנים ב/dev/
  • DMA מאפשר להתקנים לגשת ישירות לזיכרון