The purpose of the select system call is to monitor readable, writable and exception events on the file descriptor that the user is interested in within a specified period of time.
Why does the select model appear?
First look at the following code:
int iResult = recv(s, buffer,1024);
This is used to receive data. In the socket in the default blocking mode, recv will block there until the socket There is data to be read on the interface connection. The recv function will not return until the data is read into the buffer, otherwise it will always be blocked there. If this happens in a single-threaded program, the main thread (there is only one default main thread in a single-threaded program) will be blocked, so that the entire program is locked here. If no data is ever sent, the program will be blocked. Locked forever. This problem can be solved with multi-threading, but in the case of multiple socket connections, this is not a good option and has poor scalability.
Look at the code again:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul); iResult = recv(s, buffer,1024);
This time the recv call will return immediately regardless of whether there is data to be received on the socket connection. The reason is that we use ioctlsocket to set the socket to non-blocking mode. However, if you follow it, you will find that when there is no data, recv does return immediately, but it also returns an error: WSAEWOULDBLOCK, which means that the requested operation was not completed successfully.
Many people may say after seeing this, call recv repeatedly and check the return value until it succeeds, but the efficiency of this is very problematic and the overhead is too much.
The emergence of the select model is to solve the above problems.
The key to the select model is to use an orderly manner to uniformly manage and schedule multiple sockets.
As shown above, the user first adds the socket that requires IO operations to the select, and then blocks and waits for the select system call to return. When data arrives, the socket is activated and the select function returns. The user thread formally initiates a read request, reads the data and continues execution.
From a process perspective, there is not much difference between using the select function for IO requests and the synchronous blocking model. There are even additional operations of adding a monitoring socket and calling the select function, which makes the efficiency even worse. However, the biggest advantage of using select is that users can process multiple socket IO requests at the same time in one thread. Users can register multiple sockets and then continuously call select to read the activated sockets to achieve the purpose of processing multiple IO requests simultaneously in the same thread. In the synchronous blocking model, this purpose must be achieved through multi-threading.
The pseudo code of the select process is as follows:
{ select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } }
#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);
Parameter description:
maxfdp: the monitored file descriptor The total number, which is 1 greater than the maximum value of file descriptors in all file descriptor sets, because file descriptors are counted from 0;
readfds, writefds, exceptset: point to readable and writable respectively A collection of descriptors corresponding to exceptions and other events.
timeout: used to set the timeout of the select function, that is, tell the kernel how long to wait before giving up waiting. timeout == NULL means waiting for an infinite time
The timeval structure is defined as follows:
struct timeval { long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
Return value: Returns 0 for timeout; returns -1 for failure; returns an integer greater than 0 for success. This integer Represents the number of ready descriptors.
The following introduces several common macros related to the select function:
#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); //测试某个位是否被置位
select usage example:
When a file descriptor set is declared, all positions must be zeroed with FD_ZERO . Then set the bit corresponding to the descriptor we are interested in. The operation is as follows:
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);
Then call the select function and wait for the arrival of the file descriptor event in congestion; if the set time is exceeded, no longer wait. Continue to execute.
select(fd+1, &rset, NULL, NULL,NULL);
After select returns, use FD_ISSET to test whether the given bit is set:
if(FD_ISSET(fd, &rset) { ... //do something }
The following is the simplest example of using select:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> int main() { fd_set rd; struct timeval tv; int err; FD_ZERO(&rd); FD_SET(0,&rd); tv.tv_sec = 5; tv.tv_usec = 0; err = select(1,&rd,NULL,NULL,&tv); if(err == 0) //超时 { printf("select time out!\n"); } else if(err == -1) //失败 { printf("fail to select!\n"); } else //成功 { printf("data is available!\n"); } return 0; }
We run the program and Just enter some data and the program will prompt you to receive the data.
The key to understanding the select model is to understand fd_set. For the convenience of explanation, the length of fd_set is 1 byte, and each bit in fd_set Can correspond to a file descriptor fd. Then a 1-byte long fd_set can correspond to a maximum of 8 fds.
(1) Execute fd_set set; FD_ZERO(&set); then the bit representation of set is 0000,0000.
(2) If fd=5, execute FD_SET(fd,&set); and set becomes 0001,0000 (the fifth position is 1)
(3) If fd= is added again 2. fd=1, then set becomes 0001,0011
(4) Execute select(6,&set,0,0,0) and block waiting for
(5) If fd=1 , readable events occur on both fd=2, then select returns, and set changes to 0000,0011. Note: fd=5 without events is cleared.
Based on the above discussion, we can easily draw the characteristics of the select model:
(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能处理的异常情况只有一种:socket上接收到带外数据。
什么是带外数据?
带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。
我们写的select程序经常都是用于接收普通数据的,当我们的服务器需要同时接收普通数据和带外数据,我们如何使用select进行处理二者呢?
下面给出一个小demo:
#include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char* argv[]) { if(argc <= 2) { printf("usage: ip address + port numbers\n"); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); printf("ip: %s\n",ip); printf("port: %d\n",port); int ret = 0; struct sockaddr_in address; bzero(&address,sizeof(address)); address.sin_family = AF_INET; inet_pton(AF_INET,ip,&address.sin_addr); address.sin_port = htons(port); int listenfd = socket(PF_INET,SOCK_STREAM,0); if(listenfd < 0) { printf("Fail to create listen socket!\n"); return -1; } ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address)); if(ret == -1) { printf("Fail to bind socket!\n"); return -1; } ret = listen(listenfd,5); //监听队列最大排队数设置为5 if(ret == -1) { printf("Fail to listen socket!\n"); return -1; } struct sockaddr_in client_address; //记录进行连接的客户端的地址 socklen_t client_addrlength = sizeof(client_address); int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength); if(connfd < 0) { printf("Fail to accept!\n"); close(listenfd); } char buff[1024]; //数据接收缓冲区 fd_set read_fds; //读文件操作符 fd_set exception_fds; //异常文件操作符 FD_ZERO(&read_fds); FD_ZERO(&exception_fds); while(1) { memset(buff,0,sizeof(buff)); /*每次调用select之前都要重新在read_fds和exception_fds中设置文件描述符connfd,因为事件发生以后,文件描述符集合将被内核修改*/ FD_SET(connfd,&read_fds); FD_SET(connfd,&exception_fds); ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL); if(ret < 0) { printf("Fail to select!\n"); return -1; } if(FD_ISSET(connfd, &read_fds)) { ret = recv(connfd,buff,sizeof(buff)-1,0); if(ret <= 0) { break; } printf("get %d bytes of normal data: %s \n",ret,buff); } else if(FD_ISSET(connfd,&exception_fds)) //异常事件 { ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB); if(ret <= 0) { break; } printf("get %d bytes of exception data: %s \n",ret,buff); } } close(connfd); close(listenfd); return 0; }
上面提到过,,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户连接。现在,我们同样可以使用select来处理多客户问题,而不用fork。
服务器端
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <sys/time.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdlib.h> int main() { int server_sockfd, client_sockfd; int server_len, client_len; struct sockaddr_in server_address; struct sockaddr_in client_address; int result; fd_set readfds, testfds; server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(INADDR_ANY); server_address.sin_port = htons(8888); server_len = sizeof(server_address); bind(server_sockfd, (struct sockaddr *)&server_address, server_len); listen(server_sockfd, 5); //监听队列最多容纳5个 FD_ZERO(&readfds); FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中 while(1) { char ch; int fd; int nread; testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 printf("server waiting\n"); /*无限期阻塞,并测试文件描述符变动 */ result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系统默认的最大文件描述符 if(result < 1) { perror("server5"); exit(1); } /*扫描所有的文件描述符*/ for(fd = 0; fd < FD_SETSIZE; fd++) { /*找到相关文件描述符*/ if(FD_ISSET(fd,&testfds)) { /*判断是否为服务器套接字,是则表示为客户请求连接。*/ if(fd == server_sockfd) { client_len = sizeof(client_address); client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len); FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中 printf("adding client on fd %d\n", client_sockfd); } /*客户端socket中有数据请求时*/ else { ioctl(fd, FIONREAD, &nread);//取得数据量交给nread /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */ if(nread == 0) { close(fd); FD_CLR(fd, &readfds); //去掉关闭的fd printf("removing client on fd %d\n", fd); } /*处理客户数据请求*/ else { read(fd, &ch, 1); sleep(5); printf("serving client on fd %d\n", fd); ch++; write(fd, &ch, 1); } } } } } return 0; }
客户端
//客户端 #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <sys/time.h> int main() { int client_sockfd; int len; struct sockaddr_in address;//服务器端网络地址结构体 int result; char ch = 'A'; client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket address.sin_family = AF_INET; address.sin_addr.s_addr = inet_addr("127.0.0.1"); address.sin_port = htons(8888); len = sizeof(address); result = connect(client_sockfd, (struct sockaddr *)&address, len); if(result == -1) { perror("oops: client2"); exit(1); } //第一次读写 write(client_sockfd, &ch, 1); read(client_sockfd, &ch, 1); printf("the first time: char from server = %c\n", ch); sleep(5); //第二次读写 write(client_sockfd, &ch, 1); read(client_sockfd, &ch, 1); printf("the second time: char from server = %c\n", ch); close(client_sockfd); return 0; }
运行流程:
客户端:启动->连接服务器->发送A->等待服务器回复->收到B->再发B给服务器->收到C->结束
服务器:启动->select->收到A->发A+1回去->收到B->发B+1过去
测试:我们先运行服务器,再运行客户端
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的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
The above is the detailed content of Introduction to the advantages of select mechanism. For more information, please follow other related articles on the PHP Chinese website!