QNX-9—QNX官网文档翻译—Resource Managers—Writing a resource manager

发布时间 2023-07-09 20:52:39作者: Hello-World3

注:本文翻译自
QNX Software Development Platform --> Programming --> Getting Started with QNX Neutrino --> Resource Managers
http://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.getting_started/topic/s1_resmgr_Writing.html


现在我们已经介绍了基础知识——客户端如何看待世界、资源管理器如何看待世界以及库中两个合作层的概述,现在是时候关注细节了。

在本节中,我们将讨论以下主题:

(1) data structures

(2) resource manager structure

(3) POSIX-layer data structure

(4) handler routines

(5) 当然, lots of examples

请记住以下“大局”,其中几乎包含与资源管理器相关的所有内容

图 1. 资源管理器的架构:总体情况。

Data structures

我们需要了解的第一件事是用于控制库操作的数据结构。

Resource manager structure

现在我们已经了解了数据结构,我们可以讨论您提供的部件之间的交互,以实际使您的资源管理器执行某些操作。

POSIX-layer data structures

存在与 POSIX 层支持例程相关的三种数据结构。 就基础层而言,您可以使用任何您想要的数据结构; 它是 POSIX 层,要求您遵守一定的内容和布局。 POSIX 层带来的好处完全值得这个微小的限制。 正如我们稍后将看到的,您也可以将自己的内容添加到结构中

Related concepts
Resource Managers (System Architecture)
Writing a Resource Manager


一、Data structures

我们需要了解的第一件事是用于控制库操作的数据结构。

这些数据结构是:

resmgr_attr_t //control structure

resmgr_connect_funcs_t //connect table

resmgr_io_funcs_t //I/O table

以及库内部使用的一种数据结构:

resmgr_context_t //internal context block

稍后,我们将看到与 POSIX 层库一起使用的 OCB、attributes structure 和 mount structure。


(1) resmgr_attr_t control structure

控制结构(类型 resmgr_attr_t)被传递给 resmgr_attach() 函数,该函数将资源管理器的路径放入通用路径名空间中,并将该路径上的请求绑定到调度句柄。

(2) resmgr_connect_funcs_t connect table

当资源管理器库收到消息时,它会查看消息的类型并查看是否可以对其执行任何操作。 在基础层中,有两个表影响此行为:resmgr_connect_funcs_t 表(包含连接消息处理程序列表)和 resmgr_io_funcs_t 表(包含类似的 I/O 消息处理程序列表)。

(3) resmgr_io_funcs_t I/O table

I/O 表在本质上与上面显示的连接功能表非常相似。

(4) resmgr_context_t internal context block

最后,库的最低层使用一种数据结构来跟踪它需要了解的信息。 您应该将此数据结构的内容视为“只读”(iov 成员除外)。#########


1.1 resmgr_attr_t control structure

控制结构(类型 resmgr_attr_t)被传递给 resmgr_attach() 函数,该函数将资源管理器的路径放入通用路径名空间中,并将该路径上的请求绑定到分发句柄。

控制结构(来自<sys/dispatch.h>)具有以下内容:

typedef struct _resmgr_attr {
    unsigned flags;
    unsigned nparts_max;
    size_t   msg_max_size;
    int      (*other_func) (resmgr_context_t *ctp, void *msg);
    unsigned reserved[4];
} resmgr_attr_t;


The other_func message handler

一般来说,您应该避免使用此成员。 该成员(如果非 NULL)表示一个例程,当库无法识别该消息时,将使用资源管理器库接收到的当前消息来调用该例程。#####

The data structure sizing parameters

这两个参数用于控制消息区域的各种大小。

The flags parameter

该参数向资源管理器库提供附加信息。


1.1.1 The other_func message handler

一般来说,您应该避免使用此成员。 该成员(如果非 NULL)表示一个例程,当库无法识别该消息时,将使用资源管理器库接收到的当前消息来调用该例程。

虽然您可以使用它来实现“私有”或“自定义”消息,但不鼓励这种做法(使用 _IO_DEVCTL 或 _IO_MSG 处理程序,请参见下文)。 如果您希望处理传入的脉冲,我建议您改用 pulse_attach() 函数。

您应该将此成员保留为 NULL 值。


1.1.2 The data structure sizing parameters

这两个参数用于控制消息区域的各种大小。

nparts_max 参数控制资源管理器库上下文块(类型为 resmgr_context_t,见下文)中动态分配的 iov 成员的大小。 如果您从某些处理函数返回多个单部分 IOV,则通常需要调整此成员。 请注意,它对传入消息没有影响 —— 仅用于传出消息。

msg_max_size 参数控制资源管理器库应留出多少缓冲区空间作为消息的接收缓冲区。 资源管理器库会将此值设置为至少与它将接收的最大消息的标头一样大。 这确保了当您的处理函数被调用时,它将传递消息的整个标头。 但请注意,即使 msg_max_size 参数“足够大”,也不保证当前标头之外的数据(如果有)存在于缓冲区中。


1.1.3 The flags parameter

该参数向资源管理器库提供附加信息。

出于我们的目的,我们将只传递 0。您可以在 QNX Neutrino C 库参考中的 resmgr_attach() 函数下阅读有关其他值的信息。


1.2 resmgr_connect_funcs_t connect table

当资源管理器库收到消息时,它会查看消息的类型并查看是否可以对其执行任何操作。 在基础层中,有两个表影响此行为:resmgr_connect_funcs_t 表(包含连接消息处理程序列表)和 resmgr_io_funcs_t 表(包含类似的 I/O 消息处理程序列表)。

当需要填充连接和 I/O 表时,我们建议您使用 iofunc_func_init() 函数来加载带有 POSIX 层默认处理程序例程的表。 然后,如果您需要覆盖特定消息处理程序的某些功能,您只需分配自己的处理程序函数而不是 POSIX 默认例程。 我们将在 “Putting in your own functions.” 部分中看到这一点。 现在,让我们看看连接函数表本身(来自 <sys/resmgr.h>):

typedef struct _resmgr_connect_funcs {
  unsigned nfuncs;

  int (*open) (ctp, io_open_t *msg, handle, void *extra);
  int (*unlink) (ctp, io_unlink_t *msg, handle, void *reserved);
  int (*rename) (ctp, io_rename_t *msg, handle, io_rename_extra_t *extra);
  int (*mknod) (ctp, io_mknod_t *msg, handle, void *reserved);
  int (*readlink) (ctp, io_readlink_t *msg, handle, void *reserved);
  int (*link) (ctp, io_link_t *msg, handle, io_link_extra_t *extra);
  int (*unblock) (ctp, io_pulse_t *msg, handle, void *reserved);
  int (*mount) (ctp, io_mount_t *msg, handle, io_mount_extra_t *extra);

} resmgr_connect_funcs_t;

请注意,我通过省略第一个成员(ctp)的 resmgr_context_t * 类型和第三个成员(handle)的 RESMGR_HANDLE_T * 类型来缩短原型。 例如,open 的完整原型实际上是:

int (*open) (resmgr_context_t *ctp, io_open_t *msg, RESMGR_HANDLE_T *handle, void *extra);

结构的第一个成员 (nfuncs) 指示结构有多大(它包含多少个成员)。 在上面的结构中,它应该包含值“8”,因为有 8 个成员(open 到 mount)。 该成员主要是为了允许 BlackBerry QNX 升级该库而不会对您的代码产生任何不良影响。 例如,假设您编译的值为 8,然后 BlackBerry QNX 将库升级为 9。因为成员只有值 8,所以库可以对自己说:“啊哈! 这个库的用户是在我们只有 8 个函数时编译的,现在我们有 9 个。我将为第九个函数提供一个有用的默认值。” <sys/resmgr.h> 中有一个名为 _RESMGR_CONNECT_NFUNCS 的清单常量,它具有当前编号。 如果手动填写连接函数表,请使用此常量(尽管最好使用 iofunc_func_init())。

请注意,函数原型都共享通用格式。 第一个参数 ctp 是指向 resmgr_context_t 结构的指针。 这是资源管理器库使用的内部上下文块,您应该将其视为只读(除了一个字段,我们将稍后讨论)######。

第二个参数始终是指向消息的指针。 由于表中的函数用于处理不同类型的消息,因此原型与每个函数将处理的消息类型相匹配。

第三个参数是一个称为句柄的 RESMGR_HANDLE_T 结构,它用于标识此消息所针对的设备。 稍后,当我们查看属性结构时,我们也会看到这一点。


为了正确定义 RESMGR_HANDLE_T,请在 <sys/resmgr.h> 之前 #include <sys/iofunc.h>。

最后,最后一个参数要么是“保留”参数,要么是需要一些额外数据的函数的“额外”参数。 在讨论处理函数时,我们将酌情显示额外的参数。


1.3 resmgr_io_funcs_t I/O table

I/O 表在本质上与上面显示的连接功能表非常相似。

这是来自 <sys/resmgr.h> 的内容:

typedef struct _resmgr_io_funcs {
   unsigned   nfuncs;
   int (*read)       (ctp, io_read_t *msg, ocb);
   int (*write)      (ctp, io_write_t *msg, ocb);
   int (*close_ocb)  (ctp, void *reserved, ocb);
   int (*stat)       (ctp, io_stat_t *msg, ocb);
   int (*notify)     (ctp, io_notify_t *msg, ocb);
   int (*devctl)     (ctp, io_devctl_t *msg, ocb);
   int (*unblock)    (ctp, io_pulse_t *msg, ocb);
   int (*pathconf)   (ctp, io_pathconf_t *msg, ocb);
   int (*lseek)      (ctp, io_lseek_t *msg, ocb);
   int (*chmod)      (ctp, io_chmod_t *msg, ocb);
   int (*chown)      (ctp, io_chown_t *msg, ocb);
   int (*utime)      (ctp, io_utime_t *msg, ocb);
   int (*openfd)     (ctp, io_openfd_t *msg, ocb);
   int (*fdinfo)     (ctp, io_fdinfo_t *msg, ocb);
   int (*lock)       (ctp, io_lock_t *msg, ocb);
   int (*space)      (ctp, io_space_t *msg, ocb);
   int (*shutdown)   (ctp, io_shutdown_t *msg, ocb);
   int (*mmap)       (ctp, io_mmap_t *msg, ocb);
   int (*msg)        (ctp, io_msg_t *msg, ocb);
   int (*reserved)   (ctp, void *msg, ocb);
   int (*dup)        (ctp, io_dup_t *msg, ocb);
   int (*close_dup)  (ctp, io_close_t *msg, ocb);
   int (*lock_ocb)   (ctp, void *reserved, ocb);
   int (*unlock_ocb) (ctp, void *reserved, ocb);
   int (*sync)       (ctp, io_sync_t *msg, ocb);
   int (*power)      (ctp, io_power_t *msg, ocb);
   int (*acl)        (ctp, io_acl_t *msg, ocb);
   int (*pause)      (ctp, void *reserved, ocb);
   int (*unpause)    (ctp, io_pulse_t *msg, ocb);
   int (*read64)     (ctp, io_read_t *msg, ocb);
   int (*write64)    (ctp, io_write_t *msg, ocb);
   int (*notify64)   (ctp, io_notify_t *msg, ocb);
   int (*utime64)    (ctp, io_utime_t *msg, ocb);
} resmgr_io_funcs_t;

对于此结构,我还通过删除 ctp 成员 (resmgr_context_t *) 的类型和最后一个成员(ocb,类型为 RESMGR_OCB_T *)来缩短原型。 例如, read 的完整原型实际上是:

int (*read) (resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);

为了正确定义 RESMGR_OCB_T,请在 <sys/resmgr.h> 之前 #include <sys/iofunc.h>。

结构的第一个成员 (nfuncs) 指示结构有多大(它包含多少个成员)。 用于初始化的正确清单常量是 _RESMGR_IO_NFUNCS。

注意I/O表中的参数列表也很有规律。 第一个参数是 ctp,第二个参数是 msg,就像它们在连接表处理程序中一样。

然而,第三个参数不同。 它是一个 ocb,代表“开放上下文块”。 它保存由连接消息处理程序绑定的上下文(例如,作为客户端 open() 调用的结果),并且可供 I/O 函数使用。

如上所述,当需要填充两个表时,我们建议您使用 iofunc_func_init() 函数通过 POSIX 层默认处理程序例程加载表。 然后,如果您需要覆盖特定消息处理程序的某些功能,您只需分配自己的处理程序函数而不是默认例程。 我们将在 “Putting in your own functions.” 部分中看到这一点。


1.4 The resmgr_context_t internal context block

最后,库的最低层使用一种数据结构来跟踪它需要了解的信息。 您应该将此数据结构的内容视为“只读”(iov 成员除外)。

这是数据结构(来自 <sys/resmgr.h>):

typedef struct _resmgr_context {
    int                 rcvid;
    struct _msg_info    info;
    resmgr_iomsgs_t     *msg;
    dispatch_t          *dpp;
    int                 id;
    size_t              msg_max_size;
    long                status;
    size_t              offset;
    size_t              size;
    iov_t               iov[1];
} resmgr_context_t;

与其他数据结构示例一样,我冒昧地删除了保留字段。

我们来看一下内容:

rcvid

来自资源管理器库的 MsgReceivev() 函数调用的接收 ID。 指示您应该回复的人(如果您要自己回复)。

info

包含资源管理器库的接收循环中 MsgReceivev() 返回的信息结构。 对于获取有关客户端的信息非常有用,包括节点描述符、进程 ID、线程 ID 等。 有关更多详细信息,请参阅 MsgReceivev() 的文档。

msg

指向所有可能的消息类型的联合的指针。 这对您来说不是很有用,因为您的每个处理程序函数都会传递适当的联合成员作为其第二个参数。

dpp

指向您首先传入的分发结构的指针。 同样,这对您来说不是很有用,但显然对资源管理器库有用。

id

此消息所针对的安装点的标识符。 当您执行 resmgr_attach() 时,它返回一个小整数 ID。 这个ID就是id成员的值。 请注意,您很可能自己永远不会使用此参数,而是依赖在开放连接函数处理程序中传递给您的属性结构。

msg_max_size

它包含作为 resmgr_attr_t 的 msg_max_size 成员传入的 msg_max_size(提供给 resmgr_attach() 函数),以便大小、偏移量和 msg_max_size 全部包含在一个方便的结构/位置中。

status

这是处理程序函数放置操作结果的位置。 请注意,您应该始终使用宏 _RESMGR_STATUS() 来写入此字段。 例如,如果您正在处理来自 open() 的连接消息,并且您是只读资源管理器,但客户端希望打开您进行写入,您将通过(通常)_RESMGR_STATUS 返回 EROFS errno ( CTP、EROFS)。

offset

当前进入客户端消息缓冲区的字节数。 仅与带有组合消息的 resmgr_msgget() 或 resmgr_msggetv() 一起使用时与基础层库相关(见下文)。

size

这告诉您在传递给处理函数的消息区域中有多少字节是有效的。 这个数字很重要,因为它指示是否需要从客户端读取更多数据(例如,如果资源管理器基础库未读取所有客户端数据),或者是否需要分配存储空间以回复 客户端(例如,回复客户端的 read() 请求)。

iov

I/O 矢量表,您可以在其中写入返回值(如果返回数据)。 例如,当客户端调用 read() 并调用您的读取处理代码时,您可能需要返回数据。 该数据可以在 iov 数组中设置,然后您的读取处理代码可以返回类似 _RESMGR_NPARTS (2) 的内容,以指示(在本示例中)iov[0] 和 iov[1] 都包含要返回给客户端的数据。 请注意,iov 成员被定义为仅具有一个元素。 但是,您还会注意到它位于结构的末尾,很方便。 iov 数组中的实际元素数量由您在设置上述控制结构的 nparts_max 成员时定义(在上面的“resmgr_attr_t 控制结构”部分中)。


二、Resource manager structure

现在我们已经了解了数据结构,我们可以讨论您提供的部件之间的交互,以实际使您的资源管理器执行某些操作。

我们将看看:

The resmgr_attach() function and its parameters

Putting in your own functions

The general flow of a resource manager

Messages that should be connect messages but aren't

Combine messages


(1) The resmgr_attach() function and its parameters

正如您在上面的 /dev/null 示例中看到的,您要做的第一件事就是向进程管理器注册您选择的“挂载点”。###### 这是通过 resmgr_attach() 完成的。

(2) Putting in your own functions

在设计第一个资源管理器时,您很可能希望采用增量设计方法。 编写数千行代码却遇到了根本性的误解,然后不得不做出丑陋的决定,是尝试拼凑(呃,我的意思是“修复”)所有代码,还是废弃它,白手起家。这可能会非常令人沮丧。

(3) The general flow of a resource manager

正如我们在上面的概述部分中提到的,资源管理器的一般流程从客户端的 open() 开始。 这会被转换为连接消息,并最终被资源管理器的打开连接函数处理程序接收。

(4) Messages that should be connect messages but aren't

您可能已经注意到了一个有趣的点。 考虑如下所示的 chown() 客户端原型。

(5) Combine messages

事实证明,组合消息的概念不仅仅用于节省带宽(如上面的 chown() 情况)。 这对于确保操作的原子完成也至关重要。


2.1 The resmgr_attach() function and its parameters

正如您在上面的 /dev/null 示例中看到的,您要做的第一件事就是向进程管理器注册您选择的“挂载点”。 这是通过 resmgr_attach() 完成的。

该函数具有以下原型:

int resmgr_attach (void *dpp,
               resmgr_attr_t *resmgr_attr,
               const char *path,
               enum _file_type file_type,
               unsigned flags,
               const resmgr_connect_funcs_t *connect_funcs,
               const resmgr_io_funcs_t *io_funcs,
               RESMGR_HANDLE_T *handle);

让我们按顺序检查这些参数,看看它们的用途。

dpp

分发句柄。 这使得分发接口可以管理资源管理器接收的消息。

resmgr_attr

控制资源管理器特性,如上所述。

path

您正在注册的安装点。 如果您正在注册一个离散的挂载点(例如 /dev/null 或 /dev/ser1 的情况),则该挂载点必须与客户端完全匹配,并且没有超过该挂载点的路径名组件。 如果您正在注册一个目录挂载点(例如,将网络文件系统挂载为 /nfs),那么匹配也必须精确,并且添加了允许超过挂载点的路径名的功能; 它们被传递到除去挂载点的连接函数。 例如,路径名 /nfs/etc/passwd 将与网络文件系统资源管理器匹配,并且它将获取 etc/passwd 作为路径名的其余部分。

file_type

资源管理器类。 见下文。

flags

用于控制资源管理器行为的附加标志。 这些标志的定义如下。

connect_funcs 和 io_funcs

这些只是您希望绑定到安装点的连接函数和 I/O 函数的列表。

handle

这是一个“可扩展”数据结构(又名“属性结构”),用于标识正在安装的资源。 例如,对于串行端口,您可以通过添加有关串行端口基地址、波特率等的信息来扩展标准 POSIX 层属性结构。请注意,它不必是属性结构 — 如果 您正在提供自己的“开放”处理程序,然后您可以选择以任何您希望的方式解释该字段。 仅当您使用默认的 iofunc_open_default() 处理程序作为“打开”处理程序时,该字段才必须是属性结构。


flags 成员可以包含以下任何标志(如果未指定,则为常量 0):

_RESMGR_FLAG_BEFORE 或 _RESMGR_FLAG_AFTER

这些标志表明您的资源管理器希望放置在(分别)具有相同安装点的其他资源管理器之前或之后。 这两个标志对于联合(覆盖)文件系统很有用。 我们将很快讨论这些标志的相互作用。

_RESMGR_FLAG_DIR

此标志表明您的资源管理器正在接管指定的安装点及以下 — 它实际上是文件系统样式的资源管理器,而不是离散显示的(设备)资源管理器。

_RESMGR_FLAG_OPAQUE

如果设置,则会阻止解析到安装点以下的任何其他管理器(路径管理器除外)。 这有效地消除了路径上的联合。

_RESMGR_FLAG_FTYPEONLY

这可确保仅匹配与传递给 resmgr_attach() 的 file_type 具有相同 _FTYPE_* 的请求。

_RESMGR_FLAG_FTYPEALL

当资源管理器想要捕获所有客户端请求时,使用此标志,即使是那些具有与 file_type 参数中传递给 resmgr_attach() 的请求不同的 _FTYPE_* 规范的请求。 这只能与 _FTYPE_ALL 注册文件类型结合使用。

_RESMGR_FLAG_SELF

允许该资源管理器自言自语。这确实是一个“不要在家里尝试这个,孩子们”的标志,因为允许资源管理器自言自语可能会破坏发送层次结构并导致死锁(如消息传递一章中所讨论的)。

您可以根据需要多次调用 resmgr_attach() 来挂载不同的挂载点。 您还可以从连接或 I/O 函数中调用 resmgr_attach() - 这是一种巧妙的功能,允许您动态“创建”设备。

您的资源管理器在调用 resmgr_attach() 时需要启用某些功能:

(1) PROCMGR_AID_PATHSPACE,将名称添加到路径名空间
(2) PROCMGR_AID_PUBLIC_CHANNEL,创建公共channel(即不设置 _NTO_CHF_PRIVATE)

有关详细信息,请参阅《Neutrino C Library Reference》中的条目 procmgr_ability()。


当您决定安装点并想要创建它时,您需要告诉进程管理器该资源管理器是否可以处理来自任何人的请求,或者是否仅限于处理来自使用特殊标签识别其连接消息的客户端的请求。 例如,考虑 POSIX 消息队列 (mqueue) 驱动程序。 它不会允许(当然也不知道如何处理)来自任何旧客户端的“常规” open() 消息。 它将仅允许来自使用 POSIX mq_open()、mq_receive() 等函数调用的客户端的消息。

为了防止进程管理器允许常规请求到达 mqueue 资源管理器,mqueue 指定 _FTYPE_MQUEUE 作为 file_type 参数。 这意味着当客户端向进程管理器请求名称解析时,进程管理器甚至不会在搜索过程中考虑资源管理器,除非客户端指定它想要与将自己标识为 _FTYPE_MQUEUE 的资源管理器进行对话。

除非您正在做一些非常特殊的事情,否则您将使用 _FTYPE_ANY 的 file_type,这意味着您的资源管理器已准备好处理来自任何人的请求。 有关 _FTYPE_* 清单常量的完整列表,请查看 <sys/ftype.h>。

关于“之前”和“之后”标志,事情变得更有趣了。 您只能指定这些标志之一或常量 0。

让我们看看这是如何工作的。 假设多个资源管理器已按表中给出的顺序启动。 我们还看到他们为 flags 成员传递的标志。 观察他们的位置:

--------------------------------------------------
Resmgr    Flag                  Order
--------------------------------------------------
1         _RESMGR_FLAG_BEFORE   1
2         _RESMGR_FLAG_AFTER    1, 2
3         0                     1, 3, 2
4         _RESMGR_FLAG_BEFORE   1, 4, 3, 2
5         _RESMGR_FLAG_AFTER    1, 4, 3, 5, 2
6         0                     1, 4, 6, 3, 5, 2
--------------------------------------------------

正如您所看到的,第一个实际指定标志的资源管理器始终位于该位置。 (从表中看,1号资源管理器是第一个指定“before”标志的;无论谁注册,资源管理器1总是列表中的第一个。同样,资源管理器2是第一个指定“after”标志的 ; 同样,无论其他人注册,它总是最后一个。)如果未指定标志,则它实际上充当“中间”标志。 当资源管理器 3 以零标志启动时,它被放入中间。 与“之前”和“之后”标志一样,所有“中间”资源管理器都有优先顺序,其中较新的资源管理器被放置在其他现有“中间”资源管理器之前。【这做的有点烂】

然而,实际上,很少有情况会实际挂载多个资源管理器,而在同一挂载点挂载两个以上资源管理器的情况则更少。 这里有一个设计技巧:公开在资源管理器的命令行中设置标志的能力,以便资源管理器的最终用户能够指定,例如, -b 使用“before”标志,并且 - a 使用“after”标志,不指定命令行选项来指示应将零作为标志传递。

请记住,此讨论仅适用于使用相同安装点安装的资源管理器。 使用“before”标志挂载“/nfs”和使用“after”标志挂载“/disk2”不会相互影响; 只有当您安装另一个“/nfs”或“/disk2”时,这些标志(和规则)才会发挥作用。

最后,resmgr_attach() 函数在成功时返回一个小整数句柄,在失败时返回 -1。 然后可以使用该句柄将路径名从进程管理器的内部路径名表中分离出来。


2.2 Putting in your own functions

在设计第一个资源管理器时,您很可能希望采用增量设计方法。编写数千行代码却遇到了根本性的误解,然后不得不做出丑陋的决定,是尝试拼凑(呃,我的意思是“修复”)所有代码,还是废弃它,白手起家。这可能会非常令人沮丧。

建议运行的方法是使用 iofunc_func_init() POSIX 层默认初始化函数,用 POSIX 层默认函数填充连接和 I/O 表。 这意味着您可以像我们上面所做的那样,在几个函数调用中真正编写资源管理器的初始剪切。

您想要首先实现哪个功能实际上取决于您正在编写的资源管理器类型。 如果它是一个文件系统资源管理器,您要在其中接管挂载点及其下面的所有内容,那么您很可能最好从打开连接函数处理程序开始。 (_RESMGR_FLAG_DIR 标志指示路径名应被视为目录。)另一方面,如果它是执行“传统”I/O 操作的离散体现(设备)资源管理器(即,您主要通过客户端调用访问它) 就像 read() 和 write()) 一样,那么最好的起点就是读 I/O 函数处理程序和/或写 I/O 函数处理程序。

第三种可能性是,它是一个离散表现的资源管理器,不执行传统的 I/O 操作,而是依赖 devctl() 或 ioctl() 客户端调用来执行其大部分功能。 在这种情况下,您将从设备控制 I/O 函数处理程序开始。

无论从哪里开始,您都需要确保您的函数以预期的方式被调用。 POSIX 层默认函数的真正酷之处在于它们可以直接放入连接或 I/O 函数表中。 这意味着,如果您只是想获得控制权,请执行 printf() 来表示“我在 io_open 中!”,然后“做任何应该做的事情”,您将度过一段轻松的时光 。 这是接管"打开连接函数处理程序"的资源管理器的一部分:

// forward reference
int io_open (resmgr_context_t *, io_open_t *, RESMGR_HANDLE_T *, void *);

int main ()
{
    // everything as before, in the /dev/null example
    // except after this line:
    iofunc_func_init (_RESMGR_CONNECT_NFUNCS, &cfuncs, _RESMGR_IO_NFUNCS, &ifuncs);

    // add the following to gain control:
    cfuncs.open = io_open;

假设您已经正确构建了开放连接函数处理程序的原型(如代码示例中所示),您可以使用自己的默认处理程序!

int io_open (resmgr_context_t *ctp, io_open_t *msg, RESMGR_HANDLE_T *handle, void *extra)
{
    printf ("I'm here in the io_open!\n");
    return (iofunc_open_default (ctp, msg, handle, extra));
}

通过这种方式,您仍然使用默认的 POSIX 层 iofunc_open_default() 处理程序,但您也获得了执行 printf() 的控制权。

显然,您可以对读 I/O 函数处理程序、写 I/O 函数处理程序和设备控制 I/O 函数处理程序以及具有 POSIX 层默认函数的任何其他函数执行此操作。 事实上,这是一个非常好的主意,因为它向您表明客户端确实正在按预期调用您的资源管理器。


2.3 The general flow of a resource manager

正如我们在上面的概述部分中提到的,资源管理器的一般流程从客户端的 open() 开始。 这会被转换为连接消息,并最终被资源管理器的打开连接函数处理程序接收。

这确实很关键,因为开放连接函数处理程序是资源管理器的“看门人”。 如果该消息导致看门人请求失败,您将不会收到任何 I/O 请求,因为客户端从未获得有效的文件描述符。 相反,如果消息被看门人接受,则客户端现在拥有有效的文件描述符,并且您应该期望获得 I/O 消息。

但开放连接函数处理程序发挥着更大的作用。 它不仅负责验证客户端是否可以打开特定资源,还负责:

(1) 初始化内部库参数

(2) 将上下文块绑定到此请求

(2) 将属性结构绑定到上下文块。

前两个操作是通过基础层函数 resmgr_open_bind() 执行的; 属性结构的绑定是通过简单的赋值完成的。

一旦调用了 open connect 函数处理程序,它就不再存在了。 客户端可能会也可能不会发送 I/O 消息,但无论如何,最终都会使用与 close() 函数相对应的消息来终止“会话”。 请注意,如果客户端意外死亡(例如,受到 SIGSEGV 攻击,或者其运行的节点崩溃),操作系统将合成一条 close() 消息,以便资源管理器可以进行清理。 因此,您一定会收到 close() 消息!


2.4 Messages that should be connect messages but aren't

您可能已经注意到了一个有趣的点。 考虑如下所示的 chown() 客户端原型。

int chown (const char *path, uid_t owner, gid_t group);

请记住,连接消息始终包含路径名,并且要么是一次性消息,要么为进一步的 I/O 消息建立上下文。

那么,为什么客户端的 chown() 函数没有连接消息呢? 事实上,为什么会有 I/O 消息?!? 客户端原型中肯定没有隐含文件描述符!

答案是:“让你的生活更简单!”

想象一下,如果 chown()、chmod()、stat() 等函数需要资源管理器查找路径名,然后执行某种工作。 (顺便说一句,这是在 QNX 4 中实现的方式。)常见的问题是:

(1) 每个函数都必须调用查找例程。

(2) 如果存在这些函数的文件描述符版本,驱动程序必须提供两个单独的入口点; 一种用于路径名版本,另一种用于文件描述符版本。

无论如何,QNX Neutrino 下发生的情况是客户端构造一个组合消息 —— 实际上只是一条包含多个资源管理器消息的消息。 如果没有组合消息,我们可以用这样的东西来模拟 chown() :

int chown (const char *path, uid_t owner, gid_t group)
{
    int fd, sts;

    if ((fd = open (path, O_RDWR)) == -1) {
        return (-1);
    }
    sts = fchown (fd, owner, group);
    close (fd);
    return (sts);
}

其中 fchown() 是 chown() 的基于文件描述符的版本。 这里的问题是,我们现在发出三个函数调用(以及三个单独的消息传递事务),并在客户端产生 open() 和 close() 的开销。

对于组合消息,在 QNX Neutrino 下,一条如下所示的单个消息是由客户端的 chown() 库调用直接构造的:

图 1. 组合消息。

该消息有两部分,一个连接部分(类似于客户端的 open() 生成的内容)和一个 I/O 部分(相当于 fchown() 生成的消息)。 没有 close() 的等效项,因为我们在连接消息的特定选择中暗示了这一点。 我们使用了 _IO_CONNECT_COMBINE_CLOSE 消息,它有效地声明“打开此路径名,使用您获得的文件描述符来处理消息的其余部分,当您运行结束或遇到错误时,关闭文件描述符。”

您编写的资源管理器没有线索表明客户端调用了 chown() 或客户端执行了不同的 open()、后跟 fchown()、后跟 close()。 这一切都被底层库隐藏了。


2.5 Combine messages

事实证明,组合消息的概念不仅仅用于节省带宽(如上面的 chown() 情况)。 这对于确保操作的原子完成也至关重要。

假设客户端进程有两个或多个线程和一个文件描述符。 客户端中的线程之一执行 lseek(),然后执行 read()。 一切都如我们所期望的那样。 如果客户端中的另一个线程在同一个文件描述符上执行相同的操作集,我们就会遇到问题。 由于 lseek() 和 read() 函数彼此不了解,因此第一个线程可能会执行 lseek(),然后被第二个线程抢占。 第二个线程在放弃 CPU 之前先执行 lseek(),然后执行 read()。 问题是,由于两个线程共享相同的文件描述符,因此第一个线程的 lseek() 偏移量现在位于错误的位置 - 它位于第二个线程的 read() 函数给出的位置! 这也是跨进程 dup() 的文件描述符的问题,更不用说网络了。

一个明显的解决方案是将 lseek() 和 read() 函数放在互斥体中 - 当第一个线程获取互斥体时,我们现在知道它具有对文件描述符的独占访问权限。 第二个线程必须等到它能够获取互斥锁,然后才能去修改文件描述符的位置。

不幸的是,如果有人忘记为每个文件描述符操作获取互斥体,则这种“不受保护”的访问可能会导致线程将数据读取或写入到错误的位置。

让我们看一下 C 库调用 readblock()(来自 <unistd.h>):

int readblock (int fd, size_t blksize, unsigned block, int numblks, void *buff);

(writeblock() 函数类似。)

你可以想象 readblock() 的一个相当“简单”的实现:

int readblock (int fd, size_t blksize, unsigned block, int numblks, void *buff)
{
    lseek (fd, blksize * block, SEEK_SET); // get to the block
    read (fd, buff, blksize * numblks);
}

显然,这个实现在多线程环境中没有用。 我们至少必须在调用周围放置一个互斥体:

int readblock (int fd, size_t blksize, unsigned block, int numblks, void *buff)
{
    pthread_mutex_lock (&block_mutex);
    lseek (fd, blksize * block, SEEK_SET); // get to the block
    read (fd, buff, blksize * numblks);
    pthread_mutex_unlock (&block_mutex);
}

(我们假设互斥体已经初始化。)

该代码仍然容易受到“不受保护”的访问; 如果进程中的其他线程对文件描述符执行简单的非互斥 lseek(),我们就会遇到错误。

解决方案是使用组合消息,正如我们上面针对 chown() 函数所讨论的那样。 在这种情况下,readblock() 的 C 库实现将 lseek() 和 read() 操作放入一条消息中,并将其发送到资源管理器:

图 1. readblock() 函数的组合消息。

这样做的原因是消息传递是原子的。从客户端的角度来看,要么整个消息已发送到资源管理器,要么什么都没有发送到资源管理器。 因此,介入的“不受保护”的 lseek() 是无关紧要的 —— 当资源管理器接收到 readblock() 操作时,它会一次性完成。 (显然,不受保护的 lseek() 会受到损害,因为在 readblock() 之后,文件描述符的偏移量与原始 lseek() 放置的位置不同。)

但是资源管理器呢? 它如何确保一次性处理整个 readblock() 操作? 当我们讨论为每个消息组件执行的操作时,我们很快就会看到这一点。


三、POSIX-layer data structures

存在与 POSIX 层支持例程相关的三种数据结构。 就基础层而言,您可以使用任何您想要的数据结构; 它是 POSIX 层,要求您遵守一定的内容和布局。 POSIX 层带来的好处完全值得这个微小的限制。 正如我们稍后将看到的,您也可以将自己的内容添加到结构中。

下图说明了这三种数据结构,显示了一些使用碰巧显示两个设备的资源管理器的客户端:

图 1. 数据结构:总体情况。

数据结构(在<sys/iofunc.h>中定义)是:

iofunc_ocb_t

开放控制块结构:用于跟踪客户端对资源管理器提供的特定服务的访问。 该信息通常基于每次打开。

iofunc_attr_t

属性结构:包含每个资源的信息

iofunc_mount_t

挂载结构:包含每个挂载点的信息

当我们讨论 I/O 和连接表时,您会看到 OCB 和属性结构 - 在 I/O 表中,OCB 结构是最后传递的参数。 属性结构作为连接表函数中的句柄传递(第三个参数)。 挂载结构通常是一个全局结构,并且“手动”绑定到属性结构(在您为资源管理器提供的初始化代码中)。


请务必在 #include <sys/resmgr.h> 之前 #include <sys/iofunc.h>,否则数据结构将无法正确定义。


(1) The iofunc_ocb_t OCB structure

OCB 结构包含用于在每个打开或每个文件描述符的基础上跟踪客户端对资源管理器提供的特定服务的访问的信息。 这意味着当客户端执行 open() 调用并返回文件描述符(而不是错误指示)时,资源管理器将创建一个 OCB 并将其与客户端关联只要客户端打开文件描述符,该 OCB 就会一直存在

(2) The iofunc_attr_t attributes structure

属性结构是 per-resource(例如设备)的数据结构。 您看到标准 iofunc_ocb_t OCB 有一个名为 attr 的成员,它是指向属性结构的指针。 这样做是为了让 OCB 能够访问有关设备的信息。 让我们看一下属性结构(来自<sys/iofunc.h>):

(3) The iofunc_mount_t mount structure

安装结构包含多个属性结构中通用的信息。


3.1 The iofunc_ocb_t OCB structure

OCB 结构包含用于在每个打开或每个文件描述符的基础上跟踪客户端对资源管理器提供的特定服务的访问的信息。 这意味着当客户端执行 open() 调用并返回文件描述符(而不是错误指示)时,资源管理器将创建一个 OCB 并将其与客户端关联。只要客户端打开文件描述符,该 OCB 就会一直存在。

实际上,OCB 和文件描述符是一对匹配的。 每当客户端调用I/O函数时,资源管理器库将自动关联OCB,并将其与消息一起传递给I/O函数表项指定的I/O函数。 这就是为什么 I/O 函数都传递有 ocb 参数。 最后,客户端将关闭文件描述符(通过 close()),这将导致资源管理器将 OCB 与文件描述符和客户端解除关联。

请注意,客户端的 dup() 函数只是增加引用计数。 在这种情况下,仅当引用计数达到零时(即,当调用与 open() 和 dup() 相同数量的 close() 时),OCB 才会与文件描述符和客户端解除关联。

正如您可能怀疑的那样,OCB 包含对每个打开或每个文件描述符都很重要的内容。 要了解此结构中的内容,请参阅 C 库参考中的 iofunc_ocb_t。 如果您希望将附加数据与“普通”OCB 一起存储,请放心,您可以“扩展”OCB。 我们将在 “Advanced topics” 部分讨论这个问题。

3.1.1 The strange case of the offset member

至少可以说,偏移字段很有趣。 根据您设置的预处理器标志,您可能会获得偏移区域的六种(!)可能布局之一。 但不要太担心实现 —— 实际上只有两种情况需要考虑,具体取决于您是否想要支持 64 位偏移量:

(1) yes —— 偏移量成员是 64 位
(2) no(32 位整数)— 偏移成员是低 32 位; 另一个成员 offset_hi 包含高 32 位。

就我们这里的目的而言,除非我们专门讨论 32 位与 64 位,否则我们将假设所有偏移量均为 64 位,类型为 off_t,并且平台知道如何处理 64 位数量。


3.2 The iofunc_attr_t attributes structure

属性结构是每资源(例如设备)的数据结构。您看到标准 iofunc_ocb_t OCB 有一个名为 attr 的成员,它是指向属性结构的指针。 这样做是为了让 OCB 能够访问有关设备的信息。 让我们看一下属性结构(来自<sys/iofunc.h>):

typedef struct _iofunc_attr {
    IOFUNC_MOUNT_T           *mount;
    struct _iofunc_mmap_list *mmap_list;
    struct _iofunc_lock_list *lock_list;
    void                     *acl;
    union {
        void               *lockobj;
        pthread_mutex_t    lock;
    };
    uint32_t                 flags;
    uint16_t                 count;
    uint16_t                 rcount;
    uint16_t                 wcount;
    uint16_t                 rlocks;
    uint16_t                 wlocks;
    SEE_BELOW!!!             nbytes;
    SEE_BELOW!!!             inode;
    uid_t                    uid;
    gid_t                    gid;
    time_t                   mtime;
    time_t                   atime;
    time_t                   ctime;
    mode_t                   mode;
    nlink_t                  nlink;
    dev_t                    rdev;
    unsigned                 mtime_ns;
    unsigned                 atime_ns;
    unsigned                 ctime_ns;
} iofunc_attr_t;

nbytes 和 inode 成员与 OCB 的 offset 成员具有相同的 #ifdef 条件集(请参阅上面的“The strange case of the offset member” )。

请注意,属性结构的某些字段仅对 POSIX 帮助程序有用。

让我们分别看一下这些字段:

mount

指向可选 iofunc_mount_t 安装结构的指针。 它的使用方式与使用从 OCB 到属性结构的指针相同,只是该值可以为 NULL,######## 在这种情况下使用安装结构默认值(请参阅下面的 “The iofunc_mount_t mount structure” )。 如前所述,安装结构通常“手动”绑定到您为资源管理器初始化提供的代码中的属性结构中。

mmap_list

由 POSIX iofunc_mmap_default() 和 iofunc_mmap_default_ext() 在内部使用。

lock_list

由 POSIX iofunc_lock_default() 在内部使用。

acl

访问控制列表。

lockobj 和 lock

(QNX Neutrino 7.1 或更高版本)提供锁定属性结构的方法的联合成员。 为了在资源管理器中支持多线程,您需要锁定属性结构,以便一次只允许一个线程更改它。 属性结构可以递归地锁定。
默认行为是使用锁成员,它是一个内置的锁互斥体。 如果使用 iofunc_attr_init() 来初始化该结构,它将使用静态锁初始值设定项来初始化内置锁互斥锁 (lock)。
当调用某些处理函数(即 IO_*)时,资源管理器层会自动为您锁定属性结构(使用 iofunc_attr_lock())。 可以通过调用 iofunc_attr_lock() 或 iofunc_attr_trylock() 来锁定属性结构; 您可以通过调用 iofunc_attr_unlock() 来解锁它。

如果要使用外部锁对象,请通过将 lockobj 成员指向外部锁对象来覆盖内置锁。 您还需要提供自己的函数来锁定和解锁属性结构,并在 iofunc_mount_t 结构中设置 funcs 结构的适当成员。 有关详细信息,请参阅本章中的“The iofunc_mount_t mount structure”。

flags

包含描述其他属性结构字段的状态的标志。 我们很快就会讨论这些。

count

指示出于任何原因打开此属性结构的 OCB 数量。 例如,如果一个客户端打开一个 OCB 进行读,另一个客户端打开另一个 OCB 进行读/写,并且两个 OCB 都指向该属性结构,则 count 的值将为 2,表示两个客户端都打开了该资源。

rcount

计算读者数。 在给出的 count 示例中,rcount 也将具有值 2,因为两个客户端都打开资源以供读取。

wcount

计算写者家。 在给出的 count 示例中,wcount 的值为 1,因为只有一个客户端打开该资源以供写入。

rlocks

指示对特定资源具有读锁定的 OCB 数量。 如果为零,则表示没有读锁,但可能有写锁。

wlocks

与 rlock 相同,但用于写锁。

nbytes

资源的大小(以字节为单位)。 例如,如果此资源描述了一个特定文件,并且该文件的大小为 7756 字节,则 nbytes 成员将包含数字 7756。

inode

包含文件或资源序列号,每个安装点必须是唯一的。 inode 永远不应该为零,因为传统上零表示文件未在使用中。

uid

该资源所有者的用户 ID。

gid

该资源所有者的组 ID。

mtime

文件修改时间,每当处理客户端 write() 时更新或至少失效。

atime

每当处理返回超过零字节的客户端 read() 时,文件访问时间就会更新或至少失效。

ctime

文件更改时间,每当处理客户端 write()、chown() 或 chmod() 时更新或至少失效。

mode

文件模式。 这些是来自 <sys/stat.h> 的标准 S_* 值,例如 S_IFCHR,或采用八进制表示形式,例如 0664,表示所有者和组的读/写权限,以及其他组的只读权限。

nlink

由客户端的 stat() 函数调用返回的链接到此文件的链接数。

rdev

对于字符特殊设备,该字段由主设备号和次设备号组成(最低有效位置有 10 位次设备号位;接下来的 6 位是主设备号)。 对于其他类型的设备,包含设备号。 (有关更多讨论,请参阅下面的“Of device numbers, inodes, and our friend rdev”。)

mtime_ns、atime_ns 和 ctime_ns

(QNX Neutrino 7.0 或更高版本)POSIX 时间成员 mtime、atime 和 ctime 的纳秒值。
如果针对 64 位体系结构进行编译,或者在针对 32 位体系结构进行编译时在包含 <sys/iofunc.h> 之前定义 IOFUNC_NS_TIMESTAMP_SUPPORT,则会包含这些字段。

与 OCB 一样,您可以使用自己的数据扩展“正常”属性结构。 请参阅 “Advanced topics” 部分。


3.3 The iofunc_mount_t mount structure

安装结构包含多个属性结构中通用的信息。

以下是挂载结构的内容(来自 <sys/iofunc.h>):

typedef struct _iofunc_mount {
    uint32_t       flags;
    uint32_t       conf;
    dev_t          dev;
    int32_t        blocksize;
    iofunc_funcs_t *funcs;
} iofunc_mount_t;

flags 成员至少包含一个标志 IOFUNC_MOUNT_32BIT。 该标志指示 OCB 中的偏移量以及属性结构中的 nbytes 和 inode 是 32 位。 请注意,您可以使用常量 IOFUNC_MOUNT_FLAGS_PRIVATE 中的任何位在 flags 中定义自己的标志。

conf 成员包含以下标志:

IOFUNC_PC_CHOWN_RESTRICTED

指示文件系统是否以“chown-restricted”方式操作,这意味着是否只允许 root chown 文件。

IOFUNC_PC_NO_TRUNC

指示文件系统不会截断名称。

IOFUNC_PC_SYNC_IO

指示文件系统支持同步 I/O 操作。 如果未设置该位,可能会发生以下情况:
a. 如果客户端指定 O_DSYNC、O_RSYNC 或 O_SYNC,则默认 iofunc 层 _IO_OPEN 处理程序 iofunc_open_default() 将失败。
b. iofunc_sync_verify() 函数返回 EINVAL。
c. 尝试使用 fcntl() 或 DCMD_ALL_SETFLAGS devctl() 命令设置 O_DSYNC、O_RSYNC 或 O_SYNC 失败。

IOFUNC_PC_LINK_DIR

表示允许目录的链接/取消链接。

IOFUNC_PC_ACL

指示资源管理器是否支持访问控制列表。 有关 ACL 的更多信息,请参阅《QNX Neutrino 程序员指南》中的“Working with Access Control Lists (ACLs)”。

dev 成员包含设备编号,并在下面的 “Of device numbers, inodes, and our friend rdev.” 中进行了描述。

blocksize 成员描述设备的本机块大小(以字节为单位)。 例如,在典型的旋转介质存储系统上,该值将为 512。

最后,funcs 指针指向一个结构体(来自 <sys/iofunc.h>):

typedef struct _iofunc_funcs {
  unsigned      nfuncs;
  IOFUNC_OCB_T *(*ocb_calloc) (resmgr_context_t *ctp, IOFUNC_ATTR_T *attr);
  void          (*ocb_free) (IOFUNC_OCB_T *ocb);
} iofunc_funcs_t;

与连接和 I/O 函数表一样,nfuncs 成员应该填充表的当前大小。 为此,请使用常量 _IOFUNC_NFUNCS。

ocb_calloc 和 ocb_free 函数指针可以填充函数地址,以便在分配或释放 OCB 时调用。 稍后当我们讨论扩展 OCB 时,我们将讨论为什么要使用这些函数。


3.3.1 Of device numbers, inodes, and our friend rdev

mount 结构包含一个名为 dev 的成员。 属性结构包含两个成员:inode 和 rdev。 让我们通过检查传统的基于磁盘的文件系统来了解它们的关系。

文件系统安装在块设备(即整个磁盘)上。 该块设备可能称为 /dev/hd0(系统中的第一个硬盘)。 在此磁盘上,可能有多个分区,例如 /dev/hd0t177(该特定设备上的第一个 QNX 文件系统分区)。 最后,在该分区内,可能存在任意数量的文件,其中之一可能是 /hd/spud.txt。

dev(或“设备编号”)成员包含一个对该资源管理器注册的节点唯一的编号。rdev 成员是根设备号。 最后,inode 是文件的序列号。 (请注意,您可以通过调用 rsrcdbmgr_devno_attach() 来获取主设备号和次设备号;有关更多详细信息,请参阅 QNX Neutrino C 库参考。您最多只能有 64 个主设备,每个主设备最多有 1024 个次设备。)

让我们将其与我们的磁盘示例联系起来。 下表显示了一些示例数字; 在表格之后,我们将看看这些数字的来源以及它们之间的关系。

------------------------------------
Device         dev    inode    rdev
------------------------------------
/dev/hd0       6      2        1
/dev/hd0t177   1      12       77
/hd/spud.txt   77     47343    N/A
------------------------------------

对于原始块设备 /dev/hd0,进程管理器分配了 dev 和 inode 值(上表中的 6 和 2)。 资源管理器在设备启动时为其选择了一个唯一的 rdev 值(1)。

对于分区 /dev/hd0t177,dev 值来自原始块设备的 rdev 编号(1)。 inode 由资源管理器选择为唯一编号(在 rdev 内)。 这就是12的由来。 最后,rdev编号也是由资源管理器选择的 —— 在本例中,资源管理器的编写者选择了77,因为它与分区类型相对应。

最后,对于文件 /hd/spud.txt,dev 值 (77) 来自分区的 rdev 值。 inode 由资源管理器选择(对于文件,选择该数字以对应于文件的某些内部表示形式 - 只要它不为零,它是什么并不重要,并且它在rdev中是唯一的)。 这就是 47343 的由来。 对于文件来说,rdev 字段没有意义。