前言

Linux系统下,select()为最早支持IO多路复用的系统调用。该接口相比poll()epoll()来说,更为简单,但是效率较低,而且最大只支持FD_SETSIZE(1024)个描述符。如果对并发数量的要求高于1024,请考虑使用poll()或者epoll()

系统调用

#include <sys/select.h>

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);

select()监控多个文件描述符,直到其中的一个或多个文件描述符变为就绪状态。就绪状态代表该描述符可读、可写或者异常状态。select()监控的描述符个数要求小于FD_SETSIZE(1024),poll()epoll()没有这个限制。

描述符集合

3个描述符集合,既是传入参数,同时又是传出参数。调用者可以将要监听的描述符加入到集合中,传递给内核,内核在感知到该描述符就绪事件后,再通过该集合,将该描述符传出。

3个描述符集合分别代表:

  • readfds:可读集合。将描述符加入该集合,代表调用者对该描述符的读事件感兴趣,具体描述见下面小节;
  • writefds:可写集合。将描述符加入该集合,代表调用者对该描述符的写事件感兴趣,具体描述见下面小节;
  • exceptfds:异常集合。将描述符加入该集合,代表调用者对该描述符的异常事件感兴趣,具体描述见下面小节。

因为这3个集合,既是传入参数,又是传出参数,内核会修改这3个集合的值,所以在一个循环中使用select()的时候,要每次调用之前都要重新初始化这3个集合。根据man select的介绍,这个应该是个设计方面的失误。

那么如何操作描述符集合呢?比如将一个描述符添加到集合中,或者从集合中删除一个描述符。系统提供了4个宏来对描述符集合进行操控:

  • FD_ZERO():这个宏将指定的集合置空。这是初始化一个集合的第一步;
  • FD_SET():这个宏将指定的文件描述符添加到集合中。
  • FD_CLR():这个宏将指定的文件描述符从集合中移除。
  • FD_ISSET():测试指定的文件描述符是否在集合中。因为select()返回的时候,会修改集合的值,所以可以用此宏来测试描述符是否仍在集合中,如果在的话,返回非零值,如果不在的话,返回0。

参数

select()函数的5个参数如下所述:

  1. readfds:可读集合。当调用者对某一个文件描述符的可读事件感兴趣的时候,可以将该描述符加入集合。当select()函数返回后,判断该描述符是否在这个集合中,如果在这个集合中,代表该描述符可读。此处的可读不仅仅包括从缓冲区读取数据,也包括读取EOF,即正常结束。
  2. writefds:可写集合。当调用者对某一个文件描述符的可写事件感兴趣的时候,可以将该描述符加入集合。当select()函数返回后,判断该描述符是否在这个集合中,如果在这个集合中,代表该描述符可写。
  3. execeptfds:异常集合。
  4. nfds:这个参数指定三个集合中,最大的那个文件描述符数字+1。此参数告诉内核,最大遍历到哪个描述符位置,避免无效遍历。
  5. timeout:超时参数,该参数为timeval类型。该参数指定select()为阻塞状态,或者满足如下条件:

    • 某一个文件描述符变为就绪状态;
    • 系统调用被某一信号中断;
    • 发生了超时。

      该结构的定义如下:

      struct timeval {
          time_t      tv_sec;         /* seconds */
          suseconds_t tv_usec;        /* microseconds */
      };

      该参数的取值有如下几种情况:

      • 如果两个字段都为0,则select()立即返回。
      • 如果该参数指定为NULL,则select()会一直等,直到某个文件描述符就绪。
      • 如果其中的字段值不为0,则select()最长会等待指定的时间。

返回值

  • 大于0:成功。返回值代表就绪的文件描述符的个数,该个数为三个集合中描述符之和。
  • 0:超时返回。
  • -1:发生错误。errno指定错误详细原因。

代码示例

select()写一个回射服务器端,代码示例如下:

#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAXLINE 4096

int main(int argc, char *argv[])
{
    int listenfd, maxfd, nready, clientlen;
    struct sockaddr_in servaddr, clientaddr;
    fd_set rset, allset;

    int sockclients[FD_SETSIZE];
    char buf[MAXLINE];
    int maxindex;
    int n;

    if (argc != 2)
    {
        printf("usage: ./a.out <ListenPort>\n");
        return 0;
    }

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0)
    {
        perror("socket error");
        return 0;
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(atoi(argv[1]));
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        perror("bind error.");
        return 0;
    }

    listen(listenfd, SOMAXCONN);

    FD_ZERO(&rset);
    FD_ZERO(&allset);

    FD_SET(listenfd, &allset);

    maxfd = listenfd;
    maxindex = 0;

    for (size_t i = 0; i < FD_SETSIZE; i++)
    {
        sockclients[i] = -1;
    }

    for (;;)
    {
        rset = allset;
        if ((nready = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            else
            {
                perror("select error.");
                return 0;
            }
        }
        else if (nready == 0)
        {
            printf("select timeout\n");
            continue;
        }
        else // nready > 0
        {
            if (FD_ISSET(listenfd, &rset)) // new connection
            {
                clientlen = sizeof(clientaddr);
                int connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientlen);
                if (connfd < 0)
                {
                    perror("accept error.");
                    continue;
                }

                printf("new connection from %s:%d\n",
                       inet_ntop(AF_INET, &clientaddr.sin_addr, buf, sizeof(buf)),
                       ntohs(clientaddr.sin_port));

                size_t i;
                for (i = 0; i < sizeof(FD_SETSIZE); i++)
                {
                    if (sockclients[i] == -1)
                    {
                        sockclients[i] = connfd;
                        break;
                    }
                }

                if (i >= FD_SETSIZE)
                {
                    perror("too many clients connection.");
                    close(connfd);
                    continue;
                }

                maxindex = (i > maxindex ? i : maxindex);
                maxfd = (maxfd > connfd ? maxfd : connfd);

                FD_SET(connfd, &allset);

                if (--nready == 0)
                {
                    continue;
                }
            }

            if (nready > 0) // connfd can read
            {
                for (size_t i = 0; i <= maxindex; i++)
                {
                    int connfd;
                    if((connfd = sockclients[i]) < 0)
                    {
                        continue;
                    }

                    if (FD_ISSET(connfd, &rset))
                    {
                        if ((n = read(connfd, buf, sizeof(buf))) > 0)
                        {
                            write(connfd, buf, n); 
                        }
                        else if (n == 0) // EOF
                        {
                            clientlen = sizeof(clientaddr);
                            if(getpeername(connfd, (struct sockaddr*)&clientaddr, &clientlen) == 0)
                            {
                                printf("client(%s:%d) close the connection.\n",
                                            inet_ntop(AF_INET, &clientaddr.sin_addr, buf, sizeof(buf)),
                                            ntohs(clientaddr.sin_port));
                            }
                            
                            sockclients[i] = -1;
                            FD_CLR(sockclients[i], &allset);

                            break;
                        }
                        else // read error
                        {
                            clientlen = sizeof(clientaddr);
                            if(getpeername(connfd, (struct sockaddr*)&clientaddr, &clientlen) == 0)
                            {
                                printf("read error from client(%s:%d).\n",
                                            inet_ntop(AF_INET, &clientaddr.sin_addr, buf, sizeof(buf)),
                                            ntohs(clientaddr.sin_port));
                            }
                            
                            sockclients[i] = -1;
                            FD_CLR(sockclients[i], &allset); // 输出,打印信息
                            break;;
                        }
                    }
                }
            }
        }
    }
}

标签: none

添加新评论