1.概述
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。I/O复用模型会用到select、poll、epoll函数:对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性。但关键是能实现同时对多个IO端口进行监听。这几个函数也会使进程阻塞,但是和阻塞I/O所不同的是,这几个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
2.select
2.1select原理
当一个客户端连接上服务器时,服务器就将其连接的fd加入fd_set集合,等到这个连接准备好读或写的时候,就通知程序进行IO操作,与客户端进行数据通信。大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件描述符的状态变化。
2.2select函数
int select(
int maxfdp, //Winsock中此参数无意义
fd_set* readfds, //进行可读检测的Socket
fd_set* writefds, //进行可写检测的Socket
fd_set* exceptfds, //进行异常检测的Socket
const struct timeval* timeout //非阻塞模式中设置最大等待时间
)
2.3 使用select的步骤
- 创建所关注的事件的描述符集合(fd_set),对于一个描述符,可以关注其上面的读(read)、写(write)、异常(exception)事件,所以通常,要创建三个fd_set,一个用来收集关注读事件的描述符,一个用来收集关注写事件的描述符,另外一个用来收集关注异常事件的描述符集合。
- 调用select()等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。
- 轮询所有fd_set中的每一个fd,检查是否有相应的事件发生,如果有,就进行处理
2.4 select的优缺点
相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/select.h>
#include <ctype.h>
int main(){
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_port = htons(8888);
serv.sin_family = AF_INET;
serv.sin_addr.s_addr = htonl(INADDR_ANY);
//reset port
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
bind(lfd,(struct sockaddr*)&serv,sizeof(serv));
listen(lfd,128);
fd_set rdset;//readevent
fd_set allset;//bak readevent
FD_ZERO(&rdset);
FD_SET(lfd,&rdset);
allset = rdset;
struct sockaddr_in client;
socklen_t len = sizeof(client);
int nfds = lfd;
int nready = 0;
while(1){
rdset = allset;//备份传给内核
//阻塞等待事件就绪
nready = select(nfds+1,&rdset,NULL,NULL,NULL);
if(FD_ISSET(lfd,&rdset)){
//有新连接事件,将得到的CFD加入到集合
int cfd = accept(lfd,(struct sockaddr*)&client,&len);
if(cfd > 0){
//rdset是一个传入传出集合,每次都会重置
FD_SET(cfd,&allset);
}
if(nfds < cfd){
nfds = cfd;
}
nready --;
//如果就绪事件就一个,且是新连接就跳出循环
if(nready <= 0)
continue;
}
int i = 0;
for(i = lfd+1;i<nfds +1;i++){
if(FD_ISSET(i,&rdset)){
char buf[256] = {0};
int ret = read(i,buf,sizeof(buf));
if(ret < 0){
perror("read err");
close(i);
FD_CLR(i,&allset);
}
else if (ret == 0){
close(i);//client closed
FD_CLR(i,&allset);
}
else{
int j = 0;
for(;j<ret;j++){
buf[j] = toupper(buf[j]);//转换为大写字母
}
write(i,buf,ret);
}
if(--nready <= 0)
break;//no event.jump for.
}
}
}
close(lfd);
return 0;
}
够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
select的缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大!!!(复制大量句柄数据结构,产生巨大的开销 )。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大!!!(消耗大量时间去轮询各个句柄,才能发现哪些句柄发生了事件)。
- 单个进程能够监视的文件描述符的数量存在最大限制,32位机默认是1024。
- select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
- 该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
2.5select程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/select.h>
#include <ctype.h>
int main(){
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv;
bzero(&serv,sizeof(serv));
serv.sin_port = htons(8888);
serv.sin_family = AF_INET;
serv.sin_addr.s_addr = htonl(INADDR_ANY);
//reset port
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
bind(lfd,(struct sockaddr*)&serv,sizeof(serv));
listen(lfd,128);
fd_set rdset;//readevent
fd_set allset;//bak readevent
FD_ZERO(&rdset);
FD_SET(lfd,&rdset);
allset = rdset;
struct sockaddr_in client;
socklen_t len = sizeof(client);
int nfds = lfd;
int nready = 0;
while(1){
rdset = allset;//备份传给内核
//阻塞等待事件就绪
nready = select(nfds+1,&rdset,NULL,NULL,NULL);
if(FD_ISSET(lfd,&rdset)){
//有新连接事件,将得到的CFD加入到集合
int cfd = accept(lfd,(struct sockaddr*)&client,&len);
if(cfd > 0){
//rdset是一个传入传出集合,每次都会重置
FD_SET(cfd,&allset);
}
if(nfds < cfd){
nfds = cfd;
}
nready --;
//如果就绪事件就一个,且是新连接就跳出循环
if(nready <= 0)
continue;
}
int i = 0;
for(i = lfd+1;i<nfds +1;i++){
if(FD_ISSET(i,&rdset)){
char buf[256] = {0};
int ret = read(i,buf,sizeof(buf));
if(ret < 0){
perror("read err");
close(i);
FD_CLR(i,&allset);
}
else if (ret == 0){
close(i);//client closed
FD_CLR(i,&allset);
}
else{
int j = 0;
for(;j<ret;j++){
buf[j] = toupper(buf[j]);//转换为大写字母
}
write(i,buf,ret);
}
if(--nready <= 0)
break;//no event.jump for.
}
}
}
close(lfd);
return 0;
}
3.poll
3.1介绍
poll库是在linux2.1.23中引入的,windows平台不支持poll。poll本质上和select没有太大区别,都是先创建一个关注事件的描述符的集合,然后再去等待这些事件发生,然后再轮询描述符集合,检查有没有事件发生,如果有,就进行处理。因此,poll有着与select相似的处理流程:
1)创建描述符集合,设置关注的事件
2)调用poll(),等待事件发生。下面是poll的原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
类似select,poll也可以设置等待时间,效果与select一样。
3)轮询描述符集合,检查事件,处理事件。
3.2poll与select的主要区别
select需要为读、写、异常事件分别创建一个描述符集合,最后轮询的时候,需要分别轮询这三个集合。而poll只需要一个集合,在每个描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,可以同时检查三种事件。它没有最大连接数的限制,原因是它是基于链表来存储的。
3.3缺点
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
3.4程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <poll.h>
#include <arpa/inet.h>
#define _MAXLINE_ 80
#define _SERVER_PORT_ 8888
#define _MAX_OPEN 1024
int main(){
int i,maxi;
char strIP[16];
int lfd = socket(AF_INET,SOCK_STREAM,0);
struct pollfd client[_MAX_OPEN];
struct sockaddr_in clientaddr,servaddr;
int len = sizeof(clientaddr);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(_SERVER_PORT_);
//set reuse port
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(bind(lfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0){
perror("bind err");
return -1;
}
listen(lfd ,128);
client[0].fd = lfd;//监听第一个文件描述符
client[0].events = POLLIN;//监听读事件
for(i = 1; i <_MAX_OPEN;i++){
client[i].fd = -1;//用-1初始化,因为0也是描述符
}
maxi = 0;//记录client数组有效最大元素下标
while(1){
int nready = poll(client,maxi+1,-1);
//判断是否有新连接
if(client[0].revents & POLLIN){
//此处不会阻塞
int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len);
printf("recv form %s:%d\n",
inet_ntop(AF_INET,&clientaddr.sin_addr,strIP,sizeof(strIP)),
ntohs(clientaddr.sin_port));
for(i = 1;i<_MAX_OPEN;i++){
if(client[i].fd < 0){
client[i].fd = cfd;
break;
}
}
if(i == _MAX_OPEN){
//最大客户连接上限
printf("max connected...\n");
continue;
}
client[i].events = POLLIN;
if (i > maxi)
maxi = i;
if(--nready <= 0)
continue;//没有更多就绪事件,继续回到POLL阻塞
}
for(i = 1;i<=maxi;i++){
//前面的IF没有满足,说明没有新连接,而是读事件
int cfd;
//先找到第一个大于0的文件描述符
if((cfd = client[i].fd) < 0)
continue;
if(client[i].revents & POLLIN){
char buf[_MAXLINE_] = {0};
int ret = read(cfd,buf,sizeof(buf));
if(ret < 0){
if(errno == ECONNRESET){
printf("client[%d] aborted connection\n",i);
close(cfd);
client[i].fd =-1;
//POLL中不需要像SELECT一样移除,直接置-1即可
}
else{
perror("read error");
exit(-1);
}
}
else if(ret == 0){
printf("client[%d] closed\n",i);
close(cfd);
client[i].fd = -1;
}
else{
write(cfd,buf,ret);
}
if(--nready <= 0)
break;
}
}
}
close(lfd);
return 0;
}
4、epoll
4.1 epoll概述
poll和select,它们的最大的问题就在于效率。它们的处理方式都是创建一个事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表,这样在描述符比较多的应用中,效率就显得比较低下了。epoll是一种比较好的做法,它把描述符列表交给内核,一旦有事件发生,内核把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表。
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll与select和poll的调用接口上的不同:select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
4.2 epoll的使用步骤
- 创建一个epoll描述符,调用epoll_create()来完成。epoll_create()有一个整型的参数size,用来告诉内核,要创建一个有size个描述符的事件列表(集合)。
int epoll_create(int size)
- 给描述符设置所关注的事件,并把它添加到内核的事件列表中。这里需要调用epoll_ctl()来完成。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
- 等待内核通知事件发生,得到发生事件的描述符的结构列表。该过程由epoll_wait()完成。得到事件列表后,就可以进行事件处理了。
- int epoll_create(int size)
4.3 epoll的LT和ET的区别
水平触发和边缘触发的区别:只要句柄满足某种状态,水平触发就会发出通知;而只有当句柄状态改变时,边缘触发才会发出通知。
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
4.4 epoll的优点
- 没有最大并发连接的限制,能打开FD的上限远大于1024(1G的内存上能监听约10万个端口);
- 效率提升。不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
- 内存拷贝。epoll通过内核和用户空间共享一块内存来实现消息传递的。利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap 减少复制开销。epoll保证了每个fd在整个过程中只会拷贝一次(select,poll每次调用都要把fd集合从用户态往内核态拷贝一次)。
4.5epoll程序
nclude <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <errno.h>
#define MAX_EVENTS 10
#define PORT 8080
//设置socket连接为非阻塞模式
void setnonblocking(int sockfd) {
int opts;
opts = fcntl(sockfd, F_GETFL);
if(opts < 0) {
perror("fcntl(F_GETFL)\n");
exit(1);
}
opts = (opts | O_NONBLOCK);
if(fcntl(sockfd, F_SETFL, opts) < 0) {
perror("fcntl(F_SETFL)\n");
exit(1);
}
}
int main(){
struct epoll_event ev, events[MAX_EVENTS];
int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
struct sockaddr_in local, remote;
char buf[BUFSIZ];
//创建listen socket
if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("sockfd\n");
exit(1);
}
setnonblocking(listenfd);
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);;
local.sin_port = htons(PORT);
if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0)
{
perror("bind\n");
exit(1);
}
listen(listenfd, 20);//设置为监听描述符
epfd = epoll_create(MAX_EVENTS);
if (epfd == -1)
{
perror("epoll_create");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
{
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;)
{
nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);//超时时间-1,永久阻塞直到有事件发生
if (nfds == -1)
{
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (i = 0; i < nfds; ++i)
{
fd = events[i].data.fd;
if (fd == listenfd) //如果是监听的listenfd,那就是连接来了,保存来的所有连接
{
//每次处理一个连接,while循环直到处理完所有的连接
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0)
{
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET;//边沿触发非阻塞模式
ev.data.fd = conn_sock;
//把连接socket加入监听结构体
if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: add");
exit(EXIT_FAILURE);
}
}
//已经处理完所有的连:accept返回-1,errno为EAGAIN
//出错:返回-1,errno另有其值
if (conn_sock == -1)
{
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
continue;//继续循环,但是不执行该循环后面的部分
}
if (events[i].events & EPOLLIN) //可读事件
{
n = 0;
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror("read error");
}
ev.data.fd = fd;
ev.events = events[i].events | EPOLLOUT;
//修改该fd监听事件类型,监测是否可写
if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1)
{
perror("epoll_ctl: mod");
}
}
if (events[i].events & EPOLLOUT) //可写事件
{
sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
int nwrite, data_size = strlen(buf);
n = data_size;
while (n > 0)
{
nwrite = write(fd, buf + data_size - n, n);
if (nwrite < n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror("write error");
}
break;
}
n -= nwrite;
}
//写完就关闭该连接socket
close(fd);
}
}
}
return 0;
}