linux 阻塞和非阻塞IO 篇一

本文详细介绍了Linux驱动开发中的阻塞和非阻塞IO模式,包括阻塞IO如何挂起进程直至设备可用,非阻塞IO如何处理资源不可用的情况。同时,讨论了等待队列在阻塞唤醒中的作用,以及如何通过等待队列头和等待队列项进行管理。此外,文章还探讨了轮询机制,如select、poll和epoll在非阻塞访问中的应用,强调了它们在处理大量并发连接时的效率问题和解决方案。
摘要由CSDN通过智能技术生成

@在linux驱动开发中有两种常见的设备访问模式,在编写驱动中要考虑到阻塞和非阻塞两种模式。

一:简介

这里的io不是我们所说的gpio引脚,是指input/output,也就是输入/输出,是应用程序对设备驱动的输入/输出操作。
当应用程序访问设备驱动进行操作的时候,如果不能获取设备资源,阻塞式IO就会将应用程序挂起,直到设备资源可以获取为止。对于非阻塞IO,应用程序对应的线程不会挂起,它要么轮询等待,直到资源可以使用,要么直接放弃。
总结:

1->阻塞IO 应用程序对应的线程直接挂起,直到设备资源可以访问获取;
应用程序 (user)------>read(user)------>设备不可用(kernel)------>sleep状态(kernel)------>设备可用(kernel)------>从驱动中read资源(user)
应用程序实现非阻塞访问代码示例:

       int fd,data = 0;
       
       fd = open("/dev/xxx_dev",ORDWR | O_NONBLOCK); ret = read(fd,
       &data,sizeof(data));

2->非阻塞IO 应用程序对应的线程轮询等待或者放弃访问,直到设备资源可以访问
应用程序 (user)------>read(user)------>设备不可用(kernel)------>返回错误码(kernel)------>读取错误码,继续read(user)......------> 设备可用(kernel)------>从驱动中read资源(user)

应用程序实现非阻塞访问代码示例:


       int fd,data = 0;
       
       fd = open("/dev/xxx_dev",ORDWR | O_NONBLOCK); ret = read(fd,
       &data,sizeof(data));

3->IO 是指input/output 输入输出设备,不是gpio

二:等待队列

  1. 等待队列队头
    阻塞访问的好处就是设备资源不可以访问时,进程进入休眠状态,这样可以将cpu资源让出来;当设备资源可以访问时,必须唤醒进程,这一过程一般在中断
    函数里完成唤醒工作。
    kernel提供了等待队列(wait queue)来实现阻塞唤醒工作,如果我们在驱动中使用等待队列,必须创建一个等待队列的列头。

    结构体 (include/linux/wait.h)

struct __wait_queue_head {
	spinlock_t lock;
	struct list_head task_list;
};

typedef struct _wait_queue_head wait_queue_head_t;

//使用说明:定义号等待列头之后,需要用init_waitqueue_head函数初始化等待队列头,函数原型
void init_queue_head(wait_queue_headt *q) //q为初始化等待队列列头
DECLARE_WAIT_QUEUE_HEAD 
  1. 等待队列项
    等待队列头就是等待队列的头部,每个访问设备的进程都是一个等待队列项,当设备资源不可用的时候,需要将这些进程添加到等待队列里面。等待队列项结构体如下
struct __wait_queue {
	unsigned int flags;
	void * private;
	wait_queue_func_t func;
	struct list_head task_list;
};

typedef struct __wait_queue wait_queue_t;

DECLEAR_WAITQUEUE(name,tsk)
//宏定义并初始化一个等待队列项
//name 等待队列项的名字
//tsk 表示等待队列项时属于哪一个进程,一般设置为current。current在kernel中是一个全局变量,表示当前进程。
  1. 将等待队列项 添加、移除等待队列头
    当设备资源不可以访问时,需要将进程对应的等待队列项添加到创建的等待队列头中。(只有添加到等待队列头中 以后 进程才能进入睡眠状态。)当设备可以访问,将等待队列项从等待队列头中移除即可。
    函数原型:
void add_wait_queue(wait_quit_head_t *q,
					wait_queue_t  *wait)
//q 等待队列项要加入的等待队列头
//待加入的等待队列项


void remove_queue(wait_queue_heat_t *q,
					wait_queue_t *wait)
  1. 等待唤醒
    当设备资源可以访问时,需要唤醒进入睡眠的进程,可以使用以下两个函数。
void wake_up(wait_queue_head_t  *q)
void wake_up_interruptible(wait_queue_head_t *q)

//这两个函数会将等待队列头中所有的进程都唤醒。
  1. 等待事件
    除了等待唤醒之外,可以设置等待队列的某个时间,进行唤醒。当事件条件满足之后,就会唤醒。
    函数原型:
wait_queue(wp,condition)//等待以wp为等待队列头的等待队列,当conditon为true时,可以唤醒。

wait_event_timeout(wp,condition,timeout)//l可以添加超时时间

wait_event_interrputible(wp,condition)//可以被信号打断

wait_event_interrputible(wp,condition,timeout)

三:轮询

应用程序以非阻塞方式访问,设备驱动需要提供非阻塞的访问方式,也就是轮询。poll 、epoll、select可以用于处理轮询,应用程序可以通过poll 、epoll、select函数来查询设备资源是否可以操作。如果可以操作,就读写设备。

当应用程序使用select、poll、epoll函数时,驱动成勋中的poll函数就会执行,因此需要在设备驱动里编写poll函数。

  1. select 函数
int select( int nfds,
			fd_set *readfds,
			fd_set *writefds,
			fd_set *excptfds,
			struct timeval *timeout)
参数:
nfds:所监视的三类文件描述集合,最大文件描述符加1

readfds、writrefds、exceptfds:三个指针指向描述符集合,三个参数关心那些描述符,满足那些条件。 

例子:比如从一个设备文件读取数据,那么可以先定义一个fd_set 变量传递给readfds。
定义宏:

void FD_ZERO(fd_set *set) //用于将fd_set 变量的所有bit清零。
void FD_SET(int fd,fd_set *set) //FD_SET 用于将fd_set变量的某个位置置1,也就是向fd_set添加一个文件描述符,参数fd,就是要加入的文件描述符。 
void FD_CLR(int fd,fd_set *set)//将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd,就是要删除的文件描述符。
int FD_ISSET(int fd,fd_set *set)//用于测试一个文件是否属于某个集合,参数fd就是要判断的文件描述符。

timeout 超时时间,调用select函数等待某些文件描述符可以设置超时时间,超时时间结构使用timeval,结构体定义如下:

struct timeval {
	long tv_sec; //s
	long tv_usec; //us
};

select 函数使用示例:

//select 非阻塞函数访问示例

void mian{
	int ret,fd;
	fd_set readfds;
	struct timeval timeout;

	fd = open("dev",O_RDWR | O_NONBLOCK);

	FD_ZERO(&readfds);//清楚readfds
	FD_SET(fd,&readfds);//将fd添加到readfds里面
	
	timeout.tv_sec = 0;
	timeout.tv_usec = 500000;
	ret =select(fd+1 ,&readfds,NULL,NULL,&timeout);
	switch(ret){
		case 0:
			timeout 超时
		break;
		case -1:
			error 错误
		break;
		default: 
			if(FD_ISSET(fd,&readfds)){
			read()
			}
		break;
	}

}

  1. poll函数
    在单个线程中,select函数监视的文件描述符数量有最大的限制,一般为1024,可以修改内核将监视的文件描述符数量改大,但这样会降低效率。这时候就可以使用poll函数,poll函数的本质上和select没有太大区别,但是poll函数没有最大文件描述符限制。poll函数原型:
int poll(struct pollfd *fds,
		nfds_t nfds,
		int timeout)

struct pollfd{
	int fds;
	short events;
	short  revents;
};

fds//要监视的文件描述符,如果fd无效,events监视事件也无效,并且revents返回0,events是要监视的时间,可以监视的事件类型如下:
POLLIN  有数据可读
POLLPRI 有紧急的数据需要读取
POLLOUT 写数据
POLLERR 错误
POLLHUP 挂起

nfds:poll函数要监视的文件描述符数量
timeout 超时时间,单位ms.

poll函数进行非阻塞访问的示例:

void mian(void)
{
	int ret,fd;
	struct pollfd fds;
	
	fd = open("dev",O_RDWR | O_NONBLOCK);

	fds.fd = fd;
	fds.events = POLLIN;
	ret = poll(&fds, 1, 500);
	if(ret){
		read 数据
	}else if (ret==0{
		timeout 超时
	}else if(ret<0{
		error 错误
	}
}
  1. epoll函数
    无论是select还是poll函数都会随着监听的fd数量的增加,出现效率低下的问题,而且poll函数每次必须遍历所有的文件描述符来检查就绪的设备描述符,这个过程很浪费时间。为此,epoll应出来了,为处理大并发而准备的,常常在网络编程中使用epoll函数。

首先应用程序使用epoll_create函数创建一个epoll,函数原型:

int epoll_create(int size)
size :随便填写一个大于0的值就可以
返回值:为-1创建失败

句柄创建完成之后,要使用epoll_ctl 想其中添加要监视的文件描述符以及监视的事件,函数原型如下:

int epoll_ctl( 	int epfd,
			int op,
			int fd,
			struct epoll_event *event) 

)
函数参数:
epfd :要操作的epoll句柄,也就是epoll_create函数创建的epoll句柄。
op:表示要对epds(epoll句柄)进行操作,可以设置为:
	EPOLL_CTL_ADD 向epfd添加参数fd表示的描述符
	EPOLL_CTL_MOD 修改参数fd的event事件
	EPOLL_CTL_DEL 从epfd中删除fd描述符

fd:要监视的文件描述符。
event:要监视的事件类型,为epoll_event结构体类型指针:
	struct epoll_event{
		uint32_t events ;//poll事件
						//事件可以是EPOLLIN EPOLLOUT EPOLLPRI EPOLLERR  
		epoll_data_t data; //用户数据
	}
返回值-1,失败,0成功

前面设置好以后,应用程序就可以通过epoll_wait 函数来等待事件的发生,雷士select函数。epoll_wait函数原型如下:

int epoll_wait(
		int epfd,
		struct epoll_event *events,
		int maxevents,
		int timeout )
参数:
epfd:要等待的epoll
events:指向epoll_event结构体数组,当事件发生的时候,Linux内核会填写						
		events,调用者可以根据events函数判断发生哪些事件
maxevents: events数据大小,需要>0
timeout: 超时间,单位ms
返回值 :-1 失败;0 超时;其他值,准备就绪文件的描述符数量。

linux驱动 添加阻塞和非阻塞 …

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值