让我们从资源管理器的整体结构开始。首先,我们将了解客户端和服务器端的内幕情况。之后,我们将进入资源管理器中的层,然后查看一些示例。
尽管您将使用隐藏了许多详细信息的资源管理器API,但了解幕后发生的事情仍然很重要。例如,您的资源管理器是包含MsgReceive()循环的服务器,客户端使用MsgSend *()向您发送消息。这意味着您必须及时回复客户端,或者阻止客户端,但保存rcvid以便以后回复。
为了帮助您理解,我们将讨论客户端和资源管理器的封面事件。
当客户端调用需要路径名解析的函数(例如,open(),rename(),stat()或unlink())时,该函数会向进程管理器和资源管理器发送消息以获取文件描述符。获得文件描述符后,客户端可以使用它通过资源管理器将消息发送到与路径名关联的设备。
在下面,获取文件描述符,然后客户端直接写入设备:
/* |
对于上面的例子,这里是对幕后发生的事情的描述。我们假设一个串口由一个名为 devc-ser8250 的资源管理器管理,该管理器已使用路径名前缀/dev/ser1注册:
客户端的库发送“查询”消息。客户端库中的 open()向进程管理器发送一条消息,要求它查找名称(例如/dev/ser1)。
进程管理器指示谁负责,并返回与路径名前缀关联的nd,pid,chid和句柄。
Here's what went on behind the scenes...
当devc-ser8250资源管理器在命名空间中注册其名称(/dev/ser1)时,它调用了进程管理器。进程管理器负责维护有关路径名前缀的信息。在注册期间,它会在其表中添加一个类似于以下内容的条目:
0, 47167 , 1, 0, 0,/dev/ser1
表条目代表:
0
节点描述符(nd)。
47167
资源管理器的进程ID(pid)。
1
资源管理器用于接收消息的通道ID(chid)。
0
在资源管理器已注册多个名称的情况下给出句柄。名字的句柄是0,1代表下一个名字,等等。
0
在名称注册期间传递的打开类型(0是_FTYPE_ANY)。
/dev/SER1
路径名前缀。
资源管理器由节点描述符,进程ID和通道ID唯一标识。进程管理器的表条目将资源管理器与名称,句柄(在资源管理器注册多个名称时区分多个名称)和打开类型相关联。
当客户端的库在步骤1中发出查询调用时,进程管理器在其所有表中查找与该名称匹配的任何已注册路径名前缀。如果另一个资源管理器之前已经注册了名称/,则会找到多个匹配项。因此,在这种情况下,/ 和 /dev/ser1都匹配。进程管理器将使用匹配的服务器或资源管理器列表回复open()。依次询问服务器对路径的处理,首先询问最长匹配。
客户端的库向资源管理器发送“连接”消息。为此,它必须创建与资源管理器通道的连接:
fd = ConnectAttach(nd,pid,chid,0,0);
ConnectAttach()返回的文件描述符也是连接ID,用于直接向资源管理器发送消息。在这种情况下,它用于发送连接消息(
通常,只有open()等函数调用ConnectAttach()并且索引参数为0。大多数情况下,你应该将OR _NTO_SIDE_CHANNEL加入到这个参数中,以便通过辅助通道建立连接,从而产生一个连接ID。大于任何有效的文件描述符。
当资源管理器获取连接消息时,它使用open()调用中指定的访问模式执行验证(例如,您是否尝试写入只读设备?)。
资源管理器通常以pass(和带文件描述符的open()返回)响应或者失败(查询下一个服务器)。
获取文件描述符后,客户端可以使用它将消息直接发送到与路径名关联的设备。
在示例代码中,它看起来好像客户端打开并直接写入设备。实际上,write()调用向资源管理器发送_IO_WRITE消息,请求写入给定数据,资源管理器响应它写入了所有数据中的一些,或者写入失败。
最终,客户端调用close(),它向资源管理器发送_IO_CLOSE_DUP消息。资源管理器通过执行一些清理来处理此问题。
资源管理器是使用QNX Neutrino send/receive/reply消息传递协议来接收和回复消息的服务器。以下是资源管理器的伪代码:
initialize the resource manager register the name with the process manager DO forever receive a message SWITCH on the type of message CASE _IO_CONNECT: call io_open handler ENDCASE CASE _IO_READ: call io_read handler ENDCASE CASE _IO_WRITE: call io_write handler ENDCASE . /* etc. handle all other messages */ . /* that may occur, performing */ . /* processing as appropriate */ ENDSWITCH ENDDO |
您将使用的资源管理器库隐藏了上述伪代码中的许多细节。例如,您实际上不会调用MsgReceive*()函数 - 您将调用库函数,例如resmgr_block()或dispatch_block(),它会为您执行此操作。如果您正在编写单线程资源管理器,则可能会提供消息处理循环,但如果您正在编写多线程资源管理器,则会向您隐藏循环。
您不需要知道所有可能消息的格式,也不必全部处理它们。相反,您注册“handler functions”,当相应类型的消息到达时,库将调用您的处理程序。例如,假设您希望客户端使用read()从您那里获取数据 - 您将编写一个处理程序,只要收到_IO_READ消息就会调用该处理程序。由于您的处理程序处理_IO_READ消息,我们将其称为“io_read处理程序”。
资源管理器库:
收到消息。
检查消息以验证它是_IO_READ消息。
调用你的io_read处理程序。
但是,您仍然有责任回复_IO_READ消息。您可以在io_read处理程序中执行此操作,也可以在数据到达时执行此操作(可能是某些数据生成硬件的中断结果)。
该库对您不想处理的任何消息执行默认处理。毕竟,大多数资源管理器并不关心向客户端呈现正确的POSIX文件系统。在编写它们时,您需要专注于与您正在控制的设备进行通信的代码。您不希望花费大量时间来担心向客户端呈现正确的POSIX文件系统的代码。
资源管理器由以下某些层组成:
线程池层(顶层)
调度层
resmgr层
iofunc层(底层)
让我们从下往上看这些。
The iofunc layer
该层由一组函数组成,这些函数可以为您处理大多数POSIX文件系统的详细信息 - 它们提供了POSIX特性。如果您正在编写设备资源管理器,那么您将需要使用此层,这样您就不必过分担心将POSIX文件系统呈现给全世界所涉及的细节。
如果您不提供处理程序,则此层由资源管理器库使用的默认处理程序组成。例如,如果您不提供io_open处理程序,则调用iofunc_open_default()。
iofunc层还包含默认处理程序调用的辅助函数。如果使用自己的默认处理程序覆盖默认处理程序,仍可以调用这些辅助函数。例如,如果您提供自己的io_read处理程序,则可以在其开头调用iofunc_read_verify()以确保客户端可以访问该资源。
该图层的函数和结构的名称格式为iofunc_*。头文件是
The resmgr layer
该层管理大多数资源管理器库详细信息。它:
检查收到的消息
调用适当的处理程序来处理消息
如果您不使用此图层,则必须自己解析消息。大多数资源管理器都使用此层。
此图层的函数和结构的名称具有resmgr_*形式。头文件是
The dispatch layer
该层充当许多不同类型事物的单个阻塞点。使用此图层,您可以处理:
_IO_* message
它使用resmgr层。
select()
执行TCP/IP的进程通常在等待数据包到达时调用select()来阻塞,或者为写入更多数据留出空间。使用调度层,您可以注册在数据包到达时调用的处理函数。其功能是select_*()函数。
Pulses
与其他图层一样,您可以注册在特定脉冲到达时调用的处理函数。其功能是pulse_*()函数。
Other messages
您可以为调度层提供您组成的一系列消息类型和处理程序。因此,如果消息到达并且消息的前几个字节包含给定范围内的类型,则调度层将调用您的处理程序。其功能是message_*()函数。
以下是通过调度层(或更确切地说,通过dispatch_handler())处理消息的方式:根据阻塞类型,处理程序可以调用message_*()子系统。基于消息类型或脉冲代码,搜索使用message_attach()或pulse_attach()附加的匹配函数。如果找到匹配项,则调用附加函数。
如果消息类型在资源管理器处理的范围内(I/O消息)并且使用resmgr_attach()附加了路径名,则调用资源管理器子系统并处理资源管理器消息。
如果接收到脉冲,则可以将其分派给资源管理器子系统,如果它是资源管理器处理的代码之一(UNBLOCK和DISCONNECT脉冲)。如果完成select_attach()并且脉冲与select使用的脉冲匹配,则调用select子系统并调度该事件。
如果收到消息并且未找到该消息类型的匹配处理程序,则返回MsgError(ENOSYS)以取消阻止发件人。
The thread pool layer
此层允许您拥有单线程或多线程资源管理器。这意味着一个线程可以处理write()而另一个线程处理read()。
您为要使用的线程提供阻塞函数,以及在阻塞函数返回时要调用的处理函数。大多数情况下,你给它调度层的功能。但是,您也可以为其提供resmgr图层的功能或您自己的功能。
您可以独立于资源管理器层使用此层。
以下程序是完整但简单的设备资源管理器示例。
在阅读本指南时,您将遇到许多代码段。大多数代码片段都已编写,因此可以与这些简单的资源管理器结合使用。
这两个简单的设备资源管理器中的前两个在/dev/null提供之后对其功能进行建模(尽管它们使用/dev/sample来避免与“real”/dev/null冲突):
open()始终有效。
read()返回零字节(表示EOF)
任何大小的write()“工作”(丢弃数据)
许多其他POSIX函数都可以工作(例如,chown(),chmod(),lseek())
以下章节描述了如何向这些简单的资源管理器添加更多功能。
QNX Momentics集成开发环境(IDE)包括一个示例/dev/sample资源管理器,它与下面给出的单线程非常相似。要在IDE中获取示例,请选择“help”>“welcome”,然后单击“示例”图标。
这是一个简单的单线程设备资源管理器的完整代码:
#include #include #include #include #include #include #include #include
static resmgr_connect_funcs_t connect_funcs; static resmgr_io_funcs_t io_funcs; static iofunc_attr_t attr;
int main(int argc, char **argv) { /* declare variables we'll be using */ resmgr_attr_t resmgr_attr; dispatch_t *dpp; dispatch_context_t *ctp; int id;
/* initialize dispatch interface */ if((dpp = dispatch_create()) == NULL) { fprintf(stderr, "%s: Unable to allocate dispatch handle.\n", argv[0]); return EXIT_FAILURE; }
/* initialize resource manager attributes */ memset(&resmgr_attr, 0, sizeof resmgr_attr); resmgr_attr.nparts_max = 1; resmgr_attr.msg_max_size = 2048;
/* initialize functions for handling messages */ iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs);
/* initialize attribute structure used by the device */ iofunc_attr_init(&attr, S_IFNAM | 0666, 0, 0);
/* attach our device name */ id = resmgr_attach( dpp, /* dispatch handle */ &resmgr_attr, /* resource manager attrs */ "/dev/sample", /* device name */ _FTYPE_ANY, /* open type */ 0, /* flags */ &connect_funcs, /* connect routines */ &io_funcs, /* I/O routines */ &attr); /* handle */ if(id == -1) { fprintf(stderr, "%s: Unable to attach name.\n", argv[0]); return EXIT_FAILURE; }
/* allocate a context structure */ ctp = dispatch_context_alloc(dpp);
/* start the resource manager message loop */ while(1) { if((ctp = dispatch_block(ctp)) == NULL) { fprintf(stderr, "block error\n"); return EXIT_FAILURE; } dispatch_handler(ctp); } return EXIT_SUCCESS; } |
在
让我们一步一步地检查示例代码。
Initialize the dispatch interface
/* initialize dispatch interface */ |
我们需要建立一种机制,以便客户端可以向资源管理器发送消息。这是通过dispatch_create()函数完成的,该函数创建并返回调度结构。该结构包含通道ID。请注意,在附加内容之前,实际上并未创建通道ID,如resmgr_attach(),message_attach()和pulse_attach()。
要在路径名空间中注册前缀,资源管理器必须启用PROCMGR_AID_PATHSPACE功能。为了创建公共频道(即,没有设置_NTO_CHF_PRIVATE),您的进程必须启用PROCMGR_AID_PUBLIC_CHANNEL功能。 有关更多信息,请参阅procmgr_ability()。
调度结构(类型为dispatch_t)是不透明的;您无法直接访问其内容。使用message_connect()使用此隐藏的通道ID创建连接。
Initialize the resource manager attributes
当您调用resmgr_attach()时,您将resmgr_attr_t控制结构传递给它。我们的示例代码初始化此结构,如下所示:
/* initialize resource manager attributes */ |
在这种情况下,我们正在配置:
有多少IOV结构可用于服务器回复(nparts_max)
最小接收缓冲区大小(msg_max_size)
有关更多信息,请参阅QNX Neutrino C库参考中的resmgr_attach()。
Initialize functions used to handle messages
/* initialize functions for handling messages */ |
这里我们提供两个表来指定特定消息到达时要调用的函数:
连接功能表
I/O功能表
我们不是手动填写这些表,而是调用iofunc_func_init()将iofunc _*_ default()处理函数放入适当的位置。
Initialize the attribute structure used by the device
/* initialize attribute structure used by the device */ |
属性结构包含有关与名称/dev/sample关联的特定设备的信息。 它至少包含以下信息:
权限和设备类型
所有者和组ID
实际上,这是一个按名称的数据结构。在扩展POSIX层数据结构一章中,我们将看到如何扩展结构以包含您自己的每个设备信息。
Put a name into the namespace
要注册我们的资源管理器的路径,我们调用resmgr_attach(),如下所示:
/* attach our device name */ &connect_funcs, /* connect routines */ &io_funcs, /* I/O routines */ if(id == -1) { |
在资源管理器可以从其他程序接收消息之前,它需要通知其他程序(通过进程管理器)它是负责特定路径名前缀的程序。这是通过路径名注册完成的。注册名称后,其他进程可以使用注册名称查找并连接到此进程。
在此示例中,串行端口可以由名为devc-xxx的资源管理器管理,但实际资源在路径名空间中注册为/dev/sample。因此,当程序请求串行端口服务时,它会打开/dev/sample串行端口。
我们将依次查看参数,跳过我们已经讨论过的参数。
device name
与我们的设备关联的名称(即/dev/sample)。
open type
指定_FTYPE_ANY的常量值。这告诉进程管理器我们的资源管理器将接受任何类型的打开请求;我们不会限制我们将要处理的连接类型。
一些资源管理器合法地限制它们处理的打开请求的类型。例如,POSIX消息队列资源管理器仅接受类型为_FTYPE_MQUEUE的打开消息。
flags
控制进程管理器的路径名解析行为。通过指定零值,我们指示我们只接受名称“/dev/sample” 的请求。
您在此参数中使用的位是
还有一些其他标志,其名称不以下划线开头,但它们是resmgr_attr_t结构的标志成员,我们将在“Setting resource manager attributes”中更详细地查看它章节。
如果要对其他类型的通知使用相同的调度处理程序,此时还可以调用message_attach(),pulse_attach()和select_attach()。
Allocate the context structure
上下文结构包含一个缓冲区,用于接收消息。我们初始化资源管理器属性结构时设置了缓冲区的大小。上下文结构还包含IOV的缓冲区,库可用于回复消息。我们初始化资源管理器属性结构时设置了IOV的数量。
有关更多信息,请参阅QNX Neutrino C库参考中的dispatch_context_alloc()。
一旦调用了dispatch_context_alloc(),就不要调用message_attach()或resmgr_attach()来为同一个调度句柄指定更大的最大消息大小或更多的消息部分。在QNX Neutrino 7.0或更高版本中,如果发生这种情况,这些函数会指示EINVAL的错误。(这不适用于pulse_attach()或select_attach(),因为您无法使用这些函数指定大小。)
Start the resource manager message loop
/* start the resource manager message loop */ |
一旦资源管理器建立其名称,它就会在任何客户端程序尝试对该名称执行操作(例如,open(),read(),write())时接收消息。
在我们的示例中,一旦注册了/dev/sample,并执行了一个客户端程序:
fd = open ("/dev/sample", O_RDONLY); |
客户端的C库构造一个_IO_CONNECT消息并将其发送给我们的资源管理器。我们的资源管理器在dispatch_block()函数中接收消息。然后我们调用dispatch_handler(),它解码消息并根据我们之前传入的connect和I/O函数表调用适当的处理函数。在dispatch_handler()返回之后,我们返回dispatch_block()函数以等待另一条消息。
请注意,dispatch_block()返回一个指向调度上下文(dispatch_context_t)结构的指针 - 与传递给例程的指针类型相同:
如果dispatch_block()返回非NULL上下文指针,它可能与传入的指针不同,因为ctp可能会重新分配到更大的大小。在这种情况下,旧的ctp不再有效。
如果dispatch_block()返回NULL(例如,因为信号中断了MsgReceive()),则旧的上下文指针仍然有效。通常,资源管理器将任何信号定向到专用于处理信号的线程。但是,如果信号可以针对执行dispatch_block()的线程,则可以使用以下代码:
dispatch_context_t *ctp, *new_ctp; } } |
稍后,当客户端程序执行时:read (fd, buf, BUFSIZ);
客户端的C库构造一个_IO_READ消息,然后将其直接发送到我们的资源管理器,并重复解码周期。
这是一个简单的多线程设备资源管理器的完整代码:
#include #include #include #include #include #include
/* * Define THREAD_POOL_PARAM_T such that we can avoid a compiler * warning when we use the dispatch_*() functions below */ #define THREAD_POOL_PARAM_T dispatch_context_t
#include #include
static resmgr_connect_funcs_t connect_funcs; static resmgr_io_funcs_t io_funcs; static iofunc_attr_t attr;
int main(int argc, char **argv) { /* declare variables we'll be using */ thread_pool_attr_t pool_attr; resmgr_attr_t resmgr_attr; dispatch_t *dpp; thread_pool_t *tpp; int id;
/* initialize dispatch interface */ if((dpp = dispatch_create()) == NULL) { fprintf(stderr, "%s: Unable to allocate dispatch handle.\n", argv[0]); return EXIT_FAILURE; }
/* initialize resource manager attributes */ memset(&resmgr_attr, 0, sizeof resmgr_attr); resmgr_attr.nparts_max = 1; resmgr_attr.msg_max_size = 2048;
/* initialize functions for handling messages */ iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs);
/* initialize attribute structure used by the device */ iofunc_attr_init(&attr, S_IFNAM | 0666, 0, 0);
/* attach our device name */ id = resmgr_attach(dpp, /* dispatch handle */ &resmgr_attr, /* resource manager attrs */ "/dev/sample", /* device name */ _FTYPE_ANY, /* open type */ 0, /* flags */ &connect_funcs, /* connect routines */ &io_funcs, /* I/O routines */ &attr); /* handle */ if(id == -1) { fprintf(stderr, "%s: Unable to attach name.\n", argv[0]); return EXIT_FAILURE; }
/* initialize thread pool attributes */ memset(&pool_attr, 0, sizeof pool_attr); pool_attr.handle = dpp; pool_attr.context_alloc = dispatch_context_alloc; pool_attr.block_func = dispatch_block; pool_attr.unblock_func = dispatch_unblock; pool_attr.handler_func = dispatch_handler; pool_attr.context_free = dispatch_context_free; pool_attr.lo_water = 2; pool_attr.hi_water = 4; pool_attr.increment = 1; pool_attr.maximum = 50;
/* allocate a thread pool handle */ if((tpp = thread_pool_create(&pool_attr, POOL_FLAG_EXIT_SELF)) == NULL) { fprintf(stderr, "%s: Unable to initialize thread pool.\n", argv[0]); return EXIT_FAILURE; }
/* Start the threads. This function doesn't return. */ thread_pool_start(tpp); return EXIT_SUCCESS; }
|
大多数代码与单线程示例中的代码相同,因此我们仅涵盖上面未描述的那些部分。 此外,我们将在本指南后面详细介绍多线程资源管理器,因此我们将此处的详细信息保持在最低限度。
对于此代码示例,线程使用dispatch _*()函数(即调度层)作为其阻塞循环。
Define THREAD_POOL_PARAM_T
/* #include |
THREAD_POOL_PARAM_T清单告诉编译器在线程将使用的各种blocking/handling函数之间传递什么类型的参数。此参数应该是用于在函数之间传递上下文信息的上下文结构。 默认情况下,它被定义为resmgr_context_t,但由于此示例使用的是调度层,因此需要它为dispatch_context_t。 我们在上面的include指令之前定义它,因为头文件引用它。
Initialize thread pool attributes
/* initialize thread pool attributes */ |
线程池属性告诉线程哪些函数用于它们的阻塞循环并控制任何时候应该存在多少线程。当我们在本指南后面更详细地讨论多线程资源管理器时,我们将详细介绍这些属性。
Allocate a thread pool handle
/* allocate a thread pool handle */ |
线程池句柄用于控制线程池。除其他外,它包含给定的属性和标志。 thread_pool_create()函数分配并填充此句柄。
Start the threads
/* start the threads; will not return */ |
thread_pool_start()函数启动线程池。每个新创建的线程使用我们在属性结构中给出的context_alloc函数来分配由THREAD_POOL_PARAM_T定义的类型的上下文结构。然后它们将阻塞block_func,当block_func返回时,它们将调用handler_func,这两个也都是通过属性结构给出的。 每个线程基本上与上面的单线程资源管理器为其消息循环执行的操作相同。
从现在开始,您的资源管理器已准备好处理消息。由于我们将POOL_FLAG_EXIT_SELF标志赋给thread_pool_create(),一旦线程启动,将调用pthread_exit()并且此调用线程将退出。