QNX系统基础

QNX特点

  • 微内核
  • RTOS
  • 支持posix
  • 安全性

下面展开说下

区别于linux宏内核架构,微内核只提供基础的功能(message、signal)、而文件系统、网络协议栈都在用户空间实现,这样可以保证系统稳定性,但是效率会略微下降。

RTOS:QNX是实时操作系统,任务可抢占,并有任务优先级继承、aps等方式保证事件的响应速度。

QNX架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQK6QJd4-1681452661369)(images/qnx.png)]

QNX内核只提供基础的功能(消息传递、调度服务、信号、锁),而像文件系统、网络协议栈、驱动等都是以进程形式运行在用户空间,服务之间通过内核提供的消息机制IPC交互。 像QNX提供的socket、mq接口等,底层的实现也是kernel提供的message机制,只是上层做了封装。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IeNT5Imh-1681452661370)(images/QNX02.png)]

系统启动的第一个进程procnto

procnto是一个系统进程,是系统启动后拉起的第一个进程,它包括内核基本功能和进程管理器。

所以procnto=内核基本功能+进程管理器

内核基本功能:ipc、同步、时钟、进程调度

进程管理器:包括三个功能

  1. process manage进程管理

    管理进程创建、销毁和进程属性(PCB控制块)

  2. memory manage内存管理

    MMU、共享库和进程间posix共享内存

  3. pathname manage 路径名管理器

    比如一个程序想要访问某个路径,是路径名管理器先进行查找,确定想到访问路径 是属于那个资源管理器,然后把相关信息给到路径调用者。

虽然process manager和微内核在一个地址空间内,但是在使用内核功能时和用户空间是使用相同接口,执行内核代码同样需要进行权限切换。

下面详细介绍下路径名管理器处理的流程:
pathname管理器提供了路径名注册、注销、路径名查询功能,它管理是路径名空间这个资源。

系统开机pathname management管理器会默认注册五个路径,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o4Qd8gYB-1681452661371)(images/QNX03.png)]

下面是一个pathname management管理器的例子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IteXWloV-1681452661371)(images/qnx05.png)]

QNX thread线程与process进程

概念

线程是cpu最小执行单元

进程是资源分配的最小单元

进程是线程的集合

线程私有的一些属性

  • tid
  • thread priority 线程优先级
  • name 线程名称
  • register set 处理器的寄存器上下文
  • stack 堆栈
  • signal mask 信号相关
  • thread local storage 线程局部变量
  • cancellation handlers 线程退出的时候执行的操作

线程状态

线程运行状态有三种状态:
running、ready、block状态

running:程序已经拿到cpu时间片正在运行中

ready:程序已经准备就绪,但是还没有拿到时间片,在等待调度

block:程序某种原因阻塞,比如睡眠、等待锁、等待信号等

block状态的线程在收到了某种响应的事件就会从进入ready状态,等待调度运行。

线程调度

QNX IPC

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cuSUXzEW-1681452661371)(images/qnx006.png)]

QNX支持以上ipc类型通讯方式,不同方式实现不同,有的是通过内核实现,有的通过服务程序实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YY9thhed-1681452661372)(images/123-16813731638856.png)]

QNX resource manager

什么是qnx resource manager?

  • 资源管理,就是posix提供的接口来管理资源。比如open文件、read/write文件
  • resmgr本身是一个进程,在文件系统中注册了名字,其他进程就可以通过这个名字和该进程通信。比如串口进程,注册了一个/dev/ser1名字,其他进程就可以通过/dev/ser1和串口驱动通讯。
  • 我们看到的每一个设备节点都是一个resource manager\

resmgr特点

  • 灵活、方便、为QNX提供标准的posix接口
  • 独立于QNX内核(procnto)
  • QNX内核本身也是一个resource manager,比如/proc、/dev/null实际管理者

资源管理器框架

资源管理器都是由一个路经名作为入口的,而POSIX又定义了对一个文件可进行的操作,所以QNX就替大家预定义了这些操作所需要传递的消息类型和数据格式

QNX资源管理器框架大致可以分成4个部份

iofunc (iomsg) 层,这一层提供了所有POSIX对文件可以进行的io操作 (sys/iofuncs.h, sys/iomsg.h)

resmgr层,这一层提供了登记路径名,接收数据并分发给iofunc执行具体操作。iofunc 和 resmgr,是写一个资源管理器的基础

dispatch层,在一些复杂的资源管理器里,“外来的消息传递”并不是唯一需要处理的。也有可能需要处理“脉冲”,或者有时候一个“信号”。dispatch层会主动识别不同的输入信息,然后转给不同的处理函数进行处理

thread pool层,这一层提供了一个线程池管理,可以配置实现多个线程进行资源管理。

下面深入一下:

iofunc (iomsg) 层

QNX总结了总共34个对文件操作,基本上POSIX对文件的处理,都可以通过这34个操作进行。而资源管理器的iofunc层,其实也就是准备回调函数,通过响应这些操作请求,来提供服务。

这34个回调函数,又根据性质不同,被分为8个 “connect" 回调函数,和26个 “io” 回调函数。这些函数在io_func.h里都有定义,分别是:

Connect Functions
open unlink rename mknod readlink link

IO Functions:共26个

read
write
close_ocb
stat
notify
devctl
unblock
unblock
mount
pathconf
lseek
chmod
chown
utime
openfd
fdinfo
lock
space
shutdown
mmap
msg
umount
dup
close_dup
lock_ocb
unlock_ocb
sync
power

iofunc层的每一个回调函数,都有一个对应的数据结构供客户端和服务器端进行消息传递,在sys/iomsg.h里。

比如计对io_read,就有一个:

typedef union {
    struct _io_read i;
    /* unsigned char data[nbytes]; */
} io_read_t;

可以看出来 io_read,有一个从客户端发到服务器端的 struct _io_read I; 而服务器端到客户端,没有特别的消息结构,就是读到的数据直接返回了。

再比如 io_stat,就是:

typedef union {
    struct _io_stat    i;
    struct  stat       o;
} io_stat_t; 

可以看到,io_stat是从客户端向服务器端发送一个 stauct _io_stat; 而服务器端返回的,直接就是一个 struct stat o; 这个struct stat, 就是 POSIX 的 stat() 函数取到的返回值

如果你仔细观察 struct _io_read; struct _io_stat 这些数据结构,你会发现它们都是开始于:

{
    _Uint16t  type;
    _Uint16t  combine_len;
    …
}

这里,”type”可以告诉收到这个数据结构的服务器,这是个什么信息,“combined_len”则通知了服务器端这个消息(以及它可能携带的可变长参数)一共有多长。

“type"的定义,已经在 sys/iomsg.h 里定义好了, 比如“_IO_READ”, “_IO_WRITE”, “_IO_STAT”…

我们在《从API开始理解QNX》一文里解释过一个read()函数是怎样发送数据的了。再看一下:

ssize_t read(int fd, void *buff, size_t nbytes) {
    io_read_t    msg;
 
    msg.i.type = _IO_READ;
    msg.i.combine_len = sizeof msg.i;
    msg.i.nbytes = nbytes;
    msg.i.xtype = _IO_XTYPE_NONE;
    msg.i.zero = 0;
    return MsgSend(fd, &msg.i, sizeof msg.i, buff, nbytes);    
}

所以,对照 iomsg.h 里的数据结构,你不难想像libc里那些POSIX标准文件函数是怎样构造出来的吧, write() / stat() / lseek() / …

所有QNX上的程序都调用这些 libc 里的函数,而在函数里,它们都转化成一个个消息,通过fd,发送到相关的资源服务器去了。

普通的 QNX 上的开发者,只是调用了这些 read()/write()/stat()函数,然后得到结果,并不关心里面是怎么实现的。这也是为什么好多基本的Unix上的程序,都可以轻松在QNX上重新编译的原因。

在资源服务器端,根据iomsg里的type,就能判断出这是一个什么样的请求,然后调用 iofunc 里相应的处理程序处理就好了。

也许你已经想到下一步这个问题了,每写一个资源管理器,都需要准备三十几个回调函数,这也太复杂了。而且,很多时候,资源管理器只是通过路径名提供一个服务,并不需要提供所有的POSIX标准处理。比如 /dev/random 是提供随机数的一个服务,应该没有必要 lseek() 它吧。那么,从资源管理器的角度,可不可以不提供 IO_SEEK 回调函数码?

答案当然是肯定的,这就会介绍到 resmgr 层了。

resmgr层

作者:xtang
链接:https://zhuanlan.zhihu.com/p/151033583
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如果 iofunc 层提供了各种回调函数的话,resmgr层就是那个“循环按收信息并调用iofunc”的那一层。几个重要的函数差不多就是这样。

resmgr_attach(….)
ctp = resmgr_context_alloc()
for (;;) {
    ctp = resmgr_block(ctp);
    resmgr_handler(ctp)
}

resmgr层和iofunc层给合,就可以搭建一个资源管理器。让我们用一个简单的例子来看一下。

我们这次打算写一个 “/dev/now” 的资源管理器,当有人读它时,它会把当前的时间用字符串返回。让我们看一下代码:

/* 建一个 dispatcher,这个可以想像就是一个接收数据的频道 */
dispatch = dispatch_create();
 
/* 资源管理器本身的一些参数,下面这个就是指定了资源管理器最多一次可以处理10个 iov_t */
memset( &res_attr, 0, sizeof( res_attr ) );
res_attr.nparts_max = 10;
res_attr.msg_max_size = 0;
 
/* io_attr 其实可以想像成一个文件相关的参数,比如读写权限等等 */
iofunc_attr_init(&io_attr, 0666 | S_IFCHR, 0, 0);
 

/* 初始化iofunc层回调 */
iofunc_func_init( _RESMGR_CONNECT_NFUNCS, &connect_funcs, 
_RESMGR_IO_NFUNCS, &io_funcs );
 
/* 建立起资源管理层,同时注册路径 */
rmgid = resmgr_attach(dispatch, &res_attr, "/dev/now", _FTYPE_ANY, 0, &connect_funcs, 
                      &io_funcs,&io_attr);
 
/* 准备一个资源管理层的 context 以备使用 */
ctp = resmgr_context_alloc(dispatch);
 
while (1)
{
    /* resmgr_block() 相当于做了一个 MsgReceive() */
    ctp = resmgr_block(ctp);
 
    /* 根据收到的信息,调用相应的回调程序 */
    resmgr_handler(ctp);
}

如果你把上述几行代码,放在一个 main() 里编译执行的话,你就会发现系统里多了一个 /dev/now 的文件。

$ ls -la /dev/now
crw-rw-rw- 1 root root 0, 1 Jun 01 01:46 /dev/now

这个服务,因为我们根本还没有写iofunc的回调函数,当然什么服务也不会提供。

但是,等一下?如果一个回调函数都没写,ls -la 是怎么成功的?“ls -la /dev/now” 基本上做的就是:

fd = open("/dev/now", O_RDONLY);
stat(fd, &mystat);
close (fd)
printf() 

也就是说,resmgr至少已经帮我们实现了 io_open(), io_stat(), io_close() 这几个回调函数。对吧。而 “crw-rw-rw-” 其实就是因为我们初始化 io_attr 时给出的参数(0666)。

让我们加一个回调函数,来实现 /dev/now 的功能。

static int now_read(resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb)
{
    iofunc_ocb_t *o = (iofunc_ocb_t *)ocb;
    char nowstr[128];
    time_t t;
    int n;
 

    /* 取当前时间,写入字符串 */
    t = time(NULL);
    n = strftime(nowstr, 128, "%Y-%m-%d %H:%M:%S\n", localtime(&t));
 
    /* 把字符串返回给客户端 */
    MsgReply(ctp->rcvid, n, nowstr, n);
 
    /* 告诉 resmgr 层,我们已经做过Reply了,不要再reply客户端了 */
    return _RESMGR_NOREPLY;
}

然后在resmgr初始化的时候,把这个回调函数加上:

iofunc_attr_init(&io_attr, 0666 | S_IFCHR, 0, 0);
iofunc_func_init( _RESMGR_CONNECT_NFUNCS, &connect_funcs,
                           _RESMGR_IO_NFUNCS, &io_funcs );
 
io_funcs.read = now_read;

现在,再执行devnow程序启动 /dev/now 服务。

$ cat /dev/now
2008-09-12 10:23:06
2008-09-12 10:23:06
2008-09-12 10:23:06
2008-09-12 10:23:06
2008-09-12 10:23:06
2008-09-12 10:23:06

虽然我们正确得到了时间显示,但是为什么会持续不断地显示下去?这是因为 cat 的工作原理就是循环 read() 直到读到文件结尾。所以我们要怎么样跟用户程序说"已经读到文件结尾了"?其实就是read()函数返回个长度0。也就是说我们的io_read()回调函数里,需要区分第一次read(返回时间字符串)和第二次read() (返回长度0)。我们可以通过记录文件读取的 offset 来区分。第一次读时,offset还是0,但接下来再读的话,offset就不是0了。

看下面修改的代码。

static int now_read(resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb)
{
    iofunc_ocb_t *o = (iofunc_ocb_t *)ocb;
    char nowstr[128];
    time_t t;
    int n;
 
    /* 判断是不是文件 open 后第一次 read(),不是的话,回复0,这样客户端的 read()就会返回0,
     * 以示读到了文件尾    
     */
    if (o->offset != 0) {
        return 0;
    }
 
    /* 取当前时间,写入字符串 */
    t = time(NULL);
    n = strftime(nowstr, 128, "%Y-%m-%d %H:%M:%S\n", localtime(&t));
 
    o->offset += n;
 
    /* 把字符串返回给客户端 */
    MsgReply(ctp->rcvid, n, nowstr, n);
 
    /* 告诉 resmgr 层,我们已经做过Reply了,不要再reply客户端了 */
    return _RESMGR_NOREPLY;
}

现在,再 cat 的话:

$ cat /dev/now
2008-09-12 10:25:28
$

作者:xtang
链接:https://zhuanlan.zhihu.com/p/151033583
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

iofunc_default_ 回调函数

如果资源管理器,并不需要呈现标准的POSIX文件属性,那么如上面的例子,事情还比较简单。但如果资源管理器需要支持完整的POSIX文件,事情就比较复杂了。

POSIX对于文件处理,有一些比较重要的内在联系。比如在 io_open() 回调中,不光要检查客户端有没有正确的读写权限,也要记住打开文件的模式,并反应在将来的回调中(显然一个O_RDONLY打开文件的客户端是不能io_write()的);文件管理器还得不断追踪读写的位置,以保证将来的lseek() / tell() 调用能返回正确的值;另外每一次read() / write() 都需要更新 stat 结构里的 st_mtime; st_atime等等…

为了帮助用户正确处理POSIX文件,QNX在iofunc层还提供了 iofunc_default_* 函数:

iofunc_open_default()
iofunc_chmod_default()
iofunc_devctl_default()

还是资源管理器的例子,我们加几行代码,注册一个 /posix_file 的文件:

dispatch = dispatch_create();

memset( &res_attr, 0, sizeof( res_attr ) );
res_attr.nparts_max = 10;
res_attr.msg_max_size = 0;

iofunc_attr_init(&io_attr, 0666 | S_IFCHR, 0, 0);
iofunc_func_init( _RESMGR_CONNECT_NFUNCS, &connect_funcs, _RESMGR_IO_NFUNCS, &io_funcs );

connect_funcs.open  = iofunc_open_default;
io_funcs.read             = iofunc_read_default;
io_funcs.write            = iofunc_write_default;
io_funcs.chmod        = iofunc_chmod_default;
io_funcs.chown        = iofunc_chown_default;
io_funcs.stat             = iofunc_stat_default;
io_funcs.close_ocb  = iofunc_close_ocb_default;

rmgid = resmgr_attach(dispatch, &res_attr, "/posix_file", _FTYPE_ANY, 0, &connect_funcs, 
                      &io_funcs, &io_attr);

ctp = resmgr_context_alloc(dispatch);
while (1)
{
    ctp = resmgr_block(ctp);
    resmgr_handler(ctp);
}

具体代码在这里。

xtang2010/articlesgithub.com/xtang2010/articles/tree/master/QNX_Resource_Manager/posix_file[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P5n2kl7p-1681452661373)(images/v2-44352321be2ae0c7ac307219df1f7387_ipico.jpg)]

这个资源管理器我们可以做一些操作:

# ls -lc /posix_file
crw-rw-rw- 1 root root 0, 1 Jun 05 08:34 /posix_file

可以看到文件的owner是root,mode是666,文件的update time 是08:34

# chown xtang /posix_file
# chmod 777 /posix_file
# echo “hello” >/posix_file
# ls -lc /posix_file
crwxrwxrwx 1 xtang root 0, 1 Jun 05 08:35 /posix_file

可以看到,文件的mode, owner, update time 都正确地变化了。

这个示例代码,我们是直接挂接了 iofunc_ chmod_default, iofunc_chown_default等函数,虽然可以处理标准POSIX文件命令,但实际上没有什么功能。在实际情况下,一般都是挂自己的函数进行资源管理器的处理,然后调用iofunc_*_default函数来进行相关POSIX的处理。就是说:

iofuncs.write = my_write;
int my_write(resmgr_context_t *ctp, io_wriet_t msg, iofunc_ocb_t *ocb)
{
    /* do special operation */
 
    return iofunc_write_default(ctp, msg, ocb);
}

现在,我们来试试用 iofunc + resmgr 来重写我们的 md5 服务器。按照需要,我们只要挂 io_write() 和 io_read() 两个回调函数。客户端 write()时,不断把“写进来”的数据输入 MD5_Update(),一直到客户端 read(),把到目前为止的 digest 算出来,返回给用户。

这里涉及到一个问题。MD5记算,是有一个 MD5_Context_t的数据结构的,用 MD5_Init() 初始化后,对于数据,需要不断用 MD5_Update() 去计算;最后read() 时,你会得到用 MD5_Final返回的digest.

问题来了,这些 io_read()/io_write() 函数是回调函数,怎么保证陆续进来的几个write()和read()处理中,这个Context结构是贯穿始终的?

答案是扩展OCB 。

扩展OCB

OCB 是 Open Context Block. 当文件被 Open 时,一个 ocb 会准备好。这个ocb等于是绑定了fd,只要文件没有CLOSE,在同一fd上的任何iofunc回调,都会回传这个ocb。如果同时有几个 fd 被 open了以后,每一个fd,都有一个独自的ocb。显然,如果我们的 md5_context_t可以嵌入在ocb里的话,我们就会每个 fd 都有一个MD5_context_t 了。具体怎么做呢?

首先定义一个扩展的ocb结构,如下。

/* extend iofunc_ocb_t */
typedef struct {
    iofunc_ocb_t ocb;
    int         total_len;
    MD5_CTX      md5_ctx;
} md5mgr_ocb_t;

请注意,结构里第一个元素一定是 iofunc_ocb_t,这样保证我们的扩展结构,可以直接当作iofunc_ocb_t 操作。如果需要,我们依然可以调用iofunc_*_default函数来做POSIX相关的默认处理。这个结构后面的 “total_len"和“md5_ctx” 就是扩展的部份了。

接下来,这个扩展的结构,要怎样分配内存呢?有两种办法,一种是替换iofunc层的 ocb_calloc() 回调函数。

iofunc_funcs_t ocb_funcs = {
    _IOFUNC_NFUNCS,
    md5mgr_ocb_calloc,
    md5mgr_ocb_free
 };
iofunc_mount_t mountpoint = { 0, 0, 0, 0, &ocb_funcs };
iofunc_attr_init(&io_attr, 0666 | S_IFCHR, 0, 0);
io_attr.mount = &mountpoint;

另一种更直接的办法,就是自己准备一个io_open()的回调,当客户端open()时,就会进入我们的回调,在回调里,可以自己 malloc()自己的结构,然后用 resmgr_bind()把这结构当作ocb跟fd绑定。这样,接下来的所有 io 回调中,都会有这个ocb结构传进来。

这个新md5服务完整代码在这里

dispatch层

作者:xtang
链接:https://zhuanlan.zhihu.com/p/151033583
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

虽然大多数情况下,用iofunc层+resmgr层已经可以构建一个完整的资源管理器了,但是有时候资源管理器需要同时处理别的消息,这时就需要 dispatch 层了。dispatch可以处理其他形态的信息,除了可以用 resmgr_attch() 来挂接resmgr 和 iofunc以外,还可以做这些 (sys/dispatch.h):

message_attach()

int message_attach(dispatch_t *dpp, message_attr_t *attr, int low, int high,
int (*func)(message_context_t *ctp, int code, unsigned flags, void *handle),
void *handle);

有时候资源管理器在使用标准的 iomsg 以外,还想要定义自己的消息类型,那就会用到这个message_attach(). 这个函数意思是说,如果收到一个消息,它的消息类型在 low 和 high 之间的话,那就回调 func 来处理。

当然,要保证 low/high 不会与 iomsg 重叠,不然就会有歧义。在 iomsg.h 里已经定义了所有的 iomsg 在 _IO_BASE 和 _IO_MAX 之间,所以只要保证 low > _IO_MAX 就可以了。

pulse_attach()

int pulse_attach(dispatch_t *dpp, int flags, int code,
int (*func)(message_context_t *ctp, int code, unsigned flags, void *handle),
void *handle);

有许多管理硬件资源的管理器(驱动程序),除了提供iomsg消息,来对应客户端的io请求以外,也会很常见需要接收“脉冲”来处理中断 (InterruptAttachEvent)。这时,用 pulse_attach() 就可以把指定的脉冲号(code),绑定到回调函数 func 上。

作者:xtang
链接:https://zhuanlan.zhihu.com/p/151033583
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

select_attach()

int select_attach(void *dpp, select_attr_t *attr, int fd, unsigned flags,
int (*func)(select_context_t *ctp, int fd, unsigned flags, void *handle),
void *handle);

很多资源管理器,在处理客户端请求时,还需要向别的资源管理器发送一些请求。而很多时候这些请求可能不能立即返回结果,通常情况下,可以用 select() 来处理,但第一我们无法使用会阻塞的select(),因为我们是一个资源管理器的服务函数,如果被阻塞无法返回,就意味着我们无法处理客户端请求了;第二我们也无法用 select() 轮询,因为我们一旦返回,就会进入 等待客户端消息的阻塞状态,没有新消息来时,不会退出阻塞状态,也就没有机会再去轮询 select()了。

当然,你可以自设一个时钟,每隔一定时间就给自己发一个脉冲,等于自己把自己叫醒,然后再轮询 select()。做是做得到,但这样就无端增加了许多系统开销。

select_attach() 就是为了这个目的设的,针对一个特定的 fd, 这里的 unsigned flags,决定了你想要 select() 的事件(Read? Write? Except?)。这意思是说,如果对于 fd, 我选择的 flags 事件发生了的话,调用func回调函数。

在使用 dispatch时,进行特殊的 *_attach() 挂接以后,只要把resmgr层的几个函数替换成dispath层的几个函数 就可以了,比如这样:

ctp = dispatch_context_alloc(dispatch);
while (1)
{
    ctp = dispatch_block(ctp);
    dispatch_handler(ctp);
}

dispath_block() 相当于阻塞并等待,而 dispatch_handle() 则根据不同的挂接,调用不同的回调函数进行处理。

另一个比较常用的dispatch函数是dispatch_create_channel()。有时候,你希望自己用 ChannelCreate()创建频道,而不是让dispatch_create()自动为你创建频道,就可以用这个函数。之所以需要自己创建频道,是因为有时候希望在频道上设一些特殊的标志(ChannelCreate() 的 flags)。

thread pool 层

作者:xtang
链接:https://zhuanlan.zhihu.com/p/151033583
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上面这些用 while (1) 来循环处理消息的资源管理器,明显都是“单线程”资源管理器,一共只有一个线程来处理客户端请求。

对于一些需要频繁处理客户请求的资源管理器,自然会想到用“多线程”资源管理器。基本就是用几个线程来执行上面的 while(1)循环。

线程池(Thread Pool)就是用来实现这个的。使用起来也比较简单,先配置 poot_attr,然后创建并启动线程池。

memset(&pool_attr, 0x00, sizeof pool_attr);
if(!(pool_attr.handle = dpp = dispatch_create())) {
    perror("dispatch_create");
    return EXIT_FAILURE;
}
pool_attr.context_alloc = dispatch_context_alloc;
pool_attr.block_func    = dispatch_block;
pool_attr.handler_func  = dispatch_handler;
pool_attr.context_free  = dispatch_context_free;
pool_attr.lo_water      = 2;
pool_attr.hi_water      = 5;
pool_attr.increment     = 2;
pool_attr.maximum       = 10;
 
tpp = thread_pool_create( &pool_attr, POOL_FLAG_EXIT_SELF)
thread_pool_start( tpp );

pool_attr前面几个回调函数都比较简单,后面的 lo_water, hi_water, increment, maximum简要说明一下。

maximum 是最多池里可以建多少线程,

hi_water是最多这些线程可以等待任务

lo_water是至少应该有多少线程需要在等待任务

increment则是一次递增的线程数。

拿上面的例子来说,这个线程池一旦启动,首先会创建5个线程,都在 dispatch_block()上等待接受任务。

当有一个请求来时,1号线程为其服务,假设这个服务线程在服务过程中,还需要向别的线程请求数据,一时回不来,那就还剩下4个线程等待任务。

如些再来两个请求,我们会变成3个线程在进行服务,2个线程等待任务的情形。

这时,当第4个请求来时,又一个线程去进行服务,这时只有1个线程在等待任务了,比lo_water少,所以线程池会自动再新建 2 个(increment) 线程,把他们放在等待任务队列中。所以这时我们有4个线程在服务,3个线程在等待。

如果服务线程无法回收,而新的请求又进来,导致等待线程数又低于lo_water的话,那么,线层池还会继续增加线程以保证有足够线程在等待服务,但是服务线程数与等待线程数的和,不会超过10(maximum).

假如在4个线程服务,3个线程等待的状态下,有2个服务线程结束了服务,它们会被还回线程池,线程池就会把这2个线程继续放入等待队列。也就是变有还有2个服务线程,5个等待线程的状态。

现在,如果又有1个线程结束服务,回归线程池了。如果把这个线程再放入等待队列,那就会有6 个线程等待服务,这个超过了 hi_water,所以线程池会结束这个线程。这样,我们就会有1个线程在服务,5个线程在等待的情形,线程总数下降到了6。

综上,使用线程池可以动态地灵活配置多线程资源管理器,这样,当大量服务同时拥来时,线程分分钟投入服务;当空闲线程较少时,线程池会预先再多开些线程,以备服务。然后,当线程服务结束后,线程池也会停掉一些多余的线程。

结语

大家可以看到资源管理器在QNX上的重要,几乎所有的服务都是通过资源管理器来实现的。而且用资源管理器的概念,可以很好地模块化系统。所以正确地理解资源管理器的概念,熟练运用QNX提供的资源管理器,是在QNX上进行开发的重要技能。当然,资源管理器也有其自身的弱点,通常一个管理器需要跟别的管理器协同工作,才能完成系统的功能;这时,犹其需要注意单线程的管理器不要有被阻塞不能提供服务的时候,多线程管理器虽然不担心阻塞,但是更需要当心线程间同步。

另外,系统设计上一个很重要的因素,还在于正确设计资源管理的细分化。分得太细,会造成一个任务需要穿过多个资源管理器才能实现,会严重损失性能。分得太粗,当然就失去了模块分割,很容易变成一个复杂系统而增加了调试和出错管理的难度。

QNX pathname manager路径名管理器

在QNX这样的客户端服务器系统里,服务器(资源管理器)是通过注册一个路径名字来提供服务的。那么,当客户端给出一个名字,操作系统是怎样找到相对应的服务器的?这后面的水其实很深,让我们来一探究竟。

路径名管理器(pathmgr)

首先,所有名字的注册,搜索,注销都不是由内核管理的。是的,你没有看错,不在内核当中。而是由一个叫pathmgr的资源管理器来管理的。pathmgr管理的是“路径名空间”这个资源,通俗点说就是根目录 “/”。

确实pathmgr是procnto进程的一部份,但它并不是在内核态运行的。

pronto是QNX的第一个进程,但并不意味着它就是微内核。procnto进程其实是微内核+内存管理器+进程管理器+路径名管理器的组合。这几个核外的管理器,虽然是普通QNX系统一定会用到的。但是在极端的情况下,QNX也可以做到完全不用这些管理器,只用微内核+定制的进程来实现一个系统,就像早期的VxWorks那样。只是那样的系统,等于完全取消了QNX的优势,所以虽然技术上可行,但并不流行。

好了,讲回我们的路径名搜索。最简单的就是资源管理器注册"/dev/md5",然后客户端程序open(“/dev/md5”)就好了。你或许会奇怪这有什么复杂的?

可是资源管理器,不光可以注册一个文件,也是可以注册目录的。最容易想到的是文件系统,当你“把某某硬盘分区 mount 到 /home”时,文件系统资源管理器就会注册 “/home/”,意思是说“/home/ 以及至它下面所有的路径名,从此以后都归我管了”。如果你接下来 ls -l /home/user/.bashrc 的话,其实是把整个路径名都发给注册了 /home 的文件管理器,文件管理器就会去实际硬盘分区里找有没有/user/.bashrc这个文件,然后返回状态。

如果资源管理器可以注册另一个资源管理器注册目录的子目录,事情突然变得有趣起来。比如,资源管理器A注册了/home/abc,资源管理器B注册了/home/abc/utils,然后客户端open(“/home/abc/utils/readme”)时,又是哪一个资源管理器来为客户端服务呢?

这个根据客户端要求的路径名寻找对应的资源管理器的过程,就叫做路径名搜索。基本上就是让客户端跟各个可能的管理器联系,由管理器回复客户端。

我们来看一下时序图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RNqXy4tC-1681452661373)(images/v2-f10d091b60afabb396e267a9566837bc_1440w.png)]

  1. 客户端open(“/home/abc/utils/readme”),在libc的open函数里,首先开始的就是路径名搜索。第一步就是把整个路径名发送给路径名管理器pathmgr
  2. 路径名管理器根据传入的路径名,检索已经注册过的所有资源管理器,返回一个所有可能服务"/home/abc/utils/readme"的资源管理器的列表;在我们的例子里,这个列表包括了 “/home/abc/utils” 管理器,"/home/abc"管理器,还有“/”管理器
  3. open() 函数接着会给列表中的“/home/abc/utils”资源管理器发送io_open消息,这个消息里想要搜索的相对路径名 (“readme”),意思是说“我要打开 readme 这个文件,是不是你负责?”
  4. 资源管理器根据传入的路径名和自己掌握的资源来判断自己是不是应该提供服务,是的话,就可以调用io_open回调函数,处理后返回结果,open()就结束了;但如果资源管理器判断io_open里的路径名不由自己负责,资源管理器需要回复ENOENT,POSIX里这个错误的定义就是“找不到指定的文件”。我们这里假设资源管理器返回的是 ENOENT
  5. 客户端的open()函数,收到第一个资源管理器返回的 ENOENT后,知道那个资源管理器不管 “/home/abc/utils/readme”,所以它会向第二个资源管理器(/home/abc)发送io_open消息,带有相对路径 (“utils/readme”)
  6. 第二个资源管理器依然回复 ENOENT
  7. 客户端向第三个资源管理器发关相对路径进行查询(home/abc/utils/readme),看看第三个管理器是不是认得这个名字。
  8. 第三个管理器依然回复 ENOENT
  9. 这时,客户端的open()函数已经对可能服务”/home/abc/utils/readme” 的所有资源管理器进行了路径查询,找不到一个可以提供服务的管理器,所以最后把 ENOENT 写入 errno,并返回 -1 告知open()调用失败了。

这个流程看上去很长,但是注意在第4步,第6步,第8步,只有当资源管理器返回ENOENT时,路径名搜索才会继续下去;如果资源管理器返回了 EOK,那意思就是“对,是我管的,已经准备好了”,那路径名搜索就结束了。还有一种情况就是资源管理器返回别的错误,比如 EPERM,那意思就是“是的,我管这个名字,但是你没有权限”;这时,路径名搜索也会结束,并将这个错误直接返回给用户。

这个通过向一连串资源管理器发送路径名请求的设计,背后的考虑是在通常情况下,“找得到文件”是大多数情形,“找不到" 是一个错误的情形,应该比较少;而在”找得到“的前提下,怎样更好地排列资源管理器列表的前后次序,让客户端更快地命中正确的资源管理器,从而减少不必要的消息传递,也就很有意思了。

QNX的路径名管理器有一个"最长匹配优先"的规则。拿上面的例子来说,对于目标路径"/home/abc/utils/readme"来说,有两个可能的资源管理器,(好吧,三个,因为pathmgr同时也是根目录”/”的资源管理器,等于所有路径名都由它兜底),与注册路径匹配长的,就是"/home/abc/utils/“排在前面,而”/home/abc"的匹配度短一点,所以它排在第二顺位。

如果有两个管理器注册了相同的路径名呢?那么先注册的就排在了前面。(除非后注册的管理器使用了特殊的标记)。

介绍几个路径名管理下QNX特有的例子。

虚拟目录名

一个资源管理器可以注册一个目录,然后客户端对这目录下的任何文件操作,都会由这个管理器收到信息,那么完全可以用一个虚拟目录名,来提供一整套服务。这个在别的Unix也有,只是QNX下的非常灵活,因为名字不再依存于inode,所以resmgr_attach()一个调用就可以简单注册。(所以在QNX上,mount不需要预先准备mountpoint)

比如你可以写一个资源管理器,注册 /car/hvac/ 目录。 而 “ls /car/hvac”无非就是对你的资源管理器进行 opendir()/readdir()/closedir() 操作而已。只要你的服务器回复正确的数据,可以让客户端“看上去”像是你有好多子目录:

/car/hvac/ac/onoff
/car/hvac/ac/temperature
/car/hvac/heat/onoff
/car/hvac/fans/speed
….

对这些文件的读写,都会汇集到你的资源管理器,而根据传入来的相对目录,你也可以正确判断用户是要操作 “fans/speed”,还是 “ac/onoff"。

同样道理,你也可以用一个资源管理器,注册 /proc/sys/,然后提供 Linux 类似的 /proc/sys/ 服务了。

又或者,你可以注册 /mirror/;然后处理 ls -l /mirror/http://ftp.sjtu.edu.cn/ 这样的请求,实现一个基于ftp的文件系统。

同名目录联合(Union)

对目录的“联合”这个概念,也是QNX独有的。比如有一个硬盘分区1上有 “/b/”, 和 “/d/” 两个目录;而硬盘分区2上有 “/a/”, “/c/” 两个目录,通常情况下它们可以分别mount 到 /home1; /home2; 形成

/home1/b/
/home1/d/
/home2/a/
/home2/c/

但如果把他们同时mount到/home以后,你会获得:

/home/a/
/home/b/
/home/c/
/home/d/

也就是说 opendir()/readdir() 这些操作,不是找到第一个服务器就会停止了的。这其实应该也是符合一般人想像的结果。

问题来了,如果两个硬盘分区,都有一个叫 “/readme” 的文件,当把他们联合到一起时,会发生什么?是不是应该有两个:

/home/readme
/home/readme

理论上确实是这样的,但是在同目录下有重名文件,这个会迷惑用户。所以事实上,当同一目录有重名文件时,QNX会只显示一个。另一个经常让人迷惑的是,如果你 rm /home/reamde,你再 ls /home 的话,会发现 /home/readme 依然存在,因为 rm 只删除了一个资源管理器里的 readme,而另一个资源管理器里的readme 还依然存在。

多个资源管理器注册同一路径名

也许你会奇怪为什么两个资源管理器要注册同一个路径名。除了“备份”这样一个原因外,还有可能可以进行“无间断升级”。

比如服务器v1.0 通过注册 /car/speed 提供车速检索服务;经过一段时间以后,这服务器有了新的v2.0版本。一般的升级步骤当然是把前一个v1.0服务器停掉,然后启动v2.0服务器。但这意味着在哪怕只是短短的一瞬间,/car/speed 这个服务消失了。

更好的做法,是先启动 v2.0 服务器,让它也注册 /car/speed。但是,因为v2.0服务器比v1.0启动得晚,所有对 /car/speed的访问依然会去到 v1.0服务器的。这时候就需要v2.0服务器在注册时用到 _RESMGR_FLAG_BEFORE这个标记了。这个告诉路径名管理器“虽然我注册得晚,但当有人查询 /car/speed 时,请把我放在第一个”。

这样,当v2.0启动后,所有open(“/car/speed”)的客户端,都会先跟v2.0服务器通信了。而v1.0 暂时不会退出舞台,它还需要为当前已经跟它建立了连接的客户端服务,一直到这些客户端结束连接(一旦v2.0启动后,v1.0就不会有新的连接了,因为新的客户端都会连接到v2.0) 。

当v1.0的所有现行客户端都结束连接后,它就可以正式退出。这样我们从v1.0到v2.0就进行了一次不暂停服务的“无间断升级”。

有心人可能已经注意到了,使用 _RESMGR_FLAG_BEFORE 可以让后注册的服务器挡在先注册的服务器前面,这样就可以“劫持”某一个资源管理器了。是的,确实可以这样,这个基实就是QNX的 devc-ditto 服务的基础。

路径名管理器命名传递

QNX跟别的Unix一样,也有一个标准的 ln 程序,通常是用来建立链接文件的,不管是硬链接还是符号链接,在文 件系统里有具体的含义,这个想来大家都知道。但是QNX有一个强大的扩展。

*-P Create link in process manager prefix tree.*

通常我们跟-s连用,Unix的文件链接,通常有一定的限制,是不是普通文件,会不会跨越不同分区等,但是 QNX 的 -P,是直接在路径名管理器里做重定向,因为所有的路径名搜索,第一步都是询问路径名管理器,基本上 -P 可以链接任何“路径名”。

*# ln -sP /dev/shmem /tmp*

这个用共享内存做临时服务器的做法在没有外部存储的QNX系统上很常用。其实际的含义是说在路径管理器里建立一个重定向, 任何人访问 /tmp/… 下的任何文件,都会被 自动重定向到 /dev/shmem/… ,然后再开始路径名解析。

*# ln -sP /net/mediacenter/dev/snd /dev/snd*

如果你的机器上没有声卡,但是另一台mediacenter机器上有,这个链接直接在本地机器上建立起了 /dev/snd 目录,本地的播放器程序无须任何修改,当它们试图 open(“/dev/snd/…”) 时,路径名管理器自动把它置换成 /net/mediacenter/dev/snd/…,从而让本地的播放器与mediacenter上的声卡资源管理器之间建立起连接和消息传递。 这就是QNX强大的“透明分布式处理”(QNET),QNX与QNX之间,可以轻松地跨设备协同。而无须为这种协同多写任何代码。

再搞个好玩的

# /bin/hostname
localhost
# ln -sP /bin/ls /bin/hostname
# /bin/hostname
.             .lastlogin    .profile 
..            .ph           
# rm /bin/hostname
# /bin/hostname
localhost

这里发生了什么?可以看到一开始 /bin/hostname 是正常工作的;但强行将 /bin/ls“链接到”/bin/hostname 上,或者说,强行将 /bin/hostname这个路径名指向/bin/ls后,执行/bin/hostname就跟执行/bin/ls一样。而 rm /bin/hostname 则删去了这个重定向,/bin/hostname就又按硬盘上的 hostname 正确执行了…

结语

综上,这里解释了QNX路径名搜索的概念,以及一些细节。也许你会问,知道这些有什么用呢?

知道了路径名是怎么搜索的,就可以在系统设计时尽量避免一些可能降低系统性能的设计。比如,尽量不要交叉注册路径名,由一个资源管理器注册/media/db; 另一个注册/media/control; 会使得路径名搜索时简单明了;而如果一个资源管理器注册/media,另一个注册/media/player的话,以后每次对 /media/player下进行读写时,都有可能引起不必要的消息传递。

另一个比较常见的,因为路径名搜索要向不同的资源管理器查询,相对来说整个过程时间会比较长,所以尽量减少open() 的次数,也可以提高效率。如果你需要频繁地与某个资源管理器通信,每次都open() / write() / close() 它肯定不是一个好主意。最好是open()一次以后记住fd, 后面都通过fd来发送命令。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值