天天看點

linux 阻塞和非阻塞IO 篇一一:簡介

@在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驅動 添加阻塞和非阻塞 …

繼續閱讀