• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            網絡服務器軟件開發/中間件開發,關注ACE/ICE/boost

            C++博客 首頁 新隨筆 聯系 聚合 管理
              152 Posts :: 3 Stories :: 172 Comments :: 0 Trackbacks
            摘要


            編寫連接數巨大的高負載服務器程序時,經典的多線程模式和select模式都不再適用。
            應當拋棄它們,采用epoll/kqueue/dev_poll來捕獲I/O事件。最后簡要介紹了AIO。


            由來


            網絡服務在處理數以萬計的客戶端連接時,往往出現效率低下甚至完全癱瘓,這被稱為
            C10K問題。隨著互聯網的迅速發展,越來越多的網絡服務開始面臨C10K問題,作為大型
            網站的開發人員有必要對C10K問題有一定的了解。本文的主要參考文獻是
            <http://www.kegel.com/c10k.htmlhttp://www.kegel.com/c10k.htmls

            C10K問題的最大特點是:設計不夠良好的程序,其性能和連接數及機器性能的關系往往
            是非線性的。舉個例子:如果沒有考慮過C10K問題,一個經典的基于select的程序能在
            舊服務器上很好處理1000并發的吞吐量,它在2倍性能新服務器上往往處理不了并發
            2000的吞吐量。

            這是因為在策略不當時,大量操作的消耗和當前連接數n成線性相關。會導致單個任務
            的資源消耗和當前連接數的關系會是O(n)。而服務程序需要同時對數以萬計的socket進
            行I/O處理,積累下來的資源消耗會相當可觀,這顯然會導致系統吞吐量不能和機器性
            能匹配。為解決這個問題,必須改變對連接提供服務的策略。


            基本策略


            主要有兩方面的策略:1.應用軟件以何種方式和操作系統合作,獲取I/O事件并調度多
            個socket上的I/O操作;2. 應用軟件以何種方式處理任務和線程/進程的關系。前者主
            要有阻塞I/O、非阻塞I/O、異步I/O這3種方案,后者主要有每任務1進程、每任務1線
            程、單線程、多任務共享線程池以及一些更復雜的變種方案。常用的經典策略如下:

            1.         Serve one client with each thread/process, and use blocking I/O
            這是小程序和java常用的策略,對于交互式的長連接應用也是常見的選擇(比如BBS)。
            這種策略很能難足高性能程序的需求,好處是實現極其簡單,容易嵌入復雜的交互邏
            輯。Apache、ftpd等都是這種工作模式。

            2.         Serve many clients with single thread, and use nonblocking I/O
            and readiness notification
            這是經典模型,datapipe等程序都是如此實現的。優點在于實現較簡單,方便移植,也
            能提供足夠的性能;缺點在于無法充分利用多CPU的機器。尤其是程序本身沒有復雜的
            業務邏輯時。

            3.         Serve many clients with each thread, and use nonblocking I/O and
            readiness notification
            對經典模型2的簡單改進,缺點是容易在多線程并發上出bug,甚至某些OS不支持多線程
            操作readiness notification。

            4.         Serve many clients with each thread, and use asynchronous I/O
            在有AI/O支持的OS上,能提供相當高的性能。不過AI/O編程模型和經典模型差別相當
            大,基本上很難寫出一個框架同時支持AI/O和經典模型,降低了程序的可移植性。在
            Windows上,這基本上是唯一的可選方案。

            本文主要討論模型2的細節,也就是在模型2下應用軟件如何處理Socket I/O。


            select 與 poll


            最原始的同步阻塞 I/O 模型的典型流程如下:

            同步阻塞 I/O 模型的典型流程

            從應用程序的角度來說,read 調用會延續很長時間,應用程序需要相當多線程來解決
            并發訪問問題。同步非阻塞I/O對此有所改進:

            經典的單線程服務器程序結構往往如下:


            do {

                     Get Readiness Notification of all sockets

                     Dispatch ready handles to corresponding handlers

                               If (readable) {
                                        read the socket

                                        If (read done)

                                                 Handler process the request
                               }

                               if (writable)

                                        write response

                               if (nothing to do)

                                        close socket

            } while(True)

            非阻塞 I/O 模型的典型流程:

            異步阻塞 I/O 模型的典型流程

            其中關鍵的部分是readiness notification,找出哪一個socket上面發生了I/O事件。
            一般從教科書和例子程序中首先學到的是用select來實現。Select定義如下:


            int select(int n, fd_set *rd_fds, fd_set *wr_fds, fd_set *ex_fds, struct
            timeval *timeout);

            Select用到了fd_set結構,從man page里可以知道fd_set能容納的句柄和FD_SETSIZE相
            關。實際上fd_set在*nix下是一個bit標志數組,每個bit表示對應下標的fd是不是在
            fd_set中。fd_set只能容納編號小于 FD_SETSIZE的那些句柄。

            FD_SETSIZE默認是1024,如果向fd_set里放入過大的句柄,數組越界以后程序就會垮
            掉。系統默認限制了一個進程最大的句柄號不超過1024,但是可以通過ulimit -n命令
            /setrlimit函數來擴大這一限制。如果不幸一個程序在FD_SETSIZE=1024的環境下編
            譯,運行時又遇到ulimit –n > 1024的,那就只有祈求上帝保佑不會垮掉了。


            在ACE環境中,ACE_Select_Reactor針對這一點特別作了保護措施,但是還是有recv_n
            這樣的函數間接的使用了select,這需要大家注意。

            針對fd_set的問題,*nix提供了poll函數作為select的一個替代品。Poll的接口如下:


            int poll(struct pollfd *ufds, unsigned int nfds, int timeout);

            第1個參數ufds是用戶提供的一個pollfd數組,數組大小由用戶自行決定,因此避免了
            FD_SETSIZE帶來的麻煩。Ufds是fd_set的一個完全替代品,從select到poll的移植很方
            便。到此為止,至少我們面對C10K,可以寫出一個能work的程序了。

            然而Select和Poll在連接數增加時,性能急劇下降。這有兩方面的原因:首先操作系統
            面對每次的select/poll操作,都需要重新建立一個當前線程的關心事件列表,并把線
            程掛在這個復雜的等待隊列上,這是相當耗時的。其次,應用軟件在select/poll返回
            后也需要對傳入的句柄列表做一次掃描來dispatch,這也是很耗時的。這兩件事都是和
            并發數相關,而I/O事件的密度也和并發數相關,導致CPU占用率和并發數近似成O(n2)
            的關系。


            epoll, kqueue, /dev/poll


            因為以上的原因,*nix的hacker們開發了epoll, kqueue, /dev/poll這3套利器來幫助
            大家,讓我們跪拜三分鐘來感謝這些大神。其中epoll是linux的方案,kqueue是
            freebsd的方案,/dev/poll是最古老的Solaris的方案,使用難度依次遞增。

            簡單的說,這些api做了兩件事:1.避免了每次調用select/poll時kernel分析參數建立
            事件等待結構的開銷,kernel維護一個長期的事件關注列表,應用程序通過句柄修改這
            個列表和捕獲I/O事件。2.避免了select/poll返回后,應用程序掃描整個句柄表的開
            銷,Kernel直接返回具體的事件列表給應用程序。

            在接觸具體api之前,先了解一下邊緣觸發(edge trigger)和條件觸發(level trigger)
            的概念。邊緣觸發是指每當狀態變化時發生一個io事件,條件觸發是只要滿足條件就發
            生一個io事件。舉個讀socket的例子,假定經過長時間的沉默后,現在來了100個字
            節,這時無論邊緣觸發和條件觸發都會產生一個read ready notification通知應用程
            序可讀。應用程序讀了50個字節,然后重新調用api等待io事件。這時條件觸發的api會
            因為還有50個字節可讀從而立即返回用戶一個read ready notification。而邊緣觸發
            的api會因為可讀這個狀態沒有發生變化而陷入長期等待。

            因此在使用邊緣觸發的api時,要注意每次都要讀到socket返回EWOULDBLOCK為止,否則
            這個socket就算廢了。而使用條件觸發的api時,如果應用程序不需要寫就不要關注
            socket可寫的事件,否則就會無限次的立即返回一個write ready notification。大家
            常用的select就是屬于條件觸發這一類,以前本人就犯過長期關注socket寫事件從而
            CPU 100%的毛病。

            epoll的相關調用如下:


            int epoll_create(int size)


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


            int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int
            timeout)

            epoll_create創建kernel中的關注事件表,相當于創建fd_set。

            epoll_ctl修改這個表,相當于FD_SET等操作

            epoll_wait等待I/O事件發生,相當于select/poll函數

            epoll完全是select/poll的升級版,支持的事件完全一致。并且epoll同時支持邊緣觸
            發和條件觸發,一般來講邊緣觸發的性能要好一些。這里有個簡單的例子:


            struct epoll_event ev, *events;

            int kdpfd = epoll_create(100);

            ev.events = EPOLLIN | EPOLLET;  // 注意這個EPOLLET,指定了邊緣觸發

            ev.data.fd =listener;

            epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev);

            for(;;) {

               nfds = epoll_wait(kdpfd, events, maxevents, -1);

             

               for(n = 0; n < nfds; ++n) {

                   if(events[n].data.fd == listener) {

                       client = accept(listener, (struct sockaddr *) &local,

                                       &addrlen);

                       if(client < 0){

                           perror("accept");

                           continue;

                       }

                       setnonblocking(client);

                       ev.events = EPOLLIN | EPOLLET;

                       ev.data.fd = client;

                       if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {

                           fprintf(stderr, "epoll set insertion error: fd=%d0,

                                   client);

                           return -1;

                       }

                   }

                   else

                       do_use_fd(events[n].data.fd);

               }

            }

            簡單介紹一下kqueue和/dev/poll

            kqueue是freebsd的寵兒,kqueue實際上是一個功能相當豐富的kernel事件隊列,它不
            僅僅是select/poll的升級,而且可以處理signal、目錄結構變化、進程等多種事件。
            Kqueue是邊緣觸發的

            /dev/poll是Solaris的產物,是這一系列高性能API中最早出現的。Kernel提供一個特
            殊的設備文件/dev/poll。應用程序打開這個文件得到操縱fd_set的句柄,通過寫入
            pollfd來修改它,一個特殊ioctl調用用來替換select。由于出現的年代比較早,所以
            /dev/poll的接口現在看上去比較笨拙可笑。

            C++開發:ACE 5.5以上版本提供了ACE_Dev_Poll_Reactor封裝了epoll和/dev/poll兩種
            api,需要分別在config.h中定義ACE_HAS_EPOLL和ACE_HAS_DEV_POLL來啟用。

            Java開發: JDK 1.6的Selector提供了對epoll的支持,JDK1.4提供了對/dev/poll的支
            持。只要選擇足夠高的JDK版本就行了。


            異步I/O以及Windows


            和經典模型不同,異步I/O提供了另一種思路。和傳統的同步I/O不同,異步I/O允許進
            程發起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完
            成的通知時,進程就可以檢索 I/O 操作的結果。

            異步非阻塞 I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會立即返回,說明
            read 請求已經成功發起了。在后臺完成讀操作時,應用程序然后會執行其他處理操
            作。當 read 的響應到達時,就會產生一個信號或執行一個基于線程的回調函數來完成
            這次 I/O 處理過程。異步I/O 模型的典型流程:

            異步非阻塞 I/O 模型的典型流程

            對于文件操作而言,AIO有一個附帶的好處:應用程序將多個細碎的磁盤請求并發的提
            交給操作系統后,操作系統有機會對這些請求進行合并和重新排序,這對同步調用而言
            是不可能的——除非創建和請求數目同樣多的線程。

            Linux Kernel 2.6提供了對AIO的有限支持——僅支持文件系統。libc也許能通過來線
            程來模擬socket的AIO,不過這對性能沒意義。總的來說Linux的aio還不成熟

            Windows對AIO的支持很好,有IOCP隊列和IPCP回調兩種方式,甚至提供了用戶級異步調
            用APC功能。Windows下AIO是唯一可用的高性能方案,詳情請參考MSDN 

            Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1537545


            posted on 2007-04-05 09:00 true 閱讀(1237) 評論(0)  編輯 收藏 引用 所屬分類: ACE
            久久99精品久久久久久动态图 | 久久精品国产亚洲AV久| 伊人久久大香线蕉综合热线| 久久亚洲国产精品成人AV秋霞| 狠狠精品久久久无码中文字幕| 欧洲人妻丰满av无码久久不卡| 99久久免费国产精精品| 青春久久| 精品久久8x国产免费观看| 色婷婷久久综合中文久久一本| 久久99精品久久久久子伦| 久久高清一级毛片| 国产成人久久精品一区二区三区 | 国产亚洲美女精品久久久| 久久99精品久久久大学生| 久久精品国产亚洲AV不卡| 久久九九精品99国产精品| 人人狠狠综合久久亚洲| 色综合久久天天综合| 久久人爽人人爽人人片AV| 国产精品亚洲综合久久| 四虎国产永久免费久久| 久久99精品久久只有精品 | 青青草原精品99久久精品66| 久久久精品波多野结衣| 亚洲国产精品一区二区久久| 久久精品aⅴ无码中文字字幕重口| 久久综合狠狠综合久久97色| 91久久成人免费| 国产精品久久久久久搜索| 香蕉久久av一区二区三区| 久久国内免费视频| 久久99热这里只频精品6| 亚洲国产视频久久| 少妇被又大又粗又爽毛片久久黑人| 久久国产乱子伦精品免费强| 国内精品久久久久久99蜜桃| 婷婷久久久亚洲欧洲日产国码AV| 中文国产成人精品久久不卡| 性欧美丰满熟妇XXXX性久久久 | 精品多毛少妇人妻AV免费久久|