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

更新时间:2024-06-19 12:52:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

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

实验一、进程管理实验

【实验目的】

1、加深对进程概念的理解,明确进程和程序的区别 2、进一步认识并发执行的实质

3、分析进程争用资源的现象,学习理解进程互斥的方法 4、了解linux系统中进程通信的基本原理

【实验内容】

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

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

Child process 1 is killed by parent !! Child process 2 is killed by parent !! 父进程调用wait()函数等待两个子进程终止后,输出以下信息,结束进程执行:Parent process is killed!!

多运行几次编写的程序,简略分析出现不同结果的原因。

【实验指导】

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

图1-1 软中断通信程序流程图 (2)参考程序源代码 #include #include #include #include int wait_flag;

void stop(); main() {

int pid1, 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(“\\n Parent process is killed !!\\n”); exit(0); // 父进程结束 } else {

wait_flag = 1;

signal(17,stop); // 等待进程2 被杀死的中断号17 printf(“\\n Child process 2 is killed by parent !!\\n”); exit(0); } } else { wait_flag = 1;

signal(16,stop); // 等待进程1 被杀死的中断号16 printf(“\\n Child process 1 is killed by parent !!\\n”); exit(0);

} }

void stop() { wait_flag = 0; }

(3)程序运行结果

编译运行后,等待从键盘输入“Del”,有如下结果: Child process 1 is killed by parent !! Child process 2 is killed by parent !! Parent process is killed !!

或者:(运行多次后会出现如下结果) Child process 2 is killed by parent !! Child process 1 is killed by parent !! Parent process is killed !!

实验二、模块编程实验

【实验目的】

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

insmod命令来手工加载,通过命令rmmod来手工卸载。

【准备知识】

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

一、模块的组织结构

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

常大的程序,从而存在一个普遍的问题:在一个文件中实现的函数可能需要在其它文件中定义的数据。在传统的程序中,这个问题是通过链接编辑器在生成可执行对象文件时,使用链接编辑器可以解析的外部(全局)变量来解决的。又因为模块的设计和实现与内核无关,所以模块不能靠静态链接通过变量名引用内核数据结构。恰好相反,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信息

int init_module( )

{

? ? // 装载时,初始化模块的编码

}

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

write()、

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

? ?

void cleanup_module( )

{

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

二.模块的编译

一旦设计并编写好模块,必须将其编译成一个适合内核装载的对象文件。由于编写模块是用C语言来完成的,故采用gcc编译器来进行编译。若需要通知编译程序把这个模块作为内核代码而不是普通的用户代码来编译,则就需向gcc编译器传递参数“-D_ _KERNEL_ _”;若需要通知编译程序这个文件是一个模块而不是一个普通文件,则就需向gcc编译器传递参数“-DMODULE”;若需要对模块程序进行优化编译、连接,则就需使用“-O2”参数;若还需要对装载后的模块进行调试,则就需使用“-g”参数;同时需要使用“-Wall”参数来向装载程序传递all,使用“-c”开关通知编译程

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

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

#gcc -O2 -g -Wall -DMODULE -D_ _KERNEL_ _ -c filename.c

//filename.c为自己编写的模块程序源代码文件

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

三.模块的装载

内核模块的装载方式有两种。一种是使用insmod命令手工装载模块;另一种是请求装载demand loading(在需要时装载模块),即当有必要装载某个模块时,若用户安装了核心中不存在的文件系统时,核心将请求内核守护进程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(模块被卸载);同时断开内核模块链表中的连接,

修改它所依赖的其它模块的引用,重新分配模块所占的内核内存。

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

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

i rcleanupcap模ini内register_caprint……unregister_cap函数一其函给

六.模块程序中管理模块的几个文件操作 在内核内部用一个file结构来识别模块,而且内核使用file_operatuions结构来访问模块程序中的函数。file_operatuions结构是一个定义在中的函数指针表。管理模块的文件操作,通常也称为“方法”,它们都为struct file_operations提供函数指针。在struct file_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)(struct inode *,struct file *,char *, int)

该方法用来从模块中读取数据。当其为NULL指针时将引起read系统调用返回-EINVAL(“非法参数”)。函数返回一个非负值表示成功地读取了多少字节。

2、方法int (*write)(struct inode *,struct file *,const char *, int) 该方法用来向模块发送数据。当其为NULL指针时将引起write系统调用返回-EINVAL。如果函数返回一个非负值,则表示成功地写入了多少字节。

3、方法int (*open)(struct inode *,struct file *)

该方法是用来打开模块的操作,它是操作在模块节点上的第一个操作,即使这样,该方法还是可以为NULL指针。如果为NULL指针,则表示该模块的打开操作永远成功,但系统不会通知你的模块程序。

4、方法void (*release)(struct inode *,struct file *)

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

release也可以为NULL指针。

当在你的模块中需要上面这些方法时,相应的方法若没有,则在struct file_operations中相应的地方将其令为NULL指针。这样我们需要的大概象下面这样:

struct file_operations modulename_fops ={

NULL, // modulename_lseek

modulename_read,

modulename_write,

NULL, // modulename_readdir

NULL, // modulename_select

NULL, // modulename_ioctl

NULL, // modulename_mmap

modulename_open,

modulename_release,

NULL, // modulename_fsync

NULL, // modulename_fasync

NULL, // modulename_check_media_change

NULL // 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! This is a testing module!\\n”);等。为便于检查模块是否装载成功,我们可以给一个返回值,如return 0;若返回一个非0值,则表示init_module()失败,从而不

能装载模块。

3、cleanup_module()函数:

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

如printk(“Sorry! The testing module is unloaded now!\\n”);等。

4、模块的编写:

此处把该模块文件取名为testmodule.c

#include // 在内核模块中共享

#include // 一个模块

//处理CONFIG_MODVERSIONS

#if CONFIG_MODVERSIONS == 1

#define MODVERSIONS

#include

#endif

int init_module() // 初始化模块

{

printk(“Hello! This is a testing module! \\n”);

return 0;

}

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

{

printk(“Sorry! The testing module is unloading now! \\n”);

}

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

[root@linux /]# gcc –O2 –Wall –DMODULE –D__KERNEL__ -c testmodule.c

[root@linux /]# ls –s //在当前目录下查看生成的目标文件testmodule.o

现在,模块testmodule已经编译好了。用下面命令将它装载到系统中:

[root@linux /]# insmod –f testmodule.o

如果装载成功,则在/proc/modules文件中就可看到模块testmodule,并可看到

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

Hello! This is a testing module!

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

[root@linux /]# rmmod testmodule

如果卸载成功,则在/proc/devices文件中就可看到模块testmodule已经不存在

了。同时在终端显示:

Sorry! The testing module is unloading now!

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

1、函数open( )

int open(struct inode *inode,struct file *filp) {

MOD_INC_USE_COUNT; // 增加该模块的用户数目

printk(“This module is in open!\\n”);

return 0;

}

2、函数release( ) void release(struct inode *inode,struct file *filp)

{

MOD_DEC_USE_COUNT; //该模块的用户数目减1

printk(“This module is in release!\\n”);

return 0;

#ifdef DEBUG

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

#endif

}

3、函数 read() int read(struct inode *inode,struct file *filp,char *buf,int count) {

int leave;

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

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

{

__put_user(1,buf,1);

buf ++; }

return count;

}

4、函数write()

int write(struct inode *inode,struct file *filp,const char *buf,int

count) {

return count;

}

三、模块的测试

在该模块程序编译加载后,再在/dev目录下创建模块设备文件moduledev,使用命令: #mknod /dev/moduledev c major minor ,其中“c”表示moduledev是字符设备,“major”是moduledev的主设备号。 (该字符设备驱动程序编译加载后,可在/proc/modules文件中获得主设备号,或者使用命令: [root@linux /]#cat

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

#include

#include

#include

#include

main( ) {

int i,testmoduledev;

char buf[10];

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

if(testmoduledev == -1) {

printf(“Can’t open the file! \\n”);

exit(0);

}

read(testmoduledev,buf,10);

for(i=0;i<10;i++)

printf(“%d\\n”,buf[i]);

close(testmoduledev);

return 0;

}

实验三、定时器实验

【实验目的】

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_t time(time_t*tloc); char*ctime(const time_t *clock);

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

2、计时处理

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

int gettimeofday(struct timeval*tv,struct timezone*tz) 第一个参数为 timeval 类型的结构,该结构声明如下: Strut timeval{ Long tv_sec; //秒数 Long tv_usec; }

gettimeofday 将时间保存在结构 tv 之中。 3、定时器

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

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

具体的操作函数是:

int getitimer(int which,struct itimerval*value); int setitimer(int which,struct itimerval*newvalitimerval*oldval)

struct

//微秒数

相关结构类型声明如下: struct itimerval{

struct timeval it_interval; struct timeval it value; }

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

【实验步骤】

编写 timer.c 程序源代码 #include #include #include

struct timeval tpstart,tpend; float

timeuse;

static timer_count = 0;

void prompt_info(int signo) {

time_t t = time(NULL);

/* [1] 2 seconds turned, print something */ printf(\/* [2] get current time and print it */ ctime(&t);

printf(\current time %s\/* [3] stop get time, and print it */ gettimeofday(&tpend,NULL);

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

timeuse/=1000000;

printf(\Used Time:%f\\n\}

void init_sigaction(void) {

struct sigaction act; act.sa_handler=prompt_info; act.sa_flags=0; sigemptyset(&act.sa_mask); sigaction(SIGPROF,&act,NULL);

/* begin get the time */ gettimeofday(&tpstart,NULL); printf(\time\\n\

}

void init_time() {

struct itimerval value; value.it_value.tv_sec=2; value.it_value.tv_usec=0; value.it_interval=value.it_value; setitimer(ITIMER_PROF,&value,NULL);

} /*

* timer application code */

int main(int argc, 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目录下,终端输入如下命令: # make clean # make # ./timer 输出结果如下: [1] prompt_info called

current time Sat Mar 30 15:58:23 2005 Used Time:2.003054 [2] prompt_info called

current time Sat Mar 30 15:58:25 2005

Used Time:4.001216 [3] prompt_info called

current time Sat Mar 30 15:58:27 2005 Used Time:6.001144 [4] prompt_info called

current time Sat Mar 30 15:58:29 2005 Used Time:8.001138

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

实验四、设备驱动实验

【实验目的】

通过本实验的学习,了解Linux操作系统中的设备驱动程序包括哪些组成部分,并能编写简单的字符设备(scull,Simple Character Utility for Loading Localities)和块设备(sbull,Simple Block Utility for Loading Localities)的驱动程序以及

对所编写设备驱动程序的测试,最终了解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

int register_chrdev(unsigned int major, const char *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()五个基本操作,

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

【实验指导】

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

struct device_struct{

const char *name;

struct file_operations *chops;

};

static struct device_struct chrdevs[MAX_CHRDEV];

typedef struct Scull_Dev {

void **data;

int quantum; // the current quantum size

int qset; // the current array size

unsigned long size;

unsigned int access_key; // used by sculluid and scullpriv

unsigned int usage; // lock the device while using it

struct Scull_Dev *next; // next listitem

} scull; 1、字符设备的结构

字符设备的结构即字符设备的开关表。当字符设备注册到内核后,字符设备的名字和相关操作被添加到device_struct结构类型的chrdevs全局数组中,称chrdevs为字符设备的开关表。下面以一个简单的例子说明字符设备驱动程序中字符设备结构的定义:(假设设备名为scull)

**** file_operation结构定义如下,即定义chr设备的_fops **** static int scull_open(struct inode *inode,struct file *filp); static int scull_release(struct inode *inode,struct file *filp); static ssize_t scull_write(struct inode *inode,struct file *filp,const char *buffer,int count);

static ssize_t scull_read(struct inode *inode,struct file *filp,char *buffer,int count);

static int scull_ioctl(struct inode *inode,struct file *filp,unsigned long int cmd,unsigned long arg);

struct file_operation chr_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, // check media change NULL, // revalidate NULL // lock };

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

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

#include #include

int register_chrdev(unsigned int major,const char *name,struct file_operation *fops);

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

初始化部分一般还负责给设备驱动程序申请系统资源,包括内存、中断、时钟、I/O端口等,这些资源也可以在open()子程序或别的地方申请。在这些资源不用的时候,应该释放它们,以利于资源的共享。

用于字符设备的I/O调用主要有:open()、release()、read()、write()和ioctl()。

open()函数的使用比较简单,当一个设备被进程打开时,open()函数被唤醒: static int scull_open(struct inode *inode,struct file *filp) { ?? ??

MOD_INC_USE_COUNT; return 0; }

本文来源:https://www.bwwdw.com/article/6ge3.html

Top