共计 1220 个字符,预计需要花费 4 分钟才能阅读完成。
1. 处理多个 socket 链接的方法
阻塞模式下服务端要解决多个客户链接的问题的 3 个思路:
- 每个客户端的 socket 对应一个内核线程,在这个线程内部进行阻塞的 read
- 单线程,自己记录一个 socket 列表,循环去内核中查询 socket 是否 ready
- 单线程,系统提供一个 ready 状态的 socket 列表,主线程从这个列表中处理 socket
思路 1,如果链接很多(C10k)线程就会很多,消耗系统资源,并增加调度成本(Java BIO)。
思路 2,每次都要遍历一边所有 socket,链接很多时效率低,可能大部分链接都没数据(select)。
思路 3,比较理想 (epoll)。
2. select 函数
2.1. 使用方法
函数原型:
select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
select() 检查 readfds
, writefds
中的 io 描符是否可读、可写了,如果有 ready 状态的,函数就返回。nfds
是总共 fd 的个数,而不是最大的 fd。
使用 select 函数步骤:
- 初始化 fd_set,把要监控的 fd 仍进去
- 调用 select, 阻塞
- 阻塞结束后,遍历查看 fd_set 中的 ready 的 socket
fd_set 是什么?
一个结构体:
typedef struct fd_set {__int32_t fds_bits[__DARWIN_howmany(__DARWIN_FD_SETSIZE, __DARWIN_NFDBITS)];
} fd_set;
结构体中有一个数组,默认是是 1024,这就是 linux 中 select 的函数限制最大链接数的原因。
重新编译内核才能提高这个数字。FD_ISSET
就是取对应位置状态值(0,1),并且在用户空间,
用户需要遍历编一遍这个数组来检查是哪个 socket 有数据。
select 的内部实现:
- readfds 从用户空间传递到内核空间
- 将当前进程加入到 readfds 中的每个 socket 的等待队列
- 当 socket 来数据了就把 线程唤醒 (移出等待队列)
- 把有数据的 fds 从内核空间搞到用户空间
- 用户空间看一遍 fds,知道哪个 socket 有数据了,然后 read、accept
select 的问题:
- 每次调用 select 就要把 readfds 传到内核,wake 的时候再拿回来需要传递 1024 * 4 bytes
- 每次需要把当前线程加入到所有 socket 的等待队列
- 每次需要遍历一遍 readfds 来查看那个 socket 有数据
每次调用 select 都会有以上两次传递和两次遍历,当链接个数多时,性能下降比较快:
select 可能的改进:
- readfds 一直都在内核中维护,不要每次都送进来
- 可以动态单个变更内核中的 readfds
- 就绪列表,传给内核一个指针,内核把这个指针指向就绪的 sockets (避免来回传递所有的 socket)
正文完