iOS之深刻解析Dispatch Source的原理与功能

dispatch source 和 runLoop source 都是用来监听事件的,能够建立不一样类型的 dispatch source 和 runLoop source 。dispatch source 监听到事件产生时,会将 event handler 添加到目标 queue。runLoop source 须要先按照某种模式加入到指定线程的 runLoop 中。dispatch source 和 runLoop source 都是异步处理模式,只要建立、设置好,就能够在相应的 handler 中监听到相应事件的产生。node

1、Dispatch Source与内核

GCD 中除了主要的 Dispatch Queue 外, 还有不太引人注目的Dispatch Source(信号源)。它是BSD系内核惯有功能 kqueue 的包装。nginx

  • BSD (Berkeley Software Distribution, 伯克利软件套件):是Unix 的衍生系统。例如: OpenBSD、 FreeBSD、macOS。
  • kqueue(kernel queue)内核队列:最初是2000年Jonathan Lemon在 FreeBSD 系统上开发的一个高性能的事件通知接口,是用来实现 IO 多路复用。注册一批描述符注册到 kqueue 之后(被封装成kevent) ,当其中的描述符状态发生变化时,kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。kqueue 支持多种类型的文件描述符, 包括socket 、文件状态、进程通信等。kqueue 能够说是应用程序处理 XNU 内核中发生的各类事件的方法中最优秀的一种。其CPU 负荷很是小,尽可能不占用资源。
① 文件描述符
  • 文件描述符(hle descriptor) :在Unix中,任何可读/可写也就是有 I/O 的能力,不管是文件、服务、功能、设备等都被操做系统抽象成简单的文件,提供一套简单统一的接口, 这样程序就能够像访问磁盘上的文件同样访问串口、 终端、打印机、网络等功能。大多数状况下只须要open/read/write/ioctl/close 就能够实现对各类设备的输入、输出、设置、控制等。
  • 普通文件的读写,也就是输入输出(Input/Output) ,把这种 IO 放到广义的范围,放到整个Unix中,全部的 IO ,就像一个广 义的File。
  • 文件描述符在形式上是一个非负整数。 实际上,它是个索引值,指向内核为每个进程所维护的该进程开启文件的记录表, 当程式开启一个现有文件或者创建一个新文件时, 内核向进程返回一个文件描述符。
  • 为何使用文件描述符而不像标准库那样使用文件指针?
    由于记录文件相关信息的结构存储在内核中,为了避免暴露内存的地址,所以文件结构指针不能直接给用户操做。内核中记录张表,其中列是文件描述符,对应一列文件结构指针,文件描述符就至关于获取文件结构指针的下标。
  • 系统会为建立的每一个进程默认会打开3个文件描述符:
    • 标准输入(0),是stderr
    • 标准输出(1),是stdout
    • 标准错误(2),是stderr
  • 有时候,咱们会在某些脚本中看到这样一种写法 >/dev/null 2>&1 :
    • 这条命令其实分为两命令,一个是>/dev/null,另外一个是2>&1;
    • /dev/null是一个特殊的文件,写入到它的内容都会被丢弃;若是尝试从该文件读取内容,那么什么也读取不到。
    • “>/dev/null” 这条命令的做用是将标准输出(1)重定向到/dev/null中.
    • 2>&1 的意思为将标准错误(2)指向标准输出(1)所指向的文件。此时因此此时的文件标准输出(1)和标准错误(2)都指向/dev/null。
② lsof简介

lsof 是 list open files 的简称,它的做用主要是列出系统中打开的文件。乍看起来,这是个功能很是简单,使用场景很少的命令,不过是ls的另个版本。lsof 能够知道用户和进程操做了哪些文件,也能够查看系统中网络的使用状况,以及设备的信息。web

  • 列出某个进程打开的全部文件
sudo lsof -p 5858
  • 列出某个命令使用的文件信息
    -c 参数后面跟着命令的开头字符串,不必定是具体的程序名称,好比 sudo lsof -c n 也是合法的,会列出全部名字开头字母是 n 的程序打开的文件信息。
sudo lsof -c nginx
  • 列出全部的网络链接信息
sudo lsof -i
  • 只显示TCP或者UDP链接
    在 -i 后面直接跟着协议的类型(TCP或者UDP) 就能只显示该网络协议的链接信息
sudo lsof -i TCP
  • 查看某个端口的网络链接状况
sudo lsof -i :80
  • 上面的命令输入每列的内容分别是:命令名称,进程ID、用户名、FD、文件类型、文件所在的设备、文件大小或者所在设备的偏移量、node/inode 编号、文件名。
名称 说明 内容
PID 进程ID n/a
FD(file descriptor) 文件描述符

wd:当前工做目录
rtd:根目录
mem:内存映射文件
mmap:内存映射设备
txt:应用文本(代码和数据)
数字+英文字: <数字为fle descriptor编号,英文字为锁定模式>:
(a)r:只读模式
(b)w:只写模式
©u:读写模式数组

TYPE n/a

IPv4: IPv4 socket
IPv6: IPv6 socket
inet: Internet Domain Socket
unix: unix domain socket
BLK: 设备文件
CHR: 字符文件
DIR: 文件夹
FIFO: FIFQ文件
LINK: 符号连接文件
REG: 普通文件缓存

DEVICE 设备号码 n/a
SIZE/OFF 文件大小/偏移量 n/a
NODE inode编号,包含文件的元信息

文件的字节数
文件拥有者的User ID
文件的Group ID
文件的读、写、执行权限
文件的时间戳,共有三个:
(a)ctime指inode上-次变更的时间
(b)mtime指文件内容上一次变更的时间
©atime指文件上一次打开的时间
连接数,即有多少文件名指向这个inode文件数据block的位置安全

NAME 打开的文件(即file descriptor所指向的文件) n/a
  • inode:
    能够用stat命令,查看某个文件的inode信息: stat example.txt。
    每个文件都有对应的inode,里面包含了与该文件有关的一些信息。每一个inode都有一个号码,操做系统用 inode 号码来识别不一样的文件。
    Unix/Linux 系统内部不使用文件名,而使用 inode 号码来识别文件。
    对于系统来讲,文件名只是 inode 号码便于识别的别称或者绰号。
    使用ls -i命令,能够看到文件名对应的 inode 号码。表面上,用户经过文件名,打开文件。实际上,系统内部这个过程分红三步: 首先,系统找到这个文件名对应的 inode 号码;其次,经过 inode 号码,获取 inode 信息;最后,根据 inode 信息,找到文件数据所在的 block ,读出数据。
③ IO多路复用
  • CPU单核在同一时刻只能作一件事情,一种解决办法是对 CPU 进行时分复用(多个事件流将CPU 切割成多个时间片,不一样事件流的时间片交替进行)。网络

  • 在计算机系统中,咱们用线程或者进程来表示一条执行流,经过不一样的线程或进程在操做系统内部的调度,来作到对 CPU 处理的时分复用。这样多个事件流就能够并发进行,不须要一个等待另外一个过久,在用户看起来他们彷佛就是并行在作同样。多线程

  • 可是凡事都是有成本的。线程/进程也同样,有如下几个方面:
    线程/进程建立成本;
    CPU切换不一样线程/进程成本Context Switch;
    多线程的资源竞争;并发

  • 有没有一种能够在单线程/进程中处理多个事件流的方法呢? 这就是“IO多路复用”。框架

  • 什么是“IO多路复用”?简单来讲就是经过单线程或单进程同时监测若干个文件描述符是否能够执行IO操做的能力。经过把多个I/O的阻塞复用到同一个的阻塞上,从而使得系统在单线程或单进程的状况下能够同时处理多个客户端请求。
    这样在处理1000个链接时,只须要1个线程或进程监控就绪状态,对就绪的每一个链接开一个线程或进程处理就能够了。这样须要的线程或进程数大大减小,减小了内存开销和上下文切换的CPU开销。

  • 内核实现I/O多路复用,原理就是传入多个文件描述符,若是有一个文件描述符就绪,则返回,不然阻塞直到超时。获得就绪状态后进行真正的操做能够在同一个线程或进程里执行,也能够启动线程或进程来执行(好比使用线程池)。有以下几种函数:

    • select 和 poll 函数
      采用数组和链表存储。在检查链表中是否有文件描述须要读写时,采用的是线性扫描的方法,即无论这些文件描述符是否是活跃的,都会轮询一遍,因此效率比较低。
    • epoll 和 kqueue 函数
      是以前的select 和poll 的加强版本,采用的是 Reacor 模型,采用红黑树和链表存储,会注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符(省略掉了遍历文件描述符,而是经过监听回调的的机制)。

2、Dispatch Source功能

Dispatch Source 也使用在了 Core Foundation 框架的用于异步网络的 CFSocket 中。由于Foundation 框架的异步网络API 是经过 CFSocket 实的,因此可享受到仅使用 Foundation 框架的 Dispatch Source 带来的好处。

  • NSURLSession:AFNetworking以后的版本, Alamofire、 NSURLSession底层部分使用了NSURLConnection的功能。
  • NSURLConnection:AFNetworking1,基于CFNetwork的更高层封装,提供面向对象的接口。
  • CFNetwork:ASIHttpRequest,基于CFSocket等接口的上层封装。
  • CFSocket:最底层接口 ,负责socket通讯,使用了Dispatch Source。

GCD提供了一系列的 dispatch source 用来充当监听底层系统对象:

  • 文件描述符
  • Mach port
  • Signals
  • VFS 节点

处于活动状态时的接口,当监听到事件产生的时候,dispatch source 会自动的将事件触发的回调Block派发到指定的dispatch queue 执行。

① dispatch_source_create
  • 建立一个 dispatch source 来监听底层的系统事件,当监听的事件被触发时,dispatch source会自动的将事件触发的回调Block派发到指定的 dispatch queue 执行。
  • dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t Nullable queue)
    在这里插入图片描述
/* @function dispatch_source_create * @discussion dispatch source不可重入.当dispatch source被挂起或者”event handler block“正在执行,这时候“dispatch source”接收到的多个事件会被合并,而且在“dispatch source”恢复或者正在执行的“event handler block”已经执行完毕以后,再来处理合并以后的事。 * * “dispatch source”建立的时候是处于“未被激活”的状态。 * 当建立一个“dispatch source”时并配置好了全部须要的属性(例如handler、context等),要想激活“dispatch source”,能够经过调用“dispatch_activate()”来开始接受事件。 * * 在“dispatch source”未被激活前,能够调用“dispatch_set_target_queue()”来设置目标队列,可是一旦被激活以后,就不能再设置。 * * 处于向后兼容,对于未激活和未挂起的“dispatch source”调用“dispatch_resume()”和调用“dispatch_activate()”有想相同做用,固然更好的激活方式是使用”dispatch_activate()“。 * * @param type * 声明“dispatch source”类型,必须是定义的“dispatch_source_type_t”常量中的一个。 * * @param handle * 要监听的系统底层对象的句柄(标志符),要监听进程,须要传入进程的ID。 * * @param mask * 指定“dispatch_source”要监听底层对象的类型。 * * @param queue * 指定“events handler block”提交到的目标queue。 * 若是目标queue是“DISPATCH_TARGET_QUEUE_DEFAULT”,"events handler block"将被提交默认的优先级的全局queue上。 * * @result * 建立好的“dispatch_source”,若是是NULL,则传入了非法参数。 */

 dispatch_source_t 
 dispatch_source_create(dispatch_source_type_t type, 
 						uintptr_t handle, 
 						unsigned long mask, 
 						dispatch_queue_t Nullable queue)
② dispatch_source_type_t
  • 这种类型的常量表示 dispatch source 监视的底层系统对象的类型,此类型的常量做为乡数传递给dispatch source create()。
  • 实际上 dispatch_source_create() 的 handle (例如,做为文件描述符、mach por、信号数、进程标识符等)参数和 mask 参数,最后都是被传递到下面的dispatch_source_type_t结构体中:
typedef const struct dispatch_source_type_t *dispatch_source_type_t;
  • dispatch_source_type_t 的常量:
名称 说明 dispatch_source_get_handle dispatch_source_get_mask
DISPATCH_SOURCE_TYPE_DATA_ADD 自定义事件,变量增长 n/a n/a
DISPATCH_SOURCE_TYPE_DATA_OR 自定义事件,变量OR n/a n/a
DISPATCH_SOURCE_TYPE_DATA_REPLACE 自定义事件,变量REPLACE。若是传入的数据为0,将不会出发handler n/a n/a
DISPATCH_SOURCE_TYPE_MACH_SEND 监听Mach port的deadname通知,handle是具备send权限的Mach port 包括send或send_once mach port dispatch_source_mach_send_flags_t;
// receive权限对应的send权限已被销毁,DISPATCH_MACH_SEND_DEAD 0x1
DISPATCH_SOURCE_TYPE_MACH_RECV 监听Mach port获取待等待处理的消息 mach port dispatch_source_mach_recv_flags_t; n/a
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE 监听系统中的内存压力 n/a dispatch_source_memorypressure_flags_t: DISPATCH_MEMORYPRESSURE_NORMAL 0x01 DISPATCH_MEMORYPRESSURE_WARN 0x02 DISPATCH_MEMORYPRESSURE_CRITACAL 0x04
DISPATCH_SOURCE_TYPE_PROC 监听进程事件 进程ID

dispatch_source_proc_flags_t:
// 进程已退出,可能被清理,可能没有被清理 DISPATCH_PROC_EXIT 0x80000000
// 进程建立一个或多个子线程 DISPATCH_PROC_FORK 0x40000000
// 进程成为另外一个可执行映像(exec或posix_spawn函数族调用)
// DISPATCH_PROC_EXEC 0x20000000
// 进程收到了Unix signal DISPATCH_PROC_SIGNAL 0x08000000

DISPATCH_SOURCE_TYPE_READ 监听文件描述符是否有可读的数据 文件描述符(int) n/a
DISPATCH_SOURCE_TYPE_SIGNAL 监听当前进程的signal signal number(int) n/a
DISPATCH_SOURCE_TYPE_TIMER 定时器监听 n/a dispatch_source_timer_flags_t: // 系统将尽最大努力保持精度,可能会致使更高的系统能耗 DISPATH_TIMER_STRICT 0x1
DISPATCH_SOURCE_TYPE_VNODE 监听文件描述符事件 文件描述符(int)

dispatch_source_vnode_flags_t:
DISPATCH_SOURCE_DELETE 0x1
DISPATCH_SOURCE_WRITE 0x2
DISPATCH_SOURCE_EXTEND 0x4
DISPATCH_SOURCE_ATTRIB 0x8
DISPATCH_SOURCE_LINK 0x10
DISPATCH_SOURCE_RENAME 0x20
DISPATCH_SOURCE_REVOKE 0x40
DISPATCH_SOURCE_FUNLOCK 0x100

DISPATCH_SOURCE_TYPE_WRITE 监听文件描述符使用可用的buffer空间来写数据 文件描述符(int) n/a
  • 当 dispatch source 监测到系统内存压力升高,此时,应该经过改变接下来的内存使用行为来缓解内存压力。例如,在内存压力恢复正常以前,减小新开始初始化的操做带来的缓存大小。
  • 当系统内存进入到提高状态时,应用程序不该该再继续当前的遍历操做或者释放过去操做带来的缓存,由于这可能进一步加大内存压力。
③ dispatch_source_cancle

异步取消 dispatch_source,阻止events handler block被进一步调用;

/* @function dispatch_source_cancel * * @discussion * 取消“dispatch source”可阻止“events handler block”继续调用,但不会中断正在处理的“events handler block”。 * * 当“dispatch source”的“events handler”已经完成,那么“events handler”将会被提交到目标queue。 * 而且此时代表安全的关闭“dispatch source”的句柄(标志符,即文件描素符或mach port) * * See dispatch_source_set_cancle_handler() for moreinformation * * @param source * 要取消的“dispatch source” */

void dispatch_source_cancle(dispatch_source_t source);
④ dispatch_source_testcancle

测试dispatch_source是否被取消:

/* @function dispatch_source_testcancle * * @param source * 要测试的“dispatch source” * * @param result * 0:未被取消 1:被取消 */

long dispatch_source_testcancle(dispatch_source_t source);
⑤ dispatch_source_merge_data

合并数据的类型是 DISPATCH_SOURCE_TYPE_DATA_ADD,DISPATCH_SOURCE_TYPE_DATA_OR,DISPATCH_SOURCE_TYPE_DATA_REPLACE的dispatch sorce,而且提交相应的events handler block到指定的queue。

/* @function dispatch_source_merge_data * * @param source * 要合并数据的“diapatch source”。 * * @param value * 根据“diapatch source”的数据类型,指定value合并到挂起数据的操做:OR and ADD。 * 若是value为0,不会产生任何影响,也不会提交到相应的“events handler block”。 */
 
 void dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
⑥ dispatch_source_set_timer

配置“dispatch source timer”的开始时间(start time),interval(时间间隔),精度(leeway)。

/* @function dispatch_source_set_timer * * @discusstion * 一旦再次调用这个方法,那么以前的“source timer”数据会被清除。 * “source timer”下次触发的时间将会是“start”参数设置时间。 * 此后每次间隔“interval”纳秒将会继续触发,直到“source timer”被取消。 * * 系统可能会延迟调用“source timer”的触发,以提升功耗和系统性能。 * 对于刚开始“source timer”容许的最大延迟上限是“leeway”纳秒。 * 对于“start + N * interval”时间后触发的“time source”,上限为“MIN(leeway, interval/2)”, 下限由系统控制。 * * @param start 开始时间 * * @param interval “timer”时间间隔,单位是“纳秒”,使用“DISPATCH_TIMER_FOREVER”只发射一次的“timer” * * @param leeway “timer”精度,单位纳秒。 */

void dispatch_source_set_timer(dispatch_source_t source, 
							   dispatch_time_t start, 
							   uint64_t intercal, 
							   uint64_t leeway);
⑦ dispatch_source_set_event_handler 与 dispatch_source_set_event_handler_f

对指定的dispatch source设置event handler,用来响应dispatch source的触发。

/* @function dispatch_source_set_event_handler * * @param source * 要修改的“diapatch source”。 * * @param handler * 定义一个“cancle handler block”提交到“dispatch source”的指定目标queue。 */
void dispatch_source_set_event_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);

/* @function dispatch_source_set_event_handler_f * * @param source * 要修改的“diapatch source”。 * * @param handler * 定义一个“cancle handler block”提交到“dispatch source”的指定目标queue。 */
 void dispatch_source_set_event_handler_f(dispatch_source_t source, dispatch_function_t _Nullable handler);
⑧ dispatch_source_set_cancle_handler 与 dispatch_source_set_cancle_handler_f

对指定的dispatch source设置cancle handler,用来响应dispatch source的取消。

/* @function dispatch_source_set_cancle_handler * * @discussion * 当调用“dispatch_source_cancle()”时,一旦系统释放了全部的“dispatch_source”下的“handle(句柄)”引用。 * 而且“events handler”已经被执行完毕,那么就会在指定目标queue上触发“cancle handler”。 * * 若是“dispatch_source”监听的是文件描述符和mach port,那么就须要使用“cancle handler”。 * 目的是安全的关闭文件描述符和销毁mach port。 * * 若是在触发“cancle handler”以前,文件描述符和mach port就已经被关闭和销毁,就有可能致使竞争条件。 * * Race condition竞争条件是指多个进程或线程并发访问和操做同一数据且执行结果与访问的特定顺序有关的对象。 * 即线程和进程之间访问数据的前后顺序决定了数据修改的结果。 * * 若是新文件的文件描述符被初始化和当前文件的文件描述符是同样的,当“dispatch source”的“events handler”仍在运行的时候。 * “events handler”有可能会在错误的文件描述符中read/write数据。 * * @param source * 要修改的“diapatch source”。 * * @param handler * 定义一个“cancle handler block”提交到“dispatch source”的指定目标queue。 */
void dispatch_source_set_cancle_handler(dispatch_source_t source, dispatch_block_t _Nullable handler);


/* @function dispatch_source_set_cancle_handler_f * * @param source * 要修改的“dispatch source”。 * * @param handler * 定义一个“cancle handler block”提交到“dispatch source”的指定目标queue。 */
void dispatch_source_set_cancle_handler_f(dispatch_source_t source, dispatch_function_t _Nullable handler);
⑨ dispatch_source_set_registration_handler

给指定的dispatch source设置一个registration handler。

/* @function dispatch_source_set_registration_handler * * @discussion * 若是指定了“registration handler”,会在相应的“kevents()”被注册到系统时, * 在“dispatch source”调用“dispatch_resume()”以前会被提交到指定的目标queue。 * 若是“dispatch source”已经被注册了,再来添加“registration handler”,这个“registration handler”会被当即执行。 * * @param source * 要被修改的“dispatch source”。 * * @param handler * 定义一个“registration handler”提交到dispatch source指定的queue。 */

void dispatch_source_set_registration_handler(dispatch_source_t source, 		dispatch_block_t _Nullable handler);


/* @function dispatch_source_set_registration_handler_f * * @param source * 要被修改的“dispatch source”。 * * @param handler * 定义一个“registration handler”提交到dispatch source指定的queue。 */
void dispatch_source_set_registration_handler_f(dispatch_source_t source, dispatch_function_t _Nullable handler);
⑩ dispatch_source_handler函数

dispatch_source监听事件产生触发的handler。