epoll用法【整理】

l  epoll是什麼?程序員

epoll是當前在Linux下開發大規模併發網絡程序的熱門人選,epoll 在Linux2.6內核中正式引入,和select類似,都是I/O多路複用(IO multiplexing)技術數組

Linux下設計併發網絡程序,經常使用的模型有:服務器

  • Apache模型(Process Per Connection,簡稱PPC)
  • TPC(Thread PerConnection)模型
  • select模型和poll模型。
  • epoll模型

l  經常使用模型的缺點網絡

n  PPC/TPC模型數據結構

這兩種模型思想相似,就是讓每個到來的鏈接都有一個進程/線程來服務。這種模型的代價是它要時間和空間。鏈接較多時,進程/線程切換的開銷比較大。所以這類模型能接受的最大鏈接數都不會高,通常在幾百個左右。併發

n  select模型socket

  • 最大併發數限制:由於一個進程所打開的fd(文件描述符)是有限制的,由FD_SETSIZE設置,默認值是1024/2048,所以select模型的最大併發數就被相應限制了。
  • 效率問題:select每次調用都會線性掃描所有的fd集合,這樣效率就會呈現線性降低,把FD_SETSIZE改大可能形成這些fd都超時了。
  • 內核/用戶空間內存拷貝問題:如何讓內核把fd消息通知給用戶空間呢?在這個問題上select採起了內存拷貝方法。

n  poll模型函數

  • 基本上效率和select是相同的,select缺點的2和3它都沒有改掉。

l  epoll的改進oop

對比其餘模型的問題,epoll的改進以下:測試

n  epoll沒有最大併發鏈接的限制,上限是最大能夠打開文件的數目,這個數字通常遠大於2048, 通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。

n  效率提高,Epoll最大的優勢就在於它只管你活躍的鏈接,而跟鏈接總數無關,所以在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。

n  內存拷貝,Epoll在這點上使用了「共享內存」,這個內存拷貝也省略了。

 

l  epoll爲何高效

epoll的高效和其數據結構的設計是密不可分的。

首先回憶一下select模型,當有I/O事件到來時,select通知應用程序有事件到了,應用程序必須輪詢全部的fd集合,測試每一個fd是否有事件發生,並處理事件;代碼像下面這樣:

int  res = select(maxfd+1, &readfds, NULL, NULL, 120);

if (res > 0)

{

    for (int i = 0; i < MAX_CONNECTION; i++)

    {

        if (FD_ISSET(allConnection[i],&readfds))

        {

            handleEvent(allConnection[i]);

        }

    }

}

// if(res == 0) handle timeout, res < 0 handle error

 

epoll不只會告訴應用程序有I/0事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,所以根據這些信息應用程序就能直接定位到事件,而沒必要遍歷整個fd集合。

int res = epoll_wait(epfd, events, 20, 120);

for (int i = 0; i < res;i++)

{

    handleEvent(events[n]);

}

 

l  epoll關鍵數據結構

前面提到epoll速度快和其數據結構密不可分,其關鍵數據結構就是:

struct  epoll_event {

   __uint32_t      events;  //epoll events

   epoll_data_t   data;   //user data variable

};

 

typedef  union epoll_data {

   void*   ptr;

   int      fd;

   __uint32_t  u32;

   __uint64_t  u64;

}epoll_data_t;

可見epoll_data是一個union結構體,藉助於它應用程序能夠保存不少類型的信息:fd、指針等等。有了它,應用程序就能夠直接定位目標了。

 

l  使用epoll

 

epollAPI:

 

 

#include  <sys/epoll.h>

int  epoll_create(int  size);

 

int  epoll_ctl(int epfd, int op, int fd, structepoll_event *event);

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

 

  • int  epoll_create(int size);

 

建立一個epoll的文件描述符,參數size告訴內核這個監聽的數目共有多大。

 

  • int  epoll_ctl(int epfd, int op, int fd, structepoll_event *event);

epoll的事件註冊函數。

參數epfd是epoll_create返回值。

參數op爲

   EPOLL_CTL_ADD 註冊新的fd到epfd中

   EPOLL_CTL_MOD 修改已經註冊的fd的監聽事件

   EPOLL_CTL_DEL 從epfd中刪除一個fd

參數fd是須要監聽文件描述符。

參數event是告訴內核須要監聽什麼事件。event->events的不一樣的值表示對應的文件描述符的不一樣事件:

   EPOLLIN  能夠讀(包括對端Socket正常關閉)

   EPOLLOUT 能夠寫

   EPOLLPRI有緊急的數據可讀(有帶外數據OOB到來,TCP中的URG包)

   EPOLLERR該文件描述符發生錯誤

   EPOLLHUP該文件描述符被掛斷

   EPOLLET     將epoll設置爲邊緣觸發(Edge Triggered)模式。

   EPOLLONESHOT只監聽一次事件,監聽完以後,若是還想監聽須要再次把該文件描述符加入到epoll隊列中

 

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

等待事件的產生。

參數events用來從內核獲得事件的集合

參數maxevents告以內核這個events有多大(maxevents不能大於size)

參數timeout是超時時間(毫秒)

 

epoll的模式:

  • LT模式:Level Triggered水平觸發,

這個是缺省的工做模式。同時支持block socket和non-block socket。內核會告訴程序員一個文件描述符是否就緒了。若是程序員不做任何操做,內核仍會通知。

  • ET模式:Edge Triggered 邊緣觸發

是一種高速模式。僅當狀態發生變化的時候纔得到通知。這種模式假定程序員在收到一次通知後可以完整地處理事件,因而內核再也不通知這一事件。注意:緩衝區中還有未處理的數據不算狀態變化,因此ET模式下程序員只讀取了一部分數據就再也得不到通知了,正確的用法是程序員本身確認讀完了全部的字節(一直調用read/write直到出錯EAGAIN爲止)。

 

 

一個例子:

#include <netdb.h>

#include <sys/socket.h>

#include <sys/epoll.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <fcntl.h>

#include <unistd.h>

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <errno.h>

 

/*建立並綁定一個socket做爲服務器。 */

staticint  create_and_bind (char *port){

    struct  addrinfo hints;

    struct  addrinfo *result, *rp;

    int  s, sfd;

    memset (&hints, 0, sizeof (struct addrinfo));

    hints.ai_family = AF_UNSPEC;     /* Return IPv4 and IPv6 choices */

    hints.ai_socktype = SOCK_STREAM; /* 設置爲STREAM模式,即TCP連接 */

    hints.ai_flags = AI_PASSIVE;     /* All interfaces */

    s = getaddrinfo (NULL, port, &hints, &result);//得到本地主機的地址

    if (s != 0){

        fprintf (stderr, "getaddrinfo: %s\n", gai_strerror (s));

        return -1;

    }

    for (rp = result; rp != NULL; rp = rp->ai_next){//本地主機地址可能有多個,任意綁定一個便可

        sfd = socket (rp->ai_family, rp->ai_socktype, rp->ai_protocol); //建立socket

        if (sfd == -1)

            continue;

        s = bind (sfd, rp->ai_addr, rp->ai_addrlen); //並綁定socket

        if (s == 0)

        {

            /* 綁定成功 */

            break;

        }

        close (sfd);

    }

    if (rp == NULL){

        fprintf (stderr, "Could not bind\n");

        return -1;

    }

    freeaddrinfo (result);

    return sfd;

}

 

/*

   設置socket爲非阻塞模式。

   先get flag,或上O_NONBLOCK 再set flag。

 */

static  int   make_socket_non_blocking (int sfd) {

 

    int flags, s;

    flags = fcntl (sfd, F_GETFL, 0);

    if (flags == -1){

        perror ("fcntl");

        return -1;

    }

    flags |= O_NONBLOCK;

    s = fcntl (sfd, F_SETFL, flags);

    if (s == -1){

        perror ("fcntl");

        return -1;

    }

    return 0;

}

 

 

#define  MAXEVENTS 64

 

/*

   用法: ./epoll_test 8080

 */

int  main (int argc, char *argv[]) {

    int sfd, s;

    int efd;

    struct  epoll_event event;

    struct  epoll_event *events;

    if (argc != 2) {

        fprintf (stderr, "Usage: %s [port]\n", argv[0]);

        exit (EXIT_FAILURE);

    }

    sfd = create_and_bind (argv[1]); //sfd爲綁定後等待鏈接接入的文件描述符

 

    s = make_socket_non_blocking (sfd);

    s = listen (sfd, SOMAXCONN);

    efd = epoll_create1 (0);

    event.data.fd = sfd;

    event.events = EPOLLIN | EPOLLET;

    s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);

    /* Buffer where events are returned,爲events數組分配內存 */

    events = (struct  epoll_event*)calloc (MAXEVENTS, sizeof event);

    /* The event loop 事件循環*/

    while (1) {

        int n, i;

        n = epoll_wait (efd, events, MAXEVENTS, -1);

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

            if ((events[i].events & EPOLLERR) ||  (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) {

              /* 發生了錯誤或者被掛斷,或者沒有數據可讀  An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */

                fprintf (stderr, "epoll error\n");

                close (events[i].data.fd);

                continue;

            }elseif (sfd == events[i].data.fd) {//新鏈接

              /* sfd上有數據可讀,則表示有新鏈接

               * We have a notification on the listening socket,

               * which means one or more incoming connections. */

                printf("Incoming connection !\n");

                while (1) {

                    struct sockaddr in_addr;

                    socklen_t in_len;

                    int infd;

                    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

                    in_len = sizeof in_addr;

                    infd = accept (sfd, &in_addr, &in_len); //讀取到來的鏈接socket fd。

                    if (infd == -1) {

                        if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {

                            /* 已經讀完了sfd上的全部數據(全部鏈接)。最後一次讀(非阻塞讀)會返回EAGAIN(=EWOULDBLOCK)

                             * We have processed all incoming connections. */

                            break;

                        } else  {

                            perror ("accept");

                            break;

                        }

                    }

                    s = getnameinfo (&in_addr, in_len, hbuf, sizeof hbuf, sbuf, sizeof sbuf, NI_NUMERICHOST | NI_NUMERICSERV);

                    if (s == 0) {

                        printf("Accepted connection on descriptor %d (host=%s, port=%s)\n", infd, hbuf, sbuf);

                    }

                    s = make_socket_non_blocking (infd);  //設置socket爲非阻塞模式

                    event.data.fd = infd;  //將data部分設置爲fd

                    event.events = EPOLLIN | EPOLLET;  //監聽EPOLLIN事件,使用邊緣觸發模式

                    s = epoll_ctl (efd, EPOLL_CTL_ADD, infd, &event);

                }

                continue;

            } else {//有客戶端發來數據

                /* 有客戶端發來數據,由於處於ET模式,因此必須徹底讀取全部數據(要否則,剩下一部分數據後,就沒法再收到內核通知了)。*/

                int done = 0;

                while (1) {

                    ssize_t count;

                    char buf[512];

 

                    count = read (events[i].data.fd, buf, sizeof buf);

                    if (count == -1) {

                        if (errno != EAGAIN) { //若是errno=EAGAIN,表示咱們已經讀取了全部的數據

                            perror ("read");

                            done = 1;

                        }

                        break;

                    } elseif (count == 0) {  //讀到文件尾(對端被關閉了)

                        /* End of file. The remote has closed the connection. */

                        done = 1;

                        break;

                    }

                    s = write (1, buf, count); /* 打印到屏幕 */

                }

                if (done) { //讀完關閉(假設應用對每一個鏈接只讀取一次數據)

                    printf ("Closed connection on descriptor %d\n", events[i].data.fd);

                    /* Closing the descriptor will make epoll remove it from the set of descriptors which are monitored. */

                    close (events[i].data.fd);

                }

            }

        }

    }

    free (events);//釋放內存

    close (sfd);   //關閉sfd

    return EXIT_SUCCESS;

}

 

 

主要參考連接:http://blog.csdn.net/ljx0305/article/details/4065058

相關文章
相關標籤/搜索