高级I/O中多路转接-select

11 篇文章 0 订阅

我们都知道I/O的速度很慢,电脑的瓶颈很大一部分就在磁盘I/O速度跟不上CPU的处理速度。I/O的部分其实分为两部分,第一步是等待,就是等待数据到来的时候;第二步是数据拷贝。通常来说等待的时间占大头,为了提高I/O的效率就需要减少等待的时间。

五种I/O模型

阻塞I/O:这是最常见的I/O方式,在内核将数据准备好之前,系统调用会一直处于等待状态。网络套接字的默认方式都是阻塞方式。

非阻塞I/O:这种方式不会等待,如果发现内核没有将数据准备好就会直接返回,同时返回EWOULDBLOCK错误。这种方式的I/O需要代码利用循环的方式不停地向文件描述符寻找数据是否到来,这种方式叫做轮询,会占用大量的CPU时间,不推荐使用。

信号驱动I/O:内核在数据准备好的时候,通过信号的方式通知应用程序进行I/O操作,这里需要使用信号。

I/O多路转接:一次性等待多个I/O的数据。看似跟阻塞I/O一样,其中的不同在于I/O多路转接等待的数量是多个。因为是等待多个,其中出现一个I/O数据到达的概率就大大增加,所以这样看起来它的效率是更高的。

异步I/O:应用程序调用另外一个进程来帮忙等待,等数据到达在通知应用程序进行数据拷贝。

高级I/O一般包括这些:非阻塞I/O、记录所、系统V流机制、I/O多路转接、readv和writev函数以及存储映射I/O。

非阻塞I/O

我们使用的网络套接字默认都是阻塞的,但是我们可以通过fcntl函数将其变成非阻塞I/O。

#include <unistd.h>
#include <fcntl.h>
//函数可以将文件描述符设置为非阻塞,这样就算使用阻塞的I/O调用也不会出现阻塞。
int fcntl(int fd, int cmd, .../*arg*/);
//成功返回依赖于cmd,失败返回-1

常见的cmd有以下五种:

  • 复制一个已有的文件描述符(F_DUPFD或F_DUPFD_CLOEXEC)
  • 获取/设置文件描述符标志(F_GETFD或F_SETFD)
  • 获取/设置文件状态标志(F_GETFL或F_SETFL)
  • 获取/设置异步I/O所有权(F_GETOWN或F_SETOWN)
  • 获取/设置记录锁(F_GETLK、F_SETLK或F_SETLKW)

使用这个函数将文件描述符设置为非阻塞的方法是:先通过cmd = F_GETFL获取到文件描述符的属性,然后再通过F_SETFL设置回去,设置回去的同时加上一个O_NONBLOCK。

下面是一个通过轮询的方法使用非阻塞I/O从标准输入读取数据的例子:

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

void SetNonBlock(int fd){
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0){
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main(){
    SetNonBlock(0);
    while(1){
        char* buf[1024] = {0};
        int num = read(0, buf, sizeof(buf)-1);
        if(num < 0){
            printf("errno:%d, error_string:%s\n", errno, strerror(errno));
            sleep(2);
            continue;
        }
        printf("buf:%s\n", buf);
    }
    return 0;
}

实验截图:

nonblock

从实验中可以知道,如果非阻塞的I/O没有读取到消息的时候,返回的errno是11,这个错误码是EAGAIN,表示再试一次,因为没有数据到来,所以再次获取。

select

select是I/O多路转接的一种方式。等待文件描述符的读写异常事件的状态。

select函数原型及参数介绍
#include <sys/select.h>
int select(int nfds, struct fd_set* readfds, struct fd_set* writefds, \
            struct fd_set* execptfds, struct timeval *timeout);

参数


nfds:指的是监控的文件描述符最大值+1,加一的原因是文件描述符是从0开始的,但是nfds表示的是个数。该参数的作用是指定select函数在fd_set参数中搜索的范围。


readfds、writefds、execptfds:这三个参数分别表示需要关心文件描述符的读事件、写事件、异常事件,类型是描述符集的指针。类型是fd_set结构体,我们可以将它看做是一个很大的位图。他们三个既是输入型参数也是输出型参数。

描述符集

同时系统提供了四个函数,让我们方便的使用这三个描述符集。

#include <sys/select.h>
int FD_ISSET(int fd, fd_set* fdset);    //如果fd在描述符集中,返回非0,否则返回0
void FD_CLR(int fd, fd_set* fdset);
void FD_SET(int fd, fd_set* fdset);
void FD_ZERO(fd_set* fdset);

这些接口可以实现为宏或者函数。调用FD_ZERO将一个fd_set变量的所有位置为o。我们可以通过FD_SET开启其中一个位,通过FD_CLE清除一个位,通过FD_ISSET判断该位是否被打开。

这里稍微解释一下fd_set结构在函数中的作用:

  • 在select中,通过则这三个固定位置的描述符集表示关心fd的什么事件,例如readfds就表示这个描述符集只关心读事件的状态变化,其他的事件都不关心。
  • fd_set结构为什么使用位图表示,很简单,因为我们的fd从0开始,同时是一个连续的正整型数字。
  • 当fd_set作为输入型参数的时候,他的下标表示对应的文件描述符,下标对应的内容如果是1表示关心这个文件描述符上的某类事件,0表示不关心。
  • 当fd_set作为输出型参数的时候,下标对应的内容如果为1,表示关心的该类事件的下标对应的文件描述符已经就绪。
  • 正是因为fd_set不但是输入型参数,又是输出型参数,我们需要每次在使用的的是进行清零操作,同时还需要将源数据保存起来,用来下一次使用。

timeout:指的是愿意等待的时间。类型是struct timeval*,该结构体包括两个参数,一个是秒(tv_sec),一个是微秒(tv_usec)。

该参数的有三种设定方法:

  • timeout == NULL;select将会一直阻塞,一直到某事件的就绪或者被信号打断。
  • timeout==0;非阻塞状态,检测有没有事件的发生,之后立刻返回。
  • timeout>0;等待一定的时间,然后在规定时间内没有事件就绪,超时返回。

返回值,select函数的返回值有三种:

  • 如果执行成功,返回事件就绪中的所以文件描述符大的个数。
  • 如果返回0,表示timeout超时返回。
  • 如果返回-1,表示出错,将错误原因存于errno,此时的readfds、writefds、execptfds的值将没有意义。
  • errno的值可能为:EBADF:fd无效或被关闭;EINTR:select调用被信号打断;EINVAL:select参数错误;ENOMEM:内存不够。

常见的使用格式如下:

fd_set set;
FD_ZERO(set);
FD_SET(fd, set);
//表示关心fd上的读事件状态变化,同时通过阻塞的方式。
select(fd+1, &set, NULL, NULL, NULL); 
socket的读写就绪

读就绪

  • 接收缓冲区中的字节数大于等于低水位标记SO_RCVLOWAT,可以无阻塞的读,返回值大于0
  • TCP通信中,对端关闭,可以读,返回值为0
  • 监听的socket上出现了新连接

写就绪

  • 发送缓冲区中的字节数大于等于低水位标记SO_SNDLOWAT,可以无阻塞的写,返回值大于0
  • 对写操作被关闭的socket进行写,触发SIGPIPE信号,该信号默认中断
  • socket使用非阻塞connect连接成功或失败之后。
select的特点和缺点

特点

1、 可以监控的文件描述符个数跟fd_set(文件描述符集)的大小有关,sizeof(fd_set)*8就是最大的个数。

2、 select需要一个额外的数组空间进行文件描述符集源的保存,原因有两个:第一个需要保存源,因为描述符集作为输入、输出型参数,如果监听了三个文件描述符的读事件,此时只有前两个就绪,那么第三个就会在返回的时候被清空,此时就失去了源,所以需要额外的空间进行保存。第二个,我们需要用源中设置的监听文件描述符跟返回来的文件描述符进行FD_ISSET判断,用来进行下一步操作。

缺点

1、每次调用select都需要手动的设置fd集合。

2、每次调用的时候,会将fd集合从用户态拷贝到内核态,这样的开销是很大的。

3、需要维护一个额外的空间,用来保护源。

4、select支持的文件描述符是有上限的,这个是硬伤,因为设定了使用输入输出型参数的数组来保存信息。

参考资料

  • Unix环境高级编程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值