I/O多路复用之select函数

来源:互联网 发布:无线游戏鼠标 知乎 编辑:程序博客网 时间:2024/06/04 19:51

select介绍


我们先来看一下select的接口。

       int select(int nfds, fd_set *readfds, fd_set *writefds,                  fd_set *exceptfds, struct timeval *timeout);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

从上面的这些接口我们应该能有写认识,首先我么来看select系统调用的参数的含义。

参数功能nfds被监听的文件描述符的总数。通常是文件描述符最大值加1readfds可读事件的文件描述符集合writefds可写事件的文件描述符集合exceptfds异常事件的文件描述符集合timeout设定超时时间

值得注意的是,后面的三个参数即是输入型参数,又是输出型参数,输入表示关心那些文件描述符对应的特定事件发生。输出表示的是那些文件描述符对应的事件就绪。当就绪后,内核将会去修改这些文件描述符的集合。这三个参数都是fd_set结构体类型

typedef struct{    /*XPG4.2requiresthismembername.Otherwiseavoidthename    fromtheglobalnamespace.*/    #ifdef__USE_XOPEN    __fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];    #define__FDS_BITS(set)((set)->fds_bits)    #else    __fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];    #define__FDS_BITS(set)((set)->__fds_bits)    #endif}fd_set;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看出fd_set就是一个结构体数组,这个结构体数组的每一位就是一个文件描述符的标记,配套提供了一些对于fd_set操作的宏。

       void FD_CLR(int fd, fd_set *set);       int  FD_ISSET(int fd, fd_set *set);       void FD_SET(int fd, fd_set *set);       void FD_ZERO(fd_set *set);
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
宏功能FD_CLR进行对应位fdFD_ISSET进行判断对应位fdFD_SET设置fd的对应位置FD_ZERO进行清空fd_set

最后要说一下的就是timeout参数,这个参数用来设置超时时间,它也是一个结构体,它用来告诉应用程序select等待多久,这里的单位是微秒级别的。

timeout参数说明0立即返回,即轮询NULL阻塞监视文件描述符,当有时间就绪才返回大于0的时间超时时间设置

select调用时内核级别的,select的轮询方式是和非阻塞轮询方式是不同的,select的轮询方式是同时可以对多个I/O端口进行监听,任何一个端口数据好了,这个时候就可以读了。然后通过系统调用,就可以把数据从内核拷贝到用户进程。

![enter description here][1]

select缺点


1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

  一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max查看,有宏FD_SETSIZE进行限制fd的数量。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

   当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

select代码示例:

#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<unistd.h>#include<stdlib.h>#include<netinet/in.h>#include<string.h>#include<sys/time.h>static void Usage(const char *str){printf("usage: %s [server_ip][server_port]\n",str);}static int startup(const char *ip,int port){int new_socket = socket(AF_INET,SOCK_STREAM,0);if(new_socket < 0){perror("socket");exit(2);}int op = 1;int ret = setsockopt(new_socket,SOL_SOCKET,SO_REUSEADDR,&op,sizeof(op));if(ret < 0){perror("setsockopt");exit(3);}struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);serv_addr.sin_addr.s_addr = inet_addr(ip);ret = bind(new_socket,(struct sockaddr*)&serv_addr,sizeof(serv_addr));if(ret < 0){perror("bind");exit(4);}ret = listen(new_socket,128);if(ret < 0){perror("listen");exit(5);}return new_socket;}int array_fds[1024];  //定义一个全局数组。这个全局变量存放的是文件描述符。int max_fd;   //最大的文件描述符。int main(int argc ,char *argv[]){if(argc != 3){Usage(argv[0]);exit(1);}int i = 0;for(; i < 1024; ++i)array_fds[i] = -1;   //将数组中每一个元素都置为-1.int listen_sock = startup(argv[1],atoi(argv[2]) );array_fds[0] = listen_sock;  //将监听套件字放在数组中0号位置。fd_set reads;  //创建只读集合。fd_set writes; //创建只写集合。struct timeval timeout; while(1) //服务器一直处于服务状态。{FD_ZERO(&reads);  //因为select是输入输出参数,当select返回时,已经改变了read这个集合了,下次还需要监听read这个集合中的可读事件的话就需要重新赋值。FD_ZERO(&writes); //初始化。max_fd = -1;  //每次这个得重新初始化timeout.tv_sec = 10;  //定时10秒,这个参数也是输入输出参数timeout.tv_usec = 0;for(i = 0; i < 1024; ++i){if(array_fds[i] >= 0){FD_SET(array_fds[i], &reads); //将监听套接字加入到可读事件中。FD_SET(array_fds[i],&writes);//将监听套接字加入到可写事件中。if(array_fds[i] > max_fd)max_fd = array_fds[i];}}//准备工作做好后,开始真正的监听了。int j  = 0;switch(select(max_fd+1 ,&reads,&writes,NULL,&timeout)){case 0:printf("time out....\n");break;case -1:perror("select");exit(6);default: //有可读事件发生,但是不知道是那一个可读事件,需要遍历数组,查看数组中存放的描述符那一个可读了。for(; j < 1024; ++j){if(array_fds[j] < 0) //-1表示这个文件描述符没有可读事件发生。continue;char buf[BUFSIZ]; //接收数据缓冲区。if(j== 0 && FD_ISSET(array_fds[0],&reads)) //监听套接字有可读事件发生,表示有客户连接了。{struct sockaddr_in clie_addr;socklen_t  len = sizeof(clie_addr);int connect_fd = accept(array_fds[0],(struct sockaddr*)&(clie_addr),&len);if(connect_fd < 0){perror("accept");continue;  //这次连接失败,让它下次连接。}printf("get a new client :(%s:%d)\n",inet_ntoa(clie_addr.sin_addr),ntohs(clie_addr.sin_port));//连接套接字后继续监听,看这个套接字是不是有数据发送。int k = 0;for(; k < 1024; ++k){if(array_fds[k] == -1){array_fds[k] = connect_fd;break;}}}else if(j != 0 && FD_ISSET(array_fds[j],&reads)){printf("======================read start==========\n");ssize_t  s = read(array_fds[j],buf,sizeof(buf) - 1);if(s < 0){perror("read");close(array_fds[j]);array_fds[j] = -1;  //数组重新利用break;}else if(s == 0){printf("clinet quit\n");close(array_fds[j]);array_fds[j] = -1;  //数组重新利用break;}else{buf[s]  = 0;printf("clinet say:%s\n",buf);//if(FD_ISSET(array_fds[j],&writes))//{//write(array_fds[j],buf,strlen(buf));//}}}// if(j != 0 && FD_ISSET(array_fds[j],&writes)) //套接字可写事件满足,说明可以将套接字中的数据发送出去了。//{//printf("-------------------write j = %d ----------------start\n",j);//sleep(1);//write(array_fds[j],buf,strlen(buf));//}}//for结束} //switch结束} //while(1)死循环return 0;}


客户端用dup2重定向

#include<stdio.h>#include<sys/types.h>#include<sys/socket.h>#include<unistd.h>#include<stdlib.h>#include<netinet/in.h>#include<string.h>#include<sys/time.h>#include<fcntl.h>#include<sys/stat.h>static void Usage(const char *str){printf("usage: %s [server_ip][server_port]\n",str);}int main(int argc, char*argv[] ){if(argc != 3){Usage(argv[0]);exit(1);}int new_socket = socket(AF_INET,SOCK_STREAM,0);if(new_socket < 0){perror("socket");exit(2);}struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(atoi (argv[2]) );serv_addr.sin_addr.s_addr = inet_addr(argv[1]); int ret = connect(new_socket,(struct sockaddr*)&serv_addr,sizeof(serv_addr));if(ret < 0){perror("connect");exit(3);}char buf[BUFSIZ];while(1){printf("please enter#:");fflush(stdout);ssize_t s = read(0,buf,sizeof(buf) - 1);if(s > 0){int outfd = dup(1);  //outfd 指向标志输出(保存标准输出好恢复)。buf[s-1] = 0;  //去掉换行符。//write(new_socket,buf,strlen(buf));dup2(new_socket, 1);  //1号文件描述符去指向new_socket指向的内容了。printf("%s",buf);  //本来是将buf中的东西写入到标准输出中,但是现在1号描述符已经重新定向了,指向套接字的缓冲区,所以现在就是讲buf中的东西写入到套接字缓冲区了。fflush(stdout);dup2(outfd, 1);  //让1重新指向标准输出。}elsebreak;//ssize_t s2 = read(new_socket,buf,sizeof(buf) - 1);//buf[s2] = 0;//printf("sever echo # %s\n",buf);}close(new_socket);return 0;}





原创粉丝点击