Linux selectpoll機制原理分析

前言

Read the fucking source code!

——By 魯迅

A picture is worth a thousand words。

——By 高爾基

1。 概述

Linux系統在訪問裝置的時候,存在以下幾種IO模型:

Blocking IO Model,阻塞IO模型

Nonblocking I/O Model,非阻塞IO模型

I/O Multiplexing Model,IO多路複用模型

Signal Driven I/O Model,訊號驅動IO模型

Asynchronous I/O Model,非同步IO模型

今天我們來分析下IO多路複用機制,在Linux中是透過

select/poll/epoll

機制來實現的。

先看一下阻塞IO模型與非阻塞IO模型的特點:

Linux select/poll機制原理分析

阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,會將當前任務切換出去,等到條件滿足時再切換回來。

缺點:阻塞IO操作,會讓處於同一個執行緒的執行邏輯都在阻塞期間無法執行,這往往意味著需要建立單獨的執行緒來互動。

非阻塞IO模型:在IO訪問的時候,如果條件沒有滿足,直接返回,不會block該任務的後續操作。

缺點:非阻塞IO需要使用者一直輪詢操作,輪詢可能會來帶CPU的佔用問題。

對單個裝置IO操作時,問題並不嚴重,如果有多個裝置呢?比如,在伺服器中,監聽多個Client的收發處理,這時候IO多路複用就顯得尤為重要了,來張圖:

Linux select/poll機制原理分析

如果這個圖,讓你有點迷惑,那就像個男人一樣,

man

一下

select/poll

函式吧:

select

Linux select/poll機制原理分析

poll

Linux select/poll機制原理分析

簡單來說,

select/poll

能監聽多個裝置的檔案描述符,只要有任何一個裝置滿足條件,

select/poll

就會返回,否則將進行睡眠等待。看起來,

select/poll

像是一個管家了,統一負責來監聽處理了。

已經迫不及待來看看原理了,由於底層的機制大體差不多,我將選擇

select

來做進一步分析。

2. 原理

2.1 select系統呼叫

select

的系統呼叫開始:

Linux select/poll機制原理分析

select

系統呼叫,最終的核心邏輯是在

do_select

函式中處理的,參考

fs/select。c

檔案;

do_select

函式中,有幾個關鍵的操作:

初始化

poll_wqueues

結構,包括幾個關鍵函式指標的初始化,用於驅動中進行回撥處理;

迴圈遍歷監測的檔案描述符,並且呼叫

f_op->poll()

函式,如果有監測條件滿足,則會跳出迴圈;

在監測的檔案描述符都不滿足條件時,

poll_schedule_timeout

讓當前程序進行睡眠,超時喚醒,或者被所屬的等待佇列喚醒;

do_select

函式的迴圈退出條件有三個:

檢測的檔案描述符滿足條件;

超時;

有訊號要處理;

在裝置驅動程式中實現的

poll()

函式,會在

do_select()

中被呼叫,而驅動中的

poll()

函式,需要呼叫

poll_wait()

函式,

poll_wait

函式本身很簡單,就是去回撥函式

p->_qproc()

,這個回撥函式正是

poll_initwait()

函式中初始化的

__pollwait()

所以,來看看

__pollwait()

函式嘍。

C/C++Linux伺服器開發/後臺架構師【零聲教育】-學習影片教程-騰訊課堂

【文章福利】:小編整理了一些個人覺得比較好的學習書籍、影片資料共享在群檔案裡面,有需要的可以自行新增哦!~點選加入(需要自取)

Linux select/poll機制原理分析

2。2__pollwait

Linux select/poll機制原理分析

驅動中的

poll_wait

函式回撥

__pollwait

,這個函式完成的工作是向

struct poll_wqueue

結構中新增一條

poll_table_entry

poll_table_entry

中包含了等待佇列的相關資料結構;

對等待佇列的相關資料結構進行初始化,包括設定等待佇列喚醒時的回撥函式指標,設定成

pollwake

將任務新增到驅動程式中的等待佇列中,最終驅動可以透過

wake_up_interruptile

等介面來喚醒處理;

這一頓操作,其實就是驅動向

select

維護的

struct poll_wqueue

中註冊,並將呼叫

select

的任務新增到驅動的等待佇列中,以便在合適的時機進行喚醒。所以,本質上來說,這是基於等待佇列的機制來實現的。

是不是還有點抽象,來看看資料結構的組織關係吧。

2.3 資料結構關係

Linux select/poll機制原理分析

呼叫

select

系統呼叫的程序/執行緒,會維護一個

struct poll_wqueues

結構,其中兩個關鍵欄位:

pll_table

:該結構體中的函式指標

_qproc

指向

__pollwait

函式;

struct poll_table_entry[]

:存放不同裝置的

poll_table_entry

,這些條目的增加是在驅動呼叫

poll_wait->__pollwait()

時進行初始化並完成新增的;

2。4 驅動編寫啟示

如果驅動中要支援

select

的介面呼叫,那麼需要做哪些事情呢?如果理解了上文中的內容,你會毫不猶豫的大聲說出以下幾條:

定義一個等待佇列頭

wait_queue_head_t

,用於收留等待佇列任務;

struct file_operations

結構體中的

poll

函式需要實現,比如

xxx_poll()

xxx_poll()

函式中,當然不要忘了

poll_wait

函式的呼叫了,此外,該函式的返回值

mask

需要注意是在條件滿足時對應的值,比如

EPOLLIN/EPOLL/EPOLLERR

等,這個返回值是在

do_select()

函式中會去判斷處理的;

條件滿足的時候,

wake_up_interruptible

喚醒任務,當然也可以使用

wake_up

,區別是:

wake_up_interruptible

只能喚醒處於

TASK_INTERRUPTIBLE

狀態的任務,而

wake_up

能喚醒處於

TASK_INTERRUPTIBLE

TASK_UNINTERRUPTIBLE

狀態的任務;

2。5select/poll的差異

select

poll

本質上基本類似,其中

select

是由

BSD UNIX

引入,

poll

SystemV

引入;

select

poll

需要輪詢檔案描述符集合,並在使用者態和核心態之間進行複製,在檔案描述符很多的情況下開銷會比較大,

select

預設支援的檔案描述符數量是1024;

Linux提供了

epoll

機制,改進了

select

poll

在效率與資源上的缺點,未深入瞭解;

3。 示例程式碼

3。1 核心驅動

示例程式碼中的邏輯:

驅動維護一個count值,當count值大於0時,表明條件滿足,poll返回正常的mask值;

poll函式每執行一次,count值就減去一次;

count的值可以由使用者透過

ioctl

來進行設定;

#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 測試程式碼

測試程式碼邏輯:

建立一個設值執行緒,用於每隔2秒來設定一次count值;

主執行緒呼叫

select

函式監聽,當設值執行緒設定了count值後,select便會返回;

#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;}