miniGUI消息队列分析

miniGUI消息队列的定义

miniGUI(版本:libminigui-gpl-3.0.12)的消息队列支持通知消息、同步消息和普通消息三种消息类型,_MSGQUEUE消息队列结构容纳了这三种消息队列,也就是说真正的消息队列在_MSGQUEUE内部才实现,_MSGQUEUE结构只是将这三种消息队列做了一层封装。其中普通消息的内存使用方式为“动态数组”方式,即通过malloc方式申请固定大小的连续内存;同步消息和通知消息的内存使用方式为“链表”方式。

// the MSGQUEUE struct is a internal struct.
// using semaphores to implement message queue.
struct _MSGQUEUE
{
    DWORD dwState;              // message queue states

#ifdef _MGRM_THREADS
    pthread_mutex_t lock;       // lock
    sem_t wait;                 // the semaphore for wait message
    sem_t sync_msg;             // the semaphore for sync message
#endif

    PQMSG  pFirstNotifyMsg;     // head of the notify message queue
    PQMSG  pLastNotifyMsg;      // tail of the notify message queue

#ifdef _MGRM_THREADS
    PSYNCMSG pFirstSyncMsg;     // head of the sync message queue
    PSYNCMSG pLastSyncMsg;      // tail of the sync message queue
#else
    IDLEHANDLER OnIdle;         // Idle handler
#endif

#ifdef _MGRM_THREADS
    PMAINWIN pRootMainWin;      // The root main window of this message queue.
#endif

    MSG* msg;                   /* post message buffer */
    int len;                    /* buffer len */
    int readpos, writepos;      /* positions for reading and writing */

    int FirstTimerSlot;         /* the first timer slot to be checked */
    DWORD TimerMask;            /* timer slots mask */

    int loop_depth;             /* message loop depth, for dialog boxes. */
};

miniGUI消息队列的接口

消息队列作为一种抽象数据类型(ADT),是有和它自身相关的一套接口的,常见的队列接口的功能如下:

  • 创建一个队列;
  • 销毁一个队列;
  • 向队列中插入一个元素;
  • 从队列中删除一个元素;
  • 返回队列头部的第一个元素;
  • 判断队列是否为空;
  • 判断队列是否为满;
  • 返回队列的size;
  • 返回队列中元素的个数;
  • ...

创建消息队列

首先,看一下miniGUI中创建队列的函数定义,位于src/kernel/message.c文件中,该函数的原型为:

BOOL mg_InitMsgQueue (PMSGQUEUE pMsgQueue, int iBufferLen);

从其接口中可以看出来,该函数接收一个_MSGQUEUE类型的指针(所以在此函数之前,首先要申请一个_MSGQUEUE结构体或定义一个_MSGQUEUE指针),和一个整数iBufferLen(用于作为普通消息的消息队列的size),返回消息队列是否创建成功。下面为该函数的定义。

BOOL mg_InitMsgQueue (PMSGQUEUE pMsgQueue, int iBufferLen)
{
    memset (pMsgQueue, 0, sizeof(MSGQUEUE));

    pMsgQueue->dwState = QS_EMPTY;

#ifdef _MGRM_THREADS
    pthread_mutex_init (&pMsgQueue->lock, NULL);
    sem_init (&pMsgQueue->wait, 0, 0);
    sem_init (&pMsgQueue->sync_msg, 0, 0);
#endif

    if (iBufferLen <= 0)
        iBufferLen = DEF_MSGQUEUE_LEN;
    
    /* 为消息队列申请动态内存数组 */
    pMsgQueue->msg = malloc (sizeof (MSG) * iBufferLen);

    if (!pMsgQueue->msg) {
#ifdef _MGRM_THREADS
        pthread_mutex_destroy (&pMsgQueue->lock);
        sem_destroy (&pMsgQueue->wait);
        sem_destroy (&pMsgQueue->sync_msg);
        return FALSE;
#endif
    }

    pMsgQueue->len = iBufferLen;    //指定普通消息的消息队列长度

    pMsgQueue->FirstTimerSlot = 0;
    pMsgQueue->TimerMask = 0;

    return TRUE;
}

由于只有普通消息的消息队列的内存使用方式采用“动态数组”,所以创建或初始化miniGUI的消息队列时,只对普通消息队列及其它数据成员做初始化,不对采用链表的同步消息和通知消息队列初始化。那同步消息和通知消息队列的初始化放在哪了呢?
由于同步消息和通知消息采用的是链表的内存组织方式,每个节点的获取和释放都是动态的,在初始化miniGUI的消息队列时,由于还没有分配消息,所以通知消息和同步消息的队列指针没有指向实际的内存位置。通过mg_InitMsgQueue函数中的“memset (pMsgQueue, 0, sizeof(MSGQUEUE));”已将这两类消息的指针设为NULL。

销毁消息队列

销毁miniGUI的消息队列是由mg_DestroyMsgQueue函数完成的。该函数对通知消息和普通消息的消息队列做了销毁。同步消息由于它的特性,其释放或销毁不必放在该函数内进行。

void mg_DestroyMsgQueue (PMSGQUEUE pMsgQueue)
{
    PQMSG head;
    PQMSG next;

    head = next = pMsgQueue->pFirstNotifyMsg;
    while (head) {
        next = head->next;

        FreeQMSG (head);
        head = next;
    }

#ifdef _MGRM_THREADS
    pthread_mutex_destroy (&pMsgQueue->lock);
    sem_destroy (&pMsgQueue->wait);
    sem_destroy (&pMsgQueue->sync_msg);
#endif

    mg_remove_timers_by_msg_queue (pMsgQueue);
    
    /* 普通消息队列的销毁 */
    if (pMsgQueue->msg)
        free (pMsgQueue->msg);
    pMsgQueue->msg = NULL;
}

向消息队列中插入消息

普通消息的插入

普通消息的插入是通过PostMessage()函数和kernel_QueueMessage()函数完成的。PostMessage()完成对消息的封装,kernel_QueueMessage()将普通消息投递到消息队列中。

  1. PostMessage()
  • 根据窗口句柄获取消息队列句柄;
  • 根据PostMessage()参数封装消息体;
  • 调用kernel_QueueMessage()向消息队列插入消息;
  1. kernel_QueueMessage()
  • 首先是检查几种特殊消息:MSG_MOUSEMOVE、MSG_NCMOUSEMOVE、MSG_DT_MOUSEMOVE、MSG_TIMEOUT、MSG_IDLE、MSG_CARETBLINK,如果要插入的消息是这几种的话,先做特殊处理后,再变相插入到消息队列中;
  • 平常消息的插入:
/* Write the data and advance write pointer */
   msg_que->msg [msg_que->writepos] = *msg;
   msg_que->writepos++;
#if 0
//此语句无效
   if (msg_que->writepos >= msg_que->len) msg_que->writepos = 0;
#else
   msg_que->writepos %= msg_que->len;
#endif

**
通过这两个函数可以看到,对于miniGUI消息队列中的普通消息,miniGUI是采用动态数组(malloc分配内存)的方式实现循环队列,读指针指向循环队列中的头部第一个元素,写指针指向循环队列中尾部的最后一个元素的下一个写位置,并且循环队列采用“空闲一个位置”的方式避免“判断队列为空或为满时读写指针相同的情况”。**

同步消息的插入

同步消息的插入由SendSyncMessage()函数完成,该函数向另一个线程中的窗口发送消息,该函数的处理流程:一先将自己同步消息队列中所有的同步消息都处理完,这样是为了避免死锁,如果两个线程以相差较短的时间互给对方发送同步消息,线程一等待线程二处理消息而阻塞,而线程二也因等待线程一处理消息而阻塞;二是将同步消息插入到另一个线程的消息队列中,这一部分的代码如下。

    LOCK_MSGQ (pMsgQueue);

    if (pMsgQueue->pFirstSyncMsg == NULL) {
        pMsgQueue->pFirstSyncMsg = pMsgQueue->pLastSyncMsg = &SyncMsg;
    }
    else {
        pMsgQueue->pLastSyncMsg->pNext = &SyncMsg;
        pMsgQueue->pLastSyncMsg = &SyncMsg;
    }

    pMsgQueue->dwState |= QS_SYNCMSG;

    UNLOCK_MSGQ (pMsgQueue);
    POST_MSGQ (pMsgQueue);

    /* suspend until the message has been handled. */
    if (sem_wait (SyncMsg.sem_handle) < 0) {
        fprintf (stderr, 
            "KERNEL>SendSyncMessage: thread is interrupted abnormally!\n");
    }

通知消息的插入

通知消息的插入由SendNotifyMessage()和SendTopNotifyMessage()函数完成,SendTopNotifyMessage()函数主要是对于紧急的消息,亟需被处理,将其插在通知消息的头部。

  1. SendNotifyMessage()
LOCK_MSGQ (pMsgQueue);

   /* queue the notification message. */
   pqmsg->Msg.hwnd = hWnd;
   pqmsg->Msg.message = iMsg;
   pqmsg->Msg.wParam = wParam;
   pqmsg->Msg.lParam = lParam;
   pqmsg->next = NULL;

   if (pMsgQueue->pFirstNotifyMsg == NULL) {
       pMsgQueue->pFirstNotifyMsg = pMsgQueue->pLastNotifyMsg = pqmsg;
   }
   else {
       pMsgQueue->pLastNotifyMsg->next = pqmsg;
       pMsgQueue->pLastNotifyMsg = pqmsg;
   }

   pMsgQueue->dwState |= QS_NOTIFYMSG;

   UNLOCK_MSGQ (pMsgQueue);
  1. SendTopNotifyMessage()
    SendTopNotifyMessage()代码与SendNotifyMessage()类似,此处不再列出。

从消息队列中删除消息

在miniGUI中是从消息队列中获取消息,这部分工作由GetMessage()和PeekMessageEx()函数完成。GetMessage()只是对PeekMessageEx()做了一层封装,内部是直接调用PeekMessageEx()。GetMessage函数的定义如下:

/**
 * \fn BOOL GetMessage (PMSG pMsg, HWND hMainWnd)
 * \brief Gets a message from the message queue of a main window.
 *
 * This function gets a message from the message queue of the main window 
 * \a hMainWnd, and returns until there is a message in the message queue.
 *
 * \param pMsg Pointer to the result message.
 * \param hMainWnd Handle to the window.
 *
 * \return FALSE on MSG_QUIT have been found or on error, else gets a message.
 *
 * \sa HavePendingMessage, PostQuitMessage, MSG
 *
 * Example:
 *
 * \include getmessage.c
 */
static inline BOOL GUIAPI GetMessage (PMSG pMsg, HWND hWnd)
{
    return PeekMessageEx (pMsg, hWnd, 0, 0, TRUE, PM_REMOVE);
}

GetMessage()只是传递两个参数,一个是指向消息缓冲的指针,一个是要获取消息的来源窗体,内部调用PeekMessageEx()又默认添加了诸多参数。PeekMessageEx()函数的原型为:

//位置:include/window.h - line: 2218
/**
 * \fn BOOL PeekMessageEx (PMSG pMsg, HWND hWnd, \
 *               int iMsgFilterMin, int iMsgFilterMax, \
 *               BOOL bWait, UINT uRemoveMsg)
 * \brief Peeks a message from the message queue of a main window.
 *
 * This functions peek a message from the message queue of the window \a hWnd;
 * if \a bWait is TRUE, it will wait for the message, else return immediatly.
 *
 * \param pMsg Pointer to the result message.
 * \param hWnd The handle to the window.
 * \param iMsgFilterMin The min identifier of the message that should be peeked.
 * \param iMsgFilterMax The max identifier of the message that should be peeked.
 * \param bWait Whether to wait for a message.
 * \param uRemoveMsg Whether remove the message from the message queue.
 *      Should be the following values:
 *      - PM_NOREMOVE\n
 *        Leave it in the message queue.
 *      - PM_REMOVE
 *        Remove it from the message queue.
 *      - PM_NOYIELD
 *        Nouse now.
 *        
 * \return TRUE if there is a message peeked, or FALSE.
 *
 * \sa GetMessage, PeekPostMessage, HavePendingMessage, PostMessage
 */
MG_EXPORT BOOL GUIAPI PeekMessageEx (PMSG pMsg, HWND hWnd, 
                int iMsgFilterMin, int iMsgFilterMax, 
                BOOL bWait, UINT uRemoveMsg);
  1. 参数iMsgFilterMin和iMsgFilterMax
    iMsgFilterMin和iMsgFilterMax用于限定想要获取的消息范围,在PeekMessageEx()中由宏 IS_MSG_WANTED(message)使用,判断当前获取的消息是否是想要的。该宏的定义 如下:
#define IS_MSG_WANTED(message) \
       ( (iMsgFilterMin <= 0 && iMsgFilterMax <= 0) || \
         (iMsgFilterMin > 0 && iMsgFilterMax >= iMsgFilterMin && \
          message >= iMsgFilterMin && message <= iMsgFilterMax) )

由该宏的定义可知,GetMessage()获取消息时,获取到的消息总是想要的。

  1. 参数bWait
    bWait参数用于定义当消息队列中没有消息时,是否等待;GetMessage()将该值置为TRUE,表示在消息队列中暂时没有消息时,愿意等待。
  2. 参数uRemoveMsg
    uRemoveMsg参数用于表征在获取到一个消息时,是否将该消息从消息队列中删除。在队列的接口中,有“从队列中删除一个元素”和“返回队列头部的第一个元素”两种接口,对应两种不同的应用场景。该参数正是这两种接口的另一种解决方式。

由于miniGUI消息队列设计的特性,除了普通消息、通知消息和同步消息之外,另有几种消息,比如MSG_QUIT、MSG_TIMER、MSG_PAINT等,这些消息不插入到三种消息队列中,而是通过消息队列的标志位dwState提现。所以,这些消息不涉及从消息队列中删除消息这一说,而是如果相应的标志被置位,pMsg再被重新封装。
PeekMessageEx()函数内部的其它部分基本就是普通消息、通知消息和同步消息从消息队列中的获取。

  1. 同步消息的获取
   if (pMsgQueue->dwState & QS_SYNCMSG) {
       /* 检查队列是否为空 */
       if (pMsgQueue->pFirstSyncMsg) {
           *pMsg = pMsgQueue->pFirstSyncMsg->Msg;
           SET_PADD (pMsgQueue->pFirstSyncMsg);
           if (IS_MSG_WANTED(pMsg->message)) {
             if (uRemoveMsg == PM_REMOVE) {
               /* 从消息队列中删除该消息 */
                 pMsgQueue->pFirstSyncMsg = pMsgQueue->pFirstSyncMsg->pNext;
             }

             UNLOCK_MSGQ (pMsgQueue);
             return TRUE;
           }
       }
       else    /* 队列为空则去掉QS_SYNCMSG的置位 */
           pMsgQueue->dwState &= ~QS_SYNCMSG;
   }
  1. 通知消息的获取
if (pMsgQueue->dwState & QS_NOTIFYMSG) {
        /* 检查队列是否为空 */
        if (pMsgQueue->pFirstNotifyMsg) {
            phead = pMsgQueue->pFirstNotifyMsg;
            *pMsg = phead->Msg;
            SET_PADD (NULL);

            if (IS_MSG_WANTED(pMsg->message)) {
              if (uRemoveMsg == PM_REMOVE) {
                /* 从消息队列中删除该消息 */
                  pMsgQueue->pFirstNotifyMsg = phead->next;
                  FreeQMSG (phead);
              }

              UNLOCK_MSGQ (pMsgQueue);
              return TRUE;
            }
        }
        else    /* 队列为空则去掉QS_NOTIFYMSG的置位 */
            pMsgQueue->dwState &= ~QS_NOTIFYMSG;
    }
  1. 普通消息的获取
if (pMsgQueue->dwState & QS_POSTMSG) {
        /* 检查队列是否为空 */
        if (pMsgQueue->readpos != pMsgQueue->writepos) {
            *pMsg = pMsgQueue->msg[pMsgQueue->readpos];
            SET_PADD (NULL);
            if (IS_MSG_WANTED(pMsg->message)) {
                CheckCapturedMouseMessage (pMsg);
                /* 从消息队列中删除该消息 */
                if (uRemoveMsg == PM_REMOVE) {
                    pMsgQueue->readpos++;
                    /* #if 0 中对循环队列的两种处理方式都可行 */
#if 0
                    if (pMsgQueue->readpos >= pMsgQueue->len)
                        pMsgQueue->readpos = 0;
#else
                    pMsgQueue->readpos %= pMsgQueue->len;
#endif
                }

                UNLOCK_MSGQ (pMsgQueue);
                return TRUE;
            }
        }
        else    /* 队列为空则去掉QS_POSTMSG的置位 */
            pMsgQueue->dwState &= ~QS_POSTMSG;
    }

Notes

队列的实现有多种方式,按内存的申请方式包括静态数组、动态数组、链表。通过静态数组和动态数组实现的队列,通常为了避免“判断队列为空或为满的队尾、对头指针指向相同”的问题,又将队列实现为循环队列。以下为各种队列的实现方式:

  1. 来源:CSDN
    作者:乞力马扎罗的雪雪
    文章:C语言实现使用静态数组实现循环队列
  2. 来源:CSDN
    作者:乞力马扎罗的雪雪
    文章:C语言实现使用动态数组实现循环队列
  3. 来源:网易博客
    作者:Dr
    文章:用链表实现队列

Others

  • miniGUI私有块数据堆(Private Block Data Heap)
    miniGUI为了提高性能,不过于频繁地调用内存管理函数(malloc和free),自身维护一些用于分配数据块的块数据堆,这些数据块有一个特性就是大小固定,比如一个区域中的剪切矩形。块数据堆的定义如下。
typedef struct _BLOCKHEAP
{
#ifdef _MGRM_THREADS
   pthread_mutex_t lock;
#endif
   /**
    * Size of one block element.
    */
   size_t          bd_size;
   /**
    * Size of the heap in blocks.
    */
   size_t          heap_size;
   /**
    * The first free element in the heap.
    * 用于跟踪分配的堆中第一个可用的块--空块
    */
   int             free;
   /**
    * Pointer to the pre-allocated heap.
    * 通过malloc得到的内存模拟堆的起始地址
    */
   void*           heap;
} BLOCKHEAP;
typedef BLOCKHEAP* PBLOCKHEAP;

通知消息的分配方式就是从块数据堆中分配,miniGUI将QMSGHeap定义为全局静态变量,该通知消息(有待考证)堆的初始化和销毁通过mg_InitFreeQMSGList和mg_DestroyFreeQMSGList完成。

static BLOCKHEAP QMSGHeap;
//Interface
BOOL mg_InitFreeQMSGList (void);
void mg_DestroyFreeQMSGList (void);
inline static PQMSG QMSGAlloc (void);
inline static void FreeQMSG (PQMSG pqmsg);

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容