目录
所谓的事件就绪,并非只有数据就绪这种情况,比如监听套接字listen_fd收到了新的连接请求,这也算 读事件就绪的一种,用户可以通过accpet函数将新连接拷贝到上层进行维护,所谓的读不正是将数据从内核拷贝到上层的过程吗?
select的执行过程 以select函数调用为界,分为两部分。第一部分是告诉 select 哪些fd是需要被关注的;第二部分是 事件就绪以后的拷贝工作(调用read/recv/write/send函数)。
定义readfds_array数组的原因
在接收数据的时候,可能存在下面两种情况
根本原因在于 第二个参数 readfds 是一个输入输出型参数。输入时,用户告诉内核,哪些文件描述符需要被关注;输出时,readfds将原本的内容清空,换上输出结果,以此告诉用户,有哪些文件描述符上的事件已经就绪。
因此,我们需要一个数组来临时存储下一次仍然需要被关注的fd ,这个数组便是readfds_array
- int main()
- {
- //创建套接字
- int listen_fd = TcpSocket::CreateSocket();
- //绑定端口号
- TcpSocket::Bind(listen_fd, 8080);
- //设为监听套接字
- TcpSocket::Listen(listen_fd);
-
- #define NUM 1024
- int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
- for(int i=0; i< NUM ;i++){
- readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
- }
- readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
- fd_set *readfds = nullptr;
- while (1)
- {
- int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
- FD_ZERO(readfds); //清空读事件集合
- for (size_t i = 0; i < NUM; i++) //以循环的方式将数组里的fd添加到 读事件集合中
- {
- if (readfds_array[i] == -1)
- continue;
-
- //到这一步,readfds_array[i]一定存了文件描述符
- FD_SET(readfds_array[i], readfds);
- //获取到文件描述符的最大值
- max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
- }
-
- //设置阻塞时间为5s
- struct timeval timeout = {5,0};
- int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
- switch (n)
- {
- case -1:
- //select调用出错
- break;
- case 0:
- //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
- break;
- default:
- /******************* 第二部分 *****************/
- //说明有文件描述符上的事件就绪
- break;
- }
- }
-
- return 0;
- }
这部分要重点了解的是事件就绪后如何处理的问题。(只不过我们这里只关注读事件就绪)。
事件就绪有可能是读事件、写事件就绪、异常事件就绪。此时我们并不知道是哪种事件的哪个fd就绪,所以要逐一去判断。如果 readfds_array[i] == -1,说明该位置没有存放文件描述符,直接进行下一次循环。
- int main()
- {
- //创建套接字
- int listen_fd = TcpSocket::CreateSocket();
- //绑定端口号
- TcpSocket::Bind(listen_fd, 8080);
- //设为监听套接字
- TcpSocket::Listen(listen_fd);
-
- #define NUM 1024
- int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
- for(int i=0; i< NUM ;i++){
- readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
- }
- readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
- fd_set *readfds = nullptr;
- while (1)
- {
- int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
- FD_ZERO(readfds); //清空读事件集合
- for (size_t i = 0; i < NUM; i++)
- {
- if (readfds_array[i] == -1)
- continue;
-
- //到这一步,readfds_array[i]一定存了文件描述符
- FD_SET(readfds_array[i], readfds);
- //获取到文件描述符的最大值
- max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
- }
-
- //设置阻塞时间为5s
- struct timeval timeout = {5,0};
- int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
- switch (n)
- {
- case -1:
- //select调用出错
- break;
- case 0:
- //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
- break;
- default:
- /******************* 第二部分 *****************/
- //说明有文件描述符上的事件就绪
- for (size_t i = 0; i < NUM; i++)
- {
- if (readfds_array[i] == -1) continue;
-
- //判断是不是读事件就绪(判断是否在读事件集合)
- if(FD_ISSET(readfds_array[i],readfds)){
- //说明该文件描述符上的读事件就绪
-
-
- if(readfds_array[i] == listen_fd){
- //收到了新的连接,此时可以调用accept
- int sock = TcpSocket::Accept(readfds_array[i]);
- if(sock > 0){
- //说明获取成功,此时需要把这个文件描述符加入到 readfds_array[i]中
- //因此需要遍历readfds_array数组,看看有哪个位置未被使用,第0个位置固定给监听套接字使用
- int pos = 1;
- for (; pos < NUM; pos++)
- {
- if (readfds_array[i] == -1) break; //-1表示该位置未被使用
- }
-
- //如果pos
- if (pos < NUM)
- {
- readfds_array[pos] = sock;
- }
- else{
- //没有位置可以放说明服务器满载了
- close(sock);
- }
- }
- }
- else{
- //收到了对方发来的数据,此时可以调用recv/read
- char buffer[1024] = {0};
- ssize_t s = recv(readfds_array[i],buffer,sizeof(buffer)-1,0);
- if (s > 0)
- {
- // 将数据拷贝到上层,接下来可以处理数据了
- }
- else if (s == 0)
- {
- //对端关闭连接了
- close(readfds_array[i]);
- readfds_array[i] = -1; //空出位置给其他fd使用
- }
- else{
- //读取失败
- close(readfds_array[i]);
- readfds_array[i] = -1; //空出位置给其他fd使用
- }
- }
- }
-
- //判断是不是写事件就绪
- // ... ...
-
- //判断是不是异常事件就绪
- // ... ...
-
- }
-
- break;
- }
- }
-
- return 0;
- }
可以一次等待多个fd,可以让我们等待的时间重叠,一定程度上可以提高IO的效率。尽管多线程也可以实现,但是多线程的运行是受到CPU调度约束的,阻塞等待的时候会被加入到等待队列,事件就绪的时候再回到运行队列,频繁换队列存在一定的损耗。
缺点一
每次都要重新设置哪些文件描述符需要被关注。以第二个参数readfds为例,readfds是一个输入输出型参数,输入时告诉哪些文件描述符需要被关注;输出的时候,内核通知我们哪些文件描述符上的事件就绪了。
但是这样一来,我们最开始设置的输入就被输出结果给覆盖了,因此,下一次调用select的时候我们要重新设置第二个参数。
缺点二
每次调用select都需要把fd集合从用户层拷贝到内核,这个开销在fd较多时会很大。同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
缺点三
fd_set是一个位图结构,每次可以让select 关注的文件描述符数是有限的,只有1024个。