Chapter 2:The Bones of a Resource Manager
讓我們從資源管理器的整體結構開始。首先,我們將瞭解客戶端和服務器端的內幕情況。之後,我們將進入資源管理器中的層,然後查看一些示例。
Under the covers
儘管您將使用隱藏了許多詳細信息的資源管理器API,但瞭解幕後發生的事情仍然很重要。例如,您的資源管理器是包含MsgReceive()循環的服務器,客戶端使用MsgSend *()向您發送消息。這意味着您必須及時回覆客戶端,或者阻止客戶端,但保存rcvid以便以後回覆。
爲了幫助您理解,我們將討論客戶端和資源管理器的封面事件。
Under the client's covers
當客戶端調用需要路徑名解析的函數(例如,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,用於直接向資源管理器發送消息。在這種情況下,它用於發送連接消息(<sys/iomsg.h>中定義的_IO_CONNECT),其中包含資源管理器的句柄,請求打開/dev/ser1。
通常,只有open()等函數調用ConnectAttach()並且索引參數爲0。大多數情況下,你應該將OR _NTO_SIDE_CHANNEL加入到這個參數中,以便通過輔助通道建立連接,從而產生一個連接ID。大於任何有效的文件描述符。
當資源管理器獲取連接消息時,它使用open()調用中指定的訪問模式執行驗證(例如,您是否嘗試寫入只讀設備?)。
-
資源管理器通常以pass(和帶文件描述符的open()返回)響應或者失敗(查詢下一個服務器)。
-
獲取文件描述符後,客戶端可以使用它將消息直接發送到與路徑名關聯的設備。
在示例代碼中,它看起來好像客戶端打開並直接寫入設備。實際上,write()調用向資源管理器發送_IO_WRITE消息,請求寫入給定數據,資源管理器響應它寫入了所有數據中的一些,或者寫入失敗。
最終,客戶端調用close(),它向資源管理器發送_IO_CLOSE_DUP消息。資源管理器通過執行一些清理來處理此問題。
Under the resource manager's covers
資源管理器是使用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文件系統的代碼。
Layers in a resource manager
資源管理器由以下某些層組成:
-
線程池層(頂層)
-
調度層
-
resmgr層
-
iofunc層(底層)
讓我們從下往上看這些。
The iofunc layer
該層由一組函數組成,這些函數可以爲您處理大多數POSIX文件系統的詳細信息 - 它們提供了POSIX特性。如果您正在編寫設備資源管理器,那麼您將需要使用此層,這樣您就不必過分擔心將POSIX文件系統呈現給全世界所涉及的細節。
如果您不提供處理程序,則此層由資源管理器庫使用的默認處理程序組成。例如,如果您不提供io_open處理程序,則調用iofunc_open_default()。
iofunc層還包含默認處理程序調用的輔助函數。如果使用自己的默認處理程序覆蓋默認處理程序,仍可以調用這些輔助函數。例如,如果您提供自己的io_read處理程序,則可以在其開頭調用iofunc_read_verify()以確保客戶端可以訪問該資源。
該圖層的函數和結構的名稱格式爲iofunc_*。頭文件是<sys/iofunc.h>。有關更多信息,請參閱QNX Neutrino C庫參考。
The resmgr layer
該層管理大多數資源管理器庫詳細信息。它:
-
檢查收到的消息
-
調用適當的處理程序來處理消息
如果您不使用此圖層,則必須自己解析消息。大多數資源管理器都使用此層。
此圖層的函數和結構的名稱具有resmgr_*形式。頭文件是<sys/resmgr.h>。有關更多信息,請參閱QNX Neutrino C庫參考。
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圖層的功能或您自己的功能。
您可以獨立於資源管理器層使用此層。
Simple examples of device resource managers
以下程序是完整但簡單的設備資源管理器示例。
在閱讀本指南時,您將遇到許多代碼段。大多數代碼片段都已編寫,因此可以與這些簡單的資源管理器結合使用。
這兩個簡單的設備資源管理器中的前兩個在/dev/null提供之後對其功能進行建模(儘管它們使用/dev/sample來避免與“real”/dev/null衝突):
-
open()始終有效。
-
read()返回零字節(表示EOF)
-
任何大小的write()“工作”(丟棄數據)
-
許多其他POSIX函數都可以工作(例如,chown(),chmod(),lseek())
以下章節描述瞭如何向這些簡單的資源管理器添加更多功能。
QNX Momentics集成開發環境(IDE)包括一個示例/dev/sample資源管理器,它與下面給出的單線程非常相似。要在IDE中獲取示例,請選擇“help”>“welcome”,然後單擊“示例”圖標。
Single-threaded device resource manager
這是一個簡單的單線程設備資源管理器的完整代碼:
#include <errno.h> #include <stdio.h> #include <stddef.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/iofunc.h> #include <sys/dispatch.h>
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; } |
在<sys/iofunc.h>之後包含<sys/dispatch.h>,以避免重新定義某些函數成員的警告。
讓我們一步一步地檢查示例代碼。
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層數據結構一章中,我們將看到如何擴展結構以包含您自己的每個設備信息。
要註冊我們的資源管理器的路徑,我們調用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” 的請求。
您在此參數中使用的位是<sys/resmgr.h>中定義的_RESMGR_FLAG_*標誌(例如,_RESMGR_FLAG_BEFORE)。我們將在本指南中討論其中一些標誌,但您可以在QNX Neutrino C庫參考中的resmgr_attach()條目中找到完整列表。
還有一些其他標誌,其名稱不以下劃線開頭,但它們是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消息,然後將其直接發送到我們的資源管理器,並重復解碼週期。
Multithreaded device resource manager
這是一個簡單的多線程設備資源管理器的完整代碼:
#include <errno.h> #include <stdio.h> #include <stddef.h> #include <stdlib.h> #include <unistd.h> #include <string.h>
/* * 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 <sys/iofunc.h> #include <sys/dispatch.h>
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 _*()函數(即調度層)作爲其阻塞循環。
/* #include <sys/iofunc.h> |
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 */ |
線程池句柄用於控制線程池。除其他外,它包含給定的屬性和標誌。 thread_pool_create()函數分配並填充此句柄。
/* 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()並且此調用線程將退出。