• <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>
            posts - 297,  comments - 15,  trackbacks - 0

            WINDOWS完成端口編程
            1、基本概念
            2、WINDOWS完成端口的特點(diǎn)
            3、完成端口(Completion Ports )相關(guān)數(shù)據(jù)結(jié)構(gòu)和創(chuàng)建
            4、完成端口線程的工作原理
            5、Windows完成端口的實(shí)例代碼
            Linux的EPoll模型
            1、為什么select落后
            2、內(nèi)核中提高I/O性能的新方法epoll
            3、epoll的優(yōu)點(diǎn)
            4、epoll的工作模式 
            5、epoll的使用方法
            6、Linux下EPOll編程實(shí)例
            總結(jié)

            WINDOWS完成端口編程
                    摘要:開發(fā)網(wǎng)絡(luò)程序從來都不是一件容易的事情,盡管只需要遵守很少的一些規(guī)則;創(chuàng)建socket,發(fā)起連接,接受連接,發(fā)送和接受數(shù)據(jù)。真正的困難在于: 讓你的程序可以適應(yīng)從單單一個(gè)連接到幾千個(gè)連接乃至于上萬個(gè)連接。利用Windows平臺(tái)完成端口進(jìn)行重疊I/O的技術(shù)和Linux在2.6版本的內(nèi)核中 引入的EPOll技術(shù),可以很方便在在在Windows和Linux平臺(tái)上開發(fā)出支持大量連接的網(wǎng)絡(luò)服務(wù)程序。本文介紹在Windows和Linux平臺(tái) 上使用的完成端口和EPoll模型開發(fā)的基本原理,同時(shí)給出實(shí)際的例子。本文主要關(guān)注C/S結(jié)構(gòu)的服務(wù)器端程序,因?yàn)橐话銇碚f,開發(fā)一個(gè)大容量,具可擴(kuò)展 性的winsock程序一般就是指服務(wù)程序。

            1、基本概念
                設(shè)備---windows操作系統(tǒng)上允許通信的任何東西,比如文件、目錄、串行口、并行口、郵件槽、命名管道、無名管道、套接字、控制臺(tái)、邏輯磁盤、物理 磁盤等。絕大多數(shù)與設(shè)備打交道的函數(shù)都是CreateFile/ReadFile/WriteFile等。所以我們不能看到**File函數(shù)就只想到文件 設(shè)備。與設(shè)備通信有兩種方式,同步方式和異步方式。同步方式下,當(dāng)調(diào)用ReadFile函數(shù)時(shí),函數(shù)會(huì)等待系統(tǒng)執(zhí)行完所要求的工作,然后才返回;異步方式 下,ReadFile這類函數(shù)會(huì)直接返回,系統(tǒng)自己去完成對(duì)設(shè)備的操作,然后以某種方式通知完成操作。
            重疊I/O----顧名思義,當(dāng)你調(diào)用了某個(gè)函數(shù)(比如ReadFile)就立刻返回做自己的其他動(dòng)作的時(shí)候,同時(shí)系統(tǒng)也在對(duì)I/0設(shè)備進(jìn)行你要求的操 作,在這段時(shí)間內(nèi)你的程序和系統(tǒng)的內(nèi)部動(dòng)作是重疊的,因此有更好的性能。所以,重疊I/O是用于異步方式下使用I/O設(shè)備的。 重疊I/O需要使用的一個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu)OVERLAPPED。

            2、WINDOWS完成端口的特點(diǎn)
               Win32重疊I/O(Overlapped I/O)機(jī)制允許發(fā)起一個(gè)操作,然后在操作完成之后接受到信息。對(duì)于那種需要很長時(shí)間才能完成的操作來說,重疊IO機(jī)制尤其有用,因?yàn)榘l(fā)起重疊操作的線程 在重疊請(qǐng)求發(fā)出后就可以自由的做別的事情了。在WinNT和Win2000上,提供的真正的可擴(kuò)展的I/O模型就是使用完成端口(Completion Port)的重疊I/O.完成端口---是一種WINDOWS內(nèi)核對(duì)象。完成端口用于異步方式的重疊I/0情況下,當(dāng)然重疊I/O不一定非使用完成端口不 可,還有設(shè)備內(nèi)核對(duì)象、事件對(duì)象、告警I/0等。但是完成端口內(nèi)部提供了線程池的管理,可以避免反復(fù)創(chuàng)建線程的開銷,同時(shí)可以根據(jù)CPU的個(gè)數(shù)靈活的決定 線程個(gè)數(shù),而且可以讓減少線程調(diào)度的次數(shù)從而提高性能其實(shí)類似于WSAAsyncSelect和select函數(shù)的機(jī)制更容易兼容Unix,但是難以實(shí)現(xiàn) 我們想要的“擴(kuò)展性”。而且windows的完成端口機(jī)制在操作系統(tǒng)內(nèi)部已經(jīng)作了優(yōu)化,提供了更高的效率。所以,我們選擇完成端口開始我們的服務(wù)器程序的 開發(fā)。
            1、發(fā)起操作不一定完成,系統(tǒng)會(huì)在完成的時(shí)候通知你,通過用戶在完成端口上的等待,處理操作的結(jié)果。所以要有檢查完成端口,取操作結(jié)果的線程。在完成端口 上守候的線程系統(tǒng)有優(yōu)化,除非在執(zhí)行的線程阻塞,不會(huì)有新的線程被激活,以此來減少線程切換造成的性能代價(jià)。所以如果程序中沒有太多的阻塞操作,沒有必要 啟動(dòng)太多的線程,CPU數(shù)量的兩倍,一般這樣來啟動(dòng)線程。
            2、操作與相關(guān)數(shù)據(jù)的綁定方式:在提交數(shù)據(jù)的時(shí)候用戶對(duì)數(shù)據(jù)打相應(yīng)的標(biāo)記,記錄操作的類型,在用戶處理操作結(jié)果的時(shí)候,通過檢查自己打的標(biāo)記和系統(tǒng)的操作結(jié)果進(jìn)行相應(yīng)的處理。 
            3、操作返回的方式:一般操作完成后要通知程序進(jìn)行后續(xù)處理。但寫操作可以不通知用戶,此時(shí)如果用戶寫操作不能馬上完成,寫操作的相關(guān)數(shù)據(jù)會(huì)被暫存到到非 交換緩沖區(qū)中,在操作完成的時(shí)候,系統(tǒng)會(huì)自動(dòng)釋放緩沖區(qū)。此時(shí)發(fā)起完寫操作,使用的內(nèi)存就可以釋放了。此時(shí)如果占用非交換緩沖太多會(huì)使系統(tǒng)停止響應(yīng)。

            3、完成端口(Completion Ports )相關(guān)數(shù)據(jù)結(jié)構(gòu)和創(chuàng)建
                其實(shí)可以把完成端口看成系統(tǒng)維護(hù)的一個(gè)隊(duì)列,操作系統(tǒng)把重疊IO操作完成的事件通知放到該隊(duì)列里,由于是暴露 “操作完成”的事件通知,所以命名為“完成端口”(COmpletion Ports)。一個(gè)socket被創(chuàng)建后,可以在任何時(shí)刻和一個(gè)完成端口聯(lián)系起來。
            完成端口相關(guān)最重要的是OVERLAPPED數(shù)據(jù)結(jié)構(gòu)
            typedef struct _OVERLAPPED { 
                ULONG_PTR Internal;//被系統(tǒng)內(nèi)部賦值,用來表示系統(tǒng)狀態(tài) 
                ULONG_PTR InternalHigh;// 被系統(tǒng)內(nèi)部賦值,傳輸?shù)淖止?jié)數(shù) 
                union { 
                    struct { 
                        DWORD Offset;//和OffsetHigh合成一個(gè)64位的整數(shù),用來表示從文件頭部的多少字節(jié)開始 
                        DWORD OffsetHigh;//操作,如果不是對(duì)文件I/O來操作,則必須設(shè)定為0 
                    }; 
                    PVOID Pointer; 
                }; 
                HANDLE hEvent;//如果不使用,就務(wù)必設(shè)為0,否則請(qǐng)賦一個(gè)有效的Event句柄 
            } OVERLAPPED, *LPOVERLAPPED; 

            下面是異步方式使用ReadFile的一個(gè)例子 
            OVERLAPPED Overlapped; 
            Overlapped.Offset=345; 
            Overlapped.OffsetHigh=0; 
            Overlapped.hEvent=0; 
            //假定其他參數(shù)都已經(jīng)被初始化 
            ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped); 
            這樣就完成了異步方式讀文件的操作,然后ReadFile函數(shù)返回,由操作系統(tǒng)做自己的事情,下面介紹幾個(gè)與OVERLAPPED結(jié)構(gòu)相關(guān)的函數(shù) 
            等待重疊I/0操作完成的函數(shù) 
            BOOL GetOverlappedResult (
            HANDLE hFile,
            LPOVERLAPPED lpOverlapped,//接受返回的重疊I/0結(jié)構(gòu)
            LPDWORD lpcbTransfer,//成功傳輸了多少字節(jié)數(shù)
            BOOL fWait //TRUE只有當(dāng)操作完成才返回,F(xiàn)ALSE直接返回,如果操作沒有完成,通過調(diào)//用GetLastError ( )函數(shù)會(huì)返回ERROR_IO_INCOMPLETE 
            );
            宏HasOverlappedIoCompleted可以幫助我們測(cè)試重疊I/0操作是否完成,該宏對(duì)OVERLAPPED結(jié)構(gòu)的Internal成員進(jìn)行了測(cè)試,查看是否等于STATUS_PENDING值。

                    一般來說,一個(gè)應(yīng)用程序可以創(chuàng)建多個(gè)工作線程來處理完成端口上的通知事件。工作線程的數(shù)量依賴于程序的具體需要。但是在理想的情況下,應(yīng)該對(duì)應(yīng)一個(gè)CPU 創(chuàng)建一個(gè)線程。因?yàn)樵谕瓿啥丝诶硐肽P椭?,每個(gè)線程都可以從系統(tǒng)獲得一個(gè)“原子”性的時(shí)間片,輪番運(yùn)行并檢查完成端口,線程的切換是額外的開銷。在實(shí)際開 發(fā)的時(shí)候,還要考慮這些線程是否牽涉到其他堵塞操作的情況。如果某線程進(jìn)行堵塞操作,系統(tǒng)則將其掛起,讓別的線程獲得運(yùn)行時(shí)間。因此,如果有這樣的情況, 可以多創(chuàng)建幾個(gè)線程來盡量利用時(shí)間。
            應(yīng)用完成端口:
                創(chuàng)建完成端口:完成端口是一個(gè)內(nèi)核對(duì)象,使用時(shí)他總是要和至少一個(gè)有效的設(shè)備句柄進(jìn)行關(guān)聯(lián),完成端口是一個(gè)復(fù)雜的內(nèi)核對(duì)象,創(chuàng)建它的函數(shù)是:
            HANDLE CreateIoCompletionPort( 
                IN HANDLE FileHandle, 
                IN HANDLE ExistingCompletionPort, 
                IN ULONG_PTR CompletionKey, 
                IN DWORD NumberOfConcurrentThreads 
                ); 

            通常創(chuàng)建工作分兩步:
            第一步,創(chuàng)建一個(gè)新的完成端口內(nèi)核對(duì)象,可以使用下面的函數(shù):
                   HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads) 

                      return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
            };
                   
            第二步,將剛創(chuàng)建的完成端口和一個(gè)有效的設(shè)備句柄關(guān)聯(lián)起來,可以使用下面的函數(shù):
                   bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey) 

                      HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0); 
                      return h==hCompPort; 
            }; 
            說明 
            1) CreateIoCompletionPort函數(shù)也可以一次性的既創(chuàng)建完成端口對(duì)象,又關(guān)聯(lián)到一個(gè)有效的設(shè)備句柄 
            2) CompletionKey是一個(gè)可以自己定義的參數(shù),我們可以把一個(gè)結(jié)構(gòu)的地址賦給它,然后在合適的時(shí)候取出來使用,最好要保證結(jié)構(gòu)里面的內(nèi)存不是分配在棧上,除非你有十分的把握內(nèi)存會(huì)保留到你要使用的那一刻。
            3) NumberOfConcurrentThreads通常用來指定要允許同時(shí)運(yùn)行的的線程的最大個(gè)數(shù)。通常我們指定為0,這樣系統(tǒng)會(huì)根據(jù)CPU的個(gè)數(shù)來自 動(dòng)確定。創(chuàng)建和關(guān)聯(lián)的動(dòng)作完成后,系統(tǒng)會(huì)將完成端口關(guān)聯(lián)的設(shè)備句柄、完成鍵作為一條紀(jì)錄加入到這個(gè)完成端口的設(shè)備列表中。如果你有多個(gè)完成端口,就會(huì)有多 個(gè)對(duì)應(yīng)的設(shè)備列表。如果設(shè)備句柄被關(guān)閉,則表中自動(dòng)刪除該紀(jì)錄。

            4、完成端口線程的工作原理

            完成端口可以幫助我們管理線程池,但是線程池中的線程需要我們使用_beginthreadex來創(chuàng)建,憑什么通知完成端口管理我們的新線程呢?答案在函數(shù)GetQueuedCompletionStatus。該函數(shù)原型: 
            BOOL GetQueuedCompletionStatus( 
                IN HANDLE CompletionPort, 
                OUT LPDWORD lpNumberOfBytesTransferred, 
                OUT PULONG_PTR lpCompletionKey, 
                OUT LPOVERLAPPED *lpOverlapped, 
                IN DWORD dwMilliseconds 
            ); 
            這個(gè)函數(shù)試圖從指定的完成端口的I/0完成隊(duì)列中抽取紀(jì)錄。只有當(dāng)重疊I/O動(dòng)作完成的時(shí)候,完成隊(duì)列中才有紀(jì)錄。凡是調(diào)用這個(gè)函數(shù)的線程將被放入到完成 端口的等待線程隊(duì)列中,因此完成端口就可以在自己的線程池中幫助我們維護(hù)這個(gè)線程。完成端口的I/0完成隊(duì)列中存放了當(dāng)重疊I/0完成的結(jié)果---- 一條紀(jì)錄,該紀(jì)錄擁有四個(gè)字段,前三項(xiàng)就對(duì)應(yīng)GetQueuedCompletionStatus函數(shù)的2、3、4參數(shù),最后一個(gè)字段是錯(cuò)誤信息 dwError。我們也可以通過調(diào)用PostQueudCompletionStatus模擬完成了一個(gè)重疊I/0操作。 
            當(dāng)I/0完成隊(duì)列中出現(xiàn)了紀(jì)錄,完成端口將會(huì)檢查等待線程隊(duì)列,該隊(duì)列中的線程都是通過調(diào)用GetQueuedCompletionStatus函數(shù)使自 己加入隊(duì)列的。等待線程隊(duì)列很簡(jiǎn)單,只是保存了這些線程的ID。完成端口會(huì)按照后進(jìn)先出的原則將一個(gè)線程隊(duì)列的ID放入到釋放線程列表中,同時(shí)該線程將從 等待GetQueuedCompletionStatus函數(shù)返回的睡眠狀態(tài)中變?yōu)榭烧{(diào)度狀態(tài)等待CPU的調(diào)度。所以我們的線程要想成為完成端口管理的線 程,就必須要調(diào)用GetQueuedCompletionStatus函數(shù)。出于性能的優(yōu)化,實(shí)際上完成端口還維護(hù)了一個(gè)暫停線程列表,具體細(xì)節(jié)可以參考 《Windows高級(jí)編程指南》,我們現(xiàn)在知道的知識(shí),已經(jīng)足夠了。 完成端口線程間數(shù)據(jù)傳遞線程間傳遞數(shù)據(jù)最常用的辦法是在_beginthreadex函數(shù)中將參數(shù)傳遞給線程函數(shù),或者使用全局變量。但是完成端口還有自 己的傳遞數(shù)據(jù)的方法,答案就在于CompletionKey和OVERLAPPED參數(shù)。
            CompletionKey被保存在完成端口的設(shè)備表中,是和設(shè)備句柄一一對(duì)應(yīng)的,我們可以將與設(shè)備句柄相關(guān)的數(shù)據(jù)保存到CompletionKey中, 或者將CompletionKey表示為結(jié)構(gòu)指針,這樣就可以傳遞更加豐富的內(nèi)容。這些內(nèi)容只能在一開始關(guān)聯(lián)完成端口和設(shè)備句柄的時(shí)候做,因此不能在以后 動(dòng)態(tài)改變。
            OVERLAPPED參數(shù)是在每次調(diào)用ReadFile這樣的支持重疊I/0的函數(shù)時(shí)傳遞給完成端口的。我們可以看到,如果我們不是對(duì)文件設(shè)備做操作,該 結(jié)構(gòu)的成員變量就對(duì)我們幾乎毫無作用。我們需要附加信息,可以創(chuàng)建自己的結(jié)構(gòu),然后將OVERLAPPED結(jié)構(gòu)變量作為我們結(jié)構(gòu)變量的第一個(gè)成員,然后傳 遞第一個(gè)成員變量的地址給ReadFile函數(shù)。因?yàn)轭愋推ヅ洌?dāng)然可以通過編譯。當(dāng)GetQueuedCompletionStatus函數(shù)返回時(shí),我 們可以獲取到第一個(gè)成員變量的地址,然后一個(gè)簡(jiǎn)單的強(qiáng)制轉(zhuǎn)換,我們就可以把它當(dāng)作完整的自定義結(jié)構(gòu)的指針使用,這樣就可以傳遞很多附加的數(shù)據(jù)了。太好了! 只有一點(diǎn)要注意,如果跨線程傳遞,請(qǐng)注意將數(shù)據(jù)分配到堆上,并且接收端應(yīng)該將數(shù)據(jù)用完后釋放。我們通常需要將ReadFile這樣的異步函數(shù)的所需要的緩 沖區(qū)放到我們自定義的結(jié)構(gòu)中,這樣當(dāng)GetQueuedCompletionStatus被返回時(shí),我們的自定義結(jié)構(gòu)的緩沖區(qū)變量中就存放了I/0操作的 數(shù)據(jù)。CompletionKey和OVERLAPPED參數(shù),都可以通過GetQueuedCompletionStatus函數(shù)獲得。
            線程的安全退出
                   很多線程為了不止一次的執(zhí)行異步數(shù)據(jù)處理,需要使用如下語句
            while (true)
            {
                   ......
                   GetQueuedCompletionStatus(...); 
                    ......
            }
            那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函數(shù),我們可以用它發(fā)送一個(gè)自定義的包含了OVERLAPPED成員變量的結(jié)構(gòu)地址,里面包含一個(gè)狀態(tài)變量,當(dāng)狀態(tài)變量為退出標(biāo)志時(shí),線程就執(zhí)行清除動(dòng)作然后退出。

            5、Windows完成端口的實(shí)例代碼:
            DWORD WINAPI WorkerThread(LPVOID lpParam)

            ULONG_PTR *PerHandleKey;
            OVERLAPPED *Overlap;
            OVERLAPPEDPLUS *OverlapPlus,
            *newolp;
            DWORD dwBytesXfered;
            while (1)
            {
            ret = GetQueuedCompletionStatus(
            hIocp,
            &dwBytesXfered,
            (PULONG_PTR)&PerHandleKey,
            &Overlap,
            INFINITE);
            if (ret == 0)
            {
            // Operation failed
            continue;
            }
            OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
            switch (OverlapPlus->OpCode)
            {
            case OP_ACCEPT:
            // Client socket is contained in OverlapPlus.sclient
            // Add client to completion port
            CreateIoCompletionPort(
            (HANDLE)OverlapPlus->sclient,
            hIocp,
            (ULONG_PTR)0,
            0);
            // Need a new OVERLAPPEDPLUS structure
            // for the newly accepted socket. Perhaps
            // keep a look aside list of free structures.
            newolp = AllocateOverlappedPlus();
            if (!newolp)
            {
            // Error
            }
            newolp->s = OverlapPlus->sclient;
            newolp->OpCode = OP_READ;
            // This function divpares the data to be sent
            PrepareSendBuffer(&newolp->wbuf);
            ret = WSASend(
            newolp->s,
            &newolp->wbuf,
            1,
            &newolp->dwBytes,
            0,
            &newolp.ol,
            NULL);
            if (ret == SOCKET_ERROR)
            {
            if (WSAGetLastError() != WSA_IO_PENDING)
            {
            // Error
            }
            }
            // Put structure in look aside list for later use
            FreeOverlappedPlus(OverlapPlus);
            // Signal accept thread to issue another AcceptEx
            SetEvent(hAcceptThread);
            break;
            case OP_READ:
            // Process the data read 
            // Repost the read if necessary, reusing the same
            // receive buffer as before
            memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
            ret = WSARecv(
            OverlapPlus->s,
            &OverlapPlus->wbuf,
            1,
            &OverlapPlus->dwBytes,
            &OverlapPlus->dwFlags,
            &OverlapPlus->ol,
            NULL);
            if (ret == SOCKET_ERROR)
            {
            if (WSAGetLastError() != WSA_IO_PENDING)
            {
            // Error
            }
            }
            break;
            case OP_WRITE:
            // Process the data sent, etc.
            break;
            } // switch
            } // while
            } // WorkerThread
             

            查看以上代碼,注意如果Overlapped操作立刻失?。ū热纾祷豐OCKET_ERROR或其他非WSA_IO_PENDING的錯(cuò)誤),則 沒有任何完成通知時(shí)間會(huì)被放到完成端口隊(duì)列里。反之,則一定有相應(yīng)的通知時(shí)間被放到完成端口隊(duì)列。更完善的關(guān)于Winsock的完成端口機(jī)制,可以參考 MSDN的Microsoft PlatFormSDK,那里有完成端口的例子。訪問http://msdn.microsoft.com/library/techart/msdn_servrapp.htm可以獲得更多信息。

            Linux的EPoll模型
            Linux 2.6內(nèi)核中提高網(wǎng)絡(luò)I/O性能的新方法-epoll I/O多路復(fù)用技術(shù)在比較多的TCP網(wǎng)絡(luò)服務(wù)器中有使用,即比較多的用到select函數(shù)。

            1、為什么select落后
            首先,在Linux內(nèi)核中,select所用到的FD_SET是有限的,即內(nèi)核中有個(gè)參數(shù)__FD_SETSIZE定義了每個(gè)FD_SET的句柄個(gè)數(shù),在我用的2.6.15-25-386內(nèi)核中,該值是1024,搜索內(nèi)核源代碼得到:
            include/linux/posix_types.h:#define __FD_SETSIZE         1024
            也就是說,如果想要同時(shí)檢測(cè)1025個(gè)句柄的可讀狀態(tài)是不可能用select實(shí)現(xiàn)的?;蛘咄瑫r(shí)檢測(cè)1025個(gè)句柄的可寫狀態(tài)也是不可能的。其次,內(nèi)核中實(shí) 現(xiàn)select是用輪詢方法,即每次檢測(cè)都會(huì)遍歷所有FD_SET中的句柄,顯然,select函數(shù)執(zhí)行時(shí)間與FD_SET中的句柄個(gè)數(shù)有一個(gè)比例關(guān)系, 即select要檢測(cè)的句柄數(shù)越多就會(huì)越費(fèi)時(shí)。當(dāng)然,在前文中我并沒有提及poll方法,事實(shí)上用select的朋友一定也試過poll,我個(gè)人覺得 select和poll大同小異,個(gè)人偏好于用select而已。

            2、內(nèi)核中提高I/O性能的新方法epoll
            epoll是什么?按照man手冊(cè)的說法:是為處理大批量句柄而作了改進(jìn)的poll。要使用epoll只需要這三個(gè)系統(tǒng)調(diào)用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。
            當(dāng)然,這不是2.6內(nèi)核才有的,它是在2.5.44內(nèi)核中被引進(jìn)的(epoll(4) is a new API introduced in Linux kernel 2.5.44)

            Linux2.6內(nèi)核epoll介紹
            先介紹2本書《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4內(nèi)核講解Linux TCP/IP實(shí)現(xiàn),相當(dāng)不錯(cuò).作為一個(gè)現(xiàn)實(shí)世界中的實(shí)現(xiàn),很多時(shí)候你必須作很多權(quán)衡,這時(shí)候參考一個(gè)久經(jīng)考驗(yàn)的系統(tǒng)更有實(shí)際意義。舉個(gè)例子,linux內(nèi) 核中sk_buff結(jié)構(gòu)為了追求速度和安全,犧牲了部分內(nèi)存,所以在發(fā)送TCP包的時(shí)候,無論應(yīng)用層數(shù)據(jù)多大,sk_buff最小也有272的字節(jié).其實(shí) 對(duì)于socket應(yīng)用層程序來說,另外一本書《UNIX Network Programming Volume 1》意義更大一點(diǎn).2003年的時(shí)候,這本書出了最新的第3版本,不過主要還是修訂第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens給出了網(wǎng)絡(luò)IO的基本模型。在這里最重要的莫過于select模型和Asynchronous I/O模型.從理論上說,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告訴你IO操作完成。但是一直以來,如何實(shí)現(xiàn)就沒有一個(gè)完美的方 案。最著名的windows完成端口實(shí)現(xiàn)的AIO,實(shí)際上也是內(nèi)部用線程池實(shí)現(xiàn)的罷了,最后的結(jié)果是IO有個(gè)線程池,你應(yīng)用也需要一個(gè)線程池...... 很多文檔其實(shí)已經(jīng)指出了這帶來的線程context-switch帶來的代價(jià)。在linux 平臺(tái)上,關(guān)于網(wǎng)絡(luò)AIO一直是改動(dòng)最多的地方,2.4的年代就有很多AIO內(nèi)核patch,最著名的應(yīng)該算是SGI那個(gè)。但是一直到2.6內(nèi)核發(fā)布,網(wǎng)絡(luò) 模塊的AIO一直沒有進(jìn)入穩(wěn)定內(nèi)核版本(大部分都是使用用戶線程模擬方法,在使用了NPTL的linux上面其實(shí)和windows的完成端口基本上差不多 了)。2.6內(nèi)核所支持的AIO特指磁盤的AIO---支持io_submit(),io_getevents()以及對(duì)Direct IO的支持(就是繞過VFS系統(tǒng)buffer直接寫硬盤,對(duì)于流服務(wù)器在內(nèi)存平穩(wěn)性上有相當(dāng)幫助)。
            所以,剩下的select模型基本上就是我們?cè)趌inux上面的唯一選擇,其實(shí),如果加上no-block socket的配置,可以完成一個(gè)"偽"AIO的實(shí)現(xiàn),只不過推動(dòng)力在于你而不是os而已。不過傳統(tǒng)的select/poll函數(shù)有著一些無法忍受的缺 點(diǎn),所以改進(jìn)一直是2.4-2.5開發(fā)版本內(nèi)核的任務(wù),包括/dev/poll,realtime signal等等。最終,Davide Libenzi開發(fā)的epoll進(jìn)入2.6內(nèi)核成為正式的解決方案

            3、epoll的優(yōu)點(diǎn)

            <1>支持一個(gè)進(jìn)程打開大數(shù)目的socket描述符(FD)
            select 最不能忍受的是一個(gè)進(jìn)程所打開的FD是有一定限制的,由FD_SETSIZE設(shè)置,默認(rèn)值是2048。對(duì)于那些需要支持的上萬連接數(shù)目的IM服務(wù)器來說顯 然太少了。這時(shí)候你一是可以選擇修改這個(gè)宏然后重新編譯內(nèi)核,不過資料也同時(shí)指出這樣會(huì)帶來網(wǎng)絡(luò)效率的下降,二是可以選擇多進(jìn)程的解決方案(傳統(tǒng)的 Apache方案),不過雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小,但仍舊是不可忽視的,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完 美的方案。不過 epoll則沒有這個(gè)限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左 右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
            <2>IO效率不隨FD數(shù)目增加而線性下降
            傳統(tǒng)的select/poll另一個(gè)致命弱點(diǎn)就是當(dāng)你擁有一個(gè)很大的socket集合,不過由于網(wǎng)絡(luò)延時(shí),任一時(shí)間只有部分的socket是"活躍"的, 但是select/poll每次調(diào)用都會(huì)線性掃描全部的集合,導(dǎo)致效率呈現(xiàn)線性下降。但是epoll不存在這個(gè)問題,它只會(huì)對(duì)"活躍"的socket進(jìn)行 操作---這是因?yàn)樵趦?nèi)核實(shí)現(xiàn)中epoll是根據(jù)每個(gè)fd上面的callback函數(shù)實(shí)現(xiàn)的。那么,只有"活躍"的socket才會(huì)主動(dòng)的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會(huì),在這點(diǎn)上,epoll實(shí)現(xiàn)了一個(gè)"偽"AIO,因?yàn)檫@時(shí)候推動(dòng)力在os內(nèi)核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個(gè)高速LAN環(huán)境,epoll并不比select/poll有什么效率,相 反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠(yuǎn)在select/poll之上了。
            <3>使用mmap加速內(nèi)核與用戶空間的消息傳遞。
            這點(diǎn)實(shí)際上涉及到epoll的具體實(shí)現(xiàn)了。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就 很重要,在這點(diǎn)上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。而如果你想我一樣從2.5內(nèi)核就關(guān)注epoll的話,一定不會(huì)忘記手工 mmap這一步的。
            <4>內(nèi)核微調(diào)
            這一點(diǎn)其實(shí)不算epoll的優(yōu)點(diǎn)了,而是整個(gè)linux平臺(tái)的優(yōu)點(diǎn)。也許你可以懷疑linux平臺(tái),但是你無法回避linux平臺(tái)賦予你微調(diào)內(nèi)核的能力。 比如,內(nèi)核TCP/IP協(xié)議棧使用內(nèi)存池管理sk_buff結(jié)構(gòu),那么可以在運(yùn)行時(shí)期動(dòng)態(tài)調(diào)整這個(gè)內(nèi)存pool(skb_head_pool)的大小 --- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數(shù)的第2個(gè)參數(shù)(TCP完成3次握手 的數(shù)據(jù)包隊(duì)列長度),也可以根據(jù)你平臺(tái)內(nèi)存大小動(dòng)態(tài)調(diào)整。更甚至在一個(gè)數(shù)據(jù)包面數(shù)目巨大但同時(shí)每個(gè)數(shù)據(jù)包本身大小卻很小的特殊系統(tǒng)上嘗試最新的NAPI網(wǎng) 卡驅(qū)動(dòng)架構(gòu)。
            4、epoll的工作模式
            令人高興的是,2.6內(nèi)核的epoll比其2.5開發(fā)版本的/dev/epoll簡(jiǎn)潔了許多,所以,大部分情況下,強(qiáng)大的東西往往是簡(jiǎn)單的。唯一有點(diǎn)麻煩是epoll有2種工作方式:LT和ET。
            LT(level triggered)是缺省的工作方式,并且同時(shí)支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個(gè)文件描述符是否就緒了,然后你可以對(duì)這個(gè)就緒的fd進(jìn)行IO操作。如果你不作任何操作,內(nèi)核還是會(huì)繼續(xù)通知你 的,所以,這種模式編程出錯(cuò)誤可能性要小一點(diǎn)。傳統(tǒng)的select/poll都是這種模型的代表.
            ET (edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當(dāng)描述符從未就緒變?yōu)榫途w時(shí),內(nèi)核通過epoll告訴你。然后它會(huì)假設(shè)你知道文件描述符已經(jīng)就緒,并且不會(huì)再為那個(gè)文件描述 符發(fā)送更多的就緒通知,直到你做了某些操作導(dǎo)致那個(gè)文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請(qǐng)求,或者發(fā)送接收的數(shù)據(jù)少于一定量時(shí)導(dǎo)致 了一個(gè)EWOULDBLOCK 錯(cuò)誤)。但是請(qǐng)注意,如果一直不對(duì)這個(gè)fd作IO操作(從而導(dǎo)致它再次變成未就緒),內(nèi)核不會(huì)發(fā)送更多的通知(only once),不過在TCP協(xié)議中,ET模式的加速效用仍需要更多的benchmark確認(rèn)。
            epoll只有epoll_create,epoll_ctl,epoll_wait 3個(gè)系統(tǒng)調(diào)用,具體用法請(qǐng)參考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一個(gè)完整的例子,大家一看就知道如何使用了
            Leader/follower模式線程pool實(shí)現(xiàn),以及和epoll的配合。

            5、 epoll的使用方法
                首先通過create_epoll(int maxfds)來創(chuàng)建一個(gè)epoll的句柄,其中maxfds為你epoll所支持的最大句柄數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)新的epoll句柄,之后的所有操作 將通過這個(gè)句柄來進(jìn)行操作。在用完之后,記得用close()來關(guān)閉這個(gè)創(chuàng)建出來的epoll句柄。 之后在你的網(wǎng)絡(luò)主循環(huán)里面,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網(wǎng)絡(luò)接口,看哪一個(gè)可以讀,哪一個(gè)可以寫了。基本的語法為: 
            nfds = epoll_wait(kdpfd, events, maxevents, -1); 
            其中kdpfd為用epoll_create創(chuàng)建之后的句柄,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成 功之后,epoll_events里面將儲(chǔ)存所有的讀寫事件。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。最后一個(gè)timeout是 epoll_wait的超時(shí),為0的時(shí)候表示馬上返回,為-1的時(shí)候表示一直等下去,直到有事件范圍,為任意正整數(shù)的時(shí)候表示等這么長的時(shí)間,如果一直沒 有事件,則范圍。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個(gè)線程的話,則可以用0來保證主循環(huán) 的效率。

            epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán),遍利所有的事件: 
            for(n = 0; n < nfds; ++n) { 
                            if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接進(jìn)入了,進(jìn)行新連接的處理。 
                                client = accept(listener, (struct sockaddr *) &local, 
                                                &addrlen); 
                                if(client < 0){ 
                                    perror("accept"); 
                                    continue; 
                                } 
                                setnonblocking(client); // 將新連接置于非阻塞模式 
                                ev.events = EPOLLIN | EPOLLET; // 并且將新連接也加入EPOLL的監(jiān)聽隊(duì)列。 
            注意,這里的參數(shù)EPOLLIN | EPOLLET并沒有設(shè)置對(duì)寫socket的監(jiān)聽,如果有寫操作的話,這個(gè)時(shí)候epoll是不會(huì)返回事件的,如果要對(duì)寫操作也監(jiān)聽的話,應(yīng)該是EPOLLIN | EPOLLOUT | EPOLLET 
                                ev.data.fd = client; 
                                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) { 
            // 設(shè)置好event之后,將這個(gè)新的event通過epoll_ctl加入到epoll的監(jiān)聽隊(duì)列里面,這里用EPOLL_CTL_ADD來加一個(gè)新的 epoll事件,通過EPOLL_CTL_DEL來減少一個(gè)epoll事件,通過EPOLL_CTL_MOD來改變一個(gè)事件的監(jiān)聽方式。 
                                    fprintf(stderr, "epoll set insertion error: fd=%d0, 
                                            client); 
                                    return -1; 
                                } 
                            } 
                            else // 如果不是主socket的事件的話,則代表是一個(gè)用戶socket的事件,則來處理這個(gè)用戶socket的事情,比如說read(fd,xxx)之類的,或者一些其他的處理。 
                                do_use_fd(events[n].data.fd); 
            }

            對(duì),epoll的操作就這么簡(jiǎn)單,總共不過4個(gè)API:epoll_create, epoll_ctl, epoll_wait和close。 
            如果您對(duì)epoll的效率還不太了解,請(qǐng)參考我之前關(guān)于網(wǎng)絡(luò)游戲的網(wǎng)絡(luò)編程等相關(guān)的文章。


            以前公司的服務(wù)器都是使用HTTP連接,但是這樣的話,在手機(jī)目前的網(wǎng)絡(luò)情況下不但顯得速度較慢,而且不穩(wěn)定。因此大家一致同意用SOCKET來進(jìn)行連 接。雖然使用SOCKET之后,對(duì)于用戶的費(fèi)用可能會(huì)增加(由于是用了CMNET而非CMWAP),但是,秉著用戶體驗(yàn)至上的原則,相信大家還是能夠接受 的(希望那些玩家月末收到帳單不后能夠保持克制...)。
            這次的服務(wù)器設(shè)計(jì)中,最重要的一個(gè)突破,是使用了EPOLL模型,雖然對(duì)之也是一知半解,但是既然在各大PC網(wǎng)游中已經(jīng)經(jīng)過了如此嚴(yán)酷的考驗(yàn),相信他不會(huì)讓我們失望,使用后的結(jié)果,確實(shí)也是表現(xiàn)相當(dāng)不錯(cuò)。在這里,我還是主要大致介紹一下這個(gè)模型的結(jié)構(gòu)。
            6、Linux下EPOll編程實(shí)例
            EPOLL模型似乎只有一種格式,所以大家只要參考我下面的代碼,就能夠?qū)POLL有所了解了,代碼的解釋都已經(jīng)在注釋中:

            while (TRUE)
            {
            int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL時(shí)間的發(fā)生,相當(dāng)于監(jiān)聽,至于相關(guān)的端口,需要在初始化EPOLL的時(shí)候綁定。
            if (nfds <= 0)
            continue;
            m_bOnTimeChecking = FALSE;
            G_CurTime = time(NULL);
            for (int i=0; i
            {
            try
            {
            if (m_events[i].data.fd == m_listen_http_fd)//如果新監(jiān)測(cè)到一個(gè)HTTP用戶連接到綁定的HTTP端口,建立新的連接。由于我們新采用了SOCKET連接,所以基本沒用。
            {
            OnAcceptHttpEpoll ();
            }
            else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監(jiān)測(cè)到一個(gè)SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。
            {
            OnAcceptSockEpoll ();
            }
            else if (m_events[i].events & EPOLLIN)//如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù),那么進(jìn)行讀入。
            {
            OnReadEpoll (i);
            }

            OnWriteEpoll (i);//查看當(dāng)前的活動(dòng)連接是否有需要寫出的數(shù)據(jù)。
            }
            catch (int)
            {
            PRINTF ("CATCH捕獲錯(cuò)誤\n");
            continue;
            }
            }
            m_bOnTimeChecking = TRUE;
            OnTimer ();//進(jìn)行一些定時(shí)的操作,主要就是刪除一些短線用戶等。
            }
             其實(shí)EPOLL的精華,也就是上述的幾段短短的代碼,看來時(shí)代真的不同了,以前如何接受大量用戶連接的問題,現(xiàn)在卻被如此輕松的搞定,真是讓人不得不感嘆,對(duì)哪。


            總結(jié)
            Windows完成端口與Linux epoll技術(shù)方案是這2個(gè)平臺(tái)上實(shí)現(xiàn)異步IO和設(shè)計(jì)開發(fā)一個(gè)大容量,具可擴(kuò)展性的winsock程序指服務(wù)程序的很好的選擇,本文對(duì)這2中技術(shù)的實(shí)現(xiàn)原理和實(shí)際的使用方法做了一個(gè)詳細(xì)的介紹。

            轉(zhuǎn)自:

            http://blog.chinaunix.net/u2/67780/showart_2057153.html

            posted on 2009-11-24 15:28 chatler 閱讀(323) 評(píng)論(0)  編輯 收藏 引用 所屬分類: Socket
            <2010年4月>
            28293031123
            45678910
            11121314151617
            18192021222324
            2526272829301
            2345678

            常用鏈接

            留言簿(10)

            隨筆分類(307)

            隨筆檔案(297)

            algorithm

            Books_Free_Online

            C++

            database

            Linux

            Linux shell

            linux socket

            misce

            • cloudward
            • 感覺這個(gè)博客還是不錯(cuò),雖然做的東西和我不大相關(guān),覺得看看還是有好處的

            network

            OSS

            • Google Android
            • Android is a software stack for mobile devices that includes an operating system, middleware and key applications. This early look at the Android SDK provides the tools and APIs necessary to begin developing applications on the Android platform using the Java programming language.
            • os161 file list

            overall

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            色综合久久无码中文字幕| 精品久久久久久中文字幕| 久久久久久毛片免费看| 久久本道久久综合伊人| 久久伊人精品一区二区三区| 久久精品国产99国产精品导航 | 久久精品国产亚洲欧美| 91精品国产91久久久久久蜜臀| 久久免费小视频| 亚洲欧美成人综合久久久| 国产精品欧美久久久天天影视| 伊人 久久 精品| 久久狠狠色狠狠色综合| 伊人情人综合成人久久网小说| 无码人妻精品一区二区三区久久| 亚洲国产精品久久66| 国产亚洲精久久久久久无码77777| 国产AⅤ精品一区二区三区久久| 国产成人久久精品一区二区三区| 久久99精品久久久久久野外| 99久久综合狠狠综合久久止| 精品一二三区久久aaa片| 少妇久久久久久被弄到高潮| 久久精品一区二区三区不卡| 无码久久精品国产亚洲Av影片| 日本精品久久久久影院日本| 精品久久久无码中文字幕天天| 97久久精品无码一区二区| 麻豆一区二区99久久久久| 久久精品国产久精国产一老狼| 无码8090精品久久一区| 久久伊人五月天论坛| 热RE99久久精品国产66热| 久久天天躁狠狠躁夜夜2020老熟妇 | 久久91精品国产91久久户| 久久久精品人妻一区二区三区蜜桃| 热re99久久6国产精品免费| 亚洲精品高清一二区久久| 亚州日韩精品专区久久久| 国产精品久久久天天影视香蕉| 99久久国产亚洲高清观看2024|