關於select和epoll多路複用(事件輪詢API)

非阻塞IO

       當我們調用套接字的讀寫方法時默認它們是阻塞的,比如read方法要產地進去一個參數n,代表最多讀取n個字節後再返回,否則線程就會阻塞在哪裏,直到新的數據到來或者連接關閉,read方法纔可以返回,線程才能繼續處理。write方法不會阻塞,除非內核爲socket分配的寫緩衝已經滿了,直到緩衝區有空閒空間。

       非阻塞IO在socket對象上提供了一個選項non_blocking,當這個選項打開時,讀寫方法不會阻塞,能讀多少讀多少,能寫多少寫多少,這個量的大小取決於內核爲套接字分配的讀緩衝區大小和寫緩衝區的空閒大小。read和write方法都會通過返回值告知程序實際讀寫了多少字節。

事件輪詢多路複用

       非阻塞IO存在一個問題,那就是線程讀數據何時才繼續讀沒讀完的數據,也可以理解爲當新數據到來時,線程如何得到通知?寫數據時如果緩衝區滿了沒寫完,剩下的數據何時才能繼續寫?

      解決這個問題依賴事件輪詢API,最簡單事件輪詢api是select函數,它是操作系統提供給用戶程序的api。輸入是讀寫描述符列表,輸出是與之對應的可讀可寫事件。同時還提供一個timeout參數,如果沒有任何事件到來,那麼就最多等待timeout規定的時間,一旦期間有任何事件到來,就可以立即返回結果,時間過了還沒有任何事件到來,也會立即返回。每循環一輪描述符列表稱爲一個事件循環週期。

     因爲我們用select同時處理多個通道描述符的讀寫事件,所以我們將select這類系統調用稱爲多路複用api。包括redis在內的應用多用epoll(epoll_wait),它們之間主要有如下區別:

  • select允許的同時打開的文件描述符遠小於epoll
  • select每個循環週期會遍歷所有文件描述符,而epoll額外維護一個活躍的事件隊列,裏面只存儲了觸發事件的文件描述符,所以相比select,在併發比較高時,epoll佔有優勢
  • epoll採用了零拷貝技術,使用mmap系統函數將內核緩衝區與應用程序共享,減少了read和write系統調用,從而減少了內核態和用戶態切換

epoll使用例子(來源百度百科)

 int epfd = epoll_create(POLL_SIZE);
    struct epoll_event ev;
    struct epoll_event *events = NULL;
    nfds = epoll_wait(epfd, events, 20, 500);
    {
        for (n = 0; n < nfds; ++n) {
            if (events[n].data.fd == listener) {
                //如果是主socket的事件的話,則表示
                //有新連接進入了,進行新連接的處理。
                client = accept(listener, (structsockaddr *)&local, &addrlen);
                if (client < 0) {
                    perror("accept");
                    continue;
                }
                setnonblocking(client);        //將新連接置於非阻塞模式
                ev.events = EPOLLIN | EPOLLET; //並且將新連接也加入EPOLL的監聽隊列。
                //注意,這裏的參數EPOLLIN|EPOLLET並沒有設置對寫socket的監聽,
                //如果有寫操作的話,這個時候epoll是不會返回事件的,如果要對寫操作
                //也監聽的話,應該是EPOLLIN|EPOLLOUT|EPOLLET
                ev.data.fd = client;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev) < 0) {
                    //設置好event之後,將這個新的event通過epoll_ctl加入到epoll的監聽隊列裏面,
                    //這裏用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個
                    //epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
                    fprintf(stderr, "epollsetinsertionerror:fd=%d", client);
                    return -1;
                }
            }
            else if(event[n].events & EPOLLIN)
            {
                //如果是已經連接的用戶,並且收到數據,
                //那麼進行讀入
                int sockfd_r;
                if ((sockfd_r = event[n].data.fd) < 0)
                    continue;
                read(sockfd_r, buffer, MAXSIZE);
                //修改sockfd_r上要處理的事件爲EPOLLOUT
                ev.data.fd = sockfd_r;
                ev.events = EPOLLOUT | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
            }
            else if(event[n].events & EPOLLOUT)
            {
                //如果有數據發送
                int sockfd_w = events[n].data.fd;
                write(sockfd_w, buffer, sizeof(buffer));
                //修改sockfd_w上要處理的事件爲EPOLLIN
                ev.data.fd = sockfd_w;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_w, &ev)
            }
            do_use_fd(events[n].data.fd);
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章