回放机制

最近把《深入理解计算机系统》又看了一遍,除了复习了下编译链接和存储系统的知识外,还注意到一个很有意思的点:一个通用的回放机制。

系统底层的回放机制:换页


计算机处于平衡读写速度和存储容量的目的,把存储划分为类似金字塔的各个层次,每一层都可以看作是对下层的缓存,这样,存储系统利用程序的时间和空间局部性,在能提供足够的容量的前提下也能保持高速的读写速度。

其中,操作系统会把内存和磁盘看做一个整体:虚拟内存。用户空间下的内存读写不会直接操作物理内存,而是通过虚拟内存来管理的。虚拟内存会根据需要在物理内存和磁盘中做不同的操作。

另外,出于节省物理内存以便同时运行多个任务的目的,操作系统在加载程序创建进程的时候不会把整个程序一次性都加载到物理内存中,而是只加载当前需要的一部分。于是,当需要读取的数据还没加载到物理内存中,操作系统就需要提供一套机制来尽快把需要的数据从磁盘加载到物理内存中。

这套机制就叫做换页(paging),而需要读取的数据还没加载到物理内存这种情况就叫做缺页。

当发生缺页时,也就是CPU发送的内存读写指令发现要读写的内存地址不在物理内存中,会出发一个异常而进入一个内核的异常处理流程:系统会把暂时不需要的物理内存中的数据存入磁盘,把目的内存地址所在的页(操作系统一般把大小为4K的内存数据划分为一页)加载进内存,同时会重新执行之前CPU发送的内存读写指令,这时需要的内存数据就已经在物理内存中了,同时相邻的内存数据也可以直接读取了。

“重新执行执行之前CPU发送的内存读写指令”。同样,OC也有类似的回放机制。

OC runtime的回放机制:消息发送

OC 的runtime是这门语言的精髓,各种特性都离不开runtime。其中,runtime中的消息发送也有类似的回放机制。

当执行

@interface EZObject : NSObject
@end

@implementation EZObject
static void lightup_imp(id self, SEL sel) {
    NSLog(@"it's on fire!");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if ([NSStringFromSelector(sel) isEqualToString:@"lightup"]) {
        class_addMethod([self class], sel, (IMP)lightup_imp, "v@:");
    }

    return true;
}

@end

EZObject *ez_instance = [[EZObject alloc] init];
SEL lightup_sel = NSSelectorFromString(@"lightup");
[ez_instance performSelector:lightup_sel];

时,无论是在EZObject还是NSObject,无论是类的cache还是method_list中都找不到lightup的实现,OC会调用EZObject的resolveInstanceMethod方法,来再次确认是否可以找出该方法,于是这里也就提供了一个机会让我们可以利用runtime的class_addMethod来创建一个OC的方法,创建好之后告诉OC现在有了light方法,于是OC 再次向ez_instance发送lightup消息,这次就可以成功找到并调用该方法了。

具体的实现由于NSObject所在的Foundation框架并没有开源,并不能确定,但参考cocoa的开源实现 GNUStep ,可以有一些收获。

在GSFFIInvocation.m中有这么一段代码:

/**
 * Runtime hook used to provide message redirections with libobjc2.
 * If lookup fails but this function returns non-nil then the lookup
 * will be retried with the returned value.
 *
 * Note: Every message sent by this function MUST be understood by the
 * receiver.  If this is not the case then there is a potential for infinite
 * recursion.  
 */
static id gs_objc_proxy_lookup(id receiver, SEL op) {
    id cls = object_getClass(receiver);
    BOOL resolved = NO;

    /* Note that __GNU_LIBOBJC__ implements +resolveClassMethod: and
       +resolveInstanceMethod: directly in the runtime instead.  */

    /* Let the class try to add a method for this thing. */
    if (class_isMetaClass(cls)) {
        if (class_respondsToSelector(cls, @selector(resolveClassMethod: ))) {
            resolved = [receiver resolveClassMethod: op];
        }
    } else {
        if (class_respondsToSelector(object_getClass(cls),
                @selector(resolveInstanceMethod: ))) {
            resolved = [cls resolveInstanceMethod: op];
        }
    }
    if (resolved) {
        return receiver;
    }
    if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector: ))) {
        return [receiver forwardingTargetForSelector: op];
    }
    return nil;
}

gs_objc_proxy_lookup函数就是完成方法查询的工作。大致意思就是如果查询失败但返回的是一个非nil对象,那么就会在这个非nil对象上重试查询。这样,就完成了消息发送时的回放机制。

总结

应用层的很多东西都是从操作系统学习过来的,比如类似kafka和rabbitmq等消息系统,都受到了操作系统的事件通知系统的启发,比如linux的inotify,毕竟普通应用要解决的问题基本上操作系统都已经解决过了,并且经过长时间的优化改进,架构设计都已经足够成熟和优雅,因此,了解底层的操作系统对解决上层应用碰到的问题还是很有帮助的。

坚持原创技术分享,您的支持将鼓励我继续创作!