据MathWorks所述,在Matlab 2018之后就允许普通用户从官网下载安装器,制作离线安装包。然而,官方下载器又做的非常脑瘫,只支持单线程顺序下载,并且还没有暂停功能。

然而这些都是题外话,好不容易用100M的校园网带宽挂完了下载之后,安装出了幺蛾子:一打开,先是弹出一个错误界面,然后进入安装器界面后给出模棱两可的错误信息:There was an error communicating with the backend services.。起初我以为是某墙的问题,后来发现断网也无法避免。上网找了半天,没几个和Matlab安装有关的报错信息。最后在官方论坛某帖子的回复里看到了一小段回复(链接):

When I received this error, it was not due to proxy settings or firewall settings. Rather, when I downloaded the installer, the download location was on a remote drive (a windows remote profile DFS location). I copied the setup file to the local drive, and it ran fine.

大意是说,下载的Matlab安装程序不能放在外部文件系统上,包括可移动设备和网络驱动器等。而我恰好为了做备份,直接把Matlab下载到了外置的移动硬盘上,然后直接从移动硬盘上启动了安装程序,从而导致了该问题;而回复者通过把安装程序拷回内置磁盘的方式解决了问题。但是动辄近20GB的完整安装文件来回移动,可以说是duck不必。在Windows下,我们可以尝试活用mklink,把外置磁盘中的文件夹软链接至内置磁盘中,让Matlab以为它是从内置磁盘读取的数据。

事实证明这个方法是有效的。从内置磁盘的软连接中重新启动Matlab安装程序,启动界面正常了,报错也消失了,问题解决。

一开始看内存安全的时候,了解到从电路上窃取信息这种操作是真实存在的,专业术语称作Bus Snooping总线嗅探。然后当时就理所当然地认为,嗅探属于网络攻击的一种,应该尽可能防范。

然而gem5又给我上了一课(没错还是它)。在阅读代码的时候,看到src/mem/packet.hh的注释里提到了Snoop Flags。这是个啥?难道gem5还支持安全攻击建模的,之前咋没听说过?

查阅资料后,咕果搜索结果中出现了缓存一致性这个关键词。进一步了解后发现,嗅探是当今多核时代下,一种朴素而常见的、确保缓存一致性的通信手段。

根据英文wiki,对于内存来说,有两种常见的内存一致性的级别:

一是Cache Coherent缓存一致。在这种一致性级别下,要求对于单一内存地址的数据,满足程序所有线程的执行顺序(反过来想,我们是否可以理解为,某些内存地址的数据操作是不需要遵循全局顺序的,比如只有单个线程使用的私有内存区块?)。另一种定义是,如果对于同一内存地址的写操作是顺序排列的(即同一时刻只有一个写操作),那么该系统具有缓存一致性。

另一个则是Sequentially Cosistent顺序一致。在这种一致性级别下,要求程序的整个内存区块中的内存操作都必须满足所有线程的总体执行顺序。

然后看到了一篇介绍缓存一致性的文章(原文/译文),了解到了在MESI缓存一致性协议中,是如何通过总线嗅探来实现缓存区块的状态转移的。

MESI协议的名称由协议本身定义状态机的四个状态首字母缩写组成,其中MModified、SShared、IInvalid三个状态分别对应单核系统下缓存的三种状态:脏(dirty)、干净(clean)和无效(invalid)。MESI遵循了缓存一致性的原则,对于同一内存地址,同一时刻只允许一个线程具有写的权利(处于M状态)。

由于要尽可能地提升多核系统的工作效率,经典设计方案是为每个核分配一块独占的缓存空间,而这也是导致传统缓存状态机出现问题的关键所在:若是继续使用单核的强同步策略(写入即同步)的话,同步缓存的操作将大幅降低处理器运行效率;不使用同步策略的话,则会带来经典的写前读/写后读问题。

MESI协议则可以视作一种弱同步策略,仅在特殊事件发生的时候执行同步操作。这个特殊事件直接映射成了MESI中的第四个状态EExclusive,也就是独占状态。MESI规定,只有在进入E状态后,才能进入M状态。在MESI中,需要嗅探的内容就是进入这个E状态的内存操作。

关于MESI协议的具体内容可以参见wiki,这里就懒得写了

翻译自 http://iram.cs.berkeley.edu/kozyraki/project/ee241/report/crossbar.html

The memory crossbar connects the multiple Load/Store units of the processor to the multiple memory sections. The L/S units are capable of issuing either a load or a store on every cycle, and as a result, the memory sections should, ideally, be capable of processing these requests at the same rate. In order words, if there are n L/S units, each issuing one memory accesses per cycle, then the crossbar must have a peak bandwidth of at least n words per cycle.

内存交叉开关是一种用于将多个处理器(内存)读写单元与多个内存区块相连接的结构。由于单个(内存)读写单元可在单个时钟周期内完成一次读或写操作,我们可以认为,在理想状态下,被连接的(单个)内存区块也应该具有相同的请求处理速度(即每时钟周期处理一个读或写请求)。也可以理解为,如果(在使用了内存交叉开关的内存系统中)CPU有n个(内存)读写单元,每个单元每时钟周期可完成一次读或写请求的话,那么在该存储交叉开关处至少有等价于每周期n次读写请求的最大带宽。

largexbar.gif

上图是该文章给出的内存交叉开关一种实现的图例。图中上侧为内存读写单元,下侧为内存模块,中部的横线表示总线,一条线就是一根完整的总线;这里有每种四根则表示有四组相同的总线,实心点表示内存读写单元,叉则表示开关。由于总线只有单一的所有权,一个周期内则只能供一个内存读写单元和一个内存模块使用,也正是上文最大n倍单总线吞吐量的原理。

还是神奇的gem5。

这次是在学习底层实现的时候,看到的船新POSIX API:

    if (sharedBackstore.empty()) {
        shm_fd = -1;
        map_flags =  MAP_ANON | MAP_PRIVATE;
    } else {
        DPRINTF(AddrRanges, "Sharing backing store as %s\n",
                sharedBackstore.c_str());
        shm_fd = shm_open(sharedBackstore.c_str(), O_CREAT | O_RDWR, 0666);
        if (shm_fd == -1)
               panic("Shared memory failed");
        if (ftruncate(shm_fd, range.size()))
               panic("Setting size of shared memory failed");
        map_flags = MAP_SHARED;
    }

    ...

    uint8_t* pmem = (uint8_t*) mmap(NULL, range.size(),
                                    PROT_READ | PROT_WRITE,
                                    map_flags, shm_fd, 0);

由于这块代码是从gem5中内存模块复制出来的,其功能是配置内存模块维护被模拟系统数据所需的储存空间(初始化),可以预见的是单词分配的内存空间大小至少是MB,甚至GB级别的。第一眼看去有点眼花,还在想Linux的shared memory什么时候能够配置GB级别的共享内存块了?仔细一看才发现,这里用的是shm_open而不是shmget,只是名字长得像,功能有很大差别= =,前者在fnctl.h,后者在shm.h中定义。(P.S. 在APUE第三版上翻了老半天都没找到,我觉得应该不是我的问题

比较一下shmgetshm_open的函数签名:

int shmget(key_t key, size_t size, int shmflg);
int shm_open(const char *name, int oflag, mode_t mode);

不同于shmget在创建共享内存块的时候需要声明块大小,且存在比较小气的最大限制(Linux每块默认不大于4KB,MacOS和Solaris还是比较大气,大了几个数量级),shm_open的语法更偏向于open,创建的是一个默认大小为0的空文件,关于文件大小、文件内容需要开发者后续调用其它syscall来修改。

那么shm_open实际上做了什么呢?实际上是创建了一个基于内存的文件描述符。根据man7上的文档介绍,Linux上的实现方法是建立了一个基于内存的tmpfs,并挂载到/dev/shm目录下。调用shm_open的时候,会对应地在目录下用文件的形式进行相应的操作。

如上文所述,shm_open以后,文件的大小为0,用常规方法往文件写入数据并不能如我们所愿,将文件“撑”大。这时需要使用ftruncate系统调用解决该问题,由操作系统完成对文件大小的调整。

由于shm_open系统调用与进程无关,在不同程序中可以使用相同的操作获取同一块共享内存,达到无关进程内存共享的目的。这与文件系统类似,第一次调用建立文件,后面的重复调用只需要修改文件引用值即可。由shm_open创建的文件描述符多与mmap结合运用,直接将fd用于mmap中,其余按照需要配置即可,本文开头的代码段给出了较好的示范。

当一个进程不再需要使用这段共享内存的时候,使用shm_unlink函数,把获得的fd作为参数传过去即可。当最后一个引用关闭之后,共享内存对象由系统销毁。

这里顺便说一下上面的代码段中,mmap使用的一个小细节。当满足条件sharedBackstore.empty() == false的时候,fd设置为-1之后,直接执行了mmap操作。-1显然是一个无效的文件描述符,但是为什么程序还能正常运行?

在StackOverflow上搜索后,万能网友的给出了答案:此时的fd-1是允许的,而这是与mmap的参数map_flagsMAP_ANON(或MAP_ANONYMOUS)这个flag共用时的一个特殊情况。在声明了MAP_ANON/MAP_ANONYMOUS的情况下,该共享内存块仅对当前进程及其子进程有效,其他进程无法访问。某些POSIX实现要求这种情况下,fd一定要传-1,而这也只是特殊情况,并不代表在fd=-1的情况下mmap具有特殊的逻辑。