(设备号是一个32位的整数用来唯一标识Linux系统中的所有设备) https://m.toutiao.com/is/D3gS1Uc/?= 字符设备驱动是Linux内核中的一种设备驱动类型,主要用于管理字符设备的输入输出操作。如终端设备、串口、打印机、键盘等都是字符设备。与块设备驱动不同的是,字符设备驱动不需要缓存数据,因此非常适合那些不需要频繁的数据访问的设备。 一般而言,一个完整的字符设备驱动包含以下几个部分:设备初始化、设备注册、文件操作函数定义、中断处理函数定义、设备卸载等。 一、设备初始化设备初始化是字符设备驱动的第一步,其主要任务是在内核中分配设备号并将字符设备驱动注册到内核中。设备号是一个32位的整数,用来唯一标识Linux系统中的所有设备,不同的设备应使用不同的设备号。系统中的前256个设备号被预留给Linux内核使用,因此对于第三方的字符设备来说,应该选择一个未被预留的设备号。 为了分配设备号,我们需要定义一个struct cdev对象,并在其中填充设备号、文件操作函数表等相关信息。例如: struct cdev mydev; static struct file_operations mydev_fops = { .owner = THIS_MODULE, .read = mydev_read, .write = mydev_write, .open = mydev_open, .release = mydev_release, }; static int __init mydev_init(void) { int ret; dev_t devno; // 分配设备号 if (alloc_chrdev_region(&devno, 0, 1, 'mydev') < 0) { printk(KERN_ERR 'alloc_chrdev_region failed\n'); return -1; } // 初始化cdev对象 cdev_init(&mydev, &mydev_fops); mydev.owner = THIS_MODULE; // 注册字符设备驱动到内核 ret = cdev_add(&mydev, devno, 1); if (ret < 0) { printk(KERN_ERR 'cdev_add failed\n'); unregister_chrdev_region(devno, 1); return -1; } printk(KERN_INFO 'mydev driver initialized\n'); return 0; } 上面的代码中,我们首先定义了一个struct cdev对象mydev,并将其与文件操作函数表mydev_fops相关联。然后通过alloc_chrdev_region函数分配一个设备号,并将其保存在devno中。接着我们调用cdev_init函数对mydev进行初始化,并将其owner成员指向当前模块。 最后,我们使用cdev_add函数将mydev注册到内核中,并检查其返回值,如果返回值小于0则表示注册失败。当设备注册成功后,我们可以使用cat /proc/devices命令来查看系统中所有的字符设备,其中包括我们刚刚注册的设备。 二、设备注册设备注册是指将字符设备驱动注册到系统中的设备接口列表中。在注册设备之前,我们需要定义一个struct class对象,并在其中填充设备类名等相关信息,例如: struct class *mydev_class; static int __init mydev_init(void) { ... // 创建设备类 mydev_class = class_create(THIS_MODULE, 'mydev_class'); if (IS_ERR(mydev_class)) { printk(KERN_ERR 'class_create failed\n'); cdev_del(&mydev); unregister_chrdev_region(devno, 1); return -1; } // 创建设备文件 device_create(mydev_class, NULL, devno, NULL, 'mydev%d', 0); printk(KERN_INFO 'mydev driver initialized\n'); return 0; } 上面的代码中,在成功分配设备号并将mydev注册到内核中之后,我们通过class_create函数创建了一个设备类mydev_class。设备类用于将同一类型的设备分组,例如所有的串口设备可以放在一个设备类中。 然后我们使用device_create函数创建了设备文件,将设备号devno与设备类mydev_class进行关联,为设备文件命名为mydev%d,其中%d表示设备文件的编号,这里我们只创建了一个设备文件,因此参数为0。 在设备文件创建成功后,我们可以在/dev目录下看到mydev0文件。 三、文件操作函数文件操作函数是字符设备驱动中最为重要的部分,它定义了对设备的读写操作、设备打开和关闭以及设备的控制操作。在字符设备驱动中,每个文件操作函数都对应着一个文件操作符,因此任何对该设备的操作都会触发相应的文件操作函数。 字符设备驱动中文件操作函数的定义如下: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); int (*open) (struct inode *, struct file *); int (*release) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*fsync) (struct file *, int datasync); }; 其中,llseek函数用于控制读写指针的位置,read函数用于从设备中读数据,write函数用于向设备中写数据,open和release函数用于打开和关闭设备。 这里我们给出一个简单的示例,实现从设备中读取10个字节的数据。 static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { char kbuf[10] = 'abcdefghi'; int ret = 0; if (*f_pos > 9) return 0; if (count + *f_pos > 10) count = 10 - *f_pos; if (copy_to_user(buf, kbuf + *f_pos, count)) return -EFAULT; *f_pos += count; ret = count; return ret; } 上面的代码中,我们定义了一个kbuf数组来存储数据。然后我们根据当前读取的位置f_pos,计算出需要读取的字节数count,并使用copy_to_user函数将数据复制到用户空间。 最后我们更新读取的位置f_pos,并返回读取的字节数。 四、中断处理函数在字符设备驱动中,有些硬件设备会产生中断,这时候需要定义相应的中断处理函数来处理中断事件。Linux中的中断机制非常强大,可以处理各种类型的中断事件,包括定时器中断、硬件中断、软中断等。 在字符设备驱动中,我们需要定义一个中断处理函数,并将其注册到相应的中断号上。例如: static irqreturn_t mydev_interrupt(int irq, void *dev_id) { printk(KERN_INFO 'mydev_interrupt\n'); return IRQ_HANDLED; } static int __init mydev_init(void) { ... // 注册中断 if (request_irq(IRQ_NUMBER, mydev_interrupt, IRQF_SHARED, 'mydev', &mydev)) { printk(KERN_ERR 'request_irq failed\n'); device_destroy(mydev_class, devno); class_destroy(mydev_class); cdev_del(&mydev); unregister_chrdev_region(devno, 1); return -1; } printk(KERN_INFO 'mydev driver initialized\n'); return 0; } 上面的代码中,我们定义了一个mydev_interrupt函数来处理中断事件。在设备初始化中,我们通过request_irq函数将该中断处理函数注册到中断号IRQ_NUMBER上,并将mydev作为传递给中断处理函数的参数。其中,IRQF_SHARED表示该中断可以被多个驱动程序共享。 在进入中断处理函数后,我们可以对设备进行相应的操作,例如读取已经接收到的数据、清除中断标志等。最后,我们需要使用IRQ_HANDLED返回一个中断事件已经被成功处理的标志。 五、设备卸载设备卸载需要释放所有已经分配的资源,包括设备号、设备文件、设备类、中断等。一个完整的设备卸载函数应该包含以下几个步骤: static void __exit mydev_exit(void) { // 释放中断 free_irq(IRQ_NUMBER, &mydev); // 销毁设备文件和设备类 device_destroy(mydev_class, devno); class_destroy(mydev_class); // 从内核中注销字符设备驱动 cdev_del(&mydev); unregister_chrdev_region(devno, 1); printk(KERN_INFO 'mydev driver exited\n'); } module_exit(mydev_exit); 上面的代码中,我们依次使用free_irq、device_destroy、class_destroy、cdev_del和unregister_chrdev_region函数来释放各种资源。最后我们使用module_exit宏定义来注册设备卸载函数mydev_exit。当该模块被卸载时,mydev_exit函数将被自动调用。 总结本文详细介绍了字符设备驱动框架的实现,包括设备初始化、设备注册、文件操作函数定义、中断处理函数定义和设备卸载等。字符设备驱动是Linux内核中非常重要的一部分,通过学习字符设备驱动的实现,可以帮助我们更好地理解Linux内核中设备驱动的工作原理。 |
|