当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]Linux内核模块是动态加载到内核中的代码片段,常用于驱动硬件、扩展系统调用或实现新的文件系统。字符设备驱动是最基础的驱动类型——它以字节流的方式与用户空间交互,像操作普通文件一样open/read/write/close。本文从零编写一个名为"mydev"的虚拟字符设备驱动,涵盖模块框架、设备号申请、cdev注册和用户空间测试。



Linux内核模块是动态加载到内核中的代码片段,常用于驱动硬件、扩展系统调用或实现新的文件系统。字符设备驱动是最基础的驱动类型——它以字节流的方式与用户空间交互,像操作普通文件一样open/read/write/close。本文从零编写一个名为"mydev"的虚拟字符设备驱动,涵盖模块框架、设备号申请、cdev注册和用户空间测试。

一、模块框架:加载与卸载

每个内核模块必须有module_init和module_exit两个入口点,分别指定初始化函数和退出函数。

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/cdev.h>

#include <linux/uaccess.h>


#define DEVICE_NAME "mydev"


static int __init my_init(void) {

   printk(KERN_INFO "mydev: module loaded\n");

   return 0;

}


static void __exit my_exit(void) {

   printk(KERN_INFO "mydev: module unloaded\n");

}


module_init(my_init);

module_exit(my_exit);

MODULE_LICENSE("GPL");

编译后insmod mydev.ko加载,rmmod mydev卸载,通过dmesg查看内核日志。

二、设备号与cdev注册

字符设备通过主设备号和次设备号标识。推荐动态分配设备号,避免冲突。

static int major;

static struct cdev my_cdev;


static int __init my_init(void) {

   dev_t dev_num;

   // 1. 动态分配设备号

   alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);

   major = MAJOR(dev_num);


   // 2. 初始化cdev并关联file_operations

   cdev_init(&my_cdev, &my_fops);

   my_cdev.owner = THIS_MODULE;


   // 3. 添加cdev到内核

   cdev_add(&my_cdev, dev_num, 1);


   printk(KERN_INFO "mydev: registered with major %d\n", major);

   return 0;

}


static void __exit my_exit(void) {

   dev_t dev_num = MKDEV(major, 0);

   cdev_del(&my_cdev);

   unregister_chrdev_region(dev_num, 1);

   printk(KERN_INFO "mydev: unregistered\n");

}

三、file_operations实现

file_operations结构体定义了用户空间操作设备文件时的回调函数。这里实现open、read、write和release。

static int my_open(struct inode *inode, struct file *filp) {

   filp->private_data = &dev_data;  // 关联设备数据

   return 0;

}


static ssize_t my_read(struct file *filp, char __user *buf,

                      size_t count, loff_t *f_pos) {

   struct mydev_data *data = filp->private_data;

   size_t available = data->len - *f_pos;

   size_t to_copy = min(count, available);

   if (to_copy == 0) return 0;

   if (copy_to_user(buf, data->buffer + *f_pos, to_copy))

       return -EFAULT;

   *f_pos += to_copy;

   return to_copy;

}


static ssize_t my_write(struct file *filp, const char __user *buf,

                       size_t count, loff_t *f_pos) {

   struct mydev_data *data = filp->private_data;

   size_t to_copy = min(count, sizeof(data->buffer));

   if (copy_from_user(data->buffer, buf, to_copy))

       return -EFAULT;

   data->len = to_copy;

   *f_pos = 0;

   return to_copy;

}


static int my_release(struct inode *inode, struct file *filp) {

   return 0;

}


static struct file_operations my_fops = {

   .owner   = THIS_MODULE,

   .open    = my_open,

   .read    = my_read,

   .write   = my_write,

   .release = my_release,

};

注意:copy_to_user和copy_from_user是必须的,不能直接用memcpy。

四、用户空间测试

编译驱动后用insmod加载,创建设备节点(如果使用udev则自动创建),然后运行测试程序:

int fd = open("/dev/mydev", O_RDWR);

write(fd, "Hello driver!", 13);

char buf[256];

read(fd, buf, sizeof(buf));

printf("Read: %s\n", buf);

close(fd);

五、三个翻车高发点

忘记创建设备节点:如果没有udev,需要手动mknod /dev/mydev c major 0,否则open返回-1。

copy_to_user返回值未检查:如果拷贝失败返回非零,应返回-EFAULT,否则用户空间读到垃圾数据。

open时未设置private_data:后续read/write中访问野指针导致内核崩溃。

从零编写字符设备驱动是理解Linux内核运作机制的起点。掌握模块框架、设备号、cdev和file_operations,就能为各种硬件编写驱动程序。在此基础上,可以进一步学习阻塞IO、ioctl、设备树等高级主题。



本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭