目录
2.2 const struct file_operations *fops
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
设备树介绍:
//在设备树中添加一个按键设备信息,添加上使用的中断号,启动内核时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_LOW0x00000000
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;
}