文章目录
- @[TOC](文章目录)
- 前言
- 文件描述符的阻塞和非阻塞
- fcntl
- 多路转接之select
- select接口
- select的缺点
文章目录
- @[TOC](文章目录)
- 前言
- 文件描述符的阻塞和非阻塞
- fcntl
- 多路转接之select
- select接口
- select的缺点
前言
关于IO,我们已经用过了不少IO接口,从最简单的printf、scanf->C语言文件接口fprintf、fscanf->系统文件接口read、write->再到我们的系统网络套接字接口send、recv。
这些接口见证了我们的学习历程,但是这些接口真的就只有读写吗? 不光有读写,还有等。
就像是我们使用scanf的时候,需要我们去输入一些字符,程序才能继续运行,否则就会一直阻塞住,这就是等的过程。
实际等的过程,其实也可以理解为,等待资源准备就绪的过程。而当等的时候,该线程是会被挂起的,这是不是一种低效的表现呢?
等待是必然的,因为毕竟没有资源就绪,必须要等,直到资源就绪。但是我们可以将等待交给别人去做,我们可以继续做我们自己的事情。
这里的别人就是我们今天所需要学习的select,具体是如何实现的我们今天就来学习。
在学习select之前,我们先来了解一下文件描述符的阻塞和非阻塞。
文件描述符的阻塞和非阻塞
之前我们学习recv的时候,最后一个参数我们讲过是设置该函数为非阻塞还是阻塞的。
实际上,我们的文件描述符是可以对它设置阻塞和非阻塞的。
fcntl
fcntl函数是Linux系统中用于操作文件描述符的一个系统调用,它允许程序对文件描述符进行各种控制操作。
这里参数中的fd就是要对哪个文件描述符进行控制操作,参数cmd用来表明要对fd做怎样的控制操作。
这个函数的返回值根据不同的cmd操作有不同的意义。
我们今天要学习的cmd参数有 F_GETFL和F_SETFL。
如果这里cmd设置为F_GETFL,用来获取fd的文件描述符状态,如果获取成功,返回值会返回它的描述符状态,这个返回值是一个int,其实我们根据以往的经验就知道这是一个位图。
如果设置cmd设置为F_SETFL,用来设置fd的文件描述符状态,这时我们就可以传第三个参数将其设置为非阻塞。
示例代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
void SetUnblock(int fd)
{// 获取fd的属性int fl = fcntl(fd, F_GETFL);if (fl < 0){// 获取失败perror("F_GETFD Error");return;}int n = fcntl(fd, F_SETFL, fl | O_NONBLOCK);if (n < 0){perror("Set Nonblock Error");}else{std::cout << "Fd:" << fd << " ,set nonblock done" << std::endl;}
}
int main()
{// 将标准输入设置为非阻塞会发生什么?SetUnblock(0);while (1){char buffer[1024];// ssize_t n = scanf("%s",buffer);ssize_t n = read(0, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n - 1] = 0;std::cout << buffer << std::endl;}if (n < 0){std::cout << "???" << std::endl;std::cout << "errno: " << errno << " ,str: "<<strerror(errno) << std::endl; sleep(1);}}return 0;
}
运行后可以发现我们的标准输入被设置成了非阻塞,如果我们键盘不输入数据,write就会一直返回-1,可是返回-1不是代表错误吗? 我们来看看错误码的描述。
错误码为11,描述为Resource temporarily unavailable(资源暂时不可用)。
这其实就是因为我们设置非阻塞的原因,所以我们要判断是read发生错误还是非阻塞了,我们就需要对errno进行EWOULDBLOCK判断。
多路转接之select
select作为多路转接的接口之一, 其作用就是将需要关心的文件描述符交给它来管理,可以让他一次性等待多个文件描述符,我们的线程只需要等select就可以了。如果有文件描述符资源就绪,就会立马返回。试想我们之前写过的服务器,我们将每一个连接都用一个线程来管理,可是我们一个进程可以创建的线程是有限的,而如果对于用户访问量大的服务来讲,这种一个线程维护一个连接的方式是非常低效的,因为每一个客户端不可能时时刻刻都在发送数据,这种情况必然会带来许多进程长时间处于挂起状态导致系统资源浪费。
select接口
参数int nfds,是要所需要关心的所有fd中最大的fd + 1。
参数fd_set *readfds,fd_set其实就是一个位图,我们可以用FD_SET,FD_ZERO,FD_ISSET来对fd_set进行设置,这个readfds就是我们要关心读事件的文件描述符是哪些,它是输入输出型函数。
参数fd_set *writefds,与readfds差不多,只不过它关心的是写事件,它是输入输出型函数。
参数fd_set *exceptfds,与readfds差不多,只不过它关心的是错误事件,它是输入输出型函数。
参数struct timeval *timeout,该结构体有两个成员变量代表秒和毫秒,用来设置对select的等待时间,当超过这个等待时间没有文件描述符就绪了,就会返回0,它是输入输出型函数。
示例代码
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#include <cstring>#define MAX_FDS 64
#define INVALID_FD -1const std::string default_ip = "0.0.0.0";
const uint16_t default_port = 8080;class SelectServer
{
public:SelectServer(uint16_t port = default_port): _port(port) {}inline void InitFds(){_fds[0] = _listensock._sockfd;for (int i = 1; i < MAX_FDS; ++i){// 第一次for循环// 初始化_fds_fds[i] = -1;}}void Init(){_listensock.Init();_listensock.Bind(AF_INET, default_ip, _port);_listensock.Listen();InitFds();}void UpdateSet(int *max_fd, fd_set *set){std::cout << "现有fds: ";for (int i = 0; i < MAX_FDS; ++i){// 第二次for循环// 更新readfdsif (_fds[i] == INVALID_FD){continue;}std::cout << _fds[i] << " ";FD_SET(_fds[i], set);if (*max_fd < _fds[i]){*max_fd = _fds[i];}}std::cout << std::endl;}void Accepter(){struct sockaddr_in tmp;socklen_t len = sizeof tmp;int newfd = accept(_listensock._sockfd, (struct sockaddr *)&tmp, &len);for (int i = 0; i < MAX_FDS; ++i){//第四次for循环if (_fds[i] == INVALID_FD){_fds[i] = newfd;lg(Info, "Get A New Sockfd:%d", newfd);break;}if (i == MAX_FDS){lg(Warning, "Fds Is Full, Newfd:%d", newfd);close(newfd);return;}}}void Handler(int fd, int i){char buffer[1024];memset(buffer, 0, sizeof buffer);int n = read(fd, buffer, sizeof buffer);if(n > 0){buffer[n] = 0;std::string mes = buffer;std::cout << mes;}else if (n < 0){lg(Warning, "Read Error...");_fds[i] = INVALID_FD;close(_fds[i]);}else{lg(Info, "Foreign Host Closed...");_fds[i] = INVALID_FD;close(_fds[i]);}}void Dispatch(const fd_set *readfds, const fd_set *writefds, const fd_set *exceptfdss){for (int i = 0; i < MAX_FDS; ++i){// 第三次循环,看_fds里哪些准备好了if(_fds[i] == INVALID_FD){continue;}else if (FD_ISSET(_fds[i], readfds)){if (_fds[i] == _listensock._sockfd){// acceptAccepter();continue;}Handler(_fds[i], i);}}}void Start(){fd_set readfds;struct timeval tv = {5, 0};while (1){int max_fd = -1;FD_ZERO(&readfds);UpdateSet(&max_fd, &readfds);tv = {5, 0};// tv = {0, 0};int n = select(max_fd + 1, &readfds, nullptr, nullptr, &tv); // 如果tv传nullptr将成为阻塞等待,传{0,0}会变成非阻塞等待if (n == 0){lg(Info, "Select Time Out...");continue;}else if (n < 0){lg(Warning,"Select Error...");std::cout << "errno:" << errno << " strerror:" << strerror(errno) << std::endl;}else{Dispatch(&readfds, nullptr, nullptr);}}}~SelectServer(){_listensock.Close();}private:int _fds[MAX_FDS];Socket _listensock;uint16_t _port;
};
通过select可以让一个线程同时管理多个连接!!
select的缺点
缺点1:由于其参数很多都是输入输出型函数,需要不断重置结构体。
缺点2:每次进行一次select都是一次从用户态拷贝数据到内核态的过程。
缺点3:一旦有文件描述符资源准备就绪,还是要遍历一次fds,仍有资源开销。
缺点4:它所能管理的fd有限,这个大小取决于fd_set的位数,可以通过查看FD_SETSIZE()来看能管理最大的fd。
所以后面我们还需要学习多路转接更好的接口poll和epoll。