【原創】Linux select/poll機制原理分析
阿新 • • 發佈:2020-04-03
# 前言
- `Read the fucking source code!` --By 魯迅
- `A picture is worth a thousand words.` --By 高爾基
# 1. 概述
Linux系統在訪問裝置的時候,存在以下幾種IO模型:
1. `Blocking IO Model,阻塞IO模型`;
2. `Nonblocking I/O Model,非阻塞IO模型`;
3. `I/O Multiplexing Model,IO多路複用模型`;
4. `Signal Driven I/O Model,訊號驅動IO模型`;
5. `Asynchronous I/O Model,非同步IO模型`;
今天我們來分析下IO多路複用機制,在Linux中是通過`select/poll/epoll`機制來實現的。
先看一下阻塞IO模型與非阻塞IO模型的特點:
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205415798-1207933697.png)
- 阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,會將當前任務切換出去,等到條件滿足時再切換回來。
- 缺點:阻塞IO操作,會讓處於同一個執行緒的執行邏輯都在阻塞期間無法執行,這往往意味著需要建立單獨的執行緒來互動。
- 非阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,直接返回,不會block該任務的後續操作。
- 缺點:非阻塞IO需要使用者一直輪詢操作,輪詢可能會來帶CPU的佔用問題。
對單個裝置IO操作時,問題並不嚴重,如果有多個裝置呢?比如,在伺服器中,監聽多個Client的收發處理,這時候IO多路複用就顯得尤為重要了,來張圖:
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205440145-1908091076.png)
如果這個圖,讓你有點迷惑,那就像個男人一樣,`man`一下`select/poll`函式吧:
- `select`:
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205512369-1790126405.png)
- `poll`
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205544010-1458070299.png)
簡單來說,`select/poll`能監聽多個裝置的檔案描述符,只要有任何一個裝置滿足條件,`select/poll`就會返回,否則將進行睡眠等待。
看起來,`select/poll`像是一個管家了,統一負責來監聽處理了。
已經迫不及待來看看原理了,由於底層的機制大體差不多,我將選擇`select`來做進一步分析。
# 2. 原理
## 2.1 select系統呼叫
從`select`的系統呼叫開始:
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205610896-733975945.png)
- `select`系統呼叫,最終的核心邏輯是在`do_select`函式中處理的,參考`fs/select.c`檔案;
- `do_select`函式中,有幾個關鍵的操作:
1. 初始化`poll_wqueues`結構,包括幾個關鍵函式指標的初始化,用於驅動中進行回撥處理;
2. 迴圈遍歷監測的檔案描述符,並且呼叫`f_op->poll()`函式,如果有監測條件滿足,則會跳出迴圈;
3. 在監測的檔案描述符都不滿足條件時,`poll_schedule_timeout`讓當前程序進行睡眠,超時喚醒,或者被所屬的等待佇列喚醒;
- `do_select`函式的迴圈退出條件有三個:
1. 檢測的檔案描述符滿足條件;
2. 超時;
3. 有訊號要處理;
- 在裝置驅動程式中實現的`poll()`函式,會在`do_select()`中被呼叫,而驅動中的`poll()`函式,需要呼叫`poll_wait()`函式,`poll_wait`函式本身很簡單,就是去回撥函式`p->_qproc()`,這個回撥函式正是`poll_initwait()`函式中初始化的`__pollwait()`;
所以,來看看`__pollwait()`函式嘍。
## 2.2 `__pollwait`
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205650773-1956018427.png)
- 驅動中的`poll_wait`函式回撥`__pollwait`,這個函式完成的工作是向`struct poll_wqueue`結構中新增一條`poll_table_entry`;
- `poll_table_entry`中包含了等待佇列的相關資料結構;
- 對等待佇列的相關資料結構進行初始化,包括設定等待佇列喚醒時的回撥函式指標,設定成`pollwake`;
- 將任務新增到驅動程式中的等待佇列中,最終驅動可以通過`wake_up_interruptile`等介面來喚醒處理;
這一頓操作,其實就是驅動向`select`維護的`struct poll_wqueue`中註冊,並將呼叫`select`的任務新增到驅動的等待佇列中,以便在合適的時機進行喚醒。所以,本質上來說,這是基於等待佇列的機制來實現的。
是不是還有點抽象,來看看資料結構的組織關係吧。
## 2.3 資料結構關係
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205717703-646415732.png)
- 呼叫`select`系統呼叫的程序/執行緒,會維護一個`struct poll_wqueues`結構,其中兩個關鍵欄位:
1. `pll_table`:該結構體中的函式指標`_qproc`指向`__pollwait`函式;
2. `struct poll_table_entry[]`:存放不同裝置的`poll_table_entry`,這些條目的增加是在驅動呼叫`poll_wait->__pollwait()`時進行初始化並完成新增的;
## 2.4 驅動編寫啟示
如果驅動中要支援`select`的介面呼叫,那麼需要做哪些事情呢?
如果理解了上文中的內容,你會毫不猶豫的大聲說出以下幾條:
1. 定義一個等待佇列頭`wait_queue_head_t`,用於收留等待佇列任務;
2. `struct file_operations`結構體中的`poll`函式需要實現,比如`xxx_poll()`;
3. `xxx_poll()`函式中,當然不要忘了`poll_wait`函式的呼叫了,此外,該函式的返回值`mask`需要注意是在條件滿足時對應的值,比如`EPOLLIN/EPOLL/EPOLLERR`等,這個返回值是在`do_select()`函式中會去判斷處理的;
4. 條件滿足的時候,`wake_up_interruptible`喚醒任務,當然也可以使用`wake_up`,區別是:`wake_up_interruptible`只能喚醒處於`TASK_INTERRUPTIBLE`狀態的任務,而`wake_up`能喚醒處於`TASK_INTERRUPTIBLE`和`TASK_UNINTERRUPTIBLE`狀態的任務;
## 2.5 `select/poll`的差異
- `select`與`poll`本質上基本類似,其中`select`是由`BSD UNIX`引入,`poll`由`SystemV`引入;
- `select`與`poll`需要輪詢檔案描述符集合,並在使用者態和核心態之間進行拷貝,在檔案描述符很多的情況下開銷會比較大,`select`預設支援的檔案描述符數量是1024;
- Linux提供了`epoll`機制,改進了`select`與`poll`在效率與資源上的缺點,未深入瞭解;
# 3. 示例程式碼
## 3.1 核心驅動
示例程式碼中的邏輯:
1. 驅動維護一個count值,當count值大於0時,表明條件滿足,poll返回正常的mask值;
2. poll函式每執行一次,count值就減去一次;
3. count的值可以由使用者通過`ioctl`來進行設定;
```c
#include
#include
#include
#include
#include
#include
#include
#include
#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:%d\n", __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 fail\n");
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:%d\n", 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:%d\n", 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 fail\n");
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 fail\n");
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 測試程式碼
測試程式碼邏輯:
1. 建立一個設值執行緒,用於每隔2秒來設定一次count值;
2. 主執行緒呼叫`select`函式監聽,當設值執行緒設定了count值後,select便會返回;
```c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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:%s\n", 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: %s\n", 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: %d\n", 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;
}
```
![](https://img2020.cnblogs.com/blog/1771657/202004/1771657-20200402205736416-3832081