65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Visual Studio 创建 Linux 内核驱动程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (22投票s)

2014年3月19日

CPOL

3分钟阅读

viewsIcon

38544

本文介绍了如何在现代 Linux 内核上创建和调试一个简单的设备驱动程序

引言

Linux 是一个优秀的开源操作系统,广泛应用于从桌面到嵌入式 ARM 板等诸多应用中。虽然大多数与 Linux 相关的代码都在用户模式下运行,但为新设备添加支持通常需要创建内核模块,这涉及编码和调试的特殊技巧。本文将以一个简单的虚拟字符设备为例,演示 Linux 内核模式技术。

我们将使用 Microsoft Visual Studio 作为我们的 IDE,通过其各种编码和调试功能来简化我们的开发。

在开始之前,请确保您已安装以下各项

创建一个基本项目

确保您可以从 Windows 机器通过串口访问您的 Linux 机器。建议使用带有虚拟 COM 端口的 VM,因为它快速且方便。启动 Visual Studio 并创建一个新的 VisualKernel 模块项目

按照向导创建您的项目。在最后一页上指定您要使用的调试方法。如果您未使用 VMWare,请指定可用于连接到虚拟机的 COM 端口

您现在已经创建了一个基本的“Hello, World”项目。使用 Ctrl-Shift-B 构建它,并使用 F5 开始调试:

一旦我们验证了内核模块可以构建和调试,我们将创建实际的字符设备。

创建设备对象

为了创建我们的设备对象并使其对用户模式应用程序可见,我们需要执行 4 个步骤

  1. 为设备动态分配一个“主设备号”
  2. 使用分配的主设备号注册一个字符设备
  3. 为我们的设备创建一个 sysfs 设备类
  4. 创建一个 sysfs 设备,以便系统可以在 /dev 中创建一个节点

为了使本示例中的代码更简洁,我们将省略完整的错误处理,并使用 BUG_ON() 语句来检查我们的设备注册是否成功。

首先,我们需要包含包含必要内核结构的头文件

#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>

接下来,我们需要声明静态变量来保存我们的设备号、类对象和设备对象

struct file_operations demo_operations = {
.owner = THIS_MODULE,
};

static dev_t device_major_number;
static struct cdev character_device;
static struct class *class_object;
static struct device *device_object;

最后,我们需要在内核模块初始化函数中实现设备创建

static int __init LinuxKernelModule1_init(void)
{
    int status;
    const char *device_name = "MyCharacterDevice";

    cdev_init(&character_device, &demo_operations);

    printk("Allocating a major number...\n");
    status = alloc_chrdev_region(&device_major_number, 0, 1, device_name);
    BUG_ON(status < 0);

    printk("Adding a character device with number %d...\n", device_major_number);
    status = cdev_add(&character_device, device_major_number, 1);
    BUG_ON(status < 0);

    printk("Creating a device class \"%s\"...\n", device_name);
    class_object = class_create(THIS_MODULE, device_name);
    BUG_ON(IS_ERR(class_object));

    printk("Creating a device object \"%s\"...\n", device_name);
    device_object = device_create(class_object, NULL, device_major_number, NULL, device_name);
    BUG_ON(IS_ERR(device_object));

    printk("Device object created!");
    return 0;
} 

卸载模块时,我们应该撤销在初始化函数中执行的每个操作

static void __exit LinuxKernelModule1_exit(void)
{
    device_destroy(class_object, device_major_number);
    class_destroy(class_object);
    cdev_del(&character_device);
    unregister_chrdev_region(device_major_number, 1);
} 

测试它

构建项目。请注意,由于我们现在使用一些仅限 GPL 的 API,因此我们需要将模块许可证更改为 GPL。在初始化函数中设置一个断点,并使用 F5 开始调试您的项目。单步执行初始化过程,观察变量值以了解正在发生的事情:

打开 SSH 会话窗口,并尝试使用“cat”工具从设备读取一些数据

Linux 将报告“无效参数”错误。发生这种情况是因为我们没有提供一个 read() 函数,该函数将在每次有人尝试从我们的设备读取数据时被调用。我们将在下一步中完成它。

向用户模式提供数据

我们可以通过实现一个读取处理程序来控制在用户模式应用程序从我们的设备读取数据时提供的数据 - 一个特殊的函数,每次有人从我们的设备读取数据时,内核都会调用该函数。我们将实现一个非常基本的功能,返回一条固定的消息

static ssize_t demo_read_function(struct file *pFile, char __user *pBuffer, size_t size, loff_t *pOffset)
{
    static const char message[] = "Hello, User-mode\n";

    int todo = sizeof(message) - 1 - *pOffset;
    if (todo < 0)
        todo = 0;

    todo = min(todo, size);
    if (todo > 0)
        copy_to_user(pBuffer, message + *pOffset, todo);

    *pOffset += todo;
    return todo;
} 

我们现在需要做的就是将此函数注册到我们的 file_operations 结构中

 struct file_operations demo_operations = {
    .owner = THIS_MODULE,
    .read = demo_read_function
};

构建您的模块并开始调试它。运行“cat /dev/MyCharacterDevice”以查看“Hello, User-mode”消息。在 demo_read_function() 中设置一个断点,然后重新运行 cat 来单步执行它

使用调用堆栈窗口浏览 Linux 内核源代码,并查看负责调用我们代码的 Linux 代码。享受调试的乐趣!

© . All rights reserved.