42.Linux网络编程--IO多路复用(select poll epoll)

一. 预备知识

1.Unix五种IO模型

[1] blocking IO - 阻塞IO
[2] nonblocking IO - 非阻塞IO
[3] IO multiplexing - IO多路复用
[4] signal driven IO - 信号驱动IO
[5] asynchronous IO - 异步IO

 

2.用户空间 / 内核空间

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。

 

3.进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。

 

4.文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

 

5.缓存I/O

缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

 

二.select引入

学习了TCP和UDP的网络编程,我们理解了整个的一个编程过程及一些关键API的作用及使用。下面开始讲解IO多路复用。

学习着网络突然插入个IO多路复用,和网络有毛线的关联,蒙圈啊??为什么要用它?它又怎么用?同时在说相关联的三种机制,select ,poll,epoll。我们逐步深入,带你理清他们的产生过程。

假如现在要实现一个可以并发的网络服务器,那么我们该怎么写??前面我们说过使用多线程,多进程来实现并发,这些方法当然是可行的,但是并不是很理想,CPU的上下文切换是十分占用资源的。那么不用这种方式,一个进程要监控那么多的客户端,我们很容易想到的方法就是循环遍历每个客户端的状态。我们知道在Linux中一切皆文件,我们把每个客户端理解为一个小房子,我们轮询的去查看每个房子是不是有人,轮询一遍后,然后对有人的房子进行标记,然后再对有标记的进行处理。我们的I/O多路复用就是利用了这种思路。

先说一下IO多路复用的官方套话:通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

按照这种思路,我们在用户态下按照这种思路也是可以实现的,我们的内核在内核态下帮我们实现了这个逻辑并且封装成了函数,就是select函数。

那么我们只要知道怎么用这个函数,就能通过一个进程实现并发服务。

那么我们通过man select指令来详细的查看一下这个函数原型及参数含义。

 

函数名

int select(int max_fd, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);

所需头文件

#include

#include

#include

功能

监视文件描述符的变化

传入参数

maxfd

read_fds

 write_fd

except_fds

timeout

所有监控的文件描述符中最大的那一个加1

读文件描述符集合

写文件描述符集合

异常描述符集合

超时时间

Null:一直阻塞,直到有文件描述符就绪或出错

时间值为0:仅仅检测文件描述符集的状态,然后立即返回

时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

 

返回值

成功时返回文件描述符,出错时返回为-1

select在监视文件描述符之前,我们还要对文件描述符进行设置,设置需要用到以下几个和select相关的宏定义

void FD_ZERO(fd_set *fdset) //从fdset中清除所有的文件描述符

void FD_SET(int fd,fd_set *fdset) //将fd加入到fdset

void FD_CLR(int fd,fd_set *fdset) //将fd从fdset里清除

int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fdset集合中

 

三.seclect应用实例

select大概理解是什么意思了,那么到底怎么用,看一个实例我们就很清楚了,假如我们的客户端做一个类似qq的东西,此时我们要接收键盘输入的信息,进行显示,同时要接受客户端的信息,我们使用select进行处理。

如下所示 :第一部分忽略,就是创建及连接的过程,第二部分开始select的应用。

(1)首先创建了rset的这莫一个集合.(bitmap 默认1024位,有1024个坑位,需要监听的fd位上置1,不监听的置0.有点像我们平时使用1个字节的不同位来表示多种状态值的操作)

(2)使用FD_ZERO清除所有的文件描述符,

(3)FD_SET将远程服务器的fd加入到集合中,

(4)select阻塞等待,这里设置的阻塞时间是5s.

当服务器发过来数据后,通过FD_ISSET来进行判断是哪个fd触发了,然后分别进行处理。

这里键盘输入的fd我们没有添加,可以通过FD_SET添加更多的文件描述符到集合中。

42.Linux网络编程--IO多路复用(select poll epoll)_第1张图片

 

这里说一个selet的一个执行过程:

首先我们定义了rest文件描述符集合,程序本身执行在用户态空间,select函数直接将用户态空间的rest拷贝到

内核态,然后内核进行判断每个位是否有数据到来。(内核态进行监听效率提高)

42.Linux网络编程--IO多路复用(select poll epoll)_第2张图片

当有数据到来的时候,内核将fd置位,1个或多个fd置位,select函数返回。

然后FD_ISSET,判断是哪个fd置位了。

形象的可以这么理解,有这么一个酒店,有1024个房间(rset),然后全部打扫干净后,准备开始营业了,此时来了5个客人,在前台录好信息,入住房间,这些人就归你这个酒店来监控管理了。早上的时候,服务员巡逻,有的客人出去了,将门口的牌子置为“请打扫房间”,然后服务员就告诉前台,有客人出去了要打扫房间,前台告诉扫地阿姨,然后扫地阿姨查看这5个房间哪个房间要扫,然后进行处理。

 

我们的select有几个弊端:

  1. bitmap默认最大1024位(也就造成最大连接数1024)--》fd_set结构体实际上是一个long类型的数组。
  2. FD_SET不可重用。不论客人你是昨天没走还是今天刚来的,前台都要重新录入一下入住信息。
  3. 每次调用select,都要把fd集合从用户态拷贝到内核态,有较大的资源开销。
  4. 每次调用都要进行线性遍历,时间复杂度位O(n).前台告诉阿姨有客户要清扫,但是不知道是哪一个,阿姨还要从入住的5个客户中依次查看一遍,哪个房间挂牌子了。

 

四.POLL

select是比较早(1984)实现的一种方法,有缺点,随着时间的推进,1997年实现了poll机制,优化了select的一些缺点。poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。fd_set结构体的实现原理上通过数组改为了链表来实现,这样就没有了最大连接数的限制。pollfd结构体中revents的引入,每次只重置位revents,没走的客户就不用重录信息啦。

来看一下poll的实现。

42.Linux网络编程--IO多路复用(select poll epoll)_第3张图片

events,用户需要输入事件,可赋值如下参数

42.Linux网络编程--IO多路复用(select poll epoll)_第4张图片

 

函数名

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

所需头文件

#include

功能

监视文件描述符的变化

传入参数

fds 

nfds

timeout

pollfd结构体指针

标记数组中结构体元素的总个数

超时时间

超时时间 ,等于0表示非阻塞式等待,小于0表示阻塞式等待,大于0表示等待的时间。
返回值:

 

返回值

成功时返回fds数组中事件就绪的文件描述符的个数
返回0表示超时时间到了。
返回-1表示调用失败,对应的错误码会被设置。

poll的实现步骤:

1)将需要监控的文件描述符放进fds数组中
2)调用poll函数
3)函数成功返回后根据返回值遍历fds数组,将关心的事件与结构体中的revents相与判断事件是否就绪。
4)事件就绪执行相关操作。

 

poll机制的引入,解决了select的第一和第二个缺点,其他两个缺点仍然没有解决。

 

五.epoll

epoll,2002年实现,不用说,肯定是为了提高效率并且解决上面提到的几个弊端。

epoll是Linux目前大规模网络并发程序开发的首选模型。

epoll相关函数:

函数名

int epoll_create(int size);

所需头文件

#include

功能

创建一个epoll的句柄

传入参数

size

用来告诉内核这个监听的数目一共有多大

需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

 

返回值

调用成功时返回一个epoll句柄描述符,失败时返回-1

 

函数名

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

所需头文件

#include

功能

注册要监听的事件类型

传入参数

epfd

op

fd

event

 表示epoll句柄

表示fd操作类型,有如下3

EPOLL_CTL_ADD 注册新的fdepfd

EPOLL_CTL_MOD 修改已注册的fd的监听事件

EPOLL_CTL_DEL epfd中删除一个fd

监听的描述符

epoll_event 结构体

 

返回值

返回0表示成功,否则返回–1

 

函数名

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

所需头文件

#include

功能

等待事件的就绪

传入参数

epfd

events

maxevents

timeout

epoll句柄

表示从内核得到的就绪事件集合

告诉内核events的大小

超时时间

 

返回值

成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0

 

调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝,所以不存在拷贝所带来的开销。

epoll中当有事件触发,同样也需要置位fd,但是对置位的fd进行重新排序(统一放到就绪列表中),并且epoll_wait返回值会返回一共有多少个fd触发了事件。之前阿姨打扫房间,不知道几个客人走了,也不知道位置在哪,现在会告诉你走了3个人,并且将他们三个的房间都整理到了挨着的位置,此时阿姨的效率将大大提高了。时间复杂度变为了O(1).

 

五.总结select poll epoll区别

42.Linux网络编程--IO多路复用(select poll epoll)_第5张图片

网上找了两个特别形象的图,供理解:

select

42.Linux网络编程--IO多路复用(select poll epoll)_第6张图片

epoll

42.Linux网络编程--IO多路复用(select poll epoll)_第7张图片

你可能感兴趣的:(#,华清嵌入式培训,网络,IO多路复用,select,poll)