select システム コールの目的は、指定された期間内にユーザーが関心のあるファイル記述子上の読み取り可能、書き込み可能、および例外イベントを監視することです。
なぜ選択モデルが表示されるのですか?
まず次のコードを見てください:
int iResult = recv(s, buffer,1024);
これは、デフォルトのブロッキングモードでソケットでデータを受信するために使用され、ソケット接続でデータが読み取れるようになるまで、recv 関数はブロックされません。データがバッファに読み込まれるまで戻ります。そうでない場合は、データは常にそこでブロックされます。シングルスレッド プログラムでこれが発生すると、メイン スレッド (シングル スレッド プログラムにはデフォルトのメイン スレッドが 1 つだけあります) がブロックされるため、データが送信されない場合、プログラム全体がここでロックされます。永久にブロックされます。この問題はマルチスレッドで解決できますが、複数のソケット接続の場合、これは良い選択肢ではなく、拡張性も劣ります。
コードをもう一度見てください:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul); iResult = recv(s, buffer,1024);
今回は、ソケット接続で受信するデータがあるかどうかに関係なく、recv 呼び出しはすぐに戻ります。その理由は、ioctlsocket を使用してソケットをノンブロッキング モードに設定しているためです。ただし、これに従ってみると、データがない場合、recv はすぐに戻りますが、要求された操作が正常に完了しなかったことを意味するエラー WSAEWOULDBLOCK も返すことがわかります。
これを見て、recv を繰り返し呼び出して成功するまで戻り値を確認すればいいという人も多いかもしれませんが、これは効率が非常に問題があり、オーバーヘッドが大きすぎます。
上記の問題を解決するのがセレクトモデルの登場です。
モデルを選択するための鍵は、複数のソケットを均一に管理し、スケジュールするための秩序ある方法を使用することです。
上記のように、ユーザーはまず IO 操作を必要とするソケットを select に追加し、次にブロックして select システムコールが返されるのを待ちます。データが到着すると、ソケットがアクティブ化され、select 関数が戻ります。ユーザー スレッドは正式に読み取りリクエストを開始し、データを読み取り、実行を継続します。
プロセスの観点から見ると、IO リクエストに select 関数を使用する場合と同期ブロッキング モデルを使用する場合には大きな違いはありません。監視ソケットを追加して select 関数を呼び出すという追加の操作もあり、効率がさらに悪くなります。ただし、select を使用する最大の利点は、ユーザーが 1 つのスレッドで複数のソケット IO リクエストを同時に処理できることです。ユーザーは複数のソケットを登録し、継続的に select を呼び出してアクティブ化されたソケットを読み取ることで、同じスレッドで複数の IO リクエストを同時に処理するという目的を達成できます。同期ブロッキング モデルでは、この目的はマルチスレッドによって達成される必要があります。
select プロセスの疑似コードは次のとおりです:
{ 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);
パラメータの説明:
maxfdp: 監視対象のファイル記述子の総数。これは、すべてのファイル内のファイル記述子の最大数よりも大きいです。記述子セット ファイル記述子は 0 から数えられるため、値は 1 大きくなります。
readfds、writefds、excectset: それぞれ、読み取り可能、書き込み可能、例外、その他のイベントに対応する記述子セットを指します。
timeout: select 関数のタイムアウトを設定するために使用されます。つまり、待機を中止するまでの待機時間をカーネルに指示します。 timeout == NULL は、無限の時間待機することを意味します
timeval 構造体は次のように定義されます:
struct timeval { long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
戻り値: タイムアウトの場合は 0 が返され、失敗の場合は 0 より大きい整数が返されます。整数は、準備ができている記述子の数を表します。
以下では、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); //测试某个位是否被置位
Select の使用例:
ファイル記述子セットが宣言されるとき、FD_ZERO を使用してすべての位置を 0 に設定する必要があります。次に、関心のある記述子に対応するビットを設定します。操作は次のようになります。
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);
次に、選択関数を呼び出し、設定された時間を超えた場合は、輻輳状態でファイル記述子イベントの到着を待ちます。待って実行を続行します。
select(fd+1, &rset, NULL, NULL,NULL);
select が返された後、FD_ISSET を使用して、指定されたビットが設定されているかどうかをテストします:
if(FD_ISSET(fd, &rset) { ... //do something }
以下は、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; }
プログラムを実行し、何気なくデータを入力すると、プログラムがプロンプトを表示します。データを受信しました。
(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 はクリアされます。
上記の議論に基づいて、選択したモデルの特性を簡単に描くことができます:
(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的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
以上が選択メカニズムの利点の紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。