`
coolerbaosi
  • 浏览: 727128 次
文章分类
社区版块
存档分类
最新评论

linux设备驱动--globalmem字符设备框架分析

 
阅读更多

linux设备驱动--globalmem字符设备框架分析

最近正在学习设备驱动开发,因此打算写一个系列博客,即是对自己学习的一个总结,也是对自己的一个督促,有不对,不足,需要改正的地方还望大家指出,而且希望结识志同道合的朋友一起学习技术,共同进步。

作者:liufei_learning(转载请注明出处)

email:flying0216@foxmail.com

IT学习交流群:160855096

开发环境:Win7(主机)+ VisualBox + ubuntu10.10(虚拟机) + TQ2440开发板(2.6.30.4内核)

功能:1.设备驱动开发详解-globalmem字符设备框架分析(支持2个设备)

目录: 1. globalmem流程图

2.源码

3.分析

1)MKDEV

2)kmalloc和vmalloc的区别

3)module_param

4)file文件结构

5)inode结构

6)container_of()

7)ioctl函数

实现: globalmem流程图

源码:

globalmen模块

#include<linux/module.h>
#include<linux/types.h>
#include<linux/fs.h>
#include<linux/errno.h>
#include<linux/mm.h>
#include<linux/sched.h>
#include<linux/init.h>
#include<linux/cdev.h>
#include<linux/slab.h>        
#include<linux/kdev_t.h>
#include<linux/device.h>
#include <asm/io.h>
#include<asm/system.h>
#include<asm/uaccess.h>
 
#define DEVICE_NAME  "globalmem"
#defineGLOBALMEM_SIZE        0x1000        /*全局内存最大4K字节*/
#define MEM_CLEAR0x1  /*清0全局内存*/
#define GLOBALMEM_MAJOR241    /*预设的globalmem的主设备号*/
 
static intglobalmem_major = GLOBALMEM_MAJOR;
/*globalmem设备结构体*/
struct globalmem_dev                                    
{                                                       
  struct cdev cdev; /*cdev结构体*/                      
  unsigned char mem[GLOBALMEM_SIZE];/*全局内存*/       
};
 
struct globalmem_dev*globalmem_devp; /*设备结构体指针*/
 
static struct class*globalmem0_class;
static struct class*globalmem1_class;
 
/*文件打开函数*/
int globalmem_open(structinode *inode, struct file *filp)
{
  /*将设备结构体指针赋值给文件私有数据指针*/
  struct globalmem_dev *dev;
 
  dev = container_of(inode->i_cdev,structglobalmem_dev,cdev); 
  filp->private_data = dev; 
  return 0;
}
/*文件释放函数*/
intglobalmem_release(struct inode *inode, struct file *filp)
{
  return 0;
}
 
/* ioctl设备控制函数 */
static intglobalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
  int cmd, unsigned long arg)
{
  struct globalmem_dev *dev =filp->private_data;/*获得设备结构体指针*/
 
  switch (cmd)
  {
    case MEM_CLEAR:
      memset(dev->mem, 0,GLOBALMEM_SIZE);     
      printk(KERN_INFO "globalmem is setto zero\n");
      break;
 
    default:
      return - EINVAL;
  }
  return 0;
}
 
/*读函数*/
static ssize_tglobalmem_read(struct file *filp, char __user *buf, size_t size,
  loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev =filp->private_data; /*获得设备结构体指针*/
 
  /*分析和获取有效的写长度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 0;
  if (count > GLOBALMEM_SIZE - p)
    count = GLOBALMEM_SIZE - p;
 
  /*内核空间->用户空间*/
  if (copy_to_user(buf, (void*)(dev->mem +p), count))
  {
    ret = - EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;
   
    printk(KERN_INFO "read %d bytes(s)from %d\n", count, p);
  }
 
  return ret;
}
 
/*写函数*/
static ssize_tglobalmem_write(struct file *filp, const char __user *buf,
  size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev =filp->private_data; /*获得设备结构体指针*/
 
  /*分析和获取有效的写长度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 0;
  if (count > GLOBALMEM_SIZE - p)
    count = GLOBALMEM_SIZE - p;
   
  /*用户空间->内核空间*/
  if (copy_from_user(dev->mem + p, buf,count))
    ret = - EFAULT;
  else
  {
    *ppos += count;
    ret = count;
   
    printk(KERN_INFO "written %d bytes(s)from %d\n", count, p);
  }
 
  return ret;
}
 
/* seek文件定位函数 */
static loff_tglobalmem_llseek(struct file *filp, loff_t offset, int orig)
{
  loff_t ret = 0;
  switch (orig)
  {
    case 0:  /*相对文件开始位置偏移*/
      if (offset < 0)
      {
        ret = - EINVAL;
        break;
      }
      if ((unsigned int)offset >GLOBALMEM_SIZE)
      {
        ret = - EINVAL;
        break;
      }
      filp->f_pos = (unsigned int)offset;
      ret = filp->f_pos;
      break;
    case 1:  /*相对文件当前位置偏移*/
      if ((filp->f_pos + offset) >GLOBALMEM_SIZE)
      {
        ret = - EINVAL;
        break;
      }
      if ((filp->f_pos + offset) < 0)
      {
        ret = - EINVAL;
        break;
      }
      filp->f_pos += offset;
      ret = filp->f_pos;
      break;
    default:
      ret = - EINVAL;
      break;
  }
  return ret;
}
 
/*文件操作结构体*/
static const structfile_operations globalmem_fops =
{
  .owner = THIS_MODULE,
  .llseek = globalmem_llseek,
  .read = globalmem_read,
  .write = globalmem_write,
  .ioctl = globalmem_ioctl,
  .open = globalmem_open,
  .release = globalmem_release,
};
 
/*初始化并注册cdev*/
static voidglobalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
  int err, devno = MKDEV(globalmem_major,index);
 
  cdev_init(&dev->cdev,&globalmem_fops);
  dev->cdev.owner = THIS_MODULE;
  dev->cdev.ops = &globalmem_fops;
  err = cdev_add(&dev->cdev, devno, 1);
  if (err)
    printk(KERN_NOTICE "Error %d addingLED%d", err, index);
}
 
/*设备驱动模块加载函数*/
int globalmem_init(void)
{
  int result;
  dev_t devno = MKDEV(globalmem_major, 0);
 
  /* 申请设备号*/
  if (globalmem_major)
    result = register_chrdev_region(devno, 2,DEVICE_NAME);
  else /* 动态申请设备号 */
  {
    result = alloc_chrdev_region(&devno, 0,2, DEVICE_NAME);
    globalmem_major = MAJOR(devno);
  } 
  if (result < 0)
    return result;
   
  /* 动态申请2个设备结构体的内存*/
  globalmem_devp = kmalloc(2*sizeof(structglobalmem_dev), GFP_KERNEL);
  if (!globalmem_devp)    /*申请失败*/
  {
    result = - ENOMEM;
    goto fail_malloc;
  }
  memset(globalmem_devp, 0, 2*sizeof(structglobalmem_dev));
 
  globalmem_setup_cdev(&globalmem_devp[0],0);
  globalmem_setup_cdev(&globalmem_devp[1],1);
 
    //注册一个类,使mdev可以在/dev/下面建立设备节点 
    globalmem0_class =class_create(THIS_MODULE, "globalmem0"); 
    if( IS_ERR(globalmem0_class) ) 
    { 
        printk(KERN_NOTICE, "creatglobalmem0_class failed!"); 
        return -1; 
    } 
 
    //创建一个设备节点,节点名字为globalmem0 
    device_create(globalmem0_class, NULL,MKDEV(globalmem_major, 0), NULL, "globalmem0"); 
    printk(KERN_NOTICE, "globalmem0initialized!"); 
 
    globalmem1_class =class_create(THIS_MODULE, "globalmem1"); 
    if( IS_ERR(globalmem1_class) ) 
    { 
        printk(KERN_NOTICE, "creatglobalmem1_class failed!"); 
        return -1; 
    } 
 
    //创建一个设备节点,节点名字为globalmem1 
    device_create(globalmem1_class, NULL,MKDEV(globalmem_major, 1), NULL, "globalmem1"); 
    printk(KERN_NOTICE, "globalmem1initialized!"); 
 
  return 0;
 
  fail_malloc: unregister_chrdev_region(devno,2);
  return result;
}
 
/*模块卸载函数*/
void globalmem_exit(void)
{
  cdev_del(&(globalmem_devp[0].cdev));  
  cdev_del(&(globalmem_devp[1].cdev));   /*注销cdev*/
  kfree(globalmem_devp);     /*释放设备结构体内存*/
 unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /*释放设备号*/
    class_destroy(globalmem0_class); 
    class_destroy(globalmem1_class); 
}
 
MODULE_AUTHOR("SongBaohua");
MODULE_LICENSE("DualBSD/GPL");
 
module_param(globalmem_major,int, S_IRUGO);
 
module_init(globalmem_init);
module_exit(globalmem_exit);

测试程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include<linux/fcntl.h>
 
int main()
{
 int fd;
 char buf1[]="testglobalmen data!";//写入memdev设备的内容
 char buf_read[4096]; //memdev设备的内容读入到该buf中
 
 if((fd=open("/dev/globalmem0",O_RDWR))==-1)//打开memdev设备
{
  printf("open globalmem0 WRONG!\n");
return 0;
}
 
int count = 0;
while(count<=100)
{
printf("openglobalmem0 SUCCESS!\n");
 printf("buf is %s\n",buf1);
 write(fd,buf1,sizeof(buf1));//把buf中的内容写入memdev设备
 lseek(fd,0,SEEK_SET); //把文件指针重新定位到文件开始的位置
 read(fd,buf_read,sizeof(buf1));//把memdev设备中的内容读入到buf_read中
 printf("buf_read is %s\n",buf_read);
count++;
}
 
 close(fd);
 return 0;
}
 


分析:

一.MKDEV

/linux/kdev_t.h
#define _LINUX_KDEV_T_H
   3#ifdef __KERNEL__
   4#define MINORBITS       20
   5#define MINORMASK       ((1U << MINORBITS) - 1)
   6
   7#define MAJOR(dev)      ((unsigned int) ((dev) >>MINORBITS))
   8#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
   9#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

当编译内核的时候,__KERNEL__在命令行被定义

When you compile your kernel,__KERNEL__isdefined on the command line.

User-space programs need access tothe kernel headers, but some of the info in kernel headers is intended only forthe kernel. Wrapping some statements in an#ifdef __KERNEL__/#endifblockensures that user-space programs don't see those statements.

主设备号高12位,次设备号低20位

二.kmalloc和vmalloc的区别

1.kmalloc

1>kmalloc内存分配和malloc相似,除非被阻塞否则他执行的速度非常快,而且不对获得空间清零.在用kmalloc申请函数后,要对起清零.用memset()函数对申请的内存进行清零。

2>kamlloc函数原型:

#include<linux/slab.h>

Void *kmalloc(size_t size, intflags);

(1)第一个参数是要分配的块的大小

(2)第二个参数是分配标志(flags),他提供了多种kmalloc的行为。

(3)第三个最常用的GFP_KERNEL;

A.表示内存分配(最终总是调用get_free_pages来实现实际的分配;这就是GFP前缀的由来)是代表运行在内核空间的进程执行的。使用GFP_KERNEL容许kmalloc在分配空闲内存时候如果内存不足容许把当前进程睡眠以等待。因此这时分配函数必须是可重入的。如果在进程上下文之外如:中断处理程序、tasklet以及内核定时器中这种情况下current进程不该睡眠,驱动程序该使用GFP_ATOMIC.

B.GFP_ATOMIC

用来从中断处理和进程上下文之外的其他代码中分配内存.从不睡眠.

C.GFP_KERNEL

内核内存的正常分配.可能睡眠.

D.GFP_USER

用来为用户空间页来分配内存;它可能睡眠.

E.GFP_HIGHUSER

如同GFP_USER,但是从高端内存分配,如果有.高端内存在下一个子节描述.

F.GFP_NOFS,GFP_NOIO

这个标志功能如同GFP_KERNEL,但是它们增加限制到内核能做的来满足请求.一个GFP_NOFS分配不允许进行任何文件系统调用,而GFP_NOIO根本不允许任何I/O初始化.它们主要地用在文件系统和虚拟内存代码,那里允许一个分配睡眠,但是递归的文件系统调用会是一个坏注意.

上面列出的这些分配标志可以是下列标志的相或来作为参数,这些标志改变这些分配如何进行:

__GFP_DMA

这个标志要求分配在能够DMA的内存区.确切的含义是平台依赖的并且在下面章节来解释.

__GFP_HIGHMEM

这个标志指示分配的内存可以位于高端内存.

__GFP_COLD

正常地,内存分配器尽力返回"缓冲热"的页--可能在处理器缓冲中找到的页.相反,这个标志请求一个"冷"页,它在一段时间没被使用.它对分配页作DMA读是有用的,此时在处理器缓冲中出现是无用的.一个完整的对如何分配DMA缓存的讨论看"直接内存存取"一节在第1章.

__GFP_NOWARN

这个很少用到的标志阻止内核来发出警告(使用printk),当一个分配无法满足.

__GFP_HIGH

这个标志标识了一个高优先级请求,它被允许来消耗甚至被内核保留给紧急状况的最后的内存页.

Ø__GFP_REPEAT

__GFP_NOFAIL

__GFP_NORETRY

这些标志修改分配器如何动作,当它有困难满足一个分配. __GFP_REPEAT意思是"更尽力些尝试"通过重复尝试--但是分配可能仍然失败. __GFP_NOFAIL标志告诉分配器不要失败;它尽最大努力来满足要求.使用__GFP_NOFAIL是强烈不推荐的;可能从不会有有效的理由在一个设备驱动中使用它.最后, __GFP_NORETRY告知分配器立即放弃如果得不到请求的内存.

Ø内存区段

__GFP_DMA和__GFP_HIGHMEM的使用与平台相关,Linux把内存分成3个区段:可用于DMA的内存、常规内存、以及高端内存。X86平台上ISA设备DMA区段是内存的前16MB,而PCI设备无此限制。

内存区后面的机制在mm/page_alloc.c中实现,而内存区的初始化在平台特定的文件中,常常在arch目录树的mm/init.c。

3>kamlloc的使用方法:

Linux处理内存分配通过创建一套固定大小的内存对象池.分配请求被这样来处理,进入一个持有足够大的对象的池子并且将整个内存块递交给请求者.驱动开发者应当记住的一件事情是,内核只能分配某些预定义的,固定大小的字节数组.

如果你请求一个任意数量内存,你可能得到稍微多于你请求的,至多是2倍数量.同样,程序员应当记住kmalloc能够处理的最小分配是32或者64字节,依赖系统的体系所使用的页大小. kmalloc能够分配的内存块的大小有一个上限.这个限制随着体系和内核配置选项而变化.如果你的代码是要完全可移植,它不能指望可以分配任何大于128 KB.如果你需要多于几个KB,但是,有个比kmalloc更好的方法来获得内存。在设备驱动程序或者内核模块中动态开辟内存,不是用malloc,而是kmalloc ,vmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,vfree,或free_pages. kmalloc函数返回的是虚拟地址(线性地址). kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要.而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA.

注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。kmalloc用法参见khg.

内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址。

另外,很多硬件需要一块比较大的连续内存用作DMA传送。这块内存需要一直驻留在内存,不能被交换到文件中去。但是kmalloc最多只能开辟大小为32XPAGE_SIZE的内存,一般的PAGE_SIZE=4kB,也就是128kB的大小的内存。

3.kmalloc和vmalloc的区别

•vmalloc()与kmalloc()都可用于分配内存

•kmalloc()分配的内存处于3GB~high_memory之间,这段内核空间与物理内存的映射一一对应

•vmalloc()分配的内存在VMALLOC_START~4GB之间,这段非连续内存区映射到物理内存也可能是非连续的

•在内核空间中调用kmalloc()分配连续物理空间,而调用vmalloc()分配非物理连续空间。

•把kmalloc()所分配内核空间中的地址称为内核逻辑地址

•把vmalloc()分配的内核空间中的地址称为内核虚拟地址

•vmalloc()在分配过程中须更新内核页表

总结:

1.kmalloc和vmalloc分配的是内核的内存,malloc分配的是用户的内存

2.kmalloc保证分配的内存在物理上是连续的,kmalloc()分配的内存在0xBFFFFFFF-0xFFFFFFFF以上的内存中,driver一般是用它来完成对DS的分配,更适合于类似设备驱动的程序来使用;

3.vmalloc保证的是在虚拟地址空间上的连续,vmalloc()则是位于物理地址非连续,虚地址连续区,起始位置由VMALLOL_START来决定,一般作为交换区、模块的分配。

3.kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大(因为vmalloc还可以处理交换空间)。

4.内存只有在要被DMA访问的时候才需要物理上连续,vmalloc比kmalloc要慢

5.vmalloc使用的正确场合是分配一大块,连续的,只在软件中存在的,用于缓冲的内存区域。不能在微处理器之外使用。

6.vmalloc中调用了kmalloc(GFP—KERNEL),因此也不能应用于原子上下文。

7.kmalloc和kfree管理内核段内分配的内存,这是真实地址已知的实际物理内存块。

8.vmalloc对应于vfree,分配连续的虚拟内存,但是物理上不一定连续。

9.kmalloc分配内存是基于slab,因此slab的一些特性包括着色,对齐等都具备,性能较好。物理地址和逻辑地址都是连续的

三.module_param

1.为什么引入

在用户态下编程可以通过main()来传递命令行参数,而编写一个内核模块则可通过module_param()来传递命令行参数.

2.module_param宏是Linux 2.6内核中新增的,该宏被定义在include/linux/moduleparam.h文件中,具体定义如下:

 /* Helper functions: type is byte, short, ushort, int, uint, long,
   ulong,charp, bool or invbool, or XXX if you define param_get_XXX,
  param_set_XXX and param_check_XXX.
    */
  #define module_param_named(name, value, type,perm)               
   param_check_##type(name,&(value));                   
   module_param_call(name, param_set_##type, param_get_##type, &value,perm); 
   __MODULE_PARM_TYPE(name, #type)
   #define module_param(name, type,perm)                
   module_param_named(name, name, type, perm)

由此可知module_param的实现是通过module_param_named(name, name, type, perm)的。

3.module_param使用了3个参数:变量名,它的类型,以及一个权限掩码用来做一个辅助的sysfs入口。

这个宏定义应当放在任何函数之外,典型地是出现在源文件的前面。

eg:static char *whom="world"

static int tige=1;

module_param(tiger,int,S_IRUGO);

module_param(whom,charp,S_IRUGO);

4.模块参数支持许多类型:

bool

invbool

一个布尔型(true 或者 false)值(相关的变量应当是 int 类型). invbool 类型颠倒了值, 所以真值变成 false, 反之亦然.

charp:一个字符指针值. 内存为用户提供的字串分配, 指针因此设置.

int

long

short

uint

ulong

ushort

基本的变长整型值.以 u 开头的是无符号值.

5.数组参数,用逗号间隔的列表提供的值, 模块加载者也支持。

声明一个数组参数,使用:

module_param_array(name,type,num,perm);

这里 name是你的数组的名子(也是参数名),

type是数组元素的类型,

num是一个整型变量,

perm是通常的权限值.

如果数组参数在加载时设置,num 被设置成提供的数的个数. 模块加载者拒绝比数组能放下的多的值.

Tiger-John说明:

perm参数的作用是什么?

最后的module_param 字段是一个权限值,表示此参数在sysfs文件系统中所对应的文件节点的属性。你应当使用 <linux/stat.h>中定义的值. 这个值控制谁可以存取这些模块参数在 sysfs 中的表示.当perm为0时,表示此参数不存在 sysfs文件系统下对应的文件节点。 否则,模块被加载后,在/sys/module/ 目录下将出现以此模块名命名的目录, 带有给定的权限.。

权限在include/linux/stat.h中有定义

比如:

#defineS_IRWXU 00700

#defineS_IRUSR 00400

#defineS_IWUSR 00200

#defineS_IXUSR 00100

#defineS_IRWXG 00070

#defineS_IRGRP 00040

#defineS_IWGRP 00020

#defineS_IXGRP 00010

#defineS_IRWXO 00007

#defineS_IROTH 00004

#defineS_IWOTH 00002

#defineS_IXOTH 00001

使用S_IRUGO 参数可以被所有人读取, 但是不能改变; S_IRUGO|S_IWUSR 允许 root 来改变参数. 注意, 如果一个参数被 sysfs修改, 你的模块看到的参数值也改变了, 但是你的模块没有任何其他的通知. 你应当不要使模块参数可写, 除非你准备好检测这个改变并且因而作出反应.

四.file文件结构

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_tf_mode;

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

loff_tf_pos;

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

unsignedint f_flags;

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

structfile_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 是一个有用的资源, 在系统调用间保留状态信息,我们大部分例子模块都使用它.

structdentry *f_dentry;

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

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

五.inode结构

inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的.可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构.

inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:

dev_ti_rdev;

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

structcdev *i_cdev;

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

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

unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);

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

六.container_of()

container_of()的作用是通过结构体成员的指针找到对应

结构体的指针,这个技巧在Linux内核编程中十分常用。在container_of

(inode->i_cdev,structglobalmem_dev,cdev)语句中,传给container_of()的第1个参数是结

构体成员的指针,第2个参数为整个结构体的类型,第3 个参数为传入的第1 个参数即

结构体成员的类型,container_of()返回值为整个结构体的指针。


七.ioctl函数

首先说明在2.6.36以后ioctl函数已经不再存在了,而是用unlocked_ioctl和compat_ioctl两个函数实现以前版本的ioctl函数。同时在参数方面也发生了一定程度的改变,去除了原来ioctl中的struct inode参数,同时改变了返回值。

但是驱动设计过程中存在的问题变化并不是很大,同样在应用程序设计中我们还是采用ioctl实现访问,而并不是unlocked_ioctl函数,因此我们还可以称之为ioctl函数的实现。

ioctl函数的实现主要是用来实现具体的硬件控制,采用相应的命令控制硬件的具体操作,这样就能使得硬件的操作不再是单调的读写操作。使得硬件的使用更加的方便。

ioctl函数实现主要包括两个部分,首先是命令的定义,然后才是ioctl函数的实现,命令的定义是采用一定的规则。

ioctl的命令主要用于应用程序通过该命令操作具体的硬件设备,实现具体的操作,在驱动中主要是对命令进行解析,通过switch-case语句实现不同命令的控制,进而实现不同的硬件操作。

    ioctl函数的命令定义方法:

    int (*unlocked_ioctl)(struct file*filp,unsigned int cmd,unsigned long arg)

    虽然其中没有指针的参数,但是通常采用arg传递指针参数。cmd是一个命令。每一个命令由一个整形数据构成(32bits),将一个命令分成四部分,每一部分实现具体的配置,设备类型(幻数)8bits,方向2bits,序号8bits,数据大小13/14bits。命令的实现实质上就是通过简单的移位操作,将各个部分组合起来而已。

    一个命令的分布的大概情况如下:

    |---方向位(31-30)|----数据长度(29-16)----------------|---------设备类型(15-8)------|----------序号(7-0)----------|

    |----------------------------------------------------------------------------------------------------------------------------------------|

    其中方向位主要是表示对设备的操作,比如读设备,写设备等操作以及读写设备等都具有一定的方向,2个bits只有4种方向。

    数据长度表示每一次操作(读、写)数据的大小,一般而已每一个命令对应的数据大小都是一个固定的值,不会经常改变,14bits说明可以选择的数据长度最大为16k。

    设备类型类似于主设备号(由于8bits,刚好组成一个字节,因此经常采用字符作为幻数,表示某一类设备的命令),用来区别不同的命令类型,也就是特定的设备类型对应特定的设备。序号主要是这一类命令中的具体某一个,类似于次设备号(256个命令),也就是一个设备支持的命令多达256个。

    同时在内核中也存在具体的宏用来定义命令以及解析命令。

    但是大部分的宏都只是定义具体的方向,其他的都需要设计者定义。

    主要的宏如下:

#include<linux/ioctl.h>
_IO(type,nr)                    表示定义一个没有方向的命令,
_IOR(type,nr,size)            表示定义一个类型为type,序号为nr,数据大小为size的读命令
_IOW(type,nr,size)           表示定义一个类型为type,序号为nr,数据大小为size的写命令
_IOWR(type,nr,size)         表示定义一个类型为type,序号为nr,数据大小为size的写读命令

通常的type可采用某一个字母或者数字作为设备命令类型。

是实际运用中通常采用如下的方法定义一个具体的命令:

//头文件
#include<linux/ioctl.h>
/*定义一系列的命令*/
/*幻数,主要用于表示类型*/
#define MAGIC_NUM 'k'
/*打印命令*/
#define MEMDEV_PRINTF _IO(MAGIC_NUM,1)
/*从设备读一个int数据*/
#define MEMDEV_READ _IOR(MAGIC_NUM,2,int)
/*往设备写一个int数据*/
#define MEMDEV_WRITE _IOW(MAGIC_NUM,3,int)
/*最大的序列号*/
#define MEM_MAX_CMD 3

    还有对命令进行解析的宏,用来确定具体命令的四个部分(方向,大小,类型,序号)具体如下所示:

    /*确定命令的方向*/
    _IOC_DIR(nr)                    
    /*确定命令的类型*/
    _IOC_TYPE(nr)                     
    /*确定命令的序号*/
    _IOC_NR(nr)                           
    /*确定命令的大小*/
    _IOC_SIZE(nr)    

      上面的几个宏可以用来命令,实现命令正确性的检查。

      ioctl的实现过程主要包括如下的过程:

      1、命令的检测

      2、指针参数的检测

      3、命令的控制switch-case语句

      1、命令的检测主要包括类型的检查,数据大小,序号的检测,通过结合上面的命令解析宏可以快速的确定。

              /*检查类型,幻数是否正确*/
              if(_IOC_TYPE(cmd)!=MAGIC_NUM)
                      return -EINVAL;
              /*检测命令序号是否大于允许的最大序号*/
              if(_IOC_NR(cmd)> MEM_MAX_CMD)
                      return -EINVAL;

        2、主要是指针参数的检测。指针参数主要是因为内核空间和用户空间的差异性导致的,因此需要来自用户空间指针的有效性。使用copy_from_user,copy_to_user,get_user,put_user之类的函数时,由于函数会实现指针参量的检测,因此可以省略,但是采用__get_user(),__put_user()之类的函数时一定要进行检测。具体的检测方法如下所示:

        if(_IOC_DIR(cmd) & _IOC_READ)
                err = !access_ok(VERIFY_WRITE,(void *)args,_IOC_SIZE(cmd));
        else if(_IOC_DIR(cmd) & _IOC_WRITE)
                err = !access_ok(VERIFY_READ,(void *)args,_IOC_SIZE(cmd));
        if(err)/*返回错误*/
                return -EFAULT;

          当方向是读时,说明是从设备读数据到用户空间,因此要检测用户空间的指针是否可写,采用VERIFY_WRITE,而当方向是写时,说明是往设备中写数据,因此需要检测用户空间中的指针的可读性VERIFY_READ。检查通常采用access_ok()实现检测,第一个参数为读写,第二个为检测的指针,第三个为数据的大小。

          3、命名的控制:

          命令的控制主要是采用switch和case相结合实现的,这于window编程中的检测各种消息的实现方式是相同的。

          /*根据命令执行相应的操作*/
                  switch(cmd)
                  {
                          case MEMDEV_PRINTF:
                                  printk("<--------CMD MEMDEV_PRINTF Done------------>\n\n");
                                  ...
                                  break;
                          case MEMDEV_READ:
                                  ioarg = &mem_devp->data;
                                  ...
                                  ret = __put_user(ioarg,(int *)args);
                                  ioarg = 0;
                                  ...
                                  break;
                          case MEMDEV_WRITE:
                                  ...
                                  ret = __get_user(ioarg,(int *)args);
                                  printk("<--------CMD MEMDEV_WRITE Done ioarg = %d--------->\n\n",ioarg); 
                                  ioarg = 0;
                                  ...
                                  break;
                          default:
                                  ret = -EINVAL;
                                  printk("<-------INVAL CMD--------->\n\n");
                                  break;
                  }

            这只是基本的框架结构,实际中根据具体的情况进行修改。这样就实现了基本的命令控制。

            文件操作支持的集合如下:

            /*添加该模块的基本文件操作支持*/
            static const struct file_operations mem_fops =
            {
                    /*结尾不是分号,注意其中的差别*/
                    .owner = THIS_MODULE,
                    .llseek = mem_llseek,
                    .read = mem_read,
                    .write = mem_write,
                    .open = mem_open,
                    .release = mem_release,
                    /*添加新的操作支持*/
                    .unlocked_ioctl = mem_ioctl,
            };

              需要注意不是ioctl,而是unlocked_ioctl。

              参考链接:

              http://blog.csdn.net/tigerjb/article/details/6412881

              http://blog.csdn.net/lixuyuan/article/details/6246893

              http://blog.chinaunix.net/space.php?uid=20937170&do=blog&id=3033633

              分享到:
              评论

              相关推荐

              Global site tag (gtag.js) - Google Analytics