Linux基本概念
用户空间 / 内核空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
文件描述符
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。 从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
缓存I/O
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点: 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
进程阻塞
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得了CPU资源),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。(链接:juejin.cn/post/688298…
5种IO模型
- [1] blocking IO - 阻塞IO
- [2] nonblocking IO - 非阻塞IO
- [3] IO multiplexing - IO多路复用
- [4] signal driven IO - 信号驱动IO
- [5] asynchronous IO - 异步IO 前面4种IO都可以归类为synchronous IO - 同步,IOsignal driven IO平时用的比较少,这里就不介绍了
同步IO 之 Blocking IO
用户进程process在Blocking IO读recvfrom操作的两个阶段都是等待的。
在数据没准备好的时候,process原地等待kernel准备数据。
kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
同步IO 之 NonBlocking IO
process在NonBlocking IO读recvfrom操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
IO multiPlexing
IO多路复用 :select poll epoll模型
process 在两个处理阶段都是block住等待的 但是select poll epoll的优势是可以以较少代价来同时监听处理多个IO
异步IO
异步IO要求process在recvfrom操作的两个处理阶段上都不能等待,也就是process调用recvfrom后立刻返回,kernel自行去准备好数据并将数据从kernel的buffer中copy到process的buffer在通知process读操作完成了,然后process在去处理。遗憾的是,linux的网络IO中是不存在异步IO的,linux的网络IO处理的第二阶段总是阻塞等待数据copy完成的。真正意义上的网络异步IO是Windows下的IOCP(IO完成端口)模型。
linux的socket事件wakeup callback 机制
[1] 睡眠等待逻辑 :select poll epoll_wait 的阻塞等待逻辑
- select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list
- 进入循环的schedule直到关心的事件发生了
- 关心的事件发生后,将当前process的wait_entry节点从socket的sleep_list中删除。
[2] 唤醒逻辑
-
socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数
-
直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。
-
一般情况下callback包含两个逻辑:
1.wait_entry自定义的私有逻辑; 2.唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。
select 逻辑
1,fds [file descriptors]
2, 留下了三个问题:
(1)被监控的fds集合限制为1024,1024太小了,我们希望能够有个比较大的可监控fds集合
(2)fds集合需要从用户空间拷贝到内核空间的问题,我们希望不需要拷贝
(3)当被监控的fds中某些有数据可读的时候,我们希望通知更加精细一点,就是我们希望能够从通知中得到有可读事件的fds列表,而不是需要遍历整个fds来收集。
poll逻辑:
只解决了第一个问题 poll改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。
epoll逻辑:
在计算机行业中,有两种解决问题的思想:
[1] 计算机科学领域的任何问题, 都可以通过添加一个中间层来解决
[2] 变集中(中央)处理为分散(分布式)处理
下面,我们看看,epoll在解决select的遗留问题(2)和(3)的时候,怎么运用这两个思想的。
fds集合拷贝
对于IO多路复用,有两件事是必须要做的(对于监控可读事件而言):
- 准备好需要监控的fds集合;
- 探测并返回fds集合中哪些fd可读了。 细看select或poll的函数原型,我们会发现,每次调用select或poll都在重复地准备(集中处理)整个需要监控的fds集合。然而对于频繁调用的select或poll而言,fds集合的变化频率要低得多,我们没必要每次都重新准备(集中处理)整个fds集合。
epoll引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开。同时,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝。
另外,epoll通过epoll_ctl来对监控的fds集合来进行增、删、改,那么必须涉及到fd的快速查找问题,于是,一个低时间复杂度的增、删、改、查的数据结构来组织被监控的fds集合是必不可少的了。在linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是在创建epoll fd的时候,epoll需要初始化hash的大小。于是epoll_create(int size)有一个参数size,以便内核根据size的大小来分配hash的大小。在linux 2.6.8以后的内核中,epoll使用红黑树来组织监控的fds集合,于是epoll_create(int size)的参数size实际上已经没有意义了。
按需遍历就绪的fds集合
epoll引入了一个中间层,一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list)
ET(Edge Triggered 边沿触发) vs LT(Level Triggered 水平触发)
说到Epoll就不能不说说Epoll事件的两种模式了,下面是两个模式的基本概念:
Edge Triggered (ET) 边沿触发
-
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
-
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
仅在缓冲区状态变化时触发事件,比如数据缓冲去从无到有的时候(不可读-可读)
Level Triggered (LT) 水平触发
-
socket接收缓冲区不为空,有数据可读,则读事件一直触发
-
socket发送缓冲区不满可以继续写入数据,则写事件一直触发
符合思维习惯,epoll_wait返回的事件就是socket的状态