Linux设备驱动
第一章 设备驱动简介
- 驱动程序的角色是提供机制,而不是策略。
- 编写内核代码来存取硬件,但是不能强加特别的策略给用户(只需要表现出硬件的最基本的功能,如何使用这些功能又用户自己选择)
- 对策略透明的驱动特征:支持同步和异步操作、可以多次打开、利用硬件全部能力、没有软件层提供策略相关操作。
- 内核角色划分为:进程管理、内存管理、文件系统、设备控制、网络。
- 字符设备是一种可以当做一个字节流来存取的设备,字符设备通过文件系统存取。
- 块设备通过位于/dev目录的文件系统结点来存取,一个块设备只能传送一个或多个长度经常是512字节(或更大的2的幂)的整块。但是Linux允许应用程序像字符设备一样读写一个块设备。
- 一个网络接口负责发送和接受数据报文,在内核网络子系统驱动下,不需要知道如何把一个事务映射到实际的被发送和接受的报文上,例如面向流的TCP,但是网络设备被设计成处理报文的发送和接收,网络驱动只处理报文。
- 文件系统其实是一个软件驱动。
- 安 全问题:驱动编写者应避免将安全策略编写到代码中,但是有例外:某些类型的设备存取可能反面地影响系统;驱动程序BUG,特别是内存访问控制上的;用户进 程接收的输入除非能核实它,否则不要信任它;从内核获取的内存应当清零或者在对用户进程或设备可用前初始化;设备解析发送给它的数据,就需要确保用户不能 发送任何危及系统的东西;
第二章 建立和运行模块
- printk(KERN_ALERT “hello”);的优先级之后,不需要加逗号。
- 模块只连接到内核,唯一能调用的函数就是内核输出的那些,因此模块源文件不应当包含通常的头文件,<stdarg.h>和其他特殊情况例外。
- 内核编程错误至少会杀掉当前进程。
- 驱动经常做两个任务:驱动模块中一些函数作为系统调用的一部分,一些负责中断处理。
- 内核编程的堆栈远远小于用户空间编程,如果需要大的结构,应当在调用时间内动态分配内存。
- 双下划线开始的函数,是一个底层的接口组件,要小心使用。
- 内核代码不能做浮点算术(内核里不能使用浮点变量?)
- 一种方便的makefile写法,思路是使用KERNELRELEASE变量来定位内核源码目录,如果没有内核源码树目录,则从已安装模块目录中的符号连接指回内核建立树;如果有则调用默认建立工作。
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),) #在ifneq后面需要有一个空格
obj-m := hello.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
- 在编译模块时,会出现make nothing to be done for "default"错误,这是因为Makefile文件编写格式有错误,标准写法应该在default执行语句前加一个TAB键
default:
(一个TAB)$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- 测 试了一个hello模块,可以成功加载和卸载,但是没有printk()输出。(2010年1月2日问题解决)由于终端控制台输出的信息级别问题没有显 示,可以将输出级别设置为最高级KERN_EMERG,或者到/var/log/messages文件下可以查看到内核打印出的信息!
- insmod工作过程:加载模块的代码段和数据段到内核,然后连接模块中任何未解决的符号到内核的符号表上,内核不修改模块的磁盘文件,而只是在内存中又拷贝了一份。
- 大部分基于内核版本的依赖性可以使用预处理器条件解决,处理不兼容的最好方式是把他们限制到特定的头文件(如何限制?)
- 通用原则:明显版本依赖的代码应该隐藏在一个低级的宏定义或者函数后面。
- 当加载一个模块,内核为模块检查特定处理器的配置选项,确认他们匹配运行着的内核,如果模块用不同选项编译,则不会加载。
- 没明白“发布驱动-GPL-许可-以二进制发布驱动-许可”之间的关系。版权问题?
- 新的模块可以用你模块输出的符号,即可以堆叠新的模块在其他模块之上。
- modprobe可以自动加载任何你加载模块所需的其他模块,但只查找标准的已安装模块目录。
- 宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明,这个变量存储于模块的一个特殊的可执行部分(一个"ELF段"),内核用这个部分在加载时找到模块输出的变量。
- 标准初始化函数定义如下,其中_init是可选的,表示在加载模块后把这个函数丢弃,释放内存,_initdata类似,表示只在初始化时用的数据。
static int _init initialization_function(void)
{
}
module_init(initialization_function);
- 清理函数定义如下,注销接口,在模块被除去前返回所有资源给系统,_exit标识符同上
static void _exit cleanup_function(void)
{
}
module_exit(cleanup_function);
- 错误处理,如果在注册过程中失败,需要回滚所有的操作,当无法注销之前获取的东西时,内核就不稳定,包含了不存在的代码内部指针。处理错误常小心使用goto来简化代码,并在初始化过程中需要判断多次
int __init my_init_function(void)
{
int err; /* registration takes a pointer and a name */
err = register_this(ptr1, "skull");
if (err)
goto fail_this;
err = register_that(ptr2, "skull");
if (err)
goto fail_that;
err = register_those(ptr3, "skull");
if (err)
goto fail_those;
return 0; /* success */
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_this(ptr1, "skull");
fail_this:
return err; /* propagate the error */ }
- 模块加载很容易产生竞争,并且一旦你注册了模块,你的代码必须确保都可以被调用。在支持那个设备的所有初始化完成之前,不要注册任何设备。
- 加载模块时加入参数:insmod test1=10 test2="abc"
- 在模块中定义参数:module_param(参数名,类型,权限值(S_IRUGO));数组:module_param_array(参数名,类型,长度,权限值);
第三章 字符驱动
- scull(sample character utility for loading localities),是一个字符驱动,如同操作设备一样地操作一块内存区域。
- 设备惯例位于/dev目录,使用ls -l可以输出详细信息,字符设备第一列有"c"标识,而块设备为“b”
- dev_t类型用来持有设备编号,MAJOR(dev_t dev);获得主编号,MINOR(dev_t dev);获得次编号,MKDEV(int major, int minor);获得一个dev_t。
- 建 立一个字符驱动第一件事:获取一个或多个设备编号来使用,使用如下函数,其中first是分配的起始设备编号,其次编号常常是0,count是请求的连续 设备编号总数,如果count太大,可能溢出到下一个次编号,name是应该连接到这个编号方位的设备的名字,它会出现在/proc/devices和 sysfs中。分配成功返回0.
int register_chrdev_region(dev_t first, unsigned int count, char *name);
- 可以由内核动态分配主编号,可使用如下函数:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
- 不再使用设备编号时要释放它,常常是在模块的cleanup函数中释放
void unregister_chrdev_region(dev_t first, unsigned int count);
- 在加载来模块之后,可以使用mknod /dev/设备文件名 c 主设备号 次设备号来给设备创建一个设备文件;如果使用的是动态分配设备号,则需要先去知道设备号才能创建。
- 使用chmod 权限值 /dev/设备文件名。来给设备设置权限。
- 为了灵活设置主设备号,可以采用如下代码,当加载模块时有设置则使用静态设备号,没设值则动态分配一个。
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull:can't get major %d/n", scull_major);
return result;
}
- file_operation 是帮助连接字符设备操作和设备编号的结构,是一个函数指针的集合,定义在linux/fs.h中,这个函数操作大部分负责 实现系统调用。结构中到每个成员(函数)必须指向驱动中的函数,对于不支持到操作留置为NULL,但指定为NULL指针时内核的确切行为是每个函数不同 的。
- 针 对不同到驱动,采用不同的file_operation结构初始化方式,选择初始化不同到函数,例如scull,其中.owner不是一个操作,而是只想 拥有这个结构题的模块的指针,一般为THIS_MODULE,一个则<linux/module.h> 中定义的宏。
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
- struct file,文件结构,代表一个打开的文件(包括系统文件及设备文件等),由内核在open时创建,并对文件操作时需要传递它,在文件到所有实例都关闭后,内核释放这个结构。
- 一般给file取名为filp,表示文件指针。
- file中有file_operation *f_op这个成员,这表示与文件关联到操作,在文件open时需要设置,内核不保存这个值,你可以改变这个值来改变该文件关联的操作,从而实现实时替换文件操作,类似“方法重载”。
- inode,在内核内部表示文件,和代表打开文件描述符的文件结构是不同到,可能有代表单个文件的多个打开描述符,但都指向单个inode结构。
- inode中保存关于驱动的重要信息只有 dev_t i_rdev:保存设备编号,以及struct cdev *i_cdev:代表字符设备。
- 在运行时获得一个独立的cdev结构,可使用
struct cdev *my_cdev = cdev_alloc();
my_dev->ops = &my_ops;
- 将cdev结构嵌入到自己设备特定的结构,需要会初始化已经配分的结构
void cdev_init(struct cdev *cdev, struct file_operation *fops);
- 在cdev建立后,通知内核,但cdev_add可能失败,设备不能添加到系统中,但一旦cdev_add返回,设备就可以被内核调用,所有驱动完全准备好前不要调用cdev_add。
int cdev_add(struct dev *dev, dev_t num, unsigned int count);
- 除去一个字符设备,调用
void cdev_del(struct cdev *dev);
- open方法大部分驱动中应该做下面的工作:
- 检查设备特定的错误,例如设备没准备号,或者硬件错误
- 如果第一次打开,初始化设备
- 如果需要,更新f_op指针
- 分配并填充放进filp->private_data的任何数据结构
- open函数原型为int (*open)(struct inode *inode, struct file *filp);
- open函数的inode参数中有我们需要的信息i_cdev,来确定打开哪个设备,但是我们通常不想要cdev本身,而是想要包含cdev的scull_cdev结构。
- 内核实现了一个宏来做到此功能,其中,使用一个指向一个container_field类型的指针,它在包含它的container_type数据类型中,宏返回的就是container_type数据类型指针。
container_of(pointer, container_type, container_field);
- 例如如下代码利用此宏,根据inode结构的i_cdev指针,到
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
- release方法当是open的反面,任务有:
- 释放open分配在filp->private_data中的任何东西
- 在最后的close关闭设备
- release方法并不是每次文件关闭都会被调用,因为可能程序打开一个已打开文件的拷贝,而这时不调用open,只是增加一个引用计数,关闭也只是减少一个引用计数,只有最后一个关闭时才调用release。
- 内存分配和释放,flags可以使用GFP_KERNEL,而kmalloc分配的内存应该用kfree释放,kfree释放的内存应该是由kmalloc申请的,kmalloc分配不比整页分配大内存区有效,但简单:
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
- read和write方法的buff参数时用户空间的指针,所以不能被内核代码直接引用,原因时:
- 依赖于驱动运行的体系,以及内核是如何被配置的,用户空间指针当运行于内核模式可能根本无效的。
- 就算这指针在内核空间是同样的东西,但是用户空间是分页的,可能调用时这个内存没在RAM中,导致页面错,及进行系统调用的进程死亡。
- 要怀疑由用户程序提供的指针,可能时错误或恶意的。
- 使用系统的拷贝函数,这2个方法都在发生错误时返回一个负值,大于或等于0的返回值告知调用程序有多少字节已经传送,如果在发送一些数据后发生错误,返回值必须时成功发送的字节数,错误不报告直到函数下一次调用,驱动需要记住错误已经 发生:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);