使用驱动程序
Linux用户程序通过设备文件(又名:设备节点)来使用驱动程序操作字符设备和块设备,设备文件位于dev/目录下。
二、字符设备程序设计:
1>设备号
字符设备通过字符设备文件来存取。字符设备文件由使用ls -l的输出的第一列‘c’标示。如果
使用ls -l命令,会看到在设备文件项中有2个数(由一个逗号分隔)这些数字就是设备文件的主次设
备编号。
主设备号:用来标识与设备文件相连的驱动程序,用来反应设备类型
次设备号:被驱动程序用来辨别操作的哪个设备,用来区分同类型的设备。
主次设备号的描述:dev_t:其实质为unsigned int 32位整数,其中高12位为主设备号,低20位为次设备号。主设备号:MAJOR(dev_t dev),次设备号:MINOR(dev_t dev)。
分配主设备号:静态申请、动态分配。
1、静态申请:1、根据Documenttation/devices.txt,确定一个没有使用的主设备号
2、使用register_chrdev_region函数注册设备号 。
int register_chrdev_region(dev_t from, unsigned count, const char* name)
功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增加)
参数:
from:希望申请使用的设备号
cout:希望申请使用设备号数目
name:设备名(体现在/proc/devices)
该分配方式优点是简单,其缺点是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设
备号冲突,而是驱动程序无法注册。
2、动态分配:使用alloc_chrdev_region分配设备号
int alloc_chrdev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name)
功能:
请求内核动态分配count个设备号,且次设备号从baseminor开始。
参数
dev:分配到的设备号
baseminor:起始次设备号
count:需要分配到的设备号数目
name:设备名
该分配方式优点是简单,易于驱动推广,缺点是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)
安装驱动后,从/proc/devices中查询设备号
注销设备号:不论使用各种方法分配设备号,都应该在不再使用它们时释放这些设备号。
void unregister_chrdev_region(dev_t from,unsigned count)
功能:释放从from开始的coun个设备号
2>创建设备文件
使用mknod命令手工创建
mknod filename type major minor
filename :设备文件名
type :设备文件类型
major :主设备号
minor :次设备号
自动创建
3>设备注册
在linux2.6内核中,字符设备使用struct cdev来描述。字符设备的注册科分为3个步骤:
1、分配cdev:struct cdev的分配可使用cdev_alloc函数来完成。
struct cdev* cdev_alloc(void)
2、初始化cdev:struct cdev 的初始化使用cdev_init函数来完成。
void cdev_init(struct cdev* cdev, const struct file_operations* fops)
参数:
cdev:待初始化的cdev结构
fops:设备对应的操作函数集
3、添加cdev:struct cdev 的注册使用cdev_add函数来完成。
int cdev_add(struct cdev* p,dev_t dev,unsigned count)
参数:
p:待添加到内核的字符设备结构
dev:设备号
count:添加的设备个数
4>重要数据结构
在linux字符设备驱动程序设计中,有3种非常重要的数据结构:
struct file, struct inode, struct file_operations
struct file:
代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,在文件关闭后释放。
重要成员:
loff_t f_pos /*文件读写位置*/
struct file_operations* f_op :
一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL
例:mem_fops
struct file_operations mem_fops = {
.owner = THIS_MODULE,
.llseek = mem_seek,
.read = mem_read,
.write = mem_write,
.ioctl = mem_ioctl,
.open = mem_open,
.release = mem_release,
};
struct inode:
用来记录文件的物理上的信息。因此,它和代表打开文件的file结构不同的。一个文件可以对应多个file结构,但只有一个inode结构。
重要成员:
dev_t i_rdev: 设备号
5>设备操作
1、int(*open)(struct inode*,struct file*)
在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。如果该项为NULL,设备的打开操作永远成功。
open方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成的工作是:初始化设备,标明次设备号。
2、void(*release)(struct inode*, struct file*)
当设备文件被关闭时调用这个操作,与open相仿,release也可以没有。Release方法的作用正好与open相反。这个设备方法有时也称close,它的作用是:关闭设备。
3、ssize_t(*read)(struct file*,char __user*,size_t,loff_t*)
从设备中读取数据。
4、ssize_t(*write)(struct file*,const char __user*,size_t,loff_t*)
向设备发送数据。
读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递给驱动程序。他们的原型也相当类似:
ssize_t xxx_read(struct file* filp,char __user* buff,size_t count,loff_t*offp);
ssize_t xxx_read(struct file* filp,char __user* buff,size_t count,loff_t*offp);
对于2个方法,filp是文件指针,count是请求传输的数据量,buff参数指向数据缓存,最后offp之处文件当前的访问位置。
read和write方法的buff参数是用户空间指针。因此,它不能被内核代码直接饮用,理由是用户空间指针在内核空间时可能根本就是无效的——没有那么地址映射。内核提供了专门的函数用于访问用户空间的指针:
int copy_from_user(void* to,const void __user* from, int n)
int copy_to_user(void __user* to, const void* from, int n)
5、unsigned int(*poll)(struct file*, struct poll_table_struct*)
对应select系统调用
6、int(*iotcl)(struct inode*,struct file*,unsigned int,unsigned long)
控制设备
7、int(*mmap)(struct file*,struct vm_area_struct*)
将设备映射到进程虚拟地址空间中。
8、off_t(*llseek)(struct file*, loff_t,int)
修改文件的当前读写位置,并将新位置作为返回值。
9、int cdev_del(struct cdev* p)
功能:字符设备注销
参数:
p:要注销的字符设备结构
6>字符设备驱动程序设计范例:
-
/*******************************memdev.h********************************/
-
#ifndef _MEMDEV_H_
-
#define _MEMDEV_H_
-
-
#ifndef MEMDEV_MAJOR
-
#define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
-
#endif
-
-
#ifndef MEMDEV_NR_DEVS
-
#define MEMDEV_NR_DEVS 2 /*设备数*/
-
#endif
-
-
#ifndef MEMDEV_SIZE
-
#define MEMDEV_SIZE 4096
-
#endif
-
-
/*mem设备描述结构体*/
-
struct mem_dev
-
{
-
char *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 <linux/slab.h>
-
#include <asm/io.h>
-
#include <asm/system.h>
-
#include <asm/uaccess.h>
-
#include "memdev.h"
-
-
static int 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 %ld\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 %ld\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_pos + offset;
-
break;
-
case 2: /* SEEK_END */
-
newpos = MEMDEV_SIZE -1 + 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.owner = THIS_MODULE;
-
-
/* 注册字符设备 */
-
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("");
-
MODULE_LICENSE("GPL");
-
-
module_init(memdev_init);
-
module_exit(memdev_exit);
-
-
/******************app_mem.c************************/
-
-
#include <stdio.h>
-
#include <string.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);
-
/*重新定位文件位置(思考没有该指令,会有何后果)*/
-
fseek(fp0, 0, SEEK_SET);
-
/*清除Buf*/
-
strcpy(Buf, "Buf is NULL!");
-
printf("BUF: %s\n",Buf);
-
/*读出设备*/
-
fread(Buf, sizeof(Buf), 1, fp0);
-
/*检测结果*/
-
printf("BUF: %s\n",Buf);
-
return 0;
-
}
三、驱动程序调试技术
1>分类:对于驱动程序设计来说,核心问题之就是如何完成调试。当前常用的驱动调试技术分为:打印调试,调试器调试,查询调试。
打印调试:在驱动程序开发时,printk非常有助于调试。但当正式发行驱动程序时应当去掉这些打印语句。合理的使用printk:
#ifdef PDEBUG
#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
else
DEBFLAGS = -O2
endif
CFLAGS+=(DEBFLAGS)
四、并发与竞态
并发:多个执行单元同时被执行。
竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问导致的竞争状态。
例:
if(copy_from_user(&(dev->data[pos]),buf,count))
ret = -EFAULT;
goto out;
假设有2个进程试图同时向一个设备的相同位置写入数据,就会造成数据混乱。处理并发的常用技术
是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。在Linux内核中主要通
过semaphore机制和spin_lock实现。
semaphore信号量:Linux内核的信号量是一样的的,但是它不能在内核之外使用,它是一种睡眠锁。
如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队里,然后让其
睡眠。当持有信号量的任务将被唤醒,并让其获得信号量。
1>信号量的使用
信号量的实现也是与体系结构相关的,定义在
中,struct semaphore类型用来表
示信号量。
1、定义信号量
struct semaphore sem;
2、初始化信号量
void sema_init(struct semaphore* sem,int val)
该函数用于初始化设置信号量的初值,它设置信号量sem的值val。
void init_MUTEX(struct semaphore* sem)
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1.
void init_MUTEX_LOCKED(struct semaphore* sem)
该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就出在已锁状态。
定义与初始化的工作可由如下宏异步完成:
DECLARE_MTEX(name)
定义一个信号量name,并初始化它的值为1.
DECLARE_MUTEX_LOCKED(name)
定义一个信号量name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。
3、获取信号量
void down(struct semaphore* sem)
获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文中使用该函数。该函数将把sem的值减
一,如果信号量的只非负,就直接返回,否则调用者将被挂起,知道被的任务释放该信号量才能继续
运行。
int down_interruptible(struct semaphore* sem)
获取信号量sem。如果洗好量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠状态。该函数由
返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号
打断,返回-EINTR.
down_killable(struct semaphore* sem)
获取信号量sem。如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态。
注意:
down()函数现已经不建议使用。建议使用down_killable()或down_interruptible()函数。
2>自旋锁
自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获
得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里是否应该自旋锁
的保持者已经释放了锁,“自旋”就是这个意思。
spin_lock_init(x)
该宏用于初始化自旋锁x,自旋锁在使用前必须初始化。
spin_lock(lock)
获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,知道该自旋
的保持者释放。
spin_trylock(lock)
试图获取自旋锁lock,如果立即获得锁,并返回真,否则立即返回假。它不会一直等待释放。
spin_unlock(lock)
释放自旋锁lock,它与spin_trylock或spin_lock配对使用。
信号量可能允许多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号
量(只能一个持有者),允许多个持有者的信号量叫计数信号量。
信号量适合于保持时间较长的情况;而自旋锁适合于时间非常短的情况。在实际应用中自旋锁控制的
代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦进行切
换、就至少花费切出切入两次的时间,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该
选择信号量。