Java NIO分析(3): I/O多路复用之select系统调用
前面讲了一些Java-NIO分析-2-I-O多路复用历史杂谈, 谈到了多路复用的发展历史
以及为什么需要它。今天讲广受各大内核支持的select
系统调用,select
允许进程
指定内核等待1个或者多个事件的任何一个发生, 并且只在有它们发生之后或者等待一段时间后才唤醒进程。
1. select介绍
使用select, 我们即使应用进程里只有1个线程也能够接受多个连接并且做出处理,
因为应用进程不需要阻塞在socket的read,write或者accept系统调用上, 而是
内核告诉应用有事件到来了, 应用进程遍历fd_set
看是哪个fd
就绪了,然后再处理
fd_set
称为描述符集
,通常是一个整数数组,其中每个整数的每一位对应1个描述符的状态.
select的函数签名是:
|
|
其中,返回值表示fd_set
里就绪的元素总个数,包括读,写,异常fd_set
第一个参数表示待测试的fd个数,值一般是待测试的fd总数+1
中间仨代表要监听的读set, 写set和异常set
最后一个参数代表select每个fd经历的时间
操作描述符集
的方法是4个宏:
- FD_ZERO 清除fdset里的所有位
- FD_SET 开启某fd在fdset里对应的位, 一般就置1
- FD_CLR 清除某fd在fdset里对应的位, 一般就置0
- FD_ISSET 判断fdset里是否包含某个fd的位
socket描述符的读就绪条件有:
- 该socket的读缓冲区的数据字节数 >= 读缓冲区低水位标记的当前大小
- 连接的读半关闭(FIN)
- socket是监听socket且已完成的连接数不为0
- socket有异常需要处理
socket描述符的写就绪条件有:
- 该socket的发送缓冲区的可用空间字节数 >= socket发送缓冲区低水位的大小
- 连接的写半关闭
- 使用非阻塞式的connect的socket已建立连接
- socket有异常要处理
通俗点说就是, 缓冲区是一个大小为n的仓库,数据是货物。 读缓冲区就类似于,你去取货,首先要货物有一定数量(读缓冲区低水位标记), 不然就拿不到货了 写缓冲区好比,你要把货物入库,仓库得还有剩余空间(写缓冲区低水位标记), 不然仓库放不下
2. select写server实战
下面是一个使用c写的echo server
示例,可以直接编译运行
|
|
主要有5个步骤
- 新建socket, 用于监听端口
- socket的fd绑定端口
- socket监听
- 初始化fd_set
- 发起select系统调用获取所有fd_set,寻找和处理事件
|
|
然后使用任何tcp测试工具都能测, 例如好用的nc
, 多开几个终端用nc去发消息,程序也能正常处理
|
|
3. select总结
select是早期berkly随着tcp/ip协议栈一起发出来的。有非常良好的兼容性,各个平台都支持, 是早期I/O多路复用比较好的方案
select的fd_size(最大描述符数)只有1024, 是在头文件里用宏写死的, 早期bsd内核最多只能开20个进程,在当时看来1024长度的fd_set已经大到用不完了, 放在今天肯定是不够了
一旦fd_set里任意fd上有事件发生,内核会立刻返回,将数据从内核空间拷贝到用户空间,然后应用进程需要遍历整个 fd_set去寻找哪个fd有事件了。
所以,select的缺点主要有:
- 最大并发限制: 1024个描述符
- 内核/用户空间的fd_set数据拷贝
- 遍历整个fd_set集合效率低,集合越大浪费的时间越多
为了解决这些问题,后人主要有2个改进,分别是poll
和epoll
, 后面我们会讲到。
- 原文作者:Chris Wang
- 原文链接:https://www.sound2gd.wang/post/java-nio%E5%88%86%E6%9E%903-i/o%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E4%B9%8Bselect%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。