嵌入式开发交流网论坛
标题:
Linux内核学习:简单的字符设备驱动
[打印本页]
作者:
未完续·
时间:
2020-12-17 11:39
标题:
Linux内核学习:简单的字符设备驱动
学习Linux内核最好的入门方式之一是从字符设备驱动开始模仿(来自于《奔跑吧 Linux内核——入门篇》)。对于我们日常生活中存在的大量设备,如摄像头,USB充电器,蓝牙,Wi-Fi等,这些设备在电气特性和实现原理均不相同,对Linux系统来说如何抽象和描述他们呢?Linux很早就根据设备共同特征将其划分为三大类型:1,字符设备;块设备;网络设备。
Linux内核设备驱动架构如下图所示,Linux内核针对上述3类设备抽象出来一套完整的驱动框架和API接口,以便驱动开发者在编写某类设备驱动时可重复使用。
[attach]55837[/attach]
字符设备是以字节为单位的I/O传输,这种字符流的传输率通常比较低。常见字符设备有鼠标、键盘、触摸屏等;块设备是以块为单位传输的,常见块设备是磁盘;网络设备是一类比较特殊的设备,涉及到网络协议层,因此将其单独分成一类设备。·下面是一个简单字符设备示例,实现了基本的open、read和write方法:
代码文件:
#include #include #include #include #include static signed count = 1;#define DEMO_NAME "my_demo_dev" static struct cdev *demo_cdev = NULL; static dev_t dev;//打开设备 static int demodrv_open(struct inode *inode, struct file *file){int major = MAJOR(inode->i_rdev); int minor = MINOR(inode->i_rdev); printk("%s: (major=%d, minor=%d)\n", __func__, major, minor); return 0; } //关闭设备 static int demodrv_release(struct inode *inode, struct file *filp) { return 0; }//读设备static ssize_t demodrv_read(struct file *filp, char __user *buf, size_t size, loff_t *offset) { printk("%s enter\n", __func__); return 0; }//写设备 static ssize_t demodrv_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset) { return 0; }//操作方法集 static struct file_operations fops = { .owner = THIS_MODULE, .open = demodrv_open, .release= demodrv_release, .read = demodrv_read, .write = demodrv_write, };//cdev设备模块初始化 static int __init simple_char_init(void) { int ret; ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME); if(ret) { printk("failed to allocate char device region"); return ret; } //1. alloc cdev obj demo_cdev = cdev_alloc; if(NULL == demo_cdev) { printk("cdev_alloc failed\n"); goto unregister_chrdev; } //2. init cdev obj cdev_init(demo_cdev, &fops); //3. register cdev obj ret = cdev_add(demo_cdev, dev, count); if(ret){ printk("cdev_add failed\n"); goto cdev_fail; } printk("succeeded register char device: %s \n", DEMO_NAME); printk("Major number = %d, minor number = %d \n", MAJOR(dev), MINOR(dev)); return 0;unregister_chrdev: unregister_chrdev_region(dev, count);cdev_fail: cdev_del(demo_cdev); return ret;}static void __exit simple_char_exit(void){ printk("removing device \n"); if(demo_cdev) cdev_del(demo_cdev);}module_init(simple_char_init);module_exit(simple_char_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Snail");MODULE_DESCRIPTION("simple character device");
Makefile:
BASEINCLUDE ?= /lib/modules/`uname -r`/buildmydemo-objs := my_demodrv.oobj-m := mydemo.oall :$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules;clean : $(MAKE) -C $(BASEINCLUDE) SUBDIRS=$(PWD) clean; rm -f *.ko;
-编译后,通过insmod mydemo.ko加载该内核模块,通过系统日志我们可以观察到该设备初始化时侯需输出的设备号信息:
[attach]55838[/attach]
-在 /proc/devices可以看到my_demo_dev的设备
$cat /proc/devicesCharacter devices:1 mem4 /dev/vc/0………203 cpu/cpuid226 drm242 my_demo_dev
-生成的设备需要在/dev/目录下生成对应节点,此处需要手动生成:mknod /dev/demo_drv c 242 0这样在/dev/目录就可以看到一条demo_drv设备节点信息crw-r--r-- 1 root root 242, 0 Dec 12 16:51 demo_drv
·上述内容完成了内核相关的事情,接着是用户空间测试程序,用于测试read函数的调用:
#include #include #include #define DEMO_DEV_NAME "/dev/demo_drv"int main{char buffer; int fd = open(DEMO_DEV_NAME, O_RDONLY); if(0 > fd){ printf("open device %s failded\n", DEMO_DEV_NAME); return -1; } read(fd, buffer, 64); close(fd); return 0;}
编译后,运行,可以在系统日志看到demodrv_read&open函数的输出:
[attach]55839[/attach]
对于上面提到的字符设备驱动架构和所使用的API接口函数进行分析介绍:
字符设备驱动管理核心对象是以字符为数据流的设备。Linux内核对于字符设备需要有个数据结构来对其进行抽象和描述,这就是cdev数据结构():
struct cdev {struct kobject kobj;//用于Linux设备驱动模型 struct module *owner;//字符设备驱动所在内核模块对象指针 const struct file_operations *ops;//关键操作函数,和应用程序交互桥梁 struct list_head list;//将字符设备串为链表 dev_t dev;//字符设备设备号,含主次设备号 unsigned int count;//同一主设备号的次设备号个数;对应获取宏为MAJOR(dev) & MINOR(dev)};
struct cdev产生方式有两种:全局静态变量和内核接口函数cdev_alloc。即:
static struct cdev mydemo_cdev;或struct cdev mydemo_cdev = cdev_alloc;
Linux 内核cdev相关API函数还有:
cdev_init:初始化cdev数据结构,并建立设备与驱动操作方法file_operations间的连接关系——void cdev_init(struct cdev *cdev, const struct file_operations *fops)cdev_add:将一个字符设备添加到系统中,通常在驱动程序的probe函数里会调用该接口来注册一个字符设备——int cdev_add(struct cdev *p, dev_t dev, unsigned count)其中,p表示一个设备的cdev数据结构;dev表示设备的设备号;count表示这个主设备号可以有多少个次设备号(通常同一个主设备号可以有多个次设备号不同的设备)。cdev_del:从系统删除一个cdev,通常在驱动程序的卸载函数会调用——void cdev_del(struct cdev *p)
设备号的管理:
字符串驱动初始化函数probe一个重要工作就是为设备分配设备号(不能有两个设备共用同一设备号),为此Linux内核提供了两个接口函数完成设备号的申请:
-int register_chrdev_region(dev_t from, unsigned count, const char *name)
该函数需要指定主设备号,可连续分配多个,但需要驱动编写者确保主设备号未被使用,内核文档documentation/devices.txt文件描述了系统已分配出的设备号
-int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
该函数会自动分配主设备号,可以避免和系统占用的主设备号冲突
驱动程序的卸载函数需要把主设备号释放给系统,就会调用如下接口函数:void unregister_chrdev_region(dev_t from, unsigned count)
设备节点:
Linux系统万物皆文件的原则,我们知道设备节点也是一种特殊文件,称为设备文件,是连接内核空间驱动程序和用户空间应用程序的桥梁。应用程序若想使用驱动程序提供的服务OR操作设备,则需要通过访问该设备文件来完成。
按照Linux习惯,系统设备节点存放在/dev/目录,ls -l可看到文件属性第一个字符c表示字符设备,d表示块设备,后面显示设备主次设备号。
设备节点生成方式:
-手动生成:mknod filename type major minor
-udev用户空间工具动态生成:该工具可根据硬件设备状态动态更新设备节点(创建删除等),需要联合sysfs(提供设备入口+uevent通道)和tmpfs(提供存放空间)来实现。
字符设备操作方法集:
上面简单的“字符设备驱动示例”中实现了fops操作方法集,包含open, release, read, write等方法。即定义函数指针,称之为file_operations方法。该方法通过cdev_init函数和设备建立一个连接关系,故在用户空间test程序可以使用open函数打开这个设备节点。
应用程序open函数执行时,会通过系统调用进入到内核空间,在内核空间的虚拟文件系统层VFS经过复杂的转换,最终调用设备驱动的file_operations方法集中的open方法。file_operations结构体定义了众多的方法,但实际设备开发并不需要每个都去实现,常用的几个方法如下:
llseek:修改文件当前读写位置,返回新位置read:从设备驱动中读取数据到用户空间,返回成功读取的字节数,失败则返回负数write:将用户数据写入设备,返回成功写入的字节数poll:查询设备是否可以立即读写,主要用于阻塞型I/O操作mmap:将设备内存映射到进程的虚拟地址空间。open:打开设备release:关闭设备unlocked_ioctl和compat_ioctl:提供与设备相关的控制命令的实现aio_read和aio_write:异步I/O读写函数,即提交完I/O请求后立即返回,不须等待I/O操作完成再做别的事情。设备完成I/O操作后,可通过发送信号或回调函数等方式通知。fsync:异步通知方法。
欢迎光临 嵌入式开发交流网论坛 (http://www.dianzixuexi.com/bbs/)
Powered by Discuz! X3.2