Linux下的驱动与无操作系统的驱动的区别:
没有操作系统的时候,驱动程序直接访问相关寄存器的相应的位即可实现目标需求,当有操作系统的时候,我们则需要在驱动程序里面设计面向操作系统内核的接口,且这样的接口由操作系统规定,对一类设备而言结构一致,独立于具体的设备。可见,当系统中存在操作系统的时候,驱动变成了连接硬件和内核的桥梁,并不会直接服务于顶层应用程序,顶层程序需要通过系统调用访问VFS再间接调用驱动程序访问硬件。
那么问题来了,有了操作系统之后,驱动反而变得更复杂,那还需要操作系统做什么。首先操作系统的存在主要是为了高效实现多并发,还有就是虚拟内存管理机制。那么其对于驱动而言,说白了就是以复杂化底层驱动程序为代价便利顶层程序调用,当驱动程序都按照操作系统给出的独立于设备的接口而设计时,那么应用程序就可以使用统一的系统调用接口来访问各种设备。
接下来以Linux下的一个LED驱动为例看看驱动程序干了些什么,因为Linux的驱动框架庞大而复杂,这里只解释一下代码涉及到的部分,并不展开细谈。
#include <linux/modules.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
/*自定义的设备结构体*/
struct led_dev
{
struct cdev cdev; //字符设备cdev结构体
char value; //led亮时为1,灭时为0,用户可读写此值
};
struct led_dev *led_devp; //定义为全局变量,在卸载函数可以直接调用
/*以下这部分信息可以通过命令 modinfo <模块名> 获得*/
MODULE_AUTHOR("YeZhuLaiPi"); //声明作者
MODULE_LICENSE("GPL v2"); //表示遵循GPL协议,没有的话内核会发出警告
/*打开函数*/
int led_open(struct inode *inode, struct file *filp) //inode是节点结构体,描述一个文件,file是文件结构体,描述一个打开的文件
{
struct led_dev *dev;
dev = container_of(inode->i_cdev, struct led_dev, cdev); //通过传入的inode找到自定义结构体指针:这里的指针指向的就是init函数里面申请的内存,所以确实可以找到自定义结构体地址
filp->private_data = dev; //让自定义的设备结构体作为设备的私有信息,后面会通过这个私有信息拿到该结构体并访问到value值
return 0;
}
/*关闭函数*/
int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数和写函数,两者可有可无,因为通过ioctl函数就可以完成数据传输并驱动*/
ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct led_dev *dev = filp->private_data; //在这里拿到自定义结构体
if (copy_to_user(buf, &(dev->value), 1)) //写入数据到用户空间
return -EFAULT;
return 1;
}
ssize_t led_write(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct led_dev *dev = filp->private_data; //同理在这里拿到自定义结构体
if (copy_from_user(&(dev->value), buf, 1)) //从用户空间拿到数据
return -EFFAULT;
if (dev->value == 1) //根据写入的值控制灯的亮灭
led_on(); //开灯函数,这里省略
else
led_off(); //关灯函数同上
return 1;
}
/*ioctl函数*/
int led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct led_dev *dev = filp->private_data; //同上在这里拿到自定义结构体
switch (cmd)
{
case 1:
dev->value = 1;
led_on();
break;
case 0:
dev->value = 0;
led_off();
break;
default: //不支持的命令
return -ENOTTY;
}
}
/*file_operations结构体,通过赋值绑定操作函数*/
struct file_operation led_fops =
{
.read = led_read,
.write = led_write,
.unlock_ioctl = led_ioctl,
.open = led_open,
.release = led_release,
};
/*设置字符设备cdev结构体*/
static void led_setup_cdev(struct led_dev *dev, int devno)
{
cdev_init(&dev->cdev, &led_fops); //初始化cdev成员,建立与led_fops之间的连接
cdev_add(&dev->cdev, devno, 1); //注册设备,在此之前得有设备号。这里可进行容错处理
}
/*模块加载函数*/
int led_init(void)
{
int result;
result = alloc_chrdev_region(&dev, 0, 1, "LED"); //这里用动态方式申请设备号
if (result < 0)
return result;
led_devp = kmalloc(sizeof(struct led_dev), GFP_KERNEL); //申请动态内存
led_setup_cdev(led_devp, result);
led_gpio_init(); //初始化IO,这里省略
return 0;
}
/*模块卸载函数*/
void led_cleanup(void)
{
cdev_del(&led_dev->cdev); //删除设备
kfree(led_devp); //释放内存
unregister_chrdev_region(result, 1); //注销设备号
}
module_init(led_init); //绑定加载函数
module_exit(led_exit); //绑定卸载函数
补充:
在驱动针对单个设备的时候,open函数里面的私有数据可以直接赋值为全局的自定义结构体指针,在针对多个设备的时候,则应该使用如上所示的container_of(),因为当存在多个设备时,加载函数会申请的动态内存的大小为n个自定义结构体的空间,要找到目标结构体,用container_of比较快。至于为什么write、read等操作函数每次都需要从filp的私有数据里面拿到自定义结构体,这属于是Linux驱动的一个“潜规则”,私有数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux面向对象的设计思想。
接下来理一下整个驱动程序的逻辑关系(红色代表自定义函数)
加载函数主要实现:
1.申请设备号 - alloc_chrdev_region()
2.为自定义结构体分配动态内存 - kmalloc()
3.对自定义结构体里的cdev进行初始化 - led_setup_cdev()
4.初始化IO - led_gpio_init()
cdev初始化函数主要实现:
1.初始化cdev - cdev_init()
2.添加一个字符设备 - cdev_add()
卸载函数主要实现:
1.注销字符设备 - cdev_del()
2.释放内存 - kfree()
3.注销设备号 - unregister_chrdev_region()
然后剩下的就是操作函数,还有变量、结构体的定义,程序中省略的部分就是对寄存器的操作,只要知道寄存器对应的虚拟地址,再进行位操作即可