IO 多路复用(todo)

IO多路复用介绍

  • IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
  • 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
  • 没有文件句柄就绪就会阻塞应用程序,交出CPU。
  • IO多路复用的三种实现方式 select/poll/epoll

epoll

epoll的全称是eventpoll,它是基于event事件进行实现的,是linux特有的I/O复用函数。

  • 有点
    文件描述符没有限制(使用链表存储)
  • 三个核心函数
    epoll_create(建立一个epoll对象)
    epoll_ctl(epoll的事件注册函数)
    epoll_wait(等待事件的产生,类似于select()调用)

int epoll_create(int size)

在内核中创建epoll实例并返回一个epoll文件描述符。 在最初的实现中,调用者通过 size 参数告知内核需要监听的文件描述符数量。如果监听的文件描述符数量超过 size, 则内核会自动扩容。而现在 size 已经没有这种语义了,但是调用者调用时 size 依然必须大于 0,以保证后向兼容性。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

向 epfd 对应的内核epoll 实例添加、修改或删除对 fd 上事件 event 的监听。

  • epfd:epoll_create的返回值
  • op:表示动作
    EPOLL_CTL_ADD:添加新的事件到文件描述符。
    EPOLL_CTL_MOD: 修改文件描述符上的事件类型
    EPOLL_CTL_DEL: 删除一个事件

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件
当 timeout 为 0 时,epoll_wait 永远会立即返回。而 timeout 为 -1 时,epoll_wait 会一直阻塞直到任一已注册的事件变为就绪。当 timeout 为一正整数时,epoll 会阻塞直到计时 timeout 毫秒终了或已注册的事件变为就绪。因为内核调度延迟,阻塞的时间可能会略微超过 timeout 毫秒。

  • demo
#include
#include
#include
#include
#include
#define MAXLEN 1024
#define SERV_PORT 8000
#define MAX_OPEN_FD 1024

int main(int argc,char *argv[])
{
    int  listenfd,connfd,efd,ret;
    char buf[MAXLEN];
    struct sockaddr_in cliaddr,servaddr;
    socklen_t clilen = sizeof(cliaddr);
    struct epoll_event tep,ep[MAX_OPEN_FD];

    listenfd = socket(AF_INET,SOCK_STREAM,0);

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
    listen(listenfd,20);
    // 创建一个epoll fd
    efd = epoll_create(MAX_OPEN_FD);
    tep.events = EPOLLIN;tep.data.fd = listenfd;
    // 把监听socket 先添加到efd中
    ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
    // 循环等待
    for (;;)
    {
        // 返回已就绪的epoll_event,-1表示阻塞,没有就绪的epoll_event,将一直等待
        size_t nready = epoll_wait(efd,ep,MAX_OPEN_FD,-1);
        for (int i = 0; i < nready; ++i)
        {
            // 如果是新的连接,需要把新的socket添加到efd中
            if (ep[i].data.fd == listenfd )
            {
                connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
            }
            // 否则,读取数据
            else
            {
                connfd = ep[i].data.fd;
                int bytes = read(connfd,buf,MAXLEN);
                // 客户端关闭连接
                if (bytes == 0){
                    ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL);
                    close(connfd);
                    printf("client[%d] closed\n", i);
                }
                else
                {
                    for (int j = 0; j < bytes; ++j)
                    {
                        buf[j] = toupper(buf[j]);
                    }
                    // 向客户端发送数据
                    write(connfd,buf,bytes);
                }
            }
        }
    }
    return 0;
}
epoll_event
typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;        /* Epoll events */
  epoll_data_t data;        /* User data variable */
} __attribute__ ((__packed__));

原理

image.png

image.png

可以看到

  • epoll_create 创建了一个对象,这个对象包含一个event_poll线程池(红黑树)和rdlist就绪对列表(双向链表)
  • epoll_ctl 执行epoll_ctl()时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据
  • epoll_wait 执行epoll_wait()时立刻返回准备就绪链表里的数据即可。

当事件就绪后,就被加入到 rdlist(就绪链表)中。epoll_wait 检查是否有事件发生时,仅仅需要检查 rdlist 中是否有数据即可。

ep_poll_callback

ep_poll_callback函数核心功能是将被目标fd的就绪事件到来时,将fd对应的epitem实例添加到就绪队列。当应用调用epoll_wait()时,内核会将就绪队列中的事件报告给应用

参考

https://www.modb.pro/db/250807
https://www.cnblogs.com/tangxin-blog/p/5470791.html
https://sites.uclouvain.be/SystInfo/usr/include/sys/epoll.h.html
https://zhuanlan.zhihu.com/p/389407114

你可能感兴趣的:(IO 多路复用(todo))