MiniGUI 进程版体系结构
作者: 刘鹏
日期: 2008-10-21
介绍了MiniGUI 进程版 (MiniGUI-Processes) 的体系结构和核心数据结构,并分析了基于 Unix 域套接字的服务器/客户通信机制和共享内存机制。

简介

MiniGUI-Threads 不同,MiniGUI-Processes 上的每个程序都是单独的进程, 每个进程可以建立多个窗口。MiniGUI-Processes 实现了一个多进程窗口系统, 采用客户/服务器架构,通过提供一个服务器程序 mginit ,管理所有的客户程 序。

MiniGUI-Process 引入了层和客户的概念。层,类似于 X Window 的工作区,可以 将来自不同客户进程的窗口放到不同的层中。客户,即用户的程序,每个程序是 单独的进程,每个客户都归属于一个层,层把客户组织到不同的工作区中,多个 客户可在一个层中工作。服务器负责统一管理层和客户。

体系结构

MiniGUI-Process体系结构图
MiniGUI-Process体系结构图

整体上,MiniGUI-Process 采用了客户/服务器架构,服务器负责管窗口 Z 序管理、鼠标和键盘的输入事件截获和分发,客户通过发请求,要求服务器完成某个操作,服务器完成任务后将结果发送给客户。

利用 UNIX 域套接字实现了服务器和客户程序之间的通信。系统级的通信任务包括:

  • 鼠标光标的管理,当客户需要创建、销毁光标,改变光标形状、位置,显示 和隐藏光标时,需要发送请求到服务器,服务器完成相应任务后将结果发送给 客户;
  • 层管理:当客户需要新建层、删除层、查询层信息、加入某个已有层等操作 时,需要发送请求到服务器;
  • 窗口管理:当客户创建、销毁、移动主窗口时,要发送请求到服务器;
  • 转发鼠标、光标的输入事件,鼠标和光标的输入事件先由服务器截获,然后再转发给具体的客户。

全局使用的资源装载到共享内存中,供所有应用共享,这些资源包括:光标、位 图、图标、字体。共享内存的实现有两种方式选择,一是用 mmap ,二是使用 System V IPC 接口。下 面的小节会详细介绍。

服务器端数据结构

服务器采用了层/客户的方式来组织所有的应用。

层和客户的数据结构及其关系如下图所示:

层和客户的组织
层和客户的组织

所有的层组织成一个双向链表,同一个层内的所有客户组成一个双向链表,客户 链表的头结点的指针保存在层结点中。所有的客户结点保存在一块连续的内存区 域中。如上图所示,有三个层:layer1、layer2、layer3,layer1 的客户有 client7、clientN;layer2 的客户有 client0、client2、client5、client6; layer3 的客户有 client1、client3、client4。

层结点的结构如下所示:

层结点结构
层结点结构

每个层结点记录如下信息:

  • 层名字;
  • 本层客户链表的头结点指针;
  • 持有焦点的客户的指针;
  • 指向前序层结点的指针;
  • 指向后序层结点的指针;
  • 本层客户的 Z 序信息。

客户结点的结构如下所示:

客户结点结构
客户结点结构

每个客户结点记录如下信息:

  • 客户名字;
  • 客户进程的 PID;
  • 客户进程的 UID;
  • 连接到客户进程的 socket 的文件描述符;
  • 指向该客户所属层的指针;
  • 前序客户结点;
  • 后序客户结点。

基于 Unix 域套结字的客户/服务器通信

一个请求的数据格式如下所示:

请求的数据格式
请求的数据格式

请求数据由三部分组成:

  • req id,请求编号,请求的唯一标识;
  • data len,请求中的数据长度;
  • data,请求中的数据。

服务器获取请求时,首先读取请求 id,然后读取数据长度,根据数据长度分配 一个内存,之后将数据读出来放到新申请的内存中,最后根据 id 调用不同的 请求处理函数对请求进行处理。

MiniGUI 中的请求有如下几种:

req id req name description
0x0001 REQID_LOADCURSOR 装载光标
0x0002 REQID_CREATECURSOR 创建光标
0x0003 REQID_DESTROYCURSOR 删除光标
0x0004 REQID_SETCURSOR 设置光标
0x0005 REQID_GETCURRENTCURSOR 获取当前光标
0x0006 REQID_SHOWCURSOR 显示光标
0x0007 REQID_SETCURSORPOS 设置光标位置
0x0008 REQID_LAYERINFO 获取层信息
0x0009 REQID_JOINLAYER 请求加入层
0x000A REQID_JOINOP 操作层
0x000B REQID_ZORDEROP 修改 Z 序信息
... ... ...

使用 UNIX 域套接字, 服务器和客户的通信如下所示:

服务器与客户通信
服务器与客户通信

上图是服务器和一个客户的通信过程。服务器创建套接字进入监听状态,当启动客 户程序时,客户创建套接字与服务器建立连接,然后往服务器依次写入 req_id/data_len/data,等待服务器应答;服务器获得连接后,从套接字读取请 求,根据 req_id 做不同的处理,处理完后,根据处理结果发送一条消息给客户, 通知其完成相应的操作 (如窗口显示隐藏、输入事件等);客户从套接字读取消 息后将其放入消息队列中,供窗口过程处理,一次通信结束,最后服务器和客户 需要关闭各自的套接字。Unix 域套接字的接口请查阅相应的手册。

服务器对多个客户连接的管理如下图所示:

管理多个连接
管理多个连接

服务器记录了与自己建立连接的客户的 socket 描述符、进程 id 等信息,并将 这些信息组成一个客户信息数组。服务器依次从各个客户的 socket 描述符中读 取请求,处理后将结果以消息 (MSG) 的方式返回给客户。服务器不断重复这个 过程,实现对客户请求的管理。伪代码如下所示:


while (1) {

    for (i = 0; i < maxi; i++) {

        /* mgClients have all connected clients info. */
        clifd = mgClients [i].fd;
        if (clifd < 0)
            continue;

        /* read request from client. */
        nread = socket_read (clifd, &req_id, sizeof (int));
        if (nread == SOCKETERROR){
            /* remove the disconnected connection. */
            remove_client (i, clifd);
        }
        else {
            /* handle requests and return result to client*/
            handle_request (req_id);
            socket_write (clifd, &ret_value);
        }

    }

}

从全局看,服务器端要处理两类连接:

  • 随时监听新建客户端发来的连接请求;
  • 随时监测已经建立的连接是否有数据过来;

为管理这两类请求,服务器端采用下图所示的流程进行处理:

连接管理总体流程
连接管理总体流程

图中的 listenfd 用于监听是否有新的连接请求,当有新的客户请求连接时,通过 accept (listenfd, ...) 就可以接受 客户的连接请求并建立连接; clifd 用于监测在已建立的连接中是否有数据, 服务器接受客户的连接后会记下每个客户的 clifd,通过依次监测每个客户的 clifd 来决定是否需要读取数据。

使用 select 函数监听上述两类连接。将 listfd 和所有的 clifd 放入 fdset 中,将 fdset 作为参数传给 select,select 返回处于 ready 状态的文件描述 符个数。处于 ready 状态就是有新的连接请求或者在已建立的连接中有数据过 来。 当 select 返回值大于 0 时,说明有描述符处于 ready 状态,通过 FD_ISSET (listenfd) 判断是否有新的连接请求;通过 FD_ISSET (clifd) 判断 是否 clifd 对应的客户发来数据。

对两类连接的监听和处理在服务器端始终存在,将上述流程放在一个循环中, 只要服务器进程在运行,该循环就会一直执行。为了让这个循环不影响服务器进 程的其它工作,可将其放在一个单独的线程中。

进程之间共享资源

有好多资源在各个进程之间是共用的,为了节省内存空间,MiniGUI-Processes 采用了共享内存的方式,即在内存中开辟一块共享内存区,将共用资源加载到这 个共享内存区中,所有进程自己不再单独保存共用资源,而是从共享内存区中取。

资源共享的核心是共享内存的设计,下图给出了 MiniGUI-Processes 定义的共 享内存区的结构示意图。

共享内存区
共享内存区

MiniGUI-Processes 共享的资源有很多,如位图、字体、光标、图标等等。上图只给出了位图数据和字体数据 的布局,其它共享资源与此类似。

共享内存由两部分组成,一是数据头 (head),它记录了各个数据块的数据属 性,如 bmpnum 记录了位图的个数,bmpoffset 记录了位图数据区在整个共享内 存中的 offset;再比如,fontnum 记录了字体的个数,fontoffset 记录了字体 数据区的 offset。第二部分是数据区,它保存了具体的资源数据,各类资源按 种类依次存放在数据区中,通过数据头的 offset 信息,可以定位某类资源的数 据区地址。

服务器在初始化时创建共享内存,并将共享资源逐个加载到共享内存中,并正确 填写数据头的各个属性数据。当客户应用启动时,就到共享内存中取需要的资源。 在技术上有很多方式实现共享内存,如使用 mmap 或者 System V 进程通信机制 等等。

参考资源