9.1 select系统调用
- 在一段指定的事件内,监听用户感兴趣的文件描述符上:可读、可写、异常 事件、
9.1.1 select API
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
//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);
} fd_set;
- nfds:指定监听的文件描述符的总数,通常是select监听的所有文件描述符中的最大值+1。
- readfds、writefds、exceptfds分别指向可读、可写、异常事件对于文件描述符集合。
- fd_set能够容纳的文件描述符由FD_SETSIZE指定。
9.1.2 文件描述符就绪条件
下列情况socket可读
- socket内核接收缓冲区字节数大于等于低水位标记
- socket通信对方关闭连接,此时socket读操作返回0。
- 监听socket上有新的连接
- socket有处理错误,可用getsockopt读取和清除错误。
下列情况socket可写
- socket内核发送缓冲区可用字节数大于等于低水位标记
- socket通信写操作被关闭,对写操作关闭的socket执行写操作回触发SIGPIPE信号。
- socket使用非阻塞connect连接成功或失败之后
- socket有处理错误,可用getsockopt读取和清除错误。
socket能处理的异常只有:socket接收到带外数据
9.2 poll系统调用
- 与select类似,也是轮询,测试是否有就绪者
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd; //文件描述符
short events; //注册的事件
short revents; // 实际发生的事件,由内核填充
};
- fds是pollfd结构类型的数组,指定我没感兴趣的文案描述符上发生的可读可写异常事件。
- poll事件类型:
9.3 epoll 系统调用
9.3.1 内核事件表
- epoll与select和poll的区别是一组函数而不是单个函数。
- epoll把用户关系的文件描述符事件放在内核的事件表中,无需像select和poll每次调用都要重传
- 但是epoll需要一个额外的文件描述符,用来标志内核中的这个事件表。
- 这个文件描述符使用epoll_create函数创建
9.3.1.1创建内核事件表
#include <sys/epoll.h>
int epoll_create(int size);
- size参数只是给内核一个提示,告诉事件表大概需要多大,返回值是epoll文件描述符。
9.3.2 操作内核事件表
#include <sys/epoll.h>
//成功返回0,失败返回-1并设置errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event{
__uin32_t events; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- epfd是内核事件表的文件描述符,也就是epoll_create创建的文件描述符。
- fd是要操作的文件描述符
- op是指定的操作类型:
- EPOLL_CTL_ADD:往事件表中注册fd事件
- EPOLL_CTL_MOD:修改fd上的注册事件
- EPOLL_CTL_DEL:删除fd上的注册事件
- event参数指定事件,epoll支持的事件类型与poll基本相同,差别在于poll支持的事件类型前加上E,epoll有EPOLLET和EPOLLONESHOT两个事件。
- data成员用于存储用户数据,一般使用fd,或者用ptr指向的对象中有fd成员。
9.3.2 epoll_wait函数
- 该函数在一段时间内等待一组文件描述符上的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
- 成功时返回就绪文件描述符的个数,失败返回-1并设置errno。如果设置timeout,时间到还是没有事件发生,就返回0。
- maxevents指定最多监听多少事件,必须大于0
timeout参数设置毫秒超时时间。 - epollwait如果检测到时间,就将所有在epfd上注册的,就绪的事件复制到第二个参数events指向的数组中。
例如:
//对于poll返回的就绪必须遍历
assert(poll(fds, MAX_EVENT_NNUMBER, -1) != -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){
int sockfd = events[i].data.fd;
//处理socket
...
}
9.3.3 LT和ET模式
- LT:level trigger,电平触发;ET:edge trigger,边沿触发。
默认LT,ET高效 - 区别:采用LT时,epoll_wait检测到事件发生并通知程序,程序可以不立刻处理,这样下次调用epoll_wait,它会再次通知程序。采用ET时,epoll_wait检测到事件发生并通知程序,程序必须立刻处理,如果不处理,后续调用epoll_wait不会再次通知了。
- Et降低了同一个epoll重复触发的次数。
- LT和ET区别例子:
#include "../create_sockfd.h"
#include <sys/epoll.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符fd上的可读事件EPOLLIN注册到内核事件表中,参数enable_et表示是否开启ET模式
void addfd(int epollfd, int fd, bool enable_et){
epoll_event event;
event.events = EPOLLIN;
event.data.fd = fd;
if(enable_et){
event.events |= EPOLLET;
}
int ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
if(ret == -1){
printf("epoll ctl error: errno:%d\n", errno);
exit(0);
}
setnonblocking(fd);
}
//LT模式工作流程
void LTProcess(epoll_event* events, int number, int epollfd, createSockfd *sfd){
char buf[BUFFER_SIZE];
for(int i = 0; i != number; ++i){
int sockfd = events[i].data.fd;
//接受到新的连接请求
if(sockfd == sfd->sockfd){
printf("LT new connect\n");
//接受请求
sfd->acceptfd();
//注册事件
addfd(epollfd, sfd->connfd, false);
} else if(events[i].events & EPOLLIN){
//只要socket对于的读缓存还有没读出来的数据,这段代码就会被触发。
printf("LT trigger.\n");
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if(ret <= 0){
close(sockfd);
continue;
}
printf("LT:Get %d bytes of content: %s\n", ret, buf);
} else {
printf("Only support EPOLLIN.\n");
}
}
}
//ET工作模式
void ETProcess(epoll_event* events, int number, int epollfd, createSockfd *sfd){
char buf[BUFFER_SIZE];
for(int i = 0; i != number; ++i){
int sockfd = events[i].data.fd;
//接受到新的连接请求
if(sockfd == sfd->sockfd){
//接受请求
sfd->acceptfd();
//注册事件
addfd(epollfd, sfd->connfd, false);
} else if(events[i].events & EPOLLIN){
//这段代码不会重复触发,所以要一次读完。
printf("ET trigger.\n");
while(1){
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
if(ret < 0){
//对于非阻塞IO下边条件成立代表数据全部读完
if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
printf("read later.\n");
break; //跳出while循环
}
//没读完出错
close(sockfd);
break;
}else if(ret == 0){ //对方已经关闭连接
close(sockfd);
break;
}else {
printf("ET:Get %d bytes of content: %s\n", ret, buf);
}
}
} else {
printf("Only support EPOLLIN.\n");
}
}
}
int main(int argc, char *argv[]){
if(argc <= 3){
printf("Usage: %s ip_address port_number et[lt].\n", basename(argv[0]));
return 1;
}
createSockfd sockfd(argv[1], atoi(argv[2]));
assert(sockfd.bindSockfd() != -1);
assert(sockfd.listenfd(5) != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
if(strcmp(argv[3], "et") == 0){
addfd(epollfd, sockfd.sockfd, true);
}else if(strcmp(argv[3], "lt") == 0){
addfd(epollfd, sockfd.sockfd, false);
} else {
printf("Only support lt and et, you provided %s\n", argv[3]);
return 1;
}
while(1){
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if(ret < 0){
printf("epoll_wait error.\n");
return 1;
}
if(strcmp(argv[3], "et") == 0){
ETProcess(events, ret, epollfd, &sockfd);
}else if(strcmp(argv[3], "lt") == 0){
LTProcess(events, ret, epollfd, &sockfd);
}
}
return 0;
}
以LT模式运行程序./testetel 127.0.0.1 12355 lt
使用以下程序发送数据到端口
#include "../create_sockfd.h"
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#define BUFF_SIZE 1024
int main(int argc, char *argv[]){
if (argc <= 2){
std::cout << "usage: " << basename(argv[0]) << " ip_address port_number" << std::endl;
return 1;
}
createSockfd sockfd(argv[1], atoi(argv[2]));
if (sockfd.connectSockfd() < 0){
std::cout << "connect faild." << std::endl;
} else {
char data[BUFF_SIZE];
memset(data, '\0', BUFF_SIZE);
scanf("%s", data);
printf("data = %s\n", data);
printf("ret = %zu\n", send(sockfd.sockfd, data, strlen(data), 0));
}
return 0;
}
服务器端的输出,因为是LT,所以分三次接收
LT new connect
LT trigger.
LT:Get 9 bytes of content: 123456789
LT trigger.
LT:Get 9 bytes of content: 101112134
LT trigger.
LT:Get 1 bytes of content: 5
对于et模式,一次接收
ET trigger.
ET:Get 9 bytes of content: 123456789
ET:Get 9 bytes of content: 132135465
ET:Get 5 bytes of content: 13135
9.4 三组IO复用函数的比较
- poll和select都是轮询,事件复杂度是O(n)
- epoll是采用回调的方式,内核检测到就绪的文件描述符时,就会触发回调函数。算法时间复杂度是O(1)。
- 对于活动连接多的时候,epoll_wait可能会因为回调函数触发频繁导致效率降低。
- 所以epoll适合连接数多,但活动链接少的情况。
三者的区别