【C/C++】io 并发:select/poll/epoll 编程入门
本文描述了 select/poll/epoll 三种 io 编程方式,并且阐述了三者的关系
select
- nfds:值最大的 fd,初始化时置为 sockfd + 1,后续根据连接客户端变化而变化
- readfds:fd 集合指针(检查可读)
- writefds:fd 集合指针(检查可写)
- exceptfds:fd 集合指针(检查错误)
- timeout:表示阻塞多长时间,为 NULL 表示一直阻塞
类型定义
fd_set:一共16个long int,即 16 * 8 * 8 个bit位,一共可以表示1024个 fd
方法说明
- FD_SET、FD_CLR分别从fd集合(即之前定义的fd_set变量)中设置或移除一个文件描述符,在程序表现上即相应位置置1
- FD_ZERO用于初始化fd集合
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main(void) {
//监听
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2000);
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
printf("bind error %d\n", sockfd);
return -1;
}
listen(sockfd, 5);
printf("listening...\n");
//连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(struct sockaddr);
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
while (1) {
rset = rfds;
int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) {
printf("accepting...\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("client %d accepted\n", clientfd);
FD_SET(clientfd, &rfds);
if (maxfd < clientfd) {
maxfd = clientfd;
}
}
for (int i = sockfd + 1; i <= maxfd; i++) {
if (FD_ISSET(i, &rset)) {
char buffer[1024] = {0};
int count = recv(i, buffer, sizeof(buffer), 0);
if (count == 0) {
close(i);
FD_CLR(i, &rfds);
printf("client %d closed\n", i);
continue;
}
printf("RECV %s\n", buffer);
send(i, buffer, count, 0);
printf("SEND %d\n", count);
}
}
}
return 0;
}
poll
类型定义
pollfd:
从select的fd集合演变为了在拥有fd的同时给它添加了两个属性,分别是events和revents,这样做的好处是减少了函数所需要的参数
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>
int main(void) {
//初始化
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2000);
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
printf("bind error %d\n", sockfd);
return -1;
}
listen(sockfd, 5);
printf("listening...\n");
//处理
struct sockaddr_in clientaddr;
socklen_t len = sizeof(struct sockaddr);
struct pollfd fds[1024];
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;
while (1) {
int nready = poll(fds, maxfd + 1, -1);
if (fds[sockfd].revents & POLLIN) {
printf("accepting...\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("client %d accepted\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (maxfd < clientfd) {
maxfd = clientfd;
}
}
for (int i = sockfd + 1; i <= maxfd; i++) {
if (fds[i].revents & POLLIN) {
char buffer[1024] = {0};
int count = recv(i, buffer, sizeof(buffer), 0);
if (count == 0) {
fds[i].fd = -1;
fds[i].events = -1;
close(i);
printf("client %d closed\n", i);
continue;
}
printf("RECV %s\n", buffer);
send(i, buffer, count, 0);
printf("SEND %d\n", count);
}
}
}
return 0;
}
select 和 poll 使我们能够在一个线程内解决多个 clientfd 的问题,但是又出现了另一个问题:这两个函数在检查 io 就绪或者发送请求的时候都需要遍历 fd 集合,但实际上在业务开发中,有很多个客户端连接并不代表着有很多的io请求,比如有100万个客户端连接,往往只有1%左右的请求在同时发生,那么我们是否也需要同时遍历这100万个io呢?
epoll
epoll_create构建了一个机制,即将所有的 io 组织在一起,又将就绪组织在一起,然后聘请一个对象统一管理起来,epoll_ctl负责添加、修改、删除 io 连接,epoll_wait 则负责”聘请对象“多久管理一次
epoll 相比较 select、poll 的优势
- epoll 利用 epoll_ctl,在 io 数增加时,与select、poll里面的突然增加不同,而是一个一个积累,而出现 io 事件时,处理就绪就可以了,很好地解决了大并发的情况
- 在很大数目的 io (例如 100w 个 io)时,让我们更好地去处理就绪的事件
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/epoll.h>
int main(void) {
//初始化
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2000);
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1) {
printf("bind error %d\n", sockfd);
return -1;
}
listen(sockfd, 5);
printf("listening...\n");
//连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(struct sockaddr);
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];
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) {
printf("accepting...\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("client %d accepted\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) {
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd);
printf("client %d closed\n", connfd);
continue;
}
printf("RECV %s\n", buffer);
send(connfd, buffer, count, 0);
printf("SEND %d\n", count);
}
}
}
return 0;
}
总结
- select 和 poll 的底层原理相同,都是将 fd 所属的结构体拷贝到内核中,遍历其中 io 是否就绪,select 通过参数控制是可读、可写还是错误,而 poll 则是将这些转换为了 fd 的属性,即通过 pollfd 结构体中的 events 属性来判断,这样减少了 poll 函数所需的参数,但两者的性能都类似
- epoll 的出现让我们对 io 的管理从之前的 fd 集合的管理转向了对事件的管理。