简单linux字符设备驱动程序与编程小技巧(上)

这几天开始研究linux下的驱动程序编写了,遇到的问题也挺多的,好在linux是开源的,很多高人编写的技巧和思路都会在他们的源代码中体现,我也在他们的源码中学到了很多好东西,我归纳了下贴出来,希望自己的代码能帮到别人。

今天就来介绍一下linux的字符设备驱动程序:

字符驱动应该是驱动学习的第一站了,在《linux设备驱动程序第三版》这本书的第三章介绍了一个简单的字符设备scull的程序设计,这一章很详细的介绍了字符设备驱动的一些概念,建议使用的事项和设计时要用到一些结构体和函数,所以我就不需要再吧这些介绍一遍了。我为了学起来方便,简化了书里的scull例子,将储存设备定义为一块连续的数据段。

现在让我们边看代码边分析这简单的scull字符设备驱动设计要做些什么,怎么做。

1 – 39行

/* scull V1.0 */#include <linux/module.h>#include <linux/init.h>#include <linux/fs.h>           // file_operations#include <linux/cdev.h>         #include <linux/errno.h>        // err#include <linux/string.h>     // memcpy memset#include <linux/slab.h>         // kmalloc#include <linux/kernel.h>       // container_of#include <asm/uaccess.h>        // copy_xxxx_user//#include "c_device.h"#define DEBUG_SWITCH#ifdef DEBUG_SWITCH#define P_DEBUG(fmt, args...) printk(KERN_WARNING"KDEBUG-%s[%s]: "fmt, __FILE__,  __FUNCTION__, ##args)#else#define P_DEBUG(fmt, args...) printk(KERN_DEBUG"KDEBUG-%s[%s]: "fmt, __FILE__,  __FUNCTION__, ##args)#endif#define rw_BUF_SIZE 20      // 每个次设备分配到的字节宽度#define SEEK_SET 0          // llseek#define SEEK_CUR 1#define SEEK_END 2#define D_COUNT  2          // 定义的值与常量d_count一致,存在的理由下面会讲unsigned int major = 0;     // 自定义主设备号unsigned int minor = 0;     // 自定义次设备号const unsigned int d_count = 2;  // 次设备数dev_t devNo;                // dev_t设备号描述struct _dev_sct{            // 自定义设备结构,后面会详细介绍它char (*kbuf)[D_COUNT][rw_BUF_SIZE];   // 指向buf主体int cur_size;               // buf残留sizeunsigned int d_minor;       // 保存当前打开设备的次设备号struct cdev c_dev;          // 嵌入cdev结构信息};struct _dev_sct *devp;      // 创建全局的设备结构指针

首先来看看包括的头函数,后面的注释是这个头函数包含的作用,其中被我注释掉的c_device.h这个文件不存在,我原本的想法是将文件的一些声明部分放到c_device.h中的,这样可以减少源文件代码量,但是为了修改方便所以没这么做。

下面的rw_BUF_SIZE是我定义的这个内存设备的长度,每个次设备将分配到20字节宽度的内存,分配策略会在后面代码中实现。

SEEK_xxx将给llseek使用,由于内核空间不能调用C库,所以不能享受C库的好处了,自己定义吧。

D_COUNT与常量d_count都是为了描述次设备个数的,本来编写的时候是没有D_COUNT,但编译的时候报错error: variably modified ‘kbuf’ at file scope,上网一查发现GCC编译器不允许常量来做数组的参数(vc是可以这么干的)。

重点来谈谈_dev_sct这个结构吧,《linux设备驱动程序第三版》将它解释为描述设备的结构,即它代表设备的主体,这个结构是程序员定义的,里面要添加什么与具体要编写的驱动相关,存在这个结构的好处是这个结构包含了设备所有需要关注的信息,这使后面的调用变得简单,起码思路上简单了很多,不会因为包含了不同重要信息的变量变得杂乱无章。

我的_dev_sct结构描述了D_COUNT个,每个大小为rw_BUF_SIZE字节的内存buffer,这个buffer的主体是一个[D_COUNT][rw_BUF_SIZE]的数组,_dev_sct结构保存了kbuf这个指向数组的数组指针。所以在分配完_dev_sct结构后还需分配buffer主体,然后将其与kbuf关联,这个策略将在后面代码中贴出。

_dev_sct结构里嵌入cdev是一个很nice的做法,这样_dev_sct就能通过cdev结构被别的函数更方便的调用了,策略在后。

165-227

static int __init mycdev_init(void){    if(major)    {    devNo = MKDEV(major, minor);    result = register_chrdev_region(devNo, d_count, "mycdev");    }    else    {     result = alloc_chrdev_region(&devNo, minor, d_count, "mycdev");    }    if(result < 0)    {    P_DEBUG("register devNo errno!\n");    goto err0;    }        devp = (struct _dev_sct *)kmalloc(sizeof(struct _dev_sct), GFP_KERNEL);    if(!devp)    {    result = -ENOMEM;    goto err2;    }    memset(devp, 0, sizeof(struct _dev_sct));    devp->kbuf = (char (*)[d_count][rw_BUF_SIZE])kmalloc(d_count * rw_BUF_SIZE, GFP_KERNEL);    if(!devp->kbuf)    {    result = -ENOMEM;    goto err3;    }    memset(devp->kbuf, 0, d_count * rw_BUF_SIZE);        cdev_init(&devp->c_dev, &f_opts);    devp->c_dev.owner = THIS_MODULE;    devp->c_dev.ops   = &f_opts;        result = cdev_add(&devp->c_dev, devNo, d_count);    if(result < 0)    {    P_DEBUG("cdev_add errno!\n");    goto err1;    }    P_DEBUG("major = [%d]\n", MAJOR(devNo));                return result;        err0:     return result;    err2:    unregister_chrdev_region(devNo, d_count);    return result;    err3:    kfree(devp);    unregister_chrdev_region(devNo, d_count);    return result;    err1:    kfree(devp->kbuf);    kfree(devp);    unregister_chrdev_region(devNo, d_count);    return result;}

这里先贴出init代码,why? 大家注意,分析内核模块和驱动代码都应该从init开始,包括以后看行家里手写的内核模块代码,如果不从init慢慢开始看,会晕的。

字符设备首先分配和获取设备号,register_chrdev_region代表用自己配置的设备号,alloc_chrdev_region表示从系统获取空闲设备号(推荐)。

为了让设备被系统控制就要注册设备,cdev相关函数来完成注册,在注册以前就要使设备已在工作状态,意思就是说调用cdev_add以前就要把设备初始化OK了,这里就是内存设备,用kmalloc来分配内存吧,我首先分配的是_dev_sct结构,kmalloc分配的内存是不清零的,需要手动memset,然后分配内存设备主体,接着把它们拼起来:

devp->kbuf = (char (*)[d_count][rw_BUF_SIZE])kmalloc(d_count * rw_BUF_SIZE, GFP_KERNEL);

出错处理这里我发现很多资料里面都用了goto,应该是个不流行的东西,为什么这里用的比较多呢,我的理解是:内核和驱动代码在某个地方失败后需要将前面执行成功的比如内存分配之类的资源释放,而这样的出错处理如果写在函数里面就不直观,因为这部分出错处理的代码有相似性,释放顺序也很有规律。而且一般都是与资源分配顺序相反,这样goto的代码写在一起就好看很多。

上半部分就讲到这里,下半部分会继续讲解file_opt的相关函数,我的实验代码和操作步骤也会打包到网上。

下半部分《简单linux字符设备驱动程序与编程小技巧(下)》http://blog.csdn.net/jiebaoabcabc/article/details/19283859

源代码链接http://download.csdn.net/detail/jiebaoabcabc/6926511

就这样 米娜桑 贵安。

“人无完人金无足赤”,只要是人就不会是完美的,

简单linux字符设备驱动程序与编程小技巧(上)

相关文章:

你感兴趣的文章:

标签云: