Linux内核分析(五)----字符设备驱动实现

时间:2022-03-21 14:07:48

原文:Linux内核分析(五)----字符设备驱动实现

Linux内核分析(五)

昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷,我们都会以虚拟的设备为例进行学习,所以大家不必害怕没有硬件的问题。

今天我们会分析到以下内容:

1.      字符设备驱动基础

2.      简单字符设备驱动实现

3.      驱动测试

字符设备基础

1.       字符设备描述结构

在linux2.6内核中,使用cdev结构体描述一个字符设备,其定义如下:

 struct cdev {
struct kobject kobj;/*基于kobject*/
struct module *owner; /*所属模块*/
const struct file_operations *ops; /*设备文件操作函数集*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count; /*该种类型设备数目*/
};

上面结构中需要我们进行初始化的有ops和dev,下面我们会对这两个成员进行分析。

注:kobject结构是驱动中很重要的一个结构,由于其复杂性,我们现在不进行介绍,后面会详细介绍。

2.       设备号

1.        何为设备号:cdev结构体中dev成员定义了设备号,而dev_t则为U32类型的也就是32位,其中12位为主设备号,20位为次设备号。我们执行ls –l /dev/可看到下图,其中左边红框为主设备号,右边为次设备号

Linux内核分析(五)----字符设备驱动实现

2.        何为主设备号:用来对应该设备为何种类型设备。(比如串口我们用一个数字识别,而串口有好几个)

3.        何为次设备号:用来对应同一类型设备下的具体设备。(用次设备号来具体区分是哪个串口)

4.        设备号相关操作:

1.        通过主设备号和次设备号获取devdev = MKDEV(主,次);

2.        通过dev获取主设备号:主 = MAJOR(dev);

3.        通过dev获取次设备号:dev = MINOR(dev);

5.        设备号分配:设备号的分配有两种方式,一种是静态的,另一种是动态的,下面一一分析

1.        静态分配:也就是程序员自己指定设备号,通过register_chrdev_region();函数向内核申请,可能会导致和内核已有的冲突,从而失败。

2.        动态分配:通过 alloc_chrdev_region(); 函数向内核申请设备号。

3.        释放设备号:通过 unregister_chrdev_region(); 释放申请到的设备号。

3.       file_operations操作函数集

file_operations结构体中的成员函数在我们驱动开发过程中极为重要,其中的内容相当庞大,下面我们看看其定义:

 struct file_operations {
struct module *owner;/*拥有该结构的模块的指针,一般为THIS_MODULES*/
loff_t (*llseek) (struct file *, loff_t, int); /*用来修改当前文件的读写指针*/
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);/*从设备读取数据*/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);/*向设备发送数据*/
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一个异步的读取操作*/
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); /*初始化一个异步的写入操作*/
int (*readdir) (struct file *, void *, filldir_t); /*只用于读取目录,对于设备文件该字段为NULL*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);/*轮询函数,判断目前是否可以进行非阻塞的读取或写入*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 不用BLK的文件系统,将使用此函数代替ioctl*/
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); /* 代替ioctl*/
int (*mmap) (struct file *, struct vm_area_struct *);/*用于请求将设备内存映射到进程地址空间*/
int (*open) (struct inode *, struct file *);/*打开*/
int (*flush) (struct file *, fl_owner_t id); /*在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. */
int (*release) (struct inode *, struct file *);/*关闭*/
int (*fsync) (struct file *, int datasync); /*刷新待处理数据*/
int (*aio_fsync) (struct kiocb *, int datasync); /*异步fsync*/
int (*fasync) (int, struct file *, int); /*通知设备FASYNC标志发生变化*/
int (*lock) (struct file *, int, struct file_lock *);/* 实现文件加锁*/
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*通常为NULL*/
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*在当前的进程地址空间找的一个未映射的内存段*/
int (*check_flags)(int); /*法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志*/
int (*flock) (struct file *, int, struct file_lock *);/**/
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /*由VFS调用,将管道数据粘贴到文件*/
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /*由VFS调用,将文件数据粘贴到管道*/
int (*setlease)(struct file *, long, struct file_lock **);/**/
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len); /**/
};

上面结构体中的函数指针所指向的函数,在我们在进行open、write、read等系统调用的时候最终会被调用到,所以我们的驱动中想为应用层实现那种调用就要在此实现。

4.       字符设备驱动初始化

我们通过上面的分析对设备号和操作函数集有了一定的了解下面我们来看字符设备驱动初始化,其主要步骤如下。

1.        分配cdev结构:有静态(直接定义)动态(cdev_alloc();)两种方式

2.        初始化cdev结构:使用 cdev_init(struct cdev *cdev, const struct file_operations *fops) 初始化

3.        驱动注册:使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count)//count为该种类型的设备个数注册

4.        硬件初始化:阅读芯片手册进行硬件设备的初始化

5.        完成操作函数集:实现要用的操作(设备方法)

6.        驱动注销:使用 void cdev_del(struct cdev *p) 注销

5.       字符设备驱动模型及调用关系

下面我通过一张图将字符设备的驱动结构、以及字符设备驱动与用户空间的调用关系进行展示:

Linux内核分析(五)----字符设备驱动实现

6.       遗漏知识

我们内核空间和用户空间的数据交互要用到下面两个函数:

 copy_from_user();//从用户空间读
copy_to_user();//写入用户空间 

简单字符设备驱动实现

经过上面的分析我们对字符设备有一定了解,下面我们来完成一个最简单的字符设备驱动。我只展示最主要的代码,整个项目工程在https://github.com/wrjvszq/myblongs.git欢迎大家关注。

1.       字符设备驱动编写

因为驱动本身就是一个内核模块,下面的字符设备驱动只实现了部分方法,在后面的博客中我们会基于此驱动慢慢修改,希望大家掌握。

 #include<linux/module.h>
#include<linux/init.h>
#include<linux/cdev.h>
#include<linux/fs.h>
#include<asm/uaccess.h> #define MEM_SIZE 1024 MODULE_LICENSE("GPL"); struct mem_dev{
struct cdev cdev;
int mem[MEM_SIZE];//全局内存4k
dev_t devno;
}; struct mem_dev my_dev; /*打开设备*/
int mem_open(struct inode *inode, struct file *filp){
int num = MINOR(inode->i_rdev);/*获取次设备号*/ if(num == ){/*判断为那个设备*/
filp -> private_data = my_dev.mem;/*将设备结构体指针复制给文件私有数据指针*/
}
return ;
}
/*文件关闭函数*/
int mem_release(struct inode *inode, struct file *filp){
return ;
} static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){
int * pbase = filp -> private_data;/*获取数据地址*/
unsigned long p = *ppos;/*读的偏移*/
unsigned int count = size;/*读数据的大小*/
int ret = ; if(p >= MEM_SIZE)/*合法性判断*/
return ;
if(count > MEM_SIZE - p)/*读取大小修正*/
count = MEM_SIZE - p; if(copy_to_user(buf,pbase + p,size)){
ret = - EFAULT;
}else{
*ppos += count;
ret = count;
} return ret;
} 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 = ;
int *pbase = filp -> private_data; if(p >= MEM_SIZE)
return ;
if(count > MEM_SIZE - p)
count = MEM_SIZE - p; if(copy_from_user(pbase + p,buf,count)){
ret = - EFAULT;
}else{
*ppos += count;
ret = count;
}
return ret;
} /*seek文件定位函数*/
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence){ loff_t newpos; switch(whence) {
case SEEK_SET:/*从文件头开始定位*/
newpos = offset;
break;
case SEEK_CUR:/*从当前位置开始定位*/
newpos = filp->f_pos + offset;
break;
case SEEK_END:
newpos = MEM_SIZE * sizeof(int)- + offset;/*从文件尾开始定位*/
break;
default:
return -EINVAL;
} if ((newpos<) || (newpos>MEM_SIZE * sizeof(int)))/*检查文件指针移动后位置是否正确*/
return -EINVAL; filp->f_pos = newpos;
return newpos; } const struct file_operations mem_ops = {
.llseek = mem_llseek,
.open = mem_open,
.read = mem_read,
.write = mem_write,
.release = mem_release,
}; static int memdev_init(void){
int ret = -; /*动态分配设备号*/
ret = alloc_chrdev_region(&my_dev.devno,,,"memdev");
if (ret >= ){
cdev_init(&my_dev.cdev,&mem_ops);/*初始化字符设备*/
cdev_add(&my_dev.cdev,my_dev.devno,);/*添加字符设备*/
} return ret;
} static void memdev_exit(void){
cdev_del(&my_dev.cdev);
unregister_chrdev_region(my_dev.devno,); } module_init(memdev_init);
module_exit(memdev_exit);

驱动测试

经过上面的代码我们已经实现了一个简单的字符设备驱动,我们下面进行测试。(应用程序在https://github.com/wrjvszq/myblongs.git 上)

1.       加载内核模块

我们使用 insmod memdev.ko 命令加载内核模块

2.       获取设备号

我们的设备号是动态申请到的,所以我们要通过下面的命令查看设备号

cat /proc/devices

找到我们的设备memdev的设备号

Linux内核分析(五)----字符设备驱动实现

3.       建立设备文件

使用如下命令建立设备文件

mknod /dev/文件名 c 主设备号次设备号

上面命令中文件名为我们在应用程序中打开的文件名

c代表字符设备

主设备号为上一步找到的,我的位249

次设备号非负即可,但不能超过自己所创建的设备数。

比如我的就是 mknod /dev/memdev0 c

4.       编译应用程序并测试

使用gcc对应用程序进行编译,然后先使用write对设备进行写入,在使用read对设备读取,完成测试。