1 select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
(1)nfds:被监听的文件描述符的总数,通常被设置为select监听的所有文件描述符中的最大值加1
(2)
readfds:可读的文件描述符集合
writefds:可写的文件描述符集合
exceptfds:异常的文件描述符集合
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_mask fds_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结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符,fd_set能容纳的文件描述符数量由FD_SETSIZE指定。
操作:
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_ISSET(int fd, fd_set* fdset);/*测试fdset的位fd是否被设置*/
(3)timeout:超时时间。
struct timeval
{
long tv_sec;/*秒数*/
long tv_usec;/*微秒数*/
};
如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select将立即返回。如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
select成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。
select失败时返回-1并设置errno。
select每次调用需要把fd_set集合,从用户空间copy到内核空间。
1.1 select使用范例
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(ip);
servaddr.sin_port = htons(port);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno));
}
listen(sockfd, 10);
printf("listen finshed: %d\n", sockfd); // 3
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1) {
int nready = select(maxfd+1, &rfds, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rfds)) { // accept
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) maxfd = clientfd;
}
// recv
int i = 0;
for (i = sockfd+1; i <= maxfd;i++) {
if (FD_ISSET(i, &rfds)) {
char buffer[1024] = {0};
int count = recv(i, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", i);
close(i);
FD_CLR(i, &rfds);
continue;
}
printf("RECV: %s\n", buffer);
count = send(i, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
getchar();
printf("exit\n");
return 0;
上述代码中,select监听了两种fd,一种是listenfd,监听是否有连接,有连接后,select监听了clientfd,即监听是否有数据。
2 poll
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
(1)fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。
struct pollfd
{
int fd;
short events;
short revents;
};
(2)nfds参数指定被监听事件集合fds的大小
(3)timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
poll与select并没有太大差别,差别仅为select需传readfds、writefds、exceptfds这三个参数,而poll中只需要传fds。
3 epoll
epoll使用一组函数来完成任务,而不是单个函数。
epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。因此,epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
3.1 epoll_create
int epoll_create(int size);
创建内核时间表对应的fd。
3.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
操作事件:
- EPOLL_CTL_ADD,往事件表中注册fd上的事件。
- EPOLL_CTL_MOD,修改fd上的注册事件。
- EPOLL_CTL_DEL,删除fd上的注册事件。
struct epoll_event
{
__uint32_t events; /*epoll事件*/
epoll_data_t data; /*用户数据*/
};
epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是EPOLLIN。
3.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
等待一组文件描述符上的事件。
成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。
3.4 LT和ET模式
LT(Level Trigger,水平触发):如果某个文件描述符上有数据可读,内核会持续通知应用程序,直到应用程序处理完数据或者缓冲区不再有数据可读为止
ET(Edge Trigger,边缘触发):只有在文件描述符状态变化(对于EPOLLIN事件,只有在状态从未就绪变化为就绪,才叫做变化)发生时才会触发事件
对于listenfd,需要使用LT,因为每次有连接时,就会触发。
3.5 epoll使用范例
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
struct epoll_event events[1024] = {0};
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0;i < nready;i ++) {
int connfd = events[i].data.fd;
if (connfd == sockfd) {
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept finshed: %d\n", clientfd);
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
} else if (events[i].events & EPOLLIN) {
char buffer[1024] = {0};
int count = recv(connfd, buffer, 1024, 0);
if (count == 0) { // disconnect
printf("client disconnect: %d\n", connfd);
close(connfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
continue;
}
printf("RECV: %s\n", buffer);
count = send(connfd, buffer, count, 0);
printf("SEND: %d\n", count);
}
}
}
3.6 epoll的优势
epoll更适用于高并发场景,因为select/poll每次都需要将参数从用户空间拷贝到内核空间,而epoll只需要注册事件,当有事件的时候内核会直接通知,避免了频繁的拷贝。
参考文献:
《Linux高性能服务器编程》