select、 poll、epoll

I/O多路复用

作者:知乎用户

链接:https://www.zhihu.com/question/28594409/answer/52835876

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

下面举一个例子,模拟一个tcp服务器处理30个客户socket。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:

  1. 第一种选择:

按顺序逐个检查

,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。

这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。

  1. 第二种选择:你

用时间片给30个同学分

,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。

  1. 第三种选择,你

站在讲台上等,谁解答完谁举手

。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。

这种就是IO复用模型,Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用

非阻塞模式

这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是

事件驱动

,所谓的reactor模式

select 、poll、epoll只在一个线程里就能完成。

select的数据结构是数组,poll的数据结构是链表。它们都是文件描述符拷贝到内核里再拷出来。而epoll创建的有关文件描述符的数据结构本身就在内核中。

select、poll采用轮询的方式检查文件描述符是否处于就绪态。epoll采用回调机制。

一、普通的多线程网络模型

1.socket

2.bind

3.listen

4.accept

每来一个socket连接,就开一个线程去处理。在这个线程里做socket的读和写。

5.create_thread

  6.read/write

二、select模型

一般用于嵌入式

将一组socket数组投递给系统,然后去系统里去查询socket是否有信号,它是通过一个select()函数进行的,会返回有操作的select数组。

select的大致工作流程:

(1)采用数组组织文件描述符(fd 一个id,可以通过这个fd查到对应的socket)

(2)通过遍历数组的方式,监视文件描述符的状态(可读,可写,异常)

(3)如果没有可读/可写的文件描述符,进程会阻塞等待一段事件,超时就返回

(4)当有一个可读/可写的文件描述符存在时,进程会从阻塞状态醒来

(5)进行无差别轮询,找出能够操作的I/O流,若处理后,会移除对应的文件描述符

select的缺点:

(1)每次调用select,都需要把文件描述符集合从用户空间贝到内核空间,这个开销在I/O流很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所文件描述符数组,这个开销在I/O流很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

代码

fd_set allsockets;
    //清零
    FD_ZERO(&allSockets);
    //服务器装进去
    FD_SET(socketServer, &allSockets);
    while (1)
    {
        fd_set readSockets = allSockets;
        fd_set writeSockets = allSockets;
        fd_set errorSockets = allSockets;

        //时间段
        struct timeval st;
        st.tv_sec = 3;
         st.tv_usec = 0;

        //select
        int nRes = select(0, &readSockets, &writeSockets, &errorSockets, &st);
        if (0 == nRes) //没有响应的socket
        {
            continue;
        }
        else if (nRes > 0)
        {
            //处理错误
            for (u_int i = 0; i < errorSockets.fd_count; i++)
            {
                char str[100] = { 0 };
                int len = 99;
                if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, str, &len))
                {
                    printf("无法得到错误信息\n");
                }
                printf("%s\n", str);    
            }

            for (u_int i = 0; i < writeSockets.fd_count; i++)
            {
                //printf("服务器%d,%d:可写\n", socketServer, writeSockets.fd_array[i]);
                if (SOCKET_ERROR == send(writeSockets.fd_array[i], "ok", 2, 0))
                {
                    int a = WSAGetLastError();
                }
            }

            //有响应
            for (u_int i = 0; i < readSockets.fd_count; i++)
            {
                if (readSockets.fd_array[i] == socketServer)
                {
                    //accept
                    SOCKET socketClient = accept(socketServer, NULL, NULL);
                    if (INVALID_SOCKET == socketClient)
                    {
                        //链接出错
                        continue;
                    }
                    
                    FD_SET(socketClient, &allSockets);
                    //send
                }
                else
                {
                    char strBuf[1500] = { 0 };
                    //客户端吧
                    int nRecv = recv(readSockets.fd_array[i], strBuf, 1500, 0);
                    //send
                    if (0 == nRecv)
                    {
                        //客户端下线了
                        //从集合中拿掉
                        SOCKET socketTemp = readSockets.fd_array[i];
                        FD_CLR(readSockets.fd_array[i], &allSockets);
                        //释放
                        closesocket(socketTemp);
                    }
                    else if (0 < nRecv)
                    {
                        //接收到了消息
                        printf(strBuf);
                    }
                    else //SOCK_ERROR
                    {
                        //强制下线也叫出错 10054
                        int a = WSAGetLastError();
                        switch (a)
                        {
                        case 10054:
                            {
                                SOCKET socketTemp = readSockets.fd_array[i];
                                FD_CLR(readSockets.fd_array[i], &allSockets);
                                //释放
                                closesocket(socketTemp);
                            }    
                        }
                    }    
                }
            }
        }

图示
在这里插入图片描述

对于CS模型,它的accept()会阻塞,如果有连接,做了recv()和accept()后,又继续回到accept()阻塞。select模型是把这个socket加到数组里去,然后用select函数把有信号的socket发回来。传给应用程序里定义好的socket数组。

URL

网络编程–select模型(总结)

三、poll模型

   1、它主要解决的是select模型中对文件描述符数量的限制,它用链表来组织文件描述符。

   2、原理跟select一致

   3、只是解决了文件描述符容量受限的问题(用链表存储)

   4、select和poll都是水平触发(意思是我找到了要处理的socket,然后通知你,让你处理,比如read,然后你没有read,它一直未处理的状态,然后下次轮询的时候还是会收到。

四、epoll模型

  1)内核里维护一个红黑树和一个就绪队列(就绪链表)。红黑树用于管理所有的文件描述符(这样就不用每次都拷贝到内核里去),就绪队列用于保存所有存在事件的文件描述符。

  2)接收到I/O请求,会先在红黑树中查找,如果存在把文件描述符放到就绪队列中,如果不存在就添加到红黑树中。

 3)如果就绪队列为空,进程阻塞,不为空就遍历就绪队列,并通知应用程序处理文件描述符对应的I/O

工作模式

1.LT模式(水平触发)检测到可处理的文件描述符时,通知应用程序,应用程序可以不立即处理该事件,后续会再次通知。

2.ET模式(边缘触发)检测到可处理的文件描述符时,通知应用程序,应用程序必须立即处理该事件,如果本次不处理,后续不通知。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值