下面将以linux设备驱动开发详解上的globalmem设备驱动为例来详细分析字符设备驱动的过程。
#include //模块所需的大量符号和函数定义
#include
#include //文件系统相关的函数和头文件
#include
#include
#include //包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明
#include //指定初始化和清除函数
#include //cdev结构的头文件 包含
#include
#include
#include //在内核和用户空间中移动数据的函数
#define GLOBALMEM_SIZE 0x1000 /*全局内存最大4K字节*/
#define MEM_CLEAR 0x1 /*清0全局内存*/
#define GLOBALMEM_MAJOR 254 /*预设的globalmem的主设备号*/
static int globalmem_major = GLOBALMEM_MAJOR;
/*globalmem设备结构体*/
struct globalmem_dev
{
struct cdev cdev; /*cdev结构体*/
unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/
};
struct globalmem_dev *globalmem_devp; /*设备结构体指针*/
/*文件打开函数*/
int globalmem_open(struct inode *inode, struct file *filp)
{
/*将设备结构体指针赋值给文件私有数据指针.
系统在调用驱动程序的open方法前将这个指针置为NULL。
驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。
驱动程序可以用这个字段指向已分配的数据,
但是一定要在内核释放file结构前的release方法中清除它*/
filp->private_data = globalmem_devp;
//将文件私有数据private_data指向设备结构体,read,write,ioctl,llseek等函数
//通过private_data访问设备结构体
return 0;
}
/*文件释放函数*/
int globalmem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*
备方法应当进行下面的任务:
1、释放 open 分配在 filp->private_data 中的任何东西
2、在最后的 close 关闭设备globalmem_release的基本形式没有硬件去关闭, 因此需要的代码是最少的.不是每个 close 系统调用引起调用 release 方法. 只有真正释放设备数据结构的调用会调用这个方法
*/
/* ioctl设备控制函数 */
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned
int cmd, unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/
switch (cmd)
{
case MEM_CLEAR://清除全局内存
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;
default://其他不支持的命令
return - EINVAL;
}
return 0;
}
/*读函数*/
static ssize_t globalmem_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 globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= GLOBALMEM_SIZE)//0x1000 要读的偏移位置越界
return count ? - ENXIO: 0;
if (count > GLOBALMEM_SIZE - p)//要读的字节数太大
count = GLOBALMEM_SIZE - p;
/*内核空间->用户空间*/
//将内核空间的内容复制到用户空间,所复制的内容是从from来,到to去,复制n个字节。
if (copy_to_user(buf, (void*)(dev->mem + 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 globalmem_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
/*
filp 是文件指针,count 是请求的传输数据长度,buff 参数是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区,该地址在内核空间不能直接读写,offp 是一个指针指向一个"long offset type"对象, 它指出用户正在存取的文件位置. 返回值是一个"signed size type。写的位置相对于文件开头的偏移。
*/
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= GLOBALMEM_SIZE)//0x1000 要写的偏移位置越界
return count ? - ENXIO: 0; //???
if (count > GLOBALMEM_SIZE - p)//将数据写入该缓存区,直到结尾。这个是判断要写的字节数太多
count = GLOBALMEM_SIZE - p;
/*用户空间->内核空间*/
if (copy_from_user(dev->mem + p, buf, count))
//目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0
ret = - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;//如果ret=count,则完成了所请求数目的字节传送。
}
/* seek文件定位函数 */
/*
seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET,0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END,2),globalmem支持从文件开头和当前位置相对偏移。在定位的时候,应该检查用户请求的合法性,若不合法,函数返回- EINVAL,合法时返回文件的当前位置*/
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig)
{
case 0: /*相对文件开始位置偏移*/
if (offset < 0)
{
ret = - EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE)//偏移越界
{
ret = - EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;//struct file
ret = filp->f_pos;
break;
case 1: /*相对文件当前位置偏移*/
if ((filp->f_pos + offset) > GLOBALMEM_SIZE)//偏移越界
{
ret = - EINVAL;
break;
}
if ((filp->f_pos + offset) < 0)
{
ret = - EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;//合法时返回文件的当前位置
}
/*文件操作结构体*/
static const struct file_operations globalmem_fops =
{
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
/*初始化并注册cdev
globalmem_setup_cdev(globalmem_devp, 0);*/
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
int err, devno = MKDEV(globalmem_major, index);
cdev_init(&dev->cdev, &globalmem_fops);
/*将 cdev 结构嵌入一个你自己的设备特定的结构,你应当初始化你已经分配的结构使用以上函数,有一个其他的 struct cdev 成员你需要初始化. 象 file_operations 结构,struct cdev 有一个拥有者成员,应当设置为 THIS_MODULE,一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:
cdev_add(&dev->cdev, devno, 1);*/
/*void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
cdev->kobj.ktype = &ktype_cdev_default;
kobject_init(&cdev->kobj);
cdev->ops = fops;
} */
/*
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
/*globalmem设备结构体*/
struct globalmem_dev
{
struct cdev cdev; /*cdev结构体*/
unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/
};
*/
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &globalmem_fops;
err = cdev_add(&dev->cdev, devno, 1);
/*
cdev 是 cdev 结构, devno 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1, 但是有多个设备号对应于一个特定的设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模式(例如密度), 通过安排多个次编号给每一个物理设备.在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码,
你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add */
if (err)
printk(KERN_NOTICE "Error %d adding LED%d", err, index);
}
/*设备驱动模块加载函数*/
/*_init():__init是一个宏,编译时会用到。对于静态编译在内核的 模块有意义.
内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init、__devinit等。
这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,
以减少内存占用和提高内核效率。
init():标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。
#define _ _init _ _attribute_ _ ((_ _section_ _ (".text.init")))
初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。
*/
int globalmem_init(void)
{
int result;
dev_t devno = MKDEV(globalmem_major, 0);
/*通过主次设备号生成dev_t.
主次设备号的数据类型是dev_t,在/linux/types.h中定义。
在2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,
其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:
MAJOR(dev_t dev); #获得主设备号
MINOR(dev_t dev); #获得次设备号
这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,
则可使用:
MKDEV(int major, int minor);*/
/* 申请设备号*/
if (globalmem_major)/*静态申请设备号*/
result = register_chrdev_region(devno, 1, "globalmem");
/*在linux/fs.h中,
devno:是你要分配的起始设备编号. devno的次编号部分常常是 0, 但是没有要求是那个效果;
1:是所请求的连续设备号的个数;
globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。
如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。
*/
else /* 动态申请设备号*/
{
result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
/*dev_t *dev
dev:是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数;
firstminor:应当是请求的第一个要用的次编号; 它常常是 0;
1: 是所请求的连续设备号的个数;
Globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices或/sysfs中。*/
globalmem_major = MAJOR(devno);/*获得主设备号,从dev_t结构中分解出主设备号*/
}
/*
如果globalmem_major=0,利用udev工具自动向系统动态申请未被占用的设备号相关函数定义在include/linux/kdev_t.h中
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
//(1<<20 -1) 此操作后,MINORMASK宏的低20位为1,高12位为0
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
*/
if (result < 0)
return result;
/* 动态申请设备结构体的内存*/
globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
/*
在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages. 请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!要注意kmalloc最大只能开辟128k-16,16个字节是被页描述符结构占用了。内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址*/
if (!globalmem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
/*memset会将参数globalmem_devp所指的内存区域中前sizeof(struct globalmem_dev)个字节以参数0填入,然后返回指向globalmem_devp的指针*/
globalmem_setup_cdev(globalmem_devp, 0);
/*初始化并注册cdev*/
return 0;
fail_malloc: unregister_chrdev_region(devno, 1);
/*释放原先申请的设备号
如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。
devno:是要分配的主设备号范围的起始值,次设备号一般设置为0;
1:是所请求的连续设备号的个数;*/
return result;
}
/*模块卸载函数*/
/*
__exit,标记退出代码,对于非模块无效。
*/
void globalmem_exit(void)
{
cdev_del(&globalmem_devp->cdev); /*注销cdev,为从系统去除一个字符设备*/
kfree(globalmem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
/*释放设备号
我们一般在模块的清除函数中调用设备号释放函数。*/
}
MODULE_AUTHOR("Song Baohua");
MODULE_LICENSE("Dual BSD/GPL");//指定代码使用的许可证
module_param(globalmem_major, int, S_IRUGO);
/*module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值*/
module_init(globalmem_init);
module_exit(globalmem_exit);
/*向操作系统注册自己定义的这两个函数,该项目在Kconfig中配置项目为布尔型的话为Y和N两种选项,Y为编译进内核,N不编译,如果为三态型(tristate),为Y,N,M三种选项M为编译为模块,模块也可以在内核编译后根据需要进行手动加载,也可以写个脚本自动加载*/
总结:
字符设备是3大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_opersation结构体中的操作函数,实现file_opersation结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。