Linux设备驱动程式.docx

上传人:b****5 文档编号:30747838 上传时间:2023-08-20 格式:DOCX 页数:16 大小:25.24KB
下载 相关 举报
Linux设备驱动程式.docx_第1页
第1页 / 共16页
Linux设备驱动程式.docx_第2页
第2页 / 共16页
Linux设备驱动程式.docx_第3页
第3页 / 共16页
Linux设备驱动程式.docx_第4页
第4页 / 共16页
Linux设备驱动程式.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

Linux设备驱动程式.docx

《Linux设备驱动程式.docx》由会员分享,可在线阅读,更多相关《Linux设备驱动程式.docx(16页珍藏版)》请在冰豆网上搜索。

Linux设备驱动程式.docx

Linux设备驱动程式

Linux设备驱动程式之读书笔记

第三章字符设备驱动程式

○、说明

笔记适用于Linux的2.6.10以后的内核。

笔记以LinuxDeviceDriver3提供的scull程式(scull目录中的main.c和scull.h)为记录主线,并以该驱动程式中的各种系统调用和函数调用流程为记录顺序。

比如,module_init()和module_exit()为相对应的一对系统调用,一般书籍中都会放在一起讨论,不过本笔记却不会这样,而是在需要调用的时候才会涉及,因此module_init()会放在笔记开始时,也就是刚加载module时讨论,而module_exit()则会放在笔记结束前,也就是要卸载module时再加以讨论。

该笔记的的目的是为了对LinuxDeviceDrvier3中提到的各个知识点作一下整理,理清一下头绪,从而能让我对Linux驱动程式加深整体或全局上的理解。

注:

个人理解,有误难免!

*******************************************

驱动程式module的工作流程主要分为四个部分:

1、用Linux提供的命令加载驱动module

2、驱动module的初始化(初始化结束后即进入“潜伏”状态,直到有系统调用)

3、当操作设备时,即有系统调用时,调用驱动module提供的各个服务函数

4、卸载驱动module

一、驱动程式的加载

Linux驱动程式分为两种形式:

一种是直接编译进内核,另一种是编译成module形式,然后在需要该驱动module时手动加载。

对于前者,更有待学习。

Module形式的驱动,Linux提供了两个命令用来加载:

modprobe和insmod。

其中modprobe能解决驱动module的依赖性,即如果正加载的驱动module若引用了其他module提供的内核符号或其他资源,则modprobe就会自动加载那些module,不过,使用modprobe时,必须把要加载的驱动module放在当前模块搜索路径中。

而insmod命令不会考虑驱动module的依赖性,不过他却能加载任意目录下的驱动module。

一般来说,在驱动研发阶段,使用/sbin/insmod比较方便,因为不用将module放入当前module搜索路径中。

一旦使用insmod加载模块,则Linux内核就会调用module_init(scull_init_module)特别宏,其中scull_init_module是驱动初始化函数,可自定义名称。

在用insmod加载module时,还能给module提供模块参数,不过这需要在驱动原始码中加入几条语句,让模块参数对insmod和驱动程式可见,如:

staticchar*whom=”world”;

staticinthowmany=10;

module_param(howmany,int,S_IRUGO);

module_param(whom,charp,S_IRUGO);

这样,当使用/sbin/insmodscull.kowhom=”string”howmany=20这样的命令加载驱动时,whom和howmay的值就会传入scull驱动模块了。

驱动程式module被加载后,若对设备进行操作(如open,read,write等),驱动module就会调用相应的函数响应该操作。

那么,当对设备进行操作时,驱动module又怎么知道是自己应该有所响应,而不是其他的驱动module呢,也就是说,Linux内核怎么知道应该调用哪一个驱动module呢?

目前我只知道有两种方式将设备和驱动module联系在一起(也许应该说提供访问设备的一种途径比较恰当):

其一是通过某些设备的ID(比如PCI设备和USB设备的DeviceID和ProductID),Linux内核根据这些ID调用驱动module;其二是在/dev目录下根据设备的主次设备号创建对应的设备节点(即设备文件),这样当操作/dev目录下的设备文件时,就会调用相应的驱动module。

二、驱动module的初始化

使用insmod加载驱动module时,需要让驱动module为设备做一些初始化动作,主要目的是让Linux内核知道这个设备(或说module?

),及在以后对该设备进行操作(如open,read,write等等)时,让Linux内核知道,本module拥有哪些函数能服务于系统调用。

因此,scull_init_module函数中主要做了以下几件事情:

a)分配并注册主设备号和次设备号

b)初始化代表设备的struct结构体:

scull_dev

c)初始化互斥体init_MUTEX(本笔记不整理)

d)初始化在内核中代表设备的cdev结构体,最主要是将该设备和file_operations结构体联系起来。

1、分配并注册主次设备号

设备号是在驱动module中分配并注册的,也就是说,驱动module拥有这个设备号(我的理解),而/dev目录下的设备文件却是根据这个设备号创建的,因此,当访问/dev目录下的设备文件时,驱动module就知道,自己该出场服务了(当然是由内核通知)。

在Linux内核看来,主设备号标识设备对应的驱动程式,告诉Linux内核使用哪一个驱动程式为该设备(也就是/dev下的设备文件)服务;而次设备号则用来标识具体且唯一的某个设备。

在内核中,用dev_t类型(其实就是个32位的无符号整数)的变量来保存设备的主次设备号,其中高12位表示主设备号,弟20位表示次设备号。

设备获得主次设备号有两种方式:

一种是手动给定一个32位数,并将他和设备联系起来(即用某个函数注册);另一种是调用系统函数给设备动态分配一个主次设备号。

对于手动给定一个主次设备号,使用以下函数:

intregister_chrdev_region(dev_tfirst,unsignedintcount,char*name)

其中first是我们手动给定的设备号,count是所请求的连续设备号的个数,而name是和该设备号范围关联的设备名称,他将出目前/proc/devices和sysfs中。

比如,若first为0x3FFFF0,count为0x5,那么该函数就会为5个设备注册设备号,分别是0x3FFFF0,0x3FFFF1,0x3FFFF2,0x3FFFF3,0x3FFFF4,其中0x3(高12位)为这5个设备所共有的主设备号(也就是说这5个设备都使用同一个驱动程式)。

而0xFFFF0,0xFFFF1,0xFFFF2,0xFFFF3,0xFFFF4就分别是这5个设备的次设备号了。

需要注意的是,若count的值太大了,那么所请求的设备号范围可能会和下一个主设备号重叠。

比如若first还是为0x3FFFF0,而count为0x11,那么first+count=0x400001,也就是说为最后两个设备分配的主设备号已不是0x3,而是0x4了!

用这种方法注册设备号有一个缺点,那就是若该驱动module被其他人广泛使用,那么无法确保注册的设备号是其他人的Linux系统中未分配使用的设备号。

对于动态分配设备号,使用以下函数:

intalloc_chrdev_region(dev_t*dev,unsignedintfirstminor,unsignedintcount,char*name)

该函数需要传递给他指定的第一个次设备号firstminor(一般为0)和要分配的设备数count,及设备名,调用该函数后自动分配得到的设备号保存在dev中。

动态分配设备号能避免手动指定设备号时带来的缺点,不过他却也有自己的缺点,那就是无法预先在/dev下创建设备节点,因为动态分配设备号不能确保在每次加载驱动module时始终一致(其实若在两次加载同一个驱动module之间并没有加载其他的module,那么自动分配的设备号还是一致的,因为内核分配设备号并不是随机的,不过书上说某些内核研发人员预示不久的将来会用随机方式进行处理),不过,这个缺点能避免,因为在加载驱动module后,我们能读取/proc/devices文件以获得Linux内核分配给该设备的主设备号。

LinuxDeviceDriver3提供了一个脚本scull_load和scull_unload,能在动态分配的情况下为设备创建和删除设备节点。

其实他也是利用了awk工具从/proc/devices中获取了信息,然后才用mknod在/dev下创建设备节点。

其实scull_load和scull_unload脚本同样能适用于其他驱动程式,只要重新定义变量并调整mknod那几行语句就能了。

和主次设备号相关的3个宏:

MAJOR(dev_tdev):

根据设备号dev获得主设备号;

MINOR(dev_tdev):

根据设备号dev获得次设备号;

MKDEV(intmajor,intminor):

根据主设备号major和次设备号minor构建设备号。

2、初始化代表设备的scull_dev结构体

scull原始码中定义了一个scull_dev结构体,包括qset,qutuam,信号量sem及cdev等字段。

其中qset和qutuam的初始化对于Linux驱动程式的知识点来说毫不相关,因此不加讨论。

我只要知道,在加载module时所调用的module初始化函数中,能初始化一些设备相关的变量。

不过根据LinuxDeviceDrvier3作者的意思,设备相关的变量或一些资源最佳应当在open函数中初始化,比如像中断号等,虽然在module初始化函数中注册也是允许的,但最佳是在第一次打开设备,也就是open函数中再行分配。

3、初始化互斥体init_MUTEX

互斥体MUTEX,也就是信号量的一个变种,和completion,自旋锁spinlock等等都和驱动中的并发和竞态相关,以后再说。

4、初始化在内核中代表设备的cdev结构体

其实在Linux内核中,cdev结构体才是真正代表了某个设备。

在内核调用设备的open,read等操作之前,必须先分配并注册一个或多个cdev结构。

我想能这么理解,主次设备号是涉外的,主要用来在和外部(指的是驱动module和Linux内核以外)交互时确定身份;而cdev结构体则是涉内的,当需要在module内部,或和Linux内核之间传递一些变量,指针,buffer等东东,或要调用驱动module中的某个服务函数时,就要用到cdev结构体了。

在scull函数中,cdev结构体的分配,注册和初始化使用了一个自定义的scull_setup_cdev函数,在该函数中,主要由以下4条语句对cdev进行初始化:

cdev_init(&dev->cdev,&scull_fops);

dev->cdev.owner=THIS_MODULE;

dev->cdev.ops=&scull_fops;

err=cdev_add(&dev->cdev,devno,1);

(dev变量是scull程式定义的代表设备的一个结构体,他包含了cdev结构体,对于dev来说,cdev结构体应该说就是他的核心)

第一条语句是初始化cdev结构体,比如为cdev结构体分配内存,为cdev结构体指定file_operations等等,而第三条语句的作用初看起来似乎和第一条有所重复。

但scull程式中既然这么写想必就有他的用意,也许需要看Linux内核原始码才能搞明白,但目前我是这么理解的:

第一条语句中有关file_operations的部分是为了告诉Linux内核,该cdev结构体相关的file_operations是scull_fops;而第二条语句则是真正为cdev指定了他的file_operations字段就是scull_fops。

scull_fops是file_operations类型的变量,file_operations也是个结构体,而且是Linux驱动程式中非常重要的一个结构体,在scull程式中,定义如下:

structfile_operationsscull_fops={

.owner=THIS_MODULE,

.llseek=scull_llseek,

.read=scull_read,

.write=scull_write,

.ioctl=scull_ioctl,

.open=scull_open,

.release=scull_release,

};

以上定义中,第一条.owner字段说明本file_operations结构体的拥有者是本驱动module,而接下来的几个字段则是告诉驱动module,当有相应的系统调用到达该module时,module应该调用哪一个函数来为该系统调用服务。

比如说,若有一个open系统调用到达module,则module通过查询file_operations结构体就知道了,和open系统调用相关的是scull_open函数,于是module就调用scull_open函数来为open系统调用服务了。

其他几个字段也完全类似。

当然,Linux内核定义的file_operations结构体还包括其他一些字段,比如异步读写等等,但还是等用到的时候再说吧。

对cdev初始化的第二条语句是dev->cdev.owner=THIS_MODULE,这条语句就是说正在初始化的cdev结构体的拥有者就是本module

对cdev初始化的最后一条语句是err=cdev_add(&dev->cdev,devno,1),该语句的目的就是告诉内核,该cdev结构体信息。

因为cdev_add函数有可能调用失败,所以需要检测该函数调用的返回值。

而一旦cdev_add调用成功返回,那么我们的设备就“活”了!

也就是说,外部应用程式对他的操作就会被内核允许且调用。

因此在驱动程式还没有完全准备好处理设备上的操作时,就绝不能调用cdev_add。

三、设备操作

驱动module因为由insmod的加载而进行了初始化之后,就会进入“潜伏”状态,也就是说,如果没有系统调用(如open,read等),那么module中定义的其他函数就绝不会运行!

这里所说的设备操作,是指当有系统调用到达驱动module时,module就该调用某个或某些函数有所动作。

对于驱动研发来说,我主要关心的只有一点,那就是系统调用怎样把一些外部应用程式中的变量值传递给驱动module。

scull程式中和设备操作相关的函数主要分为三类:

初始化函数,实际的操作服务函数和清理函数。

其中初始化函数只有一个,就是open函数,而操作服务函数则包括read,write,llseek等等函数,至于清理函数则是release函数。

1、open函数

open函数提供给驱动程式以初始化的能力,从而为以后的操作做准备。

说起来在用insmod加载驱动后也有一个初始化动作,但那个初始化是相对于整个Linux内核,或说是针对整个module在涉外时的全局意义上的初始化;而open函数的初始化则是相对于设备操作来说的,是属于驱动内部的初始化,比如为以后read操作时用到的某个变量(如file结构体)作一下初始化,再比如初始化一下设备,清空一下buffer等等。

在大部分的驱动程式中,open应该完成如下工作:

a、确定要打开的具体设备

b、检查设备特定的错误(诸如设备未就绪或类似的硬件问题)

c、如果设备是首次打开,则对其作一下初始化

d、如有必要,更新f_op指针

e、分配并填写置于filp->private_data里的数据结构

open函数的原型如下(指的是在file_operations结构体中的定义):

int(*open)(structinode*inode,structfile*filp)

在驱动研发时要做的,就是为该函数作具体实现,当然对open函数的名称能自定义,只要在填写file_operations结构体中的open字段时,将自定义的open函数名称填上就能了。

在scull程式中,用的就是scull_open函数名。

在open函数原型中,有inode和filp两个参数,都是外部应用程式在操作设备时通过调用系统调用传递给驱动module的。

于是驱动module就能通过这两个参数来确定要打开的具体设备了。

其实这里所说的具体设备,并不是说驱动module需要从系统安装的所有设备中确定他所要服务的设备,而是指module需要从某一类拥有相同主设备号的设备中确定他要服务的设备。

之所以这么说,是因为驱动module是对应于某一个主设备号的所有设备的。

换一句话说,就是Linux内核只管设备的主设备号,而不理会设备的次设备号是什么,如果有两个,三个或更多个设备拥有同一个主设备号,那么不管外部应用程式要操作这些设备中的哪一个,Linux内核都只会调用同一个驱动module。

不过驱动module却不能不管次设备号了,因为他是跟某一个具体的设备打交道的,所以他需要根据open系统调用时传递给他的参数中找到次设备号,从而确定那唯一的一个设备(也许驱动module也能同时操作几个设备,但一时也想不起来)。

不过上面所说的通过次设备号找到具体的设备,只是其中一种方法;另外更有一种方法就是通过cdev结构体确定某个具体设备。

设备所拥有的cdev结构体,或次设备号,都保存在open函数的inode参数中。

我们能使用container_of宏通过inode所拥有的cdev确定具体设备,也能使用iminor宏从inode所拥有的i_rdev确定次设备号(i_rdev是inode结构体中的一个dev_t类型的变量,其中保存了真正的设备主次编号)。

对于open函数中的file参数,scull程式主要用他来做两件事:

其一是将根据cdev获得的代表设备的scull_dev结构体保存到file->private_data中,这样就能方便今后对该设备结构体的访问了,而不用每次都调用container_of宏或iminor宏来找到设备结构体了;其二是根据file结构体中的f_flags字段来确定,这次的open调用,是以写方式打开设备,还是以读方式来打开设备。

2、read函数

驱动module的file_operations结构体中能定义非常多设备操作服务函数,不过我目前关心的是这些函数怎样和系统调用,或说是外部应用程式交互,而不管具体的设备操作怎么实现,所以只记录read函数作为代表。

read函数的原型如下:

ssize_tread(structfile*filp,char__user*buf,size_tcount,loff_t*f_pos)

read函数原型中有4个参数,分别是filp,buf,count和f_pos。

其中file结构体指针参数能用来确定我们要操作的设备,因为在open函数中,我们将代表设备的结构体保存到了filp的private_data字段中。

buf参数是个指向用户空间的buffer的指针(buf前面的__user表示用户空间),对于read来说,就是能把要传送给外部应用程式的数据放入这个buffer中。

当然,我们不能简单地将数据copy到这个buffer中,而是要使用Linux内核提供的一些函数,比如copy_to_user函数。

count是请求传送的数据长度。

f_pos是个指向“longoffsettype”对象的指针,指明外部应用程式在文件中进行存取操作的位置。

read函数的返回值是有符号整数类型的指,一般是read操作的实际存取数。

3、release函数

release函数的作用正好和open相反,有时候release函数的实现被称为device_close,而不是device_release。

但无论那种形式,这个设备方法都应该完成如下任务:

a、释放open分配的,保存在file->private_data中的所有内容。

b、在最后一次关闭操作时关闭设备。

relese函数由close系统调用引起,但并不是每一次close系统调用都会引起release函数的调用。

只有那些真正释放设备数据结构的close系统调用才会引起release函数的调用。

因为Linux内核为每个file结构体维护其被引用多少次的计数器,只有当file结构体的计数器归0时,close系统调用才会引用release函数,这只在删除这个结构时才会发生,因此每次open驱动程式都只会看到一次对应的一次release调用。

四、卸载驱动module

每个重要的模块都需要一个清除函数,该函数在模块被移除前注销接口并向系统中返回所有资源。

如果一个模块未定义清除函数,则内核不允许卸载该模块。

Linux驱动module的卸载能用/sbin/rmmodscull.ko命令,这时Linux内核就会调用驱动程式中用module_exit(scull_cleanup_module)特别宏定义的清除函数,也就是说,module_exit声明用来帮助Linux内核找到模块的清除函数,在scull程式中,清除函数就是scull_cleanup_module函数。

模块的清除函数需要撤销初始化函数注册的所有资源,并且习惯上(但不是必须的)以和初始化函数注册相反的顺序进行撤销。

需要注意的是,这里指的初始化函数是指用module_init宏声明的初始化函数,而不是指open函数,和open函数对应的应当是release函数。

在scull程式中,清除函数主要做了两件事:

一是free了所有为scull设备分配的内存;二是收回了初始化函数所注册的设备号。

 

第二章调试技术中的printk

在Linux中系统信息的显示有以下3种情况:

1、如果系统中只运行klogd,那么能通过klogd-c重新启动klogd并设置console_loglevel,然后小于console_loglevel的所有信息都会打印到控制台;

2、如果系统中只运行syslogd,那么消息不会传递到用户空间,只能通过读/proc/kmsg来察看消息;

3、如果系统中同时允许klogd和syslogd,那么在根据console_loglevel将消息筛选打印到控制台的前提下,所有消息都会被追加到文件/var/log/messages中。

在Linux下写驱动程式的过程中肯定要用到printk语句输出信息,printk和普通printf语句的差别之一就是,printk根据附加的不同的日志级别(loglevel),或说是消息级别,来表示的严重程度对消息进行分类。

比如:

printk(KERN_DEBUG“Hi,Myprintk!

”);

上述语句所要输出的信息就是调试级别的。

需要注意的是KERN_DEBUG和后面引号中的内容之间不能用逗号隔开,否则编译时会报错!

Linux在头文件中定义了8种可用的日志级别字符串,根据严重程度排序如下:

KERN_EMERGKERN_ALERTKERN_CRITKERN_ERR

KERN_WARNINGKERN_NOTICEKERN_INFOKERN_DEBUG

未指定日志级别的printk语句采用默认级别,也就是在kernel/printk.c中被指定的宏DEFAULT_MESSAGE_LOGLEVEL,这个宏通常就是KERN_WARNING。

另外,控制台(一般是指文本终端,也有可能是串口)也有一个表示级别的整数变量,叫做console_loglevel,他在系统启动后的初始值是DEFAULT_

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工作范文 > 其它

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1