IO多路複用底層原理分析

前言

最近一直忙着找實習以及小論文的實驗,導致最近半個月都沒有汲取到新的知識,也就這兩天空閒的時候才能繼續看看Netty。其實網上關於Netty的文章很多,但是能夠從底層原理去解釋的卻不多,我們都知道Netty底層是通過IO多路複用來實現的,那麼你們有沒有考慮過在底層上IO多路複用又是是如何實現的?

這篇就從底層的select函數以及文件驅動poll函數來分享一下我的認識,若如有誤的地方,大家可以給個issue。

IO多路複用

首先來了解一下什麼是IO多路複用呢。可以拆開來理解,IO多路可以簡單的理解爲多個I/O流,複用即多個I/O流共用了一個線程,換一種說法單個線程來管理跟蹤多個I/O流。

多路複用有了簡單的瞭解後,現在問一下自己如果讓你自己來實現一個IO多路複用你會怎麼實現?那簡單,寫一個死循環一直去遍歷流,如果流有讀、寫、異常的事件就返回該流:

while true {
        for(i in stream[]) {
            if(i has data)
                read until unavailable
        }
}

這種做法的缺陷也非常的明顯,如果沒有準備就緒的流,那麼會浪費CPU時間。那麼有沒有一種方法當所有的流沒有準備就緒時,讓線程阻塞起來,直到有就緒的流出現,讓線程再執行返回呢?下面就來看看select函數是如何做的。

select函數

正如前面說的一樣,爲了不浪費CPU的時間,可以採用select來實現IO多路複用。

當所有的流都沒有準備就緒時,會把當前線程阻塞掉;當有一個或多個流的I/O事件就緒時,就從阻塞狀態中醒來,然後輪詢一遍所有的流,處理已經準備好的I/O事件。輪詢的過程可以參照如下:

while true {
        select(streams[])//會被阻塞
        for(i in stream[]) {
            if(i has data)
                read until unavailable
        }
}

那麼有沒有人有疑惑,select居然這麼強大,它的底層原理是如何實現的呢?這也是我寫這篇博客的主要原因,下面就來分析一下select函數究竟做了什麼。

爲了能解釋清楚,先來講一下什麼是文件描述符(FD):

文件描述符是內核爲了高效管理已經被打開的文件所創建的索引,他是一個從0開始的整數,程序所有執行的I/O操作都是通過文件描述符進行的。其中,在程序剛剛啓動時,0,1,2三個文件描述符已經被佔用了,0代表標準輸入設備stdin(比如鍵盤),1代表標準輸出設備stdout(顯示器),2代表標準錯誤stderr。因此再打開一個文件,它的文件描述符會是3。

注意這裏的文件並不是平時看到的txt,在unix中所有東西都是文件。文件就是一串二進制流,不管socket,還是FIFO、管道、終端,對我們來說,一切都是文件,一切都是流。

清楚了什麼是FD之後,再來看看select函數:

int select(int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

中間的三個參數readset、writeset和exceptset指定我們要讓內核監測讀、寫和異常條件的文件描述字集合。如果對某一個的條件不感興趣,就可以把它設爲空指針。struct fd_set可以理解爲一個集合,這個集合中存放的是文件描述符。

timeout告知內核等待所指定文件描述字中的任何一個就緒可花多少時間。

select的實現依賴於文件的驅動函數poll,在unix中無論是調用 select、poll 還是epoll,最終都會調用該函數

我們想在用戶空間對一個文件使用多路I/O複用,那麼我們需要實現該文件驅動的poll函數。對於一般的磁盤文件而言,這些函數都已在文件系統驅動中實現,而對於自己定義的設備,我們需要自己實現poll函數。

文件驅動poll函數

select會循環遍歷它所監測的fd_set內的所有文件描述符對應的驅動程序的poll函數。

驅動程序提供的poll函數首先會將調用select的用戶進程插入到該設備驅動對應資源的等待隊列(如讀/寫等待隊列),然後返回一個bitmask告訴select當前資源哪些可用。當select循環遍歷完所有fd_set內指定的文件描述符對應的poll函數後,如果沒有一個資源可用(即沒有一個文件可供操作),則select讓該進程睡眠,一直等到有資源可用爲止,進程被喚醒(或者timeout)繼續往下執行。

看到這裏相信大家已經明白select是如何做到當沒有IO流時被阻塞,簡單來說就是:

每一個文件描述符有對應的文件驅動poll函數,select在實際調用過程中會調用(readset、writeset和exceptset)每一個文件描述符的文件驅動函數poll

文件驅動函數poll會將調用select的進程放在設備對應資源的等待隊列中。當有描述符可進行非阻塞I/O操作時,內核喚醒該描述符poll等待隊列中的阻塞進程;進程喚醒後繼續執行I/O複用函數,I/O複用函數將進程從描述符表中所有描述符的poll等待隊列中移除;然後重新遍歷每一個文件驅動函數poll

參考文章

http://blog.chinaunix.net/uid-20643761-id-1594860.html

https://blog.csdn.net/genzld/article/details/84995021

https://www.cnblogs.com/ck1020/p/7263552.html

https://blog.csdn.net/u014590757/article/details/80106135

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章