当前位置:首页 > 学习资源 > 讲师博文 > Linux网络编程IO复用和模式

Linux网络编程IO复用和模式 时间:2023-10-12      来源:华清远见

IO多路复用概念

     IO多路复用是指通过一种机制,使得单个进程可以监控多个文件描述符的可读、可写和异常等事件。常见的IO多路复用技术包括:select、epoll等。在实际应用中,IO多路复用可以提高程序的运行效率和性能,减少系统开销,降低系统资源的使用率。它广泛应用于网络编程、服务器开发、操作系统等领域,可以帮助开发人员更好地处理大量的网络连接和数据请求。以下主要以select和epoll展开叙述。

 

IO多路复用之select

select实现原理

     IO多路复用select函数是一种实现并行IO的机制,可以同时处理多个socket连接,提高系统的性能和效率。在Linux中,select是一个阻塞函数,把需要管理的文件描述符添加到fd_set集合中,由select统一管理。如果所有的文件描述符都没有数据准备好,那么select会一直阻塞等待,如果有文件描述符的数据准备好,select函数解除阻塞,但是要通过轮循的方式把有响应的文件描述符找出来。处理完后,继续循环到select的位置监听。

 

select相关知识

   1、函数原型

   int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    

参数说明

nfds:需要监控的最大文件描述符加1。

readfds:读文件描述符集合。

writefds:写文件描述符集合。

exceptfds:异常文件描述符集合。

timeout:select函数的超时时间设置,如果设置为NULL,则一直等待直到事件发生

 

         返回值:

          >0 :  表示准备好的文件描述符个数

          =0: 超时解除阻塞

           -1:  函数出错

 

 

2、辅助函数 

void FD_CLR(int fd, fd_set *set);  //将fd从set集合中移除

int  FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中,如果在返回真,否则返回假

void FD_SET(int fd, fd_set *set); // 将fd加入set集合中

void FD_ZERO(fd_set *set);  //  清空set集合中的内容

 

select应用案例

   #include <stdio.h>

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <pthread.h>

 

//成功返回监听套接字, 失败返回NULL 

int sock_init()

{

    int sockfd;

    int ret;

 

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if(sockfd<0)

    {

    perror("socket");

    return -1;

    }

 

    //设置套接字端口复用

    int opt = 1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

 

    struct sockaddr_in  seraddr;

    int addrlen = sizeof(struct sockaddr_in);

    seraddr.sin_family = AF_INET;

    seraddr.sin_port = htons(8001);

    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);

    ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);

    if(ret<0)

    {

    perror("bind");

    return -1;

    }

 

 

    ret = listen(sockfd, 10);

    if(ret<0)

    {

    perror("bind");

    return -1;

    }

    return sockfd;

}

 

int main()

{

    char buff[1024];

    int sockfd;

    int ret;

    int cfd;

 

    sockfd = sock_init();

    if(sockfd < 0)

    {

    return -1;

    }

 

    int maxfd;

    fd_set  set, rset;

    FD_ZERO(&set);  //清空集合

 

    FD_SET(sockfd, &set); //文件描述符加入集合

    maxfd = sockfd;   //设置最大文件描述符

 

    while(1)

    {

    rset = set;//由于select每次都会修改集合中的数据,所有每次都会将set的值拷贝给rset

    printf("select..\n");

    ret = select(maxfd+1, &rset, NULL, NULL, NULL);//调用select监听rset集合

    printf("select over..\n");

    if(ret<0)

    {

    perror("select");

    break;

    }

 

    if(FD_ISSET(sockfd, &rset))   //有客户端请求连接

    {

    //1、接受客户端

printf("accept...\n");

    cfd = accept(sockfd, NULL, NULL);

printf("accept  over...\n");

    if(cfd<0)

    {

    perror("accept");

    continue;

    }

    //2、将cfd加入set集合

    FD_SET(cfd, &set);

 

    //3、判断最大值

    if(maxfd<cfd)

    {

    maxfd = cfd;

    }

    }

 

    for(int i=0; i<=maxfd; i++)

    {

    if(i == sockfd)

    {

    continue;

    }

    if(!FD_ISSET(i, &rset))

    {

    continue;

    }

 

    printf("read...\n");

    ret = read(i, buff, 1024 );

    printf("read  over...\n");

    if(ret<0)

    {

    perror("read");

    //1、关闭文件描述符

    close(i);

    //2、从set集合中移除

    FD_CLR(i, &set);

    continue;

    }

    else if(0 == ret) 

    {

    printf("tcp  broken...\n");

    //1、关闭文件描述符

    close(i);

    //2、从set集合中移除

    FD_CLR(i, &set);

    continue;

    }

 

    buff[ret] = '\0';

    printf("buff: %s\n", buff);

    }

    }

    

    return 0;

}

 

select优缺点

    Select通过串行模拟并行可以实现服务端的多任务, 但是如果某个任务处理的时间很长,就会影响后面任务的处理。除此之外,select能够监听的文件描述符的个数是有限的,默认是1024。 而且select每次解除阻塞仅仅只是说明有套接字数据准备好了,具体是哪个套接字有数据要通过轮循遍历的方式给找出来。

 

 

IO多路复用之epoll

epoll实现原理

  Epoll是一种高效的I/O多路复用机制,它可以同时监控多个文件描述符,当其中任意一个文件描述符就绪时,就会通知应用程序进行相应的操作。相比于传统的select和poll机制,epoll具有更高的性能和更好的扩展性。

Epoll的实现原理主要包括三个部分:epoll_create、epoll_ctl和epoll_wait。应用程序需要调用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于标识该实例。在Linux内核中,会为该实例创建一个红黑树和一个双向链表,用于存储待监控的文件描述符和相关的事件信息。接下来,应用程序可以通过epoll_ctl函数向epoll实例中添加、修改或删除待监控的文件描述符和事件。该函数的第一个参数是epoll实例的文件描述符,第二个参数是操作类型(EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL),第三个参数是待监控的文件描述符,第四个参数是一个epoll_event结构体,用于指定待监控的事件类型和相关的数据。应用程序需要调用epoll_wait函数等待文件描述符就绪。该函数的第一个参数是epoll实例的文件描述符,第二个参数是一个epoll_event数组,用于存储就绪的文件描述符和相关的事件信息,第三个参数是数组的大小,第四个参数是超时时间(单位为毫秒)。当有文件描述符就绪时,该函数会返回就绪的文件描述符数量,并将相关的事件信息存储在epoll_event数组中。

Epoll的高效性和扩展性主要体现在以下几个方面:

(1)Epoll使用红黑树和双向链表存储待监控的文件描述符和事件信息,可以快速地进行查找和插入操作,而不需要遍历整个文件描述符集合。

(2)Epoll支持边缘触发和水平触发两种模式。边缘触发只在文件描述符状态发生变化时通知应用程序,而水平触发则会在文件描述符处于就绪状态时不断通知应用程序。边缘触发可以减少不必要的通知,提高效率。

(3)Epoll支持EPOLLONESHOT事件,可以确保每个文件描述符只被一个线程处理,避免了多个线程同时处理同一个文件描述符的情况。

(4)Epoll支持EPOLLEXCLUSIVE事件,可以确保每个文件描述符只被一个进程处理,避免了多个进程同时处理同一个文件描述符的情况。

Epoll是一种高效的I/O多路复用机制,可以大大提高应用程序的性能和可扩展性。在实际应用中,应该根据具体的需求选择合适的触发模式和事件类型,以达到最佳的性能和可靠性。

 

epoll相关知识

    1、创建集合空间

       int epoll_create(int size);

参数: 

size  : 指定文件描述符的个数

 

返回值:

成功:返回与集合关联的文件描述符

失败:  -1

 

 

 

2、管理集合空间中的文件描述符

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

 

    参数说明:

epfd:和集合关联的文件描述符

op :  命令参数

      EPOLL_CTL_ADD:  往集合中添加文件描述符

      EPOLL_CTL_MOD: 修改集合中文件描述符的信息

      EPOLL_CTL_DEL:  删除集合中指定的文件描述符

fd:  操作的文件描述符

event:  如果是删除操作,该参数忽略,直接传NULL

             如果是添加操作, 通过该参数告诉内核监听指定文件描述符的指定时间

 

    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 */

           };

          

           events:        EPOLLIN(读事件)

 

3、监听集合中的文件描述符

 int epoll_wait(int epfd, struct epoll_event *events,

                      int maxevents, int timeout);

参数说明:

epfd:和集合关联的文件描述符

events:存放准备好文件描述符信息数组的起始地址

maxevents:  数组的最大元素个数

timeout  :设置超时时间   (以毫秒为单位的)

          >0  : 指定超时时间

          =0  : 非阻塞函数

          -1  : 永久阻塞

 

 

返回值:

>0 : 准备好的文件描述符个数

=0 :超时时间到了

-1 :出错

 

 

epoll应用案例

 #include <stdio.h>

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

#include <arpa/inet.h>

#include <unistd.h>

#include <pthread.h>

#include <stdlib.h>

#include <assert.h>

#include <sys/epoll.h>

 

//成功返回监听套接字, 失败返回NULL 

int sock_init()

{

    int sockfd;

    int ret;

 

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if(sockfd<0)

    {

    perror("socket");

    return -1;

    }

 

    //设置套接字端口复用

    int opt = 1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

 

    struct sockaddr_in  seraddr;

    int addrlen = sizeof(struct sockaddr_in);

    seraddr.sin_family = AF_INET;

    seraddr.sin_port = htons(8001);

    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);

    ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);

    if(ret<0)

    {

    perror("bind");

    return -1;

    }

 

 

    ret = listen(sockfd, 10);

    if(ret<0)

    {

    perror("bind");

    return -1;

    }

    return sockfd;

}

 

int main()

{

    char buff[1024];

    int sockfd;

    int ret, count;

    int cfd, efd;

 

    efd = epoll_create(100);

    if(efd<0)

    {

    perror("epoll_create");

    return -1;

    }

 

    sockfd = sock_init();

    if(sockfd < 0)

    {

    return -1;

    }

    

    struct  epoll_event  ev;

    struct  epoll_event  evs[10];

    ev.events = EPOLLIN;

    ev.data.fd = sockfd;

    epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev);

 

    while(1)

    {

    printf("wait...\n");

    count = epoll_wait(efd, evs, 10, -1);

    printf("wait over...\n");

    if(count < 0)

    {

    perror("epoll_wait");

    break;

    }

 

    for(int i=0; i<count; i++)

    {

    int  temp =  evs[i].data.fd;

    if(temp == sockfd)// 有客户端请求连接

    {

    //1、接收客户端

    printf("accept...\n");

    cfd = accept(sockfd, NULL, NULL);

    printf("accept  over...\n");

    if(cfd<0)

    {

    perror("accept");

    continue;

    }

    //2、cfd 加入efd关联的集合中

                ev.data.fd = cfd;

    epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);

    }

    else   //已经连接过来的客户端发来数据

    {

    printf("read..\n");

    ret = read(temp, buff, 1024);

    printf("read  over..\n");

    if(ret<0)

    {

    perror("read");

    //1、关闭文件描述符

    close(temp);

    //2、从集合中移除

        epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);

    }

    else if(ret == 0)

    {

    printf("tcp  broken...\n");

    //1、关闭文件描述符

    close(temp);

    //2、从集合中移除

        epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);

    }

    buff[ret] = '\0';

    printf("buff: %s\n", buff);

    }

    }

    }    

    return 0;

}

 

 

epoll优缺点

  缺点:通过串行模拟并行, 如果处理一个请求的时间过长,会影响后面任务的处理。这是所有多路IO转接的通病。

优点:(1)监听文件描述符的个数由用户指定

        (2)如果有套接字的数据准备好, 内核直接告诉你哪个套接字的数据准备就绪,不需要采用轮循的方式去查找

 

 

 

 

上一篇:嵌入式开发初学者必看:bootloader移植步骤

下一篇:Linux系统

戳我查看2020年嵌入式每月就业风云榜

点我了解华清远见高校学霸学习秘籍

猜你关心企业是如何评价华清学员的

干货分享
相关新闻
前台专线:010-82525158 企业培训洽谈专线:010-82525379 院校合作洽谈专线:010-82525379 Copyright © 2004-2024 北京华清远见科技发展有限公司 版权所有 ,京ICP备16055225号-5京公海网安备11010802025203号

回到顶部