📄 minigui体系结构之一 体系结构概览.htm
字号:
}
put(&buffer, OVER);
return NULL;
}
void * consumer(void * data)
{
int d;
while (1) {
d = get(&buffer);
if (d == OVER) break;
printf("---> %d\n", d);
}
return NULL;
}
int main(void)
{
pthread_t th_a, th_b;
void * retval;
init(&buffer);
/* 建立生产者和消费者线程。*/
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
/* 等待生产者和消费者结束。 */
pthread_join(th_a, &retval);
pthread_join(th_b, &retval);
return 0;
}
</PRE></TD></TR></TBODY></TABLE>
<P>在清单 2 中,程序首先建立了两个线程分别扮演生产者和消费者的角色。生产者负责将 1 到 1000
的整数写入缓冲区,而消费者负责从同一个缓冲区中读取并删除由生产者写入的整数。因为生产者和消费者是两个同时运行的线程,并且要使用同一个缓冲区进行数据交换,因此必须利用一种机制进行同步。清单
2 中的程序就利用信号量实现了同步。</P>
<P>起初程序初始化了两个信号量(init()函数),分别表示可读取的元素数目(sem_read)和可写入的空位个数(sem_write),并分别初始化为
0 和缓冲区大小减1。在生产者调用 put() 函数写入时,它首先对 sem_write 进行DOWN 操作(即 sem_wait
调用),看是否能够写入,如果此时 sem_write 信号量的值大于零,则 sem_wait 可以立即返回,否则生产者将在该 sem_write
信号量上等待。生产者在将数据写入之后,在 sem_read 信号量上进行 UP 操作(即sem_post调用)。此时如果有消费者等待在
sem_read 信号量上,则可以被系统唤醒而继续运行。消费者线程的操作恰恰相反,该线程调用 get() 函数时,首先在 sem_read 上进行
DOWN 操作,当读取数据并删除之后,在 sem_write 信号量上进行 UP 操作。</P>
<P>通过上面的两个例子,读者可以对线程之间的互操作有一个大概了解。如果读者对 System V IPC
机制比较熟悉的话,也可以作一番比较。可以看到,多线程的最大好处是,除堆栈之外,几乎所有的数据均是共享的,因此线程间的通讯效率最高;但最大坏处是,因为共享所有数据,从而非常容易导致线程之间互相破坏数据。</P><STRONG>2.3
MiniGUI 和多线程</STRONG>
<P>MiniGUI 1.0 版本采用了多线程机制,也就是说,MiniGUI 以及运行在 MiniGUI
之上的所有应用程序均运行在同一个地址空间之内。比起其他基于进程的 GUI 系统来说,虽然缺少了地址保护,但运行效率却是最高的。</P><A id=3
name=3></A>
<P><STRONG class=subhead>3 基于 PThread 的微客户/服务器结构</STRONG></P><STRONG>3.1
多线程的分层设计</STRONG>
<P>从整体结构上看,MiniGUI 是分层设计的,层次结构见图 1。在最底层,GAL 和 IAL 提供底层图形接口以及鼠标和键盘的驱动;中间层是
MiniGUI 的核心层,其中包括了窗口系统必不可少的各个模块;最顶层是 API,即编程接口。</P>
<P align=center><IMG alt="" src="MiniGUI体系结构之一 体系结构概览.files/image01.jpg"
border=0><BR>图 1 MiniGUI 的分层设计</P>
<P>GAL 和 IAL 为 MiniGUI 提供了底层的 Linux 控制台或者 X Window 上的图形接口以及输入接口,而 Pthread
是用于提供内核级线程支持的 C 函数库。</P>
<P>MiniGUI 本身运行在多线程模式下,它的许多模块都以单独的线程运行,同时,MiniGUI
还利用线程来支持多窗口。从本质上讲,每个线程有一个消息队列,消息队列是实现线程数据交换和同步的关键数据接口。一个线程向消息队列中发送消息,而另一个线程从这个消息队列中获取消息,同一个线程中创建的窗口可共享同一个消息队列。利用消息队列和多线程之间的同步机制,可以实现下面要讲到的微客户/服务器机制。</P>
<P>多线程有其一定的好处,但不方便的是不同的线程共享了同一个地址空间,因此,客户线程可能会破坏系统服务器线程的数据,但有一个重要的优势是,由于共享地址空间,线程之间就没有额外的数据复制开销。</P>
<P>由于 MiniGUI
是面向嵌入式或实时控制系统的,因此,这种应用环境下的应用程序往往具有单一的功能,从而使得采用多线程而非多进程模式实现图形界面有了一定的实际意义,也更加符合
MiniGUI 之“mini”的特色。</P><STRONG>3.2 微客户/服务器结构</STRONG>
<P>在多线程环境中,与多进程间的通讯机制类似,线程之间也有交互和同步的需求。比如,用来管理窗口的线程维持全局的窗口列表,而其他线程不能直接修改这些全局的数据结构,而必须依据“先来先服务”的原则,依次处理每个线程的请求,这就是一般性的客户/服务器模式。MiniGUI
利用线程之间的同步操作实现了客户线程和服务器线程之间的微客户/服务器机制,之所以这样命名,是因为客户和服务器是同一进程中的不同线程。</P>
<P>微客户/服务器机制的核心实现主要集中在消息队列数据结构上。比如,MiniGUI 中的 desktop
微服务器管理窗口的创建和销毁。当一个线程要求 desktop 微服务器建立一个窗口时,该线程首先在 desktop
的消息队列中放置一条消息,然后进入休眠状态而等待 desktop 处理这一请求,当 desktop
处理完成当前任务之后,或正处于休眠状态时,它可以立即处理这一请求,请求处理完成时,desktop 将唤醒等待的线程,并返回一个处理结果。</P>
<P>当 MiniGUI 在初始化全局数据结构以及各个模块之后,MiniGUI 要启动几个重要的微服务器,它们分别完成不同的系统任务:</P>
<UL>
<LI>desktop 用于管理 MiniGUI 窗口中的所有主窗口,包括建立、销毁、显示、隐藏、修改 Z-order、获得输入焦点等等。
<LI>parsor 线程用来从 IAL中收集鼠标和键盘事件,并将收集到的事件转换为消息而邮寄给 desktop 服务器。
<LI>timer 线程用来触发定时器事件。该线程启动时首先设置 Linux 定时器,然后等待 desktop
线程的结束,即处于休眠状态。当接收到 SIGALRM 信号时,该线程处理该信号并向 desktop 服务器发送定时器消息。当 desktop
接收到定时器消息时,desktop 会查看当前窗口的定时器列表,如果某个定时器过期,则会向该定时器所属的窗口发送定时器消息。
</LI></UL><A id=4 name=4></A>
<P><STRONG class=subhead>4 多线程通讯的关键数据结构——消息队列</STRONG></P><STRONG>4.1
消息和消息循环</STRONG>
<P>在任何 GUI 系统中,均有事件或消息驱动的概念。在MiniGUI中,我们使用消息驱动作为应用程序的创建构架。</P>
<P>在消息驱动的应用程序中,计算机外设发生的事件,例如键盘键的敲击、鼠标键的按击等,都由支持系统收集,将其以事先的约定格式翻译为特定的消息。应用程序一般包含有自己的消息队列,系统将消息发送到应用程序的消息队列中。应用程序可以建立一个循环,在这个循环中读取消息并处理消息,直到特定的消息传来为止。这样的循环称为消息循环。一般地,消息由代表消息的一个整型数和消息的附加参数组成。</P>
<P>应用程序一般要提供一个处理消息的标准函数。在消息循环中,系统可以调用此函数,应用程序在此函数中处理相应的消息。</P>
<P>图 2 是一个消息驱动的应用程序的简单构架示意。</P>
<P align=center><IMG alt="" src="MiniGUI体系结构之一 体系结构概览.files/image02.gif"
border=0><BR>图 2 消息驱动的应用程序的简单构架</P>
<P>MiniGUI 支持如下几种消息的传递机制。这些机制为多线程环境下的窗口间通讯提供了基本途径:</P>
<UL>
<LI>通过 PostMessage
发送。消息发送到消息队列后立即返回。这种发送方式称为“邮寄”消息。如果消息队列中的邮寄消息缓冲区已满,则该函数返回错误值。
<LI>通过 PostSyncMessage
发送。该函数用来向不同于调用该函数的线程消息队列邮寄消息,并且只有该消息被处理之后,该函数才能返回,因此这种消息称为“同步消息”。
<LI>通过 SendMessage
发送。该函数可以向任意一个窗口发送消息,消息处理完成之后,该函数返回。如果目标窗口所在线程和调用线程是同一个线程,该函数直接调用窗口过程,如果处于不同的线程,则利用
PostSyncMessage 函数发送同步消息。
<LI>通过 SendNotifyMessage
发送。该函数向指定的窗口发送通知消息,将消息放入消息队列后立即返回。由于这种消息和邮寄消息不同,是不允许丢失的,因此,系统以链表的形式处理这种消息。
<LI>通过 SendAsyncMessage 发送。利用该函数发送的消息称为“异步消息”,系统直接调用目标窗口的窗口过程。 </LI></UL>
<P>读者可以联系我们在第1节中给出的“生产者/消费者”问题而想到一个简单的消息队列的实现,该消息队列可以简单地设计为一个类似清单 2
的循环队列。但是,GUI 系统中的消息队列并不能是一个简单的循环队列,它还要注意到如下一些问题:</P>
<UL>
<LI>消息一般附带有相关的数据,这些数据对各种消息具有不同的含义,在多窗口环境,尤其是多进程环境下,消息数据的有效传递非常重要。
<LI>消息作为窗口间进行数据交换的一种方式,要提供多种传递机制。某些情况下,发送消息的窗口要等到这个消息处理完成之后,知道处理的结果之后才能继续执行;而有些情况下,发送消息的窗口只是简单地向接收消息的窗口通知某些事件的发生,一般发送出消息之后就返回。后一种情况类似于邮寄信件,所以通常称为邮寄消息。更有一种较为复杂的情况,就是等待一个可能长时间无法被处理的消息时,发送的消息的窗口设置一个超时值,以便能够在消息得不到及时处理的情况下能够恢复执行。
<LI>某些特殊消息的处理也需要注意,比如定时器。当某个定时器的频率很高,而处理这个定时器的窗口的反应速度又很慢,这时如果采用邮寄消息或者发送消息的方式,线性的循环队列最终就会塞满。
<LI>最后一个问题是消息优先级的问题。一般情况下,要考虑优先处理鼠标或键盘的输入消息,其次才是重绘和定时器等消息。
<LI>特殊消息的处理。由于窗口重绘消息的特殊性(通常比较花费时间),只有当程序将其他消息处理之后,才会处理重绘消息。并且只有存在窗口的无效区域的时候,才会通知程序处理窗口的重绘。
</LI></UL>
<P>鉴于以上要特殊考虑的问题,MiniGUI 中的消息队列要比清单 2 中的循环队列复杂。参见清单 3。</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>清单 3 MiniGUI 的消息队列定义
typedef struct _MSGQUEUE
{
DWORD dwState; // 消息队列状态
pthread_mutex_t lock; // 互斥锁
sem_t wait; // 等待信号量
PQMSG pFirstNotifyMsg; // 通知消息队列的头
PQMSG pLastNotifyMsg; // 通知消息队列的尾
PSYNCMSG pFirstSyncMsg; // 同步消息队列的头
PSYNCMSG pLastSyncMsg; // 同步消息队列的尾
MSG* msg; // 邮寄消息缓冲区
int len; // 邮寄消息缓冲区长度
int readpos, writepos; // 邮寄消息缓冲区的当前读取和写入位置
/*
* One thread can only support eight timers.
* And number of all timers in a MiniGUI applicatoin is 16.
*/
HWND TimerOwner[8]; // 定时器所有者
int TimerID[8]; // 定时器标识符
BYTE TimerMask; // 已使用的定时器掩码
} MSGQUEUE;
typedef MSGQUEUE* PMSGQUEUE;
</PRE></TD></TR></TBODY></TABLE>
<P>可以看出,在 MiniGUI 的消息队列定义中,只有邮寄消息的定义类似清单 2
中的线性循环队列。上面提到,通知消息类似邮寄消息,但该消息是不允许丢失的,因此,该消息通过链表形式实现。PMSG 结构的定义也很简单:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>typedef struct _QMSG
{
MSG Msg;
struct _QMSG* next;
BOOL fromheap;
}QMSG;
typedef QMSG* PQMSG;
</PRE></TD></TR></TBODY></TABLE>
<P>用于同步消息传递的数据结构为 SYNCMSG,该结构在消息队列中也形成了一个链表,但该结构本身稍微复杂一些:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE>typedef struct _SYNCMSG
{
MSG Msg;
int retval;
sem_t sem_handle;
struct _SYNCMSG* pNext;
}SYNCMSG;
typedef SYNCMSG* PSYNCMSG;
</PRE></TD></TR></TBODY></TABLE>
<P>可以看到,该结构中有一个信号量,该信号量就是用来通知同步消息的发送线程的。当接收并处理同步消息的线程处理该消息之后,将在 retval
成员中存放处理结果,然后通过 sem_handle 信号量唤醒同步消息的发送线程。</P>
<P>在上述消息队列结构的定义中,还有两个分别用来实现互斥访问和同步的成员,即互斥锁 lock 和信号量 wait。互斥锁 lock
用来实现不同线程对消息队列的互斥访问,比如在获取邮寄消息时的操作如下:</P>
<TABLE class=code-sample cellPadding=0 width="100%" border=0>
<TBODY>
<TR>
<TD><PRE> pthread_mutex_lock (&pMsgQueue->lock);
if (pMsgQueue->readpos != pMsgQueue->writepos) {
pMsgQueue->readpos++;
if (pMsgQueue->readpos >= pMsgQueue->len) pMsgQueue->readpos = 0;
pthread_mutex_unlock (&pMsgQueue->lock);
return 1;
}
else
pMsgQueue->dwState &= ~QS_POSTMSG;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -