導航:首頁 > 源碼編譯 > epoll源碼使用

epoll源碼使用

發布時間:2023-01-07 11:34:50

『壹』 linux select/poll/epoll 原理(一)實現基礎

本序列涉及的 Linux 源碼都是基於 linux-4.14.143 。

1.1 文件抽象

在 Linux 內核里,文件是一個抽象,設備是個文件,網路套接字也是個文件。

文件抽象必須支持的能力定義在 file_operations 結構體里。

在 Linux 里,一個打開的文件對應一個文件描述符 file descriptor/FD,FD 其實是一個整數,內核把進程打開的文件維護在一個數組里,FD 對應的是數組的下標。

文件抽象的能力定義:

1.2 文件 poll 操作

poll 函數的原型:

文件抽象 poll 函數的具體實現必須完成兩件事(這兩點算是規范了):

1. 在 poll 函數敢興趣的等待隊列上調用 poll_wait 函數,以接收到喚醒;具體的實現必須把 poll_table 類型的參數作為透明對象來使用,不需要知道它的具體結構。

2. 返回比特掩碼,表示當前可立即執行而不會阻塞的操作。

下面是某個驅動的 poll 實現示例,來自:https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch05s03.html:

poll 函數接收的 poll_table 只有一個隊列處理函數 _qproc 和感興趣的事件屬性 _key。

文件抽象的具體實現在構建時會初始化一個或多個 wait_queue_head_t 類型的事件等待隊列 。

poll 等待的過程:

事件發生時的喚醒過程:

一個小困惑:

『貳』 關於Linux下的select/epoll

select這個系統調用的原型如下

第一個參數nfds用來告訴內核 要掃描的socket fd的數量+1 ,select系統調用最大接收的數量是1024,但是如果每次都去掃描1024,實際上的數量並不多,則效率太低,這里可以指定需要掃描的數量。 最大數量為1024,如果需要修改這個數量,則需要重新編譯Linux內核源碼。
第2、3、4個參數分別是readfds、writefds、exceptfds,傳遞的參數應該是fd_set 類型的引用,內核會檢測每個socket的fd, 如果沒有讀事件,就將對應的fd從第二個參數傳入的fd_set中移除,如果沒有寫事件,就將對應的fd從第二個參數的fd_set中移除,如果沒有異常事件,就將對應的fd從第三個參數的fd_set中移除 。這里我們應該 要將實際的readfds、writefds、exceptfds拷貝一份副本傳進去,而不是傳入原引用,因為如果傳遞的是原引用,某些socket可能就已經丟失
最後一個參數是等待時間, 傳入0表示非阻塞,傳入>0表示等待一定時間,傳入NULL表示阻塞,直到等到某個socket就緒

FD_ZERO()這個函數將fd_set中的所有bit清0,一般用來進行初始化等。
FD_CLR()這個函數用來將bitmap(fd_set )中的某個bit清0,在客戶端異常退出時就會用到這個函數,將fd從fd_set中刪除。
FD_ISSET()用來判斷某個bit是否被置1了,也就是判斷某個fd是否在fd_set中。
FD_SET()這個函數用來將某個fd加入fd_set中,當客戶端新加入連接時就會使用到這個函數。

epoll_create系統調用用來創建epfd,會在開辟一塊內存空間(epoll的結構空間)。size為epoll上能關注的最大描述符數,不夠會進行擴展,size只要>0就行,早期的設計size是固定大小,但是現在size參數沒什麼用,會自動擴展。
返回值是epfd,如果為-1則說明創建epoll對象失敗

第一個參數epfd傳入的就是epoll_create返回的epfd。
第二個參數傳入對應操作的宏,包括 增刪改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD)
第三個參數傳入的是 需要增刪改的socket的fd
第四個參數傳入的是 需要操作的fd的哪些事件 ,具體的事件可以看後續。
返回值是一個int類型,如果為-1則說明操作失敗

第一個參數是epfd,也就是epoll_create的返回值。
第二個參數是一個epoll_event類型的指針,也就是傳入的是一個數組指針。 內核會將就緒的socket的事件拷貝到這個數組中,用戶可以根據這個數組拿到事件和消息等
第三個參數是maxevents,傳入的是 第二個參數的數組的容量
第四個參數是timeout, 如果設為-1一直阻塞直到有就緒數據為止,如果設為0立即返回,如果>0那麼阻塞一段時間
返回值是一個int類型,也就是就緒的socket的事件的數量(內核拷貝給用戶的events的元素的數量),通過這個數量可以進行遍歷處理每個事件

一般需要傳入 ev.data.fd 和 ev.events ,也就是fd和需要監控的fd的事件。事件如果需要傳入多個,可以通過按位與來連接,比如需要監控讀寫事件,只需要像如下這樣操作即可: ev.events=EPOLLIN | EPOLLOUT 。

LT(水平觸發), 默認 的工作模式, 事件就緒後用戶可以選擇處理和不處理,如果用戶不處理,內核會對這部分數據進行維護,那麼下次調用epoll_wait()時仍舊會打包出來
ET(邊緣觸發),事件就緒之後, 用戶必須進行處理 ,因為內核把事件打包出來之後就把對應的就緒事件給清掉了, 如果不處理那麼就緒事件就沒了 。ET可以減少epoll事件被重復觸發的次數,效率比LT高。
如果需要設置為邊緣觸發只需要設置事件為類似 ev.events=EPOLLIN | EPOLLET 即可

select/poll/epoll是nio多路復用技術, 傳統的bio無法實現C10K/C100K ,也就是無法滿足1w/10w的並發量,在這么高的並發量下,在進行上下文切換就很容易將伺服器的負載拉飛。

1.將fd_set從用戶態拷貝到內核態
2.根據fd_set掃描內存中的socket的fd的狀態,時間復雜度為O(n)
3.檢查fd_set,如果有已經就緒的socket,就給對應的socket的fd打標記,那麼就return 就緒socket的數量並喚醒當前線程,如果沒有就緒的socket就繼續阻塞當前線程直到有socket就緒才將當前線程喚醒。
4.如果想要獲取當前已經就緒的socket列表,則還需要進行一次系統調用,使用O(n)的時間去掃描socket的fd列表,將已經打上標記的socket的fd返回。

CPU在同一個時刻只能執行一個程序,通過RR時間片輪轉去切換執行各個程序。沒有被掛起的進程(線程)則在工作隊列中排隊等待CPU的執行,將進程(線程)從工作隊列中移除就是掛起,反映到java層面的就是線程的阻塞。

什麼是中斷?當我們使用鍵盤、滑鼠等IO設備的時候,會給主板一個電流信號,這個電流信號就給CPU一個中斷信號,CPU執行完當前的指令便會保存現場,然後執行鍵盤/滑鼠等設備的中斷程序,讓中斷程序獲取CPU的使用權,在中斷程序後又將現場恢復,繼續執行之前的進程。

如果第一次沒檢測到就緒的socket,就要將其進程(線程)從工作隊列中移除,並加入到socket的等待隊列中。

socket包含讀緩沖區+寫緩沖區+等待隊列(放線程或eventpoll對象)

當從客戶端往伺服器端發送數據時,使用TCP/IP協議將通過物理鏈路、網線發給伺服器的網卡設備,網卡的DMA設備將接收到的的數據寫入到內存中的一塊區域(網卡緩沖區),然後會給CPU發出一個中斷信號,CPU執行完當前指令則會保存現場,然後網卡的中斷程序就獲得了CPU的使用權,然後CPU便開始執行網卡的中斷程序,將內存中的緩存區中的數據包拿出,判斷埠號便可以判斷它是哪個socket的數據,將數據包寫入對應的socket的讀(輸入)緩沖區,去檢查對應的socket的等待隊列有沒有等待著的進程(線程),如果有就將該線程(進程)從socket的等待隊列中移除,將其加入工作隊列,這時候該進程(線程)就再次擁有了CPU的使用許可權,到這里中斷程序就結束了。

之後這個進程(線程)就執行select函數再次去檢查fd_set就能發現有socket緩沖區中有數據了,就將該socket的fd打標記,這個時候select函數就執行完了,這時候就會給上層返回一個int類型的數值,表示已經就緒的socket的數量或者是發生了錯誤。這個時候就再進行內核態到用戶態的切換,對已經打標記的socket的fd進行處理。

將原本1024bit長度的bitmap(fd_set)換成了數組的方式傳入 ,可以 解決原本1024個不夠用的情況 ,因為傳入的是數組,長度可以不止是1024了,因此socket數量可以更多,在Kernel底層會將數組轉換成鏈表。

在十多年前,linux2.6之前,不支持epoll,當時可能會選擇用Windows/Unix用作伺服器,而不會去選擇Linux,因為select/poll會隨著並發量的上升,性能變得越來越低,每次都得檢查所有的Socket列表。

1.select/poll每次調用都必須根據提供所有的socket集合,然後就 會涉及到將這個集合從用戶空間拷貝到內核空間,在這個過程中很耗費性能 。但是 其實每次的socket集合的變化也許並不大,也許就1-2個socket ,但是它會全部進行拷貝,全部進行遍歷一一判斷是否就緒。

2.select/poll的返回類型是int,只能代表當前的就緒的socket的數量/發生了錯誤, 如果還需要知道是哪些socket就緒了,則還需要再次使用系統調用去檢查哪些socket是就緒的,又是一次O(n)的操作,很耗費性能

1.epoll在Kernel內核中存儲了對應的數據結構(eventpoll)。我們可以 使用epoll_create()這個系統調用去創建一個eventpoll對象 ,並返回eventpoll的對象id(epfd),eventpoll對象主要包括三個部分:需要處理的正在監聽的socket_fd列表(紅黑樹結構)、socket就緒列表以及等待隊列(線程)。

2.我們可以使用epoll_ctl()這個系統調用對socket_fd列表進行CRUD操作,因為可能頻繁地進行CRUD,因此 socket_fd使用的是紅黑樹的結構 ,讓其效率能更高。epoll_ctl()傳遞的參數主要是epfd(eventpoll對象id)。

3.epoll_wait()這個系統調用默認會 將當前進程(線程)阻塞,加入到eventpoll對象的等待隊列中,直到socket就緒列表中有socket,才會將該進程(線程)重新加入工作隊列 ,並返回就緒隊列中的socket的數量。

socket包含讀緩沖區、寫緩沖區和等待隊列。當使用epoll_ctl()系統調用將socket新加入socket_fd列表時,就會將eventpoll對象引用加到socket的等待隊列中, 當網卡的中斷程序發現socket的等待隊列中不是一個進程(線程),而是一個eventpoll對象的引用,就將socket引用追加到eventpoll對象的就緒列表的尾部 。而eventpoll對象中的等待隊列存放的就是調用了epoll_wait()的進程(線程),網卡的中斷程序執行會將等待隊列中的進程(線程)重新加入工作隊列,讓其擁有佔用CPU執行的資格。epoll_wait()的返回值是int類型,返回的是就緒的socket的數量/發生錯誤,-1表示發生錯誤。

epoll的參數有傳入一個epoll_event的數組指針(作為輸出參數),在調用epoll_wait()返回的同時,Kernel內核還會將就緒的socket列表添加到epoll_event類型的數組當中。

『叄』 Linux下各種鎖的理解和使用及總結解決epoll驚群問題(面試常考)-

鎖出現的原因

臨界資源是什麼: 多線程執行流所共享的資源

鎖的作用是什麼, 可以做原子操作, 在多線程中針對臨界資源的互斥訪問... 保證一個時刻只有一個線程可以持有鎖對於臨界資源做修改操作...

任何一個線程如果需要修改,向臨界資源做寫入操作都必須持有鎖,沒有持有鎖就不能對於臨界資源做寫入操作.

鎖 : 保證同一時刻只能有一個線程對於臨界資源做寫入操作 (鎖地功能)

再一個直觀地代碼引出問題,再從指令集的角度去看問題

上述一個及其奇怪的結果,這個結果每一次運行都可能是不一樣的,Why ? 按照我們本來的想法是每一個線程 + 20000000 結果肯定應該是60000000呀,可以就是達不到這個值

為何? (深入匯編指令來看) 一定將過程放置到匯編指令上去看就可以理解這個過程了.

a++; 或者 a += 1; 這些操作的匯編操作是幾個步驟?

其實是三個步驟:

正常情況下,數據少,操作的線程少,問題倒是不大,想一想要是這樣的情況下,操作次數大,對齊操作的線程多,有些線程從中間切入進來了,在運算之後還沒寫回內存就另外一個線程切入進來同時對於之前的數據進行++ 再寫回內存, 啥效果,多次++ 操作之後結果確實一次加加操作後的結果。 這樣的操作 (術語叫做函數的重入) 我覺得其實就是重入到了匯編指令中間了,還沒將上一次運算的結果寫回內存就重新對這個內存讀取再運算寫入,結果肯定和正常的邏輯後的結果不一樣呀

來一幅圖片解釋一下

咋辦? 其實問題很清楚,我們只需要處理的是多條匯編指令不能讓它中間被插入其他的線程運算. (要想自己在執行匯編指令的時候別人不插入進來) 將多條匯編指令綁定成為一條指令不就OK了嘛。

也就是原子操作!!!

不會原子操作?操作系統給咱提供了線程的 綁定方式工具呀:mutex 互斥鎖(互斥量), 自旋鎖(spinlock), 讀寫鎖(readers-writer lock) 他們也稱作悲觀鎖. 作用都是一個樣,將多個匯編指令鎖成為一條原子操作 (此處的匯編指令也相當於如下的臨界資源)

悲觀鎖:鎖如其名,每次都悲觀地認為其他線程也會來修改數據,進行寫入操作,所以會在取數據前先加鎖保護,當其他線程想要訪問數據時,被阻塞掛起

樂觀鎖:每次取數據的時候,總是樂觀地認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前, 會判斷其他數據在更新前有沒有對數據進行修改。

互斥鎖

最為常見使用地鎖就是互斥鎖, 也稱互斥量. mutex

特徵,當其他線程持有互斥鎖對臨界資源做寫入操作地時候,當前線程只能掛起等待,讓出CPU,存在線程間切換工作

解釋一下存在線程間切換工作 : 當線程試圖去獲取鎖對臨界資源做寫入操作時候,如果鎖被別的線程正在持有,該線程會保存上下文直接掛起,讓出CPU,等到鎖被釋放出來再進行線程間切換,從新持有CPU執行寫入操作

互斥鎖需要進行線程間切換,相比自旋鎖而言性能會差上許多,因為自旋鎖不會讓出CPU, 也就不需要進行線程間切換的步驟,具體原理下一點詳述

加互斥量(互斥鎖)確實可以達到要求,但是會發現運行時間非常的長,因為線程間不斷地切換也需要時間, 線程間切換的代價比較大.

相關視頻推薦

你繞不開的組件—鎖,4個方面手撕鎖的多種實現

「驚群」原理、鎖的設計方案及繞不開的「死鎖」問題

學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂

需要C/C++ Linux伺服器架構師學習資料加qun812855908獲取(資料包括 C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg 等),免費分享

自旋鎖

spinlock.自旋鎖.

對比互斥量(互斥鎖)而言,獲取自旋鎖不需要進行線程間切換,如果自旋鎖正在被別的線程佔用,該線程也不會放棄CPU進行掛起休眠,而是恰如其名的在哪裡不斷地循環地查看自旋鎖保持者(持有者)是否將自旋鎖資源釋放出來... (自旋地原來就是如此)

口語解釋自旋:持有自旋鎖的線程不釋放自旋鎖,那也沒有關系呀,我就在這里不斷地一遍又一遍地查詢自旋鎖是否釋放出來,一旦釋放出來我立馬就可以直接使用 (因為我並沒有掛起等待,不需要像互斥鎖還需要進行線程間切換,重新獲取CPU,保存恢復上下文等等操作)

哪正是因為上述這些特點,線程嘗試獲取自旋鎖,獲取不到不會採取休眠掛起地方式,而是原地自旋(一遍又一遍查詢自旋鎖是否可以獲取)效率是遠高於互斥鎖了. 那我們是不是所有情況都使用自旋鎖就行了呢,互斥鎖就可以放棄使用了嗎????

解釋自旋鎖地弊端:如果每一個線程都僅僅只是需要短時間獲取這個鎖,那我自旋占據CPU等待是沒啥問題地。要是線程需要長時間地使用占據(鎖)。。。 會造成過多地無端占據CPU資源,俗稱站著茅坑不拉屎... 但是要是僅僅是短時間地自旋,平衡CPU利用率 + 程序運行效率 (自旋鎖確實是在有些時候更加合適)

自旋鎖需要場景:內核可搶占或者SMP(多處理器)情況下才真正需求 (避免死鎖陷入死循環,瘋狂地自旋,比如遞歸獲取自旋鎖. 你獲取了還要獲取,但是又沒法釋放)

自旋鎖的使用函數其實和互斥鎖幾乎是一摸一樣地,僅僅只是需要將所有的mutex換成spin即可

僅僅只是在init存在些許不同

何為驚群,池塘一堆, 我瞄準一條插過去,但是好似所有的都像是覺著自己正在被插一樣的四處逃竄。 這個就是驚群的生活一點的理解

驚群現象其實一點也不少,比如說 accept pthread_cond_broadcast 還有多個線程共享epoll監視一個listenfd 然後此刻 listenfd 說來 SYN了,放在了SYN隊列中,然後完成了三次握手放在了 accept隊列中了, 現在問題是這個connect我應該交付給哪一個線程處理呢.

多個epoll監視准備工作的線程 就是這群 (),然後connet就是魚叉,這一叉下去肯定是所有的 epoll線程都會被驚醒 (多線程共享listenfd引發的epoll驚群)

同樣如果將上述的多個線程換成多個進程共享監視 同一個 listenfd 就是(多進程的epoll驚群現象)

咱再畫一個草圖再來理解一下這個驚群:

如果是多進程道理是一樣滴,僅僅只是將所有的線程換成進程就OK了

終是來到了今天的正題了: epoll驚群問題地解決上面了...

首先 先說說accept的驚群問題,沒想到吧accept 平時大家寫它的多線程地時候,多個線程同時accept同一個listensock地時候也是會存在驚群問題地,但是accept地驚群問題已經被Linux內核處理了: 當有新的連接進入到accept隊列的時候,內核喚醒且僅喚醒一個進程來處理

但是對於epoll的驚群問題,內核卻沒有直接進行處理。哪既然內核沒有直接幫我們處理,我們應該如何針對這種現象做出一定的措施呢?

驚群效應帶來的弊端: 驚群現象會造成epoll的偽喚醒,本來epoll是阻塞掛起等待著地,這個時候因為掛起等待是不會佔用CPU地。。。 但是一旦喚醒就會佔用CPU去處理發生地IO事件, 但是其實是一個偽喚醒,這個就是對於線程或者進程的無效調度。然而進程或者線程地調取是需要花費代價地,需要上下文切換。需要進行進程(線程)間的不斷切換... 本來多核CPU是用來支持高並發地,但是現在卻被用來無效地喚醒,對於多核CPU簡直就是一種浪費 (浪費系統資源) 還會影響系統的性能.

解決方式(一般是兩種)

Nginx的解決方式:

加鎖:驚群問題發生的前提是多個進程(線程)監聽同一個套接字(listensock)上的事件,所以我們只讓一個進程(線程)去處理監聽套接字就可以了。

畫兩張圖來理解一下:

上述還沒有進行一個每一個進程都對應一個listensock 而是多線程共享一個listensock 運行結果如下

所有的線程同時被喚醒了,但是實際上會處理連接的僅僅只是一個線程,

咱僅僅只是將主線程做如上這樣一個簡單的修改,每一個線程對應一個listensock;每一個線程一個獨有的監視窗口,將問題拋給內核去處理,讓內核去負載均衡 : 結果如下

僅僅喚醒一個線程來進行處理連接,解決了驚群問題

本文通過介紹兩種鎖入手,以及為什麼需要鎖,鎖本質就是為了保護,持有鎖你就有權力有能力操作寫入一定的臨界保護資源,沒有鎖你就不行需要等待,本質其實是將多條匯編指令綁定成原子操作

然後介紹了驚群現象,通過一個巧妙地例子,扔一顆石子,只是瞄準一條魚扔過去了,但是整池魚都被驚醒了,

對應我們地實際問題就是, 多個線程或者進程共同監視同一個listensock。。。。然後IO連接事件到來地時候本來僅僅只是需要一個線程醒過來處理即可,但是卻會使得所有地線程(進程)全部醒過來,造成不必要地進程線程間切換,多核CPU被浪費喔,系統資源被浪費

處理方式 一。 Nginx 源碼加互斥鎖處理。。 二。設置SO_REUSEPORT, 使得多個進程線程可以同時連接同一個port , 為每一個進程線程搞一個listensock... 將問題拋給內核去處理,讓他去負載均衡地僅僅將IO連接事件分配給一個進程或線程

『肆』 Redis源碼分析之事件循環

本篇我們來講Redis的事件循環,Redis的事件循環會根據系統選擇evport、epoll、kqueue或select來進行IO多路復用,我們這里只分析epoll。

首先我們來看一下Redis的IO多路復用對事件循環(aeEventLoop)提供的介面。

以epoll(ae_epoll.c)為例,先來看一下Redis的IO多路復用的使用過程:

首先需要創建,即調用aeApiCreate:

aeApiState結構體有兩個成員,events和epfd。events用於存儲就緒的epoll事件,epfd存儲epoll的文件描述符。aeApiCreate的主要邏輯是為aeApiState分配存儲空間,調用epoll_create系統調用創建epoll並獲取描述符,最後將aeApiState賦值給aeEventLoop的apidata。

然後在有新的文件描述符(比如接受了一個新連接)需要加入到epoll中時,調用aeApiAddEvent:

參數fd是需要監視的文件描述符,mask標明是需要監視可讀還是可寫事件。aeApiAddEvent的主要邏輯是調用系統調用epoll_ctl注冊或修改添加事件的監聽類型到epoll。

然後在有文件描述符失效或者需要修改監聽類型時,調用aeApiDelEvent:

參數fd是需要刪除的文件描述符,mask標明是需要刪除可讀還是可寫事件。aeApiDelEvent主要邏輯是調用系統調用epoll_ctl刪除或修改刪除事件的監聽類型到epoll。

然後需要檢查是否有就緒的事件,調用aeApiPoll:

tvp是等待時間,一般而言,這個值是0(不是NULL)代表沒有就緒事件立即返回。主要邏輯是調用系統調用epoll_wait拿到就緒事件保存到events中,然後將events中的就緒事件復制到事件循環aeEventLoop的fired中,最後返回就緒事件的數量。

我們來分析一下Redis的事件循環(ae.c)。

先看主要介面:

創建過程:

初始化aeEventLoop和aeApiState並返回aeEventLoop。

注冊文件事件:

存儲到events中並調用aeApiAddEvent注冊到epoll中。

注冊定時事件:

創建aeTimeEvent並將其插入到定時事件鏈表的頭部。

主循環:

不停的調用aeProcessEvents拉取並處理事件。

接下來看aeProcessEvents的邏輯:

再看一下定時事件的觸發,也就是processTimeEvents的邏輯:

遍歷注冊的定時事件,找出到期的事件並調用處理函數,如果處理函數返回了下次執行的時間,則更新下次觸發的時間,否則刪除該事件。

『伍』 mac os下有辦法是用epoll嗎我要編譯一個linux下寫的源碼,發現系統里沒有epoll,有辦法安裝嗎

你這個程序是 linux-only 的還是 POSIX 兼容的?
如果是兼容的你看看他缺那個函數庫裝上就行了。我記得 mac 有 posix 兼容支持功能庫裝上就行了,當然這個兼容不全,有些東西還要自己另外裝。

不過 epoll 我沒印象是什麼……好像是 Linux 內核的?
如果是 Linux 內核的東西,那這個程序就是 Linux-Only 的程序,你只能做源代碼移植了。

BSD 的內核有 Linux 兼容介面層可以用,MAC 的我沒印象有。

『陸』 epoll模型

最近一段時間看epoll的源碼,看的抓耳撓腮。本著分享的原則,分享一下我對epoll的理解,注意:本文並不能讓你從零開始學epoll,而是希望在你看epoll源碼也學的抓耳撓腮的時候,看到本文能對你有一丟小小的幫助。

本篇文章並不會設計到具體源碼,只是涉及到epoll的整個數據交互的流程。

一個應用程序,想要使用epoll模型,首先會創建一個epoll模型。這里是在內核創建一個epoll模型,你可以把這個模型看做一個java對象。這個對象里有一個阻塞列表,一個就緒列表,一個紅黑樹。

每一次通訊,客戶端都會建立一個socket,socket有一個文件描述符fd,系統通過這個fd去操作socket,系統會封裝一個epitem(紅黑樹的節點),這個epitem包含socket和一個回調事件,當然還有其他屬性,我們暫且不提。
然後內核會把這個epitem添加到紅黑樹,當然,也可以修改和刪除事件。

用戶進程去就緒列表拿就緒事件。拿到就返回,拿不到就進入epoll阻塞列表,當然也可以設置超時參數為0表示拿不到也立即返回。

回調事件會根據監聽的事件類型,把fd放到就緒列表。然後去通知阻塞進程。阻塞進程從就緒列表拿走就緒事件,也就是把就緒事件從內核空間拷貝到用戶空間。

這是我對epoll模型的大概描述,接下來,從交互的層面再說一說:

當一個數據包從網路傳輸過來,包含了協議,發送端ip 和埠,目標端ip和介面。內核根據這五個要素去找到對應的socket,就可以拿到對應的fd,有了fd,就可以去紅黑樹里找epitem,找到了epitem就可以去觸發回調事件。回調事件就可以把就緒事件放到就緒列表。
輸入netstat命名就可以查看系統的所有活躍的socket鏈接:

例子里,本地機器有兩個socket鏈接和172.217.27.42.443埠建立了鏈接,當網路數據包傳輸過來,發送端ip和埠號就是172.217.27.42.443,接收端可能是54040和54035埠,用發送端ip和埠號為參數去對應foreign address,用接收端參數去對應local address,就可以找到具體的哪一個socket連接。

如果在拷貝就緒事件的時候,出現了新的就緒事件怎麼辦,其實還有一個備用鏈表,拷貝事件的時候,如果有就緒事件產生,先放到備用鏈表,拷貝完成再把備用鏈表的就緒事件放到就緒列表裡。

如果想看細節,還是得自己擼源碼,別人講只能聽個大概。

『柒』 nginx 源碼 epoll模塊在哪個文件

Linux平台上,Nginx使用epoll完成事件驅動,實現高並發;本文將不對epoll本身進行介紹(網上一堆一堆的文章介紹epoll的原理及使用方法,甚至源碼分析等),僅看一下Nginx是如何使用epoll的。

Nginx在epoll模塊中定義了好幾個函數,這些函數基本都是作為回調注冊到事件抽象層的對應介面上,從而實現了事件驅動的具體化,我們看如下的一段代碼:

[cpp] view plain print?
ngx_event_mole_t ngx_epoll_mole_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
{
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
NULL, /* process the changes */
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}
};

這段代碼就是epoll的相關函數注冊到事件抽象層,這里所謂的事件抽象層在前面的博文中有提過,就是Nginx為了方便支持和開發具體的I/O模型,從而實現的一層抽象。代碼後面的注釋將功能說明得很詳細了,本文就只重點關注ngx_epoll_init和ngx_epoll_process_events兩個函數,其他幾個函數就暫且忽略了。

ngx_epoll_init主要是完成epoll的相關初始化工作,代碼分析如下:

[cpp] view plain print?
static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
ngx_epoll_conf_t *epcf;
/*取得epoll模塊的配置結構*/
epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_mole);
/*ep是epoll模塊定義的一個全局變數,初始化為-1*/
if (ep == -1) {
/*創一個epoll對象,容量為總連接數的一半*/
ep = epoll_create(cycle->connection_n / 2);
if (ep == -1) {
ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
"epoll_create() failed");
return NGX_ERROR;
}
}
/*nevents也是epoll模塊定義的一個全局變數,初始化為0*/
if (nevents < epcf->events) {
if (event_list) {
ngx_free(event_list);
}

/*event_list存儲產生事件的數組*/
event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events,
cycle->log);
if (event_list == NULL) {
return NGX_ERROR;
}
}
nevents = epcf->events;
/*初始化全局變數ngx_io, ngx_os_is定義為:
ngx_os_io_t ngx_os_io = {
ngx_unix_recv,
ngx_readv_chain,
ngx_udp_unix_recv,
ngx_unix_send,
ngx_writev_chain,
0
};(位於src/os/unix/ngx_posix_init.c)
*/
ngx_io = ngx_os_io;
/*這里就是將epoll的具體介面函數注冊到事件抽象層介面ngx_event_actions上。
具體是上文提到的ngx_epoll_mole_ctx中封裝的如下幾個函數
ngx_epoll_add_event,
ngx_epoll_del_event,
ngx_epoll_add_event,
ngx_epoll_del_event,
ngx_epoll_add_connection,
ngx_epoll_del_connection,
ngx_epoll_process_events,
ngx_epoll_init,
ngx_epoll_done,
*/
ngx_event_actions = ngx_epoll_mole_ctx.actions;
#if (NGX_HAVE_CLEAR_EVENT)
/*epoll將添加這個標志,主要為了實現邊緣觸發*/
ngx_event_flags = NGX_USE_CLEAR_EVENT
#else
/*水平觸發*/
ngx_event_flags = NGX_USE_LEVEL_EVENT
#endif
|NGX_USE_GREEDY_EVENT /*io的時候,直到EAGAIN為止*/
|NGX_USE_EPOLL_EVENT; /*epoll標志*/
return NGX_OK;
}

epoll初始化工作沒有想像中的復雜,和我們平時使用epoll都一樣,下面看ngx_epoll_process_events,這個函數主要用來完成事件的等待並處理。

[cpp] view plain print?
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_log_t *log;
ngx_event_t *rev, *wev, **queue;
ngx_connection_t *c;
/*一開始就是等待事件,最長等待時間為timer;nginx為事件
專門用紅黑樹維護了一個計時器。後續對這個timer單獨分析。
*/
events = epoll_wait(ep, event_list, (int) nevents, timer);
if (events == -1) {
err = ngx_errno;
} else {
err = 0;
}
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
/*執行一次時間更新, nginx將時間緩存到了一組全局變數中,方便程序高效的獲取事件。*/
ngx_time_update();
}
/*處理wait錯誤*/
if (err) {
if (err == NGX_EINTR) {
if (ngx_event_timer_alarm) {
ngx_event_timer_alarm = 0;
return NGX_OK;
}
level = NGX_LOG_INFO;
} else {
level = NGX_LOG_ALERT;
}
ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
return NGX_ERROR;
}
/*wait返回事件數0,可能是timeout返回,也可能是非timeout返回;非timeout返回則是error*/
if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
}
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"epoll_wait() returned no events without timeout");
return NGX_ERROR;
}
log = cycle->log;
/*for循環開始處理收到的所有事件*/
for (i = 0; i < events; i++) {

/*取得發生此事件的連接*/
c = event_list[i].data.ptr;
instance = (uintptr_t) c & 1;
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
/*獲得該連接上的讀事件*/
rev = c->read;
。。。。。。。。。。。。。

/*取得發生一個事件*/
revents = event_list[i].events;

/*記錄wait的錯誤返回狀態*/
if (revents & (EPOLLERR|EPOLLHUP)) {
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, log, 0,
"epoll_wait() error on fd:%d ev:%04XD",
c->fd, revents);
}
if ((revents & (EPOLLERR|EPOLLHUP))
&& (revents & (EPOLLIN|EPOLLOUT)) == 0)
{
/*
* if the error events were returned without EPOLLIN or EPOLLOUT,
* then add these flags to handle the events at least in one
* active handler
*/
revents |= EPOLLIN|EPOLLOUT;
}
/*該事件是一個讀事件,並該連接上注冊的讀事件是active的*/
if ((revents & EPOLLIN) && rev->active) {
if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
rev->posted_ready = 1;
} else {
rev->ready = 1;
}

/*事件放入相應的隊列中;關於此處的先入隊再處理,在前面的文章中已經介紹過了。*/
if (flags & NGX_POST_EVENTS) {
queue = (ngx_event_t **) (rev->accept ?
&ngx_posted_accept_events : &ngx_posted_events);
ngx_locked_post_event(rev, queue); /*入隊*/
} else {
rev->handler(rev);
}
}
wev = c->write;
/*發生的是一個寫事件,和讀事件完全一樣的邏輯過程*/
if ((revents & EPOLLOUT) && wev->active) {
if (flags & NGX_POST_THREAD_EVENTS) {
wev->posted_ready = 1;
} else {
wev->ready = 1;
}
/*先入隊再處理*/
if (flags & NGX_POST_EVENTS) {
ngx_locked_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
}
return NGX_OK;
}

本文將關注的兩個epoll函數也就這么一點代碼了,但整個epoll還有添加事件和刪除事件等的相關函數,代碼都很簡單,本文就不做具體的分析了。

寫到此處的時候,我感覺epoll模塊沒有分析的足夠詳細,或者說是沒有足夠的理解作者的用意,如果你有更好的理解,希望能夠告訴我。或許,隨著後面的分析,能夠逐漸的真正明白吧。

『捌』 面試必問的epoll技術,從內核源碼出發徹底搞懂epoll

epoll是linux中IO多路復用的一種機制,I/O多路復用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。當然linux中IO多路復用不僅僅是epoll,其他多路復用機制還有select、poll,但是接下來介紹epoll的內核實現。

events可以是以下幾個宏的集合:

epoll相比select/poll的優勢

epoll相關的內核代碼在fs/eventpoll.c文件中,下面分別分析epoll_create、epoll_ctl和epoll_wait三個函數在內核中的實現,分析所用linux內核源碼為4.1.2版本。

epoll_create用於創建一個epoll的句柄,其在內核的系統實現如下:

sys_epoll_create:

可見,我們在調用epoll_create時,傳入的size參數,僅僅是用來判斷是否小於等於0,之後再也沒有其他用處。
整個函數就3行代碼,真正的工作還是放在sys_epoll_create1函數中。

sys_epoll_create -> sys_epoll_create1:

sys_epoll_create1 函數流程如下:

sys_epoll_create -> sys_epoll_create1 -> ep_alloc:


sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags:

linux內核中,current是個宏,返回的是一個task_struct結構(我們稱之為進程描述符)的變數,表示的是當前進程,進程打開的文件資源保存在進程描述符的files成員裡面,所以current->files返回的當前進程打開的文件資源。rlimit(RLIMIT_NOFILE) 函數獲取的是當前進程可以打開的最大文件描述符數,這個值可以設置,默認是1024。

相關視頻推薦:

支撐億級io的底層基石 epoll實戰揭秘

網路原理tcp/udp,網路編程epoll/reactor,面試中正經「八股文」

學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂

需要更多C/C++ Linux伺服器架構師學習資料加群 812855908 獲取(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

__alloc_fd的工作是為進程在[start,end)之間(備註:這里start為0, end為進程可以打開的最大文件描述符數)分配一個可用的文件描述符,這里就不繼續深入下去了,代碼如下:

sys_epoll_create -> sys_epoll_create1 -> ep_alloc -> get_unused_fd_flags -> __alloc_fd:

然後,epoll_create1會調用anon_inode_getfile,創建一個file結構,如下:

sys_epoll_create -> sys_epoll_create1 -> anon_inode_getfile:

anon_inode_getfile函數中首先會alloc一個file結構和一個dentry結構,然後將該file結構與一個匿名inode節點anon_inode_inode掛鉤在一起,這里要注意的是,在調用anon_inode_getfile函數申請file結構時,傳入了前面申請的eventpoll結構的ep變數,申請的file->private_data會指向這個ep變數,同時,在anon_inode_getfile函數返回來後,ep->file會指向該函數申請的file結構變數。

簡要說一下file/dentry/inode,當進程打開一個文件時,內核就會為該進程分配一個file結構,表示打開的文件在進程的上下文,然後應用程序會通過一個int類型的文件描述符來訪問這個結構,實際上內核的進程裡面維護一個file結構的數組,而文件描述符就是相應的file結構在數組中的下標。

dentry結構(稱之為「目錄項」)記錄著文件的各種屬性,比如文件名、訪問許可權等,每個文件都只有一個dentry結構,然後一個進程可以多次打開一個文件,多個進程也可以打開同一個文件,這些情況,內核都會申請多個file結構,建立多個文件上下文。但是,對同一個文件來說,無論打開多少次,內核只會為該文件分配一個dentry。所以,file結構與dentry結構的關系是多對一的。

同時,每個文件除了有一個dentry目錄項結構外,還有一個索引節點inode結構,裡面記錄文件在存儲介質上的位置和分布等信息,每個文件在內核中只分配一個inode。 dentry與inode描述的目標是不同的,一個文件可能會有好幾個文件名(比如鏈接文件),通過不同文件名訪問同一個文件的許可權也可能不同。dentry文件所代表的是邏輯意義上的文件,記錄的是其邏輯上的屬性,而inode結構所代表的是其物理意義上的文件,記錄的是其物理上的屬性。dentry與inode結構的關系是多對一的關系。

sys_epoll_create -> sys_epoll_create1 -> fd_install:

總結epoll_create函數所做的事:調用epoll_create後,在內核中分配一個eventpoll結構和代表epoll文件的file結構,並且將這兩個結構關聯在一塊,同時,返回一個也與file結構相關聯的epoll文件描述符fd。當應用程序操作epoll時,需要傳入一個epoll文件描述符fd,內核根據這個fd,找到epoll的file結構,然後通過file,獲取之前epoll_create申請eventpoll結構變數,epoll相關的重要信息都存儲在這個結構裡面。接下來,所有epoll介面函數的操作,都是在eventpoll結構變數上進行的。

所以,epoll_create的作用就是為進程在內核中建立一個從epoll文件描述符到eventpoll結構變數的通道。

epoll_ctl介面的作用是添加/修改/刪除文件的監聽事件,內核代碼如下:

sys_epoll_ctl:

根據前面對epoll_ctl介面的介紹,op是對epoll操作的動作(添加/修改/刪除事件),ep_op_has_event(op)判斷是否不是刪除操作,如果op != EPOLL_CTL_DEL為true,則需要調用_from_user函數將用戶空間傳過來的event事件拷貝到內核的epds變數中。因為,只有刪除操作,內核不需要使用進程傳入的event事件。

接著連續調用兩次fdget分別獲取epoll文件和被監聽文件(以下稱為目標文件)的file結構變數(備註:該函數返回fd結構變數,fd結構包含file結構)。

接下來就是對參數的一些檢查,出現如下情況,就可以認為傳入的參數有問題,直接返回出錯:

當然下面還有一些關於操作動作如果是添加操作的判斷,這里不做解釋,比較簡單,自行閱讀。

在ep裡面,維護著一個紅黑樹,每次添加註冊事件時,都會申請一個epitem結構的變數表示事件的監聽項,然後插入ep的紅黑樹裡面。在epoll_ctl裡面,會調用ep_find函數從ep的紅黑樹裡面查找目標文件表示的監聽項,返回的監聽項可能為空。

接下來switch這塊區域的代碼就是整個epoll_ctl函數的核心,對op進行switch出來的有添加(EPOLL_CTL_ADD)、刪除(EPOLL_CTL_DEL)和修改(EPOLL_CTL_MOD)三種情況,這里我以添加為例講解,其他兩種情況類似,知道了如何添加監聽事件,其他刪除和修改監聽事件都可以舉一反三。

為目標文件添加監控事件時,首先要保證當前ep裡面還沒有對該目標文件進行監聽,如果存在(epi不為空),就返回-EEXIST錯誤。否則說明參數正常,然後先默認設置對目標文件的POLLERR和POLLHUP監聽事件,然後調用ep_insert函數,將對目標文件的監聽事件插入到ep維護的紅黑樹裡面:

sys_epoll_ctl -> ep_insert:

前面說過,對目標文件的監聽是由一個epitem結構的監聽項變數維護的,所以在ep_insert函數裡面,首先調用kmem_cache_alloc函數,從slab分配器裡面分配一個epitem結構監聽項,然後對該結構進行初始化,這里也沒有什麼好說的。我們接下來看ep_item_poll這個函數調用:

sys_epoll_ctl -> ep_insert -> ep_item_poll:

ep_item_poll函數裡面,調用目標文件的poll函數,這個函數針對不同的目標文件而指向不同的函數,如果目標文件為套接字的話,這個poll就指向sock_poll,而如果目標文件為tcp套接字來說,這個poll就是tcp_poll函數。雖然poll指向的函數可能會不同,但是其作用都是一樣的,就是獲取目標文件當前產生的事件位,並且將監聽項綁定到目標文件的poll鉤子裡面(最重要的是注冊ep_ptable_queue_proc這個poll callback回調函數),這步操作完成後,以後目標文件產生事件就會調用ep_ptable_queue_proc回調函數。

接下來,調用list_add_tail_rcu將當前監聽項添加到目標文件的f_ep_links鏈表裡面,該鏈表是目標文件的epoll鉤子鏈表,所有對該目標文件進行監聽的監聽項都會加入到該鏈表裡面。

然後就是調用ep_rbtree_insert,將epi監聽項添加到ep維護的紅黑樹裡面,這里不做解釋,代碼如下:

sys_epoll_ctl -> ep_insert -> ep_rbtree_insert:

前面提到,ep_insert有調用ep_item_poll去獲取目標文件產生的事件位,在調用epoll_ctl前這段時間,可能會產生相關進程需要監聽的事件,如果有監聽的事件產生,(revents & event->events 為 true),並且目標文件相關的監聽項沒有鏈接到ep的准備鏈表rdlist裡面的話,就將該監聽項添加到ep的rdlist准備鏈表裡面,rdlist鏈接的是該epoll描述符監聽的所有已經就緒的目標文件的監聽項。並且,如果有任務在等待產生事件時,就調用wake_up_locked函數喚醒所有正在等待的任務,處理相應的事件。當進程調用epoll_wait時,該進程就出現在ep的wq等待隊列裡面。接下來講解epoll_wait函數。

總結epoll_ctl函數:該函數根據監聽的事件,為目標文件申請一個監聽項,並將該監聽項掛人到eventpoll結構的紅黑樹裡面。

epoll_wait等待事件的產生,內核代碼如下:

sys_epoll_wait:

首先是對進程傳進來的一些參數的檢查:

參數全部檢查合格後,接下來就調用ep_poll函數進行真正的處理:

sys_epoll_wait -> ep_poll:

ep_poll中首先是對等待時間的處理,timeout超時時間以ms為單位,timeout大於0,說明等待timeout時間後超時,如果timeout等於0,函數不阻塞,直接返回,小於0的情況,是永久阻塞,直到有事件產生才返回。

當沒有事件產生時((!ep_events_available(ep))為true),調用__add_wait_queue_exclusive函數將當前進程加入到ep->wq等待隊列裡面,然後在一個無限for循環裡面,首先調用set_current_state(TASK_INTERRUPTIBLE),將當前進程設置為可中斷的睡眠狀態,然後當前進程就讓出cpu,進入睡眠,直到有其他進程調用wake_up或者有中斷信號進來喚醒本進程,它才會去執行接下來的代碼。

如果進程被喚醒後,首先檢查是否有事件產生,或者是否出現超時還是被其他信號喚醒的。如果出現這些情況,就跳出循環,將當前進程從ep->wp的等待隊列裡面移除,並且將當前進程設置為TASK_RUNNING就緒狀態。

如果真的有事件產生,就調用ep_send_events函數,將events事件轉移到用戶空間裡面。

sys_epoll_wait -> ep_poll -> ep_send_events:

ep_send_events沒有什麼工作,真正的工作是在ep_scan_ready_list函數裡面:

sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list:

ep_scan_ready_list首先將ep就緒鏈表裡面的數據鏈接到一個全局的txlist裡面,然後清空ep的就緒鏈表,同時還將ep的ovflist鏈表設置為NULL,ovflist是用單鏈表,是一個接受就緒事件的備份鏈表,當內核進程將事件從內核拷貝到用戶空間時,這段時間目標文件可能會產生新的事件,這個時候,就需要將新的時間鏈入到ovlist裡面。

僅接著,調用sproc回調函數(這里將調用ep_send_events_proc函數)將事件數據從內核拷貝到用戶空間。

sys_epoll_wait -> ep_poll -> ep_send_events -> ep_scan_ready_list -> ep_send_events_proc:

ep_send_events_proc回調函數循環獲取監聽項的事件數據,對每個監聽項,調用ep_item_poll獲取監聽到的目標文件的事件,如果獲取到事件,就調用__put_user函數將數據拷貝到用戶空間。

回到ep_scan_ready_list函數,上面說到,在sproc回調函數執行期間,目標文件可能會產生新的事件鏈入ovlist鏈表裡面,所以,在回調結束後,需要重新將ovlist鏈表裡面的事件添加到rdllist就緒事件鏈表裡面。

同時在最後,如果rdlist不為空(表示是否有就緒事件),並且由進程等待該事件,就調用wake_up_locked再一次喚醒內核進程處理事件的到達(流程跟前面一樣,也就是將事件拷貝到用戶空間)。

到這,epoll_wait的流程是結束了,但是有一個問題,就是前面提到的進程調用epoll_wait後會睡眠,但是這個進程什麼時候被喚醒呢?在調用epoll_ctl為目標文件注冊監聽項時,對目標文件的監聽項注冊一個ep_ptable_queue_proc回調函數,ep_ptable_queue_proc回調函數將進程添加到目標文件的wakeup鏈表裡面,並且注冊ep_poll_callbak回調,當目標文件產生事件時,ep_poll_callbak回調就去喚醒等待隊列裡面的進程。

總結一下epoll該函數: epoll_wait函數會使調用它的進程進入睡眠(timeout為0時除外),如果有監聽的事件產生,該進程就被喚醒,同時將事件從內核裡面拷貝到用戶空間返回給該進程。

『玖』 mac os下有辦法是用epoll嗎我要編譯一個linux下寫的源碼,發現系統里沒有epoll,有辦法安裝嗎

kqueue也需能行,Macos畢竟是基於bsd的

閱讀全文

與epoll源碼使用相關的資料

熱點內容
linuxc多進程 瀏覽:649
android飛行游戲 瀏覽:965
數據挖掘常見演算法 瀏覽:134
python單實例化 瀏覽:351
str中python 瀏覽:89
java的equals用法 瀏覽:845
奧維雲伺服器怎麼開通 瀏覽:171
js取得伺服器地址 瀏覽:812
起點中文網小說緩存在哪個文件夾 瀏覽:216
java瘋狂講義pdf 瀏覽:300
推有錢app在哪裡 瀏覽:745
寧波鮑斯壓縮機 瀏覽:93
新建文件夾電影2完整版演員表 瀏覽:988
空調壓縮機為什麼不能放到冷庫用 瀏覽:89
江西雲伺服器節點虛擬主機 瀏覽:997
新氧app如何測試臉型 瀏覽:688
個稅app如何查詢社保 瀏覽:495
安卓設備快充什麼時候開啟的 瀏覽:13
ipad怎麼用安卓手機傳文件 瀏覽:584
編輯程序員視頻 瀏覽:634