Был ли у вас опыт программирования драйверов для Linux?

«Был ли у вас опыт программирования драйверов для Linux?» — вопрос из категории Linux и ОС, который задают на 25% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, у меня есть опыт разработки загружаемых модулей ядра (Loadable Kernel Modules, LKM) для Linux. Я работал над драйвером символьного устройства (character device) для специализированной платы сбора данных, подключаемой по PCI Express.

Ключевые аспекты работы:

  1. Структура модуля: Инициализация (module_init) и выгрузка (module_exit).
  2. Регистрация устройства: Создание major/minor номеров, регистрация в /proc/devices и создание узла в /dev с помощью mknod или devtmpfs.
  3. Операции с файлами: Определение структуры file_operations с указателями на функции для open, release, read, write, ioctl, mmap.
  4. Взаимодействие с пользовательским пространством: Использование функций copy_to_user() и copy_from_user() для безопасного обмена данными.
  5. Работа с аппаратурой:
    • Отображение регистров устройства в память с помощью ioremap().
    • Обработка аппаратных прерываний (IRQ) через request_irq().
    • Настройка DMA (Direct Memory Access) с использованием API dma_alloc_coherent.
  6. Отладка: Использование printk() с разными уровнями логирования (KERN_INFO, KERN_ERR), просмотр логов через dmesg. В сложных случаях — использование ftrace или kgdb.

Упрощённый пример скелета драйвера:

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

#define DEVICE_NAME "my_cdev"
static int major_num;
static struct cdev my_cdev;

static int device_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "my_cdev: Device opened.n");
    return 0;
}

static ssize_t device_read(struct file *filp, char __user *buffer,
                           size_t length, loff_t *offset) {
    char message[] = "Hello from kernel!n";
    size_t msg_len = strlen(message);

    if (*offset >= msg_len)
        return 0; // EOF

    if (length > msg_len - *offset)
        length = msg_len - *offset;

    // Копируем данные из пространства ядра в пользовательское
    if (copy_to_user(buffer, message + *offset, length))
        return -EFAULT;

    *offset += length;
    return length;
}

static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = device_open,
    .read = device_read,
};

static int __init mydriver_init(void) {
    dev_t dev_num;
    // Динамически выделяем major номер
    if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0)
        return -1;
    major_num = MAJOR(dev_num);

    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;

    if (cdev_add(&my_cdev, dev_num, 1) < 0) {
        unregister_chrdev_region(dev_num, 1);
        return -1;
    }
    printk(KERN_INFO "my_cdev: Loaded with major %dn", major_num);
    return 0;
}

static void __exit mydriver_exit(void) {
    dev_t dev_num = MKDEV(major_num, 0);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "my_cdev: Unloaded.n");
}

module_init(mydriver_init);
module_exit(mydriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");

Основные сложности включали обеспечение потокобезопасности, корректную обработку ошибок и прерываний, а также поддержку совместимости с разными версиями ядра, где API мог меняться.