在Linux之五种IO模型中,我们介绍了说 IO多路复用包含三种机制select、poll和epoll,本篇文章就来区分一下他们。
其中B站这个up主讲的非常好【并发】IO多路复用select/poll/epoll介绍
一、几个概念
1.1、用户空间和内核空间
现在操作系统都是采用虚拟存储器,那么对于32位操作系统而言,他的寻址空间(虚拟存储空间)为4G。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的所有权限。。为了保证内核的安全,操作系统将虚拟空间划分为两部分,即内核空间和用户空间。对于Linux系统,将高1G字节供内核使用,称为内核空间。而将较低的3G字节供各个进程使用,称为用户空间。
1.2、进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。但是进程切换很耗资源。
1.3、进程的阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。
1.4、文件描述符(FD)
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
1.5、缓存I/O
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fds[],来找到就绪的描述符。
fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄
缺点:
1、需要维护一个用来存放大量fd的数据结构(位图),这样会使得从用户态拷贝到内核态时开销大
2、同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
3、为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(32位系统为1024,64位系统为2048)。
优点:
select目前几乎在所有的平台上支持,具有良好跨平台支持。
2.2、 poll
poll几乎和select原理相同,但是poll并非使用位图(fd_set)来记录每一个文件描述符,而是使用pollfd结构体(多个就采用链表),使得poll支持的文件描述符集合限制远大于select的1024(或2048).
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
struct pollfd *fds fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;一个pollfd结构体表示一个被监视的文件描述符,通过传递fds指示 poll() 监视多个文件描述符。其中,结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域,结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域
2.3、epoll
相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。相当于共享存储文件描述符的内存
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1、epoll_create函数先创建一个epoll句柄,参数size表明内核要监听的描述符数量再通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知
2、IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数(底层红黑树)来实现的。只有就绪的fd才会执行回调函数。
三、总结