设备驱动
在Linux的设备驱动编程中,都需要遵从一种内核编程的模式,这也可以理解为一种设计模式(Design pattern)【1】,这种模式让硬件programmer脱离硬件平台,开始和操作系统打交道,就像业务和逻辑分离一样,提高了代码的可复用性。
但是这样依然没有说清楚到底Linux用户是如何使用驱动程序的,Linux系统上有三类设备:
字符驱动设备(character device)
块设备(block device)
网络设备(network device)
它们(网络设备除外)的访问都是用户通过设备文件(或者叫设备节点)来进行访问而使用的。
内存如何模拟设备
下面来分析一个简单的字符设备驱动,实现在一段内存中模拟字符设备的读和写操作
首先定义一个结构体来描述一段内存的具体信息。因为外设的访问是操作系统通过物理地址进行访问的,因而我们可以把这段内存看作是一个字符设备。
view sourceprint?1 struct mem_dev
2 {
3 char *data;
4 unsigned long size;
5 };
data是指向内存起始地址的指针,size是内存的大小。因此把这样一个封装可以看作一个设备。
字符设备的统一描述
在Linux2.6的内核中,字符设备使用结构体struct cdev进行描述。它定义在/include/linux/cdev.h中:
在这个结构体中会发现另外一个嵌套的结构体struct file_operations,它是字符设备把驱动的操作和设备联系在一起的纽带。是一个函数指针的集合,每个被打开的文件都对应于一系列的操作。它定义在/include/linux/fs.h中:
设备的注册
字符设备经过描述之后,相当于规定了它的资源,逻辑和实现。下一步就是在内核中进行注册,相当于在体制中给一个固定的编制。设备的注册分为三部分:
分配cdev
初始化cdev
添加cdev
设备可以通过静态和动态分配两种方式进行分配。下面是实现代码:
view sourceprint?1 dev_t devno = MKDEV(mem_major, 0);//MKDEV是将主设备号和次设备号转换为dev_t类型数据
2 /* 静态申请设备号*/
3 if (mem_major)
4 result = register_chrdev_region(devno, 2, "memdev");//devno为主设备号,共申请两个连续的设备
5 else /* 动态分配设备号 */
6 {
7 result = alloc_chrdev_region(&devno, 0, 2, "memdev");//&devno作为一个输出参数
8 mem_major = MAJOR(devno);//获取动态分配到的主设备号。
9 }
mem_major是在宏定义中自己选的一个设备号,定为254,所以下一步else不会执行。但当自己选定的设备号已经被使用时就会产生冲突。这时就可以使用动态分配的方式获得设备号。
设备号分配之后就是对cdev进行初始化:
view sourceprint?1 cdev_init(&cdev, &mem_fops);
2 cdev.owner = THIS_MODULE;//驱动引用计数,作用是这个驱动正在使用的时候,你再次用inmod命令时,出现警告提示
3 cdev.ops = &mem_fops;
cdev_init(&cdev, &mem_fops)是初始化cdev结构,将结构体cdev和mem_fops绑定起来。cdev.ops = &mem_fops是对cdev结构体成员进行赋值。其中mem_fops是用file_operations定义描述的文件操作结构体:
view sourceprint?1 static const struct file_operations mem_fops =
2 {
3 .owner = THIS_MODULE,
4 .llseek = mem_llseek,
5 .read = mem_read,
6 .write = mem_write,
7 .open = mem_open,
8 .release = mem_release,
9 };
最后是注册字符设备:
view sourceprint?1 cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);//MEMDEV_NR_DEVS=2,分配2个设备
这样三步就完成了字符设备的注册。
再看一下mem_read和mem_write的实现过程:
view sourceprint?01 /*读函数*/
02 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)//buf缓存区
03 {
04 unsigned long p = *ppos;//p为当前读写位置
05 unsigned int count = size;//一次读取的大小
06 int ret = 0;
07 struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
08 /*判断读位置是否有效*/
09 if (p >= MEMDEV_SIZE)//是否超出读取获围
10 return 0;
11 if (count > MEMDEV_SIZE - p)
12 count = MEMDEV_SIZE - p;//count大于可读取的范围,则缩小读取范围。
13 /*读数据到用户空间*/
14 if (copy_to_user(buf, (void*)(dev->data + p), count))//返回buf,读取位置,读取数量
15 {
16 ret = - EFAULT;
17 }
18 else
19 {
20 *ppos += count;//将文件当前位置向后移
21 ret = count;//返回实际读取字节数
22 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
23 }
24 return ret;//返回实际读取字节数,判断读取是否成功
25 }
26
27
28 /*写函数*/
29 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)//write和read类似,直接参考read
30 {
31 unsigned long p = *ppos;
32 unsigned int count = size;
33 int ret = 0;
34 struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
35 /*分析和获取有效的写长度*/
36 if (p >= MEMDEV_SIZE)
37 return 0;
38 if (count > MEMDEV_SIZE - p)
39 count = MEMDEV_SIZE - p;
40 /*从用户空间写入数据*/
41 if (copy_from_user(dev->data + p, buf, count))
42 ret = - EFAULT;
43 else
44 {
45 *ppos += count;
46 ret = count;
47 printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
48 }
49 return ret;
50 }
下面是字符设备的测试程序:
view sourceprint?01 #include <stdio.h>
02 int main()
03 {
04 FILE *fp0 = NULL;
05 char Buf[4096];
06 /*初始化内核空间的Buf*/
07 strcpy(Buf,"Mem is char dev!");
08 printf("BUF: %s\n",Buf);
09
10 /*打开设备文件*/
11 fp0 = fopen("/dev/memdev0","r+");
12 if (fp0 == NULL)
13 {
14 printf("Open Memdev0 Error!\n");
15 return -1;
16 }
17
18 /*写入设备:用户空间——>字符设备(即那一段内存中)*/
19 fwrite(Buf, sizeof(Buf), 1, fp0);//这个fwrite和.read = mem_read的关系?
20
21 /*重新定位文件位置(思考没有该指令,会有何后果) 【2】*/
22 fseek(fp0,0,SEEK_SET);//调用mem_llseek()定位
23
24 /*清除Buf*/
25 strcpy(Buf,"Buf is NULL!");
26 printf("BUF: %s\n",Buf);
27
28 /*读出设备字符设备(即那一段内存中)——>用户空间*/
29 fread(Buf, sizeof(Buf), 1, fp0);
30 /*检测结果*/
31 printf("BUF: %s\n",Buf);
32 return 0;