文章目录
- 1、字符设备实现的两套接口
- 2、现代Linux内核实现字符设备步骤
- 3、虚拟设备文件实验
- 4、设备驱动在内核模块的缓存如何实现?
- 5、设备文件的阻塞、非阻塞、定时读写如何实现
- 6、驱动真实设备要做的工作
- 问题汇总
-
- [1、read 和 write的偏移值参数问题](#1、read 和 write的偏移值参数问题)
- 2、用户调用read/write等API的过程
- 3、字符设备定时获取数据
1、字符设备实现的两套接口
Linux 字符设备驱动有两套标准实现接口,对应内核不同阶段的设计:
1、传统旧接口:基于 register_chrdev + 手动创建设备文件(静态分配设备号)
2、新内核标准接口:基于 cdev 结构体 + 动态设备号 + udev 自动创建设备文件。
现在的驱动开发多使用新接口。
2、现代Linux内核实现字符设备步骤
1、初始化file_operations 结构体
2、申请设备号
3、初始化c_dev结构体
4、注册c_dev结构体
5、创建设备类
6、创建设备
7、释放设备
8、释放设备类
9、销毁内核c_dev
10、释放设备号
3、虚拟设备文件实验
创建虚拟字符设备,read时返回数字字符,每次递增。write 设置值 。open 初始化为0。
1、代码实现
c
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/module.h>
#include <linux/uaccess.h>
//定义缓存
uint8_t kernal_buff[128] = {0};
uint8_t write_buff[128] = {0};
uint8_t read_buff[128] = {0};
//定义操作函数
static int vc_open (struct inode * node , struct file * fp)
{
printk(KERN_INFO"call vc_open\r\n");
kernal_buff[0] = 1;
return 0;
}
static int vc_close (struct inode * node, struct file * fp)
{
printk(KERN_INFO"call vc_close\r\n");
return 0;
}
static ssize_t vc_read (struct file * fp, char __user * buff, size_t cnt, loff_t * offset)
{
int retvalue = 0;
printk(KERN_INFO"vc_read input parments * fp = 0x%x, user buff * = 0x%x ,size = %d,loff_t = %d\n ",
fp,buff,cnt,*offset);
strncpy(read_buff,kernal_buff,sizeof(kernal_buff));
retvalue = copy_to_user(buff,read_buff,strlen(read_buff)<cnt?sizeof(read_buff):cnt);
if(retvalue == 0)
{
printk(KERN_INFO"kernal vc senddata OK... \r\n");
*offset += strlen(read_buff)<cnt?sizeof(read_buff):cnt;
if(*offset > 128)
{
return 0;
}
if(kernal_buff[0] == 255)
{
kernal_buff[0] == 0;
}
else{
kernal_buff[0] += 1;
}
return strlen(read_buff)<cnt?strlen(read_buff):cnt;
// return 0;
}
else
{
printk(KERN_ERR"kernal vc senddata FAILED... \r\n");
return -EFAULT;
}
}
static ssize_t vc_write (struct file * fp, const char __user * buff, size_t cnt, loff_t * offset)
{
int retvalue = 0;
int loop;
printk(KERN_INFO"======>>>> write parament cnt:%zu\r\n",cnt);
retvalue = copy_from_user(write_buff,buff,((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff))));
if(retvalue == 0)
{
printk(KERN_INFO"vc_write %d byte\n",retvalue);
*offset += strlen(read_buff)<cnt?sizeof(read_buff):cnt;
if(*offset > 128)
{
return 0;
}
for(loop = 0;loop < retvalue;loop++)
{
printk(KERN_INFO"0x%02x ",write_buff[loop]);
}
printk(KERN_INFO"\n");
kernal_buff[0] = write_buff[0];
return ((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff)));
}
else
{
printk(KERN_ERR"vc_write err\n");
return -EFAULT;
}
}
//定义操作函数结构体
static struct file_operations vc_operations =
{
.open = vc_open,
.release = vc_close,
.read = vc_read,
.write = vc_write,
.owner = THIS_MODULE
};
//定义字符设备结构体
static struct cdev my_cdev = {0};
//定义设备类指针
static struct class * my_class = NULL;
//定义设备号
static dev_t my_dev_t = 0;
//定义设备名称
static char const * const vc_name = "vc_char";
//init 函数
static int __init vc_init(void)
{
int res = 0;
//申请设备号
res = alloc_chrdev_region(&my_dev_t,0,1,vc_name);
if(res != 0)
{
printk(KERN_ERR"vc_init alloc_chrdev_region err\n");
return -EFAULT;
}
else
{
printk(KERN_INFO"vc_init alloc_chrdev_region dev_t = 0x%x\n",my_dev_t);
}
//初始化字符设备结构体
cdev_init(&my_cdev,&vc_operations);
//向内核注册字符设备
my_cdev.owner = THIS_MODULE;
res = cdev_add(&my_cdev,my_dev_t,1);
if(res != 0)
{
printk(KERN_ERR"vc_init cdev_add err\n");
//释放设备号
unregister_chrdev_region(my_dev_t,1);
return -EFAULT;
}
else
{
printk(KERN_INFO"vc_init cdev_add success\n");
}
//创建设备类
my_class = class_create(THIS_MODULE,vc_name);
if (IS_ERR(my_class)) {
int err = PTR_ERR(my_class);
printk(KERN_ERR "vc_init class_create failed! Error: %d\n", err);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_t,1);
return err;
}
printk(KERN_INFO "vc_init class_create successfuly! \n");
//创建设备文件
struct device * vc_ddev = device_create(my_class,NULL,my_dev_t,NULL,vc_name);
if (IS_ERR(vc_ddev)) {
int err = PTR_ERR(vc_ddev);
printk(KERN_ERR "vc_init device_create failed! Error: %d\n", err);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_t,1);
return err;
}
printk(KERN_INFO "vc_init device_create successfuly! \n");
return 0;
}
//exit 函数
static void __exit vc_exit(void)
{
//释放设备文件
device_destroy(my_class,my_dev_t);
//释放设备类
class_destroy(my_class);
//释放字符设备
cdev_del(&my_cdev);
//释放设备号
unregister_chrdev_region(my_dev_t,1);
printk(KERN_INFO "vc_exit successfuly! \n");
}
//注册
module_init(vc_init);
module_exit(vc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoMing 10086@xx.com");
2、编译
1、Mmakfile
c
KERNELDIR := /home/wangju/linux/linux
CURRENT_PATH := $(shell pwd)
obj-m := my_dev.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
在此makefile中,默认目标为build,依赖kernel_modules 。kernel_modules 目标的执行是跳转到Linux内核源码目录 (-C),然后为内核顶层Makefile指定外部模块路径,modules 参数告诉内核顶层Makefile是编译模块。
2、编译
c
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
指定了架构和交叉编译器前缀
3、编译结果

3、应用程序
1、代码
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
static char buff[128] = {0};
#define dev_path "/dev/vc_char"
int main()
{
int res = 0;
int fp = open(dev_path,O_RDWR);
if(fp <= 0)
{
printf("open error\n");
return 0;
}
char rc ;
while(1)
{
rc = fgetc(stdin) ;
if(rc == '1')
{
res = read(fp,buff,1);
if(res <= 0)
{
printf("read failed\n");
}
else
{
printf("read valus = %d\n",buff[0]);
}
}
else if(rc == '2')
{
char wr = 10;
res = write(fp,&wr,1);
if(res <= 0)
{
printf("write failed\n");
}
else
{
printf("write valus = %d\n",wr);
}
}
else if(rc == '3')
{
break;
}
else
{
printf("involed\n");
}
}
close(fp);
return 0;
}
2、编译
bash
arm-linux-gnueabihf-gcc vc_app.c -o vc_app
3、代码效果
应用层代码打开模块的设备文件vc_char,从键盘接收输入。输入1,读数据,内核每次++。输入2,写数据,设置内核数据为10.
4、效果检验
1、将应用程序和模块程序发送至板卡

2、加载模块
加载模块有两个命令 insmod 和 modprobe
- insmod(简单粗暴)
作用:手动加载指定路径的驱动模块用法:必须给 完整路径 / 相对路径
不能解决依赖问题
卸载使用 insmod → rmmod - modprobe(智能高级)
作用:从 系统目录 /lib/modules/xxx/ 加载模块用法:直接写 模块名,不用路径、不用 .ko 后缀
可以自动加载依赖模块
卸载使用 modprobe → modprobe -r - lsmod
作用:查看当前系统已经加载了哪些内核模块 - dmesg
查看内核打印信息(驱动里的 printk 全在这)

3、启动应用程序

可以看到,对设备文件的读写都是按照设计的思路进行的。
4、查看内核输出

4、设备驱动在内核模块的缓存如何实现?
- kmem_cache 对内核对象的频繁申请释放
创建缓存:kmem_cache_create()
分配对象:kmem_cache_alloc()
释放对象:kmem_cache_free()
销毁缓存:kmem_cache_destroy() - kmalloc
和用户空间的malloc一样的效果 - kzalloc
和kmalloc一样,不过会清零 - vmalloc
申请内存不连续的虚拟空间。 - 全局静态数组
如果模块是直接编译进内核,则占用内核镜像的 .data/.bss段
如果是insmod动态加载的,则通过SLAB动态分配。
内核模块的空间,如果动态申请,一遍都是通过SLAB分配静态缓存池的对象。
如果是静态声明的,模块编译进内核就是内核镜像的静态数据段。动态加载就是通过vmalloc申请的动态空间,最后还是通过SLAB分配的。
5、设备文件的阻塞、非阻塞、定时读写如何实现
简单概述:在模块的read和write时判断是否是非阻塞读写(标志不一样),做不同处理。
非阻塞:直接返回读写状态
阻塞:将调用read和write的进程挂入阻塞队列(内部私有队列),条件满足时唤醒进程。
是否阻塞的标志保存在 struct file中,这是每个进程独立的,在调用read、write时会作为参数传入模块的read和write。
6、驱动真实设备要做的工作
1、相关概念
- mmu的物理地址和虚拟地址的转换
- I/O内存及其读写
2、LED实例代码
c
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <linux/errno.h>
//定义缓存
uint8_t kernal_buff[128] = {0};
uint8_t write_buff[128] = {0};
uint8_t read_buff[128] = {0};
#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}
//定义操作函数
static int vc_open (struct inode * node , struct file * fp)
{
printk(KERN_INFO"call led_open\r\n");
// kernal_buff[0] = 1;
return 0;
}
static int vc_close (struct inode * node, struct file * fp)
{
printk(KERN_INFO"call led_close\r\n");
return 0;
}
static ssize_t vc_read (struct file * fp, char __user * buff, size_t cnt, loff_t * offset)
{
int retvalue = 0;
u32 val = 0;
printk(KERN_INFO"led_read input parments * fp = 0x%x, user buff * = 0x%x ,size = %d,loff_t = %d\n ",
fp,buff,cnt,*offset);
//读取数据
val = readl(GPIO1_DR);
val &= (1 << 3);
val = val >> 3;
if(val == 0)
{
//led open
read_buff[0] = 1;
}
else if(val == 1)
{
//led 关闭
read_buff[0] = 0;
}
else
{
//led 未知状态
read_buff[0] = 2;
}
printk(KERN_INFO"led read value = %d \n",val);
strncpy(read_buff,kernal_buff,sizeof(kernal_buff));
retvalue = copy_to_user(buff,read_buff,strlen(read_buff)<cnt?sizeof(read_buff):cnt);
if(retvalue == 0)
{
printk(KERN_INFO"kernal led senddata OK... \r\n");
return 1;
}
else
{
printk(KERN_ERR"kernal vc senddata FAILED... \r\n");
return -EFAULT;
}
}
static ssize_t vc_write (struct file * fp, const char __user * buff, size_t cnt, loff_t * offset)
{
int retvalue = 0;
int loop;
printk(KERN_INFO"======>>>> write parament cnt:%zu\r\n",cnt);
retvalue = copy_from_user(write_buff,buff,((sizeof(write_buff))>cnt?cnt:(sizeof(write_buff))));
if(retvalue == 0)
{
if(write_buff[0] == 1)
{
led_switch(LEDON);
}
else
{
led_switch(LEDOFF);
}
return 1;
}
else
{
printk(KERN_ERR"vc_write err\n");
return -EFAULT;
}
}
//定义操作函数结构体
static struct file_operations vc_operations =
{
.open = vc_open,
.release = vc_close,
.read = vc_read,
.write = vc_write,
.owner = THIS_MODULE
};
//定义字符设备结构体
static struct cdev my_cdev = {0};
//定义设备类指针
static struct class * my_class = NULL;
//定义设备号
static dev_t my_dev_t = 0;
//定义设备名称
static char const * const vc_name = "led";
//init 函数
static int __init vc_init(void)
{
int res = 0;
int retvalue = 0;
u32 val = 0;
//初始化LED
//寄存器映射
/* 初始化LED */
/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
/* 2、使能GPIO1时钟 */
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); /* 清除以前的设置 */
val |= (3 << 26); /* 设置新值 */
writel(val, IMX6U_CCM_CCGR1);
/* 3、IO复用*/
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
/* 4、 输出模式*/
val = readl(GPIO1_GDIR);
val &= ~(1 << 3); /* 清除以前的设置 */
val |= (1 << 3); /* 设置为输出 */
writel(val, GPIO1_GDIR);
/* 5、 关闭LED*/
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
//申请设备号
res = alloc_chrdev_region(&my_dev_t,0,1,vc_name);
if(res != 0)
{
printk(KERN_ERR"led_init alloc_chrdev_region err\n");
return -EFAULT;
}
else
{
printk(KERN_INFO"led_init alloc_chrdev_region dev_t = 0x%x\n",my_dev_t);
}
//初始化字符设备结构体
cdev_init(&my_cdev,&vc_operations);
//向内核注册字符设备
my_cdev.owner = THIS_MODULE;
res = cdev_add(&my_cdev,my_dev_t,1);
if(res != 0)
{
printk(KERN_ERR"led_init cdev_add err\n");
//释放设备号
unregister_chrdev_region(my_dev_t,1);
return -EFAULT;
}
else
{
printk(KERN_INFO"led_init cdev_add success\n");
}
//创建设备类
my_class = class_create(THIS_MODULE,vc_name);
if (IS_ERR(my_class)) {
int err = PTR_ERR(my_class);
printk(KERN_ERR "led_init class_create failed! Error: %d\n", err);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_t,1);
return err;
}
printk(KERN_INFO "led_init class_create successfuly! \n");
//创建设备文件
struct device * vc_ddev = device_create(my_class,NULL,my_dev_t,NULL,vc_name);
if (IS_ERR(vc_ddev)) {
int err = PTR_ERR(vc_ddev);
printk(KERN_ERR "led_init device_create failed! Error: %d\n", err);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(my_dev_t,1);
return err;
}
printk(KERN_INFO "led_init device_create successfuly! \n");
return 0;
}
//exit 函数
static void __exit vc_exit(void)
{
//释放内存映射
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
//释放设备文件
device_destroy(my_class,my_dev_t);
//释放设备类
class_destroy(my_class);
//释放字符设备
cdev_del(&my_cdev);
//释放设备号
unregister_chrdev_region(my_dev_t,1);
printk(KERN_INFO "led_exit successfuly! \n");
}
//注册
module_init(vc_init);
module_exit(vc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("XiaoMing 10086@xx.com");
问题汇总
1、read 和 write的偏移值参数问题
open后,文件描述符指向一个struct file,保存该进程中对对用文件操作的标志,是否阻塞、偏移量等。
普通文件:内核会自动帮你维护偏移量,驱动不用管
字符设备:偏移量内核也会自动保存,但不会自动更新 **,必须由你驱动自己去更新 **
对于cat命令,他只有驱动read返回0时才会停止(认为读完了),如果字符设备的read没有做处理就会疯狂的read(认为没有读取完)。cat等 系统命令是针对普通文件的命令。
所以,为了让字符设备可以使用cat等命令应该在read和write时对偏移量和返回值做处理。
2、用户调用read/write等API的过程
1、app调用read
2、系统read API(glibc实现)将系统调用号、read参数放入cpu通用寄存器
3、read触发中断
4、cpu进入内核态
5、进入中断处理函数sys_read,从寄存器获取调用号
6、找到对应的执行函数,获取参数,调用对应函数
7、中断处理结束
8、CPU返回用户态,read API返回。
9、用户程序继续执行。
这就是内核代替用户执行的过程,我认为这里的内核是指 CPU内核态 + 内核中注册的对应函数两部分组成。在整个过程中CPU执行的都是用应用进程,没有发生进程的切换。所以内核只是一段可执行代码,哪个CPU加载内核代码,都可以执行。内核存在于内存,不在CPU。
应为参数要存到寄存器中,32或64。所以API参数位指针或者int等都可以,但是传入一个结构体,明显是不行的,只能传入指针,直接访问内存。
3、字符设备定时获取数据
使用内核定时器或者内核线程。但是他们的创建和销毁也是在模块的init函数中完成的。所以他们还是以模块的形式加载。