字符设备驱动开发框架

目录

1 框架

1.1 模块加载命令 

1.2 导出声明

1.3 字符设备驱动要素

2 申请 / 销毁设备号

2.1 申请设备号

 2.2 const struct file_operations *fops

2.2.1 结构体中提供了哪些接口

2.2.2 数据传递 

2.3 销毁 / 注销设备号 

 3 创建设备节点

3.1 手动创建

3.2 程序创建(自动创建)(通过udev/mdev机制) 

3.3  销毁设备节点

4 地址映射

 4.1 操作地址

5. 框架总结

6 程序实例

7 按键中断驱动(在设备树中添加)

设备树介绍:

7.1 获取中断号

 7.2 申请中断

7.2.1 int request_irq

7.2.2 工作流程 

7.3 中断处理函数

7.4 释放中断

7.5 程序示例

7.5.1 结构体信息

7.5.2 应用程序接口和函数

7.5.3 入口函数

7.5.4 中断处理函数

7.5.5 应用程序

 8 文件IO模型(阻塞、非阻塞、IO多路复用、异步通知)

8.1 阻塞

8.1.1 等待队列头

 8.1.2 休眠等待

8.1.3 进程唤醒 

8.2 非阻塞

 8.3 异步通知

8.3.1 驱动编写

8.3.2 应用程序编写

8.4 示例代码 

9 中断下半部分

 9.1 中断下半部的两种实现机制

9.1.1 tasklet机制(小任务机制)

9.1.2 workqueue机制(工作队列机制)

9.2 利用workqueue示例代码

1 框架

头文件

驱动模块装载入口和卸载入口声明

模块装载函数和卸载函数

GPL声明

//头文件
#include<linux/init.h>
#include<linux/module.h>


//模块加载入口函数实现
static int __init (函数名1)(void)
{
	//资源的创建,申请,创建驱动

	return 0;
}

//模块卸载入口函数实现
static void __exit  (函数名2)(void)
{
	//资源的释放,删除驱动
	
}


//模块入口声明
//装载声明(内核加载的入口指定)
module_init(函数名1);//只要加载就执行其中声明的函数

//卸载声明(内核卸载的入口指定)
module_exit(函数名2);

//GPL开源声明
MODULE_LICENSE("GPL");

1.1 模块加载命令 

insmod 

功能:加载模块

命令:insmod 模块路径/模块名  例如:insmod /lib/modules/yhw.ko

rmmod

功能:卸载模块

命令:rmmod 模块路径/模块名  

lsmod 

功能:查看已经加载的模块

1.2 导出声明

如果模块中内容需要在其他模块中使用,可以把内容进行导出--->添加导出声明
EXPORT_SYMBOL(内容名字);
在要使用的模块中进行声明
如果模块只用于进行导出,可以不写入口声明以及定义

举例:

//math.c
#include <linux/init.h>
#include <linux/module.h>

int myadd(int a,int b)
{

return a+b;

}

int mysub(int a,int b)
{
	return a-b;
}

EXPORT_SYMBOL(myadd);
EXPORT_SYMBOL(mysub);


static int __init math_init(void)
{
	return 0;
}

static void __exit math_exit(void)
{

}

module_init(math_init);
module_exit(math_exit);

MODULE_LICENSE("GPL");
//hello.c
#include <linux/init.h>
#include <linux/module.h>

int myadd(int a,int b);
int mysub(int a,int b);

static int __init hello_init(void)
{	
//资源的创建,申请,创建驱动	
	
	printk("%d\n",myadd(1,2));
	return 0;
}

static void __exit hello_exit(void)
{	
//资源的释放,删除驱动
	printk("%d\n",mysub(1,2));

}


module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

 上面写了两个模块,一个是math.c 一个是hello.c  那么我们想要在hello.c模块中使用math.c模块中的内容,就需要在math.c中添加声明:

EXPORT_SYMBOL(myadd);
EXPORT_SYMBOL(mysub);

表示我们要在其它模块使用本模块中的两个函数myadd和mysub,然后在hello.c中定义并传参。这样就实现在其它模块中使用另一个模块中的内容。(导出的内容为函数或全局变量)

但是!在编写驱动时,有些变量是不确定的,是根据驱动具体加载到哪个设备才确定,在进行
insmod装载驱动模块时再传递这些参数值,那么如何处理参数传递呢?

module_param(name,type,perm)

参数1:参数的名字,变量名
参数2:参数的类型,int,char
参数3:/sys/modules 文件的权限,0666

例如:

module_param(a,int,0644);
module_param(b,int,0644);


1.3 字符设备驱动要素

在内核中,有很多的设备驱动,所以需要一个设备号进行区分;

用户程序也必须知道设备驱动对应到哪个设备节点(文件);

2 申请 / 销毁设备号

2.1 申请设备号

在Linux系统下/proc/devices:文件中包含了所有注册到内核的设备,可以打开看一下

int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)


参数:
参数1:
unsigned int major:主设备号
设备号:32bit == 主设备号(12bit) + 次设备号(20bit)
主设备号:表示同一类型的设备
次设备号:表示同一类型中的不同设备
参数有两种设置:
静态:例如指定一个整数:250----主设备号为250
动态:让内核随机指定,参数为0
参数2:
const char *name:一个字符串,描述设备信息,自定义
参数3:
const struct file_operations *fops:结构体指针,结构体变量的地址(结构体中就是应
用程序和驱动程序函数关联,open、read、write)---文件操作对象,提供open、read、write等驱动中的函数
返回值:
如果是静态指定主设备号,返回0表示申请成功,返回负数表示申请失败
如果是动态申请,返回值就是申请成功的主设备号
 

 2.2 const struct file_operations *fops

在驱动中要去实现文件IO的接口,通过结构体指针,指向一个结构体,这个结构体当中提供了驱动与应用程序文件IO的接口关联。

2.2.1 结构体中提供了哪些接口

struct file_operations {
struct module *owner;
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 (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
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 *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
//函数指针集合,其实就是接口,表示文件IO函数用函数指针来表示,存储驱动中的关联函数
// 如: read函数指针 = xxxx_ok;表示 read函数,在驱动中的关联函数为xxxx_ok函数

2.2.2 数据传递 

//这个功能一般用于驱动中的 read
long copy_to_user(void __user *to,const void *from, unsigned long n)

功能:从驱动空间拷贝数据给用户空间

参数:
参数1:
void __user *to:目标地址,应用空间地址
参数2:
const void *from:源地址,内核空间地址
参数3:
unsigned long n:个数
返回值:
成功返回0,失败返回大于0,表示还有多少个没有拷贝完

 long copy_from_user(void * to,const void __user * from,unsigned long n)

功能:从用户空间拷贝数据到驱动空间

参数:
参数1:
void *to:目标地址,内核空间地址
参数2:
const void *from:源地址,应用空间地址
参数3:
unsigned long n:个数
返回值:
成功返回0,失败返回大于0,表示还有多少个没有拷贝完

2.3 销毁 / 注销设备号 

void unregister_chrdev(unsigned int major,const char * name)


参数:
参数1:
unsigned int major:主设备号
参数2:
const char * name:设备信息,自定义

 3 创建设备节点

3.1 手动创建

mknod 设备节点名 设备类型 主设备号 次设备号
#如:
mknod /dev/xxx c 250 0

3.2 程序创建(自动创建)(通过udev/mdev机制) 

//创建一个类信息

struct class * class_create(owner,name)


参数:
参数1:
owner:一般填写 THIS_MODULE
参数2:
name:字符串首地址,名字,自定义
返回值:
返回值就返回信息结构体的地址

//创建设备节点(设备文件)

struct device * device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)


参数:
参数1:
struct class *class:class信息对象地址,通过 class_create()函数创建
参数2:
struct device *parent:表示父亲设备,一般填 NULL
参数3:
dev_t devt:设备号(主设备+次设备)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
参数4:
void *drvdata:私有数据,一般填NULL
参数5、参数6:
const char *fmt, ...:可变参数,表示字符设备文件名(设备节点名)
 

3.3  销毁设备节点

void device_destroy(struct class * class,dev_t devt)

4 地址映射

如果我们想要去控制外设,其实就是控制地址,内核驱动中通过虚拟地址操作,外设是实际的物理地址,每个设备出厂时就有地址

为什么要进行地址映射

1)MMU限制:大多数CPU的MMU只能映射大小为4KB的页面大小,而物理内存中的一块区域大小可能大于4KB,此时需要ioremap来映射不连续的页面。

2)ISA对齐要求:某些外设的寄存器访问需要特定的对齐方式,而CPU访问数据总线的宽度可能无法满足,需要借助ioremap来映射满足要求的虚拟地址。

3)虚拟地址空间不足:当直接映射(网络设备描述符等)占用太多的线性地址空间时,可以使用ioremap来释放部分线性地址,从而获得更多的虚拟地址空间。

4)外设物理地址变化:有些外设允许更改其物理基地址,当基地址变化后,原来的直接映射会失效,此时需要调用ioremap再次建立映射关系。

ioremap实现原理: 

利用页表 entries 将不连续的物理页面映射到连续的虚拟地址空间中。它会在页表中设置虚拟地址与物理地址的对应关系,当CPU进行地址翻译时会根据这些关系将虚拟地址转换为物理地址。

 API函数

//虚拟地址映射
void *ioremap(phys_addr_t offset, unsigned long size)
参数:
参数1:
phys_addr_t offset:物理地址
参数2:
unsigned long size:映射大小
返回值:
映射之后的虚拟地址


//结束映射
void iounmap(void *addr)

 4.1 操作地址

完成地址映射后,返回的值为映射后的地址,就可以对地址进行操作,那么如何操作呢?

第一种 通过指针取*的方式,例如volatile unsigned int *gpx1con;*gpx1con进行操作;

第二种,通过radl() writel()

//从地址中读取地址空间的值
u32 readl(const volatile void *addr)
//将value值,存储到对应地址中
void writel(u32 value, volatile void *addr)

5. 框架总结

在模块加载入口中实现:

1) 申请设备号(内核中用于区分和管理不同的字符设备驱动)
2) 创建设备节点(为用户提供一个可操作的文件接口---用户操作文件)
 3)实现硬件初始化
         地址的映射
         实现硬件的寄存器的初始化
         中断的申请
 4)实现文件io接口(struct file_operations fops结构体)

卸载入口实现资源释放 

iounmap();//解除硬件资源映射
device_destroy();//释放设备节点
class_destroy();//释放节点信息类
unregister_chrdev();//释放设备号

同时,还有出错处理,在某个位置出错,要将之前申请的资源释放;

通过面向对象编程思想,用一个结构体来表示一个对象,设计一个类型来描述一个设备的信息,例如: 

struct BEEP
{
unsigned int major;
struct class * cls;
struct device * dev;
unsigned int * pwmtcfg0;
};
struct BEEP beep;//表示一个设备对象

6 程序实例

//beep_drv.c


#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/io.h>
#include <asm/uaccess.h>

//硬件地址宏定义
#define GPD0CON 0x114000A0

#define PWM 0x139D0000

#define PWMTCFG0   0x139D0000
#define PWMTCFG1   0x139D0004
#define PWMTCON    0x139D0008
#define PWMTCNTB0  0x139D000C
#define PWMTCMPB0  0x139D0010
//结构体信息存储设备信息,包括class_create创建类信息、device_create创建设备节点、设备号
struct BEEP
{
	unsigned int major;
	struct class * cls;
	struct device * dev;

	unsigned int * pwmtcfg0;
	unsigned int * pwmtcfg1;
	unsigned int * pwmtcon;
	unsigned int * pwmtcntb0;
	unsigned int * pwmtcmpb0;
	unsigned int * gpd0con;

};

//结构体对象
struct BEEP beep;
应用程序open调用对应接口函数
int beep_open (struct inode * inode, struct file * filep)
{
	*(beep.pwmtcon) |= 1;
	return 0;
}
//应用程序release调用对应接口,这些通过结构体
int beep_release (struct inode * inode, struct file * filep)
{
	*(beep.pwmtcon) &= ~1;
	return 0;
}
//file_operations fops结构体中实现接口
ssize_t beep_write (struct file * filep, const char __user * buf, size_t size, loff_t * fops)
{
	unsigned int hz = 0;

	copy_from_user(&hz,buf,size);//从用户空间拷贝数据
	
	*(beep.pwmtcntb0) = 100000000 / hz;
	*(beep.pwmtcmpb0) = 50000000 / hz;
	*(beep.pwmtcon) |= 1<<1;
	*(beep.pwmtcon) &= ~(1<<1);
	*(beep.pwmtcon) |= 1<<2;
	*(beep.pwmtcon) |= 1;
	
	return 0;
}


//文件IO实现操作
const struct file_operations fops = {
	.open = beep_open,
	.release = beep_release,
	.write = beep_write,
};

static int __init beep_init(void)//加载入口
{
	//资源申请

	int ret;
	//申请设备号
	beep.major = register_chrdev(0,"beep",&fops);
	if( beep.major < 0 )//设备号申请失败
	{
		printk(KERN_ERR,"register_chrdev error\n");
		ret = -ENODEV;
		goto err_0;
	}
	
	//创建设备节点
	beep.cls = class_create(THIS_MODULE,"beep cls");
	if(IS_ERR(beep.cls))
	{
		printk(KERN_ERR,"class_create error\n");
		ret = PTR_ERR(beep.cls);
		goto err_1;
	}
	
	beep.dev = device_create(beep.cls,NULL,MKDEV(beep.major,0),NULL,"beep");
	if(IS_ERR(beep.dev))
	{
		printk(KERN_ERR,"device_create error\n");
		PTR_ERR(beep.dev);
		goto err_2;
	}
	

	//初始化硬件资源
	//映射寄存器地址
	beep.gpd0con = ioremap(GPD0CON,4);
	if(beep.gpd0con == NULL)
	{
		printk(KERN_ERR,"ioremap error\n");
		ret = -ENOMEM;
		goto err_3;
	}

	beep.pwmtcfg0 = ioremap(PWMTCFG0,4);
	if(beep.pwmtcfg0 == NULL)
	{
		printk(KERN_ERR,"ioremap error\n");
		ret = -ENOMEM;
		goto err_4;
	}

	beep.pwmtcfg1 = ioremap(PWMTCFG1,4);
	if(beep.pwmtcfg1 == NULL)
	{
		printk(KERN_ERR,"ioremap error\n");
		ret = -ENOMEM;
		goto err_5;
	}
	beep.pwmtcon = ioremap(PWMTCON,4);
	if(beep.pwmtcon == NULL)
	{
		printk(KERN_ERR,"ioremap error\n");
		ret = -ENOMEM;
		goto err_6;
	}
	beep.pwmtcntb0 = ioremap(PWMTCNTB0,4);
	if(beep.pwmtcntb0 == NULL)
	{
		printk(KERN_ERR,"ioremap error\n");
		ret = -ENOMEM;
		goto err_7;
	}
	beep.pwmtcmpb0 = ioremap(PWMTCMPB0,4);
	if(beep.pwmtcmpb0 == NULL)
	{
			printk(KERN_ERR,"ioremap error\n");
			ret = -ENOMEM;
			goto err_8;
	}

//readl和writel操作地址
	//*gpd0con = *gpd0con & ~(0XF<<0) | (0X2<<0);
	writel(readl(beep.gpd0con) & ~(0XF<<0) | (0X2<<0),beep.gpd0con);
	*(beep.pwmtcntb0) = 200000;
	*(beep.pwmtcmpb0) = 100000;
	*(beep.pwmtcfg0) = *(beep.pwmtcfg0) & ~0xff;
	*(beep.pwmtcfg1) &= ~0XF;
	*(beep.pwmtcon) &= ~(1<<4);
	*(beep.pwmtcon) |= 1<<3;
	*(beep.pwmtcon) |= 1<<1;
	*(beep.pwmtcon) &= ~(1<<1);
	*(beep.pwmtcon) |= 1<<2;

	return 0;
//出错处理
err_8:
	iounmap(beep.pwmtcntb0);
err_7:
	iounmap(beep.pwmtcon);

err_6:
	iounmap(beep.pwmtcfg1);

err_5:
	iounmap(beep.pwmtcfg0);

err_4:
	iounmap(beep.gpd0con);

err_3:
	device_destroy(beep.cls,MKDEV(beep.major,0));

err_2:
	class_destroy(beep.cls);

err_1:
	unregister_chrdev(beep.major,"beep");

err_0:
	return ret;

}

static void __exit beep_exit(void)//卸载入口
{

	//资源释放
	//iounmap();

		iounmap(beep.pwmtcmpb0);
		iounmap(beep.pwmtcntb0);
		iounmap(beep.pwmtcon);
		iounmap(beep.pwmtcfg1);
		iounmap(beep.pwmtcfg0);
		iounmap(beep.gpd0con);
		device_destroy(beep.cls,MKDEV(beep.major,0));

		class_destroy(beep.cls);

		unregister_chrdev(beep.major,"beep");
}


module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");


下面是应用程序:


#include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{

	int fd = open("/dev/beep",O_RDWR);

	int value = 0;
	while(1)
	{
		scanf("%d",&value);
		if(value == 0)
			break;
		write(fd,&value,4);
	}
	close(fd);

	return 0;
}


下面是Makefile文件:

KERNEL_PATH = /home/yhw/Mine/linux-3.14-fs4412  //内核文件路径

CORSS_COMPILE = /opt/arm_none_gcc/gcc-4.6.4/bin/arm-none-linux-gnueabi-   //指定交叉编译工具链
CC = $(CORSS_COMPILE)gcc

obj-m += beep_drv.o  //驱动
APP_NAME = beep_test //应用程序

all:
	make modules -C $(KERNEL_PATH) M=$(shell pwd)
	$(CC) $(APP_NAME).c -o $(APP_NAME)


install:
	cp *.ko $(APP_NAME) /home/yhw/Mine/NFS/nfshome/rootfs   //拷贝到挂载的nfs目录

clean:
	make clean -C $(KERNEL_PATH) M=$(shell pwd)
	rm $(APP_NAME)

 通过insmod 加载模块,即可实现蜂鸣器的驱动实现,同理,led也可以这样写。

7 按键中断驱动(在设备树中添加)

设备树位置:/arch/arm/boot/dts

设备树介绍:

Linux驱动——设备树_icy、泡芙的博客-CSDN博客

//在设备树中添加一个按键设备信息,添加上使用的中断号,启动内核时i,内核中就会有按键信息(中断号)
key_int_node {
compatible = "key3";
interrupt-parent = <&gpx1>;
interrupts = <2 4>;
};

7.1 获取中断号

在设备中完成信息添加后,在驱动编写中第一步要做的就是获取到中断号

//根据路径获取设备树中的节点
struct device_node *of_find_node_by_path(const char *path);
参数:
参数1:
const char *path:设备树中节点路径
返回值:获取到的节点


//根据节点获取到节点的中断号
unsigned int irq_of_parse_and_map(struct device_node *dev,int index)
参数:
参数1:
struct device_node *dev:设备节点
参数2:
int index:获取节点中第几个中断号
返回值:获取到的中断号

 7.2 申请中断

7.2.1 int request_irq

函数原型:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)

功能:对应中断号出现对应的触发方式,就调用申请中指定的函数进行处理

参数:

        参数1:

                unsigned int irq:设备对应的中断号
        参数2:

                irq_handler_t handler:中断的处理函数
                        typedef irqreturn_t (*irq_handler_t)(int, void *);

         参数3:

                unsigned long flags:中断的触发方式

#define IRQF_TRIGGER_NONE
#define IRQF_TRIGGER_RISING
#define IRQF_TRIGGER_FALLING
#define IRQF_TRIGGER_HIGH
#define IRQF_TRIGGER_LOW
0x00000000
0x00000001
0x00000002
0x00000004
0x00000008

        参数4:

                const char *name  :中断的描述

        参数5:

                void *dev    传入设备结构体指针,用于记录和调试

返回值:

        成功返回0,失败返回负值错误码

7.2.2 工作流程 

1. 检查中断是否已经申请:如果已经申请,直接返回错误。

2. 建立中断描述符:为该中断号创建中断描述符irq_desc,保存相关的处理函数、触发类型、设备信息等。

3. 注册中断:向中断控制器注册该中断服务程序,通常会指定触发条件、优先级等信息。并使能相应的中断。当中断触发后,CPU会调用注册的中断服务程序,通过irq_desc找到相关的处理函数来服务中断。

7.3 中断处理函数

typedef irqreturn_t (*irq_handler_t)(int, void *);

该函数有两个参数,int 和void *;

int :相应的中断号

void *:需要与request_irq函数的参数dev保持一致,用于区分共享中断的不同设备(dev也可以指向设备数据结构)

返回值:

IRQ_HANDLED:表示中断已被正常处理。内核会继续处理中断返回后的操作。

IRQ_NONE:表示中断未被处理。内核将该中断标记为factual,并尝试交由其他注册的中断处理程序处理,或采取默认处理方法。

一般我们的中断处理程序处理了中断请求后应返回IRQ_HANDLED。只有在确认该中断不是我们服务的中断时才返回IRQ_NONE。 

7.4 释放中断

void free_irq(unsigned int irq,void * dev_id);

参数:

irq:要释放的中断号

dev_id: request_irq时传入的dev参数,用于匹配确认正确的中断

7.5 程序示例

7.5.1 结构体信息

struct KeyNode
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	unsigned int irqno;
	unsigned int * dat;
	int value;
	wait_queue_head_t head;
	unsigned int key_state;
};
struct KeyNode key;

7.5.2 应用程序接口和函数

ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
{
	printk("-------%s--------\n",__FUNCTION__);
	if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
	{
		return -EAGAIN;
	}
	

	wait_event_interruptible(key.head,key.key_state);//阻塞

	copy_to_user(buf,&(key.value),size);

	key.key_state = 0;
	
	return 4;
}

int key_open (struct inode * inode, struct file * filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}

int key_release (struct inode * inode, struct file *filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}


const struct file_operations fops = {

	.release = key_release,
	.open = key_open,
	.read = key_read,
	

};

7.5.3 入口函数

static int __init key_drv_init(void)
{

	key.major = register_chrdev(0,"key drv",&fops);

	key.cls = class_create(THIS_MODULE,"key cls");

	key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");

	struct device_node * np = of_find_node_by_path("/key_int_node");

	key.irqno = irq_of_parse_and_map(np,0);

	request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);

	//创建等待队列
	init_waitqueue_head(&(key.head));

	key.dat = ioremap(0x11000c24,4);
	
	return 0;
}

7.5.4 中断处理函数

//中断处理函数----irqno
irqreturn_t irq_handler(int irqno, void * dev)
{
	printk("hello world\n");

	key.value = readl(key.dat) >> 2 & 1;

	key.key_state = 1;
	//唤醒
	wake_up_interruptible(&(key.head));
	
	return IRQ_HANDLED;
}

7.5.5 应用程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>	   
#include <unistd.h>
#include <stdio.h>

int main()
{
	int i = -2;
	int fd = open("/dev/key3",O_RDONLY|O_NONBLOCK);
/*
	int status = fcntl(fd,F_GETFL);
	status = status | O_NONBLOCK;
	fcntl(fd,F_SETFL,status);
*/
	while(1)
	{
		read(fd,&i,4);
		printf("%d\n",i);
	}

	close(fd);
	return 0;
}

 8 文件IO模型(阻塞、非阻塞、IO多路复用、异步通知)

8.1 阻塞

阻塞等同于休眠,当进程在读取外部设备的资源(数据),如果资源没有准备,就会进行休眠等待

8.1.1 等待队列头

根据等待队列头创建等待队列

init_waitqueue_head(wait_queue_head_t * q);

 wait_queue_head_t是一个等待队列结构体,包含了一个等待队列的相关信息,如:

-    等待队列中的任务链表
-    等待队列的锁(用于多线程同步访问)
-    等待队列的计数器(记录当前等待队列内的任务数)

init_waitqueue_head函数初始化一个wait_queue_head_t类型的等待队列,具体工作如下:

1. 将等待队列头的任务链表初始化为空。
2. 初始化等待队列头的锁。
3. 将等待队列头的计数器重置为0。
4. 如果该等待队列头是一般等待队列(WQ_FLAG_EXCLUSIVE未置位),还需要初始化其万能链表。

简而言之,init_waitqueue_head函数主要对一个等待队列头的成员进行初始化,以便其能够正常运作。在驱动开发中,我们通常在模块加载时调用此函数对需要的等待队列头进行初始化。

 8.1.2 休眠等待

wait_event_interruptible(wait_queue_head_t wq,condition)
用于将当前任务添加到一个等待队列中并睡眠,直到 condition 为真或被信号唤醒
参数1:
wait_queue_head_t wq:等待队列头
参数2:
condition:条件,为假,就会等待;为真,就不会等待

 wait_event_interruptible的主要工作流程如下:

1. 检查condition,如果为真则直接返回0。

2. 否则,将当前任务添加到等待队列wq的等待链表中。

3. 使当前任务睡眠,调度器选择其他任务运行。

4. 如果当前任务被信号唤醒,则从等待队列中移除任务,返回-ERESTARTSYS。

5. 当condition为真时,等待队列中的任务将被唤醒。等待队列将从链表中移除当前任务,并返回0。

6. 步骤1~5会重复,直到condition为真或被信号唤醒退出循环。

 简而言之,wait_event_interruptible函数用于实现条件睡眠,当前任务会一直睡眠直到condition满足或收到信号。它与等待队列结合,可以用于进程间同步与通信。

//示例代码
wait_queue_head_t wait_queue;  
int condition = 0;

// 初始化等待队列头  
init_waitqueue_head(&wait_queue);   

// 休眠任务,等待condition为真  
wait_event_interruptible(wait_queue, condition);  

// 条件满足,唤醒等待队列中的任务  
condition = 1;  
wake_up(&wait_queue);

8.1.3 进程唤醒 

wake_up_interruptible(wait_queue_head_t * q);
*q:要唤醒的队列头

 wake_up_interruptible的主要工作流程如下:

1. 启用等待队列头q的锁,以保证多线程安全访问。

2. 遍历等待队列头q中的等待任务链表。

3. 对于每个任务,执行唤醒操作try_to_wake_up。

4. try_to_wake_up函数检查任务状态,如果是可中断睡眠(TASK_INTERRUPTIBLE)则将其唤醒。

5. 唤醒的任务将从等待队列头q的等待链表中移除。

6. 禁用等待队列头q的锁,退出流程。

 简而言之,wake_up_interruptible函数会尝试唤醒等待队列q中的所有处于可中断睡眠状态(TASK_INTERRUPTIBLE)的任务。被唤醒的任务将从等待队列的等待链表中移除,恢复运行。

8.2 非阻塞

在读写的时候,如果没有数据,就立即返回,应用需要设置为非阻塞

int fd = open("/dev/key3",O_RDONLY|O_NONBLOCK);
int status = fcntl(fd,F_GETFL);status = status | O_NONBLOCK;
fcntl(fd,F_SETFL,status);

 驱动中需要区分当前模式是否为非阻塞模式:如果是非阻塞模式,且没有数据就立即返回

if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
{
return -EAGAIN;
}
fcntl函数用于操控文件描述符,它的原型为:
int fcntl(int fd, int cmd, ...);
其主要参数为:
- fd:文件描述符。
- cmd:操作命令,指定对文件描述符fd进行的控制操作。
- ...:cmd所需要的其他参数,取决于所指定的操作命令。
当cmd为F_GETFL时,fcntl函数的作用是获取文件描述符fd的文件状态标志。它不需要第三个参数,函数调用为:
int status = fcntl(fd, F_GETFL);
该调用将返回fd文件的状态标志,通常存储在status变量中。状态标志是用于指定文件的属性和操作方式的一组宏定义,包括:
- O_RDONLY: 只读文件 
- O_WRONLY: 只写文件
- O_RDWR:   读写文件
- O_APPEND: 追加模式
- O_NONBLOCK: 非阻塞I/O
- O_SYNC:    同步I/O
等等。
这些状态标志可以在打开文件时通过flags参数设置,也可以通过fcntl函数的F_SETFL操作在打开后动态修改。
例如,获取标准输入stdin的状态标志:

int status = fcntl(STDIN_FILENO, F_GETFL);
设置文件描述符fd为非阻塞I/O:

int flags = fcntl(fd, F_GETFL);  // 获取原状态标志
flags |= O_NONBLOCK;            // 设置非阻塞标志
fcntl(fd, F_SETFL, flags);      // 设置状态标志  

 8.3 异步通知

当有数据的时候,驱动就会发送信号给(SIGIO)给应用,应用就可以异步去读写数据,不用主动读写。


8.3.1 驱动编写

驱动用于发送信号,和进程进行关联,也就是信号发送给谁,通过fasync接口实现

int key_fasync (int fd, struct file * filep, int on)
{
return fasync_helper(fd,filep,on,&(key.fasync));
}
int key_fasync(int fd, struct file *filep, int on); 
参数:
fd:文件描述符。
filep:与文件描述符fd对应文件的file结构体指针。
on:开启或取消asynchronous notification。
功能:
使用key_fasync设置异步通知后,应用程序无需轮询文件描述符,内核会在I/O完成后主动发送信号通知应用程序,从而提高程序的效率与响应速度。

返回值:
0:操作成功完成。
负值:操作失败,此时不会有任何异步通知的设置或取消动作。
可能的错误码有:
- -EBADF:文件描述符fd无效。
- -EINVAL:filp结构指针filep为空或filp与fd不 match。
- -ENOMEM:内存分配失败。fasync_list链表使用kmalloc动态分配,如果分配失败将返回此错误。

详细作用描述:
当on为真时,key_fasync函数的作用是:
1. 根据文件描述符fd查找其对应的filp(file结构体指针),通常由filep参数提供。
2. 将该filp添加到fasync_list链表,该链表包含所有请求异步通知的filp。
3. 当文件可读、写或出错时,内核会遍历fasync_list链表,调用程序注册的信号驱动函数,发送相应的信号给进程。
4. 从而实现I/O完成后立即通知应用程序的效果。
当on为假时,key_fasync函数的作用是:
1. 从fasync_list链表中移除与文件描述符fd对应的filp。
2. 取消该文件描述符的异步通知请求。
//然后在某个特定的时刻(比如有数据的时候)发送信号
kill_fasync(&(key.fasync),SIGIO,POLLIN);
void kill_fasync(struct fasync_struct **fp, int sig, int band);
其主要参数为:
- fp:fasync_struct结构体指针的指针,指向I/O通知链表。
- sig:要发送的信号,一般为SIGIO。
- band:I/O操作的类型,如POLLIN、POLLOUT等。

功能:
kill_fasync函数根据 band参数检查链表fp上的每个文件描述符是否请求了相应的I/O监视,如果是则发送sig信号通知应用进程相应的I/O事件已就绪。

kill_fasync函数的主要工作流程如下:
1. 遍历I/O通知链表fp所指向的fasync_struct结构体链表。
2. 对于每个fasync_struct,检查其文件描述符的I/O监视事件是否包含band指定的I/O操作。
   - 如果不包含,继续遍历下一结构体。
   - 如果包含,构造siginfo结构体,发送sig信号给fasync_struct->fa_file->f_owner所指定的进程。
3. 发送信号通知应用程序,I/O操作band已就绪。
4. 退出,异步通知完成。

8.3.2 应用程序编写

1、设置信号怎么处理
signal(SIGIO,catch_signal);
void catch_signal(int signo)
{
if(signo == SIGIO)
{
int value = -1;
read(fd,&value,4);
printf("program data is %d\n",value);
}
}
2、将当前进程设置为SIGIO的属主进程
//设置当前进程为信号的属主进程
fcntl(fd,F_SETOWN,getpid());
3、将io模式设置为异步模式

int stats = fcntl(fd,F_GETFL);
stats = stats | FASYNC;
fcntl(fd,F_SETFL,stats);

8.4 示例代码 

//key_test_tasklet.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>	   
#include <unistd.h>
#include <stdio.h>
#include <signal.h>


int fd;

void catch_signal(int signo)
{

	if(signo == SIGIO)
	{
		int value = -1;
		read(fd,&value,4);
		printf("program data is %d\n",value);
	}

}

int main()
{
	int i = -2;
	fd = open("/dev/key3",O_RDONLY);

	//信号处理方式
	signal(SIGIO,catch_signal);

	//设置当前进程为信号的属主进程
	fcntl(fd,F_SETOWN,getpid());

	//将io模式设置为异步模式
	int stats = fcntl(fd,F_GETFL);
	stats = stats | FASYNC;
	fcntl(fd,F_SETFL,stats);

	while(1)
	{
		printf("hello world\n");
		sleep(1);
	}

	close(fd);
	return 0;
}



//key_drv_tasklet.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/device.h>

#include <asm/io.h>
#include <asm/poll.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>

struct KeyNode
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	unsigned int irqno;
	unsigned int * dat;
	int value;
	wait_queue_head_t head;
	unsigned int key_state;
	struct fasync_struct * fasync;
	struct tasklet_struct tasklet;
};

struct KeyNode key;

ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
{
	printk("-------%s--------\n",__FUNCTION__);
	if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
	{
		return -EAGAIN;
	}
	

	wait_event_interruptible(key.head,key.key_state);//阻塞

	copy_to_user(buf,&(key.value),size);

	key.key_state = 0;
	
	return 4;
}

int key_open (struct inode * inode, struct file * filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}

int key_release (struct inode * inode, struct file *filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}


int key_fasync (int fd, struct file * filep, int on)
{

	return fasync_helper(fd,filep,on,&(key.fasync));
	
}

const struct file_operations fops = {

	.release = key_release,
	.open = key_open,
	.read = key_read,
	.fasync = key_fasync,

};

//中断下半部分执行操作
void irq_func(unsigned long data)
{

	printk("data is %d\n",data);

}


//中断处理函数----irqno
irqreturn_t irq_handler(int irqno, void * dev)
{
	printk("hello world\n");

	key.value = readl(key.dat) >> 2 & 1;

	key.key_state = 1;
	//唤醒
	wake_up_interruptible(&(key.head));

	//发送信号
	kill_fasync(&(key.fasync),SIGIO,POLLIN);

	//中断下半部分启动--放入内核线程
	tasklet_schedule(&(key.tasklet));
	
	return IRQ_HANDLED;
}


static int __init key_drv_init(void)
{

	key.major = register_chrdev(0,"key drv",&fops);

	key.cls = class_create(THIS_MODULE,"key cls");

	key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");

	struct device_node * np = of_find_node_by_path("/key_int_node");

	key.irqno = irq_of_parse_and_map(np,0);

	request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);

	//初始化中断下半部分任务(struct tasklet_struct 结构体初始化)
	tasklet_init(&(key.tasklet),irq_func,45);

	//创建等待队列
	init_waitqueue_head(&(key.head));

	key.dat = ioremap(0x11000c24,4);
	
	return 0;
}

static void __exit key_drv_exit(void)
{

}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

9 中断下半部分

当中断处理程序比较长时,为了提高相应特性,linux内核处理中断的方案是:人为地将处理程序分为两部分,即中断上半部、中断下半部。其中中断上半部标记中断,调度下半部;下半部负责真正的操作(比如读取按键键值、从网卡读取缓冲数据等)

 9.1 中断下半部的两种实现机制

9.1.1 tasklet机制(小任务机制)

内部实现调用sortirq

sorfirq:处理速度比较快,但是是内核级别的机制,要修改内核源码,不推荐使用

 tasklet在中断上下文执行,因此不能被阻塞,不能睡眠,不能被打断

编程:

1. 初始化,设置tasklet任务结构体
struct tasklet_struct tasklet;
tasklet_init(&任务结构体的地址,中断下半部分执行函数,函数参数);
2. 启动中断下半部分----把任务放入到内核线程中
//中断下半部分启动--放入内核线程
tasklet_schedule(&tasklet);

9.1.2 workqueue机制(工作队列机制)

workqueue在进程上下文,可以被重新调度,可以阻塞,也可以睡眠

编程:

1. 初始化,设置workqueue任务结构体
struct work_struct workqueue;
INIT_WORK(&任务结构体的地址,中断下半部分执行函数);
2. 启动中断下半部分----把任务放入到内核线程中
//中断下半部分启动--放入内核线程
schedule_work(&workqueue);

9.2 利用workqueue示例代码

//key_drv_queue.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/device.h>

#include <asm/io.h>
#include <asm/poll.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>

struct KeyNode
{
	unsigned int major;
	struct class * cls;
	struct device * dev;
	unsigned int irqno;
	unsigned int * dat;
	int value;
	wait_queue_head_t head;
	unsigned int key_state;
	struct fasync_struct * fasync;
	struct work_struct work;
};

struct KeyNode key;

ssize_t key_read (struct file * filep, char __user * buf, size_t size, loff_t *fops)
{
	printk("-------%s--------\n",__FUNCTION__);
	if(filep->f_flags & O_NONBLOCK && !key.key_state)//非阻塞
	{
		return -EAGAIN;
	}
	

	wait_event_interruptible(key.head,key.key_state);//阻塞

	copy_to_user(buf,&(key.value),size);

	key.key_state = 0;
	
	return 4;
}

int key_open (struct inode * inode, struct file * filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}

int key_release (struct inode * inode, struct file *filep)
{
	printk("-------%s--------\n",__FUNCTION__);
	return 0;
}


int key_fasync (int fd, struct file * filep, int on)
{

	return fasync_helper(fd,filep,on,&(key.fasync));
	
}

const struct file_operations fops = {

	.release = key_release,
	.open = key_open,
	.read = key_read,
	.fasync = key_fasync,

};

//中断下半部分执行操作
void work_func(struct work_struct *work)
{

	printk("nihao\n");

}

//中断处理函数----irqno
irqreturn_t irq_handler(int irqno, void * dev)
{
	printk("hello world\n");

	key.value = readl(key.dat) >> 2 & 1;

	key.key_state = 1;
	//唤醒
	wake_up_interruptible(&(key.head));

	//发送信号
	kill_fasync(&(key.fasync),SIGIO,POLLIN);

	//中断下半部分启动--放入内核线程(不是在中断中执行任务,而是把任务加入到内核,在内核执行)
	schedule_work(&(key.work));
	
	return IRQ_HANDLED;
}


static int __init key_drv_init(void)
{

	key.major = register_chrdev(0,"key drv",&fops);

	key.cls = class_create(THIS_MODULE,"key cls");

	key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");

	struct device_node * np = of_find_node_by_path("/key_int_node");

	key.irqno = irq_of_parse_and_map(np,0);

	request_irq(key.irqno,irq_handler,IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,"key irq",NULL);


	//初始化中断下半部分任务(struct work_struct 结构体初始化)
	INIT_WORK(&(key.work),work_func);

	//创建等待队列
	init_waitqueue_head(&(key.head));

	key.dat = ioremap(0x11000c24,4);
	
	return 0;
}

static void __exit key_drv_exit(void)
{

}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
//key_test_queue.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>	   
#include <unistd.h>
#include <stdio.h>
#include <signal.h>


int fd;

void catch_signal(int signo)
{

	if(signo == SIGIO)
	{
		int value = -1;
		read(fd,&value,4);
		printf("program data is %d\n",value);
	}

}

int main()
{
	int i = -2;
	fd = open("/dev/key3",O_RDONLY);

	//信号处理方式
	signal(SIGIO,catch_signal);

	//设置当前进程为信号的属主进程
	fcntl(fd,F_SETOWN,getpid());

	//将io模式设置为异步模式
	int stats = fcntl(fd,F_GETFL);
	stats = stats | FASYNC;
	fcntl(fd,F_SETFL,stats);

	while(1)
	{
		printf("hello world\n");
		sleep(1);
	}

	close(fd);
	return 0;
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Y_寒酥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值