IO多路复用(select poll epoll)

IO多路复用是高性能网络编程一个重要的手段。

一,IO多路复用的概念
以前我们用多线程来处理并发的请求,现在可以只用单线程来实现。单线程,通过记录跟踪每个每个I/O流(sock)的状态,来达到同时管理多个I/O流的目的,提高了服务器的吞吐能力。

IO多路复用(select poll epoll)_第1张图片
如图所示,IO多路复用,就如同中间的开关,哪个sock就绪就连上开关,达到了单开关处理了多个I/O流的目的。这就是单线程却能处理多个Sock传输数据的IO多路复用模型。(当Sock请求很多时,可以进行分组,一个线程控制一个组,把多线程与IO复用结合使用)

之前的博客介绍了多线程的方式,两者的区别在于
1. 多线程模型适合于处理短连接,且连接的打开关闭非常频繁的情况,但不适合处理长连接。
2. 多线程毕竟是要耗费资源的,IO多路复用基本不耗费资源,也不必创建,维护线程,使得系统的开销大大减小,效率变得更快。(效率与单线程比较,不一定比多线程快)

I/O复用典型使用在下列网络应用场合:
1. 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
2. 如果一个TCP服务器既要处理TCP,又要处理UDP,一般就要用I/O复用。
3. 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。

二,I/O多路复用的实现
在I/O多路复用这个概念被提出以后,select()是第一个实现的(1983年)。

select()原理:

1、使用copy_from_user从用户空间拷贝fd_set到内核空间
2、注册回调函数__pollwait
3、遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
4、以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
5、__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
6、poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
7、如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
8、把fd_set从内核空间拷贝到用户空间

如下图流程所示:
IO多路复用(select poll epoll)_第2张图片
通俗点说,select将所有fd放入一个集合中,不停轮询遍历集合,并将未就绪的fd踢出集合,系统可以通过集合来调动就绪fd,只要fd就绪就会被放入集合处理,可以等价于select管理了所有的fd!

当然select细节还有很多,后面提供代码分析。先来总结select的不足之处:
1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
3. select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,只能自己一个一个的找,数据大时不方便。
4. select不是线程安全的,不同线程不可对同一sock操作。
5. select支持的文件描述符数量太小了,默认是1024。

select代码如下:

#include"../unp.h"
#include

typedef struct server_context_st
{
    int cli_cnt; 
    int clifds[SIZE];
    fd_set allfds;
    int maxfd;
}server_context_st;

static server_context_st *s_srv_ctx = NULL;

int server_init()
{
    s_srv_ctx = (server_context_st*)malloc(sizeof(server_context_st));
    if(s_srv_ctx == NULL)
        return -1;
    memset(s_srv_ctx, 0, sizeof(server_context_st));
    for(int i=0; iclifds[i] = -1;
    }
    return 0;
}
void server_uninit()
{
    if(s_srv_ctx)
    {
        free(s_srv_ctx);
        s_srv_ctx = NULL;
    }
}

int create_server_proc(const char *ip, short port)
{
    int fd;
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }
    struct sockaddr_in addrSer;
    addrSer.sin_family = AF_INET;
    addrSer.sin_port = htons(port);
    addrSer.sin_addr.s_addr = inet_addr(ip);

    socklen_t addrlen = sizeof(struct sockaddr);
    int res = bind(fd, (struct sockaddr*)&addrSer, addrlen);
    if(res == -1)
    {
        perror("bind");
        return -1;
    }
    listen(fd, LISTENQ);
    return fd;
}

int accept_client_proc(int srvfd)
{
    struct sockaddr_in addrCli;
    socklen_t addrlen = sizeof(struct sockaddr);

    int clifd;
ACCEPT:
    clifd = accept(srvfd, (struct sockaddr*)&addrCli, &addrlen);
    if(clifd == -1)
    {
        goto ACCEPT;
    }
    printf("accept a new client: %s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);

    int i;
    for(i=0; iif(s_srv_ctx->clifds[i] == -1)
        {
            s_srv_ctx->clifds[i] = clifd;
            s_srv_ctx->cli_cnt++;
            break;
        }
    }
    if(i == SIZE)
    {
        printf("Server Over Load.\n");
        return -1;
    }
}

void handle_client_msg(int fd, char *buf)
{
    printf("recv buf is:> %s\n",buf);
    send(fd, buf, strlen(buf)+1, 0);
}

void recv_client_msg(fd_set *readfds)
{
    int clifd;
    char buffer[256];
    int n;
    for(int i=0; icli_cnt; ++i)
    {
        clifd = s_srv_ctx->clifds[i];
        if(clifd < 0)
            continue;
        if(FD_ISSET(clifd, readfds))
        {
           n = recv(clifd, buffer, 256, 0);
           if(n <= 0)
           {
               FD_CLR(clifd, &s_srv_ctx->allfds);
               close(clifd);
               s_srv_ctx->clifds[i] = -1;
               s_srv_ctx->cli_cnt--;
               continue;
           }

           handle_client_msg(clifd, buffer);

        }
    }
}

int handle_client_proc(int srvfd)
{
    int clifd = -1;
    int retval = 0;
    fd_set *readfds = &s_srv_ctx->allfds;
    struct timeval tv;

    while(1)
    {
        FD_ZERO(readfds);
        FD_SET(srvfd, readfds);
        s_srv_ctx->maxfd = srvfd;
        tv.tv_sec = 30;
        tv.tv_usec = 0;

        int i;
        for(i=0; icli_cnt; ++i)
        {
            clifd = s_srv_ctx->clifds[i];
            FD_SET(clifd, readfds);
            s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
        }

        retval = select(s_srv_ctx->maxfd+1, readfds, NULL, NULL, &tv);
        if(retval == -1)
        {
            perror("select");
            return -1;
        }
        if(retval == 0)
        {
            printf("server time out.\n");
            continue;
        }

        //accept
        if(FD_ISSET(srvfd, readfds))
        {
            accept_client_proc(srvfd);
        }
        else
        {
            recv_client_msg(readfds);
        }
    }
}

int main(int argc, char *argv[])
{
    int sockSer;
    if(server_init() < 0)
        perror("server_init");
    sockSer = create_server_proc(IPADDR, PORT);
    if(sockSer < 0)
    {
        perror("create_server_porc");
        goto err;
    }
    handle_client_proc(sockSer);
    return 0;
err:
    server_uninit();
    return -1;

针对select的不足,1997年提出了poll实现IO复用的模型。

poll模型实现:
和select差别不大,最主要的区别在于:
1. **集合方式不同**select将可操作的sock存入FD_SETSIZE集合中,内核默认32*32=1024。而poll是将对应fd列表由数组保存,大小没有限制。所以poll将可操作的sock数量提升至无限大。
2. **结构不同**select将fd放入集合中后,每次轮询将未就绪的fd删除,有新的就绪的再加入。而poll处理是将每个fd对应的状态更改,就绪为1,未就绪为0,这样就不用每次轮询删除加入fd了。

poll其他与select类似,select的问题除了限制大小poll都存在,比如轮询效率低下,不是线程安全的,没有详细的sock信息。代码如下:

#include"../unp.h"
#include

int sock_bind(const char *ip, short port)
{
    int fd;
    fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addrSer;
    addrSer.sin_family = AF_INET;
    addrSer.sin_port = htons(port);
    addrSer.sin_addr.s_addr = inet_addr(ip);

    socklen_t addrlen = sizeof(struct sockaddr);
    bind(fd, (struct sockaddr*)&addrSer, addrlen);
    return fd;
}

void handle_connection(struct pollfd *connfds, int num)
{
    int n;
    char buf[256];
    for(int i=1; i<= num; ++i)
    {
        if(connfds[i].fd  == -1)
            continue;
        if(connfds[i].revents & POLLIN)
        {
            n = recv(connfds[i].fd, buf, 256, 0);
            if(n <= 0)
            {
                close(connfds[i].fd);
                connfds[i].fd = -1;
                continue;
            }
            printf("recv msg:>%s\n",buf);
            send(connfds[i].fd, buf, n, 0);
        }
    }
}

void do_poll(int sockSer)
{
    pollfd clientfds[OPEN_SIZE];
    clientfds[0].fd = sockSer;
    clientfds[0].events = POLLIN;

    for(int i=1; i1;

    int maxi = 0;
    int nready;
    struct sockaddr_in addrCli;
    socklen_t addrlen = sizeof(struct sockaddr);
    int i;
    for(;;)
    {
        nready = poll(clientfds, maxi+1,-1);
        if(nready == -1)
        {
            perror("poll");
            exit(1);
        }
        if(clientfds[0].revents & POLLIN)
        {
            int sockConn = accept(sockSer, (struct sockaddr*)&addrCli, &addrlen);
            if(sockConn == -1)
            {
                perror("accept");
                continue;
            }
            printf("accept a new client:%s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);

            for(i=1; iif(clientfds[i].fd < 0)
                {
                    clientfds[i].fd = sockConn;
                    break;
                }
            }
            if(i == OPEN_SIZE)
            {
                printf("Server Over Load.\n");
                continue;
            }
            clientfds[i].events = POLLIN;
            maxi = (i > maxi ? i : maxi);
            if(--nready <= 0)
                continue;
        }
        handle_connection(clientfds, maxi);
    }
}

int main()
{
    int sockSer;
    sockSer = sock_bind(IPADDR, PORT);
    listen(sockSer, LISTENQ);
    do_poll(sockSer);
    return 0;
}

终于,2002年实现了epoll模式!
epoll可以说是I/O多路复用最新的实现,epoll修复了poll和select绝大部分问题,比如:
epoll 是线程安全的。
epoll 对socket的数量无限制。
epoll 现在不仅告诉sock组里面数据,还会告诉具体哪个sock有数据。
epoll 不是效率低下的轮询模式,是触发模式。

epoll原理是:
epoll不同于select和poll的轮询,只有注册新的fd到epoll句柄中时,才会把所有新的fd拷贝进内核,epoll保证每个fd在整个过程中只被拷贝一次。并且为每个fd指定一个回调函数,当fd就绪时,回调函数就会将就绪的fd加入一个就绪链表中。epoll_wait的工作就是阻塞查看就绪链表中有没有就绪的fd。而如果没有就绪fd,就会把进程加入一个等待队列中。直到有fd就绪,就会调用回调函数,将就绪的fd放入就绪列表中,并唤醒epoll_wail继续等待,直到又有新的fd就绪。。。
可见,这种方式比轮询的效率高出很多,又节约了CPU时间资源。

select,poll,epoll的效率测试图:

代码实现如下:
IO多路复用(select poll epoll)_第3张图片
横轴Dead connections 就是链接数,纵轴是每秒处理请求的数量。
可以看到,epoll每秒处理请求的数量基本不会随着链接变多而下降的,而select和poll当链接数量很多时,效率就低的很多了。

epoll代码实现如下:

#include"../unp.h"
#include"utili.h"
#include

int sock_bind(const char *ip, short port)
{
    int fd;
    fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addrSer;
    addrSer.sin_family = AF_INET;
    addrSer.sin_port = htons(port);
    addrSer.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(struct sockaddr);
    bind(fd, (struct sockaddr*)&addrSer, addrlen);
    return fd;
}

void handle_accept(int epollfd, int listenfd)
{
    struct sockaddr_in addrCli;
    int sockConn;
    socklen_t addrlen = sizeof(struct sockaddr);
    sockConn = accept(listenfd, (struct sockaddr*)&addrCli, &addrlen);
    if(sockConn == -1)
        perror("accept");
    else
    {
        printf("accept a new client:%s:%d\n",inet_ntoa(addrCli.sin_addr),addrCli.sin_port);
        add_event(epollfd, sockConn, EPOLLIN);
    }
}

void do_read(int epollfd, int fd, char *buf)
{
    int nread = read(fd, buf, 256);
    if(nread <= 0)
    {
        printf("Server is Closed.\n");
        close(fd);
        delete_event(epollfd, fd, EPOLLIN);
    }
    printf("recv msg:>%s\n",buf);
    modify_event(epollfd, fd, EPOLLOUT);
}

void do_write(int epollfd, int fd, char *buf)
{
    int nwrite = write(fd, buf, strlen(buf)+1);
    if(nwrite <= 0)
    {
        printf("client is closed.\n");
        close(fd);
        delete_event(epollfd, fd, EPOLLOUT);
    }
    else
        modify_event(epollfd, fd, EPOLLIN);
}

void handle_events(int epollfd, epoll_event *events, int num, int listenfd, char *buf)
{
    int fd;
    for(int i=0; iif((fd==listenfd) && (events[i].events & EPOLLIN))
            handle_accept(epollfd, listenfd);
        else if(events[i].events & EPOLLIN)
            do_read(epollfd, fd, buf);
        else if(events[i].events & EPOLLOUT)
            do_write(epollfd, fd, buf);
    }
}

void do_epoll(int listenfd)
{
    int epollfd;
    epoll_event events[1024];
    epollfd = epoll_create(FDSIZE);
    add_event(epollfd,listenfd, EPOLLIN);
    int res;
    char buf[256];
    for(;;)
    {
        res = epoll_wait(epollfd, events, 1024,-1);
        if(res == -1)
        {
            perror("epoll_wait");
            exit(1);
        }
        handle_events(epollfd, events, res, listenfd, buf);
    }
    close(epollfd);
}

int main()
{
    int listenfd;
    listenfd = sock_bind(IPADDR, PORT);
    listen(listenfd, LISTENQ);
    do_epoll(listenfd);
    return 0;
}

你可能感兴趣的:(网络编程)