Linux_文件IO模型

1.阻塞与非阻塞IO模型

1.1.阻塞与非阻塞IO简介

首先,这里的“IO”指的是应用程序对驱动设备的输入/输出操作。
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式IO就会将应用程序对应的进程挂起,直到设备资源可以使用为止。对于非阻塞IO,应用程序对应的进程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
阻塞式IO模型如下图所示:
在这里插入图片描述
应用程序调用read函数从设备中读取数据,当设备不可用或数据未准备好的时候就会进入休眠态。等设备可用的时候就会从休眠态唤醒,然后从设备中读取数据返回给应用程序。
非阻塞式IO模型如下图所示:
在这里插入图片描述
应用程序使用非阻塞访问方式从设备读取数据,当设备不可用或数据未准备好的时候会立即向内核返回一个错误码,表示数据读取失败。应用程序会再次重新读取数据,这样一直往复循环,直到数据读取成功。
阻塞的意义:按照上面的概念理解,阻塞会在驱动中没有得到期望的结果时,挂起进程。表面上看似乎不太好,但是Linux是一个多进程的系统,一个进程挂起,不会占用CPU的资源,CPU可以去运行其他进程。当驱动数据一旦就绪,则再把休眠的进程唤醒,这样CPU的利用率可以得到提高。

1.2.应用程序访问方式

应用程序可选择以阻塞方式或非阻塞方式访问设备文件,
应用程序默认是使用阻塞方式访问设备文件。其访问方式如下:
示例:open("/dev/key",O_RDONLY);
应用程序使用非阻塞方式访问设备文件,需要添加参数O_NONBLOCK,其访问方式如下:

open("/dev/key",O_RDONLY|O_NONBLOCK);	//前提是驱动有实现

1.3.驱动实现阻塞和非阻塞IO模型的思路

1.驱动的xxx_read函数必须知道应用程序打开文件的方式
2.根据应用程序打开文件的方式编写不同处理代码
3.如果应用程序打开方式中没有指明O_NONBLOCK方式,则表示是阻塞方式打开:
3.1)如果此时没有数据可以读取,则执行休眠。
3.2)如果有数据可以读取,则马上读取数据,不休眠,读取数据后马上返回。
4.如果应用程序打开方式中指明O_NONBLOCK方式,则表示是非阻塞方式打开:
4. 1)如果此时已经有数据可以读取,则读取数据再返回。
4.2)如果没有数据可以读,也马上返回,但是返回一个错误码。
5.不管那种方式打开,只要调用read函数的时刻,刚刚好有数据可以读取,都是马上读取数据正确返回的。
所以,阻塞和非阻塞打开方式,具体代码差异就在于调用时没有数据可以读取的情况。

1.4.驱动编写相关问题及解决方法

1)驱动中如何得到应用程序打开设备文件的方式
用户空间应用程序open函数调用时候会在内核中动态创建一个struct file结构,这个结构中有一个成员是f_flags,在创建的时候内核会把应用程序open函数的打开方式flags数值保存在f_flags变量中。
struct file结构体在内核源码include/linux/fs.h文件中定义。

struct file{	
	……
	unsigned int f_flags;
	……
}

2)驱动程序中如何处理不同的打开方式
以按键驱动为例:判断当前是否有按键动作

if(没有按键动作){				    //没有数据可读
	if(pfile->f_flags & O_NONBLOCK){	//应用程序是否以非阻塞方式打开
		return -EAGAIN;		    //返回一个错误码,告诉应用程序你可以再尝试读取
	}else{				        //应用程序以阻塞方式打开
		/* 休眠,等待有按键动作唤醒 */
	}	
}

3)驱动程序如何知道是否有按键动作
如果按键按下或松开时刻,会产生一个中断,CPU就会执行中断程序,所以,在中断程序设置一个标志即可。
a.定义一个全局变量,初始值为0,表示没有按键动作发生。
static int press = 0;
b.在中断程序中对该全局变量进行设置,按键按下设置为1。

irqreturn_t keysIRQ_Handler(int irq, void *dev)
{
	if( 按键IO口电平是否为有效电平 ){	
		press = 1;			//标志有按键动作的产生
	}
	return IRQ_HANDLED;
}

4)如何让进程进入休眠状态
让一个进程休眠有很多方式,这里先介绍一个最简单,最直接的休眠方式:msleep()函数。
该函数一旦被调用,则使进程会休眠指定长的时间,时间一到内核会唤醒这个进程。
使用该函数需要包含头文件#include <linux/delay.h>

ssize_t key_read(struct file *file, char __user *buff, size_t count, loff_t *loff)
{
	if(press == 0){
		//判断pfile->f_flags成员是否设置O_NONBLOCK
		if(file->f_flags & O_NONBLOCK){	//应用程序是否以非阻塞方式打开
			return -EAGAIN;		    //返回一个错误码,告诉应用程序你可以再尝试读取
		}else{				        //应用程序以阻塞方式打开
			/* 休眠,等待有按键动作唤醒进程 */
			while(press == 0)
				msleep(5);		
		}	
	} 	
	press = 0;		//清按键处理标志,一定要清0,否则下次read时候会出现问题
	……;
	return 0;
}

1.5.上述实现阻塞方法的缺陷

使用msleep方式休眠,Ctrl + C信号不能唤醒它。需要按下Ctrl+C后,在按一下开发板上任何一个按键唤醒它,它才可以接收到Ctrl + C信号。
以上方式基本可以阻塞和非阻塞功能,但上面这种休眠方式是信号不可中断的休眠,当进程休眠时候,控制终端发送信号给进程,进程不会受到信号,例如按Ctrl + C不会退出休眠,也不会结束进程。当按下Ctrl+C信号已经发送给进程,只是进程在休眠,收不到,这时候,在按下按键,进入中断,进程被唤醒,这时候进程会收到前面发送Ctrl + C信号,结束自己。
所以这种实现方式不够完善。可以使用等待队列来实现进程阻塞与唤醒。

2.等待队列的使用

10.2.1.等待队列介绍

等待队列是以双向循环链表为基础数据结构,这个队列存储着休眠的进程。它有两种数据结构:等待队列头 (wait_queue_head_t)和等待队列项(wait_queue_t)。等待队列头和等待队列项中都包含一个list_head类型的域作为”连接件”。它通过一个双链表和把等待task的头,和等待的进程列表链接起来。等待队列项与当前进程相关联。
在这里插入图片描述
等待队列的作用:希望等待特定事件的进程把自己放进等待队列,并放弃CPU控制权(实际上是休眠了)。然后当某一条件为真时,内核从等待队列中删除该进程,从而唤醒进程。

2.2.等待队列使用步骤

0.包含相关头文件
#include <linux/wait.h> //等待队列相关数据结构及函数
#include <linux/sched.h>//进程状态定义
1.定义等待队列头并初始化
init_waitqueue_head()
2.在需要等待(没有数据)的时候,进行休眠
wait_event_interruptible()
在内部会构建一个等待队列项,将等待队列项与当前进程相关联
3.在一个合适的时候(有数据),会将进程唤醒
wake_up_interruptible()

2.3.等待队列相关API

1)DECLARE_WAIT_QUEUE_HEAD(name)
头文件:#include <linux/wait.h>
宏功能:定义并且初始化一个等待队列头结构变量,名字为name
宏参数:要定义的队列头变量名
宏原型:
在这里插入图片描述

这种定义方式叫静态初始化方式(定义时候一起初始化了该结构)

2)init_waitqueue_head(q)
头文件:#include <linux/wait.h>

宏功能:初始化一个等待队列头
宏参数:要初始化的队列头变量名的地址
宏原型: 在这里插入图片描述

该宏使用示例:

wait_queue_head_t wq;		//定义全局变量
init_waitqueue_head(&wq);	//要在其他函数内调用初始化
上面这两行等效于:DECLARE_WAIT_QUEUE_HEAD(wq)

3)wait_event_interruptible(wq, condition)
头文件:#include <linux/wait.h>
宏功能:
1.构造一个等待队列项
2.将等待队列项加入到等待队列头中(并未休眠)
3.将等待队列项系相关的进程休眠(conditon为假的时候)
将与进程关联的等待队列项添加到指定的等待队列头中,将进程设置休眠,让出CPU调度。
wait_event_interruptible函数会一直阻塞。每次被唤醒的时候,首先检查condition是否为真,如果为真则返回0,如果为假则继续阻塞。
宏参数:
wq:要休眠的队列头(不是指针);
condition:休眠的条件,0是休眠,否则不进入休眠
//休眠进程时,如果condition为真,进程不会休眠,如果condition为假,进程才会休眠;
//唤醒进程时,如果condition为假,进程不会唤醒,如果condition为真,进行才会唤醒。
宏原型:
在这里插入图片描述

4)wake_up_interruptible(x)
头文件:#include <linux/wait.h>
宏功能:
将与进程关联的等待队列项从等待队列中删除,唤醒该进程,和wait_event_interruptible成对使用
宏参数:x:要唤醒的队列头指针
宏原型:
在这里插入图片描述
注: 加入等待队列用的是变量,唤醒等待队列用的是指针

3.IO多路复用模型

3.1.IO多路复用概念

如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式,就是IO多路复用。IO多路复用允许用户应用程序同时监测多个设备文件是否可以操作,如果可以操作就从设备读取或者向设备写入数据。应用程序通过调用select、poll、epoll函数来实现IO多路复用功能。
当应用程序调用select、poll函数时设备驱动程序中的poll接口函数就会执行。因此需要在设备驱动程序中实现poll接口函数。

APP调用poll、select函数的过程:

① APP 不知道驱动程序中是否有数据,可以先调用 poll 函数查询一下, poll 函数可以传入超时时间;
② APP进入内核态,调用到驱动程序的 poll 函数,如果有数据的话立刻返回;
③ 如果发现没有数据时就休眠一段时间;
④当有数据时,比如当按下按键时,驱动程序的中断服务程序被调用,它会记录数据、唤醒 APP;
⑤ 当超时时间到了之后,内核也会唤醒 APP;
⑥APP 根据 poll 函数的返回值就可以知道是否有数据,如果有数据就调用 read 得到数据。
刚进入poll接口函数时,有数据(返回)、无数据(休眠);
超时唤醒、有数据唤醒。

注:

1、 在poll接口函数中不发生休眠,而是在poll接口函数调用完毕后由内核对APP进行休眠;
2、 应用层调用一次poll函数,会引发两次poll系统调用,即驱动poll接口函数会被执行两次。
当应用层刚调用poll函数时,驱动poll接口函数执行一次,主要是将进程挂载到等待队列中。
当超时或有数据时,驱动poll接口函数执行另一次,主要通知应用层有无数据。

3.2.字符设备中的poll接口函数

函数原型:unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
函数参数:filp	结构体file类型指针
		wait 	结构体poll_table_struct类型指针
函数返回值:向应用程序返回设备或者资源的状态标志。
注:poll接口函数中的两个参数直接拿来使用即可,不需要关心两个参数表示什么意思!

3.3.实现poll接口函数

1)驱动中poll轮序机制比较复杂,一般通过内核提供的函数来实现轮询机制。在poll接口函数中调用poll_wait()函数,将进程添加到等待队列上,轮询查询本质上是通过等待队列的来实现。

头文件:#include <linux/poll.h>
函数原型:void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);
函数功能:将设备文件添加到查询表p中,该函数不会引起阻塞
函数参数:filp			poll接口函数中的struct file *参数 
		wait_address 	等待队列头结构变量的地址
		p				poll接口函数中的struct poll_table_struct *参数
函数返回值:无

2)返回设备或者资源的状态标志;没有数据可读返回0,可以返回的状态如下:

#define POLLIN		0x0001	//有数据可以读取
#define POLLPRI		0x0002	//有紧急的数据需要读取
#define POLLOUT		0x0004	//可以写数据
#define POLLERR		0x0008	//指定的文件描述符发生错误
#define POLLHUP		0x0010	//指定的文件描述符挂起
#define POLLNVAL	0x0020	//无效的请求
#define POLLRDNORM	0x0040	//等同于POLLIN,普通数据可读
#define POLLWRNORM	0x0100	//等同于POLLOUT,普通数据可写
常用返回值:
	设备可读,通常返回:(POLLIN|POLLRDNORM)
	设备可写,通常返回:(POLLOUT|POLLWRNORM)

3.4.驱动程序中poll接口模板

static unsigned int xxx_poll (struct file *pfile, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	//调用poll_wait,将当前进程添加到等待队列中
	poll_wait(pfile,&wq,wait);
	//返回设备状态标志(是否可读可写标志)
	if(press)
		mask |= POLLIN | POLLRDNORM;	//POLLIN可读,POLLRDNORM固定和POLLIN组合。
	return mask;
}

4.异步信号通知模型

4.1.异步信号通知简介

前面使用阻塞或者非阻塞的方式来读取驱动中的数据都是应用程序主动读取的,对于非阻塞方式来说还需要应用程序通过poll等函数不断的轮询。最好的方式就是驱动程序能主动向应用程序发出通知,报告自己可以访问,然后应用程序从驱动程序中读取或写入数据,类似于裸机程序中的中断。Linux提供异步通知来完成此功能。
异步通知是当设备资源可获得时,设备驱动通过主动向应用程序发送信号的方式,来报告自己可以访问,应用程序获取到信号以后就可以从设备驱动中读取数据或者写入数据了。异步通知还有另外一种说法,即“信号驱动的异步IO”。

4.2.异步信号通知的应用程序编程

为了使在用户空间的应用程序能接收一个设备驱动释放的信号,它必须完成以下3份工作。
1)打开设备文件,设置文件属主。
目的是将本应用程序的进程号告诉给设备驱动,设备驱动就可以向该进程发送信号。
问题:怎么设置文件属主?
答:使用fcntl函数

函数原型:int fcntl(int fd, int cmd, ... /* arg */ );int fcntl(int fd, int cmd);int fcntl(int fd, int cmd, long arg);int fcntl(int fd, int cmd ,struct flock* lock);
头文件:	#include <unistd.h>
		#include <fcntl.h>
功能:fcntl函数可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性,fcntl函数功能依据cmd的值的不同而不同。
参数:cmd
F_DUPFD:与dup函数相似,复制fd指向的文件描述符,调用成功后返回新的文件描述符。 
F_GETFD:读取文件描述符close-on-exec标志
F_SETFD:将文件描述符close-on-exec标志设置为第三个参数arg的最后一位
F_GETFL:获取文件打开方式的标志,标志值含义与open调用一致,通过返回值得到
F_SETFL:设置文件打开方式为arg指定方式
F_SETLK :设置文件锁定的状态
F_SETLKW:对文件进行阻塞上锁
F_GETLK:取得文件锁定的状态
F_GETOWN:获取当前在文件描述符fd上接收到SIGIO或SIGURG事件信号的进程或进程组标识
F_SETOWN:设置将要在文件描述符fd上接收SIGIO 或 SIGURG事件信号的进程或进程组标识 ,进程号通过arg参数传递
返回值:fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。

设置文件属主:fcntl(fd, F_SETOWN, getpid());
2)设置文件的FASYNC标志,开启异步通知
这相当于打开中断使能位。使用如下两行代码即可开启异步通知:

unsigned int Oflags = fcntl(fd, F_GETFL);	//获取当前的进程状态
fcntl(fd, F_SETFL,Oflags | FASYNC); 		//开启当前进程异步通知功能

注意,应用程序通过fcntl函数将进程状态设置为FASYNC,驱动程序中的fasync接口函数就会执行。
3)注册信号处理函数
这相当于注册中断处理函数,使用signal函数来设置信号的处理函数

void Signal_Handler(int sig){}		//信号处理函数定义
signal(SIGIO,Signal_Handler);		//注册信号处理函数

在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头是在驱动端,因此要在适当的时机让设备驱动释放信号。

4.3.异步信号通知的驱动程序编程

1)与进程进行关联
其目的是在设备驱动程序中记录信号该发送给哪个进程,只需要实现文件操作对象中的fasync接口即可。fasync接口函数格式如下:

int (*fasync) (int, struct file *, int);

fasync()接口函数实现方法:
只需要简单的将该参数的3个参数以及一个fasync_struct结构体指针的指针作为第四个参数传给fasync_helper函数即可.

int XXX_fasync(int fd ,struct file *pfile, int on)
{
	printk("%s is run\n",__func__);
	return fasync_helper(fd, pfile, on, &my_fasync);
}

struct file_opreations XXX_fops{
	.fasync = XXX_fasyn,
};

2)在资源可用时,向应用程序发送信号
一旦设备资源可以获得时,应该调用 kill_fasync() 释放SIGIO信号,可读时第三个参数设置为POLL_IN,可写时第三个参数设置为POLL_OUT

irqreturn_t XXX_IRQHandler(int irq, void *dev)
{
	if(XXX){
		kill_fasunc(&my_fasync, SIGIO, POLL_IN);
	}
	return IRQ_HANDLED;
}

3)删除异步通知
当应用程序访问的设备文件关闭时,即在设备驱动的release接口函数中,应显示调用设备驱动的fasync接口函数释放fasync_struct结构体指针,将文件从异步通知的列表中删除

int XXX_release(struct inode inode *inode, struct file *pfile) 
{
	return XXX_fasync(-1,pfile,0);
}

实际上,还是通过fasync_helper函数来完成以上操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值