字符设备驱动

驱动分类

——字符设备驱动

       字符设备:字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open,close,read,write系统调用

——网络接口驱动

       网络接口:任何网络事务都通过一个接口来进行,一个接口通常是一个硬件设备(eth0),但是它也可以是一个纯粹的软件设备,比如回环接口(lo)。一个网络接口负责发送和接收数据报文。

——块设备驱动

       块设备:——在大部分的unix系统,块设备不能按字节处理数据,只能一次传送一个活多个长度是512字节(或一个更大的2次幂的数)的整块数据

                     ——而linux则允许块设备传送任意数目的字节。因此,块和字符设备的区别仅仅是驱动的与内核的接口不同。


字符设备与块设备的区别:块设备是可以进行随机访问的。而字符设备不能。在linux系统中,块设备也可以进行字节访问

 

驱动程序安装

——模块方式

——直接编译进内核:修改Kconfig、修改Makefile,即可。

                                   将要编译进内核的代码(比如hello.c)cp进内核源码树的/kernel/drivers/char。在char目录下改写Kconfig。然后再make menuconfig的时候便能看见hello world项(Kconfig是用来在menuconfig中增加菜单的,menuconfig配置后的结果保存在.config中);再修改/char目录下的Makefile添加obj-$(CONFIG_HELLO_WORLD)       +=hello.o(Makefile根据配置去选择CONFIG_HELLO_WORLD的值)。如此之后便能编译内核了(进入源码树编译)。编译好的内核位于arch/arm/boot/uImage


A:linux用户程序通过设备文件(又名:设备节点)来使用驱动程序操作字符设备和块设备

Q:设备(字符、块)文件在何处?——在/dev/目录下

 

字符设备驱动程序设计

主次设备号

字符设备通过字符设备文件来存取。字符设备文件由使用ls –l的输出的第一列的“c”标识。如果使用ls –l命令,会看到在设备文件项中有2个数(由一个逗号分隔)这些数字就是设备文件的主次设备编号(举例说明,进入/dev/目录,ls –l)

 

Q:内核中如何描述设备号?

A:dev_t   其实质为unsigned int 32位整数,其中高12位(4K)为主设备号,低20位(64K)为次设备号

 

Q:如何从dev_t中分解出主设备号?

A:MAJOR(dev_t dev)

 

Q:如何从dev_t中分解出此设备号?
A:MINOR(dev_t dev)

设备号

每个设备文件对应有自己的设备号

驱动程序也有自己的设备号

如果两者的设备号对应相同,那么设备文件便和设备驱动建立关联


设备号作用

——主设备号用来标识与设备文件相连的驱动程序。次编号被驱动程序用来辨别操作的是哪个设备

 

       主设备号用来反映设备类型

       此设备号用来区分同类型的设备

 

 

分配主设备号

Linux内核如何给设备分配主设备号?
——静态申请和动态分配两种方法

 

静态申请

——方法:1.根据documentation/devices.txt,确定一个没有使用的主设备号

                2.使用register_chrdev_region函数注册设备号

——优点:简单

——缺点:一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

       Intregister_chrdev_region(dev_t from, unsigned count, const char *name)

功能——申请使用从from开始的count个设备号(主设备号不变,次设备号增加)

参数——from:希望申请使用的设备号

       ——count:希望申请使用设备号数目

       ——name:设备名(体现在/proc/devices)

 

动态分配(让内核自动来分)

——方法:使用alloc_chrdev_region分配设备号

——有点:简单,易于驱动推广(因为内核知道哪些驱动有没使用)

——缺点:无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)

——解决办法:安装驱动后,从/proc/devices中查询设备号

 

       Intalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char*name)

——功能:请求内核动态分配count个设备号,且次设备号从baseminor开始

——参数:dev:分配到的设备号

                Baseminor:起始设备号

                Count:需要分配的设备号数目

                Name:设备名(体现在/proc/devices)

 

注销设备号

不论使用何种方法分配设备号,都应该在不再使用它们时释放这些设备号

       Voidunregister_chrdev_region(dev_t from, unsigned count)

——功能:释放从from开始的count个设备号

 

创建设备文件

2种方法:

——1.使用mknod命令手工创建

 

Mknod用法:       mknod filename typemajor minor

——filename:设备文件名

——type:设备文件类型(b/c)

——major:主设备号

——minor:次设备号

例:       mknod serial0 c 100 0

 

——2.自动创建

 

重要结构

在linux字符设备驱动程序设计中,有3种非常重要的数据结构:struct file ; struct inode ; struct file_operations

 

Structfile

代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,在文件关闭后释放。(如果有3个程序打开同一个文件,那么也有3个struct file)

——重要成员:loff_tf_pos //文件读写位置,loff_t其实是个整形

                       Struct file_operations *f_op

 

Structinode

用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构

——重要成员:dev_ti_rdev:设备号

 

Structfile_operations

一个函数指针的集合(更像是一个对应关系表,把应用程序中对文件的操作转化为驱动程序中相应的函数),定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL

例:              mem_fops

       Structfile_operations mem_fops={

       .owner= THIS_MODULE;

       .llseek= mem_seek;

       .read= mem_read;

       .write= mem_write;

       .ioctl= mem_ioctl;

       .open= mem_open;

       .release= mem_release;

       };

 

内核代码导读

 

设备注册

在linux2.6内核中,字符设备使用struct cdev来描述

字符设备的注册可分为如下3个步骤:

1.分配cdev

              Structcdev的分配可使用cdev_alloc函数来完成

              Structcdev *cdev_alloc(void)

——2.初始化cdev

              Structcdev的初始化使用cdev_init函数来完成

              Voidcdev_init(struct cdev*cdev, const structfile_operations *fops)

              ——参数:cdev:待初始化的cdev结构

                            :fops:设备对应的操作函数集

——3.添加cdev

              Structcdev的注册使用cdev_add函数来完成

              Intcdev_add(struct cdev *p, dev_t dev, unsigned count)

              ——参数:p:待添加到内核的字符设备结构

                              dev:设备号

                              count:添加的设备个数、

 

设备操作实现

完成了驱动程序的注册,下一步该做什么呢?——实现设备所支持的操作(即是file_operations中的函数指针集)

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

在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。如果该项为NULL,设备的打开操作永远成功。Open这个函数指针名可以改,比如改为上述mem_fops中的mem_open,但是其参数的类型是固定的,不能更改的。下同。

——void (*release)(struct inode *, struct file *)

当设备文件被关闭时调用这个操作。与open相仿,release也可以没有

——ssize_t (*read)(struct file*, char __user *, size_tloff_t)

从设备中读取数据

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

向设备发送数据

——unsigned int (*poll)(struct file *, structpoll_table_struct *)

对应select系统调用

——int (*ioctl)(struct inode *, struct file *, unsignedint , unsigned long)

控制设备

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

将设备映射到进程虚拟地址空间中

——off_t (*llseek)(struct file *, loff_t, int)

修改文件的当前读写位置,并将新位置作为返回值

——参数:要操作的文件,移动的偏移量,移动的起始位置(有三种取值,头、当前位置、尾)

 

那么如何实现上述函数的呢?

 

OPEN方法

OPEN方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:

——初始化设备

——标明次设备号

RELEASE方法

RELEASE方法的作用正好与open相反。这个设备方法有时也称为close,它应该:——关闭设备

 

读和写

读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递给驱动程序,它们的原型也相当相似:

ssize_t xxx_read(struct file *filp, char__user * buff, size_t count , loff_t * offp);

 

ssize_t xxx_write(struct file *filp, char__user *buff, size_t count , loff_t *offp);

对于2个方法,filp是文件指针,count是请求传输的数据量。buff参数指向数据缓存。最后,offp指出文件当前的访问位置(buff和count来自用户空间,filp和offp来自内核)

 

Read和write方法的buff参数是用户空间指针。因此,它不能被内核代码直接引用(而应由内核提供的专门函数来引用),理由如下:用户空间指针在内核空间时可能根本是无效的——没有那个地址的映射

 

内核提供了专门的函数用于访问用户空间的指针,例如:

——intcopy_from_user(void *to, const void __user *from, int n)

对应写操作,为真则是写失败

——intcopy_to_user(void __user *to , const void *from, int n)

对应读操作,为真则是读失败


设备注销

字符设备的注销使用cdev_del函数来完成

       Int cdev_del(struct cdev *p)

——参数:p:要注销的字符设备结构

 

:字符设备驱动程序:memdev.c

(分析驱动程序不像应用程序那样从头到尾看,应该看入口module_init())

(分析一个字符设备驱动程序,首先分析初始化、分析file operations的各函数(open、read、write、seek))


memdev.h 

#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 254	//预设的mem的主设备号
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2		//设备数
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096    //4K,申请的用于模拟字符设备的内存是4K

//mem设备描述结构体
Struct mem_dev
{
	Char *data;     //由于是用内存模拟字符设备,所以需要记录那块内存的地址,用data保存
	Unsigned long size;
};

#endif		/*_MEMDEV_H_*/

memdev.c

//此函数是用内存中的某一段数据来模拟一个字符设备
#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 <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#include “memdev..h”

static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);

struct mem_dev *mem_devp ; //设备结构体指针

struct cdev cdev;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
	struct mem_dev *dev;
	
	/*获取次设备号*/
	int num = MINOR(inode->i_rdev);

	if (num >= MEMDEV_NR_DEVS)
		return -ENODEV;
	dev = &mem_devp[num];

	/*将设备描述结构指针赋值给文件私有数据指针*/
	filp->private_data = dev;

	return 0;
}

/*文件释放函数*/
int mem_release(struct inode * inode , struct file * filp)
{
	return 0;
}

/*读函数*/
static ssize_t mem_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 mem_dev *dev = filp->private_data;	//获得设备结构体指针
	
	/*判断读位置是否有效*/
	if (p >= MEMDEV_SIZE)
 	 return 0;
	if (count > MEMDEV_SIZE -p)
	 count = MEMDEV_SIZE -p;
	
	/*读数据到用户空间*/
	if(copy_to_user(buf, (void *)(dev->data + 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_t mem_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 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;
}

/*seek文件定位函数*/
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
	loff_t newpos;
	
	switch(whence){
	  case 0:	/*SEEK_SET*/
	    newpos = offset;
	    break;

	  case 1:	/*SEEK_CUR*/
	    newpos = filp->f_ops + offset;
	    break;

	  case 2:	/*SEEK_END*/
	    newpos = MEMDEV_SIZE - 1 + offset;//此时offset应为负数
	    break;
	
	  default:	/*can't happen*/
	    return -EINVAL;	
	}
	if ((newpos < 0) || (newpos > MEMDEV_SIZE))
	    return -EINVAL;
	
	filp->f_pos = newpos;
	return newpos; 
}

/*文件操作结构体*/
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,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
	int result;
	int i;
	
	dev_t devno = MKDEV(mem_major, 0);

	/*静态申请设备号*/
	if (mem_major)
	 result = register_chrdev_region(devno, 2, "memdev");
	else	/*动态分配设备号*/
	 {
		result = alloc_chrdev_region(&devno, 0, 2, "memdev");
		mem_major = MAJOR(devno);	
	}
	
	if (result < 0)
	 return result;

	/*初始化cdev结构*/
	cdev_init(&cdev, &mem_fops);	//因为cdev是之前定义好了的struct cdev cdev,所以不需要分配,而直接进行初始化
	cdev.owner = THIS_MODULE;
	cdev.ops = &mem_fops;

	/*注册字符设备*/
	cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
	
	/*为设备描述结构分配内存*/
	mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
	if(!mem_devp)	//申请失败
	{
	 result = -ENOMEM;
	 goto fail_malloc;
	}
	memset(mem_devp, 0, sizeof(struct mem_dev));

	/*为设备分配内存*/
	for (i=0; i<MEMDEV_NR_DEVS; i++)
	 {
		mem_devp[i].size = MEMDEV_SIZE;
		mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
		memset(mem_devp[i].data, 0, MEMDEV_SIZE);
	}
	
	return 0;

	fail_malloc:
	unregister_chrdev_region(devno, 1);

	return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
	cdev_del(&cdev);	//注销设备
	kfree(mem_devp);	//释放设备结构体内存
	unregister_chrdev_region(MKDEV(mem_major, 0), 2);	//释放设备号
}

MODULE_AUTHOR("David Jason");
MODULE_LICENCE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);


main.c


//此为应用程序。用来测试驱动程序的读、写、重定位等功能
//当写入的内容和读出的内容一样,那么即是说读写功能成功。
#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+");//用fopen打开/dev/memdev0这个设备文件,这个设备文件是由我们自己去创建的:安装驱动程序之后,再去创建这个设备文件。
	if (fp0 == NULL)
	 {
	    printf("Open Memdev0 Error!\n");
	    return -1;
	}	

	/*写入设备*/
	fwrite(Buf, sizeof(Buf), 1, fp0);

	/*重新定位文件位置(思考没有该指令,会有何后果)*/
	fseek(fp0, 0, SEEK_SET);//如果没有SEEK_SET,那么会因为每次写入之后seek指针的位置总是在跟随变化的,所以,当读的时候,便不是从文件开头读起!

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

	/*读出设备*/
	fread(Buf, sizeof(Buf), 1, fp0);

	/*检测结果*/
	printf("Buf :%s \n", Buf);

	return 0;
}


竞争与互斥

调试技术分类

对于驱动程序设计来说,核心问题之一就是如何完成调试。当前常用的驱动调试技术科分为:

——打印调试(printk)

       在调试应用程序时,最常用的调试技术是打印,就是在应用程序中合适的点调用printf。当调试内核代码的时候,可以用printk完成类似任务

       合理使用printk

       在驱动开发时,printk非常有助于调试。但当正式发行驱动程序时,应当去掉这些打印语句。但你有可能很快又发现,你又需要在驱动程序中实现一个新功能(或者修复一个bug),这时你又要用到那些被删除的打印语句。这里介绍一种是用printk的合理方法,可以全局地打开或关闭它们,而不是简单地删除:

       #ifdefPDEBUG

       #define PLOG(fmt, args…) printk(KERN_DEBUG”scull:”fmt,##args)

#else

#define PLOG(fmt,args..)           //do nothing

#endif

 

       Makefile作如下修改:

       ——DEBUG=y

              ifeq ($(DEBUG),y)

              DEBFLAGS=-O2 –g –DPDEBUG        //D的作用是相当于#define

              else

              DEBFLAGS=-O2

              endif

              CFLAGS +=$(DEBFLAGS)

      

——调试器调试(kgdb)

——查询调试(/proc文件系统)

 

并发与竞态

——并发:多个执行单元同时被执行

——竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态

 

例:

If(copy_from_user(&(dev->data[pos]), buf, count))

Ret = -EFAULT;

Goto out;

 

假设有2个进程试图同时向一个设备的相同位置写入数据,就会造成数据混乱(对应于多核情况)

 

处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。在Linux内核中主要通过semaphore(信号量)机制和spin_lock(自旋锁)机制实现

 

信号量

Linux内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能在内核之外使用,它是一种睡眠锁。如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量

 

——信号量在创建时需要设置一个初始值,表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

 

——当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。

 

信号量的实现也是与体系结构相关的,定义在<asm/semaphore.h>中,struct semaphore类型用来表示信号量。

1.定义信号量        struct semaphore sem;

2.初始化信号量   

       voidsema_init(struct semaphore *sem, int vall)该函数用户初始化设置信号量的初值,它设置信号量sem的值为val

       voidinit_MUTEX(struct semaphore * sem)该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

       voidinit_MUTEX_LOCKED(struct semaphore *sem)该函数用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

 

定义及初始化的工作可由如下宏一步完成:

DECLARE_MUTEX(name):定义一个信号量name,并初始化它的值为1

DECLARE_MUTEX_LOCKED(name):定义一个信号量name,但它把它的初始值设置为0,即锁在创建时就处在已锁状态。

 

3.获取信号量

       voiddown(struct semaphore * sem)获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。该函数将把sem的值减1,如果信号量sem 的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行(此时处于TASK_UNINTERRUPTIBLE的状态)。

 

——intdown_interruptible(struct semaphore * sem):获取信号量sem。如果信号量不可用,进程将被设置为TASK_INTERRUPTIBLE(可被信号和中断唤醒)类型的睡眠状态。该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR

 

——down_killable(structsemaphore * sem):获取信号量sem。如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态

 

注:down()函数(linux 2.4)现已不建议继续使用。建议使用down_killable()或down_interruptible()函数

 

4.释放信号量

       voidup(struct semaphore * sem):该函数释放信号量sem,即把sem 的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

 

自旋锁

自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环(一直占有CPU),一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。

 

——spin_lock_init(x):该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化

——spin_lock(lock):获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。

——spin_trylock(lock):试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放

——spin_unlock(lock):释放自旋锁lock,它与spin_trylock或spin_lock配对使用

 

信号量PK自旋锁

——信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量

——信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该选择信号量。


实验1: 1.在mini2440平台编写实现了读、写,定位功能的字符设备驱动程序

2.编写应用程序,测试驱动


实验2: 基于实验1设计的驱动程序,加入竞争控制

Logo

更多推荐