前言
代码结构简单,旨在用最简单的原理理解最主要的框架逻辑,细节需要自行延伸。 -----------------学习的基础底层逻辑
基础步骤
开发linux内核驱动需要以下4个步骤:
- 编写驱动代码
- 编写makefile
- 编译和加载驱动
- 编写应用程序测试驱动
由于硬件设备各式各样,有了设备驱动程序,应用程序就可以不用在意设备的具体细节,而方便地与外部设备进行通信。从外部设备读取数据,或是将数据写入外部设备,即对设备进行控制。
设备驱动程序框架
- module_init 是 linux kernel 绝大多数模块的起始点。
- 我们所熟悉的应用程序都是从一个 main() 函数开始运行的,而与应用程序不同,内核模块的起始就是 module_init() 标记的函数 。
- module_init 是一个宏,它的参数就是模块自行定义的“起始函数”。这个函数使用 module_init 标记后,就会在内核初始化阶段,“自动”运行。
- 无论模块是编译进内核镜像,还是以ko的形式加载,都是从这里开始运行。
- 有开始就有结束,与 module_init 对应的就是 module_exit 。module_exit 负责进行一些和init反向的活动。
设备的种类繁多,所以设备的驱动程序也是各式各样的,SVR4是UNIX操作系统的一种内核标准。
规范有以下部分:
- 驱动程序与内核的接口:是通过数据结构file_opration完成的
- 驱动程序与设备的接口:描述了驱动程序如何与设备交互,这与具体的设备是密切相关的。
- 驱动程序与系统引导的接口:驱动程序对设备进行初始化
接下来使用mknod命令创建设备文件结点,然后用chmod命令修改权限为777。此时设备就可以使用了。
/proc/devices与/dev的不同之处
- 在/proc/devices下,显示的是驱动程序生成的设备及其主设备号。其中主设备号可用来让mknod作为参数。
- 在/dev下的设备是mknod生成的设备,其中,用户通过使用/dev下的设备名来使用设备。
上层应用如何调用底层驱动
- 应用层的程序open(“/dev/xxx”,mode,flags)打开设备文件,进入内核中,即虚拟文件系统中。
- VFS层的设备文件有对应的struct inode,其中包含该设备对应的设备号,设备类型,返回的设备的结构体。
- 在驱动层中,根据设备类型和设备号就可以找到对应的设备驱动的结构体,用i_cdev保存。该结构体中有很重要的一个操作函数接口file_operations。
- 在打开设备文件时,会分配一个struct file,将操作函数接口的地址保存在该结构体中。
- VFS层 向应用层返回一个fd,fd是和struct file相对应,这样,应用层可以通过fd调用操作函数,即通过驱动层调用硬件设备了。
代码
起始函数
module_init(hello_init);
下面的函数的主要工作是:
- 注册驱动
- 申请资源
- 节点创建
int hello_init(void)//三件事情
{
devNum = MKDEV(reg_major, reg_minor);
if(OK == register_chrdev_region(devNum, subDevNum, "helloworld"))//驱动注册
{
printk(KERN_EMERG"register_chrdev_region ok \n");
}else {
printk(KERN_EMERG"register_chrdev_region error n");
return ERROR;
}
printk(KERN_EMERG" hello driver init \n");
gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);//资源申请
gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);//资源申请
gFile->open = hello_open;
gFile->read = hello_read;
gFile->write = hello_write;
gFile->owner = THIS_MODULE;
cdev_init(gDev, gFile);
cdev_add(gDev, devNum, 3);//把它添加到系统中去
return 0;
}
驱动注册
将驱动信息注册完毕后,如果有匹配的设备接入,本驱动的 probe 就会被调用。
原生代码里有很多进一步对 module_init 封装的宏,其实做的也就是注册驱动这件事。
申请资源&创建节点
-
并非所有的模块都是驱动模块,又有一些纯软件功能的模块,例如class、bus管理等。
-
这类模块在其实函数中,则不需要注册驱动,而是按照本模块的需求,申请资源,创建节点等。
-
需要注意的是,所有在 module_init 里做的动作,都需要在 module_exit 中做反向操作,避免资源泄漏。
-
MKDEV 功能:将主设备号和次设备号转换成dev_t类型
-
register_chrdev_region(devNum, subDevNum, “helloworld”):驱动注册,第一个参数表示设备号,第二个参数表示注册的此设备数目,第三个表示设备名称。
-
kzalloc:资源申请
-
钩子函数挂钩:gFile->open = hello_open
-
cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接
-
cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中
钩子函数的几个实现:没有实现任何功能,只是为了让框架更明显
int hello_open(struct inode *p, struct file *f)
{
printk(KERN_EMERG"hello_open\r\n");
return 0;
}
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_write\r\n");
return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_read\r\n");
return 0;
}
几个重要的数据结构:
内核中每个字符设备都对应一个 cdev 结构的变量:
struct cdev:
struct cdev {
struct kobject kobj; // 每个cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
struct 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 *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
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 *, fl_owner_t id);
int (*release) (struct inode *, struct file *); //关闭
int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据
int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据
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);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
完整代码
驱动代码:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>//file_operations结构体
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>
#define BUFFER_MAX (10)
#define OK (0)
#define ERROR (-1)
struct cdev *gDev;//代表字符设备的数据结构
struct file_operations *gFile;//struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带.该驱动程序的核心。它给出了对文件操作函数的定义。当然,具体的实现函数是留给驱动程序编写的
dev_t devNum;//设备文件的设备号
unsigned int subDevNum = 1;
int reg_major = 232;
int reg_minor = 0;
char *buffer;
int flag = 0;
int hello_open(struct inode *p, struct file *f)
{
printk(KERN_EMERG"hello_open\r\n");
return 0;
}
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_write\r\n");
return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_read\r\n");
return 0;
}
int hello_init(void)//三件事情注册驱动,申请资源,节点创建
{
devNum = MKDEV(reg_major, reg_minor);//将主设备号和次设备号转换成dev_t类型
if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){
printk(KERN_EMERG"register_chrdev_region ok \n");
}else {
printk(KERN_EMERG"register_chrdev_region error n");
return ERROR;
}
printk(KERN_EMERG" hello driver init \n");
gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
gFile->open = hello_open;
gFile->read = hello_read;
gFile->write = hello_write;
gFile->owner = THIS_MODULE;
cdev_init(gDev, gFile);//初始化,建立cdev和file_operation 之间的连接
cdev_add(gDev, devNum, 3);//把它添加到系统中去,注册设备,通常发生在驱动模块的加载函数中
return 0;
}
void __exit hello_exit(void)
{
cdev_del(gDev);
unregister_chrdev_region(devNum, subDevNum);
kfree(gDev);
kfree(gFile);
return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
编译驱动的Makefile:
ifneq ($(KERNELRELEASE),)
obj-m := helloDev.o
else
PWD := $(shell pwd)
#KDIR:= /lib/modules/4.4.0-31-generic/build
KDIR := /lib/modules/`uname -r`/build
all:
make -C $(KDIR) M=$(PWD)
clean:
rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~
endif
使用make命令编译结果:
ls -l 如下:
加载进内核:
想要控制驱动就需要添加设备节点:mknod /dev/hello c 232 0
应用层代码:
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#define DATA_NUM (64)
int main(int argc, char *argv[])
{
int fd, i;
int r_len, w_len;
fd_set fdset;
char buf[DATA_NUM]="hello world";
memset(buf,0,DATA_NUM);
fd = open("/dev/hello", O_RDWR);
printf("%d\r\n",fd);
if(-1 == fd) {
perror("open file error\r\n");
return -1;
}
else {
printf("open successe\r\n");
}
w_len = write(fd,buf, DATA_NUM);
r_len = read(fd, buf, DATA_NUM);
printf("%d %d\r\n", w_len, r_len);
printf("%s\r\n",buf);
return 0;
}
编译:
gcc test.c -o test.o
结果:
执行test测试驱动:
再次执行dmesg查看驱动输出,发现驱动里的hell_open, hello_write, hello_read被依次调用了: