linux驱动程序结构框架及工作原理分别是什么?

linux驱动程序结构框架及工作原理分别是什么?,第1张

一、Linux device driver 的概念\x0d\x0a\x0d\x0a系统调用是 *** 作系统内核和应用程序之间的接口设备驱动程序是 *** 作系统内核和机器硬件之间的接枣圆口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象 *** 作普通文件一样对硬件设备进行 *** 作。设备驱动程序是内核的一部分,它完成以下的功能:\x0d\x0a\x0d\x0a1、对设早岩裤备初始化和释放;\x0d\x0a\x0d\x0a2、把数据从内核传送到硬件和从硬件读取数据;\x0d\x0a\x0d\x0a3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;\x0d\x0a\x0d\x0a4、检测和处理设备出现的错误。\x0d\x0a\x0d\x0a在Linux *** 作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O *** 作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来陆简等待。\x0d\x0a\x0d\x0a已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。\x0d\x0a\x0d\x0a最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。\x0d\x0a\x0d\x0a二、实例剖析\x0d\x0a\x0d\x0a我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。\x0d\x0a\x0d\x0a由于用户进程是通过设备文件同硬件打交道,对设备文件的 *** 作方式不外乎就是一些系统调用,如 open,read,write,close?, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:\x0d\x0a\x0d\x0aSTruct file_operatiONs {\x0d\x0a\x0d\x0aint (*seek) (struct inode * ,struct file *, off_t ,int)\x0d\x0a\x0d\x0aint (*read) (struct inode * ,struct file *, char ,int)\x0d\x0a\x0d\x0aint (*write) (struct inode * ,struct file *, off_t ,int)\x0d\x0a\x0d\x0aint (*readdir) (struct inode * ,struct file *, struct dirent * ,int)\x0d\x0a\x0d\x0aint (*select) (struct inode * ,struct file *, int ,select_table *)\x0d\x0a\x0d\x0aint (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long)\x0d\x0a\x0d\x0aint (*mmap) (struct inode * ,struct file *, struct vm_area_struct *)\x0d\x0a\x0d\x0aint (*open) (struct inode * ,struct file *)\x0d\x0a\x0d\x0aint (*release) (struct inode * ,struct file *)\x0d\x0a\x0d\x0aint (*fsync) (struct inode * ,struct file *)\x0d\x0a\x0d\x0aint (*fasync) (struct inode * ,struct file *,int)\x0d\x0a\x0d\x0aint (*check_media_change) (struct inode * ,struct file *)\x0d\x0a\x0d\x0aint (*revalidate) (dev_t dev)\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0a这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write *** 作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。\x0d\x0a\x0d\x0a下面就开始写子程序。\x0d\x0a\x0d\x0a#include 基本的类型定义\x0d\x0a\x0d\x0a#include 文件系统使用相关的头文件\x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0aunsigned int test_major = 0\x0d\x0a\x0d\x0astatic int read_test(struct inode *inode,struct file *file,char *buf,int count)\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aint left用户空间和内核空间\x0d\x0a\x0d\x0aif (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )\x0d\x0a\x0d\x0areturn -EFAULT\x0d\x0a\x0d\x0afor(left = count left >0 left--)\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0a__put_user(1,buf,1)\x0d\x0a\x0d\x0abuf++\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0areturn count\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0a这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。\x0d\x0a\x0d\x0astatic int write_test(struct inode *inode,struct file *file,const char *buf,int count)\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0areturn count\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0astatic int open_test(struct inode *inode,struct file *file )\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aMOD_INC_USE_COUNT模块计数加以,表示当前内核有个设备加载内核当中去\x0d\x0a\x0d\x0areturn 0\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0astatic void release_test(struct inode *inode,struct file *file )\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aMOD_DEC_USE_COUNT\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0a这几个函数都是空 *** 作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。\x0d\x0a\x0d\x0astruct file_operations test_fops = {?\x0d\x0a\x0d\x0aread_test,\x0d\x0a\x0d\x0awrite_test,\x0d\x0a\x0d\x0aopen_test,\x0d\x0a\x0d\x0arelease_test,\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0a设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。\x0d\x0a\x0d\x0aint init_module(void)\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aint result\x0d\x0a\x0d\x0aresult = register_chrdev(0, "test", &test_fops)对设备 *** 作的整个接口\x0d\x0a\x0d\x0aif (result \x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0a#include \x0d\x0a\x0d\x0amain()\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aint testdev\x0d\x0a\x0d\x0aint i\x0d\x0a\x0d\x0achar buf[10]\x0d\x0a\x0d\x0atestdev = open("/dev/test",O_RDWR)\x0d\x0a\x0d\x0aif ( testdev == -1 )\x0d\x0a\x0d\x0a{\x0d\x0a\x0d\x0aprintf("Cann't open file \n")\x0d\x0a\x0d\x0aexit(0)\x0d\x0a\x0d\x0a}\x0d\x0a\x0d\x0aread(testdev,buf,10)\x0d\x0a\x0d\x0afor (i = 0i 回答于 2022-11-18

写作本文档的目的,是为了解释为什么Linux既没有二进制内核接口,也没有稳定 的内核接口。这里所说的内核接口,是指内核里的接口,而不是内核和用户空间 的接口。内核到用户空间的接口,橘宴是提供给应用程序使用的系统调用,系统调用 在 历史 上几乎没有过变化,将来也不会有变化。我有一些老应用程序是在0.9版本 或者更早版本的内核上编译的,在使用2.6版本内核的Linux发布上依然用得很好 。用户和应用程序作者可以将这个接口看成是稳定的。

你也许以为自己想要稳定的内核接口,但是你不清楚你要的实际上不是它。你需 要的其实是稳定的驱动程序,而你只有将驱动程序放到公版内核的源代码树里, 才有可能达到这个目的。而且这样做还有很多其它好处,正是因为这些好处使得 Linux能成为强壮,稳定,成熟的 *** 作系统,这也是你最开始选择Linux的原因。

只有那些写驱动程序的“怪人”才会担心内核接口的改变,对广大用户来说,既 看不到内核接口,也不需要去关心它。

既然只谈技术问题,我们就有了下面两个主题:二进制内核接口和稳定的内核源 代码接口。这两个问题是互相关联的,让我们先解决掉二进制接口的问题。

假如我们有一个稳定的内核源代码接口,那么自然而然的,我们就拥有了稳定的 二进制接口,是这样的吗?错。让我们看看关于Linux内核的几点事实:

对于一个特定的内核,满足这些条件并不难,使用同一个C编译器和同样的内核配 置选项来编译驱动程序模块就可以了。这对于给一个特定Linux发布的特定版本提 供驱动程序,是完全可以满足需求的。但是如果你要给不同发布的不同版本都发 布一个驱动程序,就需要在每个发布上用不同的内核设置参数都编译一次内核, 这简直跟噩梦一样。而且还要注意到,每个Linux发布还提供不同的Linux内核, 这些内核都针对不同的硬件类型进行了优化(有很多种不同的处理器,还有不同 的内核设置选项)。所以每发布一次驱动程序,都需要提供很多不同版本的内核 模块。

相信我,如果你真的要采取这种发布方式,一定会慢慢疯掉,我很久以前就有过 深刻的教训…

如果有人不将他的内核驱动程序,放入公版内核的源代码树,而又想让驱动程序 一直保持在最新的内核中可用,那么这个话题将会变得没完没了。 内核开发是持续而且快节奏的,从来都不会慢下来。内核开发人员在当前接口中 找到bug,或者找到更好的实现方式。一旦发现这些,他们就很快会去修改当前的 接口圆埋银。修改接口意味着,函数名可能会改变,结构体可能被扩充或者删减,函数 的参数也可能发生改变。一旦接口被修改,内核中使用这些接口的地方需要同时 修正,这样才能保证所有的东西继续工作。

举一个例子,内核的USB驱动程序接口在USB子系统的整个生命周期中,至少经历 了三次重写。这些重写解决以下问题:

这和一些封闭源代码的 *** 作系统形成鲜明的对比,在那些 *** 作系统上,不液闭得不额 外的维护旧的USB接口。这导致了一个可能性,新的开发者依然会不小心使用旧的 接口,以不恰当的方式编写代码,进而影响到 *** 作系统的稳定性。 在上面的例子中,所有的开发者都同意这些重要的改动,在这样的情况下修改代 价很低。如果Linux保持一个稳定的内核源代码接口,那么就得创建一个新的接口 ;旧的,有问题的接口必须一直维护,给Linux USB开发者带来额外的工作。既然 所有的Linux USB驱动的作者都是利用自己的时间工作,那么要求他们去做毫无意 义的免费额外工作,是不可能的。 安全问题对Linux来说十分重要。一个安全问题被发现,就会在短时间内得到修 正。在很多情况下,这将导致Linux内核中的一些接口被重写,以从根本上避免安 全问题。一旦接口被重写,所有使用这些接口的驱动程序,必须同时得到修正, 以确定安全问题已经得到修复并且不可能在未来还有同样的安全问题。如果内核 内部接口不允许改变,那么就不可能修复这样的安全问题,也不可能确认这样的 安全问题以后不会发生。 开发者一直在清理内核接口。如果一个接口没有人在使用了,它就会被删除。这 样可以确保内核尽可能的小,而且所有潜在的接口都会得到尽可能完整的测试 (没有人使用的接口是不可能得到良好的测试的)。

如果你写了一个Linux内核驱动,但是它还不在Linux源代码树里,作为一个开发 者,你应该怎么做?为每个发布的每个版本提供一个二进制驱动,那简直是一个 噩梦,要跟上永远处于变化之中的内核接口,也是一件辛苦活。 很简单,让你的驱动进入内核源代码树(要记得我们在谈论的是以GPL许可发行 的驱动,如果你的代码不符合GPL,那么祝你好运,你只能自己解决这个问题了, 你这个吸血鬼把Andrew和Linus对吸血鬼的定义链接到这里>)。当你的代码加入 公版内核源代码树之后,如果一个内核接口改变,你的驱动会直接被修改接口的 那个人修改。保证你的驱动永远都可以编译通过,并且一直工作,你几乎不需要 做什么事情。

把驱动放到内核源代码树里会有很多的好处:

一、Linux device driver 的概念

系统调用是 *** 作系统内核和应用程序之间的接口,设备驱动程序是 *** 作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只枣圆是一个设备文件,应用程序可以象 *** 作普通文件一样对硬件设备进行 *** 作。设备驱动程序是内核的一部分,它完成以下的功能:

1、对设备初始化和释放;

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

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

 陆简 4、检测和处理设备出现的错误。

在Linux *** 作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O *** 作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。

已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。

最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。

二、实例剖析

我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是早岩裤通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。

由于用户进程是通过设备文件同硬件打交道,对设备文件的 *** 作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:

STruct file_operatiONs {

int (*seek) (struct inode * ,struct file *, off_t ,int)

int (*read) (struct inode * ,struct file *, char ,int)

int (*write) (struct inode * ,struct file *, off_t ,int)

int (*readdir) (struct inode * ,struct file *, struct dirent * ,int)

int (*select) (struct inode * ,struct file *, int ,select_table *)

int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long)

int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *)

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

int (*release) (struct inode * ,struct file *)

int (*fsync) (struct inode * ,struct file *)

int (*fasync) (struct inode * ,struct file *,int)

int (*check_media_change) (struct inode * ,struct file *)

int (*revalidate) (dev_t dev)

}

这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write *** 作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。

下面就开始写子程序。

#include <linux/types.h>基本的类型定义

#include <linux/fs.h>文件系统使用相关的头文件

#include <linux/mm.h>

#include <linux/errno.h>

#include <asm/segment.h>

unsigned int test_major = 0

static int read_test(struct inode *inode,struct file *file,char *buf,int count)

{

int left用户空间和内核空间

if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )

return -EFAULT

for(left = count left >0 left--)

{

__put_user(1,buf,1)

buf++

}

return count

}

这个函数是为read调用准备的。当调用read时,read_test()被调用,它把用户的缓冲区全部写1。buf 是read调用的一个参数。它是用户进程空间的一个地址。但是在read_test被调用时,系统进入核心态。所以不能使用buf这个地址,必须用__put_user(),这是kernel提供的一个函数,用于向用户传送数据。另外还有很多类似功能的函数。请参考,在向用户空间拷贝数据之前,必须验证buf是否可用。这就用到函数verify_area。为了验证BUF是否可以用。

static int write_test(struct inode *inode,struct file *file,const char *buf,int count)

{

return count

}

static int open_test(struct inode *inode,struct file *file )

{

MOD_INC_USE_COUNT模块计数加以,表示当前内核有个设备加载内核当中去

return 0

}

static void release_test(struct inode *inode,struct file *file )

{

MOD_DEC_USE_COUNT

}

这几个函数都是空 *** 作。实际调用发生时什么也不做,他们仅仅为下面的结构提供函数指针。

struct file_operations test_fops = {?

read_test,

write_test,

open_test,

release_test,

}

设备驱动程序的主体可以说是写好了。现在要把驱动程序嵌入内核。驱动程序可以按照两种方式编译。一种是编译进kernel,另一种是编译成模块(modules),如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态的卸载,不利于调试,所以推荐使用模块方式。

int init_module(void)

{

int result

result = register_chrdev(0, "test", &test_fops)对设备 *** 作的整个接口

if (result <0) {

printk(KERN_INFO "test: can't get major number\n")

return result

}

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

return 0

}

在用insmod命令将编译好的模块调入内存时,init_module 函数被调用。在这里,init_module只做了一件事,就是向系统的字符设备表登记了一个字符设备。register_chrdev需要三个参数,参数一是希望获得的设备号,如果是零的话,系统将选择一个没有被占用的设备号返回。参数二是设备文件名,参数三用来登记驱动程序实际执行 *** 作的函数的指针。

如果登记成功,返回设备的主设备号,不成功,返回一个负值。

void cleanup_module(void)

{

unregister_chrdev(test_major,"test")

}

在用rmmod卸载模块时,cleanup_module函数被调用,它释放字符设备test在系统字符设备表中占有的表项。

一个极其简单的字符设备可以说写好了,文件名就叫test.c吧。

下面编译 :

$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c –c表示输出制定名,自动生成.o文件

得到文件test.o就是一个设备驱动程序。

如果设备驱动程序有多个文件,把每个文件按上面的命令行编译,然后

ld ?-r ?file1.o ?file2.o ?-o ?modulename。

驱动程序已经编译好了,现在把它安装到系统中去。

$ insmod ?–f ?test.o

如果安装成功,在/proc/devices文件中就可以看到设备test,并可以看到它的主设备号。要卸载的话,运行 :

$ rmmod test

下一步要创建设备文件。

mknod /dev/test c major minor

c 是指字符设备,major是主设备号,就是在/proc/devices里看到的。

用shell命令

$ cat /proc/devices

就可以获得主设备号,可以把上面的命令行加入你的shell script中去。

minor是从设备号,设置成0就可以了。

我们现在可以通过设备文件来访问我们的驱动程序。写一个小小的测试程序。

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

main()

{

int testdev

int i

char buf[10]

testdev = open("/dev/test",O_RDWR)

if ( testdev == -1 )

{

printf("Cann't open file \n")

exit(0)

}

read(testdev,buf,10)

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

printf("%d\n",buf[i])

close(testdev)

}

编译运行,看看是不是打印出全1

以上只是一个简单的演示。真正实用的驱动程序要复杂的多,要处理如中断,DMA,I/O port等问题。这些才是真正的难点。上述给出了一个简单的字符设备驱动编写的框架和原理,更为复杂的编写需要去认真研究LINUX内核的运行机制和具体的设备运行的机制等等。希望大家好好掌握LINUX设备驱动程序编写的方法。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/8246484.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-14
下一篇 2023-04-14

发表评论

登录后才能评论

评论列表(0条)

保存