I/O多路转接之select

什么是多路转接?什么是select?
多路转接就是一次等待多个文件描述符;
简单来说,select只做一件事,那就是等,等至少一个文件描述符的读写时间就绪。

具体来说,系统提供select来实现多路复用输入/输出模型。
select系统调用可以让程序监听多个文件描述符的状态变化。
程序会在select在这里等待,直到被监视的文件描述符至少有一个发生了状态改变。

select函数声明

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

参数解释:

  • nfds:需要监视的最大文件描述符的值+1
  • readfds:只关心写事件,可读文件描述符的集合
  • writefds:只关心写事件,可写文件描述符的集合
  • exceptfds:只关心异常事件,异常文件描述符的集合
  • 参数timeout为结构timeval,用来设置select()的等待时间

fd_set的结构
fd_set就是一个整形数组,更严格的说,一个“位图”,位图中的位置代表对应的文件描述符,用0或1来控制。
但是,我们不能直接操作fd_set,而是要调用函数;

void FD_ZERO(fd_set& set);  //清楚set的全部位
void FD_SET(int fd, fd_set& set)  //把fd设置进set的相关位
void FD_CLR(int fd, fd_set& set)  //把fd在set的相关位清楚
int FD_ISSET(int fd, fd_set& set);  //判断fd是否被设置相关位

fd_set作为输入输出型参数:

  • 输入时:用户想告诉内核,让OS帮用户关心那些文件描述符
  • 输出时:内核告诉用户,那些文件描述符已就绪

timeout:用来设置等待时间,取值为:
NULL:表示select一直阻塞,知道某个文件描述符发生了时间;
0:非阻塞,不等待外部事件发生。
特定时间:如果在指定时间内没有时间发生,将立刻超时返回。

timeval的结构:

struct timeval
{
	_time_t tv_sec;//秒数
	_suseconds_t tv_usec;//毫秒
};

返回值解释:
执行成功则返回已经改变状态的文件描述符的个数;
返回0则表示在描述符状态改变前已超过timeout超时,没有返回;
返回-1表示发生了错误,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测;

错误值可能为:

  • EBADF 文件描述符无效或该文件描述符已关闭
  • EINTR 此调用被信号中断
  • EINVAL 参数n为负值
  • ENOMEM 核心内存不足

socket就绪条件

  • 读就绪

sochet内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RECVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
监听的socket上有新的连接请求时
socket上有未处理的错误时

  • 写就绪

socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记,SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
socket的写操作被关闭(close或者shutdown),对于一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
socket使用非阻塞connect连接成功或失败之后
socket上有未读取的错误

  • 异常就绪

socket上收到带外数据(关于带外数据,和TCP紧急模式相关,在TCP报头中有一个紧急指针的字段)

简述select的执行过程
为了方便说明,假设fd_set的长度为1字节,则一字节最大可以对应8个文件描述符。
1、fd_set set; FD_ZERO(&set); 此时set为0000 0000;
2、若fd=5,执行FD_SET(&set);此时set变为0001 0000 (第5位置为1);
3、在加上 fd=1 和 fd=2 文件描述符,则set变为0001 0011;
4、执行select(6, &set, 0, 0,0) 阻塞等待;
5、若 fd=1 和 fd=2 发生了可读事件,则select返回,此时set变为0000 0011。注意:因为 fd=5 没有发生事件,所以对应位置被清空;

编写select代码
一、检测标准输入:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/time.h>
  4 #include<sys/types.h>
  5 int main()
  6 {
  7     //将0(标准输入)设置进read_fds
  8     fd_set read_fds;
  9     FD_ZERO(&read_fds);
 10     FD_SET(0, &read_fds);
 11
 12     while(1){
 13         printf("> ");
 14         fflush(stdout);
 15
 16         //最大文件描述符为0,只关心读事件,阻塞等待
 17         int ret = select(1, &read_fds, NULL, NULL, NULL);
 18         if(ret < 0){
 19             perror("select error\n");
 20             continue;
 21         }
 22         if(FD_ISSET(0, &read_fds)){//若标准输入就绪
 23             char buf[1024] = {0};
 24             read(0, buf, sizeof(buf)-1);
 25             printf("input:%s\n", buf);
 26         }
 27         else{
 28             continue;
 29         }
 30         FD_ZERO(&read_fds);
 31         FD_SET(0, &read_fds);
 32     }
 33     return 0;
 34 }

此时的运行的效果为,找到在标准输入上输入之前,select都会一直阻塞等待。
在这里插入图片描述
在这里插入图片描述
二、模拟实现select服务器
服务器

 #include<stdio.h>
  2 #include<sys/socket.h>
  3 #include<unistd.h>
  4 #include<sys/time.h>
  5 #include<sys/types.h>
  6 #include<stdlib.h>
  7 #include<arpa/inet.h>
  8 #include<netinet/in.h>
  9
 10 int main(int argc, char* argv[])
 11 {
 12     //建立监听套接字
 13     if(argc != 3){
 14         printf("./server [ip] [port]\n");
 15         return 1;
 16     }
 17     int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
 18     if(listen_sock < 0){
 19         perror("socker error\n");
 20         return 2;
 21     }
 22     struct sockaddr_in local;
 23     local.sin_family = AF_INET;
 24     local.sin_addr.s_addr = inet_addr(argv[1]);
 25     local.sin_port = htons(atoi(argv[2]));
 26     if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
 27         perror("bind error\n");
 28         return 3;
 29     }
 30     if(listen(listen_sock,  5) < 0){
 31         perror("listen error\n");
 32         return 4;
 33     }
 35  //数组的大小就是fd_set这个位图能够存多少个fd,因为需要用数组记录
 36     int fdArray[sizeof(fd_set)*8];
 37
 38     //把listen_sock给数组的第一个元素
 39     fdArray[0] = listen_sock;
 40
 41     int num = sizeof(fdArray)/sizeof(fdArray[0]);//最多存在1024个fd
 42
 43     //然后初始化数组为-1
 44     for(int i=0; i<num; i++){
 45         fdArray[i] = -1;
 46     }
 47
 48	while(1){
 49         fd_set rfds;//建立关心读位图
 50         FD_ZERO(&rfds);//初始化位图
 51         int maxfd = fdArray[0];//当前的最大fd为listen_sock
 52
 53         for(int i=0; i<num; i++){
 54             if(fdArray[i] >= 0){
 55                 FD_SET(fdArray[i], &rfds);
 56                 //更新最大的fd
 57                 if(maxfd < fdArray[i])
 58                     maxfd = fdArray[i];
 59             }
 60         }
 61         switch(select(maxfd+1, &rfds, NULL, NULL, NULL)){
 62         case 0:
 63             printf("超时!\n");
 64             break;
 65         case -1:
 66             printf("出错!\n");
 67             break;
 68         default:
 69             {
 70                 for(int i=0; i<num; i++){
 71                     if(fdArray[i] == -1)
 72                         continue;
 73                     //若fd已经就绪
 74                     if(FD_ISSET(fdArray[i], &rfds)){
 75                         //listen_sock就绪
 76                         if(FD_ISSET(fdArray[i], &rfds) && i==0){
 77                             //建立连接
 78                             struct sockaddr_in client;
 79                             socklen_t len = sizeof(client);
 80                             int new_sock = accept(listen_sock, (struct s
 81                             if(new_sock < 0){
 82                                 perror("accept error!\n");
 83                                 return 5;
 84                             }
 85                             //遇见-1就说明处理完了
 86                             for(int i=0; i<num; i++){
 87                                 if(fdArray[i] == -1)
 88                                     break;
 89                             }
 90                             //new_sock必须在范围之内
 91                             if(i < num)
 92                                 fdArray[i] = new_sock;
 93                             else
 94                                 continue;
 95                             }
 96                         }
 97                         //普通fd就绪
 98                         if(FD_ISSET(fdArray[i], &rfds)){
 99                             char buf[1024];
100                             ssize_t s = read(fdArray[i], buf, sizeof(buf
101                             if(s > 0){
102                                 buf[s] = 0;
103                                 printf("client#%s\n", buf);
104                             }
105                             else if(s == 0){
106                                 printf("client quit!\n");
107                                 close(fdArray[i]);
108                                 fdArray[i] = -1;
109                             }
110                             else{
111                                 break;
112                             }
113                         }
114                 }
115            }
116         }
117     }
118     return 0;
119 }

客户端:

#include<sys/types.h>
  5 #include<stdlib.h>
  6 #include<arpa/inet.h>
  7 #include<netinet/in.h>
  8 #include<string.h>
  9
 10 int main(int argc, char* argv[])
 11 {
 12     if(argc != 3){
 13         printf("./client [ip] [port]\n");
 14         return 1;
 15     }
 16     struct sockaddr_in server;
 17     server.sin_family = AF_INET;
 18     server.sin_addr.s_addr = inet_addr(argv[1]);
 19     server.sin_port = htons(atoi(argv[2]));
 20
 21     int fd = socket(AF_INET, SOCK_STREAM, 0);
 22     if(fd < 0){
 23         perror("socket error\n");
 24         return 2;
 25     }
 26
 27     int ret = connect(fd, (struct sockaddr*)&server, sizeof(server));
 28     if(ret < 0){
 29         perror("connect error\n");
 30         return 3;
 31     }
 32
 33     while(1){
 34         printf("> ");
 35         fflush(stdout);
 36         char buf[1024] = {0};
 37         read(0, buf, sizeof(buf)-1);
 38
 39         ssize_t s = write(fd, buf, strlen(buf));
 40         if(s < 0){
 41             perror("write error\n");
 42             return 4;
 43         }
 44     }
 45     close(fd);
 46     return 0;
 47 }

select的特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值,在我的服务器上sizeof(fd_set) = 128,每bit表示一个文件描述符,则我的服务器上支持的最大文件描述符个数是128*8 = 1024
  • 将fd加入select监控集的同时,还需要在使用一个数据结构array保存放到select监控集中的fd

一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断
二是select返回后会把以前加入的但并没有事件发生的fd清空,则每次开始 select 前都要重新从array中取得fd逐一加入,扫描array的同时取得fd的最大值maxfd,用于select的第一个参数

select的缺点

  • 每次调用select,都需要手动设置fd集合,从接口使用来说非常不方便
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符有上限
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值