driver device linux区别,读书笔记之《Linux Device Driver》-----(一)

Linux设备驱动

第一章 设备驱动简介

驱动程序的角色是提供机制,而不是策略。

编写内核代码来存取硬件,但是不能强加特别的策略给用户(只需要表现出硬件的最基本的功能,如何使用这些功能又用户自己选择)

对策略透明的驱动特征:支持同步和异步操作、可以多次打开、利用硬件全部能力、没有软件层提供策略相关操作。

内核角色划分为:进程管理、内存管理、文件系统、设备控制、网络。

字符设备是一种可以当做一个字节流来存取的设备,字符设备通过文件系统存取。

块设备通过位于/dev目录的文件系统结点来存取,一个块设备只能传送一个或多个长度经常是512字节(或更大的2的幂)的整块。但是Linux允许应用程序像字符设备一样读写一个块设备。

一个网络接口负责发送和接受数据报文,在内核网络子系统驱动下,不需要知道如何把一个事务映射到实际的被发送和接受的报文上,例如面向流的TCP,但是网络设备被设计成处理报文的发送和接收,网络驱动只处理报文。

文件系统其实是一个软件驱动。

安 全问题:驱动编写者应避免将安全策略编写到代码中,但是有例外:某些类型的设备存取可能反面地影响系统;驱动程序BUG,特别是内存访问控制上的;用户进 程接收的输入除非能核实它,否则不要信任它;从内核获取的内存应当清零或者在对用户进程或设备可用前初始化;设备解析发送给它的数据,就需要确保用户不能 发送任何危及系统的东西;

第二章 建立和运行模块

printk(KERN_ALERT “hello”);的优先级之后,不需要加逗号。

模块只连接到内核,唯一能调用的函数就是内核输出的那些,因此模块源文件不应当包含通常的头文件,和其他特殊情况例外。

内核编程错误至少会杀掉当前进程。

驱动经常做两个任务:驱动模块中一些函数作为系统调用的一部分,一些负责中断处理。

内核编程的堆栈远远小于用户空间编程,如果需要大的结构,应当在调用时间内动态分配内存。

双下划线开始的函数,是一个底层的接口组件,要小心使用。

内核代码不能做浮点算术(内核里不能使用浮点变量?)

一种方便的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,一个则 中定义的宏。

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);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值