IO复用函数的使用


实现IO复用的系统调用有select、poll和epoll,IO复用就是在一个程序中同时监听或处理多个文件描述符上有没有我们关心的事件产生,这样就能在一个程序中处理多个文件描述符,先检测是否有我们关心的事件再去处理它。而有多少个文件描述符就创建多少个线程,这个并不能同时监听多个文件描述符,而且开销大。有了IO复用方法,我们就可以在单个线程内监听多个文件描述符,有利于提高程序的性能,使我们的服务器在不引用多进程和多线程的情况下同时有多个链接。

在这里插入图片描述

一、select

select可以在只有一个进程,以及在一个进程下只有一个线程的情况下,可以同时和多个客户端进行通讯。
只支持读、写和异常事件
内核实现:轮询方式,时间复杂度O(n)
在这里插入图片描述

在这里插入图片描述
select.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<time.h>

#define MAX 10
int socket_init();
void fds_init(int fds[])
{
    if(fds==NULL)
    {
        return;
    }
    for(int i=0;i<MAX;++i)
    {
        fds[i]=-1;
    }
}
void fds_add(int fd,int fds[])
{
    if(fds==NULL)
    {
        return;
    }
    for(int i=0;i<MAX;++i)
    {
        if(fds[i]==-1)
        {
            fds[i]=fd;
            break;
        } 
    }
}
void fds_del(int fd,int fds[])
{
    if(fds==NULL)
    {
        return;
    }
    for(int i=0;i<MAX;++i)
    {
        if(fds[i]==fd)
        {
            fds[i]=-1;
            break;
        }
    }
}
int main()
{
    int sockfd=socket_init();
    assert(sockfd!=-1);

    int fds[MAX];//存放可能有事件发生的描述符,比如:stdin 0,stdout 1,stderr 2
    fds_init(fds);//初始化数组值都为-1,代表数组是空的
    fds_add(sockfd,fds);//将监听到套接字添加到数组

    fd_set fdset;//集合,收集描述符,被select检测
    while(1)
    {
        FD_ZERO(&fdset);//清空集合
        int maxfd=-1;//记录描述符的最大值,使select只需要检测位数组fdset的前maxfd+1位
        for(int i=0;i<MAX;++i)//遍历fds,把可能发生事件的文件描述符放入fds
        {
            if(fds[i]==-1)
            {
                continue;
            }
            FD_SET(fds[i],&fdset);//把有效的描述符添加到集合
            if(maxfd<fds[i])
            {
                maxfd=fds[i];
            }
        }
        struct timeval tv={5,0};
        int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//只关注前maxfd+1位,下标是从0开始的,所以加1
        if(n==-1)//出错了
        {
            continue;
        }
        else if(n==0)//超时了
        {
            printf("time out\n");
            continue;
        }
        else
        {
            for(int i=0;i<MAX;++i)//遍历所有描述符找到就绪的
            {
                if(fds[i]==-1)
                {
                    continue;
                }
                //服务器可能有监听事件和接收数据的事件
                if(FD_ISSET(fds[i],&fdset))//描述符是否被添加到集合
                {
                    if(fds[i]==sockfd)
                    {
                        struct sockaddr_in caddr;
                        int len=sizeof(caddr);
                        int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
                        if(c<0)
                        {
                            continue;
                        }
                        printf("accept c=%d\n",c);
                        fds_add(c,fds);
                    }
                    else
                    {
                        char buff[128]={0};
                        int num=recv(fds[i],buff,127,0);
                        if(num<=0)
                        {
                            close(fds[i]);
                            fds_del(fds[i],fds);//数组删除不用的描述符
                            printf("client close\n");
                            continue;//当前描述符关闭,再检测下一个描述符
                        }
                        else
                        {
                        	printf("read:%s\n",buff);
                        	send(fds[i],"ok",2,0);
                        }  
                    }
                }
            }
        }
    }
}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        return -1;
    }
    res=listen(sockfd,5);
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

cli.c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
  int sockfd=socket(AF_INET,SOCK_STREAM,0);
  assert(sockfd!=-1);
  //bind可绑定,但一般不绑定

  struct sockaddr_in saddr;
  memset(&saddr,0,sizeof(saddr));
  saddr.sin_family=AF_INET;
  saddr.sin_port=htons(6000);
  saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

  int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//三次握手,建立连接
  assert(res!=-1);

  while(1)
  {
    char buff[128]={0};
    printf("input:\n");
    fgets(buff,128,stdin);
    if(strncmp(buff,"end",3)==0)
    {
      break;
    }
    send(sockfd,buff,strlen(buff),0);
    memset(buff,0,sizeof(buff));
    recv(sockfd,buff,127,0);
    printf("buff=%s\n",buff);
  }
  close(sockfd);//四次挥手

}

如果recv接收长度改为1,也是可以全部读完的,第二轮会继续处理,不用担心丢数据

二、poll

select最多只支持1024个,只有三种事件类型,而poll更多描述符和更多事件类型。
内核实现:轮询方式

1. poll接口

 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • poll 系统调用成功返回就绪文件描述符的总数,超时返回 0,失败返回-1
  • nfds 参数指定被监听事件集合 fds 的大小。
  • timeout 参数指定 poll 的超时值,单位是毫秒,timeout 为-1 时,poll 调用将永久阻塞,直到某个事件发生,timeout 为 0 时,poll 调用将立即返回。

fds 参数是一个 struct pollfd 结构类型的数组,它指定所有用户感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd 结构体定义如下:

struct pollfd
{
	int fd; // 文件描述符
	short events; // 注册的关注事件类型
	short revents; // 实际发生的事件类型,由内核填充
};

其中,fd 成员指定文件描述符,events 成员告诉 poll 监听 fd 上的哪些事件类型。它是一系列事件的按位或,revents 成员则有内核修改,通知应用程序 fd 上实际发生了哪些事件。

2. poll支持的事件类型

在这里插入图片描述

3. 示例代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<time.h>
#include<poll.h>

#define MAX 10
int socket_init();
void fds_init(struct pollfd fds[])
{
    int i=0;
    for(;i<MAX;++i)
    {
        fds[i].fd=-1;
        fds[i].events=0;
        fds[i].revents=0;
    }
}

//添加描述符及事件方法
void fds_add(struct pollfd fds[],int fd)
{
    int i=0;
    for(;i<MAX;++i)
    {
        if(fds[i].fd==-1)
        {
            fds[i].fd=fd;
            fds[i].events=POLLIN;//r
            fds[i].revents=0;
            break;
        }
    }
}

//移除描述符
void fds_del(struct pollfd fds[],int fd)
{
    int i=0;
    for(;i<MAX;++i)
    {
        if(fds[i].fd==fd)
        {
            fds[i].fd=-1;
            fds[i].events=0;
            fds[i].revents=0;
        }
    }
}

int main()
{
    int sockfd=socket_init();
    assert(sockfd!=-1);

    struct pollfd fds[MAX];
    fds_init(fds);
    
    fds_add(fds,sockfd);

    while(1)
    {
        int n=poll(fds,MAX,5000);//阻塞
        if(n==-1)
        {
            continue;
        }
        else if(n==0)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            int i=0;
            for(;i<MAX;i++)
            {
                if(fds[i].fd==-1)
                {
                    continue;
                }
                if(fds[i].events&POLLIN)
                {
                    if(fds[i].fd==sockfd)//accept
                    {
                        struct sockaddr_in caddr;
                        int len=sizeof(caddr);
                        int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
                        if(c<0)
                        {
                            continue;
                        }
                        printf("accept c=%d\n",c);
                        fds_add(fds,c);
                    }
                    else//recv
                    {
                        char buff[128]={0};
                        int num=recv(fds[i].fd,buff,127,0);
                        if(num<=0)
                        {
                            close(fds[i].fd);
                            fds_del(fds,fds[i].fd);
                            printf("client over\n");
                            continue;
                        }
                        printf("buff(%d)=%s\n",fds[i].fd,buff);
                        send(fds[i].fd,"ok",2,0);
                    }
                }
            }
        }
    }
}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        return -1;
    }
    res=listen(sockfd,5);
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

三、epoll

epoll是linux平台特有的IO复用方法,适合描述符特别多的情况。它在实现和使用上与 select、poll 有很大差异。首先,epoll 使用一组函数来完成任务,而不是单个函数。其次,epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

采用回调方式检测就绪事件,O(1)

解决了select和poll的痛点:

  1. select和poll每轮循环都需要把描述符和事件从用户空间传到内核空间,描述符特别多的话开销就很大
  2. 内核实现select和poll是采用轮询方式,时间复杂度O(n)
  3. 即便select和poll在内核中检测到了描述符上有数据就绪,但它并不会告诉应用程序哪些是就绪的只告诉应用程序有几个,应用程序还需要再遍历一遍描述符,才能找到哪个上面有数据,时间复杂度O(n)

1. epoll的接口

#include<sys/epoll.h>
  • epoll_create()用于创建内核事件表
int epoll_create(int size);

epoll_create()成功返回内核事件表的文件描述符,失败返回-1
size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。

  • epoll_ctl()用于操作内核事件表
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl()成功返回 0,失败返回-1
epfd 参数指定要操作的内核事件表的文件描述符
fd 参数指定要操作的文件描述符
op 参数指定操作类型:

	EPOLL_CTL_ADD 往内核事件表中注册 fd 上的事件
	EPOLL_CTL_MOD 修改 fd 上的注册事件
	EPOLL_CTL_DEL 删除 fd 上的注册事件

event 参数指定事件,它是 epoll_event 结构指针类型,epoll_event 的定义如下:

struct epoll_event
{
	_uint32_t events; // epoll 事件
	epoll_data_t data; // 用户数据
}

其中,events 成员描述事件类型,epoll 支持的事件类型与 poll 基本相同,表示epoll 事件的宏是在 poll 对应的宏前加上‘E’,比如 epoll 的数据可读事件是EPOLLIN。但是 epoll 有两个额外的事件类型–EPOLLET 和 EPOLLONESHOT。data 成员用于存储用户数据,是一个联合体,其定义如下:

typedef union epoll_data
{
	 void *ptr;
	 int fd;
	 uint32_t u32;
	 uint64_t u64;
}epoll_data_t;

其中 fd 成员使用的最多,它指定事件所从属的目标文件描述符。

  • epoll_wait()用于在一段超时时间内等待一组文件描述符上的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfd 参数指定要操作的内核事件表的文件描述符
events 参数是一个用户数组,这个数组仅仅在 epoll_wait 返回时保存内核检测到的所有就绪事件,而不像 select 和 poll 的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。
maxevents 参数指定用户数组的大小,即指定最多监听多少个事件,它必须大于0
timeout 参数指定超时时间,单位为毫秒,如果 timeout 为 0,则 epoll_wait 会立即返回,如果 timeout 为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。
在这里插入图片描述

2. 代码实现

epoll.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<time.h>
#include<sys/epoll.h>

#define MAX 10

void epoll_add(int epfd,int fd)
{
    struct epoll_event ev;
    ev.events=EPOLLIN;//读事件
    ev.data.fd=fd;//描述符
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
    {
        perror("epoll ctl err\n");
    }
}

void epoll_del(int epfd,int fd)
{
    if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
    {
        perror("epoll ctl del err\n");
    }
}

int socket_init();

int main()
{
    int sockfd=socket_init();
    assert(sockfd!=-1);

    int epfd=epoll_create(MAX);//创建内核事件表---红黑树
    assert(epfd!=-1);

    epoll_add(epfd,sockfd);//将描述符添加到内核事件表中

    struct epoll_event evs[MAX];

    while(1)
    {
        int n=epoll_wait(epfd,evs,MAX,5000);//得到就绪文件描述符的个数
        if(n==-1)
        {
            perror("epoll wait err\n");
            continue;
        }
        else if(n==0)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            int i=0;
            for(;i<n;i++)
            {
                int fd=evs[i].data.fd;
                if(evs[i].events&EPOLLIN)
                {
                    if(fd==sockfd)//accept
                    {
                        struct sockaddr caddr;
                        int len=sizeof(caddr);
                        int c=accept(fd,(struct sockaddr*)&caddr,&len);
                        if(c<0)
                        {
                            continue;
                        }
                        printf("accept c=%d\n",c);
                        epoll_add(epfd,c);
                    }
                    else//recv
                    {
                        char buff[128]={0};
                        int num=recv(fd,buff,127,0);
                        if(num<=0)
                        {
                            epoll_del(epfd,fd);
                            close(fd);
                            printf("client over!\n");
                        }
                        else
                        {
                            printf("buff[%d]=%s\n",fd,buff);
                            send(fd,"ok",2,0);
                        }
                    }
                }
            }
        }
    }


}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        return -1;
    }
    res=listen(sockfd,5);
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}

3. epoll 一定比 select 高效吗?为什么?

四、LT和ET模式

epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge Trigger,边沿触发)模式。
select和poll只有LT模式。

  1. LT 模式是默认的工作模式。当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。对于 LT 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时,还会再次向应用程序通告此事件,直到该事件被处理。
  2. 对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知。应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因此效率比 LT 模式高。
int num=recv(fd,buff,1,0);

在这里插入图片描述
ET模式的epoll代码实现

  1. 文件描述符开启 ET模式
  2. recv设置为非阻塞,缓冲区没数据的时候返回-1,而不是阻塞
  3. 循环读取缓冲区的数据,直到读完
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<time.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.h>

#define MAX 10

void setnonblock(int fd)
{
    int oldfl=fcntl(fd,F_GETFL);
    int newfl=oldfl|O_NONBLOCK|EPOLLET;//开启ET模式

    if(fcntl(fd,F_SETFL,newfl)==-1)
    {
        perror("fcntl err\n");
    }
}

void epoll_add(int epfd,int fd)
{
    struct epoll_event ev;
    ev.events=EPOLLIN;//读事件
    ev.data.fd=fd;//描述符

    setnonblock(fd);//设置为非阻塞模式
    if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
    {
        perror("epoll ctl err\n");
    }
}

void epoll_del(int epfd,int fd)
{
    if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
    {
        perror("epoll ctl del err\n");
    }
}

int socket_init();

int main()
{
    int sockfd=socket_init();
    assert(sockfd!=-1);

    int epfd=epoll_create(MAX);//创建内核事件表
    assert(epfd!=-1);

    epoll_add(epfd,sockfd);//将描述符添加到内核事件表中

    struct epoll_event evs[MAX];

    while(1)
    {
        int n=epoll_wait(epfd,evs,MAX,5000);//得到就绪文件描述符的个数
        if(n==-1)
        {
            perror("epoll wait err\n");
            continue;
        }
        else if(n==0)
        {
            printf("time out\n");
            continue;
        }
        else
        {
            int i=0;
            for(;i<n;i++)
            {
                int fd=evs[i].data.fd;
                if(evs[i].events&EPOLLIN)
                {
                    if(fd==sockfd)//accept
                    {
                        struct sockaddr caddr;
                        int len=sizeof(caddr);
                        int c=accept(fd,(struct sockaddr*)&caddr,&len);
                        if(c<0)
                        {
                            continue;
                        }
                        printf("accept c=%d\n",c);
                        epoll_add(epfd,c);
                    }
                    else//recv
                    {
                        while(1)
                        {
                            char buff[128]={0};
                            int num=recv(fd,buff,1,0);
                            if(num==-1)
                            {
                                if(errno!=EAGAIN&&errno!=EWOULDBLOCK)
                                {
                                    perror("recv err\n");
                                }
                                else
                                {
                                    send(fd,"ok",2,0);
                                }
                                break;
                            }
                            else if(num==0)
                            {
                                epoll_del(epfd,fd);
                                printf("client over!\n");
                                break;
                            }
                            else
                            {
                                printf("rev:%s\n",buff);
                            }
                        }
                    }
                }
            }
        }
    }


}
int socket_init()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        return -1;
    }
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
    {
        return -1;
    }
    res=listen(sockfd,5);
    if(res==-1)
    {
        return -1;
    }
    return sockfd;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值