实验八Linux模块和设备驱动程序.docx

上传人:b****6 文档编号:7841720 上传时间:2023-01-26 格式:DOCX 页数:13 大小:44.27KB
下载 相关 举报
实验八Linux模块和设备驱动程序.docx_第1页
第1页 / 共13页
实验八Linux模块和设备驱动程序.docx_第2页
第2页 / 共13页
实验八Linux模块和设备驱动程序.docx_第3页
第3页 / 共13页
实验八Linux模块和设备驱动程序.docx_第4页
第4页 / 共13页
实验八Linux模块和设备驱动程序.docx_第5页
第5页 / 共13页
点击查看更多>>
下载资源
资源描述

实验八Linux模块和设备驱动程序.docx

《实验八Linux模块和设备驱动程序.docx》由会员分享,可在线阅读,更多相关《实验八Linux模块和设备驱动程序.docx(13页珍藏版)》请在冰豆网上搜索。

实验八Linux模块和设备驱动程序.docx

实验八Linux模块和设备驱动程序

实验八Linux模块和设备驱动程序

一.实验目的

1.通过实验了解linux下文件驱动程序的框架;

2.通过驱动程序的编写,理解linux对设备管理的方式;

3.理解设备驱动程序中与内核交互部分。

二.实验指导

Linux是Unix操作系统的一种变种,在Linux下编写驱动程序的原理和思想完全类似于其他的Unix系统,但它和dos或window环境下的驱动程序有很大的区别。

1.Linux设备

在Linux中,用户进程不能直接对物理设备进行操作,必须通过系统调用向内核提出设备请求,由内核实现对物理设备的分配并完成进程请求的操作。

在内核中实现对设备进行操作的一组程序称为设备驱动程序。

系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口,为应用程序屏蔽了硬件的细节,在应用程序看来,硬件设备只是一个设备文件,应用程序可以像操作普通文件一样对硬件设备进行操作。

设备驱动程序是内核的一部分,它完成以下的功能:

1).对设备初始化和释放

2)把数据从内核传送到硬件和从硬件读取数据

3)读取应用程序传送给设备文件的数据和回送应用程序请求的数据

4)检测和处理设备出现的错误

在Linux系统把设备分为3类:

块设备、字符设备和网络设备。

每类设备都有独特的管理控制方式和不同的驱动程序。

字符设备:

以字符为单位进行输入输出的设备,并且以字符为单位对设备中的信息进行组织和处理。

包括:

显示器、键盘、打印机、绘图仪、串口等。

通常对字符设备传送的数据是顺序处理。

字符设备以访问文件的方式访问。

块设备:

以一定大小的数据块为单位进行输入输出,设备中的数据也以物理块为单位进行组织和管理。

块设备可以采取随机存取方法。

包括硬盘、软盘、光盘、RAM盘等。

通常作为外存使用,Linux文件系统建立在外存中,块设备通过文件系统访问。

为匹配CPU与块设备间的速度差异,通常使用缓冲区传送数据。

网络设备:

与网络通信线路连接的网络适配器。

Linux使用套接口以文件I/O方式提供对网络数据的访问。

本实验主要介绍字符设备驱动程序的编写方法。

2.有关设备操作的系统调用

在Linux系统中,应用程序操作设备就是访问设备对应的设备文件,与操作普通文件的访问方式基本相似。

设备访问的系统调用主要如下:

●打开设备系统调用open:

实现分配和打开设备的功能;

●读设备系统调用read:

从设备读取数据;

●写设备系统调用write:

向设备写数据;

●设备控制系统调用ioctl:

控制设备的工作模式和状态;

●关闭设备系统调用close:

释放设备,将设备归还系统。

3.Linux设备驱动程序的构成

Linux系统的每个系统调用都有一个相应的内核函数实现该系统调用的功能。

Linux设备驱动程序需要实现的内核函数由以下结构定义:

structfile_operations{

int(*lseek)(structinode*,structfile*,off_t,int);

int(*read)(structinode*,structfile*,char*,int);//读例程,实现读系统调用

int(*write)(structinode*,structfile*,constchar*,int);//写例程,实现读写系统调用

int(*readdir)(structinode*,structfile*,void*,filldir*,int);

int(*select)(structinode*,structfile*,int,select_table*);

int(*ioctl)(structinode*,structfile*,unsinedint,unsignedlong);//控制例程,实现设备控制

int(*mmap)(structinode*,structfile*,structvm_area_struct*);

int(*open)(structinode*,structfile*);//打开例程,打开设备

int(*release)(structinode*,structfile*);//释放例程,设备释放

int(*fsync)(structinode*,structfile*);

int(*fasync)(structinode*,structfile*,int);

int(*check_media_change)(kdev_tdev);

int(*revalidate)(dev_tdev);

}

可根据实际情况只实现必不可少的部分内核函数。

如果intinit_module(void)/

驱动程序可以按照两种方式编译。

一种是编译进kernel,另一种是编译成模块(modules)。

如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以使用模块方式。

采用模块方式还需要编写2个例程:

●init_module例程:

加载设备驱动程序模块时执行;

●cleanup_module例程:

卸载设备驱动程序时模块。

4.与设备有关的数据结构

Linux系统中与设备操作有关的数据结构如图8.1所示,包括如下几部分:

(1)设备文件

通常位于/dev目录下,如/dev/mydrv,是用户程序或用户命令操作设备的接口。

可以像操作普通文件一样来操作设备,一般先使用open系统调用打开设备,然后使用read、write、ioctl等系统调用控制设备和读写设备,最后使用close系统调用关闭设备。

设备文件在外存上并没有存放信息的数据块,只包含一个主设备号和一个次设备号。

主设备号用于指明在该设备对应设备注册表的哪一个设备。

如图8.1中设备文件mydrv的主设备号是1,对应设备注册表中的1号设备。

次设备号表示该设备是同类设备中的第几个设备。

 

图8.1Linux系统中与设备驱动有关的数据结构

 

(2)字符设备注册表chrdevs[]

Linux系统支持的所有字符设备都需要在字符设备注册表中注册,这是Linux内核中的一个数组,是设备在Linux系统是否存在的唯一标志。

每个数组元素代表一类设备,数组元素的下标就是主设备号。

设备注册表中保存了设备名和设备的file_operations结构的操作函数指针。

(3)file_operations

是Linux内核中的数据结构,保存了设备各操作内核函数的指针。

每类设备都有一个file_operations结构。

(4)设备驱动程序例程

对有关操作设备的每一个系统调用都在该设备的驱动程序中有一个对应的内核例程。

在安装设备驱动程序时要把设备驱动程序的各个例程加载到内核中。

因此,安装设备驱动程序的主要工作包括以下3个方面:

●将设备加载到Linux内核

●对设备进行注册

●创建设备文件

5.设备访问过程

根据图8.1,不难理解硬件设备的访问过程。

(1)打开设备:

当用户程序执行open()系统调用时,从设备文件中得到主设备号,访问字符设备注册表,得到设备的file_operations数据结构,其中含有该设备驱动程序各例程的指针。

(2)读设备:

当用户程序执行read()系统调用时,将调用该设备驱动程序的read()例程。

read()例程从设备读入数据到内核缓冲区,再从内核缓冲区传给用户程序的缓冲区。

(3)写设备:

当用户程序执行write()系统调用时,将调用该设备驱动程序的write()例程。

write()例程从用户缓冲区把数据复制到内核缓冲区,再从内核缓冲区把数据输出到硬件设备。

(4)设备控制:

当用户执行设备控制系统调用ioctl()时,ioctl()例程从用户态复制命令和数据到内核,再对设备进行操作。

6.设备驱动程序中可引用的函数和数据结构

设备驱动程序不在用户态运行,而在操作系统内核运行,不能访问系统的任何程序库,因此平常编写应用程序能够调用的所有库函数几乎都不能在设备驱动程序中调用。

如printf、scanf、atoi、open、fopen等函数都不可使用。

设备驱动程序不能包含系统include目录下的stdio.h、stdlib.h等头文件。

设备驱动程序只能调用Linux内核代码中实现的函数和例程。

声明这些函数和例程的头文件通常在include/linux和include/asm目录下。

设备驱动程序需要调用的函数和例程主要有以下几类:

(1)设备注册和卸载函数

#include

intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops);//设备注册

//注册设备,在设备注册表的major位置设置设备名和设备操作指针,

//参数:

//major:

希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。

//name:

设备文件名,

//fops:

:

用来登记驱动程序实际执行操作的函数的指针。

如果登记成功,返回设备的主设备

//号,不成功,返回一个负值

unregister_chrdev(intmajor,char*name);//设备卸载

//参数为设备的主设备号和设备名

(2)用户空间与内核空间之间复制数据函数

#include

unsignedlongcopy_from_user(void*to,constvoid*from,unsignedlongcount);

//将数据从用户态存储器from复制到核心态存储器to

unsignedlongcopy_to_user(void*to,constvoid*from,unsignedlongcount);

//将数据从核心态存储器from复制到用户态存储器to

(3)操作硬件设备函数

读写I/O口的函数

#include

unsignedinb(unsignedport);//从I/O端口port读入一个字节的数据并返回该数据

voidoutb(unsignedcharbyte,unsignedport);//向I/O端口port写一个字节的数据byte

unsignedinw(unsignedport);

voidoutw(unsignedshortword,unsignedport);

unsignedinl(unsignedport);

voidoutl(unsigneddoubleword,unsignedport);

unsignedinb_p(unsignedport);

在内存和I/O端口之间传递字符串.

voidinsb(unsignedport,void*addr,unsignedlongcount);

//从设备的I/O端口port读入count个字节的数据存入addr指定的内核存储器位置

voidoutsb(unsignedport,void*addr,unsignedlongcount);

//将addr指定的内核存储器位置的count个字节的数据从I/O端口port输出到设备

voidinsw(unsignedport,void*addr,unsignedlongcount);

voidoutsw(unsignedport,void*addr,unsignedlongcount);

voidinsl(unsignedport,void*addr,unsignedlongcount);

voidoutsl(unsignedport,void*addr,unsignedlongcount);

访问I/O存储器的函数

unsignedreadb(address);

//从I/O设备存储器地址address读入一个字节的数据,通常要求物理地址与逻辑地址一致

unsignedreadw(address);

unsignedreadl(address);

voidwriteb(unsignedvalue,address);

//将一个字节的数据value写入I/O设备存储器地址address

voidwritew(unsignedvalue,address);

voidwritel(unsignedvalue,address);

memset_io(address,value,count);

memcpy_fromio(dest,source,nbytes);

memcpy_toio(dest,source,nbytes);

(4)内核调试函数

#include

intprintk(intlevel,constchar*fmt,...);

该函数的语法与printf相似,fmt是格式串,后面是一些需要通过fmt显示的参数。

而level对输出信息的严重程度对其分级。

level参数的取值通常有:

KERN_EMERG:

紧急信息

KERN_CRIT:

关键条件,常常与硬件或软件故障有关

KERN_WARNING:

警告信息,本身不产生严重问题

KERN_INFO:

通常用于系统启动时打印找到的软件、硬件及配置信息

KERN_DEBUG:

用于调试信息

(5)内核变量和数据结构

Linux内核中定义的数据结构、变量都可以有设备驱动程序访问。

其中与进程管理有关的全局变量有:

●structtask_struct*current;当前运行的进程。

●intnr_tasks=1;系统中存在的进程数目。

进程管理相关的头文件是:

#include

任务结构体task_struct的主要成员有:

●intpid;:

进程号

●structtask_struct*next_run,*prev_run;可运行进程队列指针

●structtask_struct*p_pptr;父进程任务结构体指针

●structtask_struct*p_cptr;子进程任务结构体指针

●structtask_struct*p_osptr;兄进程任务结构体指针

●structtask_struct*p_ysptr;弟进程任务结构体指针

●exec_domain->name;进程启动命令名

7.设备驱动程序开发实例剖析

我们来写一个最简单的字符设备驱动程序,从用户进程传一些信息给内核,从内核传一些信息给用户态进程。

通过它可以了解Linux的设备驱动程序的工作原理。

该设备驱动程序是一个C语言程序,文件名为testdrv.c,作为模块加载,模块名为testdrv,注册的设备名为testdev,与用户程序接口的设备文件为/dev/testdev。

(1)编写设备驱动程序的代码

编写设备驱动程序的主要工作就是操作设备的各个内核函数,并填充file_operations的各个域。

首先保护开发设备驱动程序所需的头文件:

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#defineMAX_BUFFER_LEN1000

unsignedinttest_major=0;

chardata[MAX_BUFFER_LEN];

1)read()例程:

staticintread_test(structinode*inode,structfile*file,char*buf,intcount)

{

inti;

copy_to_user(buf,k_buffer,count);

//将数据从内核态缓冲区k_buffer传送到用户态缓冲区buf

returncount;//返回传送的字节数+

}

当执行read系统调用时,read_test()被调用,它把用户的缓冲区全部写1,buf是read系统调用的一个参数,它是用户进程空间的一个地址。

在test_read被调用时,系统进入核心态,将核心态数组缓冲区k_buffer的各个元素置1,并用核心态函数copy_to_user将数据从核心态存储器传送到用户态存储器,并返回实际传送的字节数。

2)write()例程

 staticintwrite_test(structinode*inode,structfile*file,constchar*buf,intcount)

{

inti;

copy_from_user(data,buf,count);

//将数据从用户态缓冲区buf传送到内核态缓冲区k_buffer

returncount;//返回传送的字节数

}

3)打开和关闭例程

staticintopen_test(structinode*inode,structfile*file)

{

MOD_INC_USE_COUNT;//模块计数增加1

return0;

}

staticvoidrelease_test(structinode*inode,structfile*file)

{

MOD_DEC_USE_COUNT;//模块计数减1

}

4)模块安装和初始化例程

如果要求设备驱动程序作为一个模块加载到系统内核中,则应该提供init_module函数,并在其中注册设备。

#defineMODULE//必须定义MODULE宏

#include

intinit_module(void)

{

//初始化file_operations结构

structfile_operationstest_fops={

NULL,

read_test,

write_test,

NULL,/*test_readdir*/

NULL,

NULL,/*test_ioctl*/

NULL,/*test_mmap*/

open_test,

NULL,

release_test,

NULL,/*test_fsync*/

NULL,/*test_fasync*/

NULL,

NULL,

NULL,

NULL,

NULL,/*nothingmore,fillwithNULLs*/

};

intresult;

result=register_chrdev(0,"testdrv",&test_fops);

//注册字符设备驱动程序,设备名testdev

//返回系统选择的主设备号

if(result<0){

printk(KERN_INFO"test:

can'tgetmajornumber");

returnresult;

}

if(test_major==0)test_major=result;/*dynamic*/

return0;

}

在用insmod命令将编译好的模块调入内存时,init_module函数被调用,调用register_chrdev向系统的字符设备表登记了一个字符设备。

5)模块卸载例程

同样,设备驱动程序作为模块加载时,还应提供cleanup_module函数,实现模块的卸载,并释放字符设备test在系统字符设备表中占有的表项。

在用rmmod卸载模块时,cleanup_module函数被调用。

cleanup_module函数的代码如下:

voidcleanup_module(void)//模块卸载,名字是规定的

{

unregister_chrdev(test_major,"testdrv");

}

一个极其简单的字符设备可以说写好了,文件名就叫testdrv.c,把以上全部代码放到一个文件testdrv.c中。

完整的程序为testdrv.c,请见附录8。

(2)编译设备驱动程序

#gcc-c-DMODULE-D__KERNEL__-otestdrv.otestdrv.c-I/usr/src/linux-2.4/include

各编译选项说明:

-c仅生成目标代码,不生成可执行程序,不需要连接语言库

-O2采用O2级优化

-DMODULE定义宏MODULE,表示设备驱动程序将作为一个模块加载到内核中

-D__KERNEL__定义一个宏__KERNEL__,表示设备驱动程序将作为内核代码运行

-otestdrv.o指明生成的目标代码文件是testdrv.o

testdrv.c设备驱动程序源代码

-I/usr/src/linux2.4/include从该目录下查找编译设备驱动程序需要的头文件

得到文件testdrv.o就是一个设备驱动程序的目标代码。

(3)安装设备驱动程序

#insmod./testdrv.o

执行该命令要求具有超级用户才能有权限,将设备驱动程序插入内核时,将执行设备驱动程序的init_module例程,完成设备驱动程序的注册。

(4)查看驱动程序主设备号

如果设备驱动程序安装成功,在/proc/modules文件中可看到该设备驱动程序模块名:

testdrv。

在/proc/devices文件中就可以看到设备testdev,并可以看到它的主设备号。

查看这两个文件的命令分别为:

#more/proc/modules

#more/proc/devices

(5)创建设备文件

根据上一步查到的主设备号,任选一个次设备号,创建设备文件。

假定查到的主设备号是253,则可用以下命令为其创建一个设备文件:

次设备号

#mknod/dev/testdrvc2531

为了让所有用户都可对其执行读写操作,设置该设备文件的访问权限:

#chmod666/dev/testdrv

(6)测试设备驱动程序

我们现在可以通过设备文件来访问我们的驱动程序。

写一个测试程序test.c。

test.c的参考程序请见附录8。

以下编译和运行test.c。

$gcctest.c–otest编译

$./test

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

当前位置:首页 > 经管营销 > 经济市场

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

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