字符设备驱动之体验篇
三.字符设备之编程
通过程序来体验字符设备驱动编程的过程
在Linux系统中,字符设备驱动由如下几个部分组成:
(1)字符设备驱动模块加载与卸载函数
(2)字符设备驱动的file_operations结构体中成员函数
file_operations结构体中成员函数是字符设备驱动与内核的接口,是用户空间对Linux进行系统调用最终的实现着。
(3)在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员。
它完成以下的功能:
1.对设备初始化和释放。
2.把数据从内核传送到硬件和从硬件读取数据。
3.读取应用程序传送给设备文件的数据和回送应用程序请求的数据。
4.检测和处理设备出现的错误。
1.设备驱动的头文件,宏即设备结构体
(1)头文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
(2)宏和全局变量
#define GLOBALMEM_MAJOR 254 //预设的globalmem的主设备号
#define GLOBALMEM_SIZE 0x1000 //全局内存最大为4kb
#define MEM_CLEAR 0x1 //清空全局内存
static globalmem_major = GLOBALMEM_MAJOR //主设备号
//globalmem设备结构体
struct globalmem_dev
{
struct cdev cdev; //cdev结构体,设备编程,必须的
unsigned char mem[GLOBALMEM_SIZE];//全局内存
};
struct globalmem_dev *dev;//设备结构体实例
1>定义globalmem_dev设备结构体包含了对应于globalmem字符设备的cdev,使用的内存mem[GLOBALMEM_SIZE].这样做体现了面向对象程序设计中“封装“的思想。
2.加载和卸载设备驱动
(1)在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,申请设备结构体的内存
static int __init global_init(void)
{
int result;
dev_t devno = MKDEV(globalmem_major,0)//保存设备号
//申请设备号
if(globalmem_major)
result = register_chrdev_region(devno,1,"globalmem");
else{//动态获得主设备号
result = alloc_chrdev_region(&devno,0,1,"globalmem")
globalmem_major = MAJOR(devno);
}
if(result < 0)
return restult;
dev = kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
if(!dev)//申请失败
{
result = -ENOMEM;
goto fail_malloc;
}
memset(dev,0,sizeof(struct globalmem_dev));
globalmem_setup_cdev(devno, 0);//字符设备的注册
return 0;
fail_malloc :
unregister_chrdev_region(devno,1);
return result;
}
(2)在卸载函数中应实现设备号的释放和cdev的注销
static void __exit global_exit(void)
{
cdev_del(&globalmem_devp->cdev); /*注销cdev,即从系统中删除此设备*/
kfree(globalmem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); /*释放设备号*/
}
要先注销字符设备之后,才能释放设备号
(3)globalmem_setup_cdev()函数完成cdev的初始化和添加
static void globalmem_setup_cdev(struct globalmem_dev *dev,int index)
{
int err;
dev_t devno = MKDEV(globalmem_major,index);
cdev_init(&dev->cdev,&globalmem_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &globalmem_fops;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}
1>在获取了设备号范围之后,需要将设备添加到字符设备结构体中,以激活设备。这需要用cdev_init初始化一个sturct cdev的实例,接下来调用cdev_add。在cdev_add成功返回后,设备进入活动状态。
2>在cdev_init()函数中,与globalmem的dev关联的file_operations结构体代码为
static const struct file_operations globalmem_fops =
{
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.open = globalmem_open,
.read = globalmem_read,
.write = globalmem_write,
.ioctl = globalmem_ioctl,
.release = globalmem_release,
};
3>申请设备号后在这部分中,比较重要的是在用函数获取设备编号后,其中的参数name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。
3.file_operations中具体函数的实现
打开的设备在内核内部由file结构标识,内核使用file_operations结构访问驱动程序的函数。下面主要介绍常用的几个成员:
(1)文件打开函数
int globalmem_open(struct inode *inode,struct file *filp)
{
//将设备结构体指针赋值给文件私有数据指针
filp->private_data = globalmem_devp;
}
(2)读写函数
globalmem设备驱动的读写函数主要是让设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更返回用户的文件读写偏移位置。
1>读函数
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)
return count ? - ENXIO: 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
/*从内核空间向用户空间写数据*/
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;
}
2>写函数
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 count ? - ENXIO: 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 %d bytes(s) from %d/n", count, p);
}
return ret;
}
(3)文件定位函数
/* seek文件定位函数*/
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;
}
//f_pos文件当前的读写位置
//SEEK_SET
filp->f_pos = (unsigned int)offset;
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;
}
/*SEEK_CUR*/
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;
}
(4)ioctl设备控制函数
/*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;
}
(5)文件释放函数
int globalmem_release(struct inode *inode,struct file *filp)
{
return 0;
}
说明:
1>一般都是将文件的私有数据private_data 指向设备结构体,read(),write(),ioctl(),llseek()等函数通过private_data访问设备结构体。
2>除了在globalmem_open()函数中通过filp->private_data = globalmem_devp将设备结构体指针赋值给文件私有数据指
针并在
globalmem_write(),globalmem_read(),globalmem_llseek()和
globalmem_ioctl()函数中通过struct globalmem_dev *dev = filp->private_data语句获得设备结构体指针并使用该指针操作设备结构体。
3>如果globalmem不只包括一个设备,而是同时包括两个或两个以上的设备,采用private_data的优势就会显示出来。
4>在对globalmem_read(),globalmem_write(),globalmem_ioctl()等重要函数及globalmem_fops结构体等数据结构体进行任何修改的前提下,只是简单地修改globalmem_init(),globalmem_exit()和globalmem_open()就可以轻松地让globalmem驱动中包含两个同样的设备(次设备好分别为0和1)
4.支持两个globalmem设备的globalmem驱动
(1)文件打开函数
int globalmem_open(struct inode *inode,struct file *file)
{
//将设备结构体指针赋值给文件私有数据指针
struct globalmem_dev *dev;
dev = container_of(inode->i_cdev,struct globalmem_dev,cdev);
filp->private_data = dev;
return 0;
}
1>container_of的作用是通过结构体成员的指针找到对应结构体的指针,这个技巧在Linux内核编程中十分常用。在container_of(inode->i_cdev,struct globalmem_dev,cdev)语句中,传给container_of()的第一个参数是结构体成员的指针,第二个参数为整个结构体的类型,第三个参数为传入的第一个参数即结构体成员的类型,container_of()返回值为整个结构体的指针。
(2)设备驱动模块加载函数
int globalmem_init(void)
{
int result;
dev_t devno = MKDEV(globalmem_major,0);
//申请设备号
if(globalmem_major)
result = register_chrdev_region(devno,2,"globalmem");
else//动态申请设备号
{
result = alloc_chrdev_region(&devno,0,2,"globalmem");
globalmem_major = MAJOR(devno);
}
if(result < 0)
return result;
//动态申请两个设备结构体的内存
globalmem_devp=kmalloc(2*sizeof(struct globalmem_dev),GFP,KERNEL);
if(!globalmem_devp) //申请失败
{
result = - ENOMEM;
goto fail_malloc;
}
memset(globalmem_devp,0,2*sizeof(struct globalmem_dev));
globalmem_setup_cdev(&globalmem_devp[0],0);
globalmem_setup_cdev(&globalmem_devp[1],1);
return 0;
fail_malloc: unregister_chrdev_region(devno, 2);
return result;
}
(3)模块卸载函数
void globalmem_exit(void)
{
cdev_del(&(globalmem_devp[0].cdev));
cdev_del(&(globalmem_devp[1].cdev));
kfree(globalmem_devp);//释放设备结构体内存
unregister_chrdev_region(MKDEV(globalmem_major,0),2);//释放设备号
}
1>在支持两个globalmem设备的驱动,在加载模块后需创建两个设备节点:
/dev/globalmem0对应主设备号globalmem_major,次设备号0,/dev/globalmem1对应主设备号globalmem_major,次设备号1。分别读写/dev/globalmem0和/dev/globalmem1,发现都可以读写到正确的对应设备。
四.globalmem驱动在用户空间的验证
方法一
1.编译globalmem驱动后,得到globalmem.ko文件。运行"insmod globalmem.ko" 命令加载模块,通过"lsmod"命令,发现globalmem模块被加载。在通过“cat/proc/devices”命令查看。
2.接下来,通过"sudo mknod /dev/globalmem c 245 0"命令创建“/dev/gbobalmem”设备节点,并通过“echo hello world>/dev/globalmem”命令和“cat /dev/globalmem”命令分别验证设备的写和读。
3.cd /dev 里可以查看建立的设备节点
如果权限不够用sudo chmod 777 globalmem改权限
方法二
编写应用层程序进行验证
1.头文件
#include
#include
2.
int main()
{
int myfile;
char buffer[100];
int retval;
myfile = open("/dev/global",O_RDWR);
if(myfile < 0){
printf("Open failed/n");
}
write(myfile,"hello,tiger",sizeof("hello,tiger"));
close(myfile);
myfile = open("/dev/global",O_RDWR);
if(myfile < 0){
printf("Open failed/n");
}
retval = read(myfile,buffer,100);
buffer[retval] = 0;
printf("Response:%s/n",buffer);
close(myfile);
}