字符设备驱动
驱动分类
——字符设备驱动
字符设备:字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
main.c
//此为应用程序。用来测试驱动程序的读、写、重定位等功能
//当写入的内容和读出的内容一样,那么即是说读写功能成功。
#include
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实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。
信号量的实现也是与体系结构相关的,定义在
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设计的驱动程序,加入竞争控制