Linux下的驱动有字符设备驱动,块设备驱动和网络设备驱动。无论哪一种设备驱动,为了减少内核的体积和易于内核的管理,驱动多以模块的形式出现。对于内核模块的编程,前面一节已经提到过,主要包括内核的加载函数和卸载函数,模块的作者和GPL协议。
对于简单的字符设备驱动而言,其实编写流程很简单,只要记住这个流程,就可以快速的写一个简单的字符设备驱动玩了。其流程主要有:
- 内核模块的加载和卸载
- 添加并实现file_operations设备驱动文件操作结构体
- 初始化并添加cdev结构体
- 实现operation中的函数体
内核模块的加载与卸载函数就相当简单了
//添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
//模块加载
int __init mymodule_init(void)
{
return 0;
}
//模块卸载
void __exit mymodule_exit(void)
{
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE(“GPL”);
添加并实现file_operations设备驱动文件操作结构体
file_operation 结构体的成员函数是字符设备驱动程序设计的主要内容。这里只添加其中的一部分函数。
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(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t); // 初始化一个异步的读取操作
ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*); // 向设备发送数据
ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t); // 初始化一个异步的写入操作
int(*readdir)(struct file *, void *, filldir_t); // 仅用于读取目录,对于设备文件,该字段为 NULL
unsigned int(*poll)(struct file *, struct poll_table_struct*); // 轮询函数,判断目前是否可以进行非阻塞的读取或写入
int(*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); // 执行设备 I/O 控制命令
long(*unlocked_ioctl)(struct file *, unsigned int, unsigned long); // 不使用 BLK 文件系统,将使用此种函数指针代替 ioctl
long(*compat_ioctl)(struct file *, unsigned int, unsigned long); // 在 64 位系统上,32 位的 ioctl 调用将使用此函数指针代替
int(*mmap)(struct file *, struct vm_area_struct*); // 用于请求将设备内存映射到进程地址空间
int(*open)(struct inode *, struct file*); // 打开
int(*flush)(struct file*); 30 int(*release)(struct inode *, struct file*); // 关闭
int(*synch)(struct file *, struct dentry *, 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(*readv)(struct file *, const struct iovec *, unsigned long, loff_t*);
ssize_t(*writev)(struct file *, const struct iovec *, unsigned long, loff_t*); // readv 和 writev:分散/聚集型的读写操作
ssize_t(*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void*); // 通常为 NULL
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); // 允许模块检查传递给 fcntl(F_SETEL...)调用的标志
int(*dir_notify)(struct file *filp, unsigned long arg); // 仅对文件系统有效,驱动程序不必实现 53 int(*flock)(struct file *, int, struct file_lock*);
};
修改模块代码
//添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int mymodule_open(struct inode *inode,struct file *filp)
{
return 0;
}
static int mymodule_release(struct inode *inode,struct file *filp)
{
return 0;
}
//*ppos 是要读写的位置对于读写文件开头的偏移
static ssize_t mymodule_read(struct file *filp,char __ user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
return ret;
}
static ssize_t mymodule_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
return ret;
}
//file_operation设备驱动文件操作结构体
static struct file_operations mymodule_fops = {
.owner = THIS_MODULE,
.open = mymodule_open,
.release = mymodule_release,
.read = mymodule_read,
.write = mymodule_write,
};
//模块加载
int __init mymodule_init(void)
{
return 0;
}
//模块卸载
void __exit mymodule_exit(void)
{
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE(“GPL”);
初始化并添加cdev结构体
cdev结构体描述字符设备,结构体的定义代码如下
struct cdev{
struct kobject kob; //内嵌kobject对象
struct module *owner; //所属模块
struct file_operations *ops; //文件操作结构体
struct list_head list;
dev_t dev; //设备号
unsigned int count;
};
cdev_t 成员定义了设备号,为32位,其中高12位为主设备号,底20位为次设备号。使用宏 MAJOR(dev_t dev) 从dev_t获取主设备号,MINOR(dev_t dev) 获取次设备号。 使用宏 MKDEV(int major,int minor) 通过主设备号和次设备号生成dev_t。
内核提供的cdev结构体的操作函数有
//cdev_init用于初始化cdev成员,并建立cdev和file_operation之间的关系
void cdev_init(struct cdev *cdev,struct file_operation *fops)
{
memset(cdev,0,sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
cdev->kobj.ktype = &ktype_cdev_default;
kobject_init(&cdev->kobj);
cdev->ops = fops;
}
//cdev_alloc用于动态申请一个cdev内存
struct cdev *cdev_alloc(void)
{
struct cdev *p = kmalloc(sizeof(struct cdev),GFP_KERNEL);
if(p){
menset(p,0,sizeof(struct cdev));
p->kobj.ktype = &ktype_cdev_dynamic;
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj);
}
return p;
}
//cdev_add()和cdev_del() 函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销,通常出现在字符设备的注册和注销函数中。
在调用cdev_add函数向系统注册字符设备之前,应先调用 register_chrdev_region() 或 alloc_chedev_region() 或 register_chrdev() 函数向系统申请设备号。调用cdev_del函数之后,还需调用unsigned_chadev() 或 unsignred_chadev_region() 释放之前申请的设备号。
- extern int register_chrdev_region(dev_t, unsigned, const char *); //静态的申请和注册设备号
- extern int alloc_chrdev_region(dev_t, unsigned, const char *); //动态的申请注册一个设备号
- extern int register_chrdev(unsigned int, const char *,struct file_operations *);//int为0时候动态注册,非零时候静态注册。
- extern int unregister_chrdev(unsigned int, const char *); //注销设备号
- extern void unregister_chrdev_region(dev_t, unsigned); //注销设备号
修改之前的模块的加载和卸载函数
//添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
static int major=0,minor=0;
//定义cdev结构体
struct mymodule_dev{
struct cdev cdev;
unsigned char buf[512];
};
struct mymodule_dev *mydev;
static int mymodule_open(struct inode *inode,struct file *filp)
{
filp->private_data = mydev;
return 0;
}
static int mymodule_release(struct inode *inode,struct file *filp)
{
return 0;
}
static ssize_t mymodule_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
struct mymodule_dev *dev = filp->private_data;
if(copy_to_user(buf,dev->buf,count))
return -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO"read %d bytes from %ld\n",count,p);
}
return ret;
}
static ssize_t mymodule_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
struct mymodule_dev *dev = filp->private_data;
if(copy_from_user(dev->buf,buf,count))
return -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO"write %d bytes from %ld\n",count,p);
}
return ret;
}
//file_operation设备驱动文件操作结构体
static struct file_operations mymodule_fops = {
.owner = THIS_MODULE,
.open = mymodule_open,
.release = mymodule_release,
.read = mymodule_read,
.write = mymodule_write,
};
//初始化并添加cdev结构体
static void mymodule_cdev_setup(struct mymodule_dev *dev)
{
int err,devno=MKDEV(major,minor);
//初始化
cdev_init(&dev->cdev,&mymodule_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &mymodule_fops;
//注册,添加
err = cdev_add(&dev->cdev,devno,1);
if(err)
printk(KERN_NOTICE"error %d adding mymodule",err);
}
//模块加载
int __init mymodule_init(void)
{
//申请设备号
int result;
dev_t devno = MKDEV(major,minor);
if(major)
result = register_chrdev_region(devno,1,"mymodule");
else{
result = alloc_chrdev_region(&devno,minor,1,"mymodule");
major = MAJOR(devno);
minor = MINOR(devno);
}
if(result<0)
return result;
//动态申请设备结构体内存
mydev = kmalloc(sizeof(struct mymodule_dev),GFP_KERNEL);
if(!mydev){
result=-ENOMEM;
goto fail_malloc;
}
memset(mydev,0,sizeof(struct mymodule_dev));
//cdev字符设备的初始化和添加
mymodule_cdev_setup(mydev);
return 0;
fail_malloc:unregister_chrdev_region(devno,1);
return result;
}
//模块卸载
void __exit mymodule_exit(void)
{
//删除cdev结构体
cdev_del(&mydev->cdev);
kfree(mydev);
//注销设备号
unregister_chrdev_region(MKDEV(major,minor),1);
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE("GPL");
添加到上一讲的Makefile中编译
将生成的ko文件复制到文件系统,insmod
然后使用 cat 命令查看驱动加载的情况和设备号
然后使用 mknod 创建设备节点
mknod /dev/mynodule c 252 0
写测试代码module_tese.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
int main()
{
char buf[50];
int fd = open("/dev/mymodules",O_RDWR);
write(fd,"hello world",sizeof("hello world"));
sleep(2);
read(fd,buf,strlen(buf));
for(int i=0;i<strlen(buf);i++)
printf("%c",buf[i]);
printf("\n");
close(fd);
return 0;
}
交叉编译,在qemu执行
对于设备节点的注册,也可以在代码中自动注册,注册代码为
//定义和创建设备类
struct class *my_class;
my_class = class_create(THIS_MODULE,"设备名称");
//定义和创建设备节点
struct device *my_device;
my_device = device_create(struct class *my_class,NULL,MKDEV(major,minor),NULL,"设备节点名");
//删除设备节点
device_destroy(struct class *my_class,MKDEV(major,minor));
//销毁设备类
class_destroy(struct class *my_class)
//头文件为 #include <linux/device.h>
修改后代码为
//添加头文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/device.h>
static int major=0,minor=0;
//定义cdev结构体
struct mymodule_dev{
struct cdev cdev;
unsigned char buf[512];
};
struct mymodule_dev *mydev;
struct class *my_class;
struct device *my_device;
static int mymodule_open(struct inode *inode,struct file *filp)
{
filp->private_data = mydev;
return 0;
}
static int mymodule_release(struct inode *inode,struct file *filp)
{
return 0;
}
static ssize_t mymodule_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 mymodule_dev *dev = filp->private_data;
if(copy_to_user(buf,dev->buf,count))
return -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO"read %d bytes from %ld\n",count,p);
}
return ret;
}
static ssize_t mymodule_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 mymodule_dev *dev = filp->private_data;
if(copy_from_user(dev->buf,buf,count))
return -EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO"write %d bytes from %ld\n",count,p);
}
return ret;
}
//file_operation设备驱动文件操作结构体
static struct file_operations mymodule_fops = {
.owner = THIS_MODULE,
.open = mymodule_open,
.release = mymodule_release,
.read = mymodule_read,
.write = mymodule_write,
};
//初始化并添加cdev结构体
static void mymodule_cdev_setup(struct mymodule_dev *dev)
{
int err,devno=MKDEV(major,minor);
//初始化
cdev_init(&dev->cdev,&mymodule_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &mymodule_fops;
//注册,添加
err = cdev_add(&dev->cdev,devno,1);
if(err)
printk(KERN_NOTICE"error %d adding mymodule",err);
}
//模块加载
int __init mymodule_init(void)
{
//申请设备号
int result;
dev_t devno = MKDEV(major,minor);
if(major)
result = register_chrdev_region(devno,1,"mymodule");
else{
result = alloc_chrdev_region(&devno,minor,1,"mymodule");
major = MAJOR(devno);
minor = MINOR(devno);
}
if(result<0)
return result;
//动态申请设备结构体内存
mydev = kmalloc(sizeof(struct mymodule_dev),GFP_KERNEL);
if(!mydev){
result=-ENOMEM;
goto fail_malloc;
}
memset(mydev,0,sizeof(struct mymodule_dev));
//cdev字符设备的初始化和添加
mymodule_cdev_setup(mydev);
//注册设备节点
my_class = class_create(THIS_MODULE,"mymodule_t");
my_device = device_create(my_class,NULL,MKDEV(major,minor),NULL,"mymodules");
return 0;
fail_malloc:unregister_chrdev_region(devno,1);
return result;
}
//模块卸载
void __exit mymodule_exit(void)
{
device_destroy(my_class,MKDEV(major,minor));
class_destroy(my_class);
//删除cdev结构体
cdev_del(&mydev->cdev);
kfree(mydev);
//注销设备号
unregister_chrdev_region(MKDEV(major,minor),1);
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_AUTHOR("HaoRan");
MODULE_LICENSE("GPL");
编译加载,其中的mymodule_t会以设备类的形式出现在/sys/class下
而mymodules作为设备节点出现在/dev下
在file_operation相关函数中的 inode 结构体记录了文件系统中文件的相关信息,包括文件的大小、创建者、创建日期等,我们称之他为“索引节点”,每个文件都有对应的inode,也就是说inode表示具体的文件。inode结构体中还包含了设备文件的主从设备号,可以用宏 unsigned int iminor(struct inode *inode)和 unsigned int imajor(struct inode *inode)获取主从设备号。
file 结构体是一个内核结构,代表一个打开的文件(文件描述符),在open时它由内核创建,并传递给file_operations所指向的各个函数,当文件关闭后给结构体被释放。该结构体记录了文件的读写模式、文件当前读写位置、还有file_operations结构体的指针等。