poll原型
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以 测试其是否有就绪者。
int poll(struct pollfd *fds, int nfds, int timeout);
fds: struct pollfd类型的数组, 用来传递用户关注的文件描述符以及事件类型
struct pollfd
{
int fd; // 文件描述符
short events; // 关注的事件类型 如果关注多种事件类型,可以使用按位或‘|’将这些事件合到一起
short revents; // 由内核填充,poll返回时用来标注就绪的事件类型
};
-
nfds: 数组的大小
-
timeout:超时时间,以毫秒为单位, -1表示一直阻塞到有事件就绪
-
返回值: == 0 超时< 0 出错 > 0 就绪的文件描述符的个数
-
fds参数是一个pollfd类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。其中fd成员指定文件描述符;events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或;revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。
每一个文件描述符都用一个结构体表示,其实传入参数时,传入的是一个结构体数组,数组可以超过1024,可以更大,可以收纳的文件描述符就更多,其次,poll的事件类型也更多。
poll支持的事件类型
- 数据可读是指,接受缓冲区中有数据,读事件就会就绪;
- 数据可写是指,发送缓冲区内有空间,send不会被阻塞,此时写事件就会就绪
因此,在与一个客户端建立连接的初期,接受缓冲区是空的,所以读事件没有就绪,但是发送缓冲区是空的,所以写事件一开始就是就绪的。
poll实现TCP服务器
服务器端代码
// ser.c
#define _GNU_SOURCE //POLLRDHUP
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define NUM 100
// 根据ip地址与端口号创建套接字,返回创建的套接字的文件描述符
int CreateSocket(char *ip, short port)
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1) return -1;
struct sockaddr_in ser_addr;
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(port);
ser_addr.sin_addr.s_addr = inet_addr(ip);
int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if(res == -1) return -1;
res = listen(listenfd, 5);
if(res == -1) return -1;
return listenfd;
}
//初始化文件描述符数组
void InitFds(struct pollfd *fds)
{
int i = 0;
for(; i < NUM; ++i)
{
fds[i].fd = -1;//表示这十个元素都是无效的
fds[i].events = 0;
//revents由内核填充,所以这里不用设置
}
}
// 向fds数组中插入一个fd, 关注的事件类型为events
void InsertFds(struct pollfd *fds, int fd, short events)
{
int i = 0;
for(; i<NUM; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = events;
break;
}
}
}
void DealReadyEvent(struct pollfd *fds, int listenfd)
{
int i = 0;
for(; i<NUM; ++i)
{
if(fds[i].fd == -1) continue;
if(fds[i].fd == listenfd) //是监听套接字
{
if(fds[i].revents & POLLIN)
{
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
int c = accept(listenfd, (struct sockaddr*)&cli_addr, &len);
if(c == -1) continue;
printf("one client link success\n");
InsertFds(fds, c, POLLIN | POLLRDHUP);
}
}
else //是链接套接字
{
if(fds[i].revents & POLLRDHUP)
{
printf("%d client over\n", fds[i].fd);
close(fds[i].fd);
fds[i].fd = -1;
fds[i].events = 0;
}
else if(fds[i].revents & POLLIN)
{
char buff[128] = {0};
int n = recv(fds[i].fd, buff, 127, 0);
if(n<=0)
{
printf("%d client error\n", fds[i].fd);
close(fds[i].fd);
fds[i].fd = -1;
fds[i].events = 0;;
}
else
{
printf("%d: %s\n", fds[i].fd, buff);
send(fds[i].fd, "OK",2, 0);
}
}
}
}
}
int main()
{
int listenfd = CreateSocket("192.168.133.132", 6000);
assert(listenfd != -1);
struct pollfd fds[NUM];
InitFds(fds);
InsertFds(fds, listenfd, POLLIN);
while(1)
{
int n = poll(fds, NUM, -1);//-1永久阻塞,一直等
if(n <= 0)
{
printf("poll error\n");
continue;
}
DealReadyEvent(fds, listenfd);
}
}
客户端代码
// cli.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //字节序的转换
#include <arpa/inet.h> //IP地址转换
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
assert(-1 != sockfd);
struct sockaddr_in ser_addr;
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(6000);
ser_addr.sin_addr.s_addr = inet_addr("192.168.133.132");
int res = connect(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));//指定连接的服务器端的 IP 地址和端口
assert(-1 != res);
while(1)
{
printf("input: ");
char buff[128] = {0};
fgets(buff, 127, stdin);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
send(sockfd, buff, strlen(buff) - 1, 0);
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("%s\n", buff);
}
close(sockfd);
exit(0);
}
poll总结
-
poll所能关注的文件描述符的个数理论上是没有限制的。文件描述符的取值范围也是没有限制的
-
poll支持的事件类型比select更多
-
poll返回时,内核修改的是数组元素中revents成员,与用户填充的关注的事件类型不冲突,所以每次调用poll之前,不需要重新设置这个数组
前三点是与select不一样的,后面的几点是与select相同的
-
poll返回时,也仅仅是返回就绪文件描述符的个数,并没有指定是哪几个文件描述符就绪。所以用户程序检测就绪文件的时间复杂度为O(n)
-
poll每次调用时,也是需要将用户空间的数组传递拷贝给内核,poll返回时又将内核的拷贝到用户空间
-
poll内核也是采用轮询的方式
-
poll也只能工作在LT模式
epoll原型
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、 poll 有很大差异。
首先, epoll 使用一组函数来完成任务,而不是单个函数。其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表
中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集进行遍历。
epoll就绪之后会直接把就绪文件的描述符返回给你,但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 该文件描述符由epoll_create创建,内核实现方式是在文件描述符上注册回调函数来检测时间。
举个例子:我坐在讲台上,注册回调函数,我就说:如果你们谁完成作业了,就交上来。我检测10人,100人是没有区别的。我坐在讲台,拿走多少作业是没有区别的!!!select和poll得循环去检测谁做作业了。而epoll是我坐在讲台上等,等5秒钟就超时,有多少收多少,开销是相同的,时间复杂度是O(1)
epoll 相关的函数如下:
epoll_create():用于创建内核事件表
epoll_ctl():用于操作内核事件表
epoll_wait():用于在一段超时时间内等待一组文件描述符上的事件
创建内核事件表
#include <sys/epoll.h>
int epoll_create(int size);
- 成功返回内核事件表的文件描述符,失败返回-1
- size:size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大
- 内核事件表:在内核中创建的一个记录用户关注的文件描述符和事件的集合,其数据结构为红黑树
操作内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
: epoll_create方法返回的文件描述符,指定要操作的内核事件表的文件描述符op
: 指定操作类型,添加 EPOLL_CTL_ADD, 修改 EPOLL_CTL_MOD, 删除 EPOLL_CTL_DELfd
: 指定要操作的文件描述符event
:指定事件,它是 epoll_event 结构指针类型, epoll_event 的定义如下:
struct epoll_event
{
uint32_t events; //事件的集合
epoll_data_t data;//用户数据,data.fd 文件描述符
};
其中, events 成员描述事件类型, epoll 支持的事件类型与 poll 基本相同,表示 epoll 事件的宏是在 poll 对应的宏前加上‘E’ ,比如 epoll 的数据可读事件是 EPOLLIN。但是 epoll 有两个额外的事件类型—EPOLLET 和 EPOLLONESHOT。 data 成员用于存储用户数据,是一个联合体,其定义如下:
typedef union epoll_data
{
int fd; //文件描述符
void *ptr;//用户数据
uint32_t u32;
uint64_t u64;
}epoll_data_t;
- 成功返回0,失败返回-1
epoll_wait:真正执行I/O复用的方法
epoll_wait函数在一段超时时间内等待一组文件描述符上的事件,原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
expfd
:这次处理的内核事件表的文件描述符events
:用户数组,这个用户数组所有的都是内核填充的,用来返回所有就绪的文件描述符以及事件,这个数组仅仅在 epoll_wait
返回时保存内核检测到的所有就绪事件,而不像 select 和 poll
的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率maxevents
:events数组的长度,即指定最多监听多少个事件timeout
:定时时间,单位为毫秒,如果 timeout 为 0,则 epoll_wait 会立即返回,如果timeout 为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。- 返回值:成功返回就绪的文件描述符的个数,出错返回-1,超时返回0
epoll实现TCP服务器
服务器端代码:
//ser.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define NUM 100
// 根据ip地址与端口号创建套接字,返回创建的套接字的文件描述符
int CreateSocket(char *ip, short port)
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd == -1) return -1;
struct sockaddr_in ser_addr;
memset(&ser_addr, 0, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(port);
ser_addr.sin_addr.s_addr = inet_addr(ip);
int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if(res == -1) return -1;
res = listen(listenfd, 5);
if(res == -1) return -1;
return listenfd;
}
void DealReadyEvents(struct epoll_event *events, int n, int epfd, int listenfd)
{
int i = 0;
for(; i<n; ++i)
{
int fd = events[i].data.fd;
if(fd == listenfd) //监听的套接字
{
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
int c = accept(listenfd, (struct sockaddr*)&cli_addr,&len);
if(c == -1) continue;
struct epoll_event event;
event.events = EPOLLIN | EPOLLRDHUP;
event.data.fd = c;
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, c, &event);
assert(res != -1);
printf("one client link success\n");
}
else //链接的套接字
{
if(events[i].events & EPOLLRDHUP) //客户端关闭
{
int res = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
assert(res != -1);
close(fd);
}
else if(events[i].events & EPOLLIN) //客户端发送来了数据
{
char buff[128] = {0};
int n = recv(fd, buff, 127, 0);
if(n <= 0)
{
printf("%d client error\n", fd);
int res = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
assert(res != -1);
close(fd);
}
else //出错
{
printf("%d: %s\n",fd, buff);
send(fd, "OK", 2,0);
}
}
else
{
printf("error\n");
}
}
}
}
int main()
{
int listenfd = CreateSocket("192.168.133.132", 6000);
assert(-1 != listenfd);
int epfd = epoll_create(5);
assert(epfd != -1);
struct epoll_event events;
events.events = EPOLLIN;
events.data.fd = listenfd;
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &events);
assert(res != -1);
while(1)
{
struct epoll_event events[NUM];
int n = epoll_wait(epfd, events, NUM, -1);
if(n<=0)
{
printf("epoll error \n");
continue;
}
DealReadyEvents(events, n,epfd, listenfd);
}
exit(0);
}
epoll总结
- epoll不再是一个接口,而是一组接口
- 能够监听的文件描述符个数没有限制,值的范围也没有限制
- 关注的事件类型更多,比poll多了EPOLLET和EPOLLONESHOT
- epoll在内核中维护用户关注的事件类型,每次epoll_wait时不需要传递用户空间的数据。epoll_wait返回时,仅仅返回就绪的文件描述符和事件,相比于select和poll效率更高
- epoll_wait返回的仅仅是就绪的事件,所以用户程序检测就绪文件描述符的时间复杂度就为O(1)
- epoll内核采用的是回调的方式检测事件就绪
- epoll不仅支持LT模式,也支持高效的ET模式
epoll更高效的原因
- select,poll,epoll虽然都会返回就绪的文件描述符的数量,但是select和poll不会明确地支出是哪些文件描述符已经就绪,而epoll会,造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听整个文件描述符找到谁处于就绪状态,而epoll直接处理即可;
- select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来,而epoll创建的有关文件描述符的数据结构本身就存在与内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销
- select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制,造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大的影响,除非活跃的socket很多。
- epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符,虽然epoll性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能都比epoll号,毕竟epoll的通知机制需要很多函数回调。
epoll的LT与ET模式
epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(EdgeTrigger,边沿触发)模式。
- LT 模式是默认的工作模式。(也是select和poll具备的)当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET模式来操作该文件描述符。 对于 LT 模式操作的文件描述符,当 epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序
下一次
调用 epoll_wait时,还会再次向应用程序通告此事件,直到该事件被处理 - 对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须
立即
处理该事件,因为后续的epoll_wait 调用将不再向应用程序通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率比 LT 模式高
EPOLLONESHOT事件
即使使用高效的ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中是线程不安全的,可能会出现两个线程同时操作一个socket的局面。如果希望一个socket连接的任一时刻都只被一个线程处理,可以使用 EPOLLONESHOT 事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
注意:注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。