Linux驱动程序设计之三---Linux字符驱动程序(下)

来源:互联网 发布:淘宝2个金冠店值多少钱 编辑:程序博客网 时间:2024/05/18 00:51

Linux字符驱动程序(下)

 

4.重要的数据结构

如同你想象的注册设备编号仅仅是驱动代码必须进行的诸多任务中的第一个我们将很快看到其他重要的驱动组件但首先需要涉及一个别的大部分的基础性的驱动操作包括 3 个重要的内核数据结构称为 file_operations, file,  inode. 需要对这些结构的基本了解才能够做大量感兴趣的事情,因此我们现在在进入如何实现基础性驱动操作的细节之前会快速查看每一个.

文件操作

到现在我们已经保留了一些设备编号给我们使用,但是我们还没有连接任何我们设备操作到这些编号上.file_operation 结构是一个字符驱动如何建立这个连接这个结构定义在 <linux/fs.h>, 是一个函数指针的集合每个打开文件(内部用一个 file 结构来代表稍后我们会查看)与它自身的函数集合相关连通过包含一个称为f_op 的成员它指向一个 file_operations 结构). 这些操作大部分负责实现系统调用因此命名为 open, read,等等我们可以认为文件是一个"对象"并且其上的函数操作称为它的"方法", 使用面向对象编程的术语来表示一个对象声明的用来操作对象的动作这是我们在Linux 内核中看到的第一个面向对象编程的现象后续章中我们会看到更多.

传统上,一个file_operation 结构或者其一个指针称为 fops( 或者它的一些变体). 结构中的每个成员必须指向驱动中的函数,这些函数实现一个特别的操作,或者对于不支持的操作留置为 NULL.当指定为 NULL 指针时内核的确切的行为是每个函数不同的,如同本节后面的列表所示.

下面的列表介绍了一个应用程序能够在设备上调用的所有操作.我们已经试图保持列表简短这样它可作为一个参考,只是总结每个操作和在 NULL 指针使用时的缺省内核行为.

在你通读file_operations 方法的列表时,你会注意到不少参数包含字串__user.这种注解是一种文档形式,注意,一个指针是一个不能被直接解引用的用户空间地址.对于正常的编译,__user 没有效果,但是它可被外部检查软件使用来找出对用户空间地址的错误使用.

 

struct module *owner

   第一个 file_operations 成员根本不是一个操作它是一个指向拥有这个结构的模块的指针这个成员用来在它的操作还在被使用时阻止模块被卸载几乎所有时间中它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.

loff_t (*llseek) (struct file *, loff_t, int);

    llseek 方法用作改变文件中的当前读/写位置并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽错误由一个负返回值指示如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改file 结构中的位置计数器"file 结构一节中描述).

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

   用来从设备中获取数据在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败一个非负返回值代表了成功读取的字节数返回值是一个 "signed size" 类型常常是目标平台本地的整数类型).

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

   发送数据给设备如果 NULL, -EINVAL 返回给调用 write 系统调用的程序如果非负返回值代表成功写的字节数.

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

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道这不是读也不是写). 另外几个 ioctl 命令被内核识别而不必引用 fops 如果设备不提供 ioctl 方法对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

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

    mmap 用来请求将设备内存映射到进程的地址空间如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

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

     尽管这常常是对设备文件进行的第一个操作不要求驱动声明一个对应的方法如果这个项是 NULL, 设备打开一直成功但是你的驱动不会得到通知.

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

     在文件结构被释放时引用这个操作如同 open, release 可以为 NULL.

 



ctest 设备驱动只实现最重要的设备方法它的 file_operations 结构是如下初始化的:

struct file_operations ctest_fops = {

 .owner =  THIS_MODULE,

 .llseek =  ctest_llseek,

 .read =  ctest_read,

 .write =  ctest_write,

 .ioctl =  ctest_ioctl,

 .open =  ctest_open,

 .release =  ctest_release, 

}; 

这个声明使用标准的 C 标记式结构初始化语法这个语法是首选的因为它使驱动在结构定义的改变之间更加可移植并且有争议地使代码更加紧凑和可读标记式初始化允许结构成员重新排序;在某种情况下真实的性能提高已经实现通过安放经常使用的成员的指针在相同硬件高速存储行中.

文件结构

struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构注意 file 与用户空间程序的 FILE 指针没有任何关系一个FILE 定义在 C 库中从不出现在内核代码中一个 struct file, 另一方面是一个内核结构从不出现在用户程序中.

文件结构代表一个打开的文件. (它不特定给设备驱动系统中每个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时创建,并传递给在文件上操作的任何函数直到最后的关闭在文件的所有实例都关闭后内核释放这个数据结构.

在内核源码中, struct file 的指针常常称为 file 或者 filp("file pointer"). 我们将一直称这个指针为 filp 以避免和结构自身混淆因此, file 指的是结构 filp 是结构指针.

struct file 的最重要成员在这展示如同在前一节第一次阅读可以跳过这个列表但是在本章后面,当我们面对一些真实 C 代码时我们将更详细讨论这些成员.

mode_t f_mode;

文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ  FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可但是你不需要检查读写许可因为内核在调用你的方法之前检查当文件还没有为那种存取而打开时读或写的企图被拒绝驱动甚至不知道这个情况.

loff_t f_pos;

当前读写位置. loff_t 在所有平台都是 64  gcc 术语里是 long long ). 驱动可以读这个值如果它需要知道文件中的当前位置但是正常地不应该改变它读和写应当使用它们作为最后参数而收到的指针来更新一个位置代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中它的目的就是改变文件位置.

unsigned int f_flags;

这些是文件标志例如 O_RDONLY, O_NONBLOCK,  O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作其他标志很少使用特别地应当检查读/写许可使用 f_mode 而不是f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.

struct file_operations *f_op;

和文件关联的操作内核安排指针作为它的 open 实现的一部分接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用这意味着你可改变你的文件关联的文件操作在你返回调用者之后新方法会起作用例如关联到主编号 1 (/dev/null, /dev/zero, 等等) open 代码根据打开的次编号来替代 filp->f_op 中的操作这个做法允许实现几种行为在同一个主编号下而不必在每个系统调用中引入开销替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.

void *private_data;

open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前你可自由使用这个成员或者忽略它你可以使用这个成员来指向分配的数据但是接着你必须记住在内核销毁文件结构之前 release 方法中释放那个内存. private_data 是一个有用的资源,在系统调用间保留状态信息我们大部分例子模块都使用它.

struct dentry *f_dentry;

关联到文件的目录入口( dentry )结构设备驱动编写者正常地不需要关心 dentry 结构除了作为 filp->f_dentry->d_inode存取 inode 结构.

真实结构有多几个成员但是它们对设备驱动没有用处我们可以安全地忽略这些成员因为驱动从不创建文件结构它们真实存取别处创建的结构.

inode 结构

inode 结构由内核在内部用来表示文件因此它和代表打开文件描述符的文件结构是不同的可能有代表单个文件的多个打开描述符的许多文件结构,但是它们都指向一个单个 inode 结构.inode 结构包含大量关于文件的信息作为一个通用的规则这个结构只有 2 个成员对于编写驱动代码有用:

dev_t i_rdev;

对于代表设备文件的节点这个成员包含实际的设备编号.

struct cdev *i_cdev;

struct cdev 是内核的内部结构代表字符设备这个成员包含一个指针指向这个结构当节点指的是一个字符设备文件时.

i_rdev 类型在 2.5 开发系列中改变了破坏了大量的驱动作为一个鼓励更可移植编程的方法内核开发者已经增加了 2 个宏可用来从一个 inode中获取主次编号:

unsigned int iminor(struct inode *inode);

unsigned int imajor(struct inode *inode);

为了不要被下一次改动抓住应当使用这些宏代替直接操作 i_rdev.

 

cdev 结构体

Linux 2.6 内核中,使用cdev 结构体描述一个字符设备,cdev 结构体的定义如下:

-------------------------------------------------------------------------------------

struct cdev{

    struct kobject kobj; /* 内嵌的kobject 对象 */

    struct module *owner; /*所属模块*/

    struct file_operations *ops; /*文件操作结构体*/

    struct list_head list;

    dev_t dev; /*设备号*/

    unsigned int count;

 };

-----------------------------------------------------------------------

ü  cdev 结构体的dev_t 成员定义了设备号,关于dev_t和主设备号和次设备号的内容已经在主设备号和次设备号一节描述过。

ü  cdev 结构体的另一个重要成员file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数,前面已做描述。

ü  struct list_head list用于把一个cdev的结构体添加到Linux内核管理字符设备的链表中。

 

5.字符驱动的注册和注销

Linux 2.6 内核提供了一组函数用于操作cdev 结构体:

void cdev_init( struct cdev *, struc t file_operations *);

struct cdev *cdev_alloc(void) ;

int cdev_add(struct cdev *, dev_t, unsigned) ;

void cdev_del(struct cdev *);

cdev_init()函数用于初始化cdev的成员,并建立cdevfile_operations之间的连接。

cdev_alloc()函数用于动态申请一个cdev 内存。

cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。

下面是一个最简单的字符驱动的实例(ctest.c),将会引用这些函数想内核中注册一个字符驱动:

-----------------------------------------------------------------------

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/moduleparam.h>

#include <linux/kdev_t.h>

#include <linux/types.h>

#include <linux/fs.h>

#include <linux/cdev.h>            //#支持cdev,以及对应的操作的头文件

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("stephanxu@eetek");

MODULE_DESCRIPTION("the first kernel module");;

static dev_t devnum;

static struct cdev ctest;          //#(1)定义全局变量ctest,代表一个字符设备。

 

 

static struct file_operations ctest_fops={  //(2)定义对这个字符设备支持的操作

 .owner = THIS_MODULE,

.open = ctest_open,

 .release = ctest_release,

};

static int __init hello_init(void)

{

  int ret=0;

  ret = alloc_chrdev_region(&devnum,0,1,"dynamic ctest");

  if(ret<0){

    goto out1;

  }

printk("register char device number successfully,

major=%d,minor=%d\n",MAJOR(devnum),MINOR(devnum));

  cdev_init(&ctest,ctest_fops);   //(3)初始化字符设备ctest,将于对应的操作绑定

  ret=cdev_add(&ctest,devnum,1);  //(4)将设备加入到内核中,与devnum建立关系

  if(ret<0){

printk(“add the ctest device into kernel failed\n”);

goto out2;

}else{

  return 0;

}

out2:                              //为何可以使用goto语句?

  Unregister_chrdev_region(devnum,1);

out1:

  return ret;

}

 

static void __exit hello_exit(void)

{

  cdev_del(&ctest);             //(5)ctest这个设备从内核中删除

  unregister_chrdev_region(devnum,1);

}

 

module_init(hello_init);

module_exit(hello_exit);

-----------------------------------------------------------------------

Makefile只需将hello模块的Makefileobj-m :=hello.o 改称obj-m := ctest

对应的代码解释如下:

(1)     static struct cdev ctest

这一行代码的目的是定义一个struct cdev类型的全局变量:ctest, 我们在程序中使用这个变量来代表一个字符设备,到现在为止,这个ctest到底代表那个设备我们还不得而知,所以,它仅仅是操作系统虚拟出来的,用来代表一个字符设备。

(2)     static struct file_operations ctest_fops

这行代码用来定义对ctest这个设备,支持的文件操作有哪些;在前面我们已经了解了一切皆是文件的思想,在这里就是定义对这个设备文件,可以支持哪一些操作,在本例中,我们仅仅支持openclose(release也就是close的意思)的操作。

(3)     cdev_init(&ctest,ctest_fops);

这一行代码的目的,就是将ctest初始化,并且与ctest_fops建立绑定关系。(1),(2)两句仅仅只是定义了两个变量而已,他们之间并没有对应关系,而这一行的目的就是在ctestctest_fops之间建立绑定关系。以后当我们操作ctest的时候,将会调用fops中的对应的函数。

(4)     ret=cdev_add(&ctest,devnum,1);

这一行代码的作用是将ctest添加到内核中,这样内核就知道了ctest这个设备的存在,并且将ctest与设备号之间建立一一对应的关系。这样,内核不仅知道这个设备,而且知道这个设备的设备号是多少。

    5   cdev_del(&ctest);

一旦模块被删除,或者说这个驱动程序将不再需要的时候,调用这一行代码就可以将先前已添加到内核中的字符驱动从内核中删除。

【注】:我们仔细研究完整段代码,我们会发现,这个ctest虽然代表某个字符设备,但是我们并没有发现它与某个具体的硬件设备扯上关系,那这能算是一个字符设备驱动吗?通过这个例子,我们现在也就不难理解cdev实际上只是有一个字符驱动的表示,而且也该知道Linux中的很多虚拟设备是怎么得来的,比如/dev/null等。

 

 

从上面的代码,我们得出几个知识点的关联:

ü  ctest与设备号的一一对应的关联,也就是说,由设备号可以查找到对应的设备,由设备可以查找对应的设备号,而这些都是由内核管理。

ü  ctestfile_operations的关联,就是使用ctest可以查找到对应于它的操作是什么。

6.文件操作接口

    在上例中,我们仅仅实现了最简单的file_operations的操作,它是与任何设备无关的。但是一个驱动程序更多的时候应该操作一个硬件单元,比如说这是一个串口的驱动程序的话,那么open的时候就应该打开串口设备,read就应该从串口设备读取数据,这样说来只有在实现file_operations的操作的时候,才知道我们具体的是针对那个设备的操作,如果file_operations不操作任何具体的硬件的话,那么这个字符驱动程序将不会与一个真正的物理硬件相对应。上面的代码,也就是一般意义上字符驱动程序的框架。

    我们在这里还是用上述的虚拟设备,实现file_operations的另外两个重要的接口:read,write

    read是希望当我们可以从这个设备读取数据,我们在这里做一个简单的定义:返回”hello,user”.表示内核对用户程序的应答。同理,我们让write操作向这个设备发送一条数据,表示对这个设备的问候。那么我们如何实现呢?

-----------------------------------------------------------------------

#include <asm/uaccess.h>

//. . . . ctest_release及其以上不变

static ssize_t ctest_read(struct file *file,char __user *buf,

size_t count,loff_t *offset)

{

    int cnt;

    char *greeting = “hello,user”;

    if(count>(strlen(greeting)+1))

       cnt = strlen(greeting)+1;

    else

       cnt = count;

if(copy_to_user(buf,greeting,cnt))      //(1)为何

       return cnt;

else

       return -1;

}

static ssize_t ctest_write(struct file *file,char __user *buf,

size_t count,loff_t *offst)

{

  char ctestbuf[256];

  int cnt;

  memset(ctestbuf,0,256);

  if(count<256)

    cnt = count;

  else

    cnt = 255;

  if(!copy_from_user(ctestbuf,buf,cnt)){  //(2)???

    printk(“%s\n”,ctestbuf);

    return cnt;

}else{

  return -1;

}

}

Static struct file_operations ctest_fops = {

 .owner = THIS_MODULE,

 .open = ctest_open,

 .release = ctest_release,

 .read = ctest_read,

 .write = ctest_write

};

//下同. . . . . .

-----------------------------------------------------------------------

上面的代码实现了一个简单的read,write的功能,当read的时候,从驱动程序返回一段数据给用户程序,write的时候,从用户程序接受一段数据。如果是一个真正的设备驱动的话,就可以对设备进行相应操作,从设备读取数据,返回给用户,从用接受数据,写向设备。

read  write 方法的 buff 参数是用户空间指针因此它不能被内核代码直接解引用这个限制有几个理由:

ü  依赖于你的驱动运行的体系以及内核被如何配置的用户空间指针当运行于内核模式可能根本是无效的可能没有那个地址的映射或者它可能指向一些其他的随机数据.

ü  就算这个指针在内核空间是同样的东西用户空间内存是分页的在做系统调用时这个内存可能没有在 RAM 试图直接引用用户空间内存可能产生一个页面错这是内核代码不允许做的事情结果可能是一个"oops", 导致进行系统调用的进程死亡.

ü  置疑中的指针由一个用户程序提供它可能是错误的或者恶意的如果你的驱动盲目地解引用一个用户提供的指针它提供了一个打开的门路使用户空间程序存取或覆盖系统任何地方的内存如果你不想负责你的用户的系统的安全危险你就不能直接解引用用户空间指针.

显然你的驱动必须能够存取用户空间缓存以完成它的工作但是为安全起见这个存取必须使用特殊的内核提供的函数.几个这样的函数定义于<asm/uaccess.h>,

读写代码需要拷贝一整段数据到或者从用户地址空间这个能力由下列内核函数提供它们拷贝一个任意的字节数组并且位于大部分读写实现的核心中.

unsigned long copy_to_user(void __user *to,const void *from,

unsigned long count);

unsigned long copy_from_user(void *to,const void __user *from,

unsigned long count);

尽管这些函数表现象正常的 memcpy 函数必须加一点小心在从内核代码中存取用户空间.

这两个函数的角色不限于拷贝数据到和从用户空间它们还检查用户空间指针是否有效如果指针无效不进行拷贝如果在拷贝中遇到一个无效地址,另一方面只拷贝部分数据在两种情况下返回值是还要拷贝的数据量.

要检查用户空间指针你可以调用 __copy_to_user  __copy_from_user 来代替这是有用处的例如如果你知道你已经检查了这些参数但是要小心事实上如果你不检查你传递给这些函数的用户空间指针那么你可能造成内核崩溃和/或安全漏洞.

Linux驱动程序设计之二---Linux字符驱动程序(下) - 御风亭 - 专业,诚信,责任

 【注】我们这里没有讨论

loff_t *pos的内容,实际的驱动中应该要考虑偏移量的问题,大家可以参考LDD中的介绍。

7.使用新设备

    当我们把驱动程序模块加载到内核之后,可以通过查看/proc/devices文件来获得我们动态申请的主设备号,也可以通过在模块加载的时候通过printk打印出其主设备号和次设备号。如果采用的是静态的设备号,这将更简单的。

    在使用新的驱动程序之前,我们必须要把硬件设备抽象成为文件,这也就是我们前面多次提到的一切皆是文件的思想。我们在这里使用前面所讲的mknod来创建一个设备节点,如果想让我们的应用程序访问节点的时候,会调用前面的驱动程序,必须把设备节点名和设备号关联起来:

#mknod /dev/ctest c major minor

这里的major,minor用驱动程序打印出来的majorminor的值代替。这样子我们就建立了一个设备节点/dev/ctest,这个名称,与我们在申请设备号的时候给的name没什么关系。

下面是测试的应用程序的代码:

-----------------------------------------------------------------------

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/stat.h>

#include <fcntl.h>

#define DEVNAME “/dev/ctest”

int main()

{

  int fd = fopen(DEVNAME,O_RDWR);

  char buf[16]={0};

  if(fd<0){

     printf(“open %s error\n”,DEVNAME);

     exit(1);

}

 read(fd,buf,16);

 printf(“%s\n”,buf);

getchar();

memset(buf0sizeof(buf));

strcpy(buf,”hello,kernel”)

 write(fd,buf,strlen(buf));

 close(fd);

 return 0;

}

-----------------------------------------------------------------------

编译并运行,这样我们可以看到main程序可以从/dev/ctest读取内容hello,user

Linux驱动程序设计之二---Linux字符驱动程序(下) - 御风亭 - 专业,诚信,责任

 同理,我们通过

dmesg命令可以查看从main写到/dev/ctest的数据”hello,kernel”.

那这里的操作流程我们解释一下:

ü  main程序里操作的都是/dev/ctest的设备节点,比如打开,关闭,读写操作。

ü  而这个节点在创建的时候和字符设备的主设备号次设备号绑定在一起。可以设备节点得到设备号。

ü  当我们把字符设备添加的内核的时候我们将它与主次设备号一一对应,这样由设备号就可以找到对应的cdev的结构。

ü  在对cdev初始化的时候将他与fops绑定在一起,这样就可以找到对应的操作。

所以你仔细观察,我们对/dev/ctest执行的几个操作都是在fops中定义的,open调用file operations中对应的open,close调用file_operations中的release,一次类推。

这些关系,可由下图总结之:

Linux驱动程序设计之二---Linux字符驱动程序(下) - 御风亭 - 专业,诚信,责任

 

 

实验(2)

(1)     完成编写一个完整的字符驱动程序结构以及Makefile

 

(2)     根据模块加载的设备号结果,建立设备节点

 

(3)     编写测试程序,要求能够对设备进行读写操作。

 

(4)     用统一的Makefile进行driverapplication的管理

 

(5)     要求在ARM开发板上进行测试,开发板使用NFS作为根文件系统。