字符驱动框架
字符设备驱动加载函数
有了《内核模块编写及编译》和 《字符设备驱动(1)》的基础,并且字符设备驱动对上注册给内核,对下要操作硬件,所以分成2层:
//加载函数(必须)
static int hello_init(void)//返回值是int类型,函数名自定义,参数是void
{
printk(KERN_ALERT "Hello World.\n");
//up kernel
//down hardware
return 0;
}
备注:上面只写了加载函数。
其中对上注册给内核,又分成3步:1、注册设备号,2、初始化字符设备结构体,3、添加字符设备结构体给内核。类似去体检,先要预约个体检号(第一步),去之前要准备一下,比如前一天饮食清淡点,早上空腹等等(第二步),最后在去体检(第三步)。
这三步分别有3个函数对应:
#include <linux/fs.h>
//1、注册设备号,下面2个二选一
//1.1、提前规划好要用的设备号,向内核单独申请
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//或者
//1.2、不知道使用哪些设备号,向内核动态申请
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
#include <linux/cdev.h>
//2、初始化字符设备结构体
void cdev_init(struct cdev *cdev , const struct file_operations *fops)
//3、添加字符设备结构体给内核
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
所以字符设备驱动加载函数如下:
//需要是全局变量
struct cdev cdevice;
struct file_operations cdev_ops;
//加载
static int hello_init(void)
{
dev_t devno = MKDEV(MAJOR_CHAR,MINOR_CHAR);
int ret = -1;
printk(KERN_ALERT "Hello World.\n");
//up kernel
//1、注册设备号
ret = register_chrdev_region(devno, 1, "hello");
if (0 != ret)
{
printk("Register char device failed.\n");
return ret;
}
//2、初始化字符设备结构体
cdev_init(&cdevice, &cdev_ops);
cdevice.owner = THIS_MODULE;
//3、添加字符设备结构体给内核
ret = cdev_add(&cdevice,devno , 1);
if (0 != ret)
{
//注意释放设备号
unregister_chrdev_region(devno,1);
printk("Unregister char device.\n");
return ret;
}
printk("Register char device success.\n");
//down hardware
return 0;
}
其中cdevice、cdev_ops不能定义为局部变量,因为这2个结构体是要在驱动(模块)加载完成后也需要长期存在,要挂载进内核,即要在hello_init函数执行完后也存在,若定义为hello_init函数中的局部变量,则函数执行完就释放掉了。
另外在cdev_add的异常分支注意需要释放设备号。
字符设备驱动卸载函数
字符设备驱动卸载函数也分成2层,和加载函数流程相反,先释放硬件资源,再从内核中注销,注销分成2部:1、从内核中删除字符设备结构体,2、注销设备号
其中从内核中删除字符设备结构体如下
#include <linux/cdev.h>
void cdev_del(struct cdev *cdevice)
卸载函数如下:
//卸载函数(必须)
static void hello_exit(void)//返回值是void类型,函数名自定义,参数是void
{
dev_t devno = MKDEV(MAJOR_CHAR, MINOR_CHAR);
printk(KERN_ALERT "Goodbye World.\n");
// down hardware
// up kernel
//1、从内核中删除字符设备结构体
cdev_del(&cdevice);
//2、注销设备号
unregister_chrdev_region(devno, 1);
}
字符设备操作
要对字符设备进行操作,可以像对文件操作,比如打开、关闭、读写等等,也就是对struct file_operations cdev_ops赋值,其成员是函数指针,赋值如下:
static int my_open(struct inode *pnode, struct file *pfile)
{
printk("Open cdev.\n");
return 0;
}
static int my_close(struct inode *pnode, struct file *pfile)
{
printk("Close cdev.\n");
return 0;
}
struct file_operations cdev_ops = {
.open = my_open,
.release = my_close,
};
字符设备驱动完整框架
cdev.c
//head
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#define MAJOR_CHAR 100
#define MINOR_CHAR 0
static int my_open(struct inode *pnode, struct file *pfile)
{
printk("Open cdev.\n");
return 0;
}
static int my_close(struct inode *pnode, struct file *pfile)
{
printk("Close cdev.\n");
return 0;
}
struct cdev cdevice;
struct file_operations cdev_ops = {
.open = my_open,
.release = my_close,
};
//加载
static int hello_init(void)
{
dev_t devno = MKDEV(MAJOR_CHAR,MINOR_CHAR);
int ret = -1;
printk(KERN_ALERT "Hello World.\n");
//up kernel
//1、注册设备号
ret = register_chrdev_region(devno, 1, "hello");
if (0 != ret)
{
printk("Register char device failed.\n");
return ret;
}
//2、初始化字符设备结构体
cdev_init(&cdevice, &cdev_ops);
cdevice.owner = THIS_MODULE;
//3、添加字符设备结构体给内核
ret = cdev_add(&cdevice,devno , 1);
if (0 != ret)
{
//注意释放设备号
unregister_chrdev_region(devno,1);
printk("Unregister char device.\n");
return ret;
}
printk("Register char device success.\n");
//down hardware
return 0;
}
//卸载函数(必须)
static void hello_exit(void)//返回值是void类型,函数名自定义,参数是void
{
dev_t devno = MKDEV(MAJOR_CHAR, MINOR_CHAR);
printk(KERN_ALERT "Goodbye World.\n");
// down hardware
// up kernel
//1、从内核中删除字符设备结构体
cdev_del(&cdevice);
//2、注销设备号
unregister_chrdev_region(devno, 1);
}
//注册(必须)
module_init(hello_init);
module_exit(hello_exit);
//license(必须)
MODULE_LICENSE("GPL");
//作者与描述(可选)
MODULE_AUTHOR("Ono Zhang");
MODULE_DESCRIPTION("A simple Hello World Module");
Makefile见《内核模块编写及编译》。
字符设备使用
那怎么把应用(app)、设备文件(mydev)、字符设备(cdevice)、操作(cdev_ops)联系起来呢?或者说应用怎么调用到驱动中的操作方法呢?需要3步:
第一,加载驱动,假设上述驱动文件编译出位cdev.ko,则使用:
#insmode cdev.ko
第二,创建设备文件,其中100是主设备号(cdev.c里面的MAJOR_CHAR ),0是次设备号(cdev.c里面的MINOR_CHAR )
#mknod /dev/mydev c 100 0
第三,编写应用文件,如下:
app.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#define FILE_NAME "/dev/mydev"
int main(void)
{
int fd;
fd = open(FILE_NAME, O_RDWR);
if (0 > fd)
{
printf("Open failed.\n");
return -1;
}
close(fd);
}
使用gcc编译
#gcc app.c
执行此文件
#./a.out
Open cdev.
Close cdev.
这样用户态应用的打开、关闭设备文件,就调动到了内核态的驱动的my_open和my_close。
这是为什么呢?简单来讲如下图:
第1步,加载驱动(insmod),把字符设备号(devno)、字符设备结构体(cdevice)、字符设备操作(cdev_ops),关联起来,并放到内核里面。
第2步,创建设备文件,把设备文件(/dev/mydev)和字符设备号关联起来。
第3步,应用程序调用open函数,会调用到系统调用函数sys_open,然后根据/dev/mydev在内核中找到devno,找到devno就能找到对应的cdevice结构体,找到cdevice结构体,就能找到其操作方法cdev_ops,最终open就调用到cdev_ops的my_open函数了。
tobecontinue
每周三、周六更新