这个程序主要参考ldd3的第三章来写,这一章主要通过介绍字符设备scull(Simple Character Utility for Loading Localities,区域装载的简单字符工具)的驱动程序编写,来学习Linux设备驱动的基本知识。scull可以为真正的设备驱动程序提供样板。
下面这个驱动程序用于驱动字符设备mychar,参考scull源码。
废话少说,直接上代码,后面再来慢慢解释:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/kernel.h>
#include <asm/uaccess.h>
MODULE_LICENSE("Dual BSD/GPL");
#define MYCHAR_MAJOR 0 //主设备号
#define MYCHAR_MINOR 0 //次设备号
#define MYCHAR_COUNT 1 //请求的设备个数
#define MYCHAR_QUANTUM 4000 //每个量子大小
#define MYCHAR_QSET 1000 //每个量子集含量子数量
/* 允许参数传递,缺省为以下默认值 */
static int mychar_major = MYCHAR_MAJOR, mychar_minor = MYCHAR_MINOR, count = MYCHAR_COUNT, quantum = MYCHAR_QUANTUM, qset = MYCHAR_QSET;
module_param(mychar_major, int, S_IRUGO);
module_param(mychar_minor,int,S_IRUGO);
module_param(count,int, S_IRUGO);
module_param(quantum, int, S_IRUGO);
module_param(qset, int, S_IRUGO);
struct mychar_dev* dev;
/* 为设备建立特定的设备结构 */
struct mychar_dev
{
struct mychar_qset *data;
int quantum;
int qset;
unsigned long size;
struct semaphore sem;
struct cdev cdev;
};
/* 定义链表项结构 */
struct mychar_qset
{
void **data;
struct mychar_qset *next;
};
/* 当设备文件以只写方式打开,释放设备结构内存 */
int mychar_trim(struct mychar_dev *dev)
{
struct mychar_qset *next, *qptr;
int i;
int qset = dev->qset;
/* 遍历所有链表项 */
for (qptr = dev->data; qptr; qptr = next)
{
if (qptr->data)
{
for (i = 0; i < qset; i++)
{
kfree(qptr->data[i]); //释放每个量子
}
kfree(qptr->data); //释放每个量子集
qptr->data = NULL;
}
next = qptr->next;
kfree(qptr);
}
/* 把设备结构设定为初始化值 */
dev->quantum = MYCHAR_QUANTUM;
dev->qset = MYCHAR_QSET;
dev->size = 0;
dev->data = NULL;
return 0;
}
/* 找到第item个链表项 */
struct mychar_qset* mychar_follow(struct mychar_dev* dev, int item)
{
int i;
struct mychar_qset* dptr = dev->data;
/* 分配内存给第0个链表项 */
if (!dptr)
{
dptr = dev->data = kmalloc(sizeof(struct mychar_qset), GFP_KERNEL);
if(!dptr)
{
return NULL;
}
memset(dptr, 0, sizeof(struct mychar_qset)); //把链表项内存清零
}
/* 遍历链表并分配内存,直到找到第item个链表项
注意不要先把next赋给dptr,否则会把一个随机的地址赋给dptr,可能导致出错,必须先分配内存,然后把内存的地址赋给next后,才可以把next赋给dptr */
for (i = 0; i < item; i++)
{
if (!dptr->next)
{
dptr->next = kmalloc(sizeof(struct mychar_qset), GFP_KERNEL);
if (!dptr->next)
{
return NULL;
}
memset(dptr->next, 0, sizeof(struct mychar_qset));
}
dptr = dptr->next;
}
return dptr;
}
/* 分配设备号 */
int alloc_mychar_dev(int major, int minor,unsigned int count)
{
int result;
dev_t devno;
if (major)
{
devno = MKDEV(major, minor);
result = register_chrdev_region(devno, count, "mychar"); //major大于0时,静态分配设备号
}
else
{
result = alloc_chrdev_region(&devno, 0, count, "mychar"); //major为0时,动态分配设备号
major = MAJOR(devno);
minor = MINOR(devno);
}
mychar_major = major;
mychar_minor = minor;
if (result)
{
printk(KERN_WARNING "mychar: can't get major %d",major);
}
return result;
}
/* 打开设备 */
int mychar_open(struct inode *inode, struct file *filp)
{
/* 打开设备文件,把设备结构与file结构关联起来,初始化file结构中某些值 */
struct mychar_dev *dev;
dev = container_of(inode->i_cdev, struct mychar_dev, cdev); //container_of宏返回的是结构体mychar_dev的地址
filp->private_data = dev;
/* 当设备文件以write-only方式打开时,把设备文件长度截断为0 */
if ((filp->f_flags & O_ACCMODE) == O_WRONLY)
{
mychar_trim(dev);
}
return 0;
}
/* 释放设备 */
int mychar_release(struct inode* inode, struct file* filp)
{
return 0;
}
/* 读设备操作 */
ssize_t mychar_read(struct file* filp, __user char* buf, size_t count, loff_t *f_pos)
{
struct mychar_dev *dev = filp->private_data;
struct mychar_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (down_interruptible(&dev->sem))
{
return -ERESTARTSYS;
}
/* 判断偏移量 */
if (*f_pos >= dev->size)
{
goto out;
}
if (*f_pos + count >= dev->size)
{
count = dev->size - *f_pos;
}
/* 在量子集中寻找链表项、qset索引以及偏移量 */
item = (long)*f_pos / itemsize; //第item链表项
rest = (long)*f_pos % itemsize; //在链表项中的第rest个字节数
s_pos = rest / quantum; //在该量子集中的第s_pos个量子
q_pos = rest % quantum; //在该量子中的第q_pos个字节
/* 沿该链表前行,直到正确的位置 */
dptr = mychar_follow(dev, item);
/* 读取该量子的数据直到结尾 */
if (count >= quantum - q_pos)
{
count = quantum -q_pos;
}
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count))
{
retval = -EFAULT;
goto out;
}
*f_pos += count; //修改文件当前位置
retval = count;
out:
up(&dev->sem);
return retval;
}
/* 写设备操作 */
ssize_t mychar_write(struct file* filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct mychar_dev* dev = filp->private_data;
struct mychar_qset* dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = qset * quantum;
int item, s_pos, q_pos, rest;
size_t retval = -ENOMEM;
if (down_interruptible(&dev->sem))
{
return -ERESTARTSYS;
}
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;
dptr = mychar_follow(dev, item);
if (dptr == NULL)
{
goto out;
}
/* 为量子集分配内存 */
if (!dptr->data)
{
dptr->data = kmalloc(qset * sizeof(char*), GFP_KERNEL);
if (!dptr->data)
{
printk(KERN_INFO "Error scull_write qs->data = kmalloc.");
goto out;
}
memset(dptr->data, 0, qset * sizeof(char*));
}
/* 为量子分配内存 */
if (!dptr->data[s_pos])
{
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
{
printk(KERN_INFO "Error scull_write qs->data[s_pos] = kmalloc.");
goto out;
}
memset(dptr->data[s_pos], 0, quantum);
}
if (count > quantum - q_pos)
{
count = quantum - q_pos;
}
if (copy_from_user(dptr->data[s_pos] + q_pos, buf, count))
{
retval = -EFAULT;
goto out;
}
retval = count;
*f_pos += count;
/* 修改设备文件大小 */
if (dev->size < *f_pos)
{
dev->size = *f_pos;
}
out:
up(&dev->sem);
return retval;
}
/* file_operations结构初始化 */
struct file_operations mychar_fops =
{
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
.write = mychar_write,
.read = mychar_read,
};
/* 利用系统分配的设备号注册字符设备 */
void mychar_setup_dev(struct mychar_dev *dev, int index)
{
int err;
dev_t devno = MKDEV(mychar_major, mychar_minor + index);
/* 初始化cdev结构 */
cdev_init(&dev->cdev, &mychar_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &mychar_fops;
/* 注册字符设备 */
err = cdev_add(&dev->cdev, devno, 1);
if (err)
{
printk(KERN_NOTICE "Error %d adding mychar%d", err, index);
}
}
/* 初始化设备 */
static __init int mychar_init(void)
{
int i;
/* 分配设备号 */
if (!alloc_mychar_dev(mychar_major, mychar_minor,count))
{
printk(KERN_ALERT "major:%d, minor: %d, count: %d\n", mychar_major, mychar_minor, count);
}
/* 为设备结构分配内存并把内存区清零 */
dev = kmalloc(count * sizeof(struct mychar_dev), GFP_KERNEL);
if (!dev)
{
printk(KERN_ALERT "kmalloc\n");
}
memset(dev, 0, sizeof(struct mychar_dev) * count);
for (i = 0; i< count; i++)
{
init_MUTEX(&dev[i].sem);
mychar_setup_dev(&dev[i], i); //注册字符设备
dev[i].qset = qset;
dev[i].quantum = quantum;
dev[i].size = 0;
}
return 0;
}
/* 卸载设备 */
static __exit void mychar_exit(void)
{
int i;
dev_t devno = MKDEV(mychar_major, mychar_minor);
/* 卸载字符设备 */
for (i = 0; i < count; i++)
{
cdev_del(&dev[i].cdev);
}
kfree(dev);
unregister_chrdev_region(devno, count);
}
module_init(mychar_init);
module_exit(mychar_exit);
代码有点长啊不好意思,但是貌似每个部分都不可或缺的说,下面挑些必须要知道的知识来说,下面所说的大部分是ldd3上的内容,用scull设备作讲解:
一、主设备号和次设备号
可以通过命令查看系统设备:
#ls /dev -l
ls -l命令可在设备文件项的最后修改日期前看到2个数此位置通常指文件长度,而设备文件却是2个数,这两个数就是相应设备的主设备号和次设备号。可通过次设备号获得一个指向内核设备的直接指针,也可将其当做设备本地数组的索引。
主设备号表示设备对应的驱动程序;次设备号由内核使用,用于正确确定设备文件所指的设备。
内核用dev_t类型(</usr/src/kernels/2.6.18-92.el5-i686/include/linux/types.h>)来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。
在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式。
(dev_t)-->主设备号、次设备号 | MAJOR(dev_t dev) MINOR(dev_t dev) |
主设备号、次设备号-->(dev_t) | MKDEV(int major,int minor) |
建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明:
int register_chrdev_region(dev_t first, unsigned int count,char *name);//指定设备编号 int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,unsigned int count, char *name);//动态生成设备编号 void unregister_chrdev_region(dev_t first, unsigned int count); //释放设备编号 |
对于一个新的驱动程序,我们强烈建议读者不要随便选择一个当前未使用的设备号作为主设备号,而应该使用动态分配机制获取主设备号。
换句话说,驱动程序应该始终使用alloc_chrdev_region而不是register_chrdev_region函数。
分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。
以下是在scull.c中用来获取主设备好的代码:
if (scull_major) { dev = MKDEV(scull_major, scull_minor); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull"); scull_major = MAJOR(dev); }if (result < 0) { printk(KERN_WARNING "scull: can't get major %d\n", scull_major); |
在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在<linux/fs.h>。
file_operations结构就是用来将驱动程序操作连接到设备编号;
// 文件操作,设备操作,将驱动程序操作链接到设备编号
struct file_operations scull_fops =
{
.owner = THIS_MODULE,
.read = scull_read, //用来从设备读取数据
.write = scull_write, //向设备发送数据
.open = scull_open,
.release = scull_release,
};
file结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构)。
inode结构,内核用inode结构在内部表示文件。inode结构中包含了大量有关文件的信息。作为常规,只有下面两个字段对编写驱动程序有用。
dev_t i_rdev ;//对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev ;//struct cdev是表示字符设备的内核的内部结构
内核内部使用struct cdev结构来表示字符设备。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev。代码应包含<linux/cdev.h>,它定义了struct cdev以及与其相关的一些辅助函数。
注册一个独立的cdev设备的基本过程如下:
1、为struct cdev 分配空间(如果已经将struct cdev 嵌入到自己的设备的特定结构体中,并分配了空间,这步略过!)
struct cdev *my_cdev = cdev_alloc();
2、初始化struct cdev
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3、初始化cdev.owner
cdev.owner = THIS_MODULE;
4、cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
从系统中移除一个字符设备:void cdev_del(struct cdev *p);(此函数后不能再访问cdev结构了)
以下是scull中的初始化代码(之前已经为struct scull_dev 分配了空间):
|
以下是scull模型的结构体:
|
scull驱动程序引入了两个Linux内核中用于内存管理的核心函数,它们的定义都在<linux/slab.h>:
|
以下是scull模块中的一个释放整个数据区的函数(类似清零),将在scull以写方式打开和scull_cleanup_module中被调用:
|
五、open和release
5.1 open方法提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:
(1)检查设备特定的错误(如设备未就绪或硬件问题);
(2)如果设备是首次打开,则对其进行初始化;
(3)如有必要,更新f_op指针;
(4)分配并填写置于filp->private_data里的数据结构。
而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在<linux/kernel.h>中的container_of宏,源码如下:
|
其实从源码可以看出,其作用就是:通过指针ptr,获得包含ptr所指向数据(是member结构体)的type结构体的指针。即是用指针得到另外一个指针。
5.2 release方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1)释放由open分配的、保存在file->private_data中的所有内容;
(2)在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。
六 、read和write
read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在<asm/uaccess.h>中定义的:
|
而值得一提的是以上两个函数和
|
之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。
至于read和write 的具体函数比较简单,就在实验中验证好了。
七、模块实验
测试程序在PC上开发,交叉编译后在arm上运行。
1、 加载驱动模块,建立设备节点
主设备号和次设备号我在初始化函数中分配以后打印出来,若不想打印的话也可用 cat /proc/devices命令找出自己设备的主设备号:
2、测试驱动程序
先贴上测试程序:
#include <stdio.h>
#include <linux/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define MYDEVICE "/dev/mychar0"
int main()
{
int fd, i, len_w, len_r, len, len_buf;
char buf_write[] = "abcdefghijklmnopqrst";
char buf_read[100];
char *tmp_buf;
for (i = 0, len = 0; buf_write[i] != '\0'; i++, len++);
if ((fd = open(MYDEVICE, O_RDWR)) < 0)
{
perror("Open");
}
len_buf = len;
tmp_buf = buf_write;
for (i = 0; i < len; i+=len_w)
{
len_w = write(fd, tmp_buf, len_buf);
printf("write %d bytes!\n", len_w);
tmp_buf += len_w;
len_buf -= len_w;
}
close(fd);
if ((fd = open(MYDEVICE, O_RDWR)) < 0)
{
perror("Open");
}
len_buf = len;
tmp_buf = buf_read;
for (i = 0; i < len; i+=len_r)
{
len_r = read(fd, tmp_buf, len_buf);
printf("read %d bytes!\n", len_r);
tmp_buf += len_r;
len_buf -= len_r;
}
for (i = 0; i < len; i++)
{
printf("[%d]: %d\n", i, buf_read[i]);
}
close(fd);
return 0;
}
编译运行:
97~116 在ASCII码上对应‘a’~‘t’,看来读写能力测试是成功了。
下面换种情况,把量子大小改为6,量子集大小改为2
测试量子读写也成功了!
在命令行上其实可以直接用cat命令查看mychar0的内容:
实验不仅测试了模块的读写能力,还测试了量子读写是否有效。
参考日志:http://myswirl.blog.163.com/blog/static/51318642201092751938393/