一、支持N个globalmem设备的驱动,在加载模块后需创建多个设备节点,如运行 mknod /dev/globalmem0 c 230 0使得/dev/globalmem0对应主设备号为globalmem_major、次设备号为0的设备,运行mknod/dev/globalmem1 c 230 1使得/dev/globalmem1对应主设备号为globalmem_major、次设备号为1的设备。分别读写/dev/globalmem0和/dev/globalmem1,发现都读写到了正确的对应的设备。
1、驱动源代码头文件global_mem.h
#ifndef __GLOBAL_MEM_H
#define __GLOBAL_MEM_H
#include <linux/cdev.h>
#define DRIVER_AUTHOR "xz@vi-chip.com.cn"
#define DRIVER_DESC "A sample driver"
#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define DEV_NAME "globalmem"
#define GLOBALMEM_MAJOR 230 /* 主设备号 */
#define DEVICE_NUM 10
/**
*
* 定义全局内存字符设备的结构体:
* 借用面向对象程序设计中“封装”的思想,体现良好的编程习惯。
*
*/
struct globalmem_dev {
struct cdev cdev;
unsigned char mem[GLOBALMEM_SIZE];
};
#endif /* __GLOBAL_MEM_H */
2、驱动源代码文件global_mem.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
/* include local head files */
#include "global_mem.h"
static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO); /* mode:S_IRUGO */
struct globalmem_dev *globalmem_devp = NULL;
/**
*
* 读写函数
* 让设备结构体globalmem_dev的mem[]数组与用户空间交互数据,
* 并随着访问的字节数变更更新文件读写偏移位置。
*/
// globalmem设备驱动的读函数,从设备中读取数据
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; // 要读的位置相对于文件开头的偏移,如果该偏移大于或等于GLOBALMEM_SIZE,表示已经到达文件末尾,所以返回0(EOF)。
unsigned int count = size; // 要读取的字节数
int ret = 0;
struct globalmem_dev *dev = filp->private_data; // 使用文件的私有数据访问设备结构体
if (p >= GLOBALMEM_SIZE)
return 0; // 表示已经到达文件末尾,所以返回0(EOF)
if (count > (GLOBALMEM_SIZE - p))
count = GLOBALMEM_SIZE - p;
if (copy_to_user(buf, dev->mem + p, count)) { // 把内核空间中的数据拷贝到用户空间
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
return ret; // 实际读取的字节数
}
// globalmem设备驱动的写函数,向设备发送数据
static ssize_t globalmem_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 globalmem_dev *dev = filp->private_data;// 使用文件的私有数据访问设备结构体
if (p >= GLOBALMEM_SIZE)
return 0;
if (count > (GLOBALMEM_SIZE - p))
count = GLOBALMEM_SIZE - p;
if (copy_from_user(dev->mem + p, buf, count)) // 把用户空间中的数据拷贝到内核空间
ret = -EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
// globalmem设备驱动的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: /* 从文件开头位置 seek */
if (offset < 0) {
ret = -EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /* 从文件当前位置开始 seek */
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;
}
// ioctl函数
// globalmem设备驱动的ioctl函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl函数应该返回-EINVAL。
static long globalmem_ioctl(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 int globalmem_open(struct inode *inode, struct file *filep)
{
printk(KERN_INFO "globalmem_open\n");
// container_of宏的解释说明 宏container_of()的作用是通过结构体成员的指针找到对应结构体的指针
// 1、ptr:表示结构体中member的地址
// 2、type:表示结构体类型
// 3、member:表示结构体中的成员
// 4、通过ptr的地址可以返回结构体的首地址
struct globalmem_dev *dev = container_of(inode->i_cdev, struct globalmem_dev, cdev);// 通过结构体变量中某个成员的首地址获得整个结构体变量的首地址。
filep->private_data = dev;// 设置私有数据,将文件的私有数据private_data指向设备结构体的实例,体现Linux的面向对象的设计思想
return 0;
}
static int globalmem_release(struct inode *inode, struct file *filep)
{
printk(KERN_INFO "globalmem_release\n");
return 0;
}
// globalmem设备驱动的文件操作结构体
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
void globalmem_setup_cdev(struct globalmem_dev *dev, int minor)
{
int err, devno = MKDEV(globalmem_major, minor);
cdev_init(&dev->cdev, &globalmem_fops); // 初始化字符设备
dev->cdev.owner = THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1); // 注册字符设备到内核
if (err)
printk(KERN_NOTICE "Error %d adding globalmem [%d]", err, minor);
}
static int __init global_mem_init(void)
{
int ret;
int i;
dev_t devno = MKDEV(globalmem_major, 0); /* 主设备号、次设备号合并为设备号 */
if (globalmem_major)
ret = register_chrdev_region(devno, DEVICE_NUM, DEV_NAME); // 静态分配设备号
else {
ret = alloc_chrdev_region(&devno, 0, DEVICE_NUM, DEV_NAME); // 动态分配设备号
globalmem_major = MAJOR(devno); // 提取主设备号
}
if (ret < 0)
return ret;
globalmem_devp = kzalloc(sizeof(struct globalmem_dev) * DEVICE_NUM, GFP_KERNEL); /* 分配内存并且清零 */
if (!globalmem_devp) {
ret = -ENOMEM;
goto fail_kmalloc;
}
for (i = 0; i < DEVICE_NUM; i++)
globalmem_setup_cdev(globalmem_devp + i, i);// 注册字符设备
printk(KERN_INFO "----global_mem_init----\n");
return 0; // 表示成功
fail_kmalloc:
unregister_chrdev_region(devno, DEVICE_NUM); // 注销分配设备号
return ret;
}
static void __exit global_mem_exit(void)
{
int i;
for (i = 0; i < DEVICE_NUM; i++)
cdev_del(&(globalmem_devp + i)->cdev);// 注销字符设备
kfree(globalmem_devp);// 释放分配的内存空间
unregister_chrdev_region(MKDEV(globalmem_major, 0), DEVICE_NUM);// 注销分配设备号
printk(KERN_INFO "----global_mem_exit----\n");
}
module_init(global_mem_init);
module_exit(global_mem_exit);
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_ALIAS(DRIVER_DESC);
3、Makefile
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
else
obj-m := global_mem.o
endif
4、验证
支持N个globalmem设备的驱动,在加载模块后需创建多个设备节点,如运行mknod /dev/globalmem0 c 230 0,使得/dev/globalmem0对应主设备号为globalmem_major、次设备号为0的设备,运行mknod /dev/globalmem1 c 230 1,使得/dev/globalmem1对应主设备号为globalmem_major、次设备号为1的设备。分别读写/dev/globalmem0和/dev/globalmem1,发现都读写到了正确的对应的设备。
二、总结
字符设备是三大类设备(字符设备、块设备和网络设备)中的一类,其驱动程序完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operations结构体中的操作函数,实现file_operations结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。