IO多路复用

IO多路复用

select系统调用

维护的是一个文件描述符(fd)集合(set),监测这些fd集合。

#include <sys/select.h> // 头文件

运行机制

fedset复制到内核空间,然后对其进行遍历,查看可读,可写,错误事件,返回就绪事件总数。

select函数

select函数调用时需要五个参数,包括文件描述符集合总数nfds(最大是1024个),底层fdset是数组实现的,fdset是比特位的集合。可读集合read_fds,可写集合write_fds,异常集合excep_fds,超时时间timeout。返回值是所有就绪事件的数量和。

fd_set fds, read_fds, write_fds, excep_fds; // fds是应用层,而其它三个都是内核层要进行操作的。
struct timeval timeout;
FD_ZERO(fds); // 首先所有标志位置0
FD_SET(listenfd, fds); // 将代表listenfd的进行置1
int maxfd = listenfd;
struct time_val timeout; // 如果为NULL,设定一直阻塞等待
timeout.tv_sec = 5; // 等待5s, 5s事件就继续执行
timeout.tv_usec = 0;
struct sockaddr_in client;
bezero(&client, sizeof(client));
socklen_t len = sizeof(client);
while(1){
    read_fds = fds;
	int nready = select(nfds, &read_fds, &write_fds, &excep_fds, &timeout); // 首先拿到这个,接下来就是处理这些集合,select会修改可读可写异常标志位
    if(FD_ISSET(listenfd, &read_fds)) {  // 考虑是否listenfds可读
        int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
        FD_SET(clientfd, fds); // 设置标志位,fds
        if(clientfd > maxfd) maxfd = clientfd; // 这一步问题是解决回收连接之后的问题。
    }
    // 循环对fd集合进行读
    for(int i = listenfd + 1; i < maxfd+1; ++i){
        char buffer[1024] = {0};
        int n = recv(i , buffer, 1024, 0);
        if(n == 0){
            close(i);
            FD_CLR(i, &fds);
            break;
        }
    }
}

注意

  1. fd阻塞和select阻塞不同,select只是分辨fd会不会阻塞于读和写就行。
  2. select一般限制最多fd大小为1024,数组实现,由一个宏定义进行限制

poll系统调用

结构体

struct pollfd{
    int fd;
    short int events; // 期望得事件
    short int revents; // 返回的就绪事件
}

运行机制

select调用的机制类似

poll函数

poll系统调用维护三个参数,pollfd集合,nfdsfd集合数量(已有的fd数量),timeout

// 一个重要的参数是数组,每个数组中保存一个pollfd元素
    struct pollfd pfds[1024] = {0};
    memset(&pfds, -1, sizeof(pfds)); // 0是存在的fd
    pfds[sockfd].fd = sockfd;
    pfds[sockfd].events = POLLIN; // 期望可读
    int maxfd = sockfd;
    while(1){
        int nready = poll(pfds, maxfd+1, -1); // timeout =-1 一直等待事件发生
        if(pfds[sockfd].revents & POLLIN){
            int clientfd = accept(sockfd, (struct sockaddr*) &client, &len);
            printf("accept finished: %d\n", clientfd);
            pfds[clientfd].fd = clientfd;
            pfds[clientfd].events = POLLIN;
            if(clientfd > maxfd) maxfd = clientfd; 
        }
        for(int i = sockfd+1;  i  < maxfd+1; ++i){
            char buffer[1024] = {0};
            if(pfds[i].revents & POLLIN){
                int n = recv(i, buffer, sizeof(buffer), 0);
                
                if(n == 0){
                    close(i);
                    pfds[i].fd = -1;
                    pfds[i].events = 0;
                    continue;
                }
                printf("recv: %s\n", buffer);
                n  = send(i, buffer, strlen(buffer), 0);
                printf("send: %d bytes!\n", n);
            }
        }   
    }

注意

  1. poll的限制是fd的最大数量,linux一般为1024

    ulimits -n # 查看fd的最大数量
    
  2. 特点:IO与事件是绑定的,读写不分离

epoll系统调用

运行机制

  1. epoll通过epoll_create通知内核可能会有多大的内核事件表(以前事件表的数据结构是数组,后面事件表的数据结构是链表,所有size对于现在的epoll_create是无效的)。
  2. 使用epoll_ctl将事件表复制到内核,控制管理添加、删除内核事件表上的事件。
  3. 使用epoll_wait监控内核事件表上的事件,如果有就绪事件就将其复制到返回集合中。epoll内核事件表每个结点都是一个不同的fd, 这个结点中维护了每个fd的事件数。存疑是怎么组织的fd集合事件表

结构事件

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

epoll_create函数

一个参数size,这里的size对于现版本是没有意义的,现版本是链表实现的。返回值是一个epollfd,生成一个epollfd管理者

epoll_ctl函数

四个参数,epollfd管理者,操作模式添加、删除等,客户fd,客户带的事件。

epoll_wait函数

四个参数,epollfd管理者, 返回就绪事件表,返回就绪事件表的大小,超时时间(控制阻塞也是控制等待就绪事件的时间)

示例

struct epoll_event epl_ent;
    epl_ent.events = EPOLLIN;
    epl_ent.data.fd = sockfd;  // 先将listenfd传上来
    // epoll_create的作用是创建一个内核事件表,用专门的一个fd去管理
    int epollfd = epoll_create(1); // 此时MAX_EVENTS不起作用,只是告诉内核有这么大
    printf("epollfd: %d\n", epollfd);
    // 将listenfd添加上去, 这里只是将事件表复制到内核
    ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd,&epl_ent);
    if(ret == -1){
        printf("epoll_ctl failure!\n");
    }
while(1){ // accept
        struct epoll_event epl_rent[MAX_EVENTS]={0};
        
        int nready = epoll_wait(epollfd, epl_rent, MAX_EVENTS, -1);
        for(int i = 0; i < nready; ++i){
            int connfd = epl_rent[i].data.fd;
            // 如果是监听fd上有事件发生
            if(connfd == sockfd){
                printf("listenfd:  %d\n", sockfd); // 输出3
                
                int clientfd = accept(connfd, (struct sockaddr*)&client, &len);
                printf("accept finished: %d\n", clientfd);
                epl_ent.data.fd = clientfd;
                epl_ent.events = EPOLLIN;
                epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &epl_ent);
            }
            // 事件也是标志位的集合
            else if(epl_rent[i].events & EPOLLIN){
                char buffer[1024] = {0};
                int n = recv(connfd, buffer, 1024, 0);
                if(n == 0){
                    // 删除这里可写可不写因为其,直接按照fd进行删除
                    ret = epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epl_ent);
                    if(ret == -1){
                        printf("epoll_ctl_del failure!\n");
                    }
                    close(connfd);
                    continue;
                }
                printf("recv: %s\n", buffer);
            }
        }
    }   

注意

  1. epoll维护的内核事件表是积累添加的,不需要像selectpoll一样循环进行复制到内核事件表。
  2. poll通过标志位标记可读等事件模式,select直接将可读、可写、异常事件分开(其中都是以数组实现,每一位代表一个fd)。
  3. selectpoll每次都需要将fd集合复制到内核
  4. selectpollepoll都是事件通知、事件触发机制
  5. linux2.4版本以前用select,没有pollepollpoll是通过pollfd进行组织的。结构体可以用链表去进行组织也可以顺序存储组织。
posted @ 2024-07-21 21:19  云中锦书来  阅读(1)  评论(0编辑  收藏  举报