一、select系统调用
1.selectAPI
原型:
#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout)
【1】nfds参数指定被监听的文件描述符的总数。
【2】readfds,writefds,exceptfds分别指向可读,可写和异常等事件对应的文件描述符集合。通过这三个参数传入自己感兴趣的文件描述符;select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。三个参数都是fd_set结构指针类型:
#include <typesizes.h>
#define __FD_SETSIZE 1024
#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef __NFDBITS
#define __NFDBITS (8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef__USE_XOPEN
__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;
fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作太繁琐,可以使用下列的宏来访问fd_set结构体中的位:
#include <sys/select.h>
FD_ZERO(fd_set *fdset) //清除fdset的所有位
FD_SET(int fd, fd_set *fdset) //设置fdset的位fd
FD_CLR(int fd, fd_set *fdset) //清除fdset的位fd
int FD_ISDET (int fd,fd_set *fdset); //测试fdset的位fd是否被设置
【3】timeout参数用来设置select函数的超时时间。内核将修改它以告诉应用程序select等待了多久。
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
};
给timeout变量的两个成员都传递0,则select将立即返回。如果给timeout传递NULL,select将一直阻塞,直到某个文件描述符就绪。
【4】select成功时返回就绪(可读、可写、异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。失败返回-1并设置errno。如果在等待期间程序接收到信号,则select立即返回-1,并设置errno为EINTR.
2.文件描述符就绪条件
【1】socket可读:
- socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RECVLOWAT.
- socket通信的对方关闭连接。此时对该socket的读操作返回0.
- 监听socket上有新的连接请求.
- socket上有未处理的错误.
【2】socket可写:
- socket 内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT.
socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号.
socket使用非阻塞connect连接成功或失败(超时)之后.
socket上有未处理的错误.
网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据
二、poll系统调用
原型:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
【1】fds参数是一个pollfd结构类型的数组,他指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。
struct pollfd
{
int fd; //文件描述符
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
}
fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。poll支持的事件类型如下:
通常,应用程序需要根据recv调用的返回值来区分socket上接收的是有效数据还是对方关闭连接的请求,并做相应的处理。GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。但使用时,需在代码最开始处定义__GNU__SOUREC.
【2】nfds参数指定被监听事件集合fds的大小,其类型定义如下:
typedef unsigned long int nfds_t;
【3】timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
poll系统调用的返回值含义与select相同。
三、epoll系列系统调用
1.内核事件表
【1】epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,所以用一个额外的文件描述符来唯一标识内核中的这个时间表。这个文件描述符使用如下epoll_create函数来创建:
#include<sys/epoll.h>
int epoll_create(int size)
size参数现在并不起作用,只是给内核一个提示,告诉它事件需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
【2】操作epoll的内核事件表函数
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
- fd参数是要操作的文件描述符;
- op参数指定操作类型,有如下三种:
EPOLL_CTL_ADD //往事件表中注册fd上的事件
EPOLL_CTL_MOD //修改fd上的注册事件
EPOLL_CTL_DEL //删除fd上的注册事件
- event参数指定事件,是epoll_event结构指针类型
struct epoll_event
{
uint32_t events; //epoll事件
epoll_data_t data; //用户数据
};
- events成员描述事件类型。epoll支持的事件类型和poll基本相同,标识epoll 事件类型的宏是在poll对应的宏前加上“E”。epoll还有两个额外的事件类型——-EPOLLET和EPOLLONESHOT。
- data成员用于存储用户数据,其类型epoll_data_t定义:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_ctl成功时返回0,失败返回-1,并设置errno。
2.epoll_wait函数
在一段超时时间内等待一组文件描述符上的时间
原型:
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
成功时返回就绪的文件描述符的个数,失败返回-1并设置errno
- timeout与poll借口的timeout参数含义相同。
- maxevents参数指定最多监听多少个事件,必须大于0.
- epoll_wait检测到事件,就将所有就绪的事件从内核表中复制到它第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件。
poll和epoll在使用上的区别
poll:
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < MAX_EVENT_NUMBER; ++i) //必须遍历一遍已注册的文件描述符
{
if (fds[i].revents&POLLIN)
{
int sockfd =fds[i].fd;
//处理sockfd
}
}
epoll:
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for (int i = 0; i < ret; i++) //只需遍历就绪的ret个文件描述符
int sockfd = events[i].data.fd;
//处理sockfd
}
}
3.LT/ET模式
【1】epoll对文件描述的操作有两种模式:LT(Level Trigger, 电平触发)模式和ET(Edge Trigger, 边沿触发)模式。
【2】默认工作模式为LT模式,当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll就以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
【3】LT模式的文件描述符,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。而ET模式下,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序必须立即处理该事件,后续将不再向应用程序通知。ET模式在很大程度上降低了同一个epoll事件被触发的次数
运行上面的代码,传输超过10字节大小的数据,结果如下:
LT模式
ET模式
4.EPOLLONESHOT模式
即使是ET模式下一个socket上某个事件还是可能被触发多次,比如一个线程在读取完socket上数据后去处理数据时,又有新数据可读,此时就会有另一个线程来读取这些数据,于是现在就是两个线程同时操作一个线程,这种情况就可以用EPOLLONESHOT事件处理
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或异常事件,且只触发一次,除非使用epoll_ctl重置该文件描述符上注册的EPOLLONESHOT事件。
所以注册了EPOLLONESHOT事件的socket,被一个线程处理完后就要立即重置这个socket上的EPOLLONESHOT事件。