你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

select poll epoll

2022/9/2 17:46:50

socket

本来要写关于socket的内容。后来想了想,socket是将传输层和IP层的协议(TCP/IP协议簇)进行封装,供上层应用程序更加方便的使用传输层和IP层提供的服务。
想要真正了解socket的原理,还得是先掌握TCP/IP四层模型。TCP/IP四层模型和socket的关系大致如下图,在传输层之上对传输层协议进行封装供应用层调用。


图片来自知乎https://zhuanlan.zhihu.com/p/497267988

socket使用流程

socket服务端,socket() -> bind() -> listen() -> accept() -> recv() -> send()
socket客户端,socket() -> connect() -> send() -> recv() -> close()
想要了解socket的底层,操作系统是如何收发数据的可以看这篇文章https://zhuanlan.zhihu.com/p/373060740

IO模型

当进程在读写数据(磁盘设备中的、网络中的)时,将产生系统调用,由用户态切换到内核态,需要内核将数据拷贝到内核的缓冲区,再将数据从内核缓冲区拷贝到进程的缓冲区,最后交给进程。
所以在进程read数据时,需要经过内核“获取”数据、将数据拷贝到进程空间的过程。所以产生了以下五种IO模型

阻塞I/O

也称为同步阻塞I/O模型
进程发起系统调用后阻塞,直到数据准备就绪,内核返回调用结果,结束进程阻塞,进程到内核缓冲区读取数据。

非阻塞I/O

也称为同步非阻塞I/O模型
进程发起系统调用后,如果数据没有准备好,内核返回error,进程获得error而不阻塞进程,系统再次发起系统调用,直至可以从内核缓冲区读取数据。

I/O多路复用

I/O多路复用是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免非阻塞I/O模型中进程轮询等待的问题。
坏处是,在中间添加了select过程。
好处是,可以通过select实现一个进程(线程)监听多个I/O事件。

信号驱动的I/O

进程发起系统调用后,内核立即返回,进程可以执行其他任务,当数据准备完成后,内核为进程产生一个信号,进程就可以到内核缓冲区读取数据。

异步I/O

在其他四种I/O模型中,当内核将数据准备好后需要进程去读取数据,读取数据的过程是阻塞的。
而异步I/O模型,在进程发起系统调用后,内核立即返回,进程可以执行其他任务,内核读取数据,并将数据拷贝给进程,然后产生信号,进程就可以直接使用数据,而不用阻塞读取读取数据。

select poll epoll

在Linux下I/O多路复用使用select poll epoll来实现。
select poll epoll本质上还是同步I/O,当数据准备就绪后,都需要自己去内核空间读取数(将数据从内核缓冲区拷贝到进程缓冲区)。

select

先来上硬菜,Linux sys/select.h头文件中select函数原型

int select (int __nfds, fd_set *__restrict __readfds,    // fd_set 使用bitmap实现
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout);

__nfds,表示监听的文件描述符中最大的文件描述符+1
__readfds,表示有可读数据到达的文件描述符集合
__writefds,表示有可写数据到达的文件描述符集合
__exceptfds,表示发生异常的文件描述符集合
__timeout,表示超时时间

存在的问题
(1) 每次调用select都需要把监听的文件描述符集合从用户空间拷贝到内核空间。
(2) select监听的文件描述符有上限。默认在32位机上为1024个,在64位机上是2048个。
(3) select只能监听文件描述符上是否有事件发生,也就是说当监听的文件描述符集合上有事件发生时,需要程序遍历文件描述符集合。

poll

poll在select的基础上针对select的问题做了优化

Linux sys/poll.h头文件中poll函数原型

struct pollfd
  {
    int fd;			/* File descriptor to poll.  */
    short int events;		/* Types of events poller cares about.  */
    short int revents;		/* Types of events that actually occurred.  */
  };

int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

__fds,文件描述符数组首地址
__nfds,文件描述符数量
__timeout,超时时间

poll将文件描述符的类型由bitmap换成了pollfd*(数组),解决了监听文件描述符上限的问题。
但是,还是需要在每次监听时,将所有文件描述符从用户空间拷贝到内核空间。

epoll

epoll主要有以下三个函数
创建epoll

int epoll_create (int __size) __THROW

__size,表示初始化epoll的大小,并不限制epoll的大小。在linux 2.6.8以后该参数就可以被忽略了,只要填写大于0的数就可以

epoll_ctl函数,事件注册

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

int epoll_ctl (int __epfd, int __op, int __fd,
		      struct epoll_event *__event) __THROW;
__epfd,epoll文件描述符
__op,表示操作,EPOLL_CTL_ADD,添加新的文件描述符;EPOLL_CTL_MOD,修改文件描述符;EPOLL_CTL_DEL,删除文件描述符
__fd,操作的文件描述符
__event,文件描述符的事件

epoll_wait函数,等待事件产生

int epoll_wait (int __epfd, struct epoll_event *__events,
		       int __maxevents, int __timeout);
__epfd,epoll文件描述符
__events,有事件发生的文件描述符数组
__maxevents,文件描述符的数量
__timeout,超时时间

(1) epoll使用红黑树存储文件描述符
(2) epoll只有在第一次时才将文件描述符从用户空间拷贝到内核空间
(3) epoll_wait时,只返回有事件发生的文件描述符

epoll实例

附上一份epoll实例代码

#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <arpa/inet.h>
#include <string.h>
#include <sys/epoll.h>
// #include <poll.h>
#include <errno.h>

using namespace std;

int main(int argc, char *argv[]) {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) { return -1; }

    int old_flag = fcntl(listen_fd, F_GETFL, 0);
    int new_flag = old_flag | O_NONBLOCK;
    if (fcntl(listen_fd, F_SETFL, new_flag) == -1) {
        close(listen_fd);
        std::cout << "fcntl fail" << std::endl;
        return -1;
    }
    
    struct sockaddr_in listen_addr;
    bzero(&listen_addr, sizeof(listen_addr));
    listen_addr.sin_family = AF_INET;
    listen_addr.sin_port = htons(8080);
    listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&listen_addr, sizeof(listen_addr)) == -1) {
        std::cout << "bind fail" << std::endl;
        close(listen_fd);
        return -1;
    }

    if (listen(listen_fd, SOMAXCONN) == -1) {
        std::cout << "listen fail" << std::endl;
        close(listen_fd);
        return -1;
    }

    int on = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on));
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, (void *)&on, sizeof(on));

    int epoll_fd = epoll_create(1);
    if (epoll_fd == -1) {
        close(listen_fd);
        std::cout << "epoll_create fail" std::endl;
        return -1;
    }

    struct epoll_event listen_fd_event;
    listen_fd_event.events = EPOLLIN;
    listen_fd_event.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1) {
        close(listen_fd);
        std::cout << "epoll_ctl fail" std::endl;
        return -1;
    }

    while (1) {
        struct epoll_event epoll_fd_event[1024];
        int epoll_fd_event_len = epoll_wait(epoll_fd, epoll_fd_event, 1024, 1000);
        if (epoll_fd_event_len < 0) {
            // close(listen_fd);
            if (errno == EINTR) {
                continue;
            } else {
                std::cout << "epoll_wait fail" << std::endl;
                break;
            }
            // return -1;
        } else {
            for (size_t i = 0; i < epoll_fd_event_len; ++i) {
                if (epoll_fd_event[i].events & EPOLLIN) {
                    if (epoll_fd_event[i].data.fd == listen_fd) {
                        // 新连接
                        struct sockaddr client_addr;
                        socklen_t client_addr_len = sizeof(client_addr);
                        int client_fd = accept(listen_fd, &client_addr, &client_addr_len);
                        if (client_addr_len != -1) {
                            int old_flag = fcntl(client_fd, F_GETFL, 0);
                            int new_flag = old_flag | O_NONBLOCK;
                            if (fcntl(client_fd, F_SETFL, new_flag) == -1) {
                                close(client_fd);
                                std::cout << "fcntl fail" std::endl;
                            } else {
                                struct epoll_event client_fd_event;
                                client_fd_event.data.fd = client_fd;
                                client_fd_event.events = EPOLLIN;
                                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1) {
                                    close(client_fd);
                                    std::cout << "epoll_ctl fail" std::endl;
                                } else {
                                    std::cout << "new client" << client_fd << std::endl;
                                }
                            }
                        }
                    } else {
                        // 读数据
                        char buf[64] = { 0 };
                        int len = recv(epoll_fd_event[i].data.fd, buf, 64, 0);
                        if (len > 0) {
                            std::cout << epoll_fd_event[i].data.fd << ": " << buf << std::endl;
                        } else if (len == 0) {
                            if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_fd_event[i].data.fd, 0) != -1) {
                                std::cout << "client disconnect: " << epoll_fd_event[i].data.fd << std::endl;
                            }
                            close(epoll_fd_event[i].data.fd);
                        } else {
                            if (errno != EWOULDBLOCK && errno != EINTR) {
                                if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_fd_event[i].data.fd, 0) != -1) {
                                    std::cout << "client disconnect: " << epoll_fd_event[i].data.fd << std::endl;
                                }
                                close(epoll_fd_event[i].data.fd);
                            }
                        }
                    }
                }
            }

        }

    }
    
    // 清空epoll_fd

    close(listen_fd);
    return 0;
}