关于selece,poll和epoll模型的总结
首先select,poll,epoll都是IO多路复用的机制,先是监听多个文件的描述符fd,一旦某个fd就绪后,就可以进行相应的读写操作。select,poll,epoll从本质上讲都是同步I/O,都需要在读写时间就绪之后自己负责读写,这个过程时阻塞的。
1.select模型
select用于io复用,用于监视多个文件描述符的集合,判断是否有符合条件的事件发生
1.1原理
使用select函数可以先对需要操作的文件描述符进行查询,查看是否目标文件描述符可以进行读写或者错误操作,然后当文件描述符满足操作的条件时才进行真正的io操作
1.2函数原型
int select(int nfds,//最大文件描述符+1
fd_set *readfds,//监控的所有读文件描述符集合
fd_set *writefds,//写集合
fd_set *exceptfds,//异常集合
struct timeval *timeout);//超时时间
其返回值:> 0 正常,== 0 超时,== -1 发生错误
从某个文件描述符的集合中取出某个文件描述符
void FD_CLR(int fd, fd_set *set);
测试某个文件描述符是否在某个集合中
int FD_ISSET(int fd, fd_set *set);
向某个文件描述符集合中加入文件描述符
void FD_SET(int fd, fd_set *set);
清理文件描述符集合
void FD_ZERO(fd_set *set);
注意:文件描述符的集合存在最大的,其最大值为FD_SETSIZE=1024
1.3使用方法
用fd_set(一个数组)来保存要监视的fd,用FD_SET来往里添加想要监视的fd,轮询(死循环)调用select,当监视的信息发生变化了,select解除阻塞状态走到下面,再遍历所有fd,用FD_ISSET确定是想要的fd发生了变化(比如当i=serv_sock时我就调accept去创建用于传数据的套接字,如果不等于就说明是有要接收的数据,直接read就行)。注意:如果新建了一个套接字,需要也用FD_SET把他加入fd_set作为监视的fd。
例如用select来实现echo服务器
客户端和服务端的功能如下:
客户端从标准输入读入一行,发送到服务端
服务端从网络读取一行,然后输出到客户端
客户端收到服务端的响应,输出这一行到标准输出echo服务器和客户端
server.c 服务端
#include <unistd.h>
#include <sys/types.h> /* basic system data types */
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <sys/select.h> /* select function*/
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#define MAXLINE 10240
void handle(int * clientSockFds, int maxFds, fd_set* pRset, fd_set* pAllset);
int main(int argc, char **argv)
{
//端口号
int servPort = 6888;
//listenq指定的其实就是accept队列也就是ESTABLISHED状态的连接
int listenq = 1024;
int listenfd, connfd;
struct sockaddr_in cliaddr, servaddr;
socklen_t socklen = sizeof(struct sockaddr_in);
int nready, nread;
char buf[MAXLINE];
//存放和客户端通信的socket描述符
int clientSockFds[FD_SETSIZE];
//描述符集合
fd_set allset, rset;
//最大的描述符
int maxfd;
//创建socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket error");
return -1;
}
int opt = 1;
//监听的端口变成可以复用的
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("setsockopt error");
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(servPort);
//绑定
if (bind(listenfd, (struct sockaddr*)&servaddr, socklen) == -1) {
perror("bind error");
exit(-1);
}
//监听
if (listen(listenfd, listenq) < 0) {
perror("listen error");
return -1;
}
//初始化数组
int i = 0;
for (i = 0; i< FD_SETSIZE; i++)
{
clientSockFds[i] = -1;
}
//清空描述符集合
FD_ZERO(&allset);
//将描述符加入集合
FD_SET(listenfd, &allset);
maxfd = listenfd;
printf("echo server use select startup, listen on port %d\n", servPort);
printf("max connection: %d\n", FD_SETSIZE);
for (;;) {
rset = allset;
//等待事件发生
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
perror("select error");
continue;
}
//处理客户端的连接
if (FD_ISSET(listenfd, &rset)) {
connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &socklen);
if (connfd < 0) {
perror("accept error");
continue;
}
sprintf(buf, "accept form %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);
printf(buf, "");
//将客户端连接的描述符加入数组
for (i = 0; i< FD_SETSIZE; i++) {
if (clientSockFds[i] == -1) {
clientSockFds[i] = connfd;
break;
}
}
if (i == FD_SETSIZE) {
fprintf(stderr, "too many connection, more than %d\n", FD_SETSIZE);
close(connfd);
continue;
}
if (connfd > maxfd)
maxfd = connfd;
//将描述符加入集合
FD_SET(connfd, &allset);
if (--nready <= 0)
continue;
}
//处理客户端的数据收发
handle(clientSockFds, maxfd, &rset, &allset);
}
}
void handle(int * clientSockFds, int maxFds, fd_set* pRset, fd_set* pAllset)
{
int nread;
int i;
char buf[MAXLINE];
for (i = 0; i< maxFds; i++) {
if (clientSockFds[i] != -1) {
if (FD_ISSET(clientSockFds[i], pRset)) {
nread = read(clientSockFds[i], buf, MAXLINE);//读取客户端socket流
if (nread < 0) {
perror("read error");
close(clientSockFds[i]);
FD_CLR(clientSockFds[i], pAllset);
clientSockFds[i] = -1;
continue;
}
if (nread == 0) {
printf("client close the connection\n");
close(clientSockFds[i]);
FD_CLR(clientSockFds[i], pAllset);
clientSockFds[i] = -1;
continue;
}
write(clientSockFds[i], buf, nread);//响应客户端有可能失败暂不处理
}
}
}
}
client.c 客户端
#include <unistd.h>
#include <sys/types.h> /* basic system data types */
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <sys/select.h> /* select function*/
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#define MAXLINE 10240
#define max(a,b) ((a) > (b) ? (a) : (b))
//typedef struct sockaddr SA;
void handle(int sockfd);
int main(int argc, char **argv)
{
char * servInetAddr = "127.0.0.1";
int servPort = 6888;
char buf[MAXLINE];
int connfd;
struct sockaddr_in servaddr;
if (argc == 2) {
servInetAddr = argv[1];
}
if (argc == 3) {
servInetAddr = argv[1];
servPort = atoi(argv[2]);
}
if (argc > 3) {
printf("usage: selectechoclient <IPaddress> <Port>\n");
return -1;
}
connfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(servPort);
inet_pton(AF_INET, servInetAddr, &servaddr.sin_addr);
if (connect(connfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
return -1;
}
printf("welcome to selectechoclient\n");
handle(connfd); /* do it all */
close(connfd);
printf("exit\n");
exit(0);
}
void handle(int connfd)
{
FILE* fp = stdin;
char sendline[MAXLINE], recvline[MAXLINE];
fd_set rset;
FD_ZERO(&rset);
int maxfds = max(fileno(fp), connfd) + 1;
int nread;
for (;;) {
FD_SET(fileno(fp), &rset);
FD_SET(connfd, &rset);
if (select(maxfds, &rset, NULL, NULL, NULL) == -1) {
perror("select error");
continue;
}
if (FD_ISSET(connfd, &rset)) {
//接收到服务器响应
nread = read(connfd, recvline, MAXLINE);
if (nread == 0) {
printf("server close the connection\n");
break;
}
else if (nread == -1) {
perror("read error");
break;
}
else {
//server response
write(STDOUT_FILENO, recvline, nread);
}
}
if (FD_ISSET(fileno(fp), &rset)) {
//标准输入可读
if (fgets(sendline, MAXLINE, fp) == NULL) {
//eof exit
break;
}
else {
write(connfd, sendline, strlen(sendline));
}
}
}
}
2.poll模型
检测文件描述符上,是否有某些事件发生。poll本质上和select没有区别,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构
2.1 函数原型
需要包含头文件#include<poll.h>
*int poll(struct pollfd fds,unsigned int nfds,int timeout);
参数:
fds:是一个poll函数监听的struct pollfd结构类型的数组
pollfd结构体定义如下:
struct pollfd{
int fd;//文件描述符(socket描述符)
short events;//等待的事件
short revents;//实际发生了的事件
};
events和revents的取值是一样的,常用的事件:
POLLIN 有数据可读
POLLOUT 写数据不会导致阻塞
POLLMSGSIGPOLL 消息可用
POLLERR 指定的文件描述符发生错误
nfds :是fds的元素个数
timeout :表示poll函数的超时时间,单位是毫秒
注意:
timeout == 0 代表立即返回
timeout > 0 代表等待指定的毫秒数后,返回
timeout < 0 代表永不过期,就是阻塞
函数返回值同select
2.2使用步骤
a:pollfd的数组 fdlist;
b:初始化fdlist;
c:将监听fdlist中,并设置要监测的事件(POLLIN)
d:调用poll函数去监测
e:遍历fdlist集合,处理客户端连接或收发数据
例如一个简单的poll模型
程序将会等待3秒去监测是否有读事件发生,若满足触发条件就会在屏幕上输出所打字节,若3s内无读事件发生会显示超时
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<poll.h>
#include<strings.h>
int main()
{
struct pollfd poll_fd;
//设置要检测的描述符,0是标准输入
poll_fd.fd = 0;
//设置检测的事件,POLLIN是读事件
poll_fd.events = POLLIN;
while(1)
{
//3000代表等待3000毫秒(3s),等待检测的最大时间值
int ret = poll(&poll_fd,1,3000);
switch(ret)
{
case 0://超时
printf("poll timeout\n");
break;
case -1://错误
printf("poll error\n");
break;
default:
{
//事件判断
if(poll_fd.revents == POLLIN)
{
char buffer[1024];
bzero(buffer,sizeof(buffer));
int nread = read(poll_fd.fd,buffer,sizeof(buffer));
if(nread > 0)
{
printf("stdin input %s\n",buffer);
}
}
}
}
}
return 0;
}
3.epoll模型
为实现高并发,在2.6内核中提出了epoll,是select和poll的增强版本,没有描述符。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件放到内核的一个事件表中,如此用户空间和内核空间的copy只需一次
3.1相关函数
epoll函数基于select和poll接口比较简单,一共3个函数。
a:创建一个epoll实例
int epoll_create(int size);
创建一个epoll的句柄,size是用来告诉内核所要监听的数目有多大
要注意的是,在创建好句柄后,其会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
b:注册socket对应的事件
**int epoll_ctl(int epfd,int op,int fd,struct epoll_event event);
epfd:epoll上下文的描述符,就是epoll_create的返回值;
op:操作命令
EPOLL_CTL_ADD 向epoll监听集合中添加socket描述符
EPOLL_CTL_DEL 从epoll监听集合中删除socket描述符
EPOLL_CTL_MOD 修改
fd :socket描述符,对TCP来说就是accept函数的返回值;
event :在向epoll监听集合当中添加socket描述符的同时,为这个描述符绑定一个触发事件。
可以是以下宏的集合:
1》EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
2》 EPOLLOUT: 表示对应的文件描述符可以写;
3》EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
4》EPOLLERR: 表示对应的文件描述符发生错误;
5》EPOLLHUP: 表示对应的文件描述符被挂断;
6》EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
7》EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
c:等待epfd_代表的epoll实例中监听的事件发生,events指针返回已经准备好的事件,最多有maxevents个,参数maxevents必须大于0
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);//返回值是需要处理的事件数目
epfd epoll上下文描述符
events 用于保存监听集合当中所有的触发事件
maxevents 最大的监听数
timeout 监听等待超时(ms)
设置timeout_为-1则epoll_wait()会一直阻塞,设置为0则会立即返回。
3.2关于TCP服务器的思路
1》创建服务器的socket
2》创建epoll实例
3》使用epoll_ctl设置
4》监听服务器socket是否有新的客户端建立连接(即是否可读)
while(true)
{
epoll_wait();
for 每个事件
{
if(服务器socket可读)
{
accept()
把新的客户端socket加入epoll监听事件
}
else
{
read()
send()
}
}
}
此刻发送数据的时候,调用了send函数,若用epoll_wait等待socket可写了后再发数据,就不会造成阻塞了
客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void Usage()
{
printf("usage: ./client [ip] [port]\n"); //输入ip地址和端口
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage();
return 1;
}
//服务器socket地址结构
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
//连接服务器
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("connect");
return 1;
}
while(1){
printf("> ");
fflush(stdout);//刷新输出缓冲区,即将缓冲区内容输出到屏幕上
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
ssize_t write_size = write(fd, buf, strlen(buf));//写函数
if (write_size < 0) {
perror("write");
continue;
}
ssize_t read_size = read(fd, buf, sizeof(buf) - 1);//读函数
if (read_size < 0) {
perror("read");
continue;
}
if (read_size == 0) {
printf("server close\n");
break;
}
printf("server say: %s", buf);
}
close(fd);
return 0;
}
服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <string.h>
//处理客户端连接accept
void handleConnect(int fdListen,int epfd)
{
struct sockaddr_in addr;
socklen_t nLen = sizeof(addr);
bzero(&addr,sizeof(addr));
int fdClient = accept(fdListen,(struct sockaddr*)&addr,&nLen);
if(fdClient < 0)
{
perror("accept");
return;
}
//显示登录信息
printf("client %s:%d connected!\n",inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port));
struct epoll_event event;
event.data.fd = fdClient;
event.events = EPOLLIN;
//注册事件
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fdClient,&event);
if(ret < 0)
{
perror("epoll_ctl");
return;
}
return;
}
//处理客户端数据收发
void handleClientRW(int fdClient,int epfd)
{
char buffer[1024] = {0};
int nsize = read(fdClient,buffer,sizeof(buffer));
if(nsize < 0)
{//error
perror("read");
return;
}
if(nsize == 0)
{//对方close
close(fdClient);
//将事件删除
epoll_ctl(epfd,EPOLL_CTL_DEL,fdClient,NULL);
printf("client say good bye!\n");
return;
}
printf("client say:%s",buffer);
//将数据发送给客户端
write(fdClient,buffer,strlen(buffer));
}
int main()
{
int fdListen;
struct sockaddr_in srvaddr;
srvaddr.sin_family = AF_INET;
srvaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
srvaddr.sin_port = htons(6888);
//创建socket
fdListen = socket(PF_INET,SOCK_STREAM,0);
if(fdListen < 0)
{
perror("socket error:");
return -1;
}
//绑定
int nRet = bind(fdListen,(struct sockadd*)&srvaddr,sizeof(srvaddr));
if(nRet < 0)
{
perror("bind error:");
return -1;
}
//监听
int nListen = listen(fdListen,5);
if(nListen < 0)
{
perror("listen error:");
return -1;
}
printf("server listening at 6888 port!\n");
//创建epoll实例
int epfd = epoll_create(10);
if(epfd < 0)
{
close(fdListen);
perror("epoll_create:");
return -1;
}
//用于添加事件
struct epoll_event event;
event.data.fd = fdListen;
event.events = EPOLLIN;
//注册事件
int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,fdListen,&event);
if(ret < 0)
{
close(fdListen);
perror("epoll_ctl:");
return -1;
}//处理客户端数据收发
//存放检测到的事件
struct epoll_event events[10];
while(1)
{
//等待检测的事件发生,返回值是发生事件的数量
int nNums = epoll_wait(epfd,&events,10,-1);
if(nNums < 0)
{
perror("epoll_wait");
continue;
}
if(nNums == 0)
{
perror("epoll_wait timeout");
continue;
}
int i;
for(i=0;i<nNums;++i)
{
if(events[i].data.fd == fdListen)
{
//处理客户端连接accept
handleConnect(fdListen,epfd);
}
else
{
//处理客户端数据收发
handleClientRW(events[i].data.fd,epfd);
}
}
}
//关闭句柄
close(epfd);
return 0;
}
总结:基于以上内容小结三者的主要区别
1.较于select,poll和epoll没有socket的FD_SETSIZE(1024)个数的
2.不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现
3.内核中select和poll的实现采用轮询来处理,轮询的fd越多耗时越长
4.epoll的实现基于回调的,如果fd有期望的事件发生就通过回调函数将其加入到epoll的就绪队列中去,即epoll只关心活跃的fd(因此效率高)
5.内核/用户空间拷贝问题,当内核把fd消息通知给用户空间时,select和poll采用内存拷贝的方法,而epoll采用的是内存共享的方式
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- vipyiyao.com 版权所有 湘ICP备2023022495号-8
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务