以用户的视角看资源管理器
我们早就看到用户所要的是什么东西的暗示了。用户需要的是使用标准POSIX函数的,基于文件描述符的接口。
实际上,在表面下面还是有很多事情值得关注的。
例如,用户如何连接到合适的资源管理器?在联合文件系统下会发生什么事?对于目录是如何处理的?
查找服务器
用户做的第一件事就是使用open()函数来获取文件描述符。(需要说明的是,当用户使用高级函数fopen()的话,最终也会回到这里,因为fopen()最终还是调用open()函数)
在C库函数的open()实现中,首先是一个消息被创建,之后将这个消息发送给进程管理器。进程管理器负责维护路径名空间的信息。这个信息包括了一个树形结构,这个树形结构包括了路径名以及节点描述符、进程ID、通道ID以及它们之间的关联。
假设用户像下面这样调用了open()函数:
fd = open ("/dev/ser1", O_WRONLY);
在用户的C库函数的open()实现中,一个消息被创建并发送给进程管理器。这个消息说:我要打开/dev/ser1,我该和谁联系?
进程管理器接收到请求,并查看其树形结构来查找是否有吻合的路径名。当然了,路径名”/dev/ser1”吻合请求,进程管理器就可以回复用户:我找到/dev/ser1了。它是由节点描述符0、进程ID44、通道ID1、句柄1所处理的。向它们发送你的请求吧!
请注意,我们现在还在用户的open()代码中。
之后,open()函数创建了另外一个消息,以及到特定节点描述符、进程ID、通道ID的连接,并将这个句柄置于消息之中。这条消息才真正是“连接”消息——这条消息被用户的open()库函数使用来建立到资源管理器的连接(如下图中的第三步所示)。当资源管理器获取了连接消息后,会查看这条消息并验证其有效性。例如,你可能会试着对一个只读的文件系统完成写操作的连接,这样的话你就会获得一个错误消息(这种情况下,是EROFS错误)。在我们的例子中,串口资源管理器查看请求(我们是设定为O_WRONLY,对于串口来说完全合适),返回的回复就是EOK(下图中的第四步所示)。
最终,用户的open()函数返回一个有效的文件描述符。
事实上,这个文件描述符就是我们刚才用来向资源管理器发送连接消息的连接ID。如果资源管理器没有给我们返回一个EOK信号,那么我们就要将错误代码返回给用户(通过errno参数并给open()函数返回-1)。(有必要注意的是,资源管理器对一个名称解析请求可以返回多个资源管理器的节点ID、进程ID以及通道ID。在这种情况下,用户将逐一的尝试连接这些资源管理器直到有一个成功为止,返回不是ENOSYS、ENOENT或EROFS的错误码;或者是用户用完了整个列表,这样的话open()函数就执行失败。)
查找进程管理器
现在我们知道寻找特定资源管理器的基本步骤了,现在我们需要解决的神秘问题就是“我们一开始怎样找到进程管理器?”。实际上,它是很简单的。根据定义,进程管理器的节点描述符为0(表示为当前节点)、进程ID为1、通道ID为1。所以,进程管理器的ND/PID/CHID组合一直是0/1/1。
目录处理
上面我们用到的例子是一个串口资源管理器。我们的假设是:“现在我们需要完全匹配”。这个假设只有一半是真的——因为我们将要谈到的目录匹配是要完全匹配路径名中的一个组成而不是匹配整个路径名。
假设我们有一段代码如下:
fp = fopen ("/etc/passwd", "r");
由于fopen()最终会调用open()函数,所以我们用open()来请求这个路径名/etc/passwd。但是在框图中没有这个路径名:
我们可以看到,fs-qnx4已经将其关联的ND/PID/CHID注册为路径名“/.”。尽管在框图中没有体现,fs-qnx4已经将其自身注册为目录资源管理器,它告诉进程管理器它将负责“/”及其子目录。这是其他的“设备”资源管理器(例如串口资源管理器)所不做的。通过设置“目录”标志,fs-qnx4就能够处理”/etc/passwd”的请求了,因为请求的第一部分“/”是一个匹配组成。
如果我们使用如下的语句会如何呢?
fd = open ("/dev/ser1/9600.8.1.n", O_WRONLY);
由于串口资源管理器没有那个目录标志,进程管理器就会查看它并会说“对不起,这个路径名/dev/ser1不是一个目录,我得让这个请求失败。”这个请求就会立即失败并且进程管理器也不会返回ND/PID/CHID数据来让open()函数尝试打开。
联合文件系统
让我们仔细看看我们上面使用的框图。
我们可以看到fs-qnx4以及进程管理器是如何都注册它们自己来负责”/”?这是没问题的,也不需要我们担心。实际上,在有些时候这还是个好主意。让我们来设想一下这些情况。
假设你的网络连接很慢,并且你还在其上装配了一个网络文件系统。你可能发现你会经常使用某几个文件并且你希望它们能够神奇的“缓存”在你的系统上,不过网络文件系统的设计者没有为你提供这么做的方法。最后,你自己写了一个缓存文件系统(fs-cache)置于中各网络文件系统之上。在用户的角度,它是这个样子的:
fs-nfs(网络文件系统)和你的缓存文件系统(fs-cache)都将自己注册了相同的前缀“/nfs”。像我们前面所说的,在Neutrino系统下,这是没有问题、正常、合法的。
假设系统启动后,你的缓存文件系统中是空的。一个用户的程序试着打开一个文件,假设是/nfs/home/rk/abc.txt。你的缓存文件系统是在网络文件系统的前端。
这时,用户的open()代码会做的常见几步是:
- 向进程管理器发送消息“我该向谁来访问文件名/nfs/home/rk/abc.txt?”
- 来自进程管理器的反应是“先联络fs-cache,再联络fs-nfs”。
可以注意到进程管理器返回了两组ND/PID/CHID/handle;一个是fs-cache的,一个是fs-nfs的。这是很关键的。
之后,用户的open()继续:
- 向fs-cache发消息“我要打开文件/nfs/home/rk/abc.txt来执行读操作”
- fs-cache的回复是“抱歉,俺从未听说过这个文件”
这时,用户的open()函数在和fs-cache互动时有些运气不佳,因为这个文件不存在。不过open()函数知道它有一个有两个ND/PID/CHID/handle数组的列表,它就会尝试第二个:
- 向fs-nfs发消息“我要打开文件/nfs/home/rk/abc.txt来执行读操作”
- fs-nfs的回复是“没问题”
现在open()函数的返回信号是EOK(表示正常),返回值是文件描述符。用户之后就与fs-nfs资源管理器开始后续的互动。
我们唯一一次“解析”到资源管理器是在open()函数执行的时候。这就是说一旦我们成功的打开特定的资源管理器,我们就会继续使用这个资源管理器来执行后续的文件描述符调用。
那么我们的fs-cache缓存文件系统是如何起作用的呢?好的,假设最终用户已经完成了对文件的读取(他们已经将这个文件载入到文本编辑器中)。现在,他们要写出这个文件。同样的执行步骤会发生,而且还有些有趣的转折:
- 发送消息到进程管理器:“和谁讨论/nfs/home/rk/abc.txt?”
- 进程管理器反馈:“先和fs-cache讨论,再和fs-nfs讨论”
- 发送消息到fs-cache:“我要打开文件/nfs/home/rk/abc.txt执行写操作”
- fs-cache反馈“没问题”
可以注意到,这次在第三步,我们打开文件来执行写操作而不是像上次那样执行读操作。不过也不用吃惊,这次fs-cache允许这个操作(第四步)。
更有趣的是,看看下一次我们读取这个文件会发生什么事:
- 发送消息到进程管理器:“和谁来讨论文件名/nfs/home/rk/abc.txt?”
- 进程管理器回复:“先和fs-cache讨论,再和fs-nfs讨论”
- 发消息到fs-cache:“我要打开文件/nfs/home/rk/abc.txt来执行读操作”
- fs-cache回复:“没问题”
可以确定,缓存文件系统这次处理读文件的请求了。
现在,我们忽略了几个细节,不过这些细节不影响基础的构思。显然,缓存文件系统需要做些事情来把数据通过网络发送给真实的存储媒体。它也应该有办法来核实它在将文件发回给用户之前没有人修改这个文件,以免用户得到失效的数据。这个缓存文件系统应该能够自己处理第一个读文件的请求,在第一次读的时候将文件从网络文件系统载入缓存中,等等。
用户总结
我们已经讨论了用户视角的对资源管理器的理解。下面是需要记住的几个关键点:
- 用户通常通过open()或fopen()函数来触发与资源管理器的通讯;
- 一旦用户的请求被解析到某特定资源管理器,我们就不再修改该资源管理器;
- 用户活动进程的后续消息全部基于文件描述符;
- 当用户关闭文件描述符或流的时候,活动进程也就结束了;
- 所有用户的基于文件描述符的函数调用都被翻译成消息。
Related posts:
近期评论