歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux技術 >> Linux 設備驅動中的 I/O模型(一)—— 阻塞和非阻塞I/O

Linux 設備驅動中的 I/O模型(一)—— 阻塞和非阻塞I/O

日期:2017/3/3 11:49:47   编辑:Linux技術
在前面學習網絡編程時,曾經學過I/O模型 Linux 系統應用編程——網絡編程(I/O模型),下面學習一下I/O模型在設備驅動中的應用。
回顧一下在Unix/Linux下共有五種I/O模型,分別是:
a -- 阻塞I/O
b -- 非阻塞I/O
c -- I/O復用(select和poll)
d -- 信號驅動I/O(SIGIO)
e -- 異步I/O(Posix.1的aio_系列函數)
下面我們先學習阻塞I/O、非阻塞I/O 、I/O復用(select和poll),先學習一下基礎概念
a -- 阻塞
阻塞操作是指在執行設備操作時,若不能獲得資源,則掛起進程知道滿足可操作的條件後再進行操作;被掛起的進程進入休眠狀態(放棄CPU),被從調度器的運行隊列移走,直到等待的條件被滿足;
b -- 非阻塞
非阻塞的進程在不能進行設備操作時,並不掛起(繼續占用CPU),它或者放棄,或者不停地查詢,直到可以操作為止;
二者的區別可以看應用程序的調用是否立即返回!
驅動程序通常需要提供這樣的能力:當應用程序進行 read()、write() 等系統調用時,若設備的資源不能獲取,而用戶又希望以阻塞的方式訪問設備,驅動程序應在設備驅動的xxx_read()、xxx_write() 等操作中將進程阻塞直到資源可以獲取,此後,應用程序的 read()、write() 才返回,整個過程仍然進行了正確的設備 訪問,用戶並沒感知到;若用戶以非阻塞的方式訪問設備文件,則當設備資源不可獲取時,設備驅動的 xxx_read()、xxx_write()
等操作立刻返回, read()、write() 等系統調用也隨即被返回

因為阻塞的進程會進入休眠狀態,因此,必須確保有一個地方能夠喚醒休眠的進程,否則,進程就真的掛了。喚醒進程的地方最大可能發生在中斷裡面,因為硬件資源獲得的同時往往伴隨著一個中斷
阻塞I/O通常由等待隊列來實現,而非阻塞I/O由輪詢來實現。
一、阻塞I/O實現 —— 等待隊列
1、基礎概念
在Linux 驅動程序中,可以使用等待隊列(wait queue)來實現阻塞進程的喚醒。wait queue 很早就作為一個基本的功能單位出現在Linux 內核裡了,它以隊列為基礎數據結構,與進程調度機制緊密結合,能夠實現內核中的異步事件通知機制。等待隊列可以用來同步對系統資源的訪問,上一篇文章所述的信號量在內核中也依賴等待隊列來實現。
在Linux內核中使用等待隊列的過程很簡單,首先定義一個wait_queue_head,然後如果一個task想等待某種事件,那麼調用wait_event(等待隊列,事件)就可以了。
等待隊列應用廣泛,但是內核實現卻十分簡單。其涉及到兩個比較重要的數據結構:__wait_queue_head,該結構描述了等待隊列的鏈頭,其包含一個鏈表和一個原子鎖,結構定義如下:
struct __wait_queue_head 
{
	 spinlock_t lock;                    /* 保護等待隊列的原子鎖 */
	 struct list_head task_list;         /* 等待隊列 */
};

typedef struct __wait_queue_head wait_queue_head_t;

__wait_queue,該結構是對一個等待任務的抽象。每個等待任務都會抽象成一個wait_queue,並且掛載到wait_queue_head上。該結構定義如下:
struct __wait_queue 
{
	unsigned int flags;
	void *private;                       /* 通常指向當前任務控制塊 */

	/* 任務喚醒操作方法,該方法在內核中提供,通常為autoremove_wake_function */
	wait_queue_func_t func;             
	struct list_head task_list;              /* 掛入wait_queue_head的掛載點 */
};

Linux中等待隊列的實現思想如下圖所示,當一個任務需要在某個wait_queue_head上睡眠時,將自己的進程控制塊信息封裝到wait_queue中,然後掛載到wait_queue的鏈表中,執行調度睡眠。當某些事件發生後,另一個任務(進程)會喚醒wait_queue_head上的某個或者所有任務,喚醒工作也就是將等待隊列中的任務設置為可調度的狀態,並且從隊列中刪除。

使用等待隊列時首先需要定義一個wait_queue_head,這可以通過DECLARE_WAIT_QUEUE_HEAD宏來完成,這是靜態定義的方法。該宏會定義一個wait_queue_head,並且初始化結構中的鎖以及等待隊列。當然,動態初始化的方法也很簡單,初始化一下鎖及隊列就可以了。
一個任務需要等待某一事件的發生時,通常調用wait_event,該函數會定義一個wait_queue,描述等待任務,並且用當前的進程描述塊初始化wait_queue,然後將wait_queue加入到wait_queue_head中。
函數實現流程說明如下:
a -- 用當前的進程描述塊(PCB)初始化一個wait_queue描述的等待任務。
b -- 在等待隊列鎖資源的保護下,將等待任務加入等待隊列。
c -- 判斷等待條件是否滿足,如果滿足,那麼將等待任務從隊列中移出,退出函數。
d -- 如果條件不滿足,那麼任務調度,將CPU資源交與其它任務。
e -- 當睡眠任務被喚醒之後,需要重復b、c 步驟,如果確認條件滿足,退出等待事件函數。
2、等待隊列接口函數
1、定義並初始化
/* 定義“等待隊列頭” */

wait_queue_head_t my_queue;

/* 初始化“等待隊列頭”*/
init_waitqueue_head(&my_queue);

直接定義並初始化。init_waitqueue_head()函數會將自旋鎖初始化為未鎖,等待隊列初始化為空的雙向循環鏈表。
DECLARE_WAIT_QUEUE_HEAD(my_queue); 定義並初始化,可以作為定義並初始化等待隊列頭的快捷方式。
2、定義等待隊列:
DECLARE_WAITQUEUE(name,tsk);

定義並初始化一個名為name的等待隊列。
3、(從等待隊列頭中)添加/移出等待隊列:
/* add_wait_queue()函數,設置等待的進程為非互斥進程,並將其添加進等待隊列頭(q)的隊頭中*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 該函數也和add_wait_queue()函數功能基本一樣,只不過它是將等待的進程(wait)設置為互斥進程。*/
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait);

4、等待事件:
(1)wait_event()宏:
[cpp] view
plain copy





/**
* wait_event - sleep until a condition gets true
* @wq: the waitqueue to wait on
* @condition: a C expression for the event to wait for
*
* The process is put to sleep (TASK_UNINTERRUPTIBLE) until the
* @condition evaluates to true. The @condition is checked each time
* the waitqueue @wq is woken up.
*
* wake_up() has to be called after changing any variable that could
* change the result of the wait condition.
*/
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
__wait_event(wq, condition); \
} while (0)
在等待會列中睡眠直到condition為真。在等待的期間,進程會被置為TASK_UNINTERRUPTIBLE進入睡眠,直到condition變量變為真。每次進程被喚醒的時候都會檢查condition的值.
(2)wait_event_interruptible()函數:
和wait_event()的區別是調用該宏在等待的過程中當前進程會被設置為TASK_INTERRUPTIBLE狀態.在每次被喚醒的時候,首先檢查condition是否為真,如果為真則返回,否則檢查如果進程是被信號喚醒,會返回-ERESTARTSYS錯誤碼.如果是condition為真,則返回0.
(3)wait_event_timeout()宏:
也與wait_event()類似.不過如果所給的睡眠時間為負數則立即返回.如果在睡眠期間被喚醒,且condition為真則返回剩余的睡眠時間,否則繼續睡眠直到到達或超過給定的睡眠時間,然後返回0
(4)wait_event_interruptible_timeout()宏:
與wait_event_timeout()類似,不過如果在睡眠期間被信號打斷則返回ERESTARTSYS錯誤碼.
(5) wait_event_interruptible_exclusive()宏
同樣和wait_event_interruptible()一樣,不過該睡眠的進程是一個互斥進程.
5、喚醒隊列
(1)wake_up()函數
[cpp] view
plain copy





#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
喚醒等待隊列.可喚醒處於TASK_INTERRUPTIBLE和TASK_UNINTERUPTIBLE狀態的進程,和wait_event/wait_event_timeout成對使用.(2)wake_up_interruptible()函數:
#define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

和wake_up()唯一的區別是它只能喚醒TASK_INTERRUPTIBLE狀態的進程.,與wait_event_interruptible/wait_event_interruptible_timeout/ wait_event_interruptible_exclusive成對使用。
下面看一個實例:
[cpp] view
plain copy





static ssize_t hello_read(struct file *filep, char __user *buf, size_t len, loff_t *pos)
{
/*
實現應用進程read的時候,如果沒有數據就阻塞
*/
if(len>64)
{
len =64;
}
wait_event_interruptible(wq, have_data == 1);
if(copy_to_user(buf,temp,len))
{
return -EFAULT;
}
have_data = 0;
return len;
}
static ssize_t hello_write(struct file *filep, const char __user *buf, size_t len, loff_t *pos)
{
if(len > 64)
{
len = 64;
}
if(copy_from_user(temp,buf,len))
{
return -EFAULT;
}
printk("write %s\n",temp);
have_data = 1;
wake_up_interruptible(&wq);
return len;
}
注意兩個概念:
a -- 瘋狂獸群
wake_up的時候,所有阻塞在隊列的進程都會被喚醒,但是因為condition的限制,只有一個進程得到資源,其他進程又會再次休眠,如果數量很大,稱為 瘋狂獸群
b -- 獨占等待
等待隊列的入口設置一個WQ_FLAG_EXCLUSIVE標志,就會添加到等待隊列的尾部,沒有設置設置的添加到頭部,wake up的時候遇到第一個具有WQ_FLAG_EXCLUSIVE這個標志的進程就停止喚醒其他進程。
二、非阻塞I/O實現方式 —— 多路復用
1、輪詢的概念和作用
在用戶程序中,select() poll() 也是設備阻塞和非阻塞訪問息息相關的論題。使用非阻塞I/O的應用程序通常會使用select() 和 poll() 系統調用查詢是否可對設備進行無阻塞的訪問。select() 和 poll() 系統調用最終會引發設備驅動中的 poll()函數被執行。
2、應用程序中的輪詢編程
在用戶程序中,select()和poll()本質上是一樣的, 不同只是引入的方式不同,前者是在BSD UNIX中引入的,後者是在System V中引入的。用的比較廣泛的是select系統調用。原型如下
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptionfds, struct timeval *timeout);

其中readfs,writefds,exceptfds分別是select()監視的讀,寫和異常處理的文件描述符集合,numfds的值是需要檢查的號碼最高的文件描述符加1,timeout則是一個時間上限值,超過該值後,即使仍沒有描述符准備好也會返回。
struct timeval
{
    int tv_sec;    //秒
    int tv_usec;   //微秒
}

涉及到文件描述符集合的操作主要有以下幾種:
1)清除一個文件描述符集 FD_ZERO(fd_set *set);
2)將一個文件描述符加入文件描述符集中 FD_SET(int fd,fd_set *set);
3)將一個文件描述符從文件描述符集中清除 FD_CLR(int fd,fd_set *set);
4)判斷文件描述符是否被置位 FD_ISSET(int fd,fd_set *set);
最後我們利用上面的文件描述符集的相關來寫個驗證添加了設備輪詢的驅動,把上邊兩塊聯系起來
3、設備驅動中的輪詢編程
設備驅動中的poll() 函數原型如下
unsigned int(*poll)(struct file *filp, struct poll_table * wait);

第一個參數是file結構體指針,第二個參數是輪詢表指針,poll設備方法完成兩件事:
a -- 對可能引起設備文件狀態變化的等待隊列調用poll_wait()函數,將對應的等待隊列頭添加到poll_table,如果沒有文件描述符可用來執行 I/O, 則內核使進程在傳遞到該系統調用的所有文件描述符對應的等待隊列上等待。
b -- 返回表示是否能對設備進行無阻塞讀、寫訪問的掩碼。
位掩碼:POLLRDNORM, POLLIN,POLLOUT,POLLWRNORM
設備可讀,通常返回:(POLLIN | POLLRDNORM)
設備可寫,通常返回:(POLLOUT | POLLWRNORM)
poll_wait()函數:用於向 poll_table注冊等待隊列
void poll_wait(struct file *filp, wait_queue_head_t *queue,poll_table *wait)

poll_wait()函數不會引起阻塞,它所做的工作是把當前進程添加到wait 參數指定的等待列表(poll_table)中。
真正的阻塞動作是上層的select/poll函數中完成的。select/poll會在一個循環中對每個需要監聽的設備調用它們自己的poll支持函數以使得當前進程被加入各個設備的等待列表。若當前沒有任何被監聽的設備就緒,則內核進行調度(調用schedule)讓出cpu進入阻塞狀態,schedule返回時將再次循環檢測是否有操作可以進行,如此反復;否則,若有任意一個設備就緒,select/poll都立即返回。
具體過程如下:
a -- 用戶程序第一次調用select或者poll,驅動調用poll_wait並使兩條隊列都加入poll_table結構中作為下次調用驅動函數poll的條件,一個mask返回值指示設備是否可操作,0為未准備狀態,如果文件描述符未准備好可讀或可寫,用戶進程被會加入到寫或讀等待隊列中進入睡眠狀態。
b -- 當驅動執行了某些操作,例如,寫緩沖或讀緩沖,寫緩沖使讀隊列被喚醒,讀緩沖使寫隊列被喚醒,於是select或者poll系統調用在將要返回給用戶進程時再次調用驅動函數poll,驅動依然調用poll_wait 並使兩條隊列都加入poll_table結構中,並判斷可寫或可讀條件是否滿足,如果mask返回POLLIN | POLLRDNORM或POLLOUT |
POLLWRNORM則指示可讀或可寫,這時select或poll真正返回給用戶進程,如果mask還是返回0,則系統調用select或poll繼續不返回
下面是一個典型模板:
[cpp] view
plain copy





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;
}
4、調用過程:
Linux下select調用的過程:
1、用戶層應用程序調用select(),底層調用poll())
2、核心層調用sys_select() ------> do_select()

  最終調用文件描述符fd對應的struct file類型變量的struct file_operations *f_op的poll函數。
  poll指向的函數返回當前可否讀寫的信息。
  1)如果當前可讀寫,返回讀寫信息。
  2)如果當前不可讀寫,則阻塞進程,並等待驅動程序喚醒,重新調用poll函數,或超時返回。
3、驅動需要實現poll函數
當驅動發現有數據可以讀寫時,通知核心層,核心層重新調用poll指向的函數查詢信息。
poll_wait(filp,&wait_q,wait) // 此處將當前進程加入到等待隊列中,但並不阻塞

在中斷中使用wake_up_interruptible(&wait_q)喚醒等待隊列。
4、實例分析
1、memdev.h
/*mem設備描述結構體*/
struct mem_dev                                     
{                                                        
  char *data;                      
  unsigned long size; 
  wait_queue_head_t inq;  
};

#endif /* _MEMDEV_H_ */

2、驅動程序 memdev.c
[cpp] view
plain copy





#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include "memdev.h"
static mem_major = MEMDEV_MAJOR;
bool have_data = false; /*表明設備有足夠數據可供讀*/
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*設備結構體指針*/
struct cdev cdev;
/*文件打開函數*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*獲取次設備號*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*將設備描述結構指針賦值給文件私有數據指針*/
filp->private_data = dev;
return 0;
}
/*文件釋放函數*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*讀函數*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*獲得設備結構體指針*/
/*判斷讀位置是否有效*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
while (!have_data) /* 沒有數據可讀,考慮為什麼不用if,而用while */
{
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
wait_event_interruptible(dev->inq,have_data);
}
/*讀數據到用戶空間*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
have_data = false; /* 表明不再有數據可讀 */
/* 喚醒寫進程 */
return ret;
}
/*寫函數*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*獲得設備結構體指針*/
/*分析和獲取有效的寫長度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p)
count = MEMDEV_SIZE - p;
/*從用戶空間寫入數據*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
have_data = true; /* 有新的數據可讀 */
/* 喚醒讀進程 */
wake_up(&(dev->inq));
return ret;
}
/* seek文件定位函數 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */
newpos = offset;
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
unsigned int mem_poll(struct file *filp, poll_table *wait)
{
struct mem_dev *dev = filp->private_data;
unsigned int mask = 0;
/*將等待隊列添加到poll_table */
poll_wait(filp, &dev->inq, wait);
if (have_data) mask |= POLLIN | POLLRDNORM; /* readable */
return mask;
}
/*文件操作結構體*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
.poll = mem_poll,
};
/*設備驅動模塊加載函數*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 靜態申請設備號*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 動態分配設備號 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}
if (result < 0)
return result;
/*初始化cdev結構*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
/* 注冊字符設備 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 為設備描述結構分配內存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申請失敗*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*為設備分配內存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
/*初始化等待隊列*/
init_waitqueue_head(&(mem_devp[i].inq));
//init_waitqueue_head(&(mem_devp[i].outq));
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模塊卸載函數*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注銷設備*/
kfree(mem_devp); /*釋放設備結構體內存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*釋放設備號*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
3、應用程序 app-write.c
[cpp] view
plain copy





#include <stdio.h>
int main()
{
FILE *fp = NULL;
char Buf[128];
/*打開設備文件*/
fp = fopen("/dev/memdev0","r+");
if (fp == NULL)
{
printf("Open Dev memdev Error!\n");
return -1;
}
/*寫入設備*/
strcpy(Buf,"memdev is char dev!");
printf("Write BUF: %s\n",Buf);
fwrite(Buf, sizeof(Buf), 1, fp);
sleep(5);
fclose(fp);
return 0;
}
4、應用程序 app-read.c
[cpp] view
plain copy





#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>
int main()
{
int fd;
fd_set rds;
int ret;
char Buf[128];
/*初始化Buf*/
strcpy(Buf,"memdev is char dev!");
printf("BUF: %s\n",Buf);
/*打開設備文件*/
fd = open("/dev/memdev0",O_RDWR);
FD_ZERO(&rds);
FD_SET(fd, &rds);
/*清除Buf*/
strcpy(Buf,"Buf is NULL!");
printf("Read BUF1: %s\n",Buf);
ret = select(fd + 1, &rds, NULL, NULL, NULL);
if (ret < 0)
{
printf("select error!\n");
exit(1);
}
if (FD_ISSET(fd, &rds))
read(fd, Buf, sizeof(Buf));
/*檢測結果*/
printf("Read BUF2: %s\n",Buf);
close(fd);
return 0;
}
Copyright © Linux教程網 All Rights Reserved