Le but de l'appel système select est de surveiller les événements lisibles, inscriptibles et d'exception sur le descripteur de fichier qui intéresse l'utilisateur dans une période de temps spécifiée.
Pourquoi le modèle de sélection apparaît-il ?
Regardez d'abord le code suivant :
int iResult = recv(s, buffer,1024);
Ceci est utilisé pour recevoir des données dans le socket en mode de blocage par défaut, recv y bloquera. La fonction recv ne reviendra que lorsque. il y a des données à lire sur la connexion socket et les données sont lues dans le tampon, sinon elles y resteront bloquées. Si cela se produit dans un programme monothread, le thread principal (il n'y a qu'un seul thread principal par défaut dans un programme monothread) sera bloqué, de sorte que l'ensemble du programme sera verrouillé ici si aucune donnée n'est jamais envoyée. sera bloqué pour toujours. Ce problème peut être résolu avec le multithreading, mais dans le cas de connexions à plusieurs sockets, ce n'est pas une bonne option et a une faible évolutivité.
Regardez à nouveau le code :
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul); iResult = recv(s, buffer,1024);
Cette fois, l'appel recv reviendra immédiatement, qu'il y ait ou non des données à recevoir sur la connexion socket. La raison en est que nous utilisons ioctlsocket pour définir le socket en mode non bloquant. Cependant, si vous le suivez, vous constaterez que lorsqu'il n'y a pas de données, recv renvoie immédiatement, mais il renvoie également une erreur : WSAEWOULDBLOCK, ce qui signifie que l'opération demandée n'a pas été terminée avec succès.
Beaucoup de gens peuvent dire après avoir vu cela, appelez recv à plusieurs reprises et vérifiez la valeur de retour jusqu'à ce que cela réussisse, mais l'efficacité de cette opération est très problématique et la surcharge est trop importante.
L'émergence du modèle sélectionné vise à résoudre les problèmes ci-dessus.
La clé du modèle sélectionné est d'utiliser une manière ordonnée de gérer et de planifier uniformément plusieurs sockets.
Comme indiqué ci-dessus, l'utilisateur ajoute d'abord le socket qui nécessite des opérations d'E/S à la sélection, puis bloque et attend le retour de l'appel système de sélection. Lorsque les données arrivent, le socket est activé et la fonction de sélection revient. Le thread utilisateur lance formellement une demande de lecture, lit les données et poursuit l'exécution.
Du point de vue du processus, il n'y a pas beaucoup de différence entre l'utilisation de la fonction select pour les requêtes IO et le modèle de blocage synchrone. Il existe même des opérations supplémentaires consistant à ajouter un socket de surveillance et à appeler la fonction select, ce qui rend l'efficacité. encore pire. Cependant, le plus grand avantage de l’utilisation de select est que les utilisateurs peuvent traiter simultanément plusieurs requêtes d’E/S de socket dans un seul thread. Les utilisateurs peuvent enregistrer plusieurs sockets, puis appeler en continu select pour lire les sockets activés afin d'atteindre l'objectif de traiter plusieurs demandes d'E/S simultanément dans le même thread. Dans le modèle de blocage synchrone, cet objectif doit être atteint grâce au multithread.
Le pseudo-code du processus de sélection est le suivant :
{ 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);
Description du paramètre :
maxfdp : surveillé Le nombre total de descripteurs de fichiers, qui est supérieur de 1 à la valeur maximale des descripteurs de fichiers dans tous les ensembles de descripteurs de fichiers, car les descripteurs de fichiers sont comptés à partir de 0
readfds, writefds, exceptset : pointez vers ; le disponible Une collection de descripteurs correspondant à des événements tels que lecture, écriture et exception.
timeout : utilisé pour définir le délai d'attente de la fonction select, c'est-à-dire indiquer au noyau combien de temps attendre avant d'abandonner. timeout == NULL signifie attendre un temps infini
La structure timeval est définie comme suit :
struct timeval { long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
Valeur de retour : le délai d'attente renvoie 0 ; l'échec renvoie -1 ; supérieur à 0, cet entier représente le nombre de descripteurs prêts.
Ce qui suit présente plusieurs macros courantes liées à la fonction 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); //测试某个位是否被置位
Exemple d'utilisation de select :
Lorsqu'un ensemble de descripteurs de fichiers est déclaré, tous doivent être effacés avec FD_ZERO Position zéro. Fixez ensuite le bit correspondant au descripteur qui nous intéresse. Le fonctionnement est le suivant :
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);
Appelez ensuite la fonction select et attendez l'arrivée de l'événement du descripteur de fichier en congestion si l'heure fixée ; est dépassé, plus besoin d'attendre et de poursuivre l'exécution.
select(fd+1, &rset, NULL, NULL,NULL);
Après le retour de select, utilisez FD_ISSET pour tester si le bit donné est défini :
if(FD_ISSET(fd, &rset) { ... //do something }
Ce qui suit est l'exemple le plus simple d'utilisation de 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; }
Nous exécutons le programme et saisissons certaines données, et le programme indique que les données ont été reçues.
La clé pour comprendre le modèle sélectionné est de comprendre fd_set Pour faciliter l'explication, la longueur de fd_set est. 1 octet, et chaque bit de fd_set peut correspondre à un descripteur de fichier fd. Ensuite, un fd_set d'un octet de long peut correspondre à un maximum de 8 fds.
(1) Exécutez fd_set set; FD_ZERO(&set); alors la représentation binaire de set est 0000,0000.
(2) Si fd=5, exécutez FD_SET(fd,&set); et set devient 0001,0000 (la cinquième position est 1)
(3) Si fd= est à nouveau ajouté 2. fd=1, alors set devient 0001,0011
(4) Exécutez select(6,&set,0,0,0) et bloquez en attendant
(5) Si fd= 1 , des événements lisibles se produisent à la fois sur fd=2, puis sélectionnez les retours et définissez les modifications sur 0000,0011. Remarque : fd=5 sans événements est effacé.
Sur la base de la discussion ci-dessus, les caractéristiques du modèle sélectionné peuvent être facilement dérivées :
(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的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!