IO复用之——select

一. select

前面提到Linux下的五种IO模型中有一个是IO复用模型,这种IO模型是可以调用一个特殊的函数同时监听多个IO事件,当多个IO事件中有至少一个就绪的时候,被调用的函数就会返回通知用户进程来处理已经ready事件的数据,这样通过同时等待IO事件来代替单一等待一个IO窗口数据的方式,可以大大提高系统的等待数据的效率;而接下来,就要讨论在Linux系统中提供的一个用来进行IO多路等待的函数——select;



二. select函数的用法

首先在使用select之前,要分清在IO事件中,往往关心的不是数据的读取就是数据的发送,也就是数据的,当然也有同时关心读写的,没有任何一个IO事件既不关系读也不关心写的,因此,在对于使用select对多个IO事件进行监听检测的时候,就要对这些事件进行读写的分类,以便日后在select返回时通过检测能够得知当前事件是读发生了还是写发生了;

函数参数中,

nfds表示当前最大文件描述符值+1;

readfds表示当前的事件中有多少是关心数据的读取的;

writefds表示当前的事件中有多少是关心数据的写入的;

excptfds表示当前事件中关心异常发生的事件集,也是数据的写入;

其中,fd_set是一个文件符集的数据类型

对于fd_set文件描述符集的设置,系统提供了四个函数来进行操作:

FD_CLR是对文件描述符集中的所有文件描述符进行清除;

FD_ISSET是判断某个文件描述符是否已经被设置进某个文件描述符集中;

FD_SET是将某个文件描述符设置进某个文件描述符集中;

FD_ZERO是对某个文件描述符集进行初始化;

timeout是时间的设定,表示当超过设定的时间仍然没有事件就绪时就超时返回不再等待;

timeout的结构体类型如下:

tv_sec是秒的设置;

tv_usec是微秒的设置;

对于select函数的返回值:

当返回值为-1的时候,表示函数出错并会置相应的错误码;

当返回值为0的时候,表示超时返回;

当返回值大于0的时候,表示至少已经有一个事件已经就绪可以处理其数据了;



三. 栗子时间

前面有一篇本人写的博客是基于TCP协议的socket编程,其中一个服务器为了能处理多个连接请求将listen监听和accept处理连接请求分开,每当listen到一个连接请求的时候就fork出一个子进程让子进程去处理,或者使用多线程,这样就不耽误对网络中连接请求的监听了;

但是同样是单进程,可以使用select的IO复用模型来解决对多个连接的数据处理,程序设计如下:

server服务器端:

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

#define _BACKLOG_ 5//设置监听队列里面允许等待的最大值

int fds[20];//用于集存需要进行处理的IO事件

void usage(const char *argv)//进行命令行参数的差错判断
{
    printf("%s   [ip]   [port]\n", argv);
    exit(0);
}

int creat_listen_sock(int ip, int port)//创建listen socket
{
    int sock = socket(AF_INET, SOCK_STREAM, 0); 
    if(sock < 0)
    {   
        perror("socket");
        exit(1);
    }   

    struct sockaddr_in server;//设置本地server端的网络地址信息
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = ip; 

    if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定端口号和网络地址信息
    {   
        perror("bind");
        exit(3);
    }
    
    if(listen(sock, _BACKLOG_) < 0)//进行监听
    {
        perror("listen");
        exit(2);
    }

    return sock;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
        usage(argv[0]);

    int port = atoi(argv[2]);
    int ip = inet_addr(argv[1]);

    int listen_sock = creat_listen_sock(ip, port);//获取监听端口号

    struct sockaddr_in client;//创建对端网络地址信息结构体用于保存对端信息
    socklen_t client_len = sizeof(client);

    size_t fds_num = sizeof(fds)/sizeof(fds[0]);
    size_t i = 0;
    for(; i < fds_num; ++i)//将存放文件描述符的数组进行初始化
        fds[i] = -1;

    fds[0] = listen_sock;//首先将listen socket添加进去
    fd_set read_fd;//创建读事件文件描述符集
    fd_set write_fd;//创建写事件文件描述符集
    int max_fd = fds[0];//首先将最大的文件描述符集设定为listen socket
    
    while(1)
    {
        FD_ZERO(&read_fd);//将两个文件描述符集进行初始化
        FD_ZERO(&write_fd);
        struct timeval timeout = {10, 0};//设定超时时间

        size_t i = 0;
        for(; i < fds_num; ++i)//每次循环都要将数组中的文件描述符进行重新添加设置
        {
            if(fds[i] > 0)
            {
                FD_SET(fds[i], &read_fd);
                if(fds[i] > max_fd)
                    max_fd = fds[i];
            }
        }

        switch(select(max_fd+1, &read_fd, &write_fd, NULL, &timeout))//进行select等待
        {
            case -1://出错
                perror("select");
                break;
            case 0://超时
                printf("time out...\n");
                break;
            default://至少有一个IO事件已经就绪
                {
                    size_t i = 0;
                    for(; i < fds_num; ++i)
                    {
                    //当为listen socket事件就绪的时候,就表明有新的连接请求
                        if(FD_ISSET(fds[i], &read_fd) && (fds[i] == listen_sock))
                        {
                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);
                            if(accept_sock < 0)
                            {
                                perror("accept");
                                continue;
                            }

                            char *client_ip = inet_ntoa(client.sin_addr);
                            int client_port = ntohs(client.sin_port);
                            printf("connect with a client...  [ip]:%s  [port]:%d\n", client_ip, client_port);

                            size_t i = 0;
                            for(; i < fds_num; ++i)//将新的连接请求的文件描述符添加进数组保存
                            {
                                if(fds[i] == -1)
                                {
                                    fds[i] = accept_sock;
                                    break;
                                }
                            }
                            if(i == fds_num)
                                close(accept_sock);
                        }
                        //除了listen socket就是别的普通进行数据传输的文件描述符
                        else if(FD_ISSET(fds[i], &read_fd) && (fds[i] > 0))
                        {
                            char buf[1024];
                            ssize_t  size = read(fds[i], buf, sizeof(buf)-1);
                            if(size < 0)
                                perror("read");
                            else if(size == 0)
                            {//当client端关闭就关闭相应的文件描述符
                                printf("client closed...\n");
                                close(fds[i]);
                                fds[i] = -1;
                            }
                            else
                            {
                                buf[size] = ‘\0‘;
                                printf("client# %s\n", buf);
                            }
                        }
                        else
                        {}
                    }

                }
                break;
        }
    }
    return 0;
}

因为客户端的程序和前面的TCP的程序一样,这里就不再多写;

上面的程序可以分为如下步骤:

  1. 创建监听套接字并绑定本地网络地址信息进行监听;
  2. 创建一个全局的数组用于存放已有事件的文件描述符,便于重新进行整理;
  3. 创建读、写事件集,这里忽略异常事件集;
  4. 循环等待各个事件的就绪,每次都重新初始化事件集和重新添加设置,因为select会将没有就绪的事件清为0;
  5. select完成进行返回值的一个判断:如果是-1,则出错返回;如果是0,则超时返回;如果是大于零的值,则表明至少有一个事件就绪,转到第6步;
  6. 将数组中的事件拿出一一进行判断:如果是listen socket就绪表明有新的连接请求,新创建一个文件描述符用于处理数据的传输,并将其添置进数组中;如果是别的文件描述符就绪表明有数据传输过来需要读取,转第7步;
  7. 读取数据时,如果判断client端关闭就将数组中相应位置还原回无效值并且关闭相应的socket文件描述符,读取成功输出数据,继续循环;

运行程序:



可以注意到上面的程序中sever端只将所有的连接请求都作为读事件添加进去了,而并没有关心写事件,事实上socket支持全双工的通信,因此,将上面的程序改为server端读取数据的同时将数据再写回给client端,以此来告知client端server端已经成功收到了数据,程序改进如下:

在循环每一次重新整理数组中的文件描述符集的时候将不是listen socket的文件描述符集同时添加进读事件集和写事件集:

        FD_ZERO(&read_fd);
        FD_ZERO(&write_fd);
        FD_SET(listen_sock, &read_fd);//先将listen socket添加进读事件集
        struct timeval timeout = {10, 0}; 

        size_t i = 1;//循环跳过listen socket从1开始
        for(; i < fds_num; ++i)
        {
            if(fds[i] > 0)
            {
                FD_SET(fds[i], &read_fd);//同时添加进读事件集和写事件集
                FD_SET(fds[i], &write_fd);
                if(fds[i] > max_fd)
                    max_fd = fds[i];
            }   
        }

而当数据就绪进行读取完毕之后,再将同一个缓冲区中的数据写回client端,这里因为读写事件中使用的是同一个文件描述符,因此,当一个socket的读事件准备就绪的时候,说明写事件同样也是就绪的,而且使用同一个缓冲区中相同的数据:

else if(FD_ISSET(fds[i], &read_fd) && (FD_ISSET(fds[i], &write_fd)) && (fds[i] > 0))
{
     char buf[1024];
     ssize_t  size = read(fds[i], buf, sizeof(buf)-1);
     if(size < 0)
         perror("read");
     else if(size == 0)
     {
         printf("client closed...\n");
         close(fds[i]);
         fds[i] = -1;
         break;
      }
      else
      {
         buf[size] = ‘\0‘;
         printf("client# %s\n", buf);
      }
      if(FD_ISSET(fds[i], &write_fd))
      {
          size = write(fds[i], buf, strlen(buf));
          if(size < 0)
               perror("write");
      }
      else
         printf("can not write back...\n");
}

因此,在client端也需要进行读取;

运行程序:

总结如上,虽然select实现IO复用在等待数据的效率看来要比单一的等待高,但是不难发现当需要等待多个事件的时候,是需要不断地进行复制和循环判断的,这也同样增加了时间复杂度增加了系统的开销,而且,作为一个数据类型的fd_set是由上限的,我的当前机器sizeof(fd_set)值为128,而一个字节能添加8个文件描述符,也就是总共只能添加128*8=1024个文件描述符,这个数目还是有些小的,无疑也是一个缺点。

《完》

时间: 2024-08-08 10:05:59

IO复用之——select的相关文章

IO复用一select, poll, epoll用法说明

三种IO复用类型 Select系统调用 #include<sys/select.h> int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* execptfds,struct timeval* timeout); #nfds表示监听的文件描述符总数: #readfds,writefds,execptfds分别表示对应的fd_set类型的集合 可以如下定义:fd_set readfds,writefds,execptfds

IO复用之select poll epoll的总结

I/O复用使得程序能够同时监听多个文件描述符,对于提高程序性能至关重要.I/O复用不仅仅在网络程序中使用,但是我接触到的例子中,TCP网络编程那块使用I/O复用比较多,例如,TCP服务器同时处理监听socket和连接socket. 在了解I/O复用之前,我们需要先了解几个概念. 1,同步I/O与异步I/O 2,LT(水平触发)和ET(边缘触发) POSIX把两个术语定义如下: 同步I/O:导致请求进程阻塞,直到I/O操作完成 异步I/O:  不导致请求进程阻塞 阻塞是进程在等待某种资源,但是不能

【Unix网络编程】chapter6 IO复用:select和poll函数

chapter6 6.1 概述 I/O复用典型使用在下列网络应用场合. (1):当客户处理多个描述符时,必须使用IO复用 (2):一个客户同时处理多个套接字是可能的,不过不叫少见. (3):如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字. (4):如果一个服务器既要处理TCP,又要处理UDP (5):如果一个服务器要处理多个服务或多个协议 IO复用并非只限于网络,许多重要的应用程序也需要使用这项技术. 6.2 I/O模型 在Unix下可用的5种I/O模型的基本区别: (1)阻塞式I

IO复用:select函数

IO模型: (1)阻塞式IO模型:          最流行的I/O模型是阻塞式I/O模型,默认情况下,所有的套接字都是阻塞的. 如上图所示,进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或发生错误才返回.最常见的错误是系统调用被信号中断,我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的.recvfrom成功返回后,应用进程开始处理数据报. (2)非阻塞式I/O模型:     进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把

IO复用与select函数

socket select函数的详细讲解 select函数详细用法解析      http://blog.chinaunix.net/uid-21411227-id-1826874.html

多路IO复用模型--select, poll, epoll

select 1.select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数 2.解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct tim

[Z] linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO

原文链接:http://blog.csdn.net/colzer/article/details/8169075 IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file descriptor(fd,文件描述符).而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符).描述符就是一个数字,指向内核中一个结构体(文件路径,数据

TCP/IP 网络编程 (抄书笔记 5) -- select 和 IO 复用

TCP/IP 网络编程 (抄书笔记 5) – select 和 IO 复用 TCP/IP 网络编程 (抄书笔记 5) – select 和 IO 复用 利用 fork() 生成子进程 可以达到 服务器端可以同时响应多个 客户端的请求, 但是这样做有缺点: 需要大量的运算和内存空间, 每个进程都要有独立的内存空间, 数据交换也很麻烦 (IPC, 如管道) IO 复用: 以太网的总线结构也是采用了 复用技术, 如果不采用, 那么两两之间就要直接通信 网络知识 int server_sock; int

linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO(转载)

IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file descriptor(fd,文件描述符).而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符).描述符就是一个数字,指向内核中一个结构体(文件路径,数据区,等一些属性).那么我们的应用程序对文件的读写就通过对描述符的读写完成. linux将内存分为内核区,用户区.l