select函数详解

select函数详解select 函数详解背景说明定义介绍 参数说明原理返回值 pselect 总结案例案例 1 案例 2 说明 本文整合网络资源和 man 帮助文档 请酌情参考 背景 select 函数是实现 IO 多路复用的一种方式 什么是 IO 多路复用 举一个简单地网络服务器的例子 如果你的服务器需要和多个客户端保持连接 处理客户端的请求 属于多进程的并发问题 如果创建很多个进程来处理这些 IO 流 会导致 CPU 占有率很高 所以人

说明:本文整合网络资源和man帮助文档,请酌情参考。

fengjingtu

背景

select函数是实现IO多路复用的一种方式。

什么是IO多路复用?

举一个简单地网络服务器的例子,如果你的服务器需要和多个客户端保持连接,处理客户端的请求,属于多进程的并发问题,如果创建很多个进程来处理这些IO流,会导致CPU占有率很高。所以人们提出了I/O多路复用模型:一个线程,通过记录I/O流的状态来同时管理多个I/O

select只是IO复用的一种方式,其他的还有:poll,epoll等。

说明

定义
 /* According to POSIX.1-2001 */ #include  
     /* According to earlier standards */ #include  
     #include  
     #include  
     int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 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); 
介绍、
参数说明

nfds:是一个整数值, 表示集合中所有文件描述符的范围,即所有文件描述符的最大值+1。在windows中不需要管这个。

fd_set:
一个文件描述符集合保存在fd_set变量中,可读,可写,异常这三个描述符集合需要使用三个变量来保存,分别是 readfds,writefds,exceptfds。我们可以认为一个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);//添加一个文件描述符,将set中的某一位设置成1; void FD_ZERO(fd_set *set);//清空集合中的文件描述符,将每一位都设置为0; 

使用案例:

fd_set readfds; int fd; FD_ZERO(&readfds)//新定义的变量要清空一下。相当于初始化。 FD_SET(fd,&readfds);//把文件描述符fd加入到readfds中。 //select 返回 if(FD_ISSET(fd,&readset))//判断是否成功监视 { //dosomething } 

readfds:
监视文件描述符的一个集合,我们监视其中的文件描述符是不是可读,或者更准确的说,读取是不是不阻塞了。
writefds:
监视文件描述符的一个集合,我们监视其中的文件描述符是不是可写,或者更准确的说,写入是不是不阻塞了。
exceptfds:
用来监视发生错误异常文件










timeout

struct timeval{ long tv_sec;//秒 long tv_usec;//微秒 } 

如果timeout->tv_sec0 && timeout->tv_sec0 ,不等待直接返回,加入的描述符都会被测试,并且返回满足要求的描述符个数,这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的时间。当有描述符复合条件或者超过超时时间的话,函数返回。等待总是会被信号中断。

原理
返回值
pselect
#include 
  
    int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); 
  

select和pselect有三个主要的区别:

1、select超时使用的是struct timeval,用秒和微秒计时,而pselect使用struct timespec ,用秒和纳秒。

struct timespec{ time_t tv_sec;//秒 long tv_nsec;//纳秒 } 

2、select会更新超时参数timeout 以指示还剩下多少时间,pselect不会。

当pselect的sigmask==NULL时pselect和select一样

当sigmask!=NULL时,等效于以下原子操作:

sigset_t origmask; sigprocmask(SIG_SETMASK, &sigmask, &origmask); ready = select(nfds, &readfds, &writefds, &exceptfds, timeout); sigprocmask(SIG_SETMASK, &origmask, NULL); 

接收信号的程序通常只使用信号处理程序来引发全局标志。全局标志将指示事件必须被处理。在程序的主循环中。一个信号将导致select和pselect返回-1 并将erron=EINTR。

我们经常要在主循环中处理信号,主循环的某个位置将会检查全局标志,那么我们会问:如果信号在条件之后,select之前到达怎么办。答案是select会无限期阻塞。

这种情况很少见,但是这就是为什么出现了pselect。因为他是类似原子操作的。

举个栗子:

 static volatile sig_atomic_t got_SIGCHLD = 0; static void child_sig_handler(int sig) { got_SIGCHLD = 1; } int main(int argc, char *argv[]) { sigset_t sigmask, empty_mask; struct sigaction sa; fd_set readfds, writefds, exceptfds; int r; sigemptyset(&sigmask); sigaddset(&sigmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) { perror("sigprocmask"); exit(EXIT_FAILURE); } sa.sa_flags = 0; sa.sa_handler = child_sig_handler; sigemptyset(&sa.sa_mask); if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } sigemptyset(&empty_mask); for (;;) { /* main loop */ /* Initialize readfds, writefds, and exceptfds before the pselect() call. (Code omitted.) */ r = pselect(nfds, &readfds, &writefds, &exceptfds, NULL, &empty_mask); if (r == -1 && errno != EINTR) { /* Handle error */ } if (got_SIGCHLD) { got_SIGCHLD = 0; /* Handle signalled event here; e.g., wait() for all terminated children. (Code omitted.) */ } /* main body of program */ } } 

总结

select()可以同时监视多个描述符,如果他们没有活动,则正确地将进程置于休眠状态。Unix程序员们经常要处理多个文件描述符的I/O,他们的数据流可能是间歇性的。如果只创建read或者write会导致程序阻塞。

在我们使用select的时候,需要注意:

1、我们应该总是设置timeout=0,因为如果没有可用的数据,程序在运行时间里将无视可做。依赖超时的代码通常是不可移植,并且很难调试。

2、nfds的值一要准备且适当。

3、如果在调用完select之后,你不想检查结果,也不想做出适当的响应,那么文件描述符不需要添加到集合中。

4、select返回后,所有的文件描述符都应该被检查,看看他们是否准备好了。

5、read,recv,write,send,这几个函数不一定读/写你所请求的全部数据。如果他们读/写全部数据,是因为低流量负载和快速流。情况并非重视如此,应该处理你的函数仅管理发送或接收单个字节的情况。

6、除非你真的确信你有少量的数据要处理,否则不要一次只读一个字节,当你每次都能缓冲的时候,尽可能多的读取数据是非常低效的。

7、read,recv,write,send和select都会有返回-1的情况,并set errno的值。这些errno必须被恰当的处理。如果你的程序不会接收到任何信号,那么errno永远都不会等于EINTR,如果你的程序并不会设置非阻塞IO,那么errno就不会等于EAGAIN。

8、调用read,recv,write,send,不要使buffer的长度为0;

9、如果read,recv,write,send调用失败,并且返回的errno不是7中说的那两种情况,或者返回0,意思是“end-of-file”,这种情况下我们不应再将文件描述符传递给select。

10、每次调用select之前,timeout都用重新设置。

11、由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集。

大多数的操作系统都支持select。相比于试图用线程,进程,IPCS,信号,内存共享等方式来解决问题,select函数更有效且轻松。系统调用poll和select相似,在监视稀疏文件集合的时候更加有效。poll现在也在被广泛的使用,但没有select简便。linux专用的epoll在监视大连数据时比select和poll更加有效。

案例

案例1

下面是”man select “帮助文档中案例:

#include 
  
    #include 
   
     #include 
    
      #include 
     
       #include 
      
        int main(void) { fd_set rfds;//定义一个能保存文件描述符集合的变量 struct timeval tv;//定义超时时间 int retval;//保存返回值 /* Watch stdin (fd 0) to see when it has input. */ /* 监测标准输入流(fd=0)看什么时候又输入*/ FD_ZERO(&rfds);//初始化集合 FD_SET(0, &rfds);//把文件描述符0加入到监测集合中。 /* Wait up to five seconds. */ /* 设置超时时间为5s */ tv.tv_sec = 5; tv.tv_usec = 0; /*调用select函数,将文件描述符集合设置成读取监测 */ retval = select(1, &rfds, NULL, NULL, &tv); /* Don't rely on the value of tv now! */ /* 这时候的tv值是不可依赖的 */ /*根据返回值类型判断select函数 */ if (retval == -1) perror("select()"); else if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ /* 因为值增加了一个fd,如果返回值>0,则说明fd=0在集合中。*/ else printf("No data within five seconds.\n"); exit(EXIT_SUCCESS); } 
       
      
     
    
  
案例2

下面是”man select_tut “帮助文档中案例:

这个例子更好的说明了select函数的作用,这是一个TCP转发相关的程序,从一个端口转发到另一个端口

#include 
  
    #include 
   
     #include 
    
      #include 
     
       #include 
      
        #include 
       
         #include 
        
          #include 
         
           #include 
          
            #include 
           
             #include 
            
              static int forward_port; #undef max #define max(x,y) ((x) > (y) ? (x) : (y)) static int listen_socket(int listen_port) { struct sockaddr_in a; int s; int yes; if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); return -1; } yes = 1; if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *) &yes, sizeof(yes)) == -1) { perror("setsockopt"); close(s); return -1; } memset(&a, 0, sizeof(a)); a.sin_port = htons(listen_port); a.sin_family = AF_INET; if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) { perror("bind"); close(s); return -1; } printf("accepting connections on port %d\n", listen_port); listen(s, 10); return s; } static int connect_socket(int connect_port, char *address) { struct sockaddr_in a; int s; if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); close(s); return -1; } memset(&a, 0, sizeof(a)); a.sin_port = htons(connect_port); a.sin_family = AF_INET; if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) { perror("bad IP address format"); close(s); return -1; } if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) { perror("connect()"); shutdown(s, SHUT_RDWR); close(s); return -1; } return s; } #define SHUT_FD1 do { \ if (fd1 >= 0) { \ shutdown(fd1, SHUT_RDWR); \ close(fd1); \ fd1 = -1; \ } \ } while (0) #define SHUT_FD2 do { \ if (fd2 >= 0) { \ shutdown(fd2, SHUT_RDWR); \ close(fd2); \ fd2 = -1; \ } \ } while (0) #define BUF_SIZE 1024 int main(int argc, char *argv[]) { int h; int fd1 = -1, fd2 = -1; char buf1[BUF_SIZE], buf2[BUF_SIZE]; int buf1_avail, buf1_written; int buf2_avail, buf2_written; //我们希望调用主函数的时候,要指明,本地端口,发送端口,还有发送的ip地址 if (argc != 4) { fprintf(stderr, "Usage\n\tfwd 
             
               " " 
               
               
                 \n"); exit(EXIT_FAILURE); } // 忽略SIGPIPE这个信号,这个信号常出现在网络编程中,访问一个已经关闭的文件描述符时候出现。 signal(SIGPIPE, SIG_IGN); //确定发送端口 forward_port = atoi(argv[2]); //监听本地端口 h = listen_socket(atoi(argv[1])); if (h == -1) exit(EXIT_FAILURE); for (;;) { int r, nfds = 0; fd_set rd, wr, er; FD_ZERO(&rd); FD_ZERO(&wr); FD_ZERO(&er); FD_SET(h, &rd); // 获取nfds的值。并把fd1,fd2分别加入到,可读,可写,异常监视集合中去。 nfds = max(nfds, h); if (fd1 > 0 && buf1_avail < BUF_SIZE) { FD_SET(fd1, &rd); nfds = max(nfds, fd1); } if (fd2 > 0 && buf2_avail < BUF_SIZE) { FD_SET(fd2, &rd); nfds = max(nfds, fd2); } if (fd1 > 0 && buf2_avail - buf2_written > 0) { FD_SET(fd1, &wr); nfds = max(nfds, fd1); } if (fd2 > 0 && buf1_avail - buf1_written > 0) { FD_SET(fd2, &wr); nfds = max(nfds, fd2); } if (fd1 > 0) { FD_SET(fd1, &er); nfds = max(nfds, fd1); } if (fd2 > 0) { FD_SET(fd2, &er); nfds = max(nfds, fd2); } //开始监视 r = select(nfds + 1, &rd, &wr, &er, NULL); if (r == -1 && errno == EINTR) continue; if (r == -1) { perror("select()"); exit(EXIT_FAILURE); } if (FD_ISSET(h, &rd)) { unsigned int l; struct sockaddr_in client_address; memset(&client_address, 0, l = sizeof(client_address)); r = accept(h, (struct sockaddr *) &client_address, &l); if (r == -1) { perror("accept()"); } else { SHUT_FD1; SHUT_FD2; buf1_avail = buf1_written = 0; buf2_avail = buf2_written = 0; fd1 = r; fd2 = connect_socket(forward_port, argv[3]); if (fd2 == -1) SHUT_FD1; else printf("connect from %s\n", inet_ntoa(client_address.sin_addr)); } } /* NB: read oob data before normal reads */ if (fd1 > 0) if (FD_ISSET(fd1, &er)) { char c; r = recv(fd1, &c, 1, MSG_OOB); if (r < 1) SHUT_FD1; else send(fd2, &c, 1, MSG_OOB); } if (fd2 > 0) if (FD_ISSET(fd2, &er)) { char c; r = recv(fd2, &c, 1, MSG_OOB); if (r < 1) SHUT_FD2; else send(fd1, &c, 1, MSG_OOB); } if (fd1 > 0) if (FD_ISSET(fd1, &rd)) { r = read(fd1, buf1 + buf1_avail, BUF_SIZE - buf1_avail); if (r < 1) SHUT_FD1; else buf1_avail += r; } if (fd2 > 0) if (FD_ISSET(fd2, &rd)) { r = read(fd2, buf2 + buf2_avail, BUF_SIZE - buf2_avail); if (r < 1) SHUT_FD2; else buf2_avail += r; } if (fd1 > 0) if (FD_ISSET(fd1, &wr)) { r = write(fd1, buf2 + buf2_written, buf2_avail - buf2_written); if (r < 1) SHUT_FD1; else buf2_written += r; } if (fd2 > 0) if (FD_ISSET(fd2, &wr)) { r = write(fd2, buf1 + buf1_written, buf1_avail - buf1_written); if (r < 1) SHUT_FD2; else buf1_written += r; } /* check if write data has caught read data */ if (buf1_written == buf1_avail) buf1_written = buf1_avail = 0; if (buf2_written == buf2_avail) buf2_written = buf2_avail = 0; /* one side has closed the connection, keep writing to the other side until empty */ if (fd1 < 0 && buf1_avail - buf1_written == 0) SHUT_FD2; if (fd2 < 0 && buf2_avail - buf2_written == 0) SHUT_FD1; } exit(EXIT_SUCCESS); } 
                
               
              
             
            
           
          
         
        
       
      
     
    
  

上面的程序可以应用于大多数类型的TCP连接,包括telnet服务器对OOB信号的转发。它处理了同时在两个方向上流动这一棘手问题。你可能会想,使用连个进程不是更有效吗?事实上使用两个进程会更复杂。另一个想法是使用fcntl设置非阻塞的I/O使用,这也有弊端,因为它使用非常低效的超时。

这个程序不能处理同时有多个连接的情况,但很容易扩展。你只需要为每个连接创建一个buffer。当前的程序中,新的连接会导致旧的连接被覆盖丢弃。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/204620.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午7:55
下一篇 2026年3月19日 下午7:55


相关推荐

  • Springboot自定义注解,支持SPEL表达式

    Springboot自定义注解,支持SPEL表达式举例,自定义redis模糊删除注解1.自定义注解importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target(E…

    2025年6月17日
    9
  • libxml2 libxslt库编译

    libxml2 libxslt库编译libxml2 官方下载地址 点击打开链接我下载的版本是 2 9 3libxslt 官方下载地址 点击打开链接我下载的版本是 1 1 29 一 编译 libxml2 2 9 3 32 位 1 iconv 库准备在 d 盘建立目录 opt opt 下面新建目录 include lib 把 iconv 的库放到 lib 下 把头文件放到 include 下 2 打开 VisualStudio 命令提示 进到相应目录

    2026年3月17日
    2
  • 使用VLC搭建视频直播服务器[通俗易懂]

    使用VLC搭建视频直播服务器[通俗易懂]去年我们信息之夜我们进行过视频直播服务,当时我们使用了WMS(WindowsMediaServer)实现了这个服务,但是编码是微软的WMV,因而像iPhone/Android这样的智能手机无法观看,今天我给大家带来一种更简便的实现方式,并帮助所有平台用户都可以观看。首先介绍一下,今天我们的工具VLCPlayer。VLC主页:http://www.videolan.org VLC

    2022年6月4日
    40
  • const修饰指针变量详解

    const修饰指针变量详解

    2022年2月7日
    84
  • Django request对象

    Django request对象Djangorequest对象1简介服务器接收到http协议的请求后,会根据报文创建HttpRequest对象,这个对象不需要我们创建,直接使用服务器构造好的对象就可以。视图的第一个参数必须是HttpRequest对象,在django.http模块中定义了HttpRequest对象的API。2request对象的属性**request.scheme:**代表请求的方案,http或…

    2022年6月11日
    35
  • java 枚举(enum) 全面解读

    java 枚举(enum) 全面解读

    2021年6月15日
    100

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号