select 和 poll

select

我们调用 select 告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。不局限于套接字,任何描述符都可以使用 select 来测试。select 与recv 和 send 直接操作文件描述符不同,它先对需要操作的文件描述符进行查询,查看是否目标文件描述符可以进行读、写或者错误操作,然后当文件描述符满足操作的条件的时候才进行真正的IO操作。

函数原型

select 函数原型如下:

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
// 成功返回就绪描述符的个数,超时返回0,失败返回-1

1.maxfdp1,指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0,1,2,…,一直到 maxfdp1 - 1 均被测试。
头文件 <sys/select.h> 中定义的 FD_SETSIZE 常值时数据类型 fd_set 中的描述符总数,其值通常是1024
2.fd_set,一个表示描述符集的数据类型,具体的实现细节被隐藏在 fd_set 和以下四个宏中:

void FD_ZERO(fd_set* fdset);	// clear all bits in fdset
void FD_SET(int fd, fd_set* fdset);	// turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set* fdset);	// turn off the bit for fd in fdset
int FD_ISSET(int fd, fd_set* fdset);	// is the bit for fd on in fdset?

描述符在描述符集中通常被称为位(bit),例如”打开读集合中表示监听描述符的位“。
readset、writeset 和 exceptset 分别是指向读、写、异常条件的描述符集的指针。
3.timeval,一个表示时间的结构,其结构如下:

struct timeval
{
	long tv_sec;	// seconds
	long tv_usec;	// microseconds
};

timeout,告知内核等待所指定描述符中的任何一个就绪可花多长时间。
将第三个参数设置成空指针表示 select 函数会一直等待下去,直到有一个描述符就绪。

函数作用:select 函数修改由指针 readset、writeset 和 exceptset 所指向的描述符集,这三个参数都是值-结果参数。调用该函数时,指定所关心的描述符的值,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,可以通过 FD_ISSET 宏来测试 fd_set 数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。


描述符就绪条件

可读性和可写性对于普通文件描述符显而易见,但对于套接字描述符需要被讨论得更加明确。

当满足下列四个条件中的任何一个时,套接字可读:
1.该套接字接收缓冲区的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小(其值默认为1)。
2.该连接的读半部关闭(接受了 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回 EOF)。
3.该套接字是一个接听套接字且已完成的连接数不为0。对这样的套接字的 accept 通常不会阻塞。
4.其上有一个套接字错误待处理。

当满足下列四个条件中的任何一个时,套接字可写:
1.该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前值(其默认值为2048)。
2.该连接的写半部关闭。对这样的套接字的写操作将产生 SIGPIPE 信号。
3.使用非阻塞式 connect 的套接字已建立连接,或者 connect 已经以失败告终。
4.如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。

当某个套接字上发生错误时,它将被 select 标记为即可读又可写。

select 使用例子

#include <sys/select.h>
#include <sys/time.h>
#include <sys/errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
using namespace std;

int main()
{
    bool isIn = true;
    char buf[1024];
    int buflen = 0;
    char* ptr;
    fd_set fdset;
    fd_set* pfdset = &fdset;
    FD_ZERO(pfdset);

    while(1)
    {
        // 由于每次select返回后会把未就绪的描述符位置为0,所以在循环开头要重新设置描述符位
        if(isIn)
            FD_SET(fileno(stdin), pfdset);
        FD_SET(fileno(stdout), pfdset);
        int maxfdp1 = max(fileno(stdin), fileno(stdout)) + 1;
        if(select(maxfdp1, pfdset, pfdset, NULL, NULL) >= 0)
        {
            if(FD_ISSET(fileno(stdout), pfdset))
            {
                ptr = buf;
                // 默认套接字发送缓冲区的低水位标记为2048,因此小于2048的字节在一次write调用中能全部写入缓冲区而不会在while循环中阻塞
                while(buflen > 0)
                {
                    int n;
                    if((n = write(fileno(stdout), ptr, buflen)) >= 0)
                    {
                        buflen -= n;
                        ptr += n;
                    }
                    else
                        exit(1);
                }
                if(!isIn)
                    break;
            }

            if(FD_ISSET(fileno(stdin), pfdset))
            {
                buflen = 0;
                // 会读入换行符 \n
                if((buflen = read(fileno(stdin), buf, 1024)) == 0)
                {
                    isIn = false;
                    FD_CLR(fileno(stdin), pfdset);
                }
                else if(buflen < 0)
                    exit(1);
            }
        }
        else
        {
            if(errno == EINTR)
                continue;
            else
                exit(1);
        }
    }

    exit(0);
}

pselect

#include <sys/select.h>
#include <signal.h>
#include <time.h>

int pselect(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timespec* timeout, const sigset_t* sigmask);
// 成功返回就绪描述符的数目,超时返回0,失败返回-1

pselect 相对于 select 有两个变化:
1.pselect 使用timespec结构,而不是用 timeval 结构。

struct timespec
{
	time_t tv_sec;	// seconds
	long tv_nsec;	// nanoseconds
};

2.pselect 增加了第六个参数:一个指向信号掩码的指针。
先看接下来两段代码:

// A1
if(intr_flag)
	handle_intr();
// A2
if((nready = select(...)) < 0)
{
// A3
	if(errno == EINTR)
	{
		if(intr_flag)
			handle_intr();
	}
	...
}
sigset_t newmask, oldmask, zeromask;

sigemptyset(&zeromsak);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
// B1
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// B2
if(intr_flag)
	handle_intr();
if((nready = pselect(..., &zeromask)) < 0)
{
// A3
	if(errno == EINTR)
	{
		if(intr_flag)
			handle_intr();
	}
	...
}

有一个前提和一个要求,前提是 SIGINT 的信号处理函数只会设置全局变量 intr_flag;要求是无论何时产生 SIGINT 信号,程序都必须执行 handle_intr 函数。

先看第一段代码,不管是在A1点还是A3点产生了SIGINT信号,程序都能够执行 handle_intr 函数。但是若在A2点产生了 SIGINT 信号,由于 select 是慢系统调用,程序可能一直不能执行 handle_intr 函数,或者是在 select 阻塞时产生了其他信号导致程序退出,并没有执行 handle_intr函数。而 pselect 能够避免这种情况的发生。

再看第二段代码,如果在B1点产生信号,信号掩码还没被更换,SIGINT 信号未被阻塞,所以 intr_flag 变量会被设置且程序能够执行 handle_intr。如果在B2点产生信号,由于信号掩码已经被更换,SIGINT 信号被阻塞,等到调用 pselect 时,它先以空集替代进程信号掩码,SIGINT 不被阻塞,信号处理函数完成后 pselect 被中断,并执行 handle_intr。如果在B3点产生 SIGINT 信号,由于在 pselect 阻塞时,进程掩码是空集,因此 SIGINT 信号不会被阻塞,pselect 会被中断并执行handle_intr。

pselect 返回时,进程的信号掩码会被重置为调用pselect之前的值。

poll

函数原型

#include <poll.h>

int poll(struct pollfd* fdarray, unsigned long nfds, int timeout);
// 成功返回就绪描述符数目,超时返回0,失败返回-1

1.第一个参数是指向一个结构数组第一个元素的指针,每个元素都是一个 pollfd 结构。pollfd 结构如下:

struct pollfd
{
	int fd;	// descriptor to check
	short events;	// events of interest on fd
	short revents;	// events that occurred on fd
};

要测试的条件由 events 成员指定,函数在相应的 revents 成员中返回该描述符的状态。这两个成员都由指定某个特定条件的一位或多位构成。

常值作为 events 的输入作为 revents 的结果说明
POLLIN11普通或优先级带数据可读
POLLRDNORM11普通数据可读
POLLRDBAND11优先级带数据可读
POLLOUT11普通数据可写
POLLWRNORM11普通数据可写
POLLWRBAND11优先级带数据可写
POLL ERR1发生错误
POLLHUP1发生挂起
POLLNVAL1描述符不是一个打开的文件

2.第二个参数 nfds 指定结构数组中元素的个数。
3.第三个参数 timeout 指定 poll 函数返回前等待多长时间,单位为毫秒。INFTIM(通常为负数) 宏为永久等待,0为立即返回,正数等待指定数目的毫秒数。


引起 poll 返回特定的 revent 的条件

就 TCP 和 UDP 套接字而言,以下条件一起 poll 返回特定的 revent:
1.所有正规 TCP 数据 和 所有 UDP 数据都被认为是普通数据。
2.TCP 的带外数据被认为是优先级带数据。
3.当 TCP 连接的读半部关闭时,也被认为是普通数据,随后的读操作返回0。
4.TCP 连接存在错误即可认为是普通数据,也可认为是错误。无论那种情况,随后的读操作将返回-1,并把 errno 设置成合适的值。这可用于处理诸如接受到 RST 或发生超时条件。
5.在监听套接字上由新的连接可认为是普通数据,也可认为是优先级带数据。大多数视之为普通数据。
6.非阻塞式 connect 的完成被认识是使相应套接字可写。


poll 简单例子

#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <iostream>
using namespace std;

int main()
{
    char buf[1024];
    int buflen = 0;
    struct pollfd fdarray;
    fdarray.fd = fileno(stdin);
    fdarray.events = POLLRDNORM | POLLRDBAND;

    if(poll(&fdarray, 1, -1) >= 0)
    {
        if(fdarray.revents & POLLRDNORM)
        {
            cout << 1 << endl;
            if((buflen = read(fdarray.fd, buf, 1024)) < 0)
            {
                exit(1);
            }

            char* ptr = buf;
            // 同样由于套接字发送缓冲区低水位标记,一般在一次write调用中就完成输出
            while(buflen)
            {
                int n;
                if((n = write(fileno(stdout), ptr, buflen)) < 0)
                {
                    exit(1);
                }
                ptr += n;
                buflen -= n;
            }
        }

        if(fdarray.revents & POLLRDBAND)
        {
            cout << 2 << endl;
            if((buflen = read(fdarray.fd, buf, 1024)) < 0)
            {
                exit(1);
            }

            char* ptr = buf;
            while(buflen)
            {
                int n;
                if((n = write(fileno(stdout), ptr, buflen)) < 0)
                {
                    exit(1);
                }
                ptr += n;
                buflen -= n;
            }
        }
    }
    else
    {
        exit(1);
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
selectpoll都是用于多路复用(multiplexing)IO操作的系统调用,用于在多个文件描述符上等待可读、可写或异常事件的发生。 1. select:早期的多路复用机制,适用于文件描述符数量较少的情况。它将一组文件描述符传递给内核,并在这些描述符上等待事件发生。当有一个或多个描述符准备好时,select函数将返回,并且可以通过遍历描述符集合来确定哪些描述符准备好了。 2. poll:与select类似,也是多路复用的一种机制,但在设计上更加灵活和高效。poll函数接受一个pollfd结构体数组,每个结构体指定一个文件描述符和所关心的事件类型。调用poll后,内核会检查每个pollfd中指定的描述符,返回就绪的描述符以及就绪的事件类型。 区别: - select使用fd_set来存放文件描述符集合,而poll使用pollfd数组来存放。 - select每次调用都需要将fd_set从用户空间拷贝到内核空间,而poll只需要一次传递pollfd数组的指针。 - select有文件描述符数量上限限制,而poll没有(但是并不是无限制)。 - select返回就绪的文件描述符时需要遍历整个fd_set,而poll返回就绪的文件描述符时只需检查就绪的pollfd。 - select在文件描述符数量较少时性能可能较好,而poll在文件描述符数量较多时性能可能更好。 需要注意的是,selectpoll都是阻塞调用,即在没有任何事件发生时会一直等待,直到有事件发生或超时。为了实现非阻塞的IO操作,可以使用非阻塞文件描述符或结合其他机制(如线程、信号等)来实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值