select函数详解

select函数详解

        • 背景
        • 说明
          • 定义
          • 介绍、
          • 参数说明
          • 原理
          • 返回值
          • pselect
        • 总结
        • 案例
          • 案例1
          • 案例2

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

背景

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

介绍、

select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。


我们使用select来监视文件描述符时,要向内核传递的信息包括:
​ 1、我们要监视的文件描述符个数
​ 2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。
​ 3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢?
​ 4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。

参数说明

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

linux select第一个参数的函数: 待测试的描述集的总个数。 但要注意, 待测试的描述集总是从0, 1, 2, …开始的。 所以, 假如你要检测的描述符为8, 9, 10, 那么系统实际也要监测0, 1, 2, 3, 4, 5, 6, 7, 此时真正待测试的描述符的个数为11个, 也就是max(8, 9, 10) + 1
注意:
​ 1、果你要检测描述符8, 9, 10, 但是你把select的第一个参数定为8, 实际上只检测0到7, 所以select不会感知到8, 9, 10描述符的变化。
​ 2、果你要检测描述符8, 9, 10, 且你把select的第一个参数定为11, 实际上会检测0-10, 但是, 如果你不把描述如0 set到描述符中, 那么select也不会感知到0描述符的变化。
​ 所以, select感知到描述符变化的必要条件是, 第一个参数要合理, 比如定义为fdmax+1, 且把需要检测的描述符set到描述集中。

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表示select返回之前的时间上限。
如果timeout==NULL,无期限等待下去,这个等待可以被一个信号中断,只有当一个描述符准备好,或者捕获到一个信号时函数才会返回。如果是捕获到信号,select返回-1,并将变量errno设置成EINTR。

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

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

原理

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
​ 执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
​ 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
​ 若再加入fd=2,fd=1,则set变为0001,0011
​ 执行select(6,&set,0,0,0)阻塞等待
​ 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

返回值

成功时:返回三中描述符集合中”准备好了“的文件描述符数量。
超时:返回0
错误:返回-1,并设置 errno

EBADF:集合中包含无效的文件描述符。(文件描述符已经关闭了,或者文件描述符上已经有错误了)。
EINTR:捕获到一个信号。
EINVAL:nfds是负的或者timeout中包含的值无效。
ENOMEM:无法为内部表分配内存。

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不会。

3、select没有sigmask参数.
sigmask:这个参数保存了一组内核应该打开的信号(即:从调用线程的信号掩码中删除)

当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。当前的程序中,新的连接会导致旧的连接被覆盖丢弃。

你可能感兴趣的:(linux网络编程)