IO多路转接—select 、poll、epoll

多路(多个文件描述符)I/O(输入输出)转接(将多个文件描述符交给select监控)

调用select函数,直到描述符表中有一个描述符准备好进入I/O时,该函数才返回,通过select的返回值告知进程哪些描述符已经准备好进入I/O。

三种模型性能分析 

select

1.select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
2.解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

readfds、writefds和exceptfds是指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。这种数据类型为每一个可能的描述符保持了一位。

三者中任意一个或全部都可以是空指针,这表示对相应状态并不关心。 

理解select模型:

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。 
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。 
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) 
(3)若再加入fd=2,fd=1,则set变为0001,0011 
(4)执行select(6,&set,0,0,0)阻塞等待  //其中6=5+1
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

 

void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0

 

返回值:

返回准备就绪的描述符,所以正返回值表示已经准备好的描述符

若超时则返回0,表示没有描述符准备好

若出错则返回-1

使用select函数的过程一般是:
1、调用宏FD_ZERO将指定的fd_set清零

  1. 调用宏FD_SET将需要测试的fd加入fd_set
  2. 调用函数select测试fd_set中的所有fd

用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。
以下是一个测试单个文件描述字可读性的例子:

poll

不同select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。 

其中,pollfd是一个结构体,里面包含就绪的文件描述符及其事件:

所以pollfd结构体整体的含义就指的是关心的fd上的event事件

其中fd+events就相当于select接口里fd_set中的内容。

timeout是一个定时器,单位是ms,不同的值有不同的含义,如下表:

timeout

含义

-1

IO不阻塞,此时poll可以任意进行IO

大于0

隔一段时间阻塞,此时poll每隔一段时间IO被阻塞一次

0

一直阻塞,此时poll不能进行

 

返回值:
由于poll返回时会将该文件描述符上的就绪事件放入revents中,所以其返回值就是就绪事件fd的个数,如下表所示:

返回值

含义

0

超时(timeout)

<0

出错

大于0

就绪事件的个数

Code用poll监控输入输出

Step1:定义pollfd结构体并将timeout设为0

Step2:加入主事件循环,调用poll接口监控标准输入

所以POLLIN 和POLLOUT都是宏定义,对服务器而言,这两个宏就表示输入输出事件。

 

epoll API(函数)

 

  1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符个数,跟内存大小有关
  2. 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
  3. 等待所监控文件描述符上事件的产生,类似于select()调用。
     

注:句柄是一个标识符,是拿来标识对象或者项目的,它就象我们的姓名一样,每个人都会有一个,不同的人的姓名不一样,但是,也可能有一个名字和你一样的人。从数据类型上来看它只是一个16位的无符号整数。应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。

 

events可以是以下几个宏的集合:

参考:http://kimi.it/515.html

LT模式 和 ET模式:

下面我们用一个快递员配送快递的例子来解释一下ET模式:

假如 : 
1. 我有5个快递,当一个快递到的时候,快递员就打电话让你取,一直打直到你把这个快递取走为止,下一个你的来了依然如此;很显然这样的快递员工作方式效率会很慢。

上面的就是属于LT模式;

2.. 同样的,如果你有5个快递,当一个快递到的时候,快递员第一次给你送的的时候打一次电话,你不来他就替你收着(而这个时候,快递员不会等你),第二个你的来了再给你打一次,你不来他依然替你收着,每次只有快递数量变化的时候才会打电话,这个时候只有你哪一次有时间,将所有的快递都拿走。此种方式效率较高:因为快递员并没有去等

这种模式属于 ET模式。

下面我来介绍一下两者之间的特点:

首先了解:

在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block,perror输出: Resource temporarily unavailable

总结:
这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。
所以,对于阻塞socket,read/write返回-1代表网络出错了。但对于非阻塞socket,read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。


 
 LT模式下只要某个socket处于readable/writable状态,无论什么时候  epoll_wait都会返回该socket(通知);
    1. 当epoll检测到socket上的事件就绪时,可以不立即处理或者只处理一部分
    (例如:2KB的数据好了,此时可以一次读1KB,然后剩1KB)
    2. 在第二次调用epoll_wait的时候它依然会立即通知你,并且通知socket的读事件就绪
       直到缓存区内的数据都读完了,epoll_wait才不会立即返回
    3. 支持非阻塞与阻塞

ET模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket(通知)。

   1. 当epoll检测到socket上的事件就绪时,必须立即处理
   (例如:2KB的数据好了,此时可以一次读1KB,然后剩1KB)
   2. 但是在第二次调用epoll_wait的时候,它不再立即返回通知你
      也就是说,ET模式下,数据就绪以后只有一次处理机会,所以要么不读,要么读完,
      不会有只读一部分的情况
      (只有在数据从 无变有 或者 少变多 的时候,才会通知你)
   3. 性能比LT高
   4. 只能采用非阻塞

 在epoll的ET模式下,正确的读写方式为:
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

另外为什么ET模式只支持非阻塞读写呢?

   因为: 数据就绪只通知一次,必须在通知后,一次处理完
         也就是说:如果使用ET模式,当数据就绪的时候就要一直读,直到数据读完为止

     1. 但是如果当前的fd是阻塞的,而读是循环的:那么在读完缓存区的时候,
        如果对端还没有数据写进来,那么该read函数就会一直阻塞,
        这不符合逻辑,不能这么使用 
      2. 那么就需要将fd设置成非阻塞,当没有数据的时候,read虽然读取不到任何的数据,
      但是肯定不会被阻塞住,其会返回-1,并且errno被设置为EAGAIN.那么此时说明缓冲区内数据已经读完,read返回继续后序的逻辑

下面介绍使用epoll的具体步骤(之前一直困惑的地方):

大致流程:

 首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作 将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。之后在你的网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
    其中kdpfd为用epoll_create创建之后的句柄,events是一个 epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。 max_events是当前需要监听的所有socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没 有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环 的效率。

具体流程以及代码示例

 Epoll模型主要负责对大量并发用户的请求进行及时处理,完成服务器与客户端的数据交互。其具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定将可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket描述符ListenSock;将该描述符设定为非阻塞模式,调用Listen()函数在套接字上侦听有无新的连接请求,在 epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非 阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET.
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据 的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。

int main()
{
   int sock = socket(AF_INET, SOCK_STREAM, 0);
	if( sock < 0 ){
		perror("socket");
		exit(1);
	}

	//Address already in use
	int opt = 1;
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = inet_addr(ip);
	local.sin_port = htons(port);

	if( bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0 ){
		perror("bind");
		exit(2);
	}

	if( listen(sock, 5) < 0 ){
		perror("listen");
		exit(3);
	}
    
    int listen_sock=sock;
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
        int client_sock=-1;

          // I/O多路复用
          struct epoll_event ev;
          struct epoll_event events[20];
          int epfd;
          int nfds=0;// 用来接收epoll_wait的返回值,表示非阻塞的文件描述符的数量
          epfd=epoll_create(256);
          ev.data.fd=listen_sock;
          ev.events=EPOLLIN;// 当绑定的那个socket文件描述符可读的时候,就触发事件(默认水平触发)
       epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev); // 把绑定的n个socket文件描述符添加到内核的红黑树里面
while (1) {
nfds = epoll_wait(epfd, events, 20, 0);
//epoll_wait范围之后应该是一个循环,遍利所有的事件:
for (n = 0; n < nfds; ++n)
{
        if (events[n].data.fd == listen_sock)
        {//如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。
                client_sock = accept (listen_sock, (struct sockaddr *) &client, &len);
                if (client_sock < 0)
                {
                        perror ("accept");
                        continue;
                }
                setnonblocking (client);        // 将新连接置于非阻塞模式
                ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。
                //注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
                //如果有写操作的话,这个时候epoll是不会返回事件的,
               //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
                ev.data.fd = client_sock;
                if (epoll_ctl (epfd, EPOLL_CTL_ADD, client_sock, &ev) < 0)
                {
                        /*
                                设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
                                这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式.
                        */
                        fprintf (stderr, "epoll set insertion error: fd=%d", client_sock);
                         return -1;
                 }
        }
        else          
 {                                                  
   // 如果不是主socket的事件的话,则代表是一个用户socket的事件,
   do_use_fd (events[n].data.fd); 
   //则来处理这个用户socket的事情,比如说 read(fd,xxx)之类的,或者一些其他的处理。
 }
   close(epfd);
   close(listen_sock);
   return 0;
 }

关于内存映射技术mmap:

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

epoll模型分析:

(1)调用epoll_create创建epoll模型的时候,实际上是在内核区创建了一棵空的红黑树和一个空的队列;

(2)调用epoll_ctl的时候,实际上是在往红黑树中添加结点,结点描述的是文件描述符及其上的对应事件;

(3)当某文件描述符上的某事件就绪的时候操作系统会创造一个结点放在队列中(此结点表示此文件描述符上的此事件就绪),这个队列通过内存映射机制让用户看到。

epoll相对于select和poll的改进:

  1. 当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术共享同一块存储,避免了fd从内核赋值到用户空间。这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
  2. epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

Select、Poll、Epoll的特点:

Select、Poll、Epoll区别:

 基于epoll的简单服务器

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

void handler_events(int epfd,struct epoll_event revs[],int num,int listen_sock)
{
    struct epoll_event ev;
    int i = 0;
    for( ; i < num; i++ )
    {
    int fd = revs[i].data.fd;

    // 如果是监听文件描述符,则调用accept接受新连接

    if( fd == listen_sock && (revs[i].events & EPOLLIN) )
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(fd,(struct sockaddr *)&client,&len);

        if( new_sock < 0 )
        {
        perror("accept fail ... \n");
        continue;
        }

       printf("get a new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

       //因为只是一个http协议:连接成功后,下面就是要 请求和响应 
       // 而服务器端响应之前:要先去读客户端要请求的内容

       ev.events = EPOLLIN;
       ev.data.fd = new_sock;
       epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);

       continue;
    }

    // 如果是普通文件描述符,则调用read提供读取数据的服务

    if(revs[i].events & EPOLLIN)
    {
        char buf[10240];
        ssize_t s = read(fd,buf,sizeof(buf)-1);
        if( s > 0 )// 读成功了
        {
        buf[s] = 0;
        printf(" %s ",buf);

        // 读成功后,就是要给服务端响应了
        // 而这里的事件是只读事件,所以要进行修改

        ev.events = EPOLLOUT;// 只写事件
        ev.data.fd = fd;
        epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);// 其中EPOLL_CTL_MOD 表示修改

        }

        else if( s == 0 )
        {
        printf(" client quit...\n ");
        close(fd);// 这里的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
        }
        else// s = -1 失败了
        {   
        printf("read fai ...\n");
        close(fd);// 这里的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
        }
        continue;
    }

    // 服务器端给客户端响应: 写

    if( revs[i].events & EPOLLOUT )
    {
        const char* echo = "HTTP/1.1 200 ok \r\n\r\n<html>hello epoll server!!!</html>\r\n";
        write(fd,echo,strlen(echo));
        close(fd);
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
    }
    }
}

int startup( int port )
{
    // 1. 创建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP
    if( sock < 0 )
    {
    perror("socket fail...\n");
    exit(2);
    }

    // 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型
    local.sin_port = htons(port);// 这里的端口号也可以直接指定8080
    // 3. 绑定端口号

    if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
    {
    perror("bind fail...\n");
    exit(3);
    }

    // 4. 获得监听套接字
    if( listen(sock,5) < 0 )
    {
    perror("listen fail...\n");
    exit(4);
    }
    return sock;
}

int main(int argc,char* argv[] )
{
    if( argc != 2 )
    {
    printf("Usage:%s port\n ",argv[0]);
    return 1;
    }

    // 1. 创建一个epoll模型: 返回值一个文件描述符

    int epfd = epoll_create(256);
    if( epfd < 0 )
    {
    perror("epoll_create fail...\n");
    return 2;
    }

    // 2. 获得监听套接字

   int listen_sock = startup(atoi(argv[1]));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型


    // 3. 初始化结构体----监听的结构列表

    struct epoll_event  ev;
    ev.events = EPOLLIN;//关心读事件
    ev.data.fd = listen_sock;// 关心的描述文件描述符

    // 4. epoll的事件注册函数---添加要关心的文件描述符的只读事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);

    struct epoll_event revs[128];
    int n = sizeof(revs)/sizeof(revs[0]);

    int timeout = 3000;
    int num = 0;


    while(1)
    {

       // 5 . 开始调用epoll等待所关心的文件描述符集就绪

       switch( num = epoll_wait(epfd,revs,n,timeout) )
       {
       case 0:// 表示词状态改变前已经超过了timeout的时间
           printf("timeout...\n");
           continue;
       case -1:// 失败了
           printf("epoll_wait fail...\n");
           continue;
       default: // 成功了

           handler_events(epfd,revs,num,listen_sock);
           break;
       }
    }
    close(epfd);
    close(listen_sock);
    return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值