IO多路复用

IO多路复用

  • IO多路复用的概念
  • SELECT
    • 经典案例:
  • POLL
    • 经典案例:
  • EPOLL

IO多路复用的概念

有一天,学校里面优化了热水的供应,增加了很多水龙头,这个时候小明同学再去装水,舍管阿姨告诉他这些水龙头都还没有水,你可以去忙别的了,等有水了告诉他。于是等啊等(select调用中),过了一会阿姨告诉他有水了。

这里有两种情况:
情况1: 阿姨只告诉来水了,但没有告诉小明是哪个水龙头来水了,要自己一个一个去尝试。(select/poll 场景)
情况2: 舍管阿姨会告诉小明同学哪几个水龙头有水了,小明同学不需要一个个打开看(epoll 场景)
(epoll这么优秀,直接用epoll呀,还用什么select/poll)
IO多路复用_第1张图片
当用户进程调用了select,那么整个进程就会被block,而同时,kernel会 “监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,IO多路复用的特点是通过一种机制,一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入就绪状态,select()函数就可以返回。
这里需要使用两个system call(select 和 recvfrom),而blocking IO只调用了一个system call(recvfrom)。但是,用select的优势在于它可以同时处理多个connection
如果处理的连接数不是很高的话,使用select/epollweb server不一定比使用mutil-threading + blocking IOweb server性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更好,而是在于能同时处理更多的连接。

SELECT

在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。

#include 
int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
nfds :   最大的文件描述符加1。
readfds: 用于检查可读的文件描述符。//fd_set 是一个数据类型,用于表示"一组"文件描述符
writefds:用于检查可写性的文件描述符。
exceptfds:用于检查异常的数据的文件描述符。
timeout:一个指向timeval结构的指针,用于决定select等待I/o的最长时间。如果为空将一直等待。(如果设置为0,那就时无限期阻塞)

timeval结构的定义:
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
}
返回值:  >0  是已就绪的文件句柄的总数, =0 超时, <0 表示出错,错误: errno 
#include  
int FD_ZERO(fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0 
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用 
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位 
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

经典案例:

//服务器端 server.c
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include 

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;//定义了两个文件描述符集合,testfds是一个临时变量
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket 
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr*)&server_address, server_len);
    listen(server_sockfd, 5); //监听队列最多容纳5个 
    FD_ZERO(&readfds);
    FD_SET(server_sockfd, &readfds);//将服务器端socket加入到读的集合中
    while (1)
    {
        char ch;
        int fd;
        int nread;
        testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 
        printf("server waiting\n");

        /*无限期阻塞,并测试文件描述符变动 */
        result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0); 
		//FD_SETSIZE:系统默认的最大文件描述符.
		//在 Linux 系统中,FD_SETSIZE 的默认值通常是 1024 或 2048。
		//而在linux中,可以认为文件描述符的范围是从 0 到 1023
        if (result < 1)//如果 select 函数返回大于 0 的值,则表示至少有一个文件描述符发生了可读事件。
        {
            perror("server5");
            exit(1);
        }

        /*扫描所有的文件描述符*/
        for (fd = 0; fd < FD_SETSIZE; fd++)//0~FD_SETSIZE-1
        {
            /*找到相关文件描述符*/
            if (FD_ISSET(fd, &testfds))
            {
                /*判断是否为服务器套接字,是则表示为客户请求连接。*/
                if (fd == server_sockfd)
                {
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,
                        (struct sockaddr*)&client_address, &client_len);
                    FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中
                    printf("adding client on fd %d\n", client_sockfd);
                }
                /*客户端socket中有数据请求时*/
                else
                {
                    ioctl(fd, FIONREAD, &nread);//取得数据量交给nread

                    /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                    if (nread == 0)//客户端关闭了
                    {
                        close(fd);
                        FD_CLR(fd, &readfds); //去掉关闭的fd
                        printf("removing client on fd %d\n", fd);
                    }
                    /*处理客户数据请求*/
                    else
                    {
                        read(fd, &ch, 1);//因为ch是char类型,所以每次只读一个字符
                        sleep(2);
                        printf("serving client on fd %d\n", fd);
                        ch++;//将ch+1,在将ch写回去
                        write(fd, &ch, 1);
                    }
                }
            }
        }
    }

    return 0;
}
//客户端
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 

int main()
{
    int client_sockfd;
    int len;
    struct sockaddr_in address;//服务器端网络地址结构体 
    int result;
    char ch = 'A';
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客户端socket 
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(9000);
    len = sizeof(address);
    result = connect(client_sockfd, (struct sockaddr*)&address, len);
    if (result == -1)
    {
        perror("oops: client2");
        exit(1);
    }
    printf("I have sent ch:%c\n", ch);
    //第一次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the first time: char from server = %c\n", ch);
    sleep(2);

    //第二次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the second time: char from server = %c\n", ch);

    close(client_sockfd);

    return 0;
}

IO多路复用_第2张图片

POLL

和select 一样,如果没有事件发生,则进入休眠状态,如果在规定时间内有事件发生,则返回成功,规定时间过后仍然没有事件发生则返回失败。可见,等待期间将进程休眠,利用事件驱动来唤醒进程,将更能提高CPU的效率。

poll 和select 区别: select 有文件句柄上线设置,值为FD_SETSIZE,而poll 理论上没有限制!

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

输入参数:
fds://可以传递多个结构体,也就是说可以监测多个驱动设备所产生的事件,只要有一个产生了请求事件,就能立即返回
struct pollfd {//该文件句柄的一些信息
      int fd;                /*文件描述符   open打开的那个*/
      short events;     /*请求的事件类型,监视驱动文件的事件掩码*/  POLLIN | POLLOUT
      short revents;    /*驱动文件实际返回的事件,比如:是POLLIN来了,还是POLLOUT来了,还是都来了,以"位"来表示*/
}
nfds:  //监测驱动文件的个数
timeout://超时时间,单位是ms .每过timeout毫秒,poll就检测一下
事件类型events 可以为下列值:
POLLIN           有数据可读
POLLRDNORM 有普通数据可读,等效与POLLIN
POLLPRI         有紧迫数据可读
POLLOUT        写数据不会导致阻塞
POLLER          指定的文件描述符发生错误
POLLHUP        指定的文件描述符挂起事件
POLLNVAL      无效的请求,打不开指定的文件描述符

返回值:
有事件发生  返回revents域不为0的文件描述符个数
超时:return 0
失败:return  -1   错误:errno

经典案例:

// 服务器端 server_poll.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_FD 8192        // 最大的文件句柄
struct pollfd fds[MAX_FD]; // 文件句柄的集合. 并可以处理最多8192个文件描述符(客户端连接)。
int cur_max_fd = 0;        // 当前最大的文件句柄

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    // fd_set readfds, testfds;
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立服务器端socket
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
    listen(server_sockfd, 5); // 监听队列最多容纳5个
    // FD_ZERO(&readfds);
    // FD_SET(server_sockfd, &readfds);//将服务器端socket加入到集合中
    fds[server_sockfd].fd = server_sockfd;
    fds[server_sockfd].events = POLLIN; // 将events字段设置为POLLIN,表示服务器有兴趣从套接字读取数据。
    fds[server_sockfd].revents = 0;
    if (cur_max_fd <= server_sockfd) // 这段代码只会执行一次,因为在程序的开始阶段,server_sockfd是唯一一个已知的文件描述符,并且cur_max_fd的值尚未初始化
    {
        cur_max_fd = server_sockfd + 1;
    }

    while (1)
    {
        char ch;
        int i, fd;
        int nread;
        // testfds = readfds;//将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
        printf("server waiting\n");

        /*无限期阻塞,并测试文件描述符变动 */
        result = poll(fds, cur_max_fd, 1000);
        // result = select(FD_SETSIZE, &testfds, (fd_set*)0, (fd_set*)0, (struct timeval*)0); //FD_SETSIZE:系统默认的最大文件描述符
        if (result < 0)
        {
            perror("server5");
            exit(1);
        }
        /*扫描所有的文件描述符*/
        for (i = 0; i < cur_max_fd; i++)
        {

            /*找到相关文件描述符*/
            if (fds[i].revents)
            {
                fd = fds[i].fd;
                /*判断是否为服务器套接字,是则表示为客户请求连接。*/
                if (fd == server_sockfd)
                {
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,
                                           (struct sockaddr *)&client_address, &client_len);
                    fds[client_sockfd].fd = client_sockfd; // 将客户端socket加入到集合中
                    fds[client_sockfd].events = POLLIN;
                    fds[client_sockfd].revents = 0;

                    if (cur_max_fd <= client_sockfd)
                    {
                        cur_max_fd = client_sockfd + 1;
                    }

                    printf("adding client on fd %d\n", client_sockfd);
                    // fds[server_sockfd].events = POLLIN;
                }
                /*客户端socket中有数据请求时*/
                else
                {
                    if (fds[i].revents & POLLIN) // 要读客户端的
                    {
                        // ioctl(fd, FIONREAD, &nread);//取得数据量交给nread
                        nread = read(fd, &ch, 1);
                        /*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                        if (nread == 0)
                        {
                            close(fd);
                            memset(&fds[i], 0, sizeof(struct pollfd)); // 去掉关闭的fd
                            printf("removing client on fd %d\n", fd);
                        }
                        /*处理客户数据请求*/
                        else
                        {
                            // read(fds[fd].fd, &ch, 1);
                            sleep(2);
                            printf("serving client on fd %d, read: %c\n", fd, ch);
                            ch++;
                            // write(fd, &ch, 1);回复客户端
                            fds[fd].events = POLLOUT;
                        }
                    }
                    else if (fds[i].revents & POLLOUT) // 要给客户端写
                    {
                        write(fd, &ch, 1);       // 回复客户端
                        fds[fd].events = POLLIN; // 写完了,让它继续监视POLLIN
                    }
                }
            }
        }
    }

    return 0;
}
// 客户端
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int client_sockfd;
    int len;
    struct sockaddr_in address; // 服务器端网络地址结构体
    int result;
    char ch = 'A';
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立客户端socket
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(9000);
    len = sizeof(address);
    result = connect(client_sockfd, (struct sockaddr *)&address, len);
    if (result == -1)
    {
        perror("oops: client2");
        exit(1);
    }
    printf("I have sent ch:%c\n", ch);
    // 第一次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the first time: char from server = %c\n", ch);
    sleep(2);

    // 第二次读写
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the second time: char from server = %c\n", ch);

    close(client_sockfd);

    return 0;
}

IO多路复用_第3张图片

EPOLL

在下一篇文章中讲解

你可能感兴趣的:(Linux,IO多路复用,c,linux)