文章目录
poll
poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。poll的原型如下:
#include <poll.h>
int poll(struct pollfd *fds, int nfds, int timeout);
- fds:struct pollfd类型的数组,用来传递用户关注的文件描述符以及事件类型,pollfd结构体如下:
struct pollfd { int fd; // 文件描述符 short events; // 关注的事件类型 short revents; // 由内核填充,poll返回时用来标注就绪的事件类型 }
- nfds:数组的大小
- timeout:定时时间,单位毫秒,-1表示一直阻塞直到有事件就绪,timeout 为 0 时, poll 调用将立即返回。
- 返回值:成功返回就绪文件描述符的总数,超时返回 0,失败返回-1
poll支持的事件类型
(可以在events中填充的事件类型,如果关注多种事件类型,可以使用按位或将这些事件合到一起)
值 | 描述 |
---|---|
POLLIN | 数据可读(包括普通数据和优先数据) |
POOLLOUT | 数据可写(包括普通数据和优先数据) |
POLLRDHUP | TCP连接被对方关闭,对方关闭了写操作,它由 GNU 引入,使用时,需要在代码开始处定义_GNU_SOURCE |
POLLHUP | 挂起,管道写端关闭,读端会收到该事件 |
POLLERR | 错误 |
POLLNVAL | 文件描述符没有打开 |
使用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);
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总结
1、poll所能关注的文件描述符的个数理论上是没有限制的。文件描述符的取值范围也是没有限制的
2、poll支持的事件类型比select更多
3、poll返回时,内核修改的是数组元素中revents成员,与用户填充的关注的事件类型不冲突,所以每次调用poll之前,不需要重新设置这个数组
前三点是与select不一样的,后面的几点是与select相同的
4、poll返回时,也仅仅是返回就绪文件描述符的个数,并没有指定是哪几个文件描述符就绪。所以用户程序检测就绪文件的时间复杂度为O(n)
5、poll每次调用时,也是需要将用户空间的数组传递拷贝给内核,poll返回时又将内核的拷贝到用户空间
6、poll内核也是采用轮询的方式
7、poll也只能工作在LT模式
epoll
epoll的原型
epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、 poll 有很大差异。
首先, epoll 使用一组函数来完成任务,而不是单个函数。其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 该文件描述符由epoll_create创建
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_DEL
- fd: 指定要操作的文件描述符
- event:指定事件,它是 epoll_event 结构指针类型, epoll_event 的定义如下:
其中, events 成员描述事件类型, epoll 支持的事件类型与 poll 基本相同,表示 epoll 事件的宏是在 poll 对应的宏前加上‘E’ ,比如 epoll 的数据可读事件是 EPOLLIN。但是 epoll 有两个额外的事件类型—EPOLLET 和 EPOLLONESHOT。 data 成员用于存储用户数据,是一个联合体,其定义如下:struct epoll_event { uint32_t events; //事件的集合 epoll_data_t data;//用户数据,data.fd 文件描述符 };
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);
}
客户端代码同poll客户端
执行结果
epoll总结
1、epoll不再是一个接口,而是一组接口
2、能够监听的文件描述符个数没有限制,值的范围也没有限制
3、关注的事件类型更多,比poll多了EPOLLET和EPOLLONESHOT
4、epoll在内核中维护用户关注的事件类型,每次epoll_wait时不需要传递用户空间的数据。epoll_wait返回时,仅仅返回就绪的文件描述符和事件,相比于select和poll效率更高
5、epoll_wait返回的仅仅是就绪的事件,所以用户程序检测就绪文件描述符的时间复杂度就为O(1)
6、epoll内核采用的是回调的方式检测事件就绪
7、epoll不仅支持LT模式,也支持高效的ET模式
epoll的LT与ET模式
- LT模式:Level Trigger,电平触发,当epoll_wait将就绪事件返回后,如果用户程序没有处理该就绪事件,下一次epoll_wait还会通知这个就绪事件,LT是默认的工作模式
- ET模式:Edge Trigger,边沿触发,当epoll_wait将就绪事件返回后,用户没有处理该就绪事件,则下一次epoll_wait不会通知该就绪事件。即同一个就绪事件只会通知一次,相比于LT模式,效率更高
EPOLLONESHOT事件
即使使用高效的ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中是线程不安全的,可能会出现两个线程同时操作一个socket的局面。如果希望一个socket连接的任一时刻都只被一个线程处理,可以使用 EPOLLONESHOT 事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
注意:注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket
三组I/O复用函数的比较
这3组系统调用都能同时监听多个文件描述符。它们将等待由timeout参数指定的超时时间,直到一个或多个文件描述符上有事件发生时返回,返回值就是就绪的文件描述符的数量。返回0表示没有事件发生,出错返回-1
select、poll、epoll区别:
select | poll | epoll | |
---|---|---|---|
事件集合 | 用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数 | 统一处理所有事件类型,因此只需要一个事件集参数,用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件 |
应用程序索引就绪文件的时间复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符个数 | 一般有最大值限制 | 65535(系统允许打开的最大文件描述符数目) | 65535 |
工作模式 | LT | LT | LT与ET |
内核实现和工作效率 | 采用轮询方式来检测就绪事件,算法时间复杂度为O(n) | 采用轮询方式来检测就绪事件,算法时间复杂度为O(n) | 采用回调方式来检测就绪事件,算法时间复杂度为O(1) |
参考文献
[1]游双. Linux高性能服务器编程. 机械工业出版社,2013.5