IO多路复用

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的步骤

 

  1. 创建所关注的事件的描述符集合(fd_set),对于一个描述符,可以关注其上面的读(read)、写(write)、异常(exception)事件,所以通常,要创建三个fd_set,一个用来收集关注读事件的描述符,一个用来收集关注写事件的描述符,另外一个用来收集关注异常事件的描述符集合。
  2. 调用select()等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。
  3. 轮询所有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的缺点:

 

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大!!!(复制大量句柄数据结构,产生巨大的开销 )。
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大!!!(消耗大量时间去轮询各个句柄,才能发现哪些句柄发生了事件)。
  3. 单个进程能够监视的文件描述符的数量存在最大限制,32位机默认是1024。
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
  5. 该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

 

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缺点

 

  1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 
  2. 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的优点

  1. 没有最大并发连接的限制,能打开FD的上限远大于1024(1G的内存上能监听约10万个端口);
  2. 效率提升。不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
  3. 内存拷贝。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;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值