1、select():
// 监控readfds、writefds和exceptfds,等待其中的一个或多个文件描述符“就绪”(可读、可写或异常)或到达超时时间
// 成功时返回就绪的描述符数,超时时返回0,出错时返回-1并设置errno int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
timeout参数的三种可能:NULL:select()将永远阻塞直到有描述符准备好I/O;0:检查描述符后立即返回(轮询,polling);等待它指定的一段固定时间。
readfds、writefds和exceptfds描述符集指定我们要让内核测试读、写和异常条件的描述符。以下是操作这些描述符集的四个宏:
void FD_CLR(int fd, fd_set *set); // 从某个集合中移除描述符 int FD_ISSET(int fd, fd_set *set); // 测试描述符是否在某个集合中。通常在select()返回后使用 void FD_SET(int fd, fd_set *set); // 将描述符加入到某个集合 void FD_ZERO(fd_set *set); // 清空某个描述符集
nfds参数是这三个集合中所有文件描述符的最大值加1。描述符0~nfds均将被测试。存在这个参数是为了效率:内核正是通过在进程与内核间不复制描述符集中不必要的部分,从而不测试总为0的那些位来提高效率的。
例子:监控在5秒内标准输入是否准备好读
int main(void) { fd_set rfds; FD_ZERO(&rfds); FD_SET(0, &rfds); // 将标准输入描述符加入rfds struct timeval tv; // 定义超时时间 tv.tv_sec = 5; tv.tv_usec = 0; int retval = select(1, &rfds, NULL, NULL, &tv); if (retval == -1) { /* 出错 */ } else if (retval) { /* 有描述符就绪。此时FD_ISSET(0, &rfds)为真 */ } else { /* 超时返回*/ } return 0; }
select()存在的问题:
1)select()监控的描述符集的大小有限,为FD_SETSIZE(一般是1024。描述符的值也必须小于1024)。
2)描述符集内未就绪描述符对应的位在select()返回时均被清成0,因此每次重新调用该函数时都需要将关注的描述符的位置为1,然后再把描述符集从用户空间拷贝到内核。内核再遍历这些描述符以检查就绪条件。在描述符很多时,这些操作的开销较大。
2、poll():提供的功能和select()类似。
// 成功时返回就绪描述符数(revents成员值非0的描述符数),超时时返回0,出错时返回-1并设置errno int poll(struct pollfd *fds, nfds_t nfds, int timeout);
timeout参数的可能值:大于0:poll()将等待指定数目的毫秒数;0:立即返回;小于0:永远等待。
fds参数指定被监控的描述符集(nfds指定集合的元素数)。以下是pollfd结构:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
其中,fd指定我们关注的文件描述符。如果我们不想再关注某个描述符,可以直接把它设置成负值。
events指定了我们对于fd所感兴趣的事件,revents则是内核返回的实际发生的事件。一些用于events和revents的常值:
POLLIN:有数据可读;POLLPRI:有紧急数据可读(如TCP的带外数据);POLLOUT:可写;POLLERR:发生错误(仅用在revents中);POLLHUP:发生挂起(仅用在revents中);POLLNVAL:描述符不是一个打开的文件(仅用在revents中)。
poll()和select()的比较:
1)poll()没有最大描述符数目的限制。
2)poll()在处理流设备时,提供额外的信息。它识别三类数据:普通、优先级带(如TCP的带外数据)和高优先级。
带外数据:一个连接的某端发生了重要的事情,而且该端希望迅速通告其对端,即这种通知应该在已排队等待发送的任何“普通”/“带内”数据之前发送。带外数据主要用于telnet、rlogin和FTP等远程非活跃应用中。
3)poll()不需要在每次调用时都把关注的fd和events设置一遍,但需要在每次调用时都把关注的fd的pollfd结构拷贝到内核。
3、epoll:以下的系统调用用来创建和管理epoll实例。
1)epoll_create():创建一个epoll实例,并返回其文件描述符。
int epoll_create(int size);
size参数最初是为了告诉内核调用者希望添加到epoll实例的文件描述符数,以此作为内核分配给相关数据结构内存空间大小的参考。自Linux 2.6.8起,size参数被忽略,但必须大于0以保证向后兼容(新的epoll应用运行在旧的内核上时)。内核会根据使用情况动态调整这部分内存空间的大小。
2)epoll_ctl():在epfd指代的epoll实例上执行控制操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op参数的可能值:EPOLL_CTL_ADD(将fd注册到epfd,并关联event和fd)、EPOLL_CTL_MOD(改变fd所关联的事件)、EPOLL_CTL_DEL(从epfd中删除fd。此时event可置为NULL)。
epoll_event结构的定义:
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
其中,events的值包括:EPOLLIN(指定文件描述符可读)、EPOLLOUT(可写)、EPOLLPRI(有紧急数据可读)、EPOLLET(使用边缘触发模式)等。
当前注册到一个epoll实例的文件描述符的集合有时也被称为epoll set。
3)epoll_wait():在epoll文件描述符epfd上等待I/O事件。
// 成功时返回I/O就绪的文件描述符数量,超时时返回0,出错时返回-1并设置errno int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
events指向的结构包含了epoll_wait()返回时的就绪fd(最多maxevents个)。
timeout参数指定epoll_wait()阻塞的最小毫秒数。-1表示epoll_wait()会永远等待;0表示立即返回。
epoll的事件分发接口支持水平触发模式(LT,默认)和边缘触发模式(ET)。两者的区别如下:
假设以下情景:管道读端的文件描述符rfd被注册到epoll实例 -> 写端往管道写2KB数据 -> epoll_wait()将rfd作为就绪的文件描述符返回 -> 管道读端从rfd读取1KB数据 -> 再次调用epoll_wait()。如果刚开始将rfd加入epoll实例时指定了EPOLLET(边缘触发),则尽管输入缓存区还有数据可读,最后一步的epoll_wait()还是会阻塞,直到下一次的事件发生。如果没有指定EPOLLET,则采用默认的水平触发模式,这种情况下只要有就绪的输入数据就会一直通知可读,即最后一步的epoll_wait()将立即返回rfd。
因此,使用ET模式时,一般将相应的fd设置为非阻塞,并在epoll_wait()返回该fd时由我们自己判断何时停止读/写。比如,每次获取到一个读就绪的fd时,循环读取该fd直到出现EAGAIN(当一个fd设置了O_NONBLOCK,而对它的读操作将不得不阻塞时,返回EAGAIN),以读取所有就绪的输入数据。否则,未读完的数据会留到下一次epoll_wait()返回该fd时才读取。容易验证,信号驱动式I/O是边缘触发模式。
而使用LT模式时,epoll只是一个更快的poll(),它们有相同的语意,epoll可以使用在poll()使用的任何场合。容易验证,select()和poll()都是水平触发模式。与ET模式相比,这种模式的编码较为简单,不过系统调用次数更多。
epoll和select()/poll()的比较:
1)epoll在需要监控大量文件描述符时有很好的扩展性。
2)epoll不需要在每次调用时都把关注的fd和events设置一遍,也不用每次都把关注的fd的epoll_event结构拷贝到内核。
一个例子:
// 省略了非关键代码和错误处理代码 #define MAX_EVENTS 10 void do_use_fd(int fd) { while (true) { /* 循环读到EAGAIN */ } } int main() { int epollfd = epoll_create(10); // 监听套接字使用默认的水平触发模式 struct epoll_event ev, events[MAX_EVENTS]; ev.events = EPOLLIN; ev.data.fd = listen_sock; epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev); for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); for (int n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen); // 将已连接套接字设置为非阻塞方式,使用边缘触发模式 setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev); } else { do_use_fd(events[n].data.fd); } } } return 0; }
参考资料:
《Unix网络编程 卷1:套接字联网API》
http://www.cnblogs.com/Anker/p/3265058.html
不断学习中。。。