Linux 设备驱动中的 I/O模型(一)—— 阻塞和非阻塞I/O
在前面學(xué)習(xí)網(wǎng)絡(luò)編程時(shí),曾經(jīng)學(xué)過I/O模型?Linux 系統(tǒng)應(yīng)用編程——網(wǎng)絡(luò)編程(I/O模型),下面學(xué)習(xí)一下I/O模型在設(shè)備驅(qū)動(dòng)中的應(yīng)用。
? ? ? ?回顧一下在Unix/Linux下共有五種I/O模型,分別是:
a -- 阻塞I/O
b -- 非阻塞I/O
c -- I/O復(fù)用(select和poll)
d -- 信號(hào)驅(qū)動(dòng)I/O(SIGIO)
e -- 異步I/O(Posix.1的aio_系列函數(shù))
?
? ? ? ?下面我們先學(xué)習(xí)阻塞I/O、非阻塞I/O?、I/O復(fù)用(select和poll),先學(xué)習(xí)一下基礎(chǔ)概念
a -- 阻塞?
? ? ? ?阻塞操作是指在執(zhí)行設(shè)備操作時(shí),若不能獲得資源,則掛起進(jìn)程知道滿足可操作的條件后再進(jìn)行操作;被掛起的進(jìn)程進(jìn)入休眠狀態(tài)(放棄CPU),被從調(diào)度器的運(yùn)行隊(duì)列移走,直到等待的條件被滿足;?
b -- 非阻塞
? ? ? 非阻塞的進(jìn)程在不能進(jìn)行設(shè)備操作時(shí),并不掛起(繼續(xù)占用CPU),它或者放棄,或者不停地查詢,直到可以操作為止;
? ? ? 二者的區(qū)別可以看應(yīng)用程序的調(diào)用是否立即返回!
? ? ? 驅(qū)動(dòng)程序通常需要提供這樣的能力:當(dāng)應(yīng)用程序進(jìn)行 read()、write() 等系統(tǒng)調(diào)用時(shí),若設(shè)備的資源不能獲取,而用戶又希望以阻塞的方式訪問設(shè)備,驅(qū)動(dòng)程序應(yīng)在設(shè)備驅(qū)動(dòng)的xxx_read()、xxx_write() 等操作中將進(jìn)程阻塞直到資源可以獲取,此后,應(yīng)用程序的?read()、write() 才返回,整個(gè)過程仍然進(jìn)行了正確的設(shè)備 訪問,用戶并沒感知到;若用戶以非阻塞的方式訪問設(shè)備文件,則當(dāng)設(shè)備資源不可獲取時(shí),設(shè)備驅(qū)動(dòng)的?xxx_read()、xxx_write() 等操作立刻返回,?read()、write() 等系統(tǒng)調(diào)用也隨即被返回。
? ? ? 因?yàn)樽枞倪M(jìn)程會(huì)進(jìn)入休眠狀態(tài),因此,必須確保有一個(gè)地方能夠喚醒休眠的進(jìn)程,否則,進(jìn)程就真的掛了。喚醒進(jìn)程的地方最大可能發(fā)生在中斷里面,因?yàn)橛布Y源獲得的同時(shí)往往伴隨著一個(gè)中斷。
? ? ??阻塞I/O通常由等待隊(duì)列來實(shí)現(xiàn),而非阻塞I/O由輪詢來實(shí)現(xiàn)。
一、阻塞I/O實(shí)現(xiàn) —— 等待隊(duì)列
1、基礎(chǔ)概念
? ? ? ?在Linux 驅(qū)動(dòng)程序中,可以使用等待隊(duì)列(wait queue)來實(shí)現(xiàn)阻塞進(jìn)程的喚醒。wait queue 很早就作為一個(gè)基本的功能單位出現(xiàn)在Linux 內(nèi)核里了,它以隊(duì)列為基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),與進(jìn)程調(diào)度機(jī)制緊密結(jié)合,能夠?qū)崿F(xiàn)內(nèi)核中的異步事件通知機(jī)制。等待隊(duì)列可以用來同步對(duì)系統(tǒng)資源的訪問,上一篇文章所述的信號(hào)量在內(nèi)核中也依賴等待隊(duì)列來實(shí)現(xiàn)。
? ? ??在Linux內(nèi)核中使用等待隊(duì)列的過程很簡(jiǎn)單,首先定義一個(gè)wait_queue_head,然后如果一個(gè)task想等待某種事件,那么調(diào)用wait_event(等待隊(duì)列,事件)就可以了。
? ? ? 等待隊(duì)列應(yīng)用廣泛,但是內(nèi)核實(shí)現(xiàn)卻十分簡(jiǎn)單。其涉及到兩個(gè)比較重要的數(shù)據(jù)結(jié)構(gòu):__wait_queue_head,該結(jié)構(gòu)描述了等待隊(duì)列的鏈頭,其包含一個(gè)鏈表和一個(gè)原子鎖,結(jié)構(gòu)定義如下: ? ?
struct __wait_queue_head? { spinlock_t lock; ? ? ? ? ? ? ? ? ? ?/* 保護(hù)等待隊(duì)列的原子鎖 */ struct list_head task_list; ? ? ? ? /* 等待隊(duì)列 */ }; typedef struct __wait_queue_head wait_queue_head_t;
__wait_queue,該結(jié)構(gòu)是對(duì)一個(gè)等待任務(wù)的抽象。每個(gè)等待任務(wù)都會(huì)抽象成一個(gè)wait_queue,并且掛載到wait_queue_head上。該結(jié)構(gòu)定義如下:
struct __wait_queue? { unsigned int flags; void *private; ? ? ? ? ? ? ? ? ? ? ? /* 通常指向當(dāng)前任務(wù)控制塊 */ /* 任務(wù)喚醒操作方法,該方法在內(nèi)核中提供,通常為autoremove_wake_function */ wait_queue_func_t func; ? ? ? ? ? ?? struct list_head task_list; ? ? ? ? ? ? ?/* 掛入wait_queue_head的掛載點(diǎn) */ };
? ? Linux中等待隊(duì)列的實(shí)現(xiàn)思想如下圖所示,當(dāng)一個(gè)任務(wù)需要在某個(gè)wait_queue_head上睡眠時(shí),將自己的進(jìn)程控制塊信息封裝到wait_queue中,然后掛載到wait_queue的鏈表中,執(zhí)行調(diào)度睡眠。當(dāng)某些事件發(fā)生后,另一個(gè)任務(wù)(進(jìn)程)會(huì)喚醒wait_queue_head上的某個(gè)或者所有任務(wù),喚醒工作也就是將等待隊(duì)列中的任務(wù)設(shè)置為可調(diào)度的狀態(tài),并且從隊(duì)列中刪除。
? ? ? ?使用等待隊(duì)列時(shí)首先需要定義一個(gè)wait_queue_head,這可以通過DECLARE_WAIT_QUEUE_HEAD宏來完成,這是靜態(tài)定義的方法。該宏會(huì)定義一個(gè)wait_queue_head,并且初始化結(jié)構(gòu)中的鎖以及等待隊(duì)列。當(dāng)然,動(dòng)態(tài)初始化的方法也很簡(jiǎn)單,初始化一下鎖及隊(duì)列就可以了。
? ? ? ?一個(gè)任務(wù)需要等待某一事件的發(fā)生時(shí),通常調(diào)用wait_event,該函數(shù)會(huì)定義一個(gè)wait_queue,描述等待任務(wù),并且用當(dāng)前的進(jìn)程描述塊初始化wait_queue,然后將wait_queue加入到wait_queue_head中。
函數(shù)實(shí)現(xiàn)流程說明如下:
a -- 用當(dāng)前的進(jìn)程描述塊(PCB)初始化一個(gè)wait_queue描述的等待任務(wù)。
b -- 在等待隊(duì)列鎖資源的保護(hù)下,將等待任務(wù)加入等待隊(duì)列。
c -- 判斷等待條件是否滿足,如果滿足,那么將等待任務(wù)從隊(duì)列中移出,退出函數(shù)。
d -- ?如果條件不滿足,那么任務(wù)調(diào)度,將CPU資源交與其它任務(wù)。
e -- 當(dāng)睡眠任務(wù)被喚醒之后,需要重復(fù)b、c 步驟,如果確認(rèn)條件滿足,退出等待事件函數(shù)。
2、等待隊(duì)列接口函數(shù)
1、定義并初始化
/* 定義“等待隊(duì)列頭” */ wait_queue_head_t my_queue; /* 初始化“等待隊(duì)列頭”*/ init_waitqueue_head(&my_queue);
直接定義并初始化。init_waitqueue_head()函數(shù)會(huì)將自旋鎖初始化為未鎖,等待隊(duì)列初始化為空的雙向循環(huán)鏈表。
DECLARE_WAIT_QUEUE_HEAD(my_queue);?定義并初始化,可以作為定義并初始化等待隊(duì)列頭的快捷方式。
2、定義等待隊(duì)列:
定義并初始化一個(gè)名為name的等待隊(duì)列。
3、(從等待隊(duì)列頭中)添加/移出等待隊(duì)列:
/* add_wait_queue()函數(shù),設(shè)置等待的進(jìn)程為非互斥進(jìn)程,并將其添加進(jìn)等待隊(duì)列頭(q)的隊(duì)頭中*/ void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); /* 該函數(shù)也和add_wait_queue()函數(shù)功能基本一樣,只不過它是將等待的進(jìn)程(wait)設(shè)置為互斥進(jìn)程。*/ void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);
4、等待事件:
(1)wait_event()宏:
[cpp]?view plaincopy
? ? 在等待會(huì)列中睡眠直到condition為真。在等待的期間,進(jìn)程會(huì)被置為TASK_UNINTERRUPTIBLE進(jìn)入睡眠,直到condition變量變?yōu)檎?。每次進(jìn)程被喚醒的時(shí)候都會(huì)檢查condition的值.
(2)wait_event_interruptible()函數(shù):
? ?和wait_event()的區(qū)別是調(diào)用該宏在等待的過程中當(dāng)前進(jìn)程會(huì)被設(shè)置為TASK_INTERRUPTIBLE狀態(tài).在每次被喚醒的時(shí)候,首先檢查condition是否為真,如果為真則返回,否則檢查如果進(jìn)程是被信號(hào)喚醒,會(huì)返回-ERESTARTSYS錯(cuò)誤碼.如果是condition為真,則返回0.
(3)wait_event_timeout()宏:
? ?也與wait_event()類似.不過如果所給的睡眠時(shí)間為負(fù)數(shù)則立即返回.如果在睡眠期間被喚醒,且condition為真則返回剩余的睡眠時(shí)間,否則繼續(xù)睡眠直到到達(dá)或超過給定的睡眠時(shí)間,然后返回0
(4)wait_event_interruptible_timeout()宏:
? ?與wait_event_timeout()類似,不過如果在睡眠期間被信號(hào)打斷則返回ERESTARTSYS錯(cuò)誤碼.
(5) wait_event_interruptible_exclusive()宏
? ?同樣和wait_event_interruptible()一樣,不過該睡眠的進(jìn)程是一個(gè)互斥進(jìn)程.
5、喚醒隊(duì)列
(1)wake_up()函數(shù)
[cpp]?view plaincopy
#define wake_up_interruptible(x) ? ?__wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
和wake_up()唯一的區(qū)別是它只能喚醒TASK_INTERRUPTIBLE狀態(tài)的進(jìn)程.,與wait_event_interruptible/wait_event_interruptible_timeout/ wait_event_interruptible_exclusive成對(duì)使用。
下面看一個(gè)實(shí)例:
[cpp]?view plaincopy注意兩個(gè)概念:
a -- ?瘋狂獸群
? ? ??wake_up的時(shí)候,所有阻塞在隊(duì)列的進(jìn)程都會(huì)被喚醒,但是因?yàn)閏ondition的限制,只有一個(gè)進(jìn)程得到資源,其他進(jìn)程又會(huì)再次休眠,如果數(shù)量很大,稱為?瘋狂獸群。
b -- 獨(dú)占等待
? ? ? 等待隊(duì)列的入口設(shè)置一個(gè)WQ_FLAG_EXCLUSIVE標(biāo)志,就會(huì)添加到等待隊(duì)列的尾部,沒有設(shè)置設(shè)置的添加到頭部,wake up的時(shí)候遇到第一個(gè)具有WQ_FLAG_EXCLUSIVE這個(gè)標(biāo)志的進(jìn)程就停止喚醒其他進(jìn)程。
二、非阻塞I/O實(shí)現(xiàn)方式 —— 多路復(fù)用
1、輪詢的概念和作用
? ? ??在用戶程序中,select()?和?poll()?也是設(shè)備阻塞和非阻塞訪問息息相關(guān)的論題。使用非阻塞I/O的應(yīng)用程序通常會(huì)使用select() 和 poll() 系統(tǒng)調(diào)用查詢是否可對(duì)設(shè)備進(jìn)行無阻塞的訪問。select() 和 poll() 系統(tǒng)調(diào)用最終會(huì)引發(fā)設(shè)備驅(qū)動(dòng)中的 poll()函數(shù)被執(zhí)行。
2、應(yīng)用程序中的輪詢編程
? ? ? 在用戶程序中,select()和poll()本質(zhì)上是一樣的, 不同只是引入的方式不同,前者是在BSD UNIX中引入的,后者是在System V中引入的。用的比較廣泛的是select系統(tǒng)調(diào)用。原型如下:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptionfds, struct timeval *timeout);
??? 其中readfs,writefds,exceptfds分別是select()監(jiān)視的讀,寫和異常處理的文件描述符集合,numfds的值是需要檢查的號(hào)碼最高的文件描述符加1,timeout則是一個(gè)時(shí)間上限值,超過該值后,即使仍沒有描述符準(zhǔn)備好也會(huì)返回。
struct timeval {int tv_sec; ? //秒int tv_usec; ? //微秒 }
涉及到文件描述符集合的操作主要有以下幾種:
1)清除一個(gè)文件描述符集 ? FD_ZERO(fd_set *set);2)將一個(gè)文件描述符加入文件描述符集中 ? ?FD_SET(int fd,fd_set *set);
3)將一個(gè)文件描述符從文件描述符集中清除 ?FD_CLR(int fd,fd_set *set);
4)判斷文件描述符是否被置位 ? ?FD_ISSET(int fd,fd_set *set);
最后我們利用上面的文件描述符集的相關(guān)來寫個(gè)驗(yàn)證添加了設(shè)備輪詢的驅(qū)動(dòng),把上邊兩塊聯(lián)系起來
3、設(shè)備驅(qū)動(dòng)中的輪詢編程
? ? ? ?設(shè)備驅(qū)動(dòng)中的poll() 函數(shù)原型如下
unsigned int(*poll)(struct file *filp, struct poll_table * wait);
第一個(gè)參數(shù)是file結(jié)構(gòu)體指針,第二個(gè)參數(shù)是輪詢表指針,poll設(shè)備方法完成兩件事:
a -- 對(duì)可能引起設(shè)備文件狀態(tài)變化的等待隊(duì)列調(diào)用poll_wait()函數(shù),將對(duì)應(yīng)的等待隊(duì)列頭添加到poll_table,如果沒有文件描述符可用來執(zhí)行 I/O, 則內(nèi)核使進(jìn)程在傳遞到該系統(tǒng)調(diào)用的所有文件描述符對(duì)應(yīng)的等待隊(duì)列上等待。
b -- 返回表示是否能對(duì)設(shè)備進(jìn)行無阻塞讀、寫訪問的掩碼。
位掩碼:POLLRDNORM, POLLIN,POLLOUT,POLLWRNORM
設(shè)備可讀,通常返回:(POLLIN | POLLRDNORM)
設(shè)備可寫,通常返回:(POLLOUT | POLLWRNORM)
? ? ???
poll_wait()函數(shù):用于向 poll_table注冊(cè)等待隊(duì)列
?void poll_wait(struct file *filp, wait_queue_head_t *queue,poll_table *wait) ?
? ? ??poll_wait()函數(shù)不會(huì)引起阻塞,它所做的工作是把當(dāng)前進(jìn)程添加到wait 參數(shù)指定的等待列表(poll_table)中。
? ? ?真正的阻塞動(dòng)作是上層的select/poll函數(shù)中完成的。select/poll會(huì)在一個(gè)循環(huán)中對(duì)每個(gè)需要監(jiān)聽的設(shè)備調(diào)用它們自己的poll支持函數(shù)以使得當(dāng)前進(jìn)程被加入各個(gè)設(shè)備的等待列表。若當(dāng)前沒有任何被監(jiān)聽的設(shè)備就緒,則內(nèi)核進(jìn)行調(diào)度(調(diào)用schedule)讓出cpu進(jìn)入阻塞狀態(tài),schedule返回時(shí)將再次循環(huán)檢測(cè)是否有操作可以進(jìn)行,如此反復(fù);否則,若有任意一個(gè)設(shè)備就緒,select/poll都立即返回。
具體過程如下:
a --?用戶程序第一次調(diào)用select或者poll,驅(qū)動(dòng)調(diào)用poll_wait并使兩條隊(duì)列都加入poll_table結(jié)構(gòu)中作為下次調(diào)用驅(qū)動(dòng)函數(shù)poll的條件,一個(gè)mask返回值指示設(shè)備是否可操作,0為未準(zhǔn)備狀態(tài),如果文件描述符未準(zhǔn)備好可讀或可寫,用戶進(jìn)程被會(huì)加入到寫或讀等待隊(duì)列中進(jìn)入睡眠狀態(tài)。
b --?當(dāng)驅(qū)動(dòng)執(zhí)行了某些操作,例如,寫緩沖或讀緩沖,寫緩沖使讀隊(duì)列被喚醒,讀緩沖使寫隊(duì)列被喚醒,于是select或者poll系統(tǒng)調(diào)用在將要返回給用戶進(jìn)程時(shí)再次調(diào)用驅(qū)動(dòng)函數(shù)poll,驅(qū)動(dòng)依然調(diào)用poll_wait 并使兩條隊(duì)列都加入poll_table結(jié)構(gòu)中,并判斷可寫或可讀條件是否滿足,如果mask返回POLLIN | POLLRDNORM或POLLOUT | POLLWRNORM則指示可讀或可寫,這時(shí)select或poll真正返回給用戶進(jìn)程,如果mask還是返回0,則系統(tǒng)調(diào)用select或poll繼續(xù)不返回
? ? ?
下面是一個(gè)典型模板:
[cpp]?view plaincopy
4、調(diào)用過程:
Linux下select調(diào)用的過程:
1、用戶層應(yīng)用程序調(diào)用select(),底層調(diào)用poll())
2、核心層調(diào)用sys_select() ------> do_select()
最終調(diào)用文件描述符fd對(duì)應(yīng)的struct file類型變量的struct file_operations *f_op的poll函數(shù)。
poll指向的函數(shù)返回當(dāng)前可否讀寫的信息。
1)如果當(dāng)前可讀寫,返回讀寫信息。
2)如果當(dāng)前不可讀寫,則阻塞進(jìn)程,并等待驅(qū)動(dòng)程序喚醒,重新調(diào)用poll函數(shù),或超時(shí)返回。
3、驅(qū)動(dòng)需要實(shí)現(xiàn)poll函數(shù)
當(dāng)驅(qū)動(dòng)發(fā)現(xiàn)有數(shù)據(jù)可以讀寫時(shí),通知核心層,核心層重新調(diào)用poll指向的函數(shù)查詢信息。
4、實(shí)例分析
1、memdev.h
/*mem設(shè)備描述結(jié)構(gòu)體*/ struct mem_dev { char *data; unsigned long size; wait_queue_head_t inq; };#endif /* _MEMDEV_H_ */ 2、驅(qū)動(dòng)程序 memdev.c [cpp]?view plaincopy
4、應(yīng)用程序 app-read.c
[cpp]?view plaincopy總結(jié)
以上是生活随笔為你收集整理的Linux 设备驱动中的 I/O模型(一)—— 阻塞和非阻塞I/O的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022数学建模“五一杯”B题
- 下一篇: Linux环境变量的设置和查看