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

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

一、Dispatch Source与内核

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

  • 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 可以知道用户和进程操作了哪些文件,也可以查看系统中网络的使用情况,以及设备的信息。

  • 列出某个进程打开的所有文件
	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的回调机制,迅速激活这个文件描述符(省略掉了遍历文件描述符,而是通过监听回调的的机制)。

二、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。

猜你喜欢

转载自blog.csdn.net/Forever_wj/article/details/108318994