Neutrino 有一个特别的优势就是它具有可缩放性(scalable)所谓缩放性我认为就是在最小单元系统下它可以运行,在大规模的网络工作模式下它依然可以运行,且它们的基本结构相同。 它的基本架构如下图所示
在这些模块中,你可以决定是否使用它们,你也可以在使用后卸载,卸载后还可以重新安装。
要实现这些就必须用到消息传递机制(message passing)。比如说你打开一个文件并写入一些数据,那么消息就要从应用程序传递到文件系统,依靠的就是消息传递。
应用程序从文件系统读取数据是消息传递的一种,在QNX中,应用程序代表客户端client,文件系统代表服务器端server。
服务器 server 有两个状态(state) READY RECEIVE
客户端 client 有三个状态 READY REPLY SEND 且通常情况下REPLY状态要比SEND状态多。因为正常情况下client发送message后,server接受到message就要给client回复。有两种情况client会处于SEND状态,一是server正忙(跟电话占线一样),二是服务器挂了(有人把电话线拔了,或者电话除了啥毛病。。。)
这里有一个客户端消息传递的例子:
#include
#include
int
main (void)
{
int fd;
fd = open ("filename", O_WRONLY);
write (fd, "This is message passing\n", 24);
close (fd);
return (EXIT_SUCCESS);
}
很简单,这完全是一个C程序,需要注意的就是 open write close 代表了三个不同的 message 然后它们被发送给了服务器,有了这个概念,就可以接着往下看了。
messaging passing 是在Neutrino中处于核心地位,理解它的使用和含义可以更有效的使用OS。
messaging passing 具有很好的网络协同性(network-distributed),但是它最大的优势是测试和运行软件,消息传递使得软件可以很完美的模块化且便于管理。
客户端/服务器模型很容易让人理解,但是更常用的是下面两种模型:
1、multiple threads 多线程
多线程参考之前的学习笔记
2、server/subserver 服务器/子服务器
这个模型进场运用于network-distributed的设计中
也可以将上述两个模型结合使用,在网络多进程任务中,它们的结合使用将异常强大。
这个模型可以用上面这个图片描述。其中的箭头表示的是发送(send)的方向。
subserver发送消息后并不会立即得到回复(reply)直到server获得了来自客户端(client)的请求(request),subserver得到回复后就会处理client的请求。在这个模型中server相当于一个协调者(coordinator)
因此可以说
server/subserver 是 reply-driven 回复驱动 模型
client/server 是 send-driven 发送驱动 模型
使用消息传递有以下函数:
ChannelCreate(), ChannelDestroy()
ConnectAttach(), ConnectDetach()
MsgDeliverEvent()
MsgError()
MsgRead(), MsgReadv()
MsgReceive(), MsgReceivePulse(), MsgReceivev()
MsgReply(), MsgReplyv()
MsgSend(), MsgSendnc(), MsgSendsv(), MsgSendsvnc(), MsgSendv(), MsgSendvnc(), MsgSendvs(), MsgSendvsnc()
MsgWrite(), MsgWritev()
其中可以完成消息传递的最小使用集是 ChannelCreate(), ConnectAttach(), MsgReply(), MsgSend(), and MsgReceive().
接下来就从client和server两个方面了解一下这些函数该怎么使用,或者应该遵循什么样的原则。
client 只干两件事儿,
1、建立连接(establish a connection)
通过ConnectAttach()函数,该函数定义如下:
#include
int ConnectAttach (int nd,
pid_t pid,
int chid,
unsigned index,
int flags);
nd Node Descriptor 节点描述符
pid process ID 线程ID
chid channel ID 信道ID
剩下两个参数暂时不需要学习,暂且为0(默认值)
int coid;
coid = ConnectAttach (0, 77, 1, 0, 0);
表示连接到process ID 77,channel ID 1,原节点(0即为原节点)
可以通过以下方式中断连接:
ConnectDetach (coid);
2、传消息/数据 (transfer data)
需要用到的是 MsgSend() 函数,该函数定义如下:
#include
int MsgSend (int coid,
const void *smsg,
int sbytes,
void *rmsg,
int rbytes);
具体参数解释如下:
the connection ID of the target server (coid),
a pointer to the send message (smsg),
the size of the send message (sbytes),
a pointer to the reply message (rmsg), and
the size of the reply message (rbytes).
下面通过一段代码感受以下client的全部过程:
#include
char *smsg = "This is the outgoing buffer";
char rmsg [200];
int coid;
// establish a connection
coid = ConnectAttach (0, 77, 1, 0, 0);
if (coid == -1) {
fprintf (stderr, "Couldn't ConnectAttach to 0/77/1!\n");
perror (NULL);
exit (EXIT_FAILURE);
}
// send the message
if (MsgSend (coid,
smsg,
strlen (smsg) + 1,
rmsg,
sizeof (rmsg)) == -1) {
fprintf (stderr, "Error during MsgSend\n");
perror (NULL);
exit (EXIT_FAILURE);
}
if (strlen (rmsg) > 0) {
printf ("Process ID 77 returns \"%s\"\n", rmsg);
}
同样server也需要干两件事儿:
1、创建信道(creating the channel)
与信道有关的函数如下:
#include
int ChannelCreate (unsigned flags);
int ChannelDestroy (int chid);
暂且不学习其中的参数用法,且置为0.
int chid;
chid = ChannelCreate (0);
channel是连接client和server的一个通道,它的由来是方便多线程的通信,因为之前版本的QNX基本是单线程的,连接通信仅靠 node ID 和 process ID ,在多线程中,这就很容易造成混淆,比如之前的thread pool例子,因此就需要一个channel ID 参数连接thread pool中的特定thread。
2、接收消息 (message handling)
这其中又分为接收和回复两个阶段。
#include
int MsgReceive (int chid,
void *rmsg,
int rbytes,
struct _msg_info *info);
int MsgReply (int rcvid,
int status,
const void *msg,
int nbytes);
从下面这幅图中,你可以很清楚的看到数据的“流动”方向。
下面通过另一段代码感受以下server的全过程:
#include
…
void
server (void)
{
int rcvid; // indicates who we should reply to
int chid; // the channel ID
char message [512]; // big enough for our purposes
// create a channel
chid = ChannelCreate (0);
// this is typical of a server: it runs forever
while (1) {
// get the message, and print it
rcvid = MsgReceive (chid, message, sizeof (message),
NULL);
printf ("Got a message, rcvid is %X\n", rcvid);
printf ("Message was \"%s\".\n", message);
// now, prepare the reply. We reuse "message"
strcpy (message, "This is the reply");
MsgReply (rcvid, EOK, message, sizeof (message));
}
}
有关消息回复的一部分内容:
1、reply()不是必须的,你也可以不回复
2、你可以只回复确没有任何内容,使用MsgReply(rcvid,EOK,NULL,0);
3、如果发生了错误的话,你可以使用MsgError(rcvid,EROFS);
在消息传递的环境中,不太明显的一点是 线程需要严格遵守发送等级
所有的发送都会从一个等级到一个更高的等级,不可能是同级或低级。而回复正好与它相反。
不过你终究会碰到要打破这种等级制度的情况,在后面的部分会介绍MsgDeliverEvent()函数。
server经常需要知道谁发送了消息,但是对于client来说,每发送一次消息都要提供发送者信息是十分繁琐的,甚至会有安全漏洞。
kernel会填充MsgReceive()中的struct _msg_info 参数,这个参数包含了许多信息,结构体定义如下:
struct _msg_info
{
int nd;
int srcnd;
pid_t pid;
int32_t chid;
int32_t scoid;
int32_t coid;
int32_t msglen;
int32_t tid;
int16_t priority;
int16_t flags;
int32_t srcmsglen;
int32_t dstmsglen;
};
如果你将它填充为NULL也没有关系,因为这些信息会通过MsgInfo() 被检索出来。
receive ID是MsgReceive() 的返回值,同时它也是server回复client的关键参数,如果你在使用MsgReceive()时没有接受其返回值,那么receive ID 就没有了。
如果你接收了receive ID ,并且你使用了MsgReply() 进行回复消息,那么receive ID就失去了意义。
1、使用name-location函数,name_attach()和name_detach(),然后在client端使用name_open()和name_close()函数操作。
2、使用resource manager,这个内容将会在Resource Managers这章中讲到。
消息传递终究还是thread的运行,所以它还是要遵循之前的高优先级先运行,同优先级看时间顺序的原则。
如果client发送的消息比server预设的缓存小,那么server当然能够接收全部消息;但是,如果比预设的缓存大呢?那么一段data就会被分割成好多段接收,这样就会有不太好的缺点出现:
1、如果发送了一部分消息后,有更高优先级的client发送消息抢占了cpu怎么办?
2、会影响程序的运行速度。
有两个函数可以更好地帮助读写数据,MsgRead() 和 MsgWrite()。
#include
int MsgRead (int rcvid,
void *msg,
int nbytes,
int offset);
int MsgWrite (int rcvid,
const void *msg,
int nbytes,
int offset);
在fs-qnx4中数据的传送会是以下方式:
先通过一段伪代码了解一下server是怎么接收和读取消息的:
// part of the headers, fictionalized for example purposes
struct _io_write {
uint16_t type;
uint16_t combine_len;
int32_t nbytes;
uint32_t xtype;
};
typedef union {
uint16_t type;
struct _io_read io_read;
struct _io_write io_write;
…
} header_t;
header_t header; // declare the header
rcvid = MsgReceive (chid, &header, sizeof (header), NULL);
switch (header.type) {
…
case _IO_WRITE:
number_of_bytes = header.io_write.nbytes;
…
其中nbytes这个变量告诉了server数据的大小,然后server就会以下面这种方式传送数据到缓冲区:
MsgRead (rcvid, cache_buffer [index].data,
cache_buffer [index].size, sizeof (header.io_write));
当server要向client写数据时,基本原理与上述过程一致。
有时候message会在不连续的缓冲区/存储区中,同时发送好几个文件一样,当数量足够多时你就需要一个批量发送的功能,不然就会太影响工作效率。
Neutrino中引入了IOV机制,即Input/Output Vector。
直接上代码感受:
#include
ssize_t write (int fd, const void *buf, size_t nbytes)
{
io_write_t whdr;
iov_t iov [2];
// set up the IOV to point to both parts:
SETIOV (iov + 0, &whdr, sizeof (whdr));
SETIOV (iov + 1, buf, nbytes);
// fill in the io_write_t at the beginning
whdr.type = _IO_WRITE;
whdr.nbytes = nbytes;
// send the message to the server
return (MsgSendv (coid, iov, 2, iov, 1));
}
然后解释一下iov_t这个变量类型:
typedef struct iovec
{
void *iov_base;
size_t iov_len;
} iov_t;
很明显,它包含地址和数据的长度。
接着,你还需要了解MsgSendv()这个函数:
#include
int MsgSendv (int coid,
const iov_t *siov,
int sparts,
const iov_t *riov,
int rparts);
这里这需要解释一下sparts和rparts这两个参数,原文解释为The number of send and receive parts specified by the iov_t parameters.
它的最根本的原理就是传递指针永远要比复制数据快得多。