前言
- Read the fucking source code! --By 魯迅
- A picture is worth a thousand words. --By 高爾基
1. 概述
linux系統(tǒng)在訪問設(shè)備的時候,存在以下幾種IO模型:
- Blocking IO Model,阻塞IO模型;
- Nonblocking I/O Model,非阻塞IO模型;
- I/O Multiplexing Model,IO多路復(fù)用模型;
- Signal Driven I/O Model,信號驅(qū)動IO模型;
- Asynchronous I/O Model,異步IO模型;
今天我們來分析下IO多路復(fù)用機制,在Linux中是通過select/poll/epoll機制來實現(xiàn)的。
先看一下阻塞IO模型與非阻塞IO模型的特點:
- 阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,會將當(dāng)前任務(wù)切換出去,等到條件滿足時再切換回來。
- 缺點:阻塞IO操作,會讓處于同一個線程的執(zhí)行邏輯都在阻塞期間無法執(zhí)行,這往往意味著需要創(chuàng)建單獨的線程來交互。
- 非阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,直接返回,不會block該任務(wù)的后續(xù)操作。
- 缺點:非阻塞IO需要用戶一直輪詢操作,輪詢可能會來帶CPU的占用問題。
對單個設(shè)備IO操作時,問題并不嚴(yán)重,如果有多個設(shè)備呢?比如,在服務(wù)器中,監(jiān)聽多個Client的收發(fā)處理,這時候IO多路復(fù)用就顯得尤為重要了,來張圖:
如果這個圖,讓你有點迷惑,那就像個男人一樣,man一下select/poll函數(shù)吧:
- select:
- poll
簡單來說,select/poll能監(jiān)聽多個設(shè)備的文件描述符,只要有任何一個設(shè)備滿足條件,select/poll就會返回,否則將進行睡眠等待??雌饋恚瑂elect/poll像是一個管家了,統(tǒng)一負責(zé)來監(jiān)聽處理了。
已經(jīng)迫不及待來看看原理了,由于底層的機制大體差不多,我將選擇select來做進一步分析。
2. 原理
2.1 select系統(tǒng)調(diào)用
從select的系統(tǒng)調(diào)用開始:
- select系統(tǒng)調(diào)用,最終的核心邏輯是在do_select函數(shù)中處理的,參考fs/select.c文件;
- do_select函數(shù)中,有幾個關(guān)鍵的操作:
- 初始化poll_wqueues結(jié)構(gòu),包括幾個關(guān)鍵函數(shù)指針的初始化,用于驅(qū)動中進行回調(diào)處理;
- 循環(huán)遍歷監(jiān)測的文件描述符,并且調(diào)用f_op->poll()函數(shù),如果有監(jiān)測條件滿足,則會跳出循環(huán);
- 在監(jiān)測的文件描述符都不滿足條件時,poll_schedule_timeout讓當(dāng)前進程進行睡眠,超時喚醒,或者被所屬的等待隊列喚醒;
- do_select函數(shù)的循環(huán)退出條件有三個:
- 檢測的文件描述符滿足條件;
- 超時;
- 有信號要處理;
- 在設(shè)備驅(qū)動程序中實現(xiàn)的poll()函數(shù),會在do_select()中被調(diào)用,而驅(qū)動中的poll()函數(shù),需要調(diào)用poll_wait()函數(shù),poll_wait函數(shù)本身很簡單,就是去回調(diào)函數(shù)p->_qproc(),這個回調(diào)函數(shù)正是poll_initwait()函數(shù)中初始化的__pollwait();
所以,來看看__pollwait()函數(shù)嘍。
2.2 __pollwait
- 驅(qū)動中的poll_wait函數(shù)回調(diào)__pollwait,這個函數(shù)完成的工作是向struct poll_wqueue結(jié)構(gòu)中添加一條poll_table_entry;
- poll_table_entry中包含了等待隊列的相關(guān)數(shù)據(jù)結(jié)構(gòu);
- 對等待隊列的相關(guān)數(shù)據(jù)結(jié)構(gòu)進行初始化,包括設(shè)置等待隊列喚醒時的回調(diào)函數(shù)指針,設(shè)置成pollwake;
- 將任務(wù)添加到驅(qū)動程序中的等待隊列中,最終驅(qū)動可以通過wake_up_interruptile等接口來喚醒處理;
這一頓操作,其實就是驅(qū)動向select維護的struct poll_wqueue中注冊,并將調(diào)用select的任務(wù)添加到驅(qū)動的等待隊列中,以便在合適的時機進行喚醒。所以,本質(zhì)上來說,這是基于等待隊列的機制來實現(xiàn)的。
是不是還有點抽象,來看看數(shù)據(jù)結(jié)構(gòu)的組織關(guān)系吧。
2.3 數(shù)據(jù)結(jié)構(gòu)關(guān)系
- 調(diào)用select系統(tǒng)調(diào)用的進程/線程,會維護一個struct poll_wqueues結(jié)構(gòu),其中兩個關(guān)鍵字段:
- pll_table:該結(jié)構(gòu)體中的函數(shù)指針_qproc指向__pollwait函數(shù);
- struct poll_table_entry[]:存放不同設(shè)備的poll_table_entry,這些條目的增加是在驅(qū)動調(diào)用poll_wait->__pollwait()時進行初始化并完成添加的;
2.4 驅(qū)動編寫啟示
如果驅(qū)動中要支持select的接口調(diào)用,那么需要做哪些事情呢?如果理解了上文中的內(nèi)容,你會毫不猶豫的大聲說出以下幾條:
- 定義一個等待隊列頭wait_queue_head_t,用于收留等待隊列任務(wù);
- struct file_operations結(jié)構(gòu)體中的poll函數(shù)需要實現(xiàn),比如xxx_poll();
- xxx_poll()函數(shù)中,當(dāng)然不要忘了poll_wait函數(shù)的調(diào)用了,此外,該函數(shù)的返回值mask需要注意是在條件滿足時對應(yīng)的值,比如EPOLLIN/EPOLL/EPOLLERR等,這個返回值是在do_select()函數(shù)中會去判斷處理的;
- 條件滿足的時候,wake_up_interruptible喚醒任務(wù),當(dāng)然也可以使用wake_up,區(qū)別是:wake_up_interruptible只能喚醒處于TASK_INTERRUPTIBLE狀態(tài)的任務(wù),而wake_up能喚醒處于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE狀態(tài)的任務(wù);
2.5 select/poll的差異
- select與poll本質(zhì)上基本類似,其中select是由BSD UNIX引入,poll由SystemV引入;
- select與poll需要輪詢文件描述符集合,并在用戶態(tài)和內(nèi)核態(tài)之間進行拷貝,在文件描述符很多的情況下開銷會比較大,select默認支持的文件描述符數(shù)量是1024;
- Linux提供了epoll機制,改進了select與poll在效率與資源上的缺點,未深入了解;
3. 示例代碼
3.1 內(nèi)核驅(qū)動
示例代碼中的邏輯:
- 驅(qū)動維護一個count值,當(dāng)count值大于0時,表明條件滿足,poll返回正常的mask值;
- poll函數(shù)每執(zhí)行一次,count值就減去一次;
- count的值可以由用戶通過ioctl來進行設(shè)置;
#include <linux/init.h>
#include <linux/module.h>#include <linux/poll.h>#include <linux/wait.h>#include <linux/cdev.h>#include <linux/mutex.h>#include <linux/slab.h>#include <asm/ioctl.h>#define POLL_DEV_NAME "poll"
#define POLL_MAGIC 'P'
#define POLL_SET_COUNT (_IOW(POLL_MAGIC, 0, unsigned int))
struct poll_dev {
struct cdev cdev;
struct class *class;
struct device *device;
wait_queue_head_t wq_head; struct mutex poll_mutex;
unsigned int count; dev_t devno;};struct poll_dev *g_poll_dev = NULL;
static int poll_open(struct inode *inode, struct file *filp)
{ filp->private_data = g_poll_dev; return 0;
}static int poll_close(struct inode *inode, struct file *filp)
{ return 0;
}static unsigned int poll_poll(struct file *filp, struct poll_table_struct *wait)
{ unsigned int mask = 0;
struct poll_dev *dev = filp->private_data;
mutex_lock(&dev->poll_mutex); poll_wait(filp, &dev->wq_head, wait); if (dev->count > 0) {
mask |= POLLIN | POLLRDNORM; /* decrease each time */
dev->count--; } mutex_unlock(&dev->poll_mutex); return mask;
}static long poll_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg){ struct poll_dev *dev = filp->private_data;
unsigned int cnt; switch (cmd) { case POLL_SET_COUNT: mutex_lock(&dev->poll_mutex); if (copy_from_user(&cnt, (void __user *)arg, _IOC_SIZE(cmd))) {
pr_err("copy_from_user fail:%dn", __LINE__);
return -EFAULT;
} if (dev->count == 0) {
wake_up_interruptible(&dev->wq_head); } /* update count */
dev->count += cnt; mutex_unlock(&dev->poll_mutex); break;
default: return -EINVAL;
} return 0;
}static struct file_operations poll_fops = {
.owner = THIS_MODULE, .open = poll_open, .release = poll_close, .poll = poll_poll, .unlocked_ioctl = poll_ioctl, .compat_ioctl = poll_ioctl,};static int __init poll_init(void)
{ int ret; if (g_poll_dev == NULL) {
g_poll_dev = (struct poll_dev *)kzalloc(sizeof(struct poll_dev), GFP_KERNEL);
if (g_poll_dev == NULL) {
pr_err("struct poll_dev allocate failn");
return -1;
} } /* allocate device number */
ret = alloc_chrdev_region(&g_poll_dev->devno, 0, 1, POLL_DEV_NAME);
if (ret < 0) {
pr_err("alloc_chrdev_region fail:%dn", ret);
goto alloc_chrdev_err; } /* set char-device */
cdev_init(&g_poll_dev->cdev, &poll_fops); g_poll_dev->cdev.owner = THIS_MODULE; ret = cdev_add(&g_poll_dev->cdev, g_poll_dev->devno, 1);
if (ret < 0) {
pr_err("cdev_add fail:%dn", ret);
goto cdev_add_err; } /* create device */
g_poll_dev->class = class_create(THIS_MODULE, POLL_DEV_NAME); if (IS_ERR(g_poll_dev->class)) {
pr_err("class_create failn");
goto class_create_err; } g_poll_dev->device = device_create(g_poll_dev->class, NULL, g_poll_dev->devno, NULL, POLL_DEV_NAME); if (IS_ERR(g_poll_dev->device)) {
pr_err("device_create failn");
goto device_create_err; } mutex_init(&g_poll_dev->poll_mutex); init_waitqueue_head(&g_poll_dev->wq_head); return 0;
device_create_err: class_destroy(g_poll_dev->class);class_create_err: cdev_del(&g_poll_dev->cdev);cdev_add_err: unregister_chrdev_region(g_poll_dev->devno, 1);
alloc_chrdev_err: kfree(g_poll_dev); g_poll_dev = NULL; return -1;
}static void __exit poll_exit(void)
{ cdev_del(&g_poll_dev->cdev); device_destroy(g_poll_dev->class, g_poll_dev->devno); unregister_chrdev_region(g_poll_dev->devno, 1);
class_destroy(g_poll_dev->class); kfree(g_poll_dev); g_poll_dev = NULL;}module_init(poll_init);module_exit(poll_exit);MODULE_DESCRIPTION("select/poll test");
MODULE_AUTHOR("LoyenWang");
MODULE_LICENSE("GPL");
3.2 測試代碼
測試代碼邏輯:
- 創(chuàng)建一個設(shè)值線程,用于每隔2秒來設(shè)置一次count值;
- 主線程調(diào)用select函數(shù)監(jiān)聽,當(dāng)設(shè)值線程設(shè)置了count值后,select便會返回;
#include <stdio.h>
#include <string.h>
#include <fcntl.h>#include <pthread.h>#include <errno.h>#include <unistd.h>#include <sys/ioctl.h>#include <sys/stat.h>#include <sys/types.h>#include <sys/time.h>static void *set_count_thread(void *arg){ int fd = *(int *)arg;
unsigned int count_value = 1;
int loop_cnt = 20;
int ret;
while (loop_cnt--) { ret = ioctl(fd, NOTIFY_SET_COUNT, &count_value); if (ret < 0) {
printf("ioctl set count value fail:%sn", strerror(errno));
return NULL;
} sleep(1);
} return NULL;
}int main(void)
{ int fd;
int ret;
pthread_t setcnt_tid; int loop_cnt = 20;
/* for select use */
fd_set rfds; struct timeval tv;
fd = open("/dev/poll", O_RDWR);
if (fd < 0) {
printf("/dev/poll open failed: %sn", strerror(errno));
return -1;
} /* wait up to five seconds */
tv.tv_sec = 5;
tv.tv_usec = 0;
ret = pthread_create(&setcnt_tid, NULL, set_count_thread, &fd); if (ret < 0) {
printf("set_count_thread create fail: %dn", ret);
return -1;
} while (loop_cnt--) { FD_ZERO(&rfds); FD_SET(fd, &rfds); ret = select(fd + 1, &rfds, NULL, NULL, &tv);
//ret = select(fd + 1, &rfds, NULL, NULL, NULL);
if (ret == -1) {
perror("select()");
break;
}
else if (ret)
printf("Data is available now.n");
else {
printf("No data within five seconds.n");
}
}
ret = pthread_join(setcnt_tid, NULL);
if (ret < 0) {
printf("set_count_thread join fail.n");
return -1;
}
close(fd);
return 0;
}