OSX/iOS中多路I/O复用总结

在OSX/iOS中IO多路复用有两个选择:select和kqueue,最近在尝试优化socket改进通信效率,所以总结一下两种模型的用法。
1.select
select是socket编程中非常重要的一个函数,并且也是兼容性最好的一种模型,在unix、linux、windows都有对应的实现,其函数原型是:

nfds:最大的文件描述符加1;
readfds:用于检查可读性的描述符集合,同时也是可读描述符的结果返回;
writefds:用于检查可写性的描述符集合,同时也是可写描述符的结果返回;
errorfds:用于检查异常的描述符集合,同时也是异常描述符的结果返回;
timeout:一个指向timeval结构的指针,用于决定select等待I/O的最长时间,它可以使select处于三种状态:
(1)第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
(2)第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
(3)第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
函数返回值:
负值:select错误
正值:某些文件可读写或出错
零值:等待超时,没有可读写或错误的文件

2.kqueue
kqueue是FreeBSD上的一种的多路复用机制,所以刚好能在OSX/iOS中使用。它是针对传统的select处理大量的文件描述符性能较低效而开发出来的。注册一批描述符到kqueue以后,当其中的描述符状态发生变化时,kqueue将一次性通知应用程序哪些描述符可读、可写或出错了.
kqueue模型最主要的函数就是kevent,它与select类似,提供向内核注册/反注册/修改事件和返回就绪事件或错误事件,函数原型为:

kq:由kqueue()返回的一个内核事件队列标识;
changelist:注册/反注册事件的列表;
nchanges:changelist的个数;
eventlist:用于返回有事件发生的列表;
nevents:传入的eventlist的最大长度;
timeout:一个指向timeval结构的指针,指定超时时间;
函数返回值与select类似。

另外比较重要的就是struct kevent这个结构体了,它既是注册、反注册、修改事件的载体,也是事件返回的载体,它的原型为:

由于kqueue不仅仅用于socket,还可用于如文件状态、信号、进程等用途,以下仅仅当在网络编程时这些元素的值及含义:
ident:事件的id,实际应用中,一般设置为文件描述符;
filter:指定你希望内核用于ident成员的过滤器,我们可以指定EVFILT_READ(读状态)和EVFILT_READ(写状态);
flags:告诉内核应当对该事件在队列做何处理,我们可以指定EV_ADD(注册)、EV_DELETE(删除)、EV_ENABLE(开启,默认)、EV_DISABLE(停用),当flags用于eventlist返回事件时,可能包含EV_ERROR的掩码表示描述符错误事情;
fflags:用于指定你想让内核使用的特定于过滤器的标志;
data:用于保存任何特定于过滤器的数据,当filter指定为EVFILT_READ或EVFILT_READ时,data表示可读或可写的数据长度,当事件的描述符出错时,data表示错误代码;
udata:data成员并不由kqueue使用,kqueue会把它的值不加修改地透传,用于类似于上下文。

3.总结
select的缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024(不同平台对FD_SETSIZE设定不一样)
(4)select接口使用并不灵活,无法分步提交集合,也无法将提交和查询分步,必须在一次调用中完成
(4)fd_set不能逆向转换为fd,需要再单独维护一份描述符的列表
而kqueue刚好能弥补这些缺陷,但却无法在其它平台使用

关于两种模型的实现示例下载:
SocketServer_select.zip
SocketServer_kqueue.zip

利用ARC解决遗忘unlock的毛病

我们在使用lock的时候会有这样的情况:因为某方法内部逻辑较为复杂,会有很多地方return,稍不留神都会漏掉一次unlock的操作,在查看一些C++源码时发现一种比较优雅的设计,在某个方法或一个代码块需要锁时,不直接对锁进行操作,而是通过创建一个局部对象来管理这个锁,我们将这个对象称之为”哨兵”,该对象在构造函数中执行lock操作,而在析构函数中执行unlock,这样当我们需要加锁时只需要创建这个对象,当函数return后或代码块结束时,因为局部对象在超出作用域后释放,便自动完成了unlock的动作。
而在Objective-C中我们不能创建局部对象,但利用ARC的特性却也可以达到超出作用域自动释放的特性。
以下是”哨兵”类的实现:

以下是我们在使用锁的时候:

Objective-C实现的简单逻辑表达式解析

因为项目中会用到远程配置,配置内容会进行很多条件判定,为了让配置更灵活,所以希望最终能将条件满足情况直接进行逻辑表达式运算.
所以就涉及到表达式的解析,开源的表达式解析的库倒不是少,但考虑仅是逻辑表达式的处理,便不想引入庞大的外部库,于是就决定自己来造这个轮子.
简单分析,其实需要处理的表达式一共就包含这7种符号:&&,||,!,(,),YES,NO,并且相互之间的优先级也非常简单.
将解析的过程分解为如下几个步骤:
1.从表达式中按顺序读取并匹配符号;
2.当匹配到的是(,&&,||,!都直接放于栈中,继续步骤1;
3.当匹配到的是YES或NO,就依次从栈中取出前面的符号,如果是!取反后放入栈继续此步骤,如果是&&或||就再取前一个操作数作逻辑运算,并继续重复此步骤,直到栈内没有内容或遇上(便将当前操作结果的数放入栈继续步骤1;
4.当遇上)则取出栈顶的YES或NO,并将前一个(取出扔掉,然后继续执行步骤3.
5.当表达式解析到末尾,栈内便只有栈顶的这个结果.

最终代码实现,这可能不是最优的方案,代码也不是很简洁,但问题算是解决了:

解决NSDistributedLock进程互斥锁的死锁问题(二)

上一篇文章中介绍了采用了文件记录锁来实现更加安全的多进程互斥,它的平台兼容性也非常好,并且我们也采用它实现了NSDistributedLock的所有的方法.
其实在OSX还可以采用文件读写锁来实现更加方便的进程互斥,在fcntl.h中我们可以看到这样的宏定义:

这些宏是同O_RDONLY,O_WRONLY等一样,都是用于打开文件时使用的掩码,也就是说我们可以在open文件的时候加上这些掩码来实现读写互斥,具体的规则就是(适用于多进程或多线程):
只要没有写模式下的加锁,就可以进行读模式下的加锁;
只有读写锁处于不加锁状态时,才能进行写模式下的加锁;

当然我们大多数情况下都只是简单的资源独占的互斥,所以直接采用写模式下的互斥便可,例如当进程A有如下执行了操作:

其它进程再要做同样操作时就会进入阻塞状态,直到进程A中close了文件描述符或进程退出.

因为同样适用于多线程,所以我们可以实现以下阻塞版本的NSDistributedLock:

解决NSDistributedLock进程互斥锁的死锁问题(一)

在MAC下的多进程开发中,NSDistributedLock是一个非常方便的互斥锁解决方案,一般的使用方法:

但在实际使用过程中,当执行到do something时程序退出,程序再次启动之后tryLock就再也不能成功了,陷入死锁状态.这是使用NSDistributedLock时非常隐蔽的风险.其实要解决的问题就是如何在进程退出时会自动释放锁.
在《Unix环境高级编程》中有对record locking(记录锁)的介绍,而这种锁的机制也是基于文件,但它的解锁条件刚好可以解决我们的问题,因为当进程退出时记录锁会自动释放所持有的锁.

fcntl是Unix兼容最好的一种记录锁实现方案,关于它的接口描述在sys/fcntl.h内,函数原型是

该函数用于记录锁时,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。

关于fcntl函数的三种命令描述:
F_GETLK 取得文件锁定的状态。
F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。

用flock实现NSDistributedLock中tryLock:

用flock实现NSDistributedLock中unlock:

但需要注意的是fcntl实现的记录锁是不支持进程内的多线程间的互斥,为了解决这些问题并完整实现NSDistributedLock中的所有方法和实际表现,下面是我封装的QMDistributedLock:
站内下载:QMDistributedLock.zip

Yosemite自定义window的titleView

在OSX系统中对Windows的自定义向来都是一个很麻烦的事情,从事MAC开发的同学应该都深有体会.
在过去,如果我们需要在标题栏中添加自定义的控件,可能通过下面的方式:

不过这样的做法在使用Yosemite的SDK编译时会产生这样的警告:
titleView_error

虽然这个警告并不会造成显示的异常.
我特意测试了一下Apple的兼容性设计,我发现使用10.9的SDK编译就不会产生这样的警告,通过打印window的tree结构发现一个奇怪的现象:不同的SDK编译的程序中的window的组成结构是一样的?后来发现在Info.plist中的DTSDKBuild项与编译的SDK相对应,在运行时会作为环境变量决定系统采取什么样的兼容策略.

回到正题,那如何在Yosemite中添加自定义的元素titleView呢,根据警告内容,NSThemeFrame不能添加未知的View,通过打印NSThemeFrame的子视图可以发现它包含两个视图,一个就是window的contentView,另一个是NSTitleBarContainerView,这个视图是关闭,最小化,缩放等按钮的容量,经试验放在它上面是可行的.

代码跑起来发现添加上去的控件处于半透明的状态,通过查询window的文档,发现有一个titlebarAppearsTransparent的属性,将它置为YES,问题解决.

另外附上一个可以统一设置标题和窗口背影色的window,能兼容不同的系统版本:
CustomWindow.zip

由一个像素引发的”血案”

由于项目中经常需要程序绘制各种奇形怪状的自定义控件,所以NSBezierPath是一个非常不错的选择,偶然的机会发现当设置NSBezierPath的lineWidth为1.0时,绘制出来的效果很不理想,如下图所示:
NSBezierPath1

这当然不是我所想要的效果,线条模糊,宽度似乎也不是我想要的1.0个像素,经过多次尝试,发现当NSBezierPath的x坐标和y坐标均处在0.5的位置时效果就令人满意了:
NSBezierPath2

最初我也不求甚解,知道如何应对便可,但每次代码写到此处时,好奇心便又多了一点儿,虽然根据自己经验已经能猜到大概,但仍然去拜读了一下Apple的文档,结果正如心中所料:当我们用整数定义某路径时,路径的坐标总是在两个像素之间的,而NSBezierPath在绘制线条时,线条宽度刚好平均分配在路径两侧,而当宽度为1或其它奇数时,两侧就会出现半像素的情况,并且在非Retina显示下,每一个需要绘制的像素是与屏幕最终显示的单元是一一对应的,如图:
NSBezierPath3

所以结果就导致原本一个像素的内容最终呈现在了两个像素的显示单元上了,最终造成了显示效果并非我们所想要的.而当宽度为偶数时,就恰好可以避免这样的情况:
NSBezierPath4

而当我们将位置设定在0.5个像素的时候,正好可以在路径两边都占用整数个显示单位,如下图:
NSBezierPath5
问题也就迎刃而解了,所以在平时使用NSBezierPath画边框时,这样的写法可以很容易避开这样问题:

MAC系统状态栏通过插件添加图标MenuExtra Plugin

在开发MAC软件时,为是让用户更方便的使用功能并且不占用过多桌面空间,我们一般都会选择在状态栏添加图标,就如Windows上的任务栏一样.我们通常使用如下的方式的生成状态栏图标:

当然这也是推荐的方式,因为采用的是标准的API,对于需要上架MAS(Mac App Store)的软件来说,这也是唯一的方法.但这样生成的图标优先级低,当左边应用程序的菜单项过多时很容易就被遮挡而不显示,对于不上架MAS的软件来说,我们可以使用以下的Private API:

使用上面的方法便可让图标的优先级仅次于系统的图标,放在其它应用程序图标的最右边,但即便如此仍然不能像系统图标一样,按住command就能随便拖动位置,并且在点击之后也不能像系统图标一样左右自由切换,所以还有一种方式就是采用插件的方式.

系统的状态栏图标由SystemUIServer这个系统进程负责管理,它是通过插件的方式展示每一个系统图标,我们可以在/System/Library/CoreServices/Menu Extras这个目录下面找到所有的系统状态栏图标.而这里面每一个menu扩展名的bundle就是状态栏图标插件.称之为MenuExtra plugin.但是在10.2以后的,MAX OSX就不再允许第三方的MenuExtra plugin,而此时我们就需要一个开源的项目,叫做MenuCracker,它本身也是一个MenuExtra plugin,但是当加载了这个插件之后,便开启了第三方MenuExtra plugin的支持,它的项目托管在sourceforge,地址是:http://sourceforge.net/projects/menucracker/,至于这个项目的稳定性,可以在sourceforge看到它已经有十年的历史了,所以…

当然MAC OSX系统并未开放MenuExtra plugin的开发,Xcode也没有相应的模板,标准的API也没有任何支持.所以如果我们要开发MenuExtra plugin就只能采用Private API,所以需要上架MAS的软件或拒绝使用Private API就可以打住了,下面就是实际动手来开发一个简单的MenuExtra plugin.

1.首先通过class-dump导出Private API的头文件
class-dump对于做MAC OSX开发的童鞋来说应该都不陌生了,这是下载地址:http://stevenygard.com/projects/class-dump/
而我们所需要的头文件都在SystemUIPlugin.framework这个框架下,通过以下的命令便可将需要的头文件都导出来:

2.在Xcode中创建插件的工程
在Xcode中选择Bundle这个模板,然后在Build Settind中将Wrapper Extension项修改为”menu”.
将步骤1中导出的头文件添加到工程中(删除类似这样的#import “NSObject.h”无用的头文件引用,因为可能编译会出错).
在工程的Link Binary With Libraries中引用/System/Library/PrivateFrameworks/SystemUIPlugin.framework这个框架(直接拖进来即可).
创建用于显示图标的View的类,让它继承于NSMenuExtraView,然后让它画上你想要的图形,比如:

创建图标这个对象,也是这个插件的Principal class,让它继承于NSMenuExtra(NSStatusItem的子类),比如我们类名叫THMenuExtra,那么也将工程的info.plist中将Principal class的值设置为THMenuExtra,这是插件创建的入口.
THMenuExtra类需要去重写父类的以下两个方法:

3.插件安装及卸载
如上文所描述的,如果需要让我们的插件跑起来,肯定需要先行安装MenuCracker.menu(双击即可),如果我们有其它外部程序,可以将MenuCracker打包我们的程序之内,并拷贝到用户合适的目标再open它.
完成了上一步骤,我们插件的安装就非常简单,与MenuCracker的安装一样,直接Open即可.
但卸载略麻烦,你可以在插件中将自己unload完成卸载,或者将插件删除,然后结束SystemUIServer进程即可(SystemUIServer会自动重新启动).
插件的更新,如果你修改了插件的内容,即便插件自身已经unload,你双击新的插件,你会发现仍然是旧的版本,那是因为SystemUIServer会自动缓存你的Bundle,所以如果需要更新的话,你依然需要结束SystemUIServer进程才可完成.

以上插件Demo源码请访问:https://github.com/tanhaogg/THExtraMenu

解决MacBookPro在EFI模式下使用Windows8.1声卡驱动问题

最近因为要使用Windows系统,所以索性就在我的MacBookPro上安装了Windows8.1,因为Windows8.1可以支持EFI模式安装,所以各个过程非常方便,但安装之后唯独声卡驱动不了,安装最新BootCamp的驱动也不管用,最后终于找到别人给出的解决方法,转载记录一下,原帖地址:http://www.pcbeta.com/forum.php?mod=viewthread&tid=1501058

MacBookPro支持EFI安装和启动Windows8的消息出一经传出,其启动快速、原生AHCI等诱惑让发烧友们纠结于Bootcamp和EFI之间。相较于苹果推荐的Bootcamp模式,EFI的优势是显而易见的:
Bootcamp方式:

优点:系统装完后安装一下Bootcamp驱动就OK了,什么都不用管。

缺点:对于换了SSD硬盘的用户,没有AHCI就没有Trim,没有快速启动,或有了AHCI(更改MBR程序)有了Trim,但不能睡眠(唤醒死机),还是没有快速启动。
EFI方式:

优点:原生AHCI、快速启动(换了SSD的5秒开机)

缺点:声卡驱动失败。

对于纠结于Bootcamp和EFI的朋友来说肯定是已经搞清楚安装和设置等问题的了。声卡驱动问题,本人曾发文介绍过解决办法,但有些朋友反映遇到了麻烦,经过再次验证,并对步骤简化。下面先说说解决的原理:即在Windows下创建启动文件并(在Mac OS下)将引导文件bootmgfw.ef放入 EFI 分区中的EFI\Microsoft\Boot 目录下,替换微软的引导工具即可,方法如下。请先将你的MBP设置为win8默认启动。
1、Windows8下创建启动文件

Ctrl+X运行命令提示符(管理员),输入:

显示“已成功创建启动文件”,重启电脑进入Mac OS,然后:
2、Mac OS下替换bootmgfw.efi文件:

打开“终端”,输入:

桌面上(或Finder里)出现EFI分区图标,复制附件bootmgfw.efi粘贴至\EFI\Microsoft\Boot替换,重启,OK。
注意:

1 必须默认为windows启动才有效,即使按上述方法解决了声卡驱动问题,如果你修改默认启动为Mac OS或option启动Windows的话,声卡驱动将再次失效。

2 在EFI模式下完成Win8系统安装后,切记要手动安装Bootcamp5,该Bootcamp对MBP的硬件有着极好的支持,无需其他任何驱动程序。

文中的附件下载:bootmgfw.zip

对替身(Alias)文件的操作

对于替身文件的操作,以前我们可以直接使用CarbonCore/Aliases.h这个头里面的方法,比如FSIsAliasFile,FSResolveAlias,FSNewAliasFromPath等方法来判定是否是替身文件,找到替身的原文件和创建替身,但这些方法在10.8系统上都已经DEPRECATED,所以目前最好都使用更高级的API来对替身操作.在更高级的API中我们就会发现,原来Alias会被用在Bookmark之中,所以通过对NSURL中Bookmark相关的方法就可以方便的处理替身文件了,以下就是对应上文中的FS中的三个方法新的实现:

更多的代码片段可以watch我GitHub上的这个Repository:https://github.com/tanhaogg/THCategory