如果你認爲本系列文章對你有所幫助,請大家有錢的捧個錢場,點擊此處贊助,贊助額0.1元起步,多少隨意
聲明:本文只用於個人學習交流,若不慎造成侵權,請及時聯繫我,立即予以改正
鋒影
email:[email protected]
The Bones of a Resource Manager
這篇文章將從服務器端和客戶端兩側來描述大體的框架和分層,並會給出實例。
1. Under the covers
1.1 Under the client's covers
當一個客戶端調用需要路徑名解析的函數時(比如open()/rename()/stat()/unlink()
),它會同時向進程管理器和對應的資源管理器發送消息,進而去獲取文件描述符,並通過這個文件描述符向與路徑名關聯的設備發送消息。
/*
* In this stage, the client talks
* to the process manager and the resource manager.
*/
fd = open("/dev/ser1", O_RDWR);
/*
* In this stage, the client talks directly to the
* resource manager.
*/
for (packet = 0; packet < npackets; packet++)
{
write(fd, packets[packet], PACKET_SIZE);
}
close(fd);
上邊的這個代碼,背後的邏輯是怎麼實現的呢?我們假設這是名爲devc-ser8250
資源管理器管理的串口設備,註冊的路徑名爲dev/ser1
:
Under-the-cover communication between the client, the process manager, and the resource manager
-
客戶端發送
query
消息,open()
函數會向進程管理器發送消息,請求查找名字,比如/dev/ser1
; -
進程管理器會返回與路徑名關聯的
nd/pid/child/handle
。當devc-ser8250
資源管理器使用/dev/ser1
註冊時,進程管理器會維護一個類似於下邊的條目信息:0, 47167, 1, 0, 0, /dev/ser1
其中條目的各項分別代表:
0
, Node descriptor(nd
),用於描述在網絡中的節點;47167
, Process ID(pid
),資源管理器的進程ID號;1
, Channel ID(chid
),資源管理器用於接收消息的通道;0
, Handle的索引號,由資源管理器註冊的Handle;0
, 在名稱註冊期間傳遞的打開類型;/dev/ser1
,註冊的路徑名;
在進程管理器中會維護一個條目表,用於記錄各個資源管理器的信息。在名字匹配的時候會選擇最長匹配。
- 客戶端需要向資源管理器發送一個
connect
消息,首先它需要創建一個連接的通道:
fd = ConnectAttach(nd, pid, chid, 0, 0);
客戶端也是使用這個fd
向資源管理器發送連接消息,請求打開/dev/ser1
。當資源管理器收到連接消息後,會進行權限檢查。
-
資源管理器通常會迴應通過或失敗。
-
當客戶端獲取到文件描述符後,可以直接通過它向設備發送消息。示例代碼看起來像是客戶端直接向設備寫入,實際上在調用
write()
時,會先發送一個_IO_WRITE
消息給資源管理器,請求數據寫入,客戶端調用close()
時,會發送_IO_CLOSE_DUP
消息給資源管理器,完成最後資源的回收清理工作。
1.2 Under the resource manager's covers
資源管理器就是一個服務器,通過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
事實上,上述代碼中的很多細節在實現一個資源管理器中可能用不到,因爲有一些庫進行了封裝,但是自己還是需要去實現對消息的回覆。
2. Layers in a resource manager
資源管理器由四層組成,從下到上分別爲:
- iofunc layer
- resmgr layer
- dispatch layer
- thread pool layer
2.1 iofunc layer
這一層的主要功能是提供POSIX特性,用戶編寫資源管理器時不需要太關心向外界提供POSIX文件系統所涉及的細節。
這一層由一組函數iofunc_*
組成,包含了默認處理程序,如果沒有提供自己的處理程序的話,就會使用默認提供的程序。比如,如果沒有提供io_open
處理程序的話,就會調用iofunc_open_default()
。同時也包含了默認處理程序調用的幫助函數,當自己實現處理程序時,也可以調用這些幫助函數。
2.2 resmgr layer
這一層負責管理資源管理器庫的大部分細節:
- 檢查傳入消息
-
調用合適的處理程序來處理消息
如果不使用這一層的話,則需要用戶自己解析消息,大部分資源管理器都會用到這一層。The resmgr layer to handle _IO_* message
2.3 dispatch layer
這一層充當多種事件類型的阻塞點,使用這一層,可以處理:
- `IO* 消息, 使用resmgr層;
select()
,註冊一個處理函數,當數據包到達時調用,這裏的函數是select_*()
函數;Pulses
,註冊一個處理函數,當pulses
來的時候調用,這裏的函數是pulse_*()
函數;- 其他消息,可以提供自己創建的一系列消息類型和處理函數,當消息到達時調用對應的處理函數,這裏的函數是
message_*()
函數;use the dispatch layer to handle _IO_* messages, select, pulses, and other messages
消息進行分發時,會進行查找和匹配,然後會調用到匹配的Handler函數,如果沒有找到匹配的,則返回MsgError
。
2.4 thread pool layer
這一層允許用戶創建單線程或多線程資源管理器,這意味着可以一個線程讀,一個線程寫。需要爲線程提供阻塞函數,以及阻塞函數返回時需要調用的處理函數。大多數情況下,可以將dispatch layer
的任務分配到線程上來,當然,也可以是resmgr layer
任務或自己實現的功能。
3. Simple examples of device resource managers
3.1 Single-threaded device resource managers
先來看一份完整的單線程設備資源管理器代碼吧:
#include <errno.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.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;
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);
}
}
上述代碼完成以下功能:
-
初始化dispatch接口
通過調用dispatch_create()
接口創建dispatch_t
結構,這個結構中包含了channel ID
,但channel ID
實際的創建是在附加其他內容的時候,比如調用resmgr_attach()/message_attach()/pulse_attach()
等。 -
初始化資源管理器屬性
當調用resmgr_attach()
時會傳入resmgr_attr_t
結構,在這個示例中主要配置:
nparts_max
,可供服務器回覆的IOV的個數;msg_max_size
,最大接收緩衝大小;
- 初始化消息處理函數
在這個例子中提供了兩張表,用於指定在特定消息到達時調用哪個函數:
- 連接函數表;
- I/O函數表;
可以調用iofunc_func_init()
接口來配置默認操作函數。
- 初始化設備使用的屬性結構
調用iofunc_attr_init()
接口來設置,屬性結構中至少應該包括以下信息:
- 權限和設備類型
- 組ID和屬主ID
-
在名字空間中註冊一個名字
調用resmgr_attach()
來註冊資源管理器的路徑。在資源管理器能接收到其他程序的消息之前,需要通過進程管理器通知其他程序它與路徑名的綁定關係。 -
分配context結構體
調用dispatch_context_alloc()
接口來分配,context
結構體包含了用於接收消息的緩衝,也包含了可用於回覆消息的IOVs
緩衝。 -
開始資源管理器消息循環
當進入循環後,資源管理器便在dispatch_block()
中接收消息,並調用dispatch_handler()
進行分發處理,選擇連接函數表和I/O函數表中的合適函數進行執行。當執行完畢後,會再進入dispatch_block()
來等待接收其他消息。
3.2 Multi-threaded device resource managers
下邊是多線程設備資源管理器示例代碼:
#include <errno.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.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;
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;
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;
}
/* 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; will not return */
thread_pool_start(tpp);
}
從代碼中可以看出,大部分代碼與單線程示例一樣。在這個代碼中,線程在阻塞循環中用到了dispatch_*()
函數(dispatch layer
)。
-
初始化線程池的屬性
給pool_attr
的各個字段賦值,用於告知線程在阻塞循環時調用哪些函數,以及線程池需要維護多少線程。 -
分配一個線程池的
handle
調用thread_pool_create()
接口來分配,這個handle
用於控制整個線程池。 -
開啓線程
調用thread_pool_start()
接口,啓動線程池。每個新創建的線程都會使用在屬性結構中給出的context_alloc()
函數來分配THREAD_POOL_PARAM_T
定義類型的context
結構。
3.3 Using MsgSend() and MsgReply()
可以不使用read()
和write()
接口來與資源管理器交互,可以調用MsgSend()
來發送消息,下邊的示例將分兩部分:服務器和客戶端。服務器端需要使能PROCMGR_AID_PATHSPACE
,用於確保能調用resmgr_attach()
函數。
- 服務器代碼:
/*
* ResMgr and Message Server Process
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>
#include <sys/dispatch.h>
resmgr_connect_funcs_t ConnectFuncs;
resmgr_io_funcs_t IoFuncs;
iofunc_attr_t IoFuncAttr;
typedef struct
{
uint16_t msg_no;
char msg_data[255];
} server_msg_t;
int message_callback( message_context_t *ctp, int type, unsigned flags,
void *handle )
{
server_msg_t *msg;
int num;
char msg_reply[255];
/* Cast a pointer to the message data */
msg = (server_msg_t *)ctp->msg;
/* Print some useful information about the message */
printf( "\n\nServer Got Message:\n" );
printf( " type: %d\n" , type );
printf( " data: %s\n\n", msg->msg_data );
/* Build the reply message */
num = type - _IO_MAX;
snprintf( msg_reply, 254, "Server Got Message Code: _IO_MAX + %d", num );
/* Send a reply to the waiting (blocked) client */
MsgReply( ctp->rcvid, EOK, msg_reply, strlen( msg_reply ) + 1 );
return 0;
}
int main( int argc, char **argv )
{
resmgr_attr_t resmgr_attr;
message_attr_t message_attr;
dispatch_t *dpp;
dispatch_context_t *ctp, *ctp_ret;
int resmgr_id, message_id;
/* Create the dispatch interface */
dpp = dispatch_create();
if( dpp == NULL )
{
fprintf( stderr, "dispatch_create() failed: %s\n",
strerror( errno ) );
return EXIT_FAILURE;
}
memset( &resmgr_attr, 0, sizeof( resmgr_attr ) );
resmgr_attr.nparts_max = 1;
resmgr_attr.msg_max_size = 2048;
/* Setup the default I/O functions to handle open/read/write/... */
iofunc_func_init( _RESMGR_CONNECT_NFUNCS, &ConnectFuncs,
_RESMGR_IO_NFUNCS, &IoFuncs );
/* Setup the attribute for the entry in the filesystem */
iofunc_attr_init( &IoFuncAttr, S_IFNAM | 0666, 0, 0 );
resmgr_id = resmgr_attach( dpp, &resmgr_attr, "serv", _FTYPE_ANY,
0, &ConnectFuncs, &IoFuncs, &IoFuncAttr );
if( resmgr_id == -1 )
{
fprintf( stderr, "resmgr_attach() failed: %s\n", strerror( errno ) );
return EXIT_FAILURE;
}
/* Setup our message callback */
memset( &message_attr, 0, sizeof( message_attr ) );
message_attr.nparts_max = 1;
message_attr.msg_max_size = 4096;
/* Attach a callback (handler) for two message types */
message_id = message_attach( dpp, &message_attr, _IO_MAX + 1,
_IO_MAX + 2, message_callback, NULL );
if( message_id == -1 )
{
fprintf( stderr, "message_attach() failed: %s\n", strerror( errno ) );
return EXIT_FAILURE;
}
/* Setup a context for the dispatch layer to use */
ctp = dispatch_context_alloc( dpp );
if( ctp == NULL )
{
fprintf( stderr, "dispatch_context_alloc() failed: %s\n",
strerror( errno ) );
return EXIT_FAILURE;
}
/* The "Data Pump" - get and process messages */
while( 1 )
{
ctp_ret = dispatch_block( ctp );
if( ctp_ret )
{
dispatch_handler( ctp );
}
else
{
fprintf( stderr, "dispatch_block() failed: %s\n",
strerror( errno ) );
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
- 首先是調用
dispatch_create()
接口來創建dpp
,通過這個handle
將接收到的消息轉發到合適的層(resmgr, message, pulse
); - 設置調用
resmgr_attach()
所需要的變量; - 調用
iofunc_func_init()
來設置默認處理函數,調用iofunc_attr_init()
來設置屬性結構; - 調用
resmgr_attach()
,注意這時候註冊的路徑不是絕對路徑,因此默認在它的執行路徑下。. - 告訴
dispatch layer
,除了由resmgr layer
處理的標準I/O和連接消息之外,還需要處理自己的消息。 - 調用
message_attach()
接口來註冊自己的消息處理函數。 - 當調用
dispatch_block()
接收消息後,調用dispatch_handler()
來處理,實際上會在dispatch_handler()
中調用message_callback()
函數。當消息類型爲_IO_MAX + 1
或_IO_MAX + 2
時,就會回調函數。
- 客戶端代碼:
/*
* Message Client Process
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/neutrino.h>
#include <sys/iofunc.h>
#include <sys/dispatch.h>
typedef struct
{
uint16_t msg_no;
char msg_data[255];
} client_msg_t;
int main( int argc, char **argv )
{
int fd;
int c;
client_msg_t msg;
int ret;
int num;
char msg_reply[255];
num = 1;
/* Process any command line arguments */
while( ( c = getopt( argc, argv, "n:" ) ) != -1 )
{
if( c == 'n' )
{
num = strtol( optarg, 0, 0 );
}
}
/* Open a connection to the server (fd == coid) */
fd = open( "serv", O_RDWR );
if( fd == -1 )
{
fprintf( stderr, "Unable to open server connection: %s\n",
strerror( errno ) );
return EXIT_FAILURE;
}
/* Clear the memory for the msg and the reply */
memset( &msg, 0, sizeof( msg ) );
memset( &msg_reply, 0, sizeof( msg_reply ) );
/* Set up the message data to send to the server */
msg.msg_no = _IO_MAX + num;
snprintf( msg.msg_data, 254, "client %d requesting reply.", getpid() );
printf( "client: msg_no: _IO_MAX + %d\n", num );
fflush( stdout );
/* Send the data to the server and get a reply */
ret = MsgSend( fd, &msg, sizeof( msg ), msg_reply, 255 );
if( ret == -1 )
{
fprintf( stderr, "Unable to MsgSend() to server: %s\n", strerror( errno ) );
return EXIT_FAILURE;
}
/* Print out the reply data */
printf( "client: server replied: %s\n", msg_reply );
close( fd );
return EXIT_SUCCESS;
}
客戶端通過open()
接口獲取connection id
,調用MsgSend()
接口去往服務器上發送消息,並且等待回覆。
上述服務器和客戶端的例子非常簡單,但覆蓋了大部分的內容,基於這個基礎的框架,還可以做其他的事情:
- 基於不同的消息類型,註冊不同的消息回調函數;
- 除了消息之外,使用
pulse_attach()
來接收pulse
; - 重寫默認的I/O消息處理函數以便客戶端也能使用
read()
和write()
來與服務器交互; - 使用線程池來編寫多線程服務器;