I/O-NIO-多路复用器

by shihang.mai

1. 概述

通过一个系统调用,获取多个IO状态,叫多路复用器。在Linux下多路复用器都是同步模型。(只要程序自己读写IO,那么IO模型就是同步的

多路复用器
只关注IO:不关注从IO读写完之后的事情

同步:app自己R/W
异步:kernel完成R/W 只有win:iocp
阻塞:blocking
非阻塞:non-blocking

linux以及成熟的框架netty:同步非阻塞 同步阻塞

2. select poll

select poll

系统调用

  1. 调用socket()返回一个fd
  2. 调用bind(fd,9090),将fd和端口绑定起来
  3. 调用listen(),监听端口
  4. 调用select(fds)查找有状态的IO
  5. 调用recv(有状态的fd)

其实无论NIO SELECT POLL都需要遍历所有的IO询问状态,只不过:

  • 普通NIO:这个遍历成本在用户态切换内核态

  • select poll :这个遍历过程只触发了一次系统调用,用户态内核态的切换,过程中,把fds传递给内核,内核重新根据用户这次调用传过来的fds,遍历,修改状态

2.1 弊端

  1. 每次重复传递fds
  2. 每次内核被调用之后,针对这次调用,触发一次遍历fds全量的复杂度

3. epoll

3.1 概述

epoll

现象调用过程

  1. 首先,执行无论BIO NOI SELECT poll都有的socket->bind->listen,如listen得到fd4->socket连接
  2. 调用epoll_create,创建fd6(叫epfd)->红黑树
  3. 调用epoll_ctl(fd6,add,fd4,accept),把fd4加入到fd6红黑树中
  4. 调用epoll_wait等待一个链表,这时链表没数据
  5. 当客户端通过网卡发送消息时,会在fd4的buffer中写数据,并且做一个延伸处理(原来的中断中加入延伸逻辑),将fd4在fd6中查找,并改变状态复制到链表中,这时链表含有fd4。
  6. 这时调用epoll_wait的话直接得到有状态的IO。

系统调用过程

  1. 调用socket()返回一个fd,例如fd1
  2. 调用bind(fd1,9090),将fd和端口绑定起来
  3. 调用listen(),监听端口
  4. 调用epoll_create(),产生一个红黑树,当然也会产生一个fd代表这个红黑树,例如fd2
  5. 调用epoll_ctl(fd2,add,fd1,accept),意思是在红黑树添加fd1,并且关注的是accept事件
  6. 调用epoll_wait(),它等待的是一个链表
  7. 当有一个连接过来,即是fd1的accept事件,就会将fd1转移到链表中,epoll_wait拿到了后accept产生一个fd3,调用epoll_ctl(fd2,add,fd3,recv)重新加入到红黑树
  8. 当这个链接发消息过来,就会将这个fd3移动到链表里
  9. 那么epoll_wait拿到的都是有状态的IO

优势:

  1. epoll直接调用epoll_ctl增量加入新的fd,解决重复传入fds问题,
  2. 在内核中做了将有状态的IO直接copy到链表,调用epoll_wait直接拿到有状态的IO,解决了遍历fds问题

3.2 举例

public class SocketMultiplexingSingleThreadv1 {

    private ServerSocketChannel server = null;
    private Selector selector = null;
    int port = 9090;

    public void initServer() {
        try {
            //这个server相当于listen状态的fd4
            server = ServerSocketChannel.open();
            //设置non-blocking
            server.configureBlocking(false);
            //绑定端口
            server.bind(new InetSocketAddress(port));
            /*
             * 创建多路复用器 优先使用epoll
             * select poll:创建一个数组用来存放fds
             * epoll:调用epoll_create==>fd3
             */
            selector = Selector.open();
            /*
             * select poll:把fd4放入数组
             * epoll:调用epoll_ctl(fd3,add,fd4,epoll_in) 将fd4放入 上面epoll_create得到的内存空间fd3(实际运行时在selector.select时才放进去,懒加载),注册accept事件
             */
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {
                /*
                 * select poll:查看数组中有的fds
                 * epoll:查看fd3 红黑树中的fds
                 */
                Set<SelectionKey> keys = selector.keys();
                System.out.println(keys.size()+"   size");
                /*
                 * select poll:传入fds,查找有状态的IO
                 * epoll:epoll_wait
                 */
                while (selector.select(500) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    //无论基于那种多路复用器,得到有状态的IO后,都要自行R/W
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        //是否需要accept。服务端未accept前,客户端建立连接并发消息,就会进到这里
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                            //是否可读。即是否已经分配进程处理socket连接
                        } else if (key.isReadable()) {
                            readHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //accept得到client
            SocketChannel client = ssc.accept();
            //设置client non-blocking 为了读不阻塞
            client.configureBlocking(false);
            //创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            /*
             *注册当前fd对应的read buffer
             */
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
        service.start();
    }
}

这个红黑树观察的文件描述符是有限的,属性max_user_watch

4. epoll单线程/多线程

场景:服务端接收到客户端数据,写回给客户端

单线程_多线程epoll

4.1 单线程

  1. 主线程通过selector查找有状态的IO,然后调用readHandler(),readHandler读取后直接write

  2. 主线程通过selector查找有状态的IO,然后调用readHandler(),readHandler里将当前fd.register selector OP_WRITE,那么再查找有状态的IO时,就会触发writeHandler(),再去write

4.2 多线程

  1. 主线程通过selector查找有状态的IO,然后new thread()去执行readHandler(),readHandler里将当前fd.register selector OP_WRITE,但这个操作需要时间,当抛出线程,立刻返回,再一次通过selector查找有状态的IO,仍然会有当前这个fd。所有会重复调起readHandler()
  2. 当1中注册了写事件,而写事件是看send-q有没空位,所以当主线程再次通过selector查找有状态的IO,会重复new thread()调起writeHandler

解决重复调用办法:在调用readHandler()和writeHandler()前加入key.cancle(epoll_ctl(del))

4.2.1 多线程弊端:

  1. 考虑资源利用,充分利用cpu核数。

  2. 考虑有一个fd执行耗时长,在一个线程里会阻塞后续的fd的处理

    但是会重复register和cancle系统调用。

4.3 解决

  • 当有N个fd有R/w处理时,将N个FD分组,每一个组一个selector,将一个selector压到一个线程上
  • 最好的线程数量时cpu个数或者cpu个数*2
  • 其实单看一个线程,里面只有一个selector,有一部分fd,它们在自己的cpu上执行,代表会有多个selector并行,并且线程内部线性的,最终是并行的fd被处理
  • 那么我也可以拿出一个线程的selector只关注accept,然后将接受的fd分配给其他线程selector
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容