内存模拟设备进行驱动编程

设备驱动

在Linux的设备驱动编程中,都需要遵从一种内核编程的模式,这也可以理解为一种设计模式(Design pattern)【1】,这种模式让硬件programmer脱离硬件平台,开始和操作系统打交道,就像业务和逻辑分离一样,提高了代码的可复用性。

但是这样依然没有说清楚到底Linux用户是如何使用驱动程序的,Linux系统上有三类设备:

  • 字符驱动设备(character device)
  • 块设备(block device)
  • 网络设备(network device)

它们(网络设备除外)的访问都是用户通过设备文件(或者叫设备节点)来进行访问而使用的。

内存如何模拟设备

下面来分析一个简单的字符设备驱动,实现在一段内存中模拟字符设备的读和写操作
首先定义一个结构体来描述一段内存的具体信息。因为外设的访问是操作系统通过物理地址进行访问的,因而我们可以把这段内存看作是一个字符设备。

<!-- lang: cpp -->
struct mem_dev                                     
{                                                        
char *data;                 
unsigned long size;  
};

data是指向内存起始地址的指针,size是内存的大小。因此把这样一个封装可以看作一个设备。

字符设备的统一描述

在Linux2.6的内核中,字符设备使用结构体struct cdev进行描述。它定义在/include/linux/cdev.h中:

内存模拟设备进行驱动编程_第1张图片

在这个结构体中会发现另外一个嵌套的结构体struct file_operations,它是字符设备把驱动的操作和设备联系在一起的纽带。是一个函数指针的集合,每个被打开的文件都对应于一系列的操作。它定义在/include/linux/fs.h中:

内存模拟设备进行驱动编程_第2张图片

设备的注册

字符设备经过描述之后,相当于规定了它的资源,逻辑和实现。下一步就是在内核中进行注册,相当于在体制中给一个固定的编制。设备的注册分为三部分:

  1. 分配cdev
  2. 初始化cdev
  3. 添加cdev

设备可以通过静态和动态分配两种方式进行分配。下面是实现代码:

<!-- lang: cpp -->
dev_t devno = MKDEV(mem_major, 0);//MKDEV是将主设备号和次设备号转换为dev_t类型数据
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");//devno为主设备号,共申请两个连续的设备
else  /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");//&devno作为一个输出参数
mem_major = MAJOR(devno);//获取动态分配到的主设备号。
}  

mem_major是在宏定义中自己选的一个设备号,定为254,所以下一步else不会执行。但当自己选定的设备号已经被使用时就会产生冲突。这时就可以使用动态分配的方式获得设备号。

设备号分配之后就是对cdev进行初始化:

<!-- lang: cpp -->
 cdev_init(&cdev, &mem_fops);
 cdev.owner = THIS_MODULE;//驱动引用计数,作用是这个驱动正在使用的时候,你再次用inmod命令时,出现警告提示
 cdev.ops = &mem_fops;

cdev_init(&cdev, &mem_fops)是初始化cdev结构,将结构体cdev和mem_fops绑定起来。cdev.ops = &mem_fops是对cdev结构体成员进行赋值。其中mem_fops是用file_operations定义描述的文件操作结构体:

<!-- lang: cpp -->
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,
};

最后是注册字符设备:

<!-- lang: cpp -->
 cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);//MEMDEV_NR_DEVS=2,分配2个设备

这样三步就完成了字符设备的注册。

再看一下mem_read和mem_write的实现过程:

<!-- lang: cpp -->
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)//buf缓存区
{
unsigned long p =  *ppos;//p为当前读写位置
unsigned int count = size;//一次读取的大小
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE)//是否超出读取获围
return 0;
if (count > MEMDEV_SIZE - p)   //【2】
count = MEMDEV_SIZE - p;//count大于可读取的范围,则缩小读取范围。
/*读数据到用户空间*/
if (copy_to_user(buf, (void*)(dev->data + p), count))//返回buf,读取位置,读取数量
{
ret =  - EFAULT;
}
else
{
*ppos += count;//将文件当前位置向后移
 ret = count;//返回实际读取字节数
 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
 }
 return ret;//返回实际读取字节数,判断读取是否成功
 }


 /*写函数*/
 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)//write和read类似,直接参考read
{
unsigned long p =  *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret =  - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}

下面是字符设备的测试程序:

<!-- lang: cpp -->
#include <stdio.h>
int main()
{
 FILE *fp0 = NULL;
 char Buf[4096];
  /*初始化内核空间的Buf*/
 strcpy(Buf,"Mem is char dev!");
 printf("BUF: %s\n",Buf);

 /*打开设备文件*/
 fp0 = fopen("/dev/memdev0","r+");
 if (fp0 == NULL)
 {
  printf("Open Memdev0 Error!\n");
  return -1;
 }

 /*写入设备:用户空间——>字符设备(即那一段内存中)*/
 fwrite(Buf, sizeof(Buf), 1, fp0);//这个fwrite和.read = mem_read的关系?

 /*重新定位文件位置(思考没有该指令,会有何后果)  【3】*/
 fseek(fp0,0,SEEK_SET);//调用mem_llseek()定位

 /*清除Buf*/
 strcpy(Buf,"Buf is NULL!");
 printf("BUF: %s\n",Buf);

  /*读出设备字符设备(即那一段内存中)——>用户空间*/
 fread(Buf, sizeof(Buf), 1, fp0);
  /*检测结果*/
 printf("BUF: %s\n",Buf);
  return 0; 
 }

Reference

[1].http://en.wikipedia.org/wiki/Design_pattern
[2].C Traps and Pitfalls。 Page51 边界计算与不对称边界
[3].C Traps and Pitfalls。 Page85-86 更新顺序文件

你可能感兴趣的:(内存模拟设备进行驱动编程)