天天看點

Hasen的linux裝置驅動開發學習之旅--阻塞與非阻塞I/O

/**
 * Author:hasen
 * 參考 :《linux裝置驅動開發詳解》
 * 簡介:android小菜鳥的linux
 * 	         裝置驅動開發學習之旅
 * 主題:阻塞與非阻塞I/O
 * Date:2014-11-05
 */
		阻塞操作是指在執行裝置操作時,若不能獲得資源,則挂起程序直到滿足可操作的條件後再進行操作。被
	挂起的程序進入休眠狀态,被從排程器的運作隊列移走,直到等待的條件被滿足。而非阻塞操作的程序在不能
	進行裝置操作時,并不挂起,它或者放棄,或者不停地查詢,直到條件滿足以進行操作為止。
		驅動程式通常需要提供這樣的能力,當應用程式進行read()、write()等系統調用時,若裝置的資源不能
	擷取,而使用者又希望以阻塞的方式通路裝置,驅動程式應在裝置驅動的xxx_read()、xxx_write()等操作中将
	程序阻塞直到資源可以擷取,此後,應用程式的read()、write()等調用才傳回,整個過程仍然進行了正确的
	裝置通路,使用者沒有感覺到;若使用者以非阻塞的方式通路裝置檔案,則當裝置資源不可擷取時,裝置驅動的
	xxx_read()、xxx_write()等操作應該立即傳回,read()、write()等系統調用也随即被傳回。
		阻塞的程序會進入休眠狀态,需要有一個地方來喚醒休眠的程序。喚醒程序的最大可能是在中斷裡面。
		
		下面的代碼段分别示範了以阻塞和非阻塞方式讀取一個字元的代碼。實際的序列槽程式設計中,若使用非阻塞模
		式,還可借助信号(sigaction)以異步方式通路序列槽以提高CPU的使用率。
		
		代碼段1:阻塞地讀序列槽一個字元
		
		char buf ;
		fd = open("/dev/ttyS1",O_RDWR) ; /*以阻塞的方式打開序列槽*/
		...
		res = read(fd,&buf,1) ; /*當序列槽上有輸入時才傳回*/
		if(res == 1)
			printf("%c\n",buf) ;
		
		代碼段2:非阻塞地讀序列槽一個字元
		
		char buf ;
		fd = open("/dev/ttyS1",O_RDWR|O_NONBLOCK) ;/*以非阻塞的方式打開序列槽*/
		...
		while(read(fd,&buf,1) == 1)
			continue ; /*序列槽上無輸入也傳回,是以要循環嘗試讀取序列槽*/
		printf("%c\n",buf) ;
	
	等待隊列
		linux驅動程式中,可以使用等待隊列(wait queue)來是實作阻塞程序的喚醒。wait queue很早就作為
	一個基本的功能機關出現在linux的核心裡了,它以隊列為基礎資料結構,與程序排程機制緊密結合,能夠用
	于實作核心中的異步事件通知機制。等待隊列可以用來同步對系統資源的通路。信号量在核心中依賴等待隊列
	實作的。
	
	等待隊列的操作如下:
		(1)定義“等待隊列頭”
			wait_queue_head_t my_queue ;
		(2)初始化“等待隊列頭”
			init_waitqueue_head(&my_queue) ;
		用下面的宏可以實作定義和初始化“等待隊列頭”
			DECLARE_WAIT_QUEUE_HEAD(name) 
		(3)定義等待隊列
			該宏用于定義并初始化一個名為name的等待隊列
			DECLARE_WAITQUEUE(name,tsk)
		(4)添加/移除等待隊列
			void fastcall add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait) ;
			void fastcall remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait) ;
			/*add_wait_queue用于将等待隊列wait添加到等待隊列頭q指向的等待隊列連結清單中,而
			 remove_wait_queue用于将等待隊列wait從附屬的等待隊頭q指向的等待隊列連結清單中移除*/
		(5)等待事件
			wait_event(queue,condition) ;
			wait_event_interruptible(queue,condition) ;
			wait_event_timeout(queue,condition,timeout) ;
			wait_event_interruptible_timeout(queue,condition,timeout) ;
			
			等待第一個參數queue作為等待隊列頭的等待隊列被喚醒,而且第二個參數condition必須滿足,否
			則繼續阻塞。wait_event()和wait_event_interruptible()的差別在于後者可以被信号打斷,而
			前者不能。加上timeout後意味着阻塞等待的逾時時間,以jiffy為機關,在第三個參數的timeout到
			達時,不論condition是否滿足,均傳回。
			
		(6)喚醒隊列
			void wake_up(wait_queue_head_t *queue) ;
			void wake_up_interruptible(wait_queue_head_t *queue) ;
			
			上述操作會喚醒以queue作為等待隊列頭的所有等待隊列中屬于該等待隊列頭的等待隊列對應的程序。
			
			wake_up和wait_event或wait_event_timeout成對使用,wake_up_interruptible
			和wait_event_interruptible或者wait_event_interruptible_timeout成對使用,wake_up可
			喚醒處于TASK_INTERRUPTIBLE和TASH_UNINTERRUPTIBLE的程序,而wait_event_interruptible
			僅能喚醒處于TASK_INTERRUPTIBLE的程序。
		(7)在等待隊列上睡眠
			sleep_on(wait_queue_head_t *q) ;
			interruptible_sleep_on(wait_queue_head_t *q) ;
			
			sleep_on()函數的作用是把目前的程序狀态置成TASK_UNINTERRUPTIBLE,并定義一個等待隊列,
			之後把它附屬到等待隊列q,直到資源可或獲得,q引導的等待隊列被喚醒。
			interruptible_sleep_on()函數的作用是把目前的程序狀态置成TASK_UNINTERRUPTIBLE,并定
			義一個等待隊列,之後把它附屬到等待隊列q,直到資源可或獲得,q引導的等待隊列被喚醒或者進
			程收到信号。
			
		在許多的linux驅動程式之中,并不調用sleep_on()或者interruptible_sleep_on(),而是直接
		進行程序狀态的改編和切換。
		
		代碼示例:在驅動代碼中改變程序狀态并調用schedule()
		
			static int xxx_write(struct file *filp,const char *buffer,
					size_t count,loff_t *ppos)
			{
				...
				DECLARE_WAITQUEUE(wait,current) ;/*定義等待隊列*/
				add_wait(&xxx_wait,&wait) ;/*添加等待隊列*/
				
				ret = count ;
				/*等待裝置緩沖區可寫*/
				do{
					avail = device_writable(...) ;
					if(avail < 0)
						__set_current_state(TASK_INTERRUPTIBLE) ;/*改變程序狀态*/
					
					if(avail < 0){
						if(file->f_flags & O_NONOBLOCK){ /*非阻塞*/
							if(!ret)
								ret = -EAGAIN ;
							goto out ;
						}
						schedule() ;/*排程其他程序執行*/
						if(signal_pending(current)){ /*如果是因為信号喚醒*/
							if(!ret)
								ret = -ERESTARTSYS ;
							goto out ;
						}
					}
				}while(avail<0) ;
				/*寫裝置緩沖區*/
				device_write(...) ;
				out :
				remove_wait_queue(&xxx_wait ,&wait) ;/*将等待隊列移出等待隊列頭*/
				set_current_state(TASK_RUNNING) ;/*設定程序狀态為TASK_RUNNNIG*/
			}
			
		上述的代碼示例對了解程序切換非常重要,是以應該讀幾遍直至完全領悟,下面是幾個要點:
			(1)如果是非阻塞通路(O_NONOBLOCK被設定),裝置忙時,直接傳回"-EAGAIN" .
			(2)對于阻塞通路,會進行狀态的切換并顯式通過"schedule()"排程其他程序執行。
			(3)醒來時要注意,由于排程出去的時候,程序的狀态是TASK_INTERRUPTIBLE,即淺度的睡眠,因
				此喚醒它的有可能是信号,是以,我們首先通過"signal_pending(current)"了解是不是信
				号喚醒的,如果是,立即傳回"-ERESTARTSYS" 。
				
	輪詢操作
			在使用者程式中,select()和poll()也是裝置阻塞與非阻塞通路息息相關的論題。使用非阻塞I/O的
		應用程式通常會使用select()和poll()系統調用查詢是否可以對裝置進行無阻塞的通路。select()和
		poll()系統調用最終會引發裝置驅動中的poll()函數被執行。epoll()是擴充的poll() .
			應用程式中最廣泛的應用的是select()系統調用,其函數原型是:
			int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,
					struct timeval *timeout) ;
			其中,readfds,writefds,exceptfds分别是被select()監視的讀、寫、異常處理的檔案描述符集
		合,numfds的值是需要檢查的号碼最高的檔案描述符加1,timeout參數是一個指向struct timeval類型
		的指針,它可以使select()在等待timeout時間後若沒有檔案描述符準備好則傳回。
			struct timeval 的定義如下:
			
			struct timeval{
				int tv_sec ;/*秒*/
				int tv_usec ;/*微秒*/
			};
			
		下面的操作用來設定、清除、判斷檔案描述符集合:
		FD_ZERO(fd_set *set) ;/*清除一個檔案描述符集*/
		FD_SET(int fd ,fd_set *set) ;/*将一個檔案描述符加入到檔案描述符集*/
		FD_CLR(int fd ,fd_set *set) ;/*将一個檔案描述符從檔案描述符集中清除*/
		FD_ISSET(int fd ,fd_set *set) ;/*判斷檔案描述符是否被置位*/
		
		裝置驅動中的輪詢程式設計
		
		裝置驅動中poll()函數的原型是:
		unsigned int (*poll) (struct poll_table *wait) ;
		第一個參數為file結構體指針,第二個參數為輪詢表指針。這個函數進行兩項工作:
		
		(1)對可能引起裝置檔案狀态變化的等待隊列調用poll_wait()函數,将對應的等待隊列頭添加
			到poll_table.
		(2)傳回表示是否能對裝置進行無阻塞讀、寫通路的掩碼。
		
		關鍵的用于向poll_table注冊等待隊列的poll_wait()函數的原型如下:
		void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait) ;
			poll_wait()函數的名字非常容易讓人産生誤會,以為它和wait_event()等一樣,會阻塞地
		等待某時間發生,其實這個函數并不會引起阻塞。poll_wait()函數所作的工作是把目前的程序添加到
		wait參數指定的等待清單(poll_table)中。
			驅動程式poll()函數應該傳回裝置資源的可擷取狀态,即POLLIN、POLLOUT、POLLPRI、POLLERR、
		POLLNVAL等宏的位“或”結果。每個宏的含義都是表明裝置的一種狀态,如POLLIN(定義為0x0001)意味着
		裝置可以無阻塞地讀,POLLOUT(定義為0x0004)意味着裝置可以無阻塞地寫。
			
			poll()函數的典型模闆
			
			static unsigned int xxx_poll(struct file *filp,poll_table *wait)
			{
				unsigned int mask = 0 ;
				struct xxx_dev *dev = filp->private_data ;/*擷取裝置結構體指針*/
				...
				poll_wait(filp,&dev->r_wait,wait) ;/*加讀等待隊列頭*/
				poll_wait(filp,&dev->w_wait,wait) ;/*加寫等待隊列頭*/
				
				if(...) /*可讀*/
					mask |= POLLIN | POLLRDNORM ;/*标示資料可獲得*/
				
				if(...) /*可寫*/
					mask |= POLLOUT | POLLWRNORM ;/*标示資料可寫入*/
				...
				return mask ;
			}
			
	總結:
		阻塞與非阻塞通路是I/O操作的兩種不同模式,前者在操作暫時不可進行時會讓程序睡眠,後者則不然。
		在裝置驅動中阻塞I/O一般基于等待隊列來說實作,等待隊列可用于同步驅動中事件發生的先後順序。
	使用非阻塞I/O的應用程式也可借助輪詢函數來查詢裝置是否能立即被通路,使用者空間調用select()和poll()
	接口,裝置驅動提供poll()函數。裝置驅動的poll()本身不會阻塞,但是poll()和select()系統調用則會阻
	塞地等待檔案描述符集合中的至少一個可通路或逾時。
           

繼續閱讀