在linux系统应用中,设备驱动程序编写是比较困难的,初学者往往摸不着头绪。在这片文章中,我给大家讲解一个实例,带大家进行一步一步的分析字符设备驱动程序的编写方法。
1、功能实现
首先我们先来介绍一下,我们用这个实例来做什么。我们做的这个驱动程序的作用是将用户空间的一块1KByte的内存模拟成一个设备,并设计了这个设备的打开、关闭、读写等功能的驱动,并用一个应用程序来验证驱动的有效性。
2、源码简介
本文的资源中可以下载驱动的源码,源码由4个文件组成,分别是:
memdev.c:驱动程序源码
memdev.h:驱动程头文件
Makefile:驱动程序的makefile文件
mem_test.c:驱动验证应用程序
3、驱动模块介绍
设备驱动程序的本质是内核模块,所以驱动程序内包含了内核模块必备的组成部分,如下所示。
MODULE_AUTHOR("Bigmarshal");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
其中,第3行和第4行是必备的,分别指定了设备的初始化函数和退出函数,第一行和第二行分别指明作者和许可证。
4、file_operations结构体
在介绍初始化函数和退出函数之前,我们先来介绍一下file_operations结构体,这个结构体是形成一个用户系统调用函数与内核驱动函数之间的映射关系,如下所示。
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,
};
在这个结构体中,第一行指明了结构体的所有者,2~6行与用户空间的系统调用函数之间的对应关系分别为lseek()、read()、write()、open()、close()。与之相对应的内核函数分别为这些行后半部分的函数。举个例子来说,我们的用户程序使用系统调用read()来读取数据时,转到内核空间里执行的代码为mem_read函数。通过这个结构体,我们就可以将用户的系统调用函数与内核的驱动函数映射到一起了。
5、模块初始化
memdev_init()函数是模块初始化函数,函数的源码如下所示。
static int memdev_init(void)
{
int result;
int i;
printk(KERN_EMERG "memdev init start!\n");
//生成设备号
dev_t dev_no = MKDEV(mem_major,0);
//向系统申请设备号
if(mem_major)
result = register_chrdev_region(dev_no,2,"memdev");
else
{
result = alloc_chrdev_region(&dev_no,0,2,"memdev");
mem_major = MAJOR(dev_no);
}
if(result<0)
return result;
//初始化cdev结构体,与mem_fops结构体绑定
cdev_init(&cdev,&mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
//注册字符设备
result = cdev_add(&cdev,MKDEV(mem_major,0),MEMDEV_SUM);
if(result)
printk(KERN_EMERG "Add cdev error!\n");
//为设备描述结构体分配内存,返回指向结构体的指针
mem_devp = kmalloc(MEMDEV_SUM*sizeof(struct mem_dev),GFP_KERNEL);
if(!mem_devp) //分配内存失败
{
result = -ENOMEM;
goto fail_malloc;
}
memset(mem_devp,0,MEMDEV_SUM*sizeof(struct mem_dev)); //清零结构体
//为存储器设备分配内存
for(i=0;i<MEMDEV_SUM;i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE,GFP_KERNEL);
memset(mem_devp[i].data,0,MEMDEV_SIZE);
}
my_class = class_create(THIS_MODULE, "my_class");
if(IS_ERR(my_class))
{
printk(KERN_EMERG "failed in creating class!\n");
return -1;
}
device_create(my_class, NULL, MKDEV(mem_major, 0),NULL, "memdev0");
printk(KERN_EMERG "module memdev init ok!\n");
return 0;
fail_malloc:
unregister_chrdev_region(dev_no,1);
return result;
}
在这个函数中,通过几个重要步骤,实现设备驱动的初始化。
第一步:申请设备号,MKDEV(mem_major,0)函数的作用是生成一个完整的设备号,其中mem_major为主设备号。程序中设置了一个没有使用的主设备号,并通过register_chrdev_region()函数实现设备号的申请。如果mem_major赋值为0,则需要动态分配设备号,调用alloc_chrdev_region()函数实现设备号的分配。
第二步:初始化和注册字符设备,这一步首先是初始化cdev结构体,它是描述字符设备特征的一个结构体。将它和file_operations结构体进行绑定,这样这个字符设备的驱动函数就定好了,然后是注册字符设备,通过cdev_add()函数实现。相当于将字符设备和设备号一起注册到内核中。
第三步:分配内存空间,由于我们自己做的驱动程序的作用是用内存空间来模拟设备,所以要分配内存空间。首先是为设备描述结构体分配内存,由于最多允许2个设备所有要分配2个描述结构体的内存,调用kmalloc函数进行分配,返回值是描述结构体的首地址。之后是为两个设备分配2K的空闲内存,用来模拟我们的驱动设备。
第四部:自动生成设备,驱动模块除了要按照成内核模块外,还需要在根文件系统的dev文件夹下生成设备文件,这一步就是自动生成这个设备文件。只需要调用两个函数就可以完成。分别是class_create()和device_create()函数,最后生成的设备文件名为memdev0,应用程序会通过这个文件进行设备的操作。
6、模块退出函数
模块的退出函数是memdev_exit()函数,这个函数的源码如下所示,它是模块卸载时执行的函数。
static void memdev_exit(void)
{
cdev_del(&cdev);
device_destroy(my_class, MKDEV(mem_major, 0));
class_destroy(my_class);
kfree(mem_devp[0].data);
kfree(mem_devp[1].data);
kfree(mem_devp);
unregister_chrdev_region(MKDEV(mem_major,0),2);
}
在这个函数中,先是删除了字符设备,然后删除掉设备文件,接下来释放掉占用的内存空间,最后解除掉字符设备的注册。这样就把驱动的所有痕迹清楚掉了。
7、打开和关闭设备函数
打开和关闭设备函数与用户程序的open()和close()函数相对应。设备驱动中的打开函数如下所示。
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
int num = MINOR(inode->i_rdev);
printk(KERN_EMERG "KERN_EMERGnum = %d\n",num);
if(num >= MEMDEV_SUM)
return -ENODEV;
dev = &mem_devp[num];
filp->private_data = dev;
printk(KERN_EMERG "open ok!\n");
return 0;
}
打开函数做的主要工作的是把设备描述结构体的地址传给filp->private_data,可以被其他函数使用。
设备的关闭函数mem_release()没有任何代码。直接退出。
8、读取函数
驱动的读取函数如下所示。
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 from %d\n",count,p);
}
return ret;
}
读取函数的作用是将内存中数据读取出来,传送给用户空间的数组。
9、写入函数
写入函数的源码如下所示。
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 "write %d bytes to %d\n",count,p);
}
return ret;
}
写入函数的作用是将用户空间传送来的数据写入到内存中。
10、文件定位函数
文件定位函数的代码如下所示。
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:
return -EINVAL;
break;
}
if((newpos<0)||(newpos>=MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
定位函数的作用是重新定位文件中读取和写入的位置。
11、安装驱动
分析完驱动代码之后就是安装驱动。在驱动代码文件夹下通过make命令对驱动进行编译,编译之后,生成一个memdev.ko文件,执行下面的命令来安装模块
sudo insmod memdev.ko
安装好之后,通过lsmod命令可以查看模块是否安装了,在/dev文件夹下也可以找到新添加的设备文件memdev0。
12、驱动的验证
为了验证驱动程序的功能,编写了一个应用程序用来验证驱动程序的功能。主要代码如下所示。
int main()
{
int fd;
char Buf[1024];
//初始化Buf
strcpy(Buf,"Mem is char device!");
printf("BUF: %s\n",Buf);
//打开设备
fd = open("/dev/memdev0",O_RDWR);
printf("fd = %d\n",fd);
if(fd <= 0)
{
printf("Open memdev0 error!\n");
return -1;
}
//写入设备
write(fd, Buf, sizeof(Buf));
//重新定位文件位置
lseek(fd,0,SEEK_SET);
//清除Buf
strcpy(Buf,"Buf is Null!");
printf("BUF: %s\n",Buf);
//读设备
read(fd,Buf, sizeof(Buf));
printf("BUF: %s\n",Buf);
close(fd);
return 0;
}
这段代码的作用是打开新创建的设备,向设备中写入数据,再读回数据进行验证,最后关闭设备。编译代码,并运行,就可以验证驱动的功能了。
本文用的源码可以从本文的资源中下载。