<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

深度剖析Linux的epoll機制

Linux愛好者 ? 來源:奇伢云存儲 ? 作者:奇伢 ? 2021-07-29 10:52 ? 次閱讀

Linux 系統之中有一個核心武器:epoll 池,在高并發的,高吞吐的 IO 系統中常常見到 epoll 的身影。

IO 多路復用

在 Go 里最核心的是 Goroutine ,也就是所謂的協程,協程最妙的一個實現就是異步的代碼長的跟同步代碼一樣。比如在 Go 中,網絡 IO 的 read,write 看似都是同步代碼,其實底下都是異步調用,一般流程是:

write ( /* IO 參數 */ )

請求入隊

等待完成

后臺 loop 程序

發送網絡請求

喚醒業務方

Go 配合協程在網絡 IO 上實現了異步流程的代碼同步化。核心就是用 epoll 池來管理網絡 fd 。

實現形式上,后臺的程序只需要 1 個就可以負責管理多個 fd 句柄,負責應對所有的業務方的 IO 請求。這種一對多的 IO 模式我們就叫做 IO 多路復用。

多路是指?多個業務方(句柄)并發下來的 IO 。

復用是指?復用這一個后臺處理程序。

站在 IO 系統設計人員的角度,業務方咱們沒辦法提要求,因為業務是上帝,只有你服從的份,他們要創建多個 fd,那么你就需要負責這些 fd 的處理,并且最好還要并發起來。

業務方沒法提要求,那么只能要求后臺 loop 程序了!

要求什么呢?快!快!快!這就是最核心的要求,處理一定要快,要給每一個 fd 通道最快的感受,要讓每一個 fd 覺得,你只在給他一個人跑腿。

那有人又問了,那我一個 IO 請求(比如 write )對應一個線程來處理,這樣所有的 IO 不都并發了嗎?是可以,但是有瓶頸,線程數一旦多了,性能是反倒會差的。

這里不再對比多線程和 IO 多路復用實現高并發之間的區別,詳細的可以去了解下 nginx 和 redis 高并發的秘密。

1 最樸實的實現方式?

我不用任何其他系統調用,能否實現 IO 多路復用?

可以的。那么寫個 for 循環,每次都嘗試 IO 一下,讀/寫到了就處理,讀/寫不到就 sleep 下。這樣我們不就實現了 1 對多的 IO 多路復用嘛。

while True:

for each 句柄數組 {

read/write(fd, /* 參數 */)

}

sleep(1s)

慢著,有個問題,上面的程序可能會被卡死在第三行,使得整個系統不得運行,為什么?

默認情況下,我們 create 出的句柄是阻塞類型的。我們讀數據的時候,如果數據還沒準備好,是會需要等待的,當我們寫數據的時候,如果還沒準備好,默認也會卡住等待。所以,在上面偽代碼第三行是可能被直接卡死,而導致整個線程都得到不到運行。

舉個例子,現在有 11,12,13 這 3 個句柄,現在 11 讀寫都沒有準備好,只要 read/write(11, /*參數*/) 就會被卡住,但 12,13 這兩個句柄都準備好了,那遍歷句柄數組 11,12,13 的時候就會卡死在前面,后面 12,13 則得不到運行。這不符合我們的預期,因為我們 IO 多路復用的 loop 線程是公共服務,不能因為一個 fd 就直接癱瘓。

那這個問題怎么解決?

只需要把 fd 都設置成非阻塞模式。這樣 read/write 的時候,如果數據沒準備好,返回 EAGIN 的錯誤即可,不會卡住線程,從而整個系統就運轉起來了。比如上面句柄 11 還未就緒,那么 read/write(11, /*參數*/) 不會阻塞,只會報個 EAGIN 的錯誤,這種錯誤需要特殊處理,然后 loop 線程可以繼續執行 12,13 的讀寫。

以上就是最樸實的 IO 多路復用的實現了。但好像在生產環境沒見過這種 IO 多路復用的實現?為什么?

因為還不夠高級。for 循環每次要定期 sleep 1s,這個會導致吞吐能力極差,因為很可能在剛好要 sleep 的時候,所有的 fd 都準備好 IO 數據,而這個時候卻要硬生生的等待 1s,可想而知。。。

那有同學又要質疑了,那 for 循環里面就不 sleep 嘛,這樣不就能及時處理了嗎?

及時是及時了,但是 CPU 估計要跑飛了。不加 sleep ,那在沒有 fd 需要處理的時候,估計 CPU 都要跑到 100% 了。這個也是無法接受的。

糾結了,那 sleep 吞吐不行,不 sleep 浪費 cpu,怎么辦?

這種情況用戶態很難有所作為,只能求助內核來提供機制協助來。因為內核才能及時的管理這些事件的通知和調度。

我們再梳理下 IO 多路復用的需求和原理。IO 多路復用就是 1 個線程處理 多個 fd 的模式。我們的要求是:這個 “1” 就要盡可能的快,避免一切無效工作,要把所有的時間都用在處理句柄的 IO 上,不能有任何空轉,sleep 的時間浪費。

有沒有一種工具,我們把一籮筐的 fd 放到里面,只要有一個 fd 能夠讀寫數據,后臺 loop 線程就要立馬喚醒,全部馬力跑起來。其他時間要把 cpu 讓出去。

能做到嗎?能,但這種需求只能內核提供機制滿足你。

2 這事 Linux 內核必須要給個說法?

是的,想要不用 sleep 這種辣眼睛的實現,Linux 內核必須出手了,畢竟 IO 的處理都是內核之中,數據好沒好內核最清楚。

內核一口氣提供了 3 種工具 select,poll,epoll 。

為什么有 3 種?

歷史不斷改進,矬 -》 較矬 -》 臥槽、高效 的演變而已。

Linux 還有其他方式可以實現 IO 多路復用嗎?

好像沒有了!

這 3 種到底是做啥的?

這 3 種都能夠管理 fd 的可讀可寫事件,在所有 fd 不可讀不可寫無所事事的時候,可以阻塞線程,切走 cpu 。fd 有情況的時候,都要線程能夠要能被喚醒。

而這三種方式以 epoll 池的效率最高。為什么效率最高?

其實很簡單,這里不詳說,其實無非就是 epoll 做的無用功最少,select 和 poll 或多或少都要多余的拷貝,盲猜(遍歷才知道)fd ,所以效率自然就低了。

舉個例子,以 select 和 epoll 來對比舉例,池子里管理了 1024 個句柄,loop 線程被喚醒的時候,select 都是蒙的,都不知道這 1024 個 fd 里誰 IO 準備好了。這種情況怎么辦?只能遍歷這 1024 個 fd ,一個個測試。假如只有一個句柄準備好了,那相當于做了 1 千多倍的無效功。

epoll 則不同,從 epoll_wait 醒來的時候就能精確的拿到就緒的 fd 數組,不需要任何測試,拿到的就是要處理的。

epoll 池原理

下面我們看一下 epoll 池的使用和原理。

1 epoll 涉及的系統調用

epoll 的使用非常簡單,只有下面 3 個系統調用。

epoll_create

epollctl

epollwait

就這?是的,就這么簡單。

epollcreate 負責創建一個池子,一個監控和管理句柄 fd 的池子;

epollctl 負責管理這個池子里的 fd 增、刪、改;

epollwait 就是負責打盹的,讓出 CPU 調度,但是只要有“事”,立馬會從這里喚醒;

2 epoll 高效的原理

Linux 下,epoll 一直被吹爆,作為高并發 IO 實現的秘密武器。其中原理其實非常樸實:epoll 的實現幾乎沒有做任何無效功。 我們從使用的角度切入來一步步分析下。

首先,epoll 的第一步是創建一個池子。這個使用 epoll_create 來做:

原型:

int epoll_create(int size);

示例:

epollfd = epoll_create(1024);

if (epollfd == -1) {

perror(“epoll_create”);

exit(EXIT_FAILURE);

}

這個池子對我們來說是黑盒,這個黑盒是用來裝 fd 的,我們暫不糾結其中細節。我們拿到了一個 epollfd ,這個 epollfd 就能唯一代表這個 epoll 池。注意,這里又有一個細節:用戶可以創建多個 epoll 池。

然后,我們就要往這個 epoll 池里放 fd 了,這就要用到 epoll_ctl 了

原型:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

示例:

if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 11, &ev) == -1) {

perror(“epoll_ctl: listen_sock”);

exit(EXIT_FAILURE);

}

上面,我們就把句柄 11 放到這個池子里了,op(EPOLL_CTL_ADD)表明操作是增加、修改、刪除,event 結構體可以指定監聽事件類型,可讀、可寫。

第一個跟高效相關的問題來了,添加 fd 進池子也就算了,如果是修改、刪除呢?怎么做到快速?

這里就涉及到你怎么管理 fd 的數據結構了。

最常見的思路:用 list ,可以嗎?功能上可以,但是性能上拉垮。list 的結構來管理元素,時間復雜度都太高 O(n),每次要一次次遍歷鏈表才能找到位置。池子越大,性能會越慢。

那有簡單高效的數據結構嗎?

有,紅黑樹。Linux 內核對于 epoll 池的內部實現就是用紅黑樹的結構體來管理這些注冊進程來的句柄 fd。紅黑樹是一種平衡二叉樹,時間復雜度為 O(log n),就算這個池子就算不斷的增刪改,也能保持非常穩定的查找性能。

現在思考第二個高效的秘密:怎么才能保證數據準備好之后,立馬感知呢?

epoll_ctl 這里會涉及到一點。秘密就是:回調的設置。在 epoll_ctl 的內部實現中,除了把句柄結構用紅黑樹管理,另一個核心步驟就是設置 poll 回調。

思考來了:poll 回調是什么?怎么設置?

先說說 file_operations-》poll 是什么?

在 文件描述符 fd 究竟是什么 說過,Linux 設計成一切皆是文件的架構,這個不是說說而已,而是隨處可見。實現一個文件系統的時候,就要實現這個文件調用,這個結構體用 struct file_operations 來表示。這個結構體有非常多的函數,精簡了一些,如下:

struct file_operations {

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

__poll_t (*poll) (struct file *, struct poll_table_struct *);

int (*open) (struct inode *, struct file *);

int (*fsync) (struct file *, loff_t, loff_t, int datasync);

// 。。。。

};

你看到了 read,write,open,fsync,poll 等等,這些都是對文件的定制處理操作,對于文件的操作其實都是在這個框架內實現邏輯而已,比如 ext2 如果有對 read/write 做定制化,那么就會是 ext2_read,ext2_write,ext4 就會是 ext4_read,ext4_write。在 open 具體“文件”的時候會賦值對應文件系統的 file_operations 給到 file 結構體。

那我們很容易知道 read 是文件系統定制 fd 讀的行為調用,write 是文件系統定制 fd 寫的行為調用,file_operations-》poll 呢?

這個是定制監聽事件的機制實現。通過 poll 機制讓上層能直接告訴底層,我這個 fd 一旦讀寫就緒了,請底層硬件(比如網卡)回調的時候自動把這個 fd 相關的結構體放到指定隊列中,并且喚醒操作系統。

舉個例子:網卡收發包其實走的異步流程,操作系統把數據丟到一個指定地點,網卡不斷的從這個指定地點掏數據處理。請求響應通過中斷回調來處理,中斷一般拆分成兩部分:硬中斷和軟中斷。poll 函數就是把這個軟中斷回來的路上再加點料,只要讀寫事件觸發的時候,就會立馬通知到上層,采用這種事件通知的形式就能把浪費的時間窗就完全消失了。

劃重點:這個 poll 事件回調機制則是 epoll 池高效最核心原理。

劃重點:epoll 池管理的句柄只能是支持了 file_operations-》poll 的文件 fd。換句話說,如果一個“文件”所在的文件系統沒有實現 poll 接口,那么就用不了 epoll 機制。

第二個問題:poll 怎么設置?

在 epoll_ctl 下來的實現中,有一步是調用 vfs_poll 這個里面就會有個判斷,如果 fd 所在的文件系統的 file_operations 實現了 poll ,那么就會直接調用,如果沒有,那么就會報告響應的錯誤碼。

static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)

{

if (unlikely(!file-》f_op-》poll))

return DEFAULT_POLLMASK;

return file-》f_op-》poll(file, pt);

}

你肯定好奇 poll 調用里面究竟是實現了什么?

總結概括來說:掛了個鉤子,設置了喚醒的回調路徑。epoll 跟底層對接的回調函數是:ep_poll_callback,這個函數其實很簡單,做兩件事情:

把事件就緒的 fd 對應的結構體放到一個特定的隊列(就緒隊列,ready list);

喚醒 epoll ,活來啦!

當 fd 滿足可讀可寫的時候就會經過層層回調,最終調用到這個回調函數,把對應 fd 的結構體放入就緒隊列中,從而把 epoll 從 epoll_wait 出喚醒。

這個對應結構體是什么?

結構體叫做 epitem ,每個注冊到 epoll 池的 fd 都會對應一個。

就緒隊列需要用很高級的數據結構嗎?

就緒隊列就簡單了,因為沒有查找的需求了呀,只要是在就緒隊列中的 epitem ,都是事件就緒的,必須處理的。所以就緒隊列就是一個最簡單的雙指針鏈表。

小結下:epoll 之所以做到了高效,最關鍵的兩點:

內部管理 fd 使用了高效的紅黑樹結構管理,做到了增刪改之后性能的優化和平衡;

epoll 池添加 fd 的時候,調用 file_operations-》poll ,把這個 fd 就緒之后的回調路徑安排好。通過事件通知的形式,做到最高效的運行;

epoll 池核心的兩個數據結構:紅黑樹和就緒列表。紅黑樹是為了應對用戶的增刪改需求,就緒列表是 fd 事件就緒之后放置的特殊地點,epoll 池只需要遍歷這個就緒鏈表,就能給用戶返回所有已經就緒的 fd 數組;

3 哪些 fd 可以用 epoll 來管理?

再來思考另外一個問題:由于并不是所有的 fd 對應的文件系統都實現了 poll 接口,所以自然并不是所有的 fd 都可以放進 epoll 池,那么有哪些文件系統的 file_operations 實現了 poll 接口?

首先說,類似 ext2,ext4,xfs 這種常規的文件系統是沒有實現的,換句話說,這些你最常見的、真的是文件的文件系統反倒是用不了 epoll 機制的。

那誰支持呢?

最常見的就是網絡套接字:socket 。網絡也是 epoll 池最常見的應用地點。Linux 下萬物皆文件,socket 實現了一套 socket_file_operations 的邏輯( net/socket.c ):

static const struct file_operations socket_file_ops = {

.read_iter = sock_read_iter,

.write_iter = sock_write_iter,

.poll = sock_poll,

// 。。。

};

我們看到 socket 實現了 poll 調用,所以 socket fd 是天然可以放到 epoll 池管理的。

還有支持的嗎?

有的,很多。其實 Linux 下還有兩個很典型的 fd ,常常也會放到 epoll 池里。

eventfd:eventfd 實現非常簡單,故名思義就是專門用來做事件通知用的。使用系統調用 eventfd 創建,這種文件 fd 無法傳輸數據,只用來傳輸事件,常常用于生產消費者模式的事件實現;

timerfd:這是一種定時器 fd,使用 timerfd_create 創建,到時間點觸發可讀事件;

小結一下:

ext2,ext4,xfs 等這種真正的文件系統的 fd ,無法使用 epoll 管理;

socket fd,eventfd,timerfd 這些實現了 poll 調用的可以放到 epoll 池進行管理;

其實,在 Linux 的模塊劃分中,eventfd,timerfd,epoll 池都是文件系統的一種模塊實現。

思考

前面我們已經思考了很多知識點,有一些簡單有趣的知識點,提示給讀者朋友,這里只拋磚引玉。

問題:單核 CPU 能實現并行嗎?

不行。

問題:單線程能實現高并發嗎?

可以。

問題:那并發和并行的區別是?

一個看的是時間段內的執行情況,一個看的是時間時刻的執行情況。

問題:單線程如何做到高并發?

IO 多路復用唄,今天講的 epoll 池就是了。

問題:單線程實現并發的有開源的例子嗎?

redis,nginx 都是非常好的學習例子。當然還有我們 Golang 的 runtime 實現也盡顯高并發的設計思想。

總結

IO 多路復用的原始實現很簡單,就是一個 1 對多的服務模式,一個 loop 對應處理多個 fd ;

IO 多路復用想要做到真正的高效,必須要內核機制提供。因為 IO 的處理和完成是在內核,如果內核不幫忙,用戶態的程序根本無法精確的抓到處理時機;

fd 記得要設置成非阻塞的哦,切記;

epoll 池通過高效的內部管理結構,并且結合操作系統提供的 poll 事件注冊機制,實現了高效的 fd 事件管理,為高并發的 IO 處理提供了前提條件;

epoll 全名 eventpoll,在 Linux 內核下以一個文件系統模塊的形式實現,所以有人常說 epoll 其實本身就是文件系統也是對的;

socketfd,eventfd,timerfd 這三種”文件“fd 實現了 poll 接口,所以網絡 fd,事件fd,定時器fd 都可以使用 epoll_ctl 注冊到池子里。我們最常見的就是網絡fd的多路復用;

ext2,ext4,xfs 這種真正意義的文件系統反倒沒有提供 poll 接口實現,所以不能用 epoll 池來管理其句柄。那文件就無法使用 epoll 機制了嗎?不是的,有一個庫叫做 libaio ,通過這個庫我們可以間接的讓文件使用 epoll 通知事件,以后詳說,此處不表;

后記

epoll 池使用很簡潔,但實現不簡單。還是那句話,Linux 內核幫你包圓了。今天并沒有羅列太多源碼實現,以很小的思考點為題展開,簡單講了一些 epoll 的思考。

編輯:jq

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Linux
    +關注

    關注

    87

    文章

    11011

    瀏覽量

    206919

原文標題:深入理解 Linux 的 epoll 機制

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    深度剖析 IGBT 柵極驅動注意事項

    深度剖析 IGBT 柵極驅動注意事項
    的頭像 發表于 11-24 14:48 ?347次閱讀
    <b class='flag-5'>深度</b><b class='flag-5'>剖析</b> IGBT 柵極驅動注意事項

    Epoll封裝類實現

    關于epoll的原理,以及和poll、select、IOCP之間的比較,網上的資料很多,這些都屬于I/O復用的實現方法,即可以同時監聽發生在多個I/O端口(socket套接字描述符或文件描述符
    的頭像 發表于 11-13 11:54 ?328次閱讀

    epoll源碼分析

    Linux內核提供了3個關鍵函數供用戶來操作epoll,分別是: epoll_create(), 創建eventpoll對象 epoll_ctl(), 操作eventpoll對象
    的頭像 發表于 11-13 11:49 ?569次閱讀
    <b class='flag-5'>epoll</b>源碼分析

    epoll底層如何使用紅黑樹

    epoll和poll的一個很大的區別在于,poll每次調用時都會存在一個將pollfd結構體數組中的每個結構體元素從用戶態向內核態中的一個鏈表節點拷貝的過程,而內核中的這個鏈表并不會一直保存
    的頭像 發表于 11-10 15:13 ?339次閱讀
    <b class='flag-5'>epoll</b>底層如何使用紅黑樹

    如何實現一套linux進程間通信的機制

    我們知道linux的進程的間通信的組件有管道,消息隊列,socket, 信號量,共享內存等。但是我們如果自己實現一套進程間通信的機制的話,要怎么做?了解android 開發的可能會知道
    的頭像 發表于 11-10 14:56 ?403次閱讀
    如何實現一套<b class='flag-5'>linux</b>進程間通信的<b class='flag-5'>機制</b>

    epoll的基礎數據結構

    一、epoll的基礎數據結構 在開始研究源代碼之前,我們先看一下 epoll 中使用的數據結構,分別是 eventpoll、epitem 和 eppoll_entry。 1、eventpoll 我們
    的頭像 發表于 11-10 10:20 ?368次閱讀
    <b class='flag-5'>epoll</b>的基礎數據結構

    epoll和select使用區別

    epoll 和select 相比于select,epoll最大的好處在于它不會隨著監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時
    的頭像 發表于 11-09 14:14 ?271次閱讀
    <b class='flag-5'>epoll</b>和select使用區別

    epoll 的實現原理

    今兒我們就從源碼入手,來幫助大家簡單理解一下 epoll 的實現原理,并在后邊分析一下,大家都說 epoll 性能好,那到底是好在哪里。 epoll 簡介 1、epoll 的簡單使用
    的頭像 發表于 11-09 11:14 ?257次閱讀
    <b class='flag-5'>epoll</b> 的實現原理

    epoll來實現多路復用

    本人用epoll來實現多路復用,epoll觸發模式有兩種: ET(邊緣模式) LT(水平模式) LT模式 是標準模式,意味著每次epoll_wait()返回后,事件處理后,如果之后還有數據,會不斷
    的頭像 發表于 11-09 10:15 ?233次閱讀
    用<b class='flag-5'>epoll</b>來實現多路復用

    linux異步io框架iouring應用

    完善的異步IO(網絡IO、磁盤IO)機制。 在網絡編程中,我們通常使用epoll IO多路復用來處理網絡IO,然而epoll也并不是異步網絡IO,僅僅是內核提供
    的頭像 發表于 11-08 15:39 ?307次閱讀
    <b class='flag-5'>linux</b>異步io框架iouring應用

    一種嵌入式Linux系統多重備份與恢復機制

    提出了一種嵌入式 Linux系統多重備份與恢復機制。采用在一片NAND Flash 上劃分多個系統鏡像區(包括內核和文件系統),在U-Boot和系統鏡像中添加多重備份與恢復機制。當運行中的鏡像區域
    發表于 09-20 07:01

    Linux kernel的kretprobe機制和kprobe有何區別?

    Linux kernel 的 kretprobe 機制和 kprobe 完全不同,本質原因在于,函數的入口地址是固定的,但函數的返回地址不固定,由于返回位置不固定,無法固定函數大小,無法事先插樁。
    的頭像 發表于 08-07 09:15 ?651次閱讀
    <b class='flag-5'>Linux</b> kernel的kretprobe<b class='flag-5'>機制</b>和kprobe有何區別?

    一文解析Linux中ARP學習和老化機制

    ARP學習和老化機制Linux網絡通信中起著至關重要的作用。ARP(Address Resolution Protocol)地址解析協議是將IP地址解析為MAC地址的一種機制。
    發表于 08-04 16:55 ?964次閱讀

    Java、Spring、Dubbo三者SPI機制的原理和區別

    其實我之前寫過一篇類似的文章,但是這篇文章主要是剖析dubbo的SPI機制的源碼,中間只是簡單地介紹了一下Java、Spring的SPI機制,并沒有進行深入,所以本篇就來深入聊一聊這三者的原理和區別。
    的頭像 發表于 06-05 15:21 ?464次閱讀
    Java、Spring、Dubbo三者SPI<b class='flag-5'>機制</b>的原理和區別

    圖文詳解Linux分頁機制

    分頁機制是 80x86 內存管理機制的第二種機制,分段機制用于把虛擬地址轉換為線性地址,而分頁機制用于把線性地址轉換為物理地址。
    發表于 05-30 09:10 ?312次閱讀
    圖文詳解<b class='flag-5'>Linux</b>分頁<b class='flag-5'>機制</b>
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>