天天看點

13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術

目錄

  • 13、SOCKET-IO複用技術
    • 1、五種I/O模型
    • 2、阻塞I/O模型
    • 3、非阻塞I/O模型
    • 4、I/O複用模型
    • 5、信号驅動I/O模型
    • 6、異步I/O模型
    • 7、I/O複用
    • 8、shutdown函數
    • 9、select函數
    • 10、poll函數
    • 11、epoll函數
      • 11-1、epoll_create
      • 11-2、epoll_ctl
      • 11-3、epoll_wait
      • 11-4、Epoll工作模式
      • 11-5、epoll示例
      • 12、總結

13、SOCKET-IO複用技術

1、五種I/O模型

  • 阻塞I/O
  • 非阻塞I/O
  • I/O複用(select和poll)
  • 信号驅動I/O
  • 異步I/O

2、阻塞I/O模型

  • 最流行的I/O模型是阻塞I/O模型,預設時,所有的套接口都是阻塞的
13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術

3、非阻塞I/O模型

  • 當我們把一個套接口設定為非阻塞方式時,即通知核心:當請求的I/O操作非得讓程序睡眠不能完成時,不要讓程序睡眠,而應傳回一個錯誤
13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術
應用程式連續不斷地查詢核心,看看某操作是否準備好,這對cpu時間是極大的浪費,一般隻在專門提供某種功能的系統中才會用到

4、I/O複用模型

  • 有了I/O複用,我們就可以調用select或poll,在這兩個系統調用的某一個上阻塞,而不是真正阻塞于真正的I/O系統調用
13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術

5、信号驅動I/O模型

  • 我們也可以用信号,讓核心在描述字準備好時用信号SIGIO通知我們,我們将此方法稱為信号驅動I/O
13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術

6、異步I/O模型

  • 異步I/O是Posix.1的1993版本中的新内容,我們讓核心啟動操作,并在整個操作完成後通知我們
13、SOCKET-IO複用技術 - select、poll、epoll13、SOCKET-IO複用技術

7、I/O複用

  • 如果一個或多個I/O條件滿足(例如:輸入已準備好被讀,或者描述字可以承接更多輸出的時候)我們就能夠被通知到,這樣的能力被稱為I/O複用,是由函數select和poll支援的

I/O複用網絡應用場合

  • 當客戶處理多個描述字
  • 一個客戶同時處理多個套接口
  • 如果一個tcp伺服器既要處理監聽套接口,又要處理連接配接套接口
  • 如果一個伺服器既要處理TCP,又要處理UDP

8、shutdown函數

  • 功能:關閉套接字兩端或一端的socket
#include <sys/socket.h>

int shutdown(int sockfd,int howto);
           
  • 參數:
    • SHUT_RD:關閉連接配接的讀這一半,不再接收套接口中的資料且現留在套接口接收緩沖區中的資料都廢棄
    • SHUT_WR:關閉連接配接的寫這一半,在TCP場合下,這稱為為半關閉。目前留在套接口發送緩沖區中的資料都被發送,後跟正常的tcp連接配接終止序列
    • SHUT_RDWR 連接配接的讀這一半和寫這一半都關閉
  • 傳回值:成功:0,失敗:錯誤代碼

shutdown與close的差別

  • 終止網絡連接配接的正常方法是調用close,但close有兩個限制可由函數shutdown來避免。
  • close将描述字的通路計數減1,僅在此計數為0時才關閉套接口;用shutdown我們可以激發TCP的正常連接配接終止序列,而不管通路計數
  • Close終止了資料傳送的兩個方向:讀和寫。由于TCP連接配接是全雙工的,有很多時候我們要通知另一端我們已完成了資料發送,即使一端仍有許多資料要發送也是如此。

9、select函數

  • 這個函數允許程序訓示核心等待多個事件中的任一個發生,并僅在一個或多個事件發生或經過某指定的時間後才喚醒程序
  • 功能:提供了即時響應多個套接的讀寫事件
#include <sys/select.h>
#include <sys/socket.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *except,const struct timeval *timeout);

struct timeval(
  long tv_sec;  //秒
  long tv_usec;//微秒
);
           
  • 參數
    • maxfdp1:等待最大套接字值加1,(等待套接字的數量)
    • readset:要檢查讀事件的容器
    • writeset:要檢查寫事件的容器
    • timeout:逾時時間
  • 傳回值:
    • 傳回觸發套件接字的個數

timeout參數

  • 永遠等待下去:僅在有一個描述字準備好I/O時才傳回,為此,我們将timeout設定為空指針
  • 等待固定時間:在有一個描述字準備好I/O是傳回,但不超過由timeout參數所指timeval結構中指定的秒數和微秒數
  • 根本不等待:檢查描述字後立即傳回,這稱為輪詢。定時器的值必須為0

fd_set參數

  • select使用描述字集,它一般是一個整數數組,每個數中的每一位對應一個描述字。
  • 使用fd_set資料類型來表示這個描述字集,我們不用去關心具體的實作細節。

操作fd_set的四個宏

  • void FD_ZERO(fd_set *fdset); //清空描述字集合
  • void FD_SET(int fd, fd_set *fdset); //添加一個描述字到集合中
  • void FD_CLR(int fd, fd_set *fdset); //從集合中删除一個描述字
  • int FD_ISSET(int fd, fd_set *fdset);//描述字是否在該集合中

select函數傳回值

  • 當傳回時,結果訓示哪些描述字已準備好。
  • 傳回時,我們用宏FD_ISSET來測試結構fd_set中的描述字。描述字集中任何與沒有準備好的描述字相對應的位傳回時清成0。為此,每次調用select時,我們都得将所有描述字集中關心的都置為1
  • 如果在任何描述字準備好之前定時器時間到,則傳回0
  • 傳回-1表示有錯。

select缺點

  • 每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
  • 同時每次調用select都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
  • select支援的檔案描述符數量太小了,預設是1024

select示例

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <error.h>
#include <termios.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define ERR_EXIT(M)\
do\
{\
	perror(M);\
	exit(1);\
}while(0);

int main(int argc,char *argv[])
{
    int sockfd = socket(PF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
        ERR_EXIT("socket");
    int on = 1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) == -1)
        ERR_EXIT("setsockopt");
    struct sockaddr_in sockaddr; 
    bzero(&sockaddr,sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
    sockaddr.sin_port = htons(5566);
    if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
        ERR_EXIT("bind");
    if(listen(sockfd,5) == -1)
        ERR_EXIT("listen");
    
    int maxfd = sockfd;
    int client[FD_SETSIZE];
    
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(sockfd,&allset);
    int nready;
    int conn;
    int i;
    for(i = 0;i < FD_SETSIZE;i++)
        client[i] = -1;
    while(1){
        rset = allset;
        nready = select(maxfd + 1,&rset,NULL,NULL,NULL);
        if(nready == -1)
            ERR_EXIT("select");
        //新用戶端
        if(FD_ISSET(sockfd,&rset)){
            struct sockaddr_in peer_addr;
            memset(&peer_addr,0,sizeof(peer_addr));
            socklen_t socklen = sizeof(peer_addr);
            conn = accept(sockfd,(struct sockaddr *)&peer_addr,&socklen);
            if(conn == -1)
                ERR_EXIT("accept");
            for(i = 0;i < FD_SETSIZE;i++){
                if(client[i] < 0){
                    client[i] = conn;
                    break;
                }
            }
            FD_SET(conn,&allset);
            if(conn > maxfd)
                maxfd = conn;
            if(--nready <= 0)//1件事情
                continue;
        }
        //已連接配接FD産生可讀事件
        for( i = 0;i < FD_SETSIZE;i++){
            if(FD_ISSET(client[i],&rset)){
                conn = client[i];
                char buf[1024]={0};
                int nread = read(conn,buf,sizeof(buf));
                if(nread == 0){//對方關閉
                    printf("client is close\n");
                    FD_CLR(conn,&allset);
                    client[i] = -1;
                    close(conn);
                }
                fputs(buf,stdout);
                write(conn,buf,nread);
                memset(buf,0,1024);
                if(--nready <= 0)//1件事情
                    break;
            }
        }
    }
    return 0;
}
           

10、poll函數

  • poll函數和select類似,但它是用檔案描述符而不是條件的類型來組織資訊的.
  • 也就是說,一個檔案描述符的可能事件都存儲在struct pollfd中.與之相反,select用事件的類型來組織資訊,而且讀,寫和錯誤情況都有獨立的描述符掩碼.
  • 參數:
    • fdarray是一個pollfd的結構體數組用來表示檔案描述符的監視資訊
    • nfds參數給出了要監視的描述符數目
    • timeout參數是一個用豪秒表示的時間,是poll在傳回前沒有接收事件是應等待的時間,如果timeout的值為-1,poll就永遠不會逾時.如果整數值為32個比特,那麼最大逾時周期約為30分鐘
  • 傳回值:準備好描述字的個數,0-逾時,1-出錯

pollfd結構體

  • fd是檔案描述符值
  • event和revents是通過代表各種事件的标準符進行邏輯或運算建構而成的
struct pollfd
{
    int fd;
    short events;   //感興趣的事件
    short revents;  //fd上觸發的事情
}
           

poll函數事件标志

事件标志符 含義
POLLIN 無阻塞地讀除了具有高優先級的資料之外的資料
POLLRONORM 無阻塞地讀正常資料
POLLRDBAND 無阻塞地讀具有優先級的資料
POLLOUT 無阻塞的寫正常資料

poll優缺點

  • 缺點:
    • 每次調用poll,都需要把fdarray數組從使用者态拷貝到核心态,這個開銷在連接配接數fd很多時會很大。
    • 同時每次調用poll都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
  • 優點:
    • 支援的檔案描述符數量沒有限制。
與select在本質上沒有多大差别,管理多個描述符也是進行輪詢,根據描述符的狀态進行處理。

poll示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

#define PORT 5566

#define MAXSIZE 1024
#define FDSIZE 1000
#define ERR_EXIT(M)\
do\
{\
	perror(M);\
	exit(1);\
}while(0);

int main()
{
    
    
    int sockfd = socket(PF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
        ERR_EXIT("socket");
    int optval = 1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval)) == -1)
		ERR_EXIT("setsockopt");
    struct sockaddr_in sockaddr;
    bzero(&sockaddr,sizeof(sockaddr));
    sockaddr.sin_family = AF_INET;
    //sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
    sockaddr.sin_port = htons(PORT);
	if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
		ERR_EXIT("bind");
    if(listen(sockfd,5) == -1)
		ERR_EXIT("listen");
    
	/*開始poll流程*/
	int connfd;
	struct sockaddr_in clientaddr;
	memset(&clientaddr,0,sizeof(clientaddr));
    socklen_t clientaddrlen;
    struct pollfd clientfds[FDSIZE];//監聽1000個
    int maxi;
    int i,n;
	
	/*初始化客戶連接配接描述符*/
	for(i = 0;i < FDSIZE;i++)
		clientfds[i].fd = -1;
	//添加監聽描述符
	clientfds[0].fd = sockfd;
	clientfds[0].events = POLLIN;
	maxi = 0;
	//循環處理
	while(1){
		//擷取可用描述符的個數
        int nready = poll(clientfds,maxi+1,-1);//永遠不會逾時
        if(nready == -1){
            ERR_EXIT("poll");
        }else if(nready == 0){//逾時
            printf("select timeout!\n");
            continue;
        }
        //測試監聽描述符是否準備好
        if(clientfds[0].revents & POLLIN){
        	clientaddrlen = sizeof(clientaddr);
            //接受新的連接配接
            connfd = accept(sockfd,(struct sockaddr *)&clientaddr,&clientaddrlen);
            if(connfd == -1){
                ERR_EXIT("accept");
            }
        
            printf("accept a new client:%s:%d\n",inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
            //将新的連接配接描述符添加到數組中
            for(i = 1;i < FDSIZE;i++){
                if(clientfds[i].fd < 0){
                    clientfds[i].fd = connfd;
                    clientfds[i].events = POLLIN;
                    break;
                }
            }
            if(i == FDSIZE){
                fprintf(stderr,"Too many clients.\n");
                exit(1);
            }
            maxi = (i > maxi ? i : maxi);
        }
        //檢查用戶端連接配接是否有讀事件
        for(int i = 1;i <= maxi;i++){
            if(clientfds[i].fd < 0)
                continue;
            if(clientfds[i].revents & POLLIN){
                char buf[MAXSIZE] = {0};
                n = read(clientfds[i].fd,buf,sizeof(buf));
                if(n == 0){
                    close(clientfds[i].fd);
                    clientfds[i].fd = -1;
                    printf("client is closed.\n");
                    continue;
                }
                printf("read msg is:%s\n",buf);
                write(clientfds[i].fd,buf,n);
            }
        }
	}
       
    return 0;
}
           

11、epoll函數

  • 相對于select和poll來說,epoll更加靈活,沒有描述符限制。
  • epoll使用一個檔案描述符管理多個描述符,将使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy隻需一次。

11-1、epoll_create

  • 建立一個epoll的句柄,size用來告訴核心這個監聽的數目一共有多大。從linux2.6.8之後size參數被忽略。
  • 需要注意的是,當建立好epoll句柄後,它就是會占用一個fd值,在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。

檢視最大數:

cat /proc/sys/fs/file-max

11-2、epoll_ctl

  • 注冊要監聽的事件類型。select是在監聽事件時告訴核心要監聽什麼類型的事件。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

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;
           
  • 參數
    • epfd:epoll_create()的傳回值
    • op:表示動作,用三個宏來表示:
      • EPOLL_CTL_ADD:注冊新的fd到epfd中;
      • EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
      • EPOLL_CTL_DEL:從epfd中删除一個fd
    • fd:需要監聽的fd
    • event是告訴核心需要監聽什麼事件

struct epoll_event結構

參數 描述
EPOLLIN 表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉)
EPOLLOUT 表示對應的檔案描述符可以寫
EPOLLPRI 表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來)
EPOLLERR 表示對應的檔案描述符發生錯誤
EPOLLHUP 表示對應的檔案描述符被挂斷
EPOLLET 将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的
EPOLLONESHOT 隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡

11-3、epoll_wait

  • 等待事件的産生,類似于select()調用。
int epoll_wait(int epfd, 
                struct epoll_event * events, 
                int maxevents, 
                int timeout);
           
  • 參數:
    • events:用來從核心得到事件的集合
    • maxevents:告之核心這個events有多大,這個值不能大于建立epoll_create()時的size
    • timeout:逾時時間(毫秒,0會立即傳回,-1不會逾時)。
  • 傳回值:該函數傳回需要處理的事件數目,如傳回0表示已逾時。

11-4、Epoll工作模式

  • epoll對檔案描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是預設模式,差別如下:
    • LT模式:當epoll_wait檢測到描述符事件發生并将此事件通知應用程式,應用程式可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程式并通知此事件。
    • ET模式:當epoll_wait檢測到描述符事件發生并将此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程式并通知此事件。
    ET模式在很大程度上減少了epoll事件被重複觸發的次數,是以效率要比LT模式高。

11-5、epoll示例

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

#define EPOLLSIZE 1024
#define PORT 5566
#define ERR_EXIT(M) do{perror(M);exit(1);}while(0)
int main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd == -1)
		ERR_EXIT("socket");
	int opt = 1;
	if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)) == -1)
		ERR_EXIT("setsockopt");
	struct sockaddr_in sockaddr;
	bzero(&sockaddr,sizeof(sockaddr));
	sockaddr.sin_family = AF_INET;
	sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//inet_pton(AF_INET,"0.0.0.0",&sockaddr.sin_addr);
	sockaddr.sin_port = htons(PORT);
	if(bind(sockfd,(struct sockaddr *)&sockaddr,sizeof(sockaddr)) == -1)
		ERR_EXIT("bind");
	if(listen(sockfd,EPOLLSIZE) == -1)
		ERR_EXIT("listen");
	int epfd = epoll_create(EPOLLSIZE);
	if(epfd == -1)
		ERR_EXIT("epoll_create");
	struct epoll_event events[EPOLLSIZE],ep_event;
	bzero(&ep_event,sizeof(ep_event));
	ep_event.events = EPOLLIN;
	ep_event.data.fd = sockfd;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ep_event) == -1)
		ERR_EXIT("epoll_ctl");
	int nready = 0;
	for(;;){
		nready = epoll_wait(epfd,events,EPOLLSIZE,-1);
		if(nready == -1)
			ERR_EXIT("epoll_wait");
		for(int i = 0;i < nready;i++){
			if(events[i].events == EPOLLIN){
				if(events[i].data.fd == sockfd){
					struct sockaddr_in caddr;
					bzero(&caddr,sizeof(caddr));
					socklen_t addrlen = sizeof(caddr);
					int cfd = accept(sockfd,(struct sockaddr *)&caddr,&addrlen);
					if(cfd == -1)
						ERR_EXIT("accept");
					ep_event.events = EPOLLIN;
					ep_event.data.fd = cfd;
					if(epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ep_event) == -1)
						ERR_EXIT("epoll_ctl_add");
					printf("connect: ip:%s,port:%d\n",inet_ntoa(caddr.sin_addr),caddr.sin_port);	
				}else{
					int cfd = events[i].data.fd;
					char buf[1024]={0};
					int nread = read(cfd,buf,sizeof(buf));
					if(nread == 0){
						if(epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ep_event) == -1)
							ERR_EXIT("epoll_ctl_del");
					}else if(nread == -1){
						ERR_EXIT("read");
					}else{
						printf("client:%s",buf);
						if(write(cfd,buf,sizeof(buf)) == -1)
							ERR_EXIT("write");	
					}
				}
			}
		}
	}
	return 0;
}
           

12、總結

  • select,poll實作需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒連結清單,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,調用回調函數,把就緒fd放入就緒連結清單中,并喚醒在epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和合poll在“醒着”的時候要周遊整個fd集,而epoll在“醒着”的時候隻要判斷一下就緒連結清單是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
  • select,poll每次調用都要把fd集合從使用者态往核心态拷貝一次,并且要把current往裝置等待隊列中挂一次,而epoll隻要一次拷貝,而且把current往等待隊列上挂也隻挂一次(在epoll_wait的開始,注意這裡的等待隊列并不是裝置等待隊列,隻是一個epoll内部定義的等待隊列)。這也能節省不少的開銷。

繼續閱讀