@在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
二:等待隊列
-
等待隊列隊頭
阻塞通路的好處就是裝置資源不可以通路時,程序進入休眠狀态,這樣可以将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
-
等待隊列項
等待隊列頭就是等待隊列的頭部,每個通路裝置的程序都是一個等待隊列項,當裝置資源不可用的時候,需要将這些程序添加到等待隊列裡面。等待隊列項結構體如下
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中是一個全局變量,表示目前程序。
-
将等待隊列項 添加、移除等待隊列頭
當裝置資源不可以通路時,需要将程序對應的等待隊列項添加到建立的等待隊列頭中。(隻有添加到等待隊列頭中 以後 程序才能進入睡眠狀态。)當裝置可以通路,将等待隊列項從等待隊列頭中移除即可。
函數原型:
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)
-
等待喚醒
當裝置資源可以通路時,需要喚醒進入睡眠的程序,可以使用以下兩個函數。
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
//這兩個函數會将等待隊列頭中所有的程序都喚醒。
-
等待事件
除了等待喚醒之外,可以設定等待隊列的某個時間,進行喚醒。當事件條件滿足之後,就會喚醒。
函數原型:
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函數。
- 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;
}
}
-
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 錯誤
}
}
-
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 逾時;其他值,準備就緒檔案的描述符數量。