Writing a Resource Manager -- Chapter 2:The Bones of a Resource Manager
Chapter 2:The Bones of a Resource Manager
讓我們從資源管理器的整體結構開始。首先,我們將瞭解客戶端和伺服器端的內幕情況。之後,我們將進入資源管理器中的層,然後檢視一些示例。
Under the covers
儘管您將使用隱藏了許多詳細資訊的資源管理器API,但瞭解幕後發生的事情仍然很重要。例如,您的資源管理器是包含MsgReceive()迴圈的伺服器,客戶端使用MsgSend *()向您傳送訊息。這意味著您必須及時回覆客戶端,或者阻止客戶端,但儲存rcvid以便以後回覆。
為了幫助您理解,我們將討論客戶端和資源管理器的封面事件。
Under the client's covers
當客戶端呼叫需要路徑名解析的函式(例如,open(),rename(),stat()或unlink())時,該函式會向程序管理器和資源管理器傳送訊息以獲取檔案描述符。獲得檔案描述符後,客戶端可以使用它通過資源管理器將訊息傳送到與路徑名關聯的裝置。
在下面,獲取檔案描述符,然後客戶端直接寫入裝置:
/* * resource manager. */ for (packet = 0; packet < npackets; packet++) { write(fd, packets[packet], PACKET_SIZE); } close(fd); |
對於上面的例子,這裡是對幕後發生的事情的描述。我們假設一個串列埠由一個名為 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建立連線。
當您呼叫resmgr_attach()時,您將resmgr_attr_t控制結構傳遞給它。我們的示例程式碼初始化此結構,如下所示:
/* initialize resource manager attributes */ |
在這種情況下,我們正在配置:
-
有多少IOV結構可用於伺服器回覆(nparts_max)
-
最小接收緩衝區大小(msg_max_size)
有關更多資訊,請參閱QNX Neutrino C庫參考中的resmgr_attach()。
/* initialize functions for handling messages */ |
這裡我們提供兩個表來指定特定訊息到達時要呼叫的函式:
-
連線功能表
-
I/O功能表
我們不是手動填寫這些表,而是呼叫iofunc_func_init()將iofunc _*_ default()處理函式放入適當的位置。
/* 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()。
上下文結構包含一個緩衝區,用於接收訊息。我們初始化資源管理器屬性結構時設定了緩衝區的大小。上下文結構還包含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 */ |
一旦資源管理器建立其名稱,它就會在任何客戶端程式嘗試對該名稱執行操作(例如,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 */ |
執行緒池屬性告訴執行緒哪些函式用於它們的阻塞迴圈並控制任何時候應該存在多少執行緒。當我們在本指南後面更詳細地討論多執行緒資源管理器時,我們將詳細介紹這些屬性。
/* 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()並且此呼叫執行緒將退出。