《Linux内核分析》实验指导书10_第1页
《Linux内核分析》实验指导书10_第2页
《Linux内核分析》实验指导书10_第3页
《Linux内核分析》实验指导书10_第4页
《Linux内核分析》实验指导书10_第5页
已阅读5页,还剩14页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

《Linux内核分析》实验指导书10《Linux内核分析》课程试验指导书

试验一、进程管理试验

1、加深对进程概念的理解,明确进程和程序的区分

2、进一步熟悉并发执行的实质

3、分析进程争用资源的现象,学习理解进程互斥的方法

4、了解linux系统中进程通信的基本原理

编写一段程序,实现软中断通信。

使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕获键盘上发出的中断信号(即按Del键),当父进程接受到这两个软中断的其中某一个后,父进程用系统调用kill()向两个子进程分别发送整数值为16和17软中断信号,子进程获得对应软中断信号,然后分别输出下列信息后终止:

Childprocess1iskilledbyparent!!Childprocess2iskilledbyparent!!

父进程调用wait()函数等待两个子进程终止后,输出以下信息,结束进程执行:Parentprocessiskilled!!

多运行几次编写的程序,简略分析消失不同结果的缘由。

(1)算法流程图(图1-1)

图1-1软中断通信程序流程图(2)参考程序源代码

#include

#include

#include

#include

intwait_flag;

voidstop();

main()

{

intpid1,pid2;//定义两个进程号变量

signal(3,stop);//或者signal(14,stop);

while((pid1=fork())==-1);//若创建子进程1不胜利,则空循环。if(pid1>0){//子进程创建胜利,pid1为进程号

while((pid2=fork())==-1);//创建子进程2

if(pid2>0){

wait_flag=1;

sleep(5);//父进程等待5秒

kill(pid1,16);//杀死进程1

kill(pid2,17);//杀死进程2

wait(0);//等待子进程1结束的信号

wait(0);//等待子进程2结束的信号

printf(“\nParentprocessiskilled!!\n”);

exit(0);//父进程结束

}

else{

wait_flag=1;

signal(17,stop);//等待进程2被杀死的中断号17printf(“\nChildprocess2iskilledbyparent!!\n”);

exit(0);

}

}

else{

wait_flag=1;

signal(16,stop);//等待进程1被杀死的中断号16printf(“\nChildprocess1iskilledbyparent!!\n”);

exit(0);

}

}

voidstop(){

wait_flag=0;

}

(3)程序运行结果

编译运行后,等待从键盘输入“Del”,有如下结果:Childprocess1iskilledbyparent!!Childprocess2iskilledbyparent!!Parentprocessiskilled!!

或者:(运行多次后会消失如下结果)

Childprocess2iskilledbyparent!!

Childprocess1iskilledbyparent!!

Parentprocessiskilled!!

试验二、模块编程试验

通过学习内核模块的编写和运行,了解模块是LinuxOS的一种特有的机制,可依据用户的实际需要在不需要对内核进行重新编译的状况下,模块能在内核中被动态地加载和卸载。编写一个模块,将它作为LinuxOS内核空间的扩展来执行,并通过insmod命令来手工加载,通过命令rmmod来手工卸载。

Linux模块是一些可以作为独立程序来编译的函数和数据类型的集合。在装载这些模块时,将它的代码链接到内核中。Linux模块有两种装载方式:静态装载(内核启动时装载)和动态装载(在内核运行过程中装载)。若在模块装载之前就调用了动态模块的一个函数,则此调用将失败;若模块已被装载,则内核就可以使用系统调用,并将其传递到模块中的相应函数。模块通常用来实现设备驱动程序(这要求模块的API和设备驱动程序的API相全都)。模块可用来实现所期望的任何功能。

一、模块的组织结构

模块一旦被装载进系统,就在内核地址空间中管态下执行。它就像任何标准的内核代码一样成为内核的一部分,并拥有其它内核代码相同的权限和职责(当然也会引起系统的崩溃)。若模块知道内核数据结构的地址,则它可以读写内核数据结构。但Linux是一个整体式的内核(monolithickernel)结构,整个内核是一个单独的且非

常大的程序,从而存在一个普遍的问题:在一个文件中实现的函数可能需要在其它文

件中定义的数据。在传统的程序中,这个问题是通过链接编辑器在生成可执行对象文

件时,使用链接编辑器可以解析的外部(全局)变量来解决的。又由于模块的设计和

实现与内核无关,所以模块不能靠静态链接通过变量名引用内核数据结构。恰好相反,

Linux内核采纳了另外一种机制:实现数据结构的文件可以导出结构的符号名(可以

从文件/proc/ksyms或文件/…/kernel/ksyms.c中以文本方式读取这个公开符号表),

这样在运行时就可以使用这个结构了。不过在编写模块的过程中,编写(修改)导出

变量时要非常留意,由于通过修转变量会修改内核的状态,其结果可能并不是内核设

计者所期望的。在确信自己了解修改内核变量的后果之前,应当对这些变量只进行读

操作。

模块作为一种抽象数据类型,它具有一个可以通过静态内核中断的接口。最小的

模块结构必需包括两个函数,它们在系统装载模块和卸载模块时调用,分别是

init_module()和cleanup_module()。可以编写一个只包括这两个函数的模块,这样

该模块中唯一会被调用的函数就是模块被装载时所调用的函数init_module()和模块

被卸载时所调用的函数cleanup_module()。并且用函数init_module()来启动模块装

载期间的操作,用函数cleanup_module()来停止这些操作。

由于模块可以实现相当简单的功能,故可以在模块中加入许多新函数以实现所要

期望的功能。不过加入模块的每个新函数都必需在该模块装载到内核中时进行注册。

若该模块是静态装载的,则该模块的全部函数都是在内核启动时进行注册的;若该模

块是动态装载的,则这些新函数必需在装载这个模块时动态注册。当然,假如该模块

被动态卸载了,则该模块的函数都必需从系统中注销。通过这种方式,当这个模块不

在系统中时,就不能调用该模块的函数。其中注册工作通常是在函数init_module()

中完成的,而注销工作通常是在函数cleanup_module()中完成的。

由上述定义的模块应有如下的格式:

#include#include……//其它header信息

intinit_module(){……//装载时,初始化模块的编码

}……

……//期望该模块所能实现的一些功能函数,如open()、release()、

write()、

//read()、ioctl()等函数

……

voidcleanup_module()

{……//卸载时,注销模块的编码

二.模块的编译

一旦设计并编写好模块,必需将其编译成一个适合内核装载的对象文件。由于编

写模块是用C语言来完成的,故采纳gcc编译器来进行编译。若需要通知编译程序把

这个模块作为内核代码而不是一般的用户代码来编译,则就需向gcc编译器传递参数

“-D__KERNEL__”;若需要通知编译程序这个文件是一个模块而不是一个一般文件,

则就需向gcc编译器传递参数“-DMODULE”;若需要对模块程序进行优化编译、连接,

则就需使用“-O2”参数;若还需要对装载后的模块进行调试,则就需使用“-g”参

数;同时需要使用“-Wall”参数来向装载程序传递all,使用“-c”开关通知编译程

序在编译完这个模块文件后不调用链接程序。

一般编译模块文件的命令格式如下:

#gcc-O2-g-Wall-DMODULE-D__KERNEL__-cfilename.c//filename.c为自己编写的模块程序源代码文件

执行命令后就会得到文件filename.o,该文件就是一个可装载的目标代码文件。

三.模块的装载

内核模块的装载方式有两种。一种是使用insmod命令手工装载模块;另一种是

恳求装载demandloading(在需要时装载模块),即当有必要装载某个模块时,若用

户安装了核心中不存在的文件系统时,核心将恳求内核守护进程kerneld预备装载适

当的模块。该内核守护进程是一个带有超级用户权限的一般用户进程。此试验中我们

主要采纳insmod命令手工装载模块。

系统启动时,kerneld开头执行,并为内核打开一个IPC通道,内核通过向kerneld

发送消息恳求执行各种任务。kerneld的主要功能是装载和卸载内核模块,kerneld

自身并不执行这些任务,它通过某些程序(如insmod)来完成。Kerneld只是内核的

代理,只为内核进行调度。

insmod程序必需找到恳求装载的内核模块(该恳求装载的模块一般被保存在

/lib/modules/kernel-version中)。这些模块与系统中其它程序一样是已连接的目标

文件,但不同的是它们被连接成可重定位映象(即映象没有被连接到在特定的地址上

运行,其文件格式是a.out或ELF)。亦就是说,模块在用户空间(使用适当的标志)

进行编译,结果产生一个可执行格式的文件。在用insmod命令装载一个模块时,将

会发生如下大事:

(1)新模块(通过内核函数create_module())加入到内核地址空间。

(2)insmod执行一个特权级系统调用get_kernel_syms()函数以找到内核的输

出符号(一个符号表示为符号名和符号值,如地址值)。

(3)create_module()为这个模块安排内存空间,并将新模块添加在内核模块链

表的尾部,然后将新模块标记为UNINITIALIZED(模块未初始化)。

(4)通过init_module()系统调用装载模块。(该模块定义的符号在此时被导出,供其它可能后来装载的模块使用)

(5)insmod为新装载的模块调用init_module()函数,然后将新模块标志为RUNNING(模块正在运行)。

在执行完insmod命令后,就可在/proc/modules文件中看到装载的新模块了。(为证明其正确性,可在执行insmod命令之前先查看/proc/modules文件,执行之后再查看比较)

四.模块的卸载

当一个模块不需要使用时,可以使用rmmod命令卸载该模块。由于无需连接,故它的任务比加载模块要简洁得多。但假如恳求装载模块在其使用计数为0时,kerneld将自动从系统中卸载该模块。卸载时调用模块的cleanup_module()释放安排给该模块的内核资源,并将其标志为DELETED(模块被卸载);同时断开内核模块链表中的连接,修改它所依靠的其它模块的引用,重新安排模块所占的内核内存。

五.模块连接到内核的示意图

该图比较明显地展现了模块连接到内核所使用的命令和函数,以及各个函数之间的调用关系。通过该图,可以比较清楚地看出模块连接到内核的整个连接过程,这也有助于内核模块的编写。

六.模块程序中管理模块的几个文件操作

在内核内部用一个file结构来识别模块,而且内核使用file_operatuions结构

来访问模块程序中的函数。file_operatuions结构是一个定义在中的函

数指针表。管理模块的文件操作,通常也称为“方法”,它们都为struct

file_operations供应函数指针。在structfile_operations中的操作一般按如下顺

序消失,除非说明,它们返回0值时表示访问胜利;发生错误时返回一个负的错误值

(目前共有13个操作):

int(*lseek)()、int(*read)()、int(*write)()、int(*readdir)()、int

(*select)()、int(*ioctl)()、int(*mmap)()、int(*open)()、void(*release)()、

int(*fsync)()、int(*fasync)()、int(*check_media_change)()、int

(*revalidate)()下面我们只简洁介绍其中的几个操作,其它在以后涉准时再介绍:

1、方法int(*read)(structinode*,structfile*,char*,int)

该方法用来从模块中读取数据。当其为NULL指针时将引起read系统调用返回

-EINVAL(“非法参数”)。函数返回一个非负值表示胜利地读取了多少字节。

2、方法int(*write)(structinode*,structfile*,constchar*,int)

该方法用来向模块发送数据。当其为NULL指针时将引起write系统调用返回

-EINVAL。假如函数返回一个非负值,则表示胜利地写入了多少字节。

3、方法int(*open)(structinode*,structfile*)

该方法是用来打开模块的操作,它是操作在模块节点上的第一个操作,即使这样,

该方法还是可以为NULL指针。假如为NULL指针,则表示该模块的打开操作永久胜利,

但系统不会通知你的模块程序。

4、方法void(*release)(structinode*,structfile*)

该方法是用来关闭模块的操作。当节点被关闭时就调用这个操作。与open类似,

release也可以为NULL指针。

当在你的模块中需要上面这些方法时,相应的方法若没有,则在struct

file_operations中相应的地方将其令为NULL指针。这样我们需要的也许象下面这样:structfile_operationsmodulename_fops={NULL,//modulename_lseekmodulename_read,modulename_write,NULL,//modulename_readdirNULL,//modulename_select

NULL,//modulename_ioctlNULL,//modulename_mmapmodulename_open,modulename_release,NULL,//modulename_fsyncNULL,//modulename_fasyncNULL,//modulename_check_media_changeNULL//modulename_revalidate}

1、编写一个简洁的内核模块,该模块至少需要有两个函数:一个是init_module()

函数,在把模块装载到内核时被调用,它为内核的某些东西注册一个处理程序,或是

用自身的代码取代某个内核函数;另一个是cleanup_module()函数,在卸载模块时被

调用,其任务是清除init_module()函数所做的一切操作。编写完成后进行该模块的

编译、装载和卸载操作。

2、向上面模块中再添加一些新函数,如open()、release()、write()和read()

函数,并编写一个函数来测试你的模块能否实现自己添加的函数的功能。其中open()、

release()和write()函数都可以是空操作或较少的操作,它们仅仅为结构

file_operations供应函数指针。

一、一个简洁的内核模块

1、必要的header文件:

除了前面讲到的头文件#include和#include

外,假如你的内核打开了版本检查,那么我们就还必需增加头文件

#include,否则就会出错。

2、init_module()函数:

由于题目的要求不高,故可只在该函数里完成一个打印功能,如printk(“Hello!

Thisisatestingmodule!\n”);等。为便于检查模块是否装载胜利,我们可以给

一个返回值,如return0;若返回一个非0值,则表示init_module()失败,从而不

能装载模块。

3、cleanup_module()函数:

只需用一条打印语句来取消init_module()函数所做的打印功能操作就可以了,

如printk(“Sorry!Thetestingmoduleisunloadednow!\n”);等。

4、模块的编写:

此处把该模块文件取名为testmodule.c#include//在内核模块中共享

#include//一个模块

//处理CONFIG_MODVERSIONS#ifCONFIG_MODVERSIONS==1#defineMODVERSIONS#include#endifintinit_module()//初始化模块

{printk(“Hello!Thisisatestingmodule!\n”);

return0;

}voidcleanup_module()//取消init_module()函数所做的打印功能操作

{printk(“Sorry!Thetestingmoduleisunloadingnow!\n”);

}

5、模块的编译、装载和卸载:

#gcc–O2–Wall–DMODULE–D__KERNEL__-ctestmodule.c#ls–s//在当前名目下查看生成的目标文件testmodule.o现在,模块testmodule已经编译好了。用下面命令将它装载到系统中:

#insmod–ftestmodule.o

假如装载胜利,则在/proc/modules文件中就可看到模块testmodule,并可看到

它的主设备号。同时在终端显示:

Hello!Thisisatestingmodule!

假如要卸载,就用如下命令:

#rmmodtestmodule

假如卸载胜利,则在/proc/devices文件中就可看到模块testmodule已经不存在

了。同时在终端显示:

Sorry!Thetestingmoduleisunloadingnow!

二、向testmodule模块中添加新函数open()、release()、write()和read()

1、函数open()

intopen(structinode*inode,structfile*filp){MOD_INC_USE_COUNT;//增加该模块的用户数目

printk(“Thismoduleisinopen!\n”);

return0;

}

2、函数release()

voidrelease(structinode*inode,structfile*filp)

{MOD_DEC_USE_COUNT;//该模块的用户数目减1printk(“Thismoduleisinrelease!\n”);

return0;

#ifdefDEBUGprintk(“release(%p,%p)\n”,inode,filp);

#endif}

3、函数read()

intread(structinode*inode,structfile*filp,char*buf,intcount)

{

intleave;

if(verify_area(VERIFY_WRITE,buf,count)==DEFAULT)returnDEFAULT;

for(leave=count;leave>0;leave--)

{

__put_user(1,buf,1);

buf++;

}

returncount;

}

4、函数write()

intwrite(structinode*inode,structfile*filp,constchar*buf,int

count){returncount;

}三、模块的测试

在该模块程序编译加载后,再在/dev名目下创建模块设备文件moduledev,使用

命令:#mknod/dev/moduledevcmajorminor,其中“c”表示moduledev是

字符设备,“major”是moduledev的主设备号。(该字符设备驱动程序编译加载后,

可在/proc/modules文件中获得主设备号,或者使用命令:#cat

/proc/modules|awk”\\$2==\”moduledev\”{print\\$1}”获得主设备号)

#include#include#include#includemain(){inti,testmoduledev;

charbuf;

testmoduledev=open(“/dev/moduledev”,O_RDWR);

if(testmoduledev==-1){printf(“Can’topenthefile!\n”);

exit(0);

}read(testmoduledev,buf,10);

for(i=0;i<10;i++)printf(“%d\n”,buf);

close(testmoduledev);

return0;

}

试验三、定时器试验

1、把握Linux下的定时器编程方法;

2、把握Linux下的常用时间函数编程方法。

1、编写定时器程序timer;

2、编写Makefile文件;

3、下载并调试timer。

1、C语言的基础学问;

2、程序调试的基础学问和方法;

3、Linux的基本操作;

4、把握Linux下的程序编译与交叉编译过程;

5、把握Linux下基本的应用程序编写方法。

操作系统应当能够在将来某个时刻准时调度某个任务。所以需要一种能保证任务较准时调度运行的机制。盼望支持每种操作系统的微处理器必需包含一个可周期性中断它的可编程间隔定时器。这个周期性中断被称为系统时钟滴答,它象节拍器一样来组织系统任务。Linux的时钟观念很简洁:它表示系统启动后的以时钟滴答记数的时间。全部的系统时钟都基于这种量度,在系统中的名称和一个全局变量相同-jiffies。

Linux包含两种类型的系统定时器,它们都可以在某个系统时间上被队列例程使用,但是它们的实现稍有区分。

第一个是老的定时器机制,它包含指向timer_struct结构的32位指针的静态数组以当前活动定时器的屏蔽码:time_active。

此定时器表中的位置是静态定义的(类似底层部分处理表bh_base)。其入口在系统初始化时被加入到表中。其次种是相对较新的定时器,它使用一个到期时间以升序排列的timer_list结构链表。

这两种方法都使用jiffies作为终结时间,这样盼望运行5秒的定时器将不得不将5秒时间转换成jiffies的单位并且将它和以jiffies记数的当前系统时间相加从而得到定时器的终结时间。在每个系统时钟滴答时,定时器的底层部分处理过程被标记成活动状态以便调度管理器下次运行时能进行定时器队列的处理。定时器底层部分处理过程包含两种类型的系统定时器。老的系统定时器将检查timer_active位是否置位。假如活动定时器已经到期则其定时器例程将被调用同时

它的活动位也被清除。新定时器位于timer_list结构链表中的入口也将受到检查。每个过期定时器将从链表中清除,同时它的例程将被调用。新定时器机制的优点之一是能传递一个参数给定时器例程。

在本试验应用程序中,需要进行时间相关的编程动作,如猎取当前时间,对某一段工作进行计时处理以及定时执行某一动作等。本试验将介绍如何在Linux调用时间相关函数完成上述功能。

1、猎取当前时间在程序当中,可以使用下面两个函数输出系统当前的时间:

time_ttime(time_t*tloc);

char*ctime(consttime_t*clock);

time函数返回从1970年1月1日0点以来的秒数.存储在time_t结构之中.这个函数的返回值由于不够直观,以人类的理解方式,这组抽象的数字好像缺乏实际意义。所以我们可以另一个函数ctime(consttime_t*clock)将抽象的时间记录转化为直观的字符串,以便显示。

2、计时处理

有时候我们要计算程序执行的时间。比如我们要对算法进行时间分析。这个时候可以使用下面这个函数,加在需要计算时间的程序的两端:

intgettimeofday(structtimeval*tv,structtimezone*tz)

第一个参数为timeval类型的结构,该结构声明如下:

Struttimeval{

Longtv_sec;//秒数

Longtv_usec;//微秒数

}

gettimeofday将时间保存在结构tv之中。

3、定时器

Linux操作系统为每一个进程供应了3个内部间隔计时器。ITIMER_REAL:削减实际时间。到时的时候发出SIGALRM信号。TIMER_VIRTUAL:削减有效时间(进程执行的时间)。到时的时候产生SIGVTALRM信号。

ITIME_PROF:削减进程的有效时间和系统时间(为进程调度用的时间)。到时的时候产生SIGPROF信号。

详细的操作函数是:

intgetitimer(intwhich,structitimerval*value);

intsetitimer(intwhich,structitimerval*newval,structitimerval*oldval)

相关结构类型声明如下:

structitimerval{

structtimevalit_interval;

structtimevalitvalue;

}

getitimer函数得到间隔计时器的时间值并保存在value中。setitimer函数设置间隔计时器的时间值为newval,并将旧值保存在oldval中。which表示使用三个计时器中的哪一个。itimerval结构中的it_value是削减的时间,当这个值为0的时候就发出相应的信号了,然后设置为it_interval值。

编写timer.c程序源代码

#include

#include

#include

structtimevaltpstart,tpend;

floattimeuse;

statictimer_count=0;

voidprompt_info(intsigno)

{

time_tt=time(NULL);

/*2secondsturned,printsomething*/

printf("prompt_infocalled\n",++timer_count);

/*getcurrenttimeandprintit*/

ctime(

printf("currenttime%s",ctime(

/*stopgettime,andprintit*/

gettimeofday(

timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+tpend.tv_usec-tpstart.tv_usec;

timeuse/=1000000;

printf("UsedTime:%f\n",timeuse);

}

voidinit_sigaction(void)

{

structsigactionact;act.sa_handler=prompt_info;act.sa_flags=0;sigemptyset(sigaction(SIGPROF,

/*begingetthetime*/gettimeofday(printf("begintime\n");

}

voidinit_time()

{

structitimervalvalue;value.it_value.tv_sec=2;value.it_value.tv_usec=0;value.it_interval=value.it_value;setitimer(ITIMER_PROF,

}

/*

*timerapplicationcode

*/

intmain(intargc,char**argv)

{

init_sigaction();init_time();while(1);exit(0);

}

3、编写Makefile文件,Makefile内容如下:

CC=gcc

LD=ld

EXEC=timer

OBJS=timer.o

CFLAGS+=LDFLAGS+=

all:$(EXEC)

$(EXEC):$(OBJS)

$(CC)$(LDFLAGS)-o$@$(OBJS)$(LDLIBS$(LDLIBS_$@))

clean:

-rm-f$(EXEC)*.elf*.gdb*.o

4、编译timer,在timer名目下,终端输入如下命令:

#makeclean

#make

#./timer

输出结果如下:

prompt_infocalled

currenttimeSatMar3015:58:232024

UsedTime:2.003054

prompt_infocalled

currenttimeSatMar3015:58:252024

UsedTime:4.001216

prompt_infocalled

currenttimeSatMar3015:58:272024

UsedTime:6.001144

prompt_infocalled

currenttimeSatMar3015:58:292024

UsedTime:8.001138

该程序正确执行时将每隔两秒钟打印一次上述信息,“prompt_infocalled”字符串是在定时器处理函数中打印的,还打印了当前系统时间和从第1次启动定时器开头猎取的时间间隔。

试验四、设备驱动试验

通过本试验的学习,了解Linux操作系统中的设备驱动程序包括哪些组成部分,并能编写简洁的字符设备(scull,SimpleCharacterUtilityforLoadingLocalities)和块设备(sbull,SimpleBlockUtilityforLoadingLocalities)的驱动程序以及对所编写设备驱动程序的测试,最终了解Linux操作系统是如何管理设备的。

一.设备驱动程序的简洁介绍

Linux设备驱动程序集成在内核中,实际上是处理或操作硬件掌握器的软件。从本质上讲,驱动程序是常驻内存的低级硬件处理程序的共享库,设备驱动程序就是对设备的抽象处理;也即是说,设备驱动程序是内核中具有高特权级的、常驻内存的、可共享的下层硬件处理例程。

设备驱动程序软件封装了如何掌握这些设备的技术细节,并通过特定的接口导出一个规范的操作集合(见图1);内核使用规范的设备接口(字符设备接口和块设备接口)通过文件系统接口把设备操作导出到用户空间程序中。(由于本试验不涉及网络设备,故在此就不作争论)

图1字符(块)设备、驱动

在Linux中,字符设备和块设备的I/O操作是有区分的。块设备在每次硬件操作时把多个字节传送到主存缓存中或从主存缓存中把多个字节信息传送到设备中;而字符设备并不使用缓存,信息传送是一个字节一个字节地进行的。

Linux操作系统允许设备驱动程序作为可装载内核模块实现,这也就是说,设备的接口实现不仅可以在Linux操作系统启动时进行注册,而且还可以在Linux操作系统启动后装载模块时进行注册。

总之,Linux操作系统支持多种设备,这些设备的驱动程序有如下一些特点:

(1)内核代码:设备驱动程序是内核的一部分,假如驱动程序出错,则可能导致系统崩溃。

(2)内核接口:设备驱动程序必需为内核或者其子系统供应一个标准接口。比如,一个终端驱动程序必需为内核供应一个文件I/O接口;一个SCSI设备驱动程序应当为SCSI子系统供应一个SCSI设备接口,同时SCSI子系统也必需为内核供应文件的I/O接口及缓冲区。

(3)内核机制和服务:设备驱动程序使用一些标准的内核服务,如内存安排等。

(4)可装载:大多数的Linux操作系统设备驱动程序都可以在需要时装载进内核,在不需要时从内核中卸载。

(5)可设置:Linux操作系统设备驱动程序可以集成为内核的一部分,并可以依据需要把其中的某一部分集成到内核中,这只需要在系统编译时进行相应的设置即可。

(6)动态性:当系统启动且各个设备驱动程序初始化后,驱动程序将维护其掌握的设备。假如该设备驱动程序掌握的设备不存在也不影响系统的运行,此时的设备驱动程序只是多占用了一点系统内存罢了。

二.设备驱动程序与外界的接口

每种类型的驱动程序,不管是字符设备还是块设备都为内核供应相同的调用接口,故内核能以相同的方式处理不同的设备。Linux为每种不同类型的设备驱动程序维护相应的数据结构,以便定义统一的接口并实现驱动程序的可装载性和动态性。

Linux设备驱动程序与外界的接口可以分为如下三个部分:

(1)驱动程序与操作系统内核的接口:这是通过数据结构file_operations来完成的。

(2)驱动程序与系统引导的接口:这部分利用驱动程序对设备进行初始化。

(3)驱动程序与设备的接口:这部分描述了驱动程序如何与设备进行交互,这与详细设备亲密相关。

可归结为如下图2:

图2设备驱动程序与

三.设备驱动程序的组织结构

设备驱动程序有一个比较标准的组织结构,一般可以分为下面三个主要组成部分:

(1)自动配置和初始化子程序

这部分程序负责检测所要驱动的硬件设备是否存在以及是否能正常工作。假如该设备正常,则对设备及其驱动程序所需要的相关软件状态进行初始化。这部分程序仅在初始化时被调用一次。

(2)服务于I/O恳求的子程序

该部分又可称为驱动程序的上半部分。系统调用对这部分进行调用。系统认为这部分程序在执行时和进行调用的进程属于同一个进程,只是由用户态变成了内核态,而且具有进行此系统调用的用户程序的运行环境。故可以在其中调用与进程运行环境有关的函数。

(3)中断服务子程序

该部分又可称为驱动程序的下半部分。设备在I/O恳求结束时或其它状态转变时产生中断。中断可以产生在任何一个进程运行时,因此中断服务子程序被调用时不能依靠于任何进程的状态,因而也就不能调用与进程运行环境有关的函数。由于设备驱动程序一般支持同一类型的若干设备,所以一般在系统调用中断服务子程序时都带有一个或多个参数,以唯一标识恳求服务的设备。

四.设备驱动程序的代码

设备驱动程序是一些函数和数据结构的集合,这些函数和数据结构是为实现管理设备的一个简洁接口。操作系统内核使用这个接口来恳求驱动程序对设备进行I/O操作。甚至,我们可以把设备驱动程序看成是一个抽象数据类型,它为计算机中的每个硬件设备都建立了一个通用函数接口。由于一个设备驱动程序就是一个模块,所以在内核内部用一个file结构来识别设备驱动程序,而且内核使用file_operatuions结构来访问设备驱动程序中的函数。

了解设备驱动程序代码的如下几个部分:

◆驱动程序的注册与注销。◆设备的打开与释放。◆设备的读写操作。

◆设备的掌握操作。◆设备的中断和轮询处理。

五、字符设备驱动程序的代码

1、了解什么是字符设备

2、了解字符设备的基本入口点

字符设备的基本入口点也可称为子程序,它们被包含在驱动程序的file_operations结构中。

①open()函数;②release()函数;③read()函数;④write()函数;

⑤ioctl()函数;⑥select()函数。

3、字符设备的注册

设备驱动程序供应的入口点在设备驱动程序初始化时向系统登记,以便系统调

用。Linux系统通过调用register_chrdev()向系统注册字符型设备驱动程序。

register_chrdev()定义如下:

#include#include

intregister_chrdev(unsignedintmajor,constchar*name,struct

file_operations*ops);

其中major时设备驱动程序向系统申请的主设备号。假如它为0,则系统为该驱

动程序动态地安排第一个空闲的主设备号,并把设备名和文件操作表的指针置于

chrdevs表的相应位置。name是设备名,ops是对各个调用入口点的说明。

register_chrdev()函数返回0表示注册胜利;返回-EINVAL表示申请的主设备号非法,

一般主设备号大于系统所允许的最大设备号;返回-EBUSY表示所申请的主设备号正被

其它设备驱动程序使用。假如动态安排主设备号胜利,则该函数将返回所安排的主设

备号。假如register_chrdev()操作胜利,则设备名就会消失在/proc/devices文件

中。

字符设备注册以后,还必需在文件系统中为其创建一个代表节点。该节点可以是

在/dev名目中的一个节点,这种节点都是文件节点,且每个节点代表一个详细的设备。

不过要有主设备号和从设备号两个参数才能创建一个节点。还可以是在devfs设备文

件名目下的一个节点,对于这种节点应依据主设备号给每一种设备都创建一个名目节

点,在这个名目下才是代表详细设备的文件节点。

编写一个简洁的字符设备驱动程序。要求该字符设备包括scull_open()、

scull_write()、scull_read()、scull_ioctl()和scull_release()五个基本操作,

并编写一个测试程序来测试你所编写的字符设备驱动程序。

先给出字符设备驱动程序要用到的数据结构定义:

structdevice_struct{constchar*name;

structfile_operations*chops;

};

staticstructdevice_structchrdevs;

typedefstructScull_Dev{

void**data;

intquantum;//thecurrentquantumsizeintqset;//thecurrentarraysizeunsignedlongsize;

unsignedintaccess_key;//usedbysculluidandscullprivunsignedintusage;//lockthedevicewhileusingitstructScull_Dev*next;//nextlistitem}scull;

1、字符设备的结构

字符设备的结构即字符设备的开关表。当字符设备注册到内核后,字符设备的名

字和相关操作被添加到device_struct结构类型的chrdevs全局数组中,称chrdevs

为字符设备的开关表。下面以一个简洁的例子说明字符设备驱动程序中字符设备结构

的定义:(假设设备名为scull)

****file_operation结构定义如下,即定义chr设备的_fops****

staticintscull_open(structinode*inode,structfile*filp);

staticintscull_release(structinode*inode,structfile*filp);

staticssize_tscull_write(structinode*inode,structfile*filp,const

char*buffer,intcount);

staticssize_tscull_read(structinode*inode,structfile*filp,char

*buffer,intcount);

staticintscull_ioctl(structinode*inode,structfile*filp,unsigned

longintcmd,unsignedlongarg);

structfile_operationchr_fops={

NULL,//seek

scull_read,//read

scull_write,//write

NULL,//readdir

NULL,//poll

scull_ioctl,//ioctl

NULL,//mmap

scull_open,//open

NULL,//flush

scull_release,//release

NULL,//fsync

NULL,//fasync

NULL,//checkmediachange

NULL,//revalidate

NULL//lock

};

2、字符设备驱动程序入口点

字符设备驱动程序入口点主要包括初始化字符设备、字符设备的I/O调用和中断。在引导系统时,每个设备驱动程序通过其内部的初始化函数init()对其掌握的设备及其自身初始化。字符设备初始化函数为chr_dev_init(),包含在/linux/drivers/char/mem.c中,它的主要功能之一是在内核中登记设备驱动程序。详细调用是通过register_chrdev()函数。register_chrdev()函数定义如下:

#include

#include

intregister_chrdev(unsignedintmajor,constchar*name,structfile_operation*fops);

其中major是为设备驱动程序向系统申请的主设备号。假如为0,则系统为此驱动程序动态地安排一个主设备号。name是设备名。fops是前面定义的file_operation结构的指针。在登记胜利的状况下,假如指定了major,则register_chrdev()函数返回值为0;假如major值为0,则返回内核安排的主设备号。并且register_chrdev()函数操作胜利,设备名就会消失在/proc/devices文件里;在登记失败的状况下,register_chrdev()函数返回值为负。

初始化部分一般还负责给设备驱动程

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论