如何从零开始开发 Linux 驱动程序

大家好!我是大聪明-PLUS

我最近一直在研究物联网,由于设备短缺,在尝试模拟固件时经常遇到缺少必要的 /dev/xxx 设备的问题。于是我开始思考是否可以自己编写驱动程序来让固件正常工作。无论难度如何,也无论最终能否成功,学习如何从零开始开发 Linux 驱动程序绝对不会让你后悔。

❯ 介绍

我撰写了一系列文章,主要侧重于实践,理论背景较少。我从《Linux 设备驱动程序》这本书中学习了如何开发驱动程序。

首先,Linux 操作系统分为内核空间和用户空间。对硬件设备的访问只能通过内核空间进行,而设备驱动程序可以被视为内核空间提供的一种 API,允许用户空间代码访问设备。

基于这些基本概念,我发现了一些问题,这些问题促使我去学习驱动程序开发。

  1. 在编程中,学习总是从"Hello World"程序开始,那么在这种情况下,你该如何编写"Hello World"程序呢?
  2. 驱动程序如何在 /dev 目录下生成设备文件?
  3. 驱动程序究竟是如何访问现有硬件的?
  4. 如何编写系统管理代码?或者,是否可以在不编写代码的情况下提取驱动程序?存储驱动程序的二进制文件在哪里?未来,所有这些都可以进行测试,以确定特定设备的安全性。

❯ 一切都始于"Hello World"

复制代码
`#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Hcamal");
int hello_init(void)
{
    printk(KERN_INFO "Hello World\n");
    return 0;
}
void hello_exit(void)
{
    printk(KERN_INFO "Goodbye World\n");
}
module_init(hello_init);
module_exit(hello_exit);`

Linux 驱动程序是用 C 语言编写的,我对 C 语言并不特别熟悉。在我的工作中,我经常使用 Libc 库,该库并未包含在内核中。由于驱动程序是一个运行在内核中的程序,因此我们需要使用内核中的库函数。

例如,` printkprintf` 是一个定义在内核中的输出函数,它类似于 Libc 中的 `printf` 函数。但对我来说,它更像是 Python 中的日志函数,因为输出printk会直接写入内核日志,而该日志可以通过 `log` 命令查看dmesg

驱动程序代码只有一个入口点和一个出口点。当驱动程序加载到内核时,会执行由module_init`printf` 定义的函数(在上面的代码中称为 `printf`)hello_init。当驱动程序从内核卸载时,会执行由 ` module_exitprintf` 定义的函数(在上面的代码中称为 `printf`)hello_exit

从上面的代码可以看出,加载时,驱动程序会打印"Hello World",卸载时,它会打印"" Goodbye World

顺便说一下, `printf`MODULE_LICENSE和 ` MODULE_AUTHORprintf` 并不重要,这里我就不赘述了。

还有一点:要输出 printk 函数的内容,必须添加换行符,否则缓冲区将不会被清空。

❯ 编译驱动程序

需要使用命令编译驱动程序make,相应的 Makefile 文件如下所示:

复制代码
`ifneq ($(KERNELRELEASE),)
    obj-m := hello.o
else
    KERN_DIR ?= /usr/src/linux-headers-$(shell uname -r)/
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERN_DIR) M=$(PWD) modules
endif
clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions`

通常情况下,内核源代码位于 /usr/src/linux-headers-$(shell uname -r)/ 目录中,例如:

我们需要一个用于存放已编译源代码的目录,即 `<directory> /usr/src/linux-headers-4.4.0-135-generic/`。

驱动程序代码的头文件将从该目录中查找。

`M=$(PWD)` 参数指定驱动程序编译输出将保存到当前目录。最后,这里是用于将 `<directory>` 加载到 `<directory>` 中的命令,其中

` ko` 是内核空间中的一个文件。obj-m := hello.o``hello.o``hello.ko

❯ 将驱动程序加载到内核

以下是我们需要用到的一些系统命令:

  • Lsmod查看当前正在加载的内核模块。
  • Insmod加载内核模块后要求管理员权限。
  • Rmmod:移除模块。

例如:

复制代码
`# insmod hello.ko        // Load the hello.ko module into the kernel
# rmmod hello          // Remove the hello module from the kernel`

在较早的内核版本中,内核本身的加载和卸载都是使用相同的方法,但较新的 Linux 内核版本增加了模块验证。这就是我们目前面临的情况:

复制代码
`# insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Required key not available`

从安全角度来看,当前内核假定此模块不受信任。要加载此模块,必须使用受信任的证书对其进行签名。

这可以通过两种方式实现:

  1. 进入BIOS,在UEFI中禁用安全启动。
  2. 向内核添加自签名证书,并使用它来对驱动程序模块进行签名。

❯ 查看结果

❯ 在 /dev 目录下添加设备文件

复制代码
`#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>   /* printk() */
#include <linux/slab.h>     /* kmalloc() */
#include <linux/fs.h>       /* everything... */
#include <linux/errno.h>    /* error codes */
#include <linux/types.h>    /* size_t */
#include <linux/fcntl.h>    /* O_ACCMODE */
#include <linux/cdev.h>
#include <asm/uaccess.h>    /* copy_*_user */
MODULE_LICENSE("Dual BSD/GPL");
int scull_major =   0;
int scull_minor =   0;
int scull_nr_devs = 4;
int scull_quantum = 4000;
int scull_qset = 1000;
struct scull_qset {
    void **data;
    struct scull_qset *next;
};
struct scull_dev {
    struct scull_qset *data;  /* Pointer to first quantum set. */
    int quantum;              /* The current quantum size. */
    int qset;                 /* The current array size. */
    unsigned long size;       /* Amount of data stored here. */
    unsigned int access_key;  /* Used by sculluid and scullpriv. */
    struct mutex mutex;       /* Mutual exclusion semaphore. */
    struct cdev cdev;     /* Char device structure. */
};
struct scull_dev *scull_devices;    /* allocated in scull_init_module */
/*
 * Follow the list.
 */
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
    struct scull_qset *qs = dev->data;
        /* Allocate the first qset explicitly if need be. */
    if (! qs) {
        qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
        if (qs == NULL)
            return NULL;
        memset(qs, 0, sizeof(struct scull_qset));
    }
    /* Then follow the list. */
    while (n--) {
        if (!qs->next) {
            qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
            if (qs->next == NULL)
                return NULL;
            memset(qs->next, 0, sizeof(struct scull_qset));
        }
        qs = qs->next;
        continue;
    }
    return qs;
}
/*
 * Data management: read and write.
 */
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr; /* the first listitem */
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset; /* how many bytes in the listitem */
    int item, s_pos, q_pos, rest;
    ssize_t retval = 0;
    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    if (*f_pos >= dev->size)
        goto out;
    if (*f_pos + count > dev->size)
        count = dev->size - *f_pos;
    /* Find listitem, qset index, and offset in the quantum */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum; q_pos = rest % quantum;
    /* follow the list up to the right position (defined elsewhere) */
    dptr = scull_follow(dev, item);
    if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
        goto out; /* don't fill holes */
    /* read only up to the end of this quantum */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (raw_copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
  out:
    mutex_unlock(&dev->mutex);
    return retval;
}
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
                loff_t *f_pos)
{
    struct scull_dev *dev = filp->private_data;
    struct scull_qset *dptr;
    int quantum = dev->quantum, qset = dev->qset;
    int itemsize = quantum * qset;
    int item, s_pos, q_pos, rest;
    ssize_t retval = -ENOMEM; /* Value used in "goto out" statements. */
    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;
    /* Find the list item, qset index, and offset in the quantum. */
    item = (long)*f_pos / itemsize;
    rest = (long)*f_pos % itemsize;
    s_pos = rest / quantum;
    q_pos = rest % quantum;
    /* Follow the list up to the right position. */
    dptr = scull_follow(dev, item);
    if (dptr == NULL)
        goto out;
    if (!dptr->data) {
        dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
        if (!dptr->data)
            goto out;
        memset(dptr->data, 0, qset * sizeof(char *));
    }
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
    /* Write only up to the end of this quantum. */
    if (count > quantum - q_pos)
        count = quantum - q_pos;
    if (raw_copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
        retval = -EFAULT;
        goto out;
    }
    *f_pos += count;
    retval = count;
        /* Update the size. */
    if (dev->size < *f_pos)
        dev->size = *f_pos;
  out:
    mutex_unlock(&dev->mutex);
    return retval;
}
/* Beginning of the scull device implementation. */
/*
 * Empty out the scull device; must be called with the device
 * mutex held.
 */
int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset;   /* "dev" is not-null */
    int i;
    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) {
            for (i = 0; i < qset; i++)
                kfree(dptr->data[i]);
            kfree(dptr->data);
            dptr->data = NULL;
        }
        next = dptr->next;
        kfree(dptr);
    }
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}
int scull_release(struct inode *inode, struct file *filp)
{
    printk(KERN_DEBUG "process %i (%s) success release minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}
/*
 * Open and close
 */
int scull_open(struct inode *inode, struct file *filp)
{
    struct scull_dev *dev; /* device information */
    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = dev; /* for other methods */
    /* If the device was opened write-only, trim it to a length of 0. */
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
        if (mutex_lock_interruptible(&dev->mutex))
            return -ERESTARTSYS;
        scull_trim(dev); /* Ignore errors. */
        mutex_unlock(&dev->mutex);
    }
    printk(KERN_DEBUG "process %i (%s) success open minor(%u) file\n", current->pid, current->comm, iminor(inode));
    return 0;
}
/*
 * The "extended" operations -- only seek.
 */
loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
    struct scull_dev *dev = filp->private_data;
    loff_t newpos;
    switch(whence) {
      case 0: /* SEEK_SET */
        newpos = off;
        break;
      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + off;
        break;
      case 2: /* SEEK_END */
        newpos = dev->size + off;
        break;
      default: /* can't happen */
        return -EINVAL;
    }
    if (newpos < 0)
        return -EINVAL;
    filp->f_pos = newpos;
    return newpos;
}
struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    // .unlocked_ioctl = scull_ioctl,
    .open =     scull_open,
    .release =  scull_release,
};
/*
 * Set up the char_dev structure for this device.
 */
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    dev->cdev.ops = &scull_fops;
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be. */
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
    else
        printk(KERN_INFO "scull: %d add success\n", index);
}
void scull_cleanup_module(void)
{
    int i;
    dev_t devno = MKDEV(scull_major, scull_minor);
    /* Get rid of our char dev entries. */
    if (scull_devices) {
        for (i = 0; i < scull_nr_devs; i++) {
            scull_trim(scull_devices + i);
            cdev_del(&scull_devices[i].cdev);
        }
        kfree(scull_devices);
    }
    /* cleanup_module is never called if registering failed. */
    unregister_chrdev_region(devno, scull_nr_devs);
    printk(KERN_INFO "scull: cleanup success\n");
}
int scull_init_module(void)
{
    int result, i;
    dev_t dev = 0;
    /*
     * Get a range of minor numbers to work with, asking for a dynamic major
     * unless directed otherwise at load time.
     */
    if (scull_major) {
        dev = MKDEV(scull_major, scull_minor);
        result = register_chrdev_region(dev, scull_nr_devs, "scull");
    } else {
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
        scull_major = MAJOR(dev);
    }
    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
        return result;
    } else {
        printk(KERN_INFO "scull: get major %d success\n", scull_major);
    }
        /*
     * Allocate the devices. This must be dynamic as the device number can
     * be specified at load time.
     */
    scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
    if (!scull_devices) {
        result = -ENOMEM;
        goto fail;
    }
    memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));
        /* Initialize each device. */
    for (i = 0; i < scull_nr_devs; i++) {
        scull_devices[i].quantum = scull_quantum;
        scull_devices[i].qset = scull_qset;
        mutex_init(&scull_devices[i].mutex);
        scull_setup_cdev(&scull_devices[i], i);
    }
    return 0; /* succeed */
  fail:
    scull_cleanup_module();
    return result;
}
module_init(scull_init_module);
module_exit(scull_cleanup_module);`

❯ 驱动分类

驱动程序分为三类:字符设备、块设备和网络接口。上面的代码主要针对字符设备,而讨论其他两类设备超出了本文的范围。

如上所示,brw-rw--块设备的访问权限字符串以字母"b"开头,而字符设备的访问权限字符串以字母"c"开头。

❯ 关于大数和小数

主设备号用于区分不同的驱动程序。原则上,如果设备的主设备号相同,则表示它们由同一个驱动程序控制。

可以在同一个驱动器目录中创建多个设备,它们之间的区别仅在于次设备号。主设备号和次设备号共同决定了驱动程序控制的设备(如上所示)。

复制代码
`brw-rw----  1 root disk      8,   0 Dec 17 13:02 sda
brw-rw----  1 root disk      8,   1 Dec 17 13:02 sda1`

该设备的最高有效数字为 8,最低有效数字sdasda1两个:一个设备的最低有效数字为 0,另一个设备的最低有效数字为 1。

❯ 驱动程序如何提供 API

我习惯把它看作/dev/xxx是由文件提供的接口,而在 Linux 系统中,"一切皆文件"。因此,当我们操作驱动程序时,本质上就是在操作一个文件,驱动程序负责定义、打开、读取、写入......等等/dev/xxx。任何使用驱动程序 API 的操作都是文件操作。这里涉及哪些文件操作呢?它们都定义在内核头文件<linux/fs.h>的 file_operations 结构中。 在上面的示例代码中:

复制代码
`struct file_operations scull_fops = {
    .owner =    THIS_MODULE,
    .llseek =   scull_llseek,
    .read =     scull_read,
    .write =    scull_write,
    .open =     scull_open,
    .release =  scull_release,
};`

我定义并赋值结构体。除了所有者之外,所有其他成员的值都是函数指针。

然后,我使用方括号 cdev_add为每个驱动程序注册文件操作结构体,这在方括号 函数中完成scull_setup_cdev

例如,当对驱动程序管理的设备执行"打开"操作时,我执行 scull_open 函数,这相当于在系统调用中"钩住"了 open 函数。

❯ 如何在 /dev 下生成我们需要的设备

编译上述代码后,我们得到scull.ko一个文件,然后对其进行签名,最后使用 将其加载到内核中insmod

让我们检查一下是否加载成功:

是的,设备驱动程序已成功加载,但未在 /dev 目录中创建设备文件。您必须手动使用 mknod 命令关联设备:

❯ 结果

在这个例子中,我们没有对特定设备执行任何操作;我们只是使用了方括号 kmalloc来访问内核空间中的一块内存。

我不会深入讲解代码细节;你可以通过谷歌搜索或研究头文件来找到所有相关信息。

在本文中,我想和大家分享如何自学开发驱动程序:首先,阅读书籍来掌握基本概念,然后在实际应用中查找具体细节的信息。

例如,我不知道驱动程序提供了哪些 API。我只需要知道这样的 API 仅限于文件操作。目前,我只需要 openclose read 操作write。如有需要,我可以查看其他文件操作是如何实现的。

相关推荐
摇滚侠1 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 小时前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈3 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
国产化创客4 小时前
ESP32 CameraWebServer 原生摄像头项目全解析
物联网·开源·嵌入式·实时音视频·智能硬件
凡人叶枫4 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875245 小时前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant
java_cj5 小时前
深入kube-apiserver认证机制:从Bearer Token到mTLS的完整认证链解析
linux·运维·服务器·云原生·容器·kubernetes
lsyeei5 小时前
linux 系统目录详解
linux·运维·服务器