I/O 多路复用select、poll

I/O 多路复用

I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是 读就绪 或者 写就绪 ),能够通知程序进行相应的读写操作。
select 、 pollepoll 是 Linux API 提供的 I/O 复用方式。.
本质上就是,把本应该由程序员的事情交给内核负责:检测这些读写缓冲区是否可用(需要调用阻塞函数);检测一轮以后内核将可操作的fd告诉我们,我们再去操作就不会再阻塞。

select和poll的底层是线性表


服务器的文件描述符

在这里插入图片描述
服务器包含两种文件描述符,每一个都有两个缓存区:
1、监听LFD:用于标志是否有数据是否能够读写数据,需要调用accept系统调用。(有且仅有一个)
2、通讯FD1:读取和输出数据,调用read/recv、write/send方法。N,每建立一次连接加1个
问题在于该三个方法(系统调用)都是阻塞的,当全部在一个线程里就会出现一个阻塞都阻塞。

使用多路IO复用,四个buffer就不需要自己去维护了;交由内核维护。
当内核检测之后会通知那些缓冲区 可以读写,再去系统调用,由于已经就绪不会再有堵塞发生
当处理数据并不是同时进行,而是优先到后进行的(因为是单线程)。


select

概述

可跨平台
select 系统调用是用来让我们的程序**监视多个文件句柄的状态变化的** 。
select 函数监视的文件描述符分 3 类:writefds 、 readfds 、和 exceptfds 。
调用后 select 函数会阻塞 ,直到有描述符就绪(有数据可读 、可写 、或者有 except ),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回 。

缺点
select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1 、 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限;

2 、 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有**O(n)**的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

3 、需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

select的实现

在这里插入图片描述
1、假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把 进程A 分别加入这三个socket的等待队列中。

在这里插入图片描述
2、当有任意socket收到数据之后,中断程序唤醒进程A,因此进程A加入工作队列;
3、当进程A被加入工作进程说明起码会有一个socket有数据,因此接下来就需要遍历所有的socket即可。

问题:
1、进程A需要加入到 所有监视socket的等待队列 中,并且当被唤醒时有需要从 每一个socket中被移除
2、进程A被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。


函数

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

n:要检测的文件描述符集合里,最大的文件描述符的值+1;这是因为fd是线性表(数组)的下标;或者直接写1024。
readfds:读集合,检测一系列文件描述符的读缓冲区。
    传入传出参数,读集合(放需要写的fd) 一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据。
writefds:写集合,检测一系列文件描述符的读缓冲区。
    传入传出参数,如果不需要使用这个参数可以指定为NULL。
exceptfds :异常集合

slect函数检测这些集合里的文件描述符,如果这些集合都没有满足条件(读集合里fd的读缓存区全为空,写集合里fd的写缓冲区全为慢的)select会一直在检测;timeout设置最多检测时间;timeout=0,select函数不阻塞,直接退出。

为什么类型是fd_set *?
因为我们需要把一块内存传给select,在函数体内部修改这个内存;也就是缺点3。

fd_set

占据1024bit,128Byte,int[32];=但是以bit去处理数据。
每一个文件标识符对应一个bit。
如果集合中标志位为 0代表不检测 这个fd状态;
如果集合中标志位为 1代表检测 这个fd状态;这个fd在fd_set里
在这里插入图片描述
fd3 6 9 10读缓存区有数据,记录下来并回传给用户空间;

在这里插入图片描述
遍历检查fs_set是否有为1的,为1的说明已经准备就绪,分辨一下是什么类型的缓存区。

void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);

select流程

  1. 先将需要监控读、写或异常的fd分别存入对应的集合中;
  2. 这三个集合会被拷入进内核中;内核基于一个线性表去轮询这些集合的事件;
  3. 内核发现出现了事件(可以读写或者出现异常),传出三个集合;
  4. 传出的三个集合会被内核写到传入的三个集合的地址上(readfds 、writefds 、exceptfds 被内核修改)。

举个栗子:

集合 传入 传出
readfds 5、6、7、8、9 5、6、7、8、9
writefds 7、8、9、10 9、10
exceptfds 5-10

传出和传入的集合在同一块内存上,由内核负责改写。
读集合中有5和6号描述符,判断一下到底是用于监听的还是数据的;如果是监听的调用accept,与客户端建立链接;读数据就是调用read/rev。
写集合有9、10,调用send。

服务器代码示例

  1. 初始化fd_set,服务器的套接字fd并绑定
  2. while(1)
  3. 运行select,并把select之后的fd_set提出来进行遍历,分辨是监听的还是数据的
  4. 如果是监听的,将新的fd加入fd_set中

初始化服务器套接字并绑定

	/*服务器描述符,创建服务器基本性质,但不包含地址信息,需要使用bind绑定部分信息*/
	int iSocketServer= socket(AF_INET, SOCK_STREAM, 0);
	
	/* 端口复用,服务器关闭后端口可以like使用 */
	int opt = 1;
	setsockopt(iSocketServer, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
	
	/*创建服务器的Addr信息,IP和端口*/
	struct sockaddr_in tSocketServerAddr;
	/*客户端IP和端口*/
	struct sockaddr_in tSocketClientAddr;
	
	/*定义服务器的相关性质*/
	tSocketServerAddr.sin_family      =</
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值