Linux NIO 系列(04-1) select

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

select 系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

一、select 机制的优势

为什么会出现 select 模型?

先看一下下面的这句代码:

int iResult = recv(s, buffer,1024);

这是用来接收数据的,在默认的阻塞模式下的套接字里,recv 会阻塞在那里,直到套接字连接上有数据可读,把数据读到 buffer 里后 recv 函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

再看代码:

int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);

这一次 recv 的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用 ioctlsocket 把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv 确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。

看到这里很多人可能会说,那么就重复调用 recv 并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。

select 模型的出现就是为了解决上述问题。

select 模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。

如上所示,用户首先将需要进行 IO 操作的 socket 添加到 select 中,然后阻塞等待 select 系统调用返回。当数据到达时,socket 被激活,select 函数返回。用户线程正式发起 read 请求,读取数据并继续执行。

从流程上来看,使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

select 流程伪代码如下:

{
select(socket);
while(1) {
sockets = select();
for(socket in sockets) {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
}
}

二、select API 介绍与使用

2.1 select

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明:

  • maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大 1,因为文件描述符是从 0 开始计数的;

  • readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

  • timeout:用于设置 select 函数的超时时间,即告诉内核 select 等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间

timeval 结构体定义如下:

struct timeval {
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
};

返回值:超时返回 0 ;失败返回 -1;成功返回大于 0 的整数,这个整数表示就绪描述符的数目。

2.2 fd_set 集合操作

以下介绍与 select 函数相关的常见的几个宏:

#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset); // 一个 fd_set 类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); // 清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); // 设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); // 测试某个位是否被置位

2.3 select 使用范例

当声明了一个文件描述符集后,必须用 FD_ZERO 将所有位置零。之后将我们所感兴趣的描述符所对应的位置位,操作如下:

fd_set rset;
int fd;
FD_ZERO(&rset);
FD_SET(fd, &rset);
FD_SET(stdin, &rset);

然后调用 select 函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。

select(fd+1, &rset, NULL, NULL,NULL);

select 返回后,用 FD_ISSET 测试给定位是否置位:

if(FD_ISSET(fd, &rset) {
...
//do something
}

三、深入理解 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) 阻塞等待

(5)若 fd=1, fd=2 上都发生可读事件,则 select 返回,此时 set 变为 0000,0011。注意:没有事件发生的 fd=5 被清空。

基于上面的讨论,可以轻松得出 select 模型的特点:

(1)可监控的文件描述符个数取决与 sizeof(fd_set) 的值。我这边服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符是 512 * 8 = 4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

(2)将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd,一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数。

(3)可见 select 模型必须在 select 前循环加 fd,取 maxfd,select 返回后利用 FD_ISSET 判断是否有事件发生。

四、select总结

select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以 cat/proc/sys/fs/file-max 查看。32 位机默认是 1024 个。64 位机默认是 2048.

  2. 对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次 select() 都要通过遍历 FD_SETSIZE 个 Socket 来完成调度,不管哪个 Socket 是活跃的,都遍历一遍。这会浪费很多 CPU 时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是 epoll 与 kqueue 做的。

  3. 需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

当然 select 也有优点:兼容性好,不管是 Linux 还是 Windows 都支持 select。

附1:select 网络编程代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> #define SERVER_PORT 8888
#define OPEN_MAX 3000
#define BACKLOG 10
#define BUF_SIZE 1024 void main() {
int i, j, maxi;
int listenfd, connfd, sockfd; // 定义套接字描述符
int nready; // 接受 pool 返回值
int recvbytes; // 接受 recv 返回值 char recv_buf[BUF_SIZE]; // 发送缓冲区
fd_set readSet, totalSet; // 定义读集合,备份集合 // 定义 IPV4 套接口地址结构
struct sockaddr_in seraddr; // service 地址
struct sockaddr_in cliaddr; // client 地址
int cliaddr_len; // 初始化IPV4套接口地址结构
seraddr.sin_family = AF_INET; // 指定该地址家族
seraddr.sin_port = htons(SERVER_PORT); // 端口
seraddr.sin_addr.s_addr = INADDR_ANY; // IPV4的地址
bzero(&(seraddr.sin_zero), 8); // 启动 server
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr));
listen(listenfd, BACKLOG); // select 模型处理过程
// 1. 初始化套接字集合,添加监听 socket 到这个集合
FD_ZERO(&totalSet);
FD_SET(listenfd, &totalSet);
maxi = listenfd; while(1) {
// 2. 将集合的一个拷贝传递给 select 函数。当有事件发生时,select 移除未决的 socket 然后返回。
// 也就是说 select 返回时,集合 readSet 中就是发生事件的 readSet
readSet = totalSet;
int nready = select(maxi + 1, &readSet, NULL, NULL, NULL);
if (nready > 0) {
if (FD_ISSET(listenfd, &readSet)) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); FD_SET(connfd, &totalSet);
maxi = connfd;
if (--nready == 0) {
continue;
}
} for (i = listenfd + 1; i <= maxi; i++) {
sockfd = i;
if (FD_ISSET(sockfd, &readSet)) {
recvbytes = read(sockfd, recv_buf, sizeof(recv_buf));
if (recvbytes == 0) { // 客户端关闭
close(sockfd);
FD_CLR(sockfd, &totalSet);
} else if (recvbytes == -1) { // read 异常
perror("read error");
exit(1);
} else { // 正常读取数据
write(sockfd, recv_buf, recvbytes);
printf("receive %s\n", recv_buf);
}
}
}
}
}
}

参考:

  1. Linux编程之select

每天用心记录一点点。内容也许不重要,但习惯很重要!

Linux NIO 系列(04-1) select的更多相关文章

  1. Linux NIO 系列(04-4) select、poll、epoll 对比

    目录 一.API 对比 1.1 select API 1.2 poll API 1.3 epoll API 二.总结 2.1 支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统 ...

  2. Linux NIO 系列(02) 阻塞式 IO

    目录 一.环境准备 1.1 代码演示 二.Socket 是什么 2.1 socket 套接字 2.2 套接字描述符 2.3 文件描述符和文件指针的区别 三.基本的 SOCKET 接口函数 3.1 so ...

  3. Linux NIO 系列(03) 非阻塞式 IO

    目录 一.非阻塞式 IO 附:非阻塞式 IO 编程 Linux NIO 系列(03) 非阻塞式 IO Netty 系列目录(https://www.cnblogs.com/binarylei/p/10 ...

  4. Linux NIO 系列(04-2) poll

    目录 一.select 和 poll 比较 二.poll API 附1:linux 每个进程IO限制 附2:poll 网络编程 Linux NIO 系列(04-2) poll Netty 系列目录(h ...

  5. Linux NIO 系列(04-3) epoll

    目录 一.why epoll 1.1 select 模型的缺点 1.2 epoll 模型优点 二.epoll API 2.1 epoll_create 2.2 epoll_ctl 2.3 epoll_ ...

  6. Linux Shell系列教程之(十四) Shell Select教程

    本文是Linux Shell系列教程的第(十四)篇,更多Linux Shell教程请看:Linux Shell系列教程 在上一篇文章:Linux Shell系列教程之(十三)Shell分支语句case ...

  7. (转)Linux Shell系列教程之(十四) Shell Select教程

    本文属于<Linux Shell 系列教程>文章系列,该系列共包括以下 18 部分: Linux Shell系列教程之(一)Shell简介 Linux Shell系列教程之(二)第一个Sh ...

  8. Java NIO系列教程(三) Channel之Socket通道

    目录: <Java NIO系列教程(二) Channel> <Java NIO系列教程(三) Channel之Socket通道> 在<Java NIO系列教程(二) Ch ...

  9. Java NIO系列教程(三-十二) Buffer

    原文链接     作者:Jakob Jenkov     译者:airu     校对:丁一 Java NIO中的Buffer用于和NIO通道进行交互.如你所知,数据是从通道读入缓冲区,从缓冲区写入到 ...

随机推荐

  1. VS2017/VS2019 git Authentication failed for "XXXXXXXXXx"

    解决办法: 控制面板,凭证管理==>删掉 对应代码仓库地址的凭证.删掉,是删掉.因为我更新了还是没有用.

  2. python自带的split VS numpy中的split比较

    Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则分隔 num+1 个子字符串 str1.split() 里面的参数,可以是空格,逗号,字符串啥的,具体应用与 ...

  3. shell 中 比较 diff

    diff 可以用来比较文件和文件夹是否相同 比较文件 diff file1 file2 >/dev/null 比较文件夹 diff -rNaq dir1 dir2 >/dev/null - ...

  4. 正则finditer的使用

    import re #\. 是刚需必须有 d+ 必须一个或多个数字 pattern = re.compile(r'\d+\.\d*') d = pattern.finditer('3.14159265 ...

  5. JavaScript 标准参考教程(alpha) 阮一峰

    JavaScript 标准参考教程(alpha)http://javascript.ruanyifeng.com/#introduction

  6. 热修复设计之AOT/JIT&dexopt 与 dex2oat (一)

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680本篇文章将先从AOT/JIT&dexopt 与 dex2o ...

  7. C++ 中的 const、引用和指针的深入分析

    1,关于 const 的疑问: 1,const 什么时候为只读变量,什么时候是常量: 1,const 从 C 到 C++ 进化的过程中得到了升级,const 在 C++ 中不仅仅像在 C 中声明一个只 ...

  8. Java script-1

    什么是JavaScript? JavaScript是一种直译式脚本语言,一种轻量级的脚本语言. 什么是脚本语言? Script language指的是它不具备开发操作系统的能力,而是只用来编写控制其他 ...

  9. postmortem报告【第二组】

    一.alpha阶段的经验教训 1.针对 进度规划不到位,任务完成速度慢 的问题,引入teambition规范任务管理,每周组会验收上一周任务,发布下一周任务,对各组员是否完成任务以及完成质量进行评价. ...

  10. Codeforces 1188A 构造

    题意:给你一颗树,树的边权都是偶数,并且边权各不相同.你可以选择树的两个叶子结点,并且把两个叶子结点之间的路径加上一个值(可以为负数),问是否可以通过这种操作构造出这颗树?如果可以,输出构造方案.初始 ...