Linux编程--IO多路复用

目录

IO多路复用概念 

IO的五种模型

 IO多路复用select函数

 select函数使用步骤

 select函数实现

 IO多路复用poll函数

IO多路复用epoll函数

epoll函数实现


IO多路复用概念 

IO多路复用是指通过一种机制,使得单个进程可以监控多个文件描述符的可读、可写和异常等事件。常见的IO多路复用技术包括:select、poll、epoll等。

在实际应用中,IO多路复用可以提高程序的运行效率和性能,减少系统开销,降低系统资源的使用率。它广泛应用于网络编程、服务器开发、操作系统等领域,可以帮助开发人员更好地处理大量的网络连接和数据请求。

IO的五种模型

  1. 阻塞IO模型:在这种模型中,当一个I/O操作被触发时,程序将一直阻塞直到操作完成返回结果。

  2. 非阻塞IO模型:这种IO模型中,程序在发起了I/O操作后,可以立即返回并继续执行其他操作,同时也会不断地轮询I/O操作是否完成,直到返回结果。

  3. IO复用模型:这种模型引入了一个IO复用器(如select、epoll等),通过它可以同时监听多个I/O操作是否完成的事件,当某个I/O操作完成时,复用器会通知程序进行处理,这种模型能够大大提高I/O效率。

  4. 信号驱动IO模型:这种模型中,当一个I/O操作完成时,操作系统会向程序发送一个信号,程序在收到信号后,再去处理I/O操作的结果。

  5. 异步IO模型:异步IO模型与IO复用模型类似,但是它不需要轮询IO操作的完成状态,而是注册一个回调函数,当IO操作完成后,操作系统会自动调用回调函数进行处理,这种模型能够进一步提高I/O效率。

Linux编程--IO多路复用_第1张图片

阻塞型:

Linux编程--IO多路复用_第2张图片 非阻塞型:

Linux编程--IO多路复用_第3张图片

io多路复用模型

Linux编程--IO多路复用_第4张图片

信号驱动模型:

Linux编程--IO多路复用_第5张图片

异步模型:

Linux编程--IO多路复用_第6张图片

 IO多路复用select函数

IO多路复用select函数是一种实现并行IO的机制,可以同时处理多个socket连接,提高系统的性能和效率。

在Linux中,select函数使用fd_set结构体来维护文件描述符集合,通过设置文件描述符集合来监听事件。当文件描述符集合中的任何一个文件描述符就绪、可读或可写时,select函数会返回,并将这些文件描述符标记为就绪状态,从而使程序能够处理对这些文件描述符的相应操作。

以下是select函数的基本用法:

#include 

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

  • nfds:文件描述符的最大值,即文件描述符集合中最大的文件描述符值加1;
  • readfds:读文件描述符集合;
  • writefds:写文件描述符集合;
  • exceptfds:异常文件描述符集合;
  • timeout:超时时间。

在使用select函数时,需要将需要监听的文件描述符添加到相应的文件描述符集合中,并调用select函数等待就绪状态的文件描述符。一旦有文件描述符就绪,select函数将返回,并将就绪文件描述符集合返回给程序,程序可以针对这些就绪文件描述符进行相应操作。

使用select函数可以同时监听多个文件描述符的状态,从而避免了使用多个线程或进程对每个文件描述符进行监听的复杂性和开销,提高了系统的可扩展性和性能。

 select函数使用步骤

IO多路复用是指在一个进程中可以同时监视多个文件描述符(fd),可以是socket,文件,标准输入输出等等。等待数据准备好的同时也可以处理其他任务。

        1.包含头文件

#include 

        2.函数声明

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

  1. 参数说明
  • nfds:需要监控的最大文件描述符加1。
  • readfds:读文件描述符集合。
  • writefds:写文件描述符集合。
  • exceptfds:异常文件描述符集合。
  • timeout:select函数的超时时间设置,如果设置为NULL,则一直等待直到事件发生。
  1. fd_set结构体

fd_set结构体是一个位图类型结构体,它定义了一个可以包含1024个文件描述符的数组。

typedef struct fd_set {
    unsigned long fds_bits[FD_SETSIZE / sizeof(unsigned long)];
} fd_set;

其中,FD_SETSIZE被定义为1024,所以fd_set结构体可以保存1024个文件描述符。fds_bits是一个数组,每个数组元素的每个二进制位表示一个文件描述符是否在集合中。

  1. 函数返回值
  • 成功:返回已经准备好的文件描述符数目,0表示超时,-1表示出错。
  • 失败:设置errno。
  1. 函数使用步骤

(1)初始化fd_set结构体

fd_set readfds;
fd_set writefds;
fd_set exceptfds;

FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);

(2)将需要监控的文件描述符添加到fd_set结构体中

FD_SET(fd, &readfds);   // 添加读文件描述符
FD_SET(fd, &writefds);  // 添加写文件描述符
FD_SET(fd, &exceptfds); // 添加异常文件描述符

(3)调用select函数,等待事件发生

int ret = select(nfds, &readfds, &writefds, &exceptfds, &timeout);
if (ret == -1) {
    perror("select");
    exit(1);
} else if (ret == 0) {
    // 超时
} else {
    if (FD_ISSET(fd, &readfds)) {
        // 该读文件描述符已准备好
    } else if (FD_ISSET(fd, &writefds)) {
        // 该写文件描述符已准备好
    } else if (FD_ISSET(fd, &exceptfds)) {
        // 该异常文件描述符已准备好
    }
}

其中,FD_ISSET函数用于判断某个文件描述符是否在fd_set结构体中。

  1. select函数的局限性
  • 最大文件描述符数目有限,一般为1024。
  • 每次调用select函数时都需要将所有需要监控的文件描述符从用户空间复制到内核空间,效率较低。

 select函数实现

服务端:

#include "net.h"
#include 
#define MAX_SOCK_FD 1024

int main(int argc, const char *argv[])
{
        int i, ret, fd ,newfd;
        fd_set set, tmpset;
        Addr_in clientaddr;
        socklen_t clientlen = sizeof(Addr_in);

        Argment(argc, argv);

        fd = CreateSocket(argv);

        FD_ZERO(&set);
        FD_ZERO(&tmpset);
        FD_SET(fd, &set);

        /*接受客户端连接,生成新的套接字*/
        while(1){
                tmpset = set;
                if((ret = select(MAX_SOCK_FD, &tmpset, NULL, NULL, NULL)) < 0)
                        ErrExit("select");

                if (FD_ISSET(fd, &tmpset)){
                        //接受客户端连接
                        if ((newfd = accept(fd, (Addr *)&clientaddr, &clientlen))<0)
                                perror("accept");
                        printf("[%s:%d]已建立连接\n",
                    inet_ntoa(clientaddr.sin_addr),
                                        ntohs(clientaddr.sin_port));
                        FD_SET(newfd, &set);
                }else{
                        //处理客户端
                        for (i=fd+1; i < MAX_SOCK_FD; i++){
                                if(FD_ISSET(i, &tmpset)){
                                        if( (ret = DataHandle(i)) < 0){
                                                if( getpeername(i, (Addr *)&clientaddr, &clientlen) )
                            perror("getpeername");
                        printf("[%s:%d]断开连接\n",
                                inet_ntoa(clientaddr.sin_addr),
                                                                ntohs(clientaddr.sin_port));
                                                FD_CLR(i ,&set);
                                        }
                                }
                        }

                }
        }
        return 0;
}

 IO多路复用poll函数

IO多路复用(IO Multiplexing)是指使用一个线程监听多个文件描述符(File Descriptor),实现对多个IO请求的同时服务。在Linux中,常见的IO多路复用函数有select、poll和epoll。

poll函数是一个常用的IO多路复用函数,它可以同时监听多个文件描述符上的可读、可写和错误事件。与select函数不同的是,poll函数可以处理的文件描述符数量没有上限,且效率更高。

poll函数的基本用法如下:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其中,fds是一个指向pollfd结构体数组的指针,每个结构体对应一个被监听的文件描述符;nfds是fds数组中结构体的数量;timeout是等待的毫秒数(如果设置为-1,则表示无限等待)。

pollfd结构体定义如下:

struct pollfd {
    int fd; //文件描述符
    short events; //要监听的事件类型
    short revents; //实际发生的事件类型
};

使用poll函数的流程如下:

  1. 创建pollfd结构体数组,并将需要监听的文件描述符和事件类型填入结构体中;
  2. 调用poll函数开始监听,等待文件描述符上的事件发生;
  3. 返回时,检查pollfd结构体数组中每个元素的revents字段,判断哪些文件描述符上发生了事件;
  4. 处理发生的事件,然后继续监听。如果不需要再监听,则关闭文件描述符。

示例代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main(int argc, char *argv[])
{
    if (argc <= 2) {
        printf("Usage: %s ip port\n", argv[0]);
        return 1;
    }
    const char *ip = argv[1];
    int port = atoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 2;
    }

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_addr.sin_addr);
    server_addr.sin_port = htons(port);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        return 3;
    }

    struct pollfd fds[2];
    fds[0].fd = 0; //标准输入
    fds[0].events = POLLIN;
    fds[0].revents = 0;
    fds[1].fd = sockfd; //连接socket
    fds[1].events = POLLIN | POLLRDHUP;
    fds[1].revents = 0;

    char buffer[BUFFER_SIZE];
    int ret, i;
    while (1) {
        ret = poll(fds, 2, -1);
        if (ret < 0) {
            printf("poll failure\n");
            break;
        }

        if (fds[1].revents & POLLRDHUP) {
            printf("server close the connection\n");
            break;
        } else if (fds[1].revents & POLLIN) {
            memset(buffer, '\0', BUFFER_SIZE);
            recv(fds[1].fd, buffer, BUFFER_SIZE - 1, 0);
            printf("%s\n", buffer);
        }

        if (fds[0].revents & POLLIN) {
            //从标准输入读取数据
            memset(buffer, '\0', BUFFER_SIZE);
            fgets(buffer, BUFFER_SIZE - 1, stdin);
            if (strcmp(buffer, "exit\n") == 0) {
                break;
            }
            //将读取的数据写入连接socket
            ret = send(sockfd, buffer, strlen(buffer), 0);
            if (ret < 0) {
                printf("send failure\n");
                break;
            }
        }
    }

    close(sockfd);
    return 0;
}

上述代码使用poll函数同时监听标准输入和连接socket,实现了一个简单的客户端程序。当用户从标准输入中输入数据时,程序将数据发送到服务器;当从连接socket中接收到数据时,程序将数据输出到屏幕上。程序可以通过输入“exit”来退出。

IO多路复用epoll函数

IO多路复用是一种高效的编程技术,可以同时监视多个文件描述符,当其中任意一个文件描述符发生状态变化时,就会触发相应的事件。

在Linux系统下,IO多路复用有多种实现方式,包括select、poll和epoll函数。其中,epoll函数是Linux 2.6以后推出的一种高性能IO多路复用技术,相对于select和poll函数,它有以下优点:

  1. 没有最大并发连接数限制,能够处理大量的并发连接。
  2. 内核与用户空间共享同一块内存,避免了用户空间和内核空间之间的数据拷贝。
  3. 支持水平触发和边缘触发两种模式,可以根据需要选择不同的模式。
  4. 通过epoll_ctl函数可以动态地添加、修改和删除事件,更加灵活。

IO多路复用是指单个进程同时监控多个文件描述符,一旦某个文件描述符就绪(读写就绪、异常事件就绪),就能够进行相应的读写操作。其中,epoll是Linux环境下最常用的IO多路复用机制。下面是epoll函数的使用步骤和函数详解:

        1.创建epoll句柄

int epoll_create(int size);

size表示新创建的epoll句柄的大小,一般可以设置为文件描述符的个数。如果size小于0,会返回错误。

        2.添加、修改和删除事件

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

epfd表示epoll句柄,op表示操作类型,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL之一,分别表示添加、修改、删除事件。fd表示需要监听的文件描述符,event表示事件的类型和属性,包括:

  • EPOLLIN:表示可读事件
  • EPOLLOUT:表示可写事件
  • EPOLLRDHUP:表示TCP连接被对方关闭
  • EPOLLPRI:表示带外数据可读
  • EPOLLERR:表示错误事件
  • EPOLLHUP:表示连接已经挂断
  • EPOLLONESHOT:表示触发一次后会自动删除这个事件

        3.等待事件就绪

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

epfd表示epoll句柄,events表示就绪的事件数组,maxevents表示最多等待的事件个数,timeout表示等待的超时时间,单位为毫秒。如果timeout为-1,则表示永远等待,直到有事件就绪。如果timeout为0,则表示立即返回,不进行等待。函数返回值表示就绪的事件个数。

使用epoll函数时,一般的流程是先创建epoll句柄,然后添加需要监听的文件描述符和相应的事件,最后使用epoll_wait函数等待事件就绪并处理就绪的事件。处理就绪的事件可以通过对事件的类型和属性进行判断,然后进行相应的读写操作等。

epoll函数实现

#include "net.h"
#include 
#define MAX_SOCK_FD 1024

int main(int argc, const char *argv[])
{
        int i,nfds, epfd, fd ,newfd;
        Addr_in clientaddr;
        socklen_t clientlen = sizeof(Addr_in);
        struct epoll_event tmp, events[MAX_SOCK_FD] = {};

        Argment(argc, argv);

        fd = CreateSocket(argv);

        if ((epfd = epoll_create(1)) < 0)
                ErrExit("epoll_creat");

        tmp.events = EPOLLIN;
        tmp.data.fd = fd;

        if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &tmp))
                ErrExit("epoll_ctl");

        /*接受客户端连接,生成新的套接字*/
        while(1){
                if ((nfds = epoll_wait(epfd, events, MAX_SOCK_FD, -1))<0)                        ErrExit("epoll_wait");
                //接受客户端连接
                for(i = 0; i< nfds; i++){
                        if (events[i].data.fd == fd){
                                if ((newfd = accept(fd, (Addr *)&clientaddr, &clientlen))<0)
                                        perror("accept");
                                printf("[%s:%d]已建立连接\n",
                                                inet_ntoa(clientaddr.sin_addr),
                                                ntohs(clientaddr.sin_port));
                                tmp.events = EPOLLIN;
                                tmp.data.fd = newfd;
                                if (epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &tmp))
                                        ErrExit("epoll_ctl");
                        }else{
                                // 处理客户端数据
                                if( (DataHandle(events[i].data.fd)) <= 0){
                                        if( getpeername(events[i].data.fd, (Addr *)&clientaddr, &clientlen) )
                                                perror("getpeername");
                                        printf("[%s:%d]断开连接\n",
                                                        inet_ntoa(clientaddr.sin_addr),
                                                        ntohs(clientaddr.sin_port));
                                        if (epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &tmp))
                                                ErrExit("epoll_ctl");
                                        close(events[i].data.fd);
                                }
                        }
                }
        }
        close(fd);
        return 0;
}
#include "net.h"
 
void Argment(int argc, char *argv[]){
    if(argc < 3){
        fprintf(stderr, "%s\n", argv[0]);
        exit(0);
    }
}
int CreateSocket(char *argv[]){
    /*创建套接字*/
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0)
        ErrExit("socket");
    /*允许地址快速重用*/
    int flag = 1;
    if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
        perror("setsockopt");
    /*设置通信结构体*/
    Addr_in addr;
    bzero(&addr, sizeof(addr) );
    addr.sin_family = AF_INET;
    addr.sin_port = htons( atoi(argv[2]) );
    /*绑定通信结构体*/
    if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )
        ErrExit("bind");
    /*设置套接字为监听模式*/
    if( listen(fd, BACKLOG) )
        ErrExit("listen");
    return fd;
}
int DataHandle(int fd){
    char buf[BUFSIZ] = {};
    Addr_in peeraddr;
    socklen_t peerlen = sizeof(Addr_in);
    if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )
        perror("getpeername");
    int ret = recv(fd, buf, BUFSIZ, 0);
    if(ret < 0)
        perror("recv");
    if(ret > 0){
        printf("[%s:%d]data: %s\n", 
                inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
    }
    return ret;
}
#ifndef _NET_H_
#define _NET_H_
 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
#define BACKLOG 5
#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)
 
void Argment(int argc, char *argv[]);
int CreateSocket(char *argv[]);
int DataHandle(int fd);
 
 
#endif

你可能感兴趣的:(网络)