文章目录
- Windows平台进程内消息总线
- 如果没有消息总线,会产生什么问题
- 死循环包含关系
- 高耦合、低内聚
- 消息总线
- 结构图
- 原理
- 生产者与总线的关系
- 总线与消费者的关系
- Linux进程内消息总线设计
- 使用进程间实时信号来实现进程内消息总线
- 参考文档
- 整体流程
- 主线程注册总线消息处理函数
- 生产者线程产生并发送消息到总线
- 接收并处理消息
- 总线接收到消息
- 总线消息处理函数
- 消费者总线消息处理虚函数
- 消费者消息处理实函数
- 核心原理
- sigqueue发送消息
- sigaction处理消息
- 存在的问题
- 消息被哪个线程执行,不可预知
- 不可调用不可重入函数
- 不可调用异步信号不安全函数
- 使用线程间信号来实现进程内消息总线
- 整体流程
- 主线程注册总线消息处理函数
- 注册总线消息处理函数
- 创建总线消息处理专用线程
- 总线消息处理专用线程等待消息
- 生产者线程产生并发送消息到总线
- 接收并处理消息
- 总线消息处理专用线程接收到消息
- 总线消息处理函数
- 消费者总线消息处理虚函数
- 消费者消息处理实函数
- 核心原理
- sigprocmask阻塞所有异步消息
- pthread_sigqueue发送消息
- 创建总线消息专用线程使用sigwaitinfo接收消息
- 小结
Windows平台进程内消息总线
??Windows平台下开发C/C++程序,同一进程中的不同模块,如果要发送消息,可以使用PostMessage、SendMessage等函数。其中:PostMessage为异步消息发送接口;SendMessage为同步消息发送接口。
??比如:
??如果窗口A要给窗口B发送消息。那么:窗口A只需要拥有窗口B的窗口句柄,然后 调用PostMessage向窗口B的句柄发送消息;窗口B只需要在自己的消息处理函数中增加对消息的处理即可。
??如果窗口B要给窗口A发送消息。那么:窗口B只需要拥有窗口A的窗口句柄,然后调用 PostMessage向窗口A的句柄发送消息;窗口A只需要在自己的消息处理函数中增加对消息的处理即可。
??因此,无论窗口A向窗口B发送消息,还是窗口B给窗口A发送消息,或者A、B之间互发消息,A、B都不需要关注对方如果处理消息,而只需要关注对方需要处理什么消息。
如果没有消息总线,会产生什么问题
??还是上面的例子,如果没有消息总线:
??如果窗口A要给窗口B发送消息。那么窗口A需要调用窗口B的消息处理函数。窗口A需要包含窗口B的头文件。窗口A需要关注窗口B如何处理消息。即:窗口A跟窗口B耦合度太高。
??如下图所示:
??如果窗口B要给窗口A发送消息。那么窗口B需要调用窗口A的消息处理函数。窗口B需要包含窗口A的头文件。窗口B需要关注窗口A如何处理消息。即:窗口B跟窗口A耦合度太高。
??如下图所示:
死循环包含关系
??如果窗口A要给窗口B发送消息,同时窗口B也要给窗口A发送消息。那么窗口A需要包含窗口B的头文件,同时窗口B也需要包含窗口A的头文件。这样就会出现A包含B,同时B又包含A的死循环包含关系。在编译的时候就出错。当然即便出现了死循环关系,我们也可以通过一些小技巧避免编译时错误。这里不再缀述。
??如下图所示:
高耦合、低内聚
??如果没有消息总线,消息发送者就必须关注消息接收者如何处理消息,从而导致发送者模块与接收者模块间的强耦合关系。这种设计不符合软件设计的高内聚、低耦合原则。
消息总线
??消息总线正好可以解决以上问题。
结构图
原理
??在消息总线的设计中,共有三个角色、一个定义、一个消息处理过程。三个角色是:生产者、消费者、总线。一个定义是:消息定义。一个消息处理过程是:消息处理函数。生产者只负责生产消息。消费者只负责处理消息。总线只负责接收和驱动消息。生产者与消费者之间,共享消息定义。 总线的子类的实例包含了所有消费者。各消费者只需要实现自己关注的消息处理函数,即可完成消息处理。
生产者与总线的关系
??生产者负责生产消息,并将消息发送给总线,总线负责接收消息。
总线与消费者的关系
??总线负责接收消息,并驱动消费者执行消息处理函数。消费者总线继承于总线。消息者总线中包含了所有的消费者。各消息者只需要实现自己关注的消息处理函数,即可完成消息处理。
??这样生产者与消费者之间的耦合性仅存在于消息定义。实现了两者间的高内聚、低耦合。同时当两者需要互发消息时,也解除了两者间的死循环包括关系。
??由于消息发送者不需要关注消息的处理。因此,消息总线给程序设计带来以下好外:
- 避免了发送者与接收者之间的死循环包含关系。
- 模块符合高内聚、低耦合的设计原则,模块易于维护。
Linux进程内消息总线设计
??Windows平台进程内消息总线的实现,可以通过:PostMessage,SendMessage函数来实现。遗憾的是,Linux系统本身并未直接提供与PostMessage(SendMessage)函数功能类似的函数供进程内不同模块间实现消息互通。不过,我们可以使用Linux系统的相关库函数自己封装一套与PostMessage(SendMessage)函数功能类似的进程内消息总线。
??下面我们将详细介绍两种方案来实现Linux进程内消息总线。第一种方案:使用进程间实时信息来实现。第二种方案:使用线程间信号来实现。同时,我们将对比两种方案的优劣。
使用进程间实时信号来实现进程内消息总线
参考文档
《Linux-UNIX系统编程手册》第22.8章 实时信号
整体流程
??消息总线的主要流程如下:
- 主线程注册总线消息处理函数
- 注册总线消息处理函数
- 总线等待消息
- 生产者线程产生并发送消息到总线
- 接收并处理消息
- 总线接收到消息
- 总线消息处理函数
- 消费者总线消息处理虚函数
- 消费者消息处理实函数
主线程注册总线消息处理函数
int main(int argc, char* argv[])
{
int error = S_OK;
if (nullptr != g_Service)
{
...
message_register_handler(message_handler, g_Service);
...
}
message_unregister_handler();
return 0;
}
int message_register_handler(lpfn_message_handler msgHandler, void *userData)
{
int hr = S_OK;
struct sigaction act;
g_userSig.signum = SIGUSER;
g_userSig.msgHandler = msgHandler;
g_userSig.userData = userData;
//register message process function
act.sa_sigaction = signal_handler;
act.sa_flags = SA_SIGINFO;
//wait signal
hr = sigaction(SIGUSER, &act, NULL);
return hr;
}
生产者线程产生并发送消息到总线
int message_post(int message, void *data1, void *data2)
{
if (!g_userSig.tidp)
return 0;
int hr = 0;
pid_t pid = getpid();
union sigval val;
user_signal_data_t *sig = (user_signal_data_t *)malloc(sizeof(user_signal_data_t));
memset(sig, 0, sizeof(user_signal_data_t));
sig->checkCode = USER_SIGNAL_CHECK_CODE;
sig->message = message;
sig->data1 = data1;
sig->data2 = data2;
val.sival_ptr = sig;
hr = sigqueue(pid, g_userSig.signum, val);
if (0 != hr)
{
//EAGAIN
printf("message post: hr:%d, errorcode: %d, errorstr: %s\n", hr, errno, strerror(errno));
hr = E_PTHREAD_SIGQUEUE_FAILED;
}
return hr;
}
接收并处理消息
总线接收到消息
原理请参考:《Linux-UNIX系统编程手册》22.8.2 处理实时信号
//总线等待消息。收到消息时,即会触发总线消息处理函数
hr = sigaction(SIGUSER, &act, NULL);
总线消息处理函数
void signal_handler(int signum, siginfo_t * info, void * context)
{
user_signal_data_t *pdata = nullptr;
if (g_userSig.signum == signum)
{
if (context)
{
pdata = (user_signal_data_t *)info->si_ptr;
if (g_userSig.msgHandler)
{
g_userSig.msgHandler(pdata->message, pdata->data1, pdata->data2, g_userSig.userData);
}
free(pdata);
pdata = nullptr;
}
}
else
{
printf("unknown signal: %d\n", signum);
}
}
int message_handler(int message, void *msgData1, void *msgData2, void *userData)
{
CBaseService *service = (CBaseService *)userData;
service->on_message(message, msgData1, msgData2);
return S_OK;
}
消费者总线消息处理虚函数
int CBaseComsumerService::on_message(int message, void *msgData1, void *msgData2)
{
int hr = E_NOTIMPL;
return hr;
}
消费者消息处理实函数
int CComsumerService::on_message(int message, void *msgData1, void *msgData2)
{
int hr = S_OK;
hr = CBaseComsumerService::on_message(message, msgData1, msgData2);
if (SUCCEEDED(hr))
return hr;
//process messages related to myself only
switch (message)
{
case UM_MESSAGE_A:
on_msg_a(message, msgData1, msgData2);
break;
case UM_MESSAGE_B:
on_msg_b(message, msgData1, msgData2);
break;
default:
hr = E_NOTIMPL;
break;
}
return hr;
}
核心原理
sigqueue发送消息
??在消息总线的设计中,使用sigqueue携带自定义参数,将消息发送给本进程。从而实现了进程内消息的发送。
sigaction处理消息
??使用sigaction接收消息,并交给signal_handler信号处理函数。signal_handler调用注册的总线消息处理函数:message_handler。message_handler调用消费者的消息处理虚函数,实现消息的分发。
存在的问题
??本小节的大多数内容摘抄自:《Linux-UNIX系统编程手册》第21.1.2章 可重入函数和异步信号安全函数。
消息被哪个线程执行,不可预知
??使用sigqueue发送信号时,对信号的处置属于进程层面,进程中的所有线程共享对每个信号的处置设置。如果某一线程使用函数sigaction()为某类信号(比如,SIGINT)创建了处理函数,那么当收到SIGINT时,任何线程都会去调用该处理函数。与之类似,如果将对信号的处置设置为忽略(ignore),那么所有线程都会忽略该信号。
??这意味着,在本例的消息总线中:消息被哪个线程执行,不可预知。
不可调用不可重入函数
??要解释可重入函数、不可重入函数为何物,首先需要区分单线程程序和多线程程序。典型UNIX程序都具有一条执行线程,贯穿程序始终,CPU围绕单条执行逻辑来处理指令。而对于多线程程序而言,同一进程却存在多条独立、并发的执行逻辑流。
??不过,多执行线程的概念与使用了信号处理器函数的程序也有关联。因为信号处理器函数可能会在任一时点异步中断程序的执行,从而在同一个进程中实际形成了两条(即主程序和信号处理器函数)独立(虽然不是并发)的执行线程。
??如果同一个进程的多条线程可以同时安全地调用某一函数,那么该函数就是可重入的。此处,“安全”意味着,无论其他线程调用该函数的执行状态如何,函数均可产生预期结果。
??更新全局变量或静态数据结构的函数可能是不可重入的。(只用到本地变量的函数肯定是可重入的。)如果对函数的两个调用(例如:分别由两条执行线程发起)同时试图更新同一全局变量或数据类型,那么二者很可能会相互干扰并产生不正确的结果。例如,假设某线程正在为一链表数据结构添加一个新的链表项,而另一线程也正试图更新同一链表。由于为链表添加新项涉及对多枚指针的更新,一旦另一线程中断这些步骤并修改了相同的指针,结果就会产生混乱。
??在 C 语言标准函数库中,这种可能性非常普遍。例如,malloc()和free()就维护有一个针对已释放内存块的链表,用于从堆中重新分配内存。如果主程序在调用 malloc()期间为一个同样调用malloc()的信号处理器函数所中断,那么该链表可能会遭到破坏。因此,malloc()函数族以及使用它们的其他库函数都是不可重入的。
??还有一些函数库之所以不可重入,是因为它们使用了经静态分配的内存来返回信息。此类函数的例子包括 crypt()、getpwnam()、gethostbyname()以及
getservbyname()。如果信号处理器用到了这类函数,那么将会覆盖主程序中上次调用同一函数所返回的信息(反之亦然)。
??将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是stdio函数库成员(printf()、scanf()等),它们会为缓冲区I/O更新内部数据结构。所以,如果在信号处理器函数中调用了printf(),而主程序又在调用printf()或其他stdio 函数期间遭到了处理器函数的中断,那么有时就会看到奇怪的输出,甚至导致程序崩溃或者数据的损坏。
??即使并未使用不可重入的库函数,可重入问题依然不容忽视。如果信号处理器函数和主程序都要更新由程序员自定义的全局性数据结构,那么对于主程序而言,这种信号处理器函数就是不可重入的。
??如果函数是不可重入的,那么其手册页通常会或明或暗地给出提示。对于其中那些使用或返回静态分配变量的函数,需要特别留意。
??这意味着,在本例的消息总线中:像malloc、printf等系统函数均不可使用,因为他们都是不可重入函数。
不可调用异步信号不安全函数
??异步信号安全的函数是指当从信号处理器函数调用时,可以保证其实现是安全的。如果某一函数是可重入的,又或者信号处理器函数无法将其中断时,就称该函数是异步信号安全的。
??POSIX.1-1990提供了异步信号安全函数表。SUSv3强调,表之外的所有函数对于信号而言都是不安全的,但同时指出,仅当信号处理器函数中断了不安全函数的执行,且处理器函数自身也调用了这个不安全函数时,该函数才是不安全的。换言之,编写信号处理器函数有如下两种选择。
- 确保信号处理器函数代码本身是可重入的,且只调用异步信号安全的函数。
- 当主程序执行不安全函数或是去操作信号处理器函数也可能更新的全局数据结构时,阻塞信号的传递。
??第 2 种方法的问题是,在一个复杂程序中,要想确保主程序对不安全函数的调用不为信号处理器函数所中断,这有些困难。出于这一原因,通常就将上述规则简化在信号处理器函数中绝不调用不安全的函数。
??这意味着,在本例的消息总线中:不可调用异步信号不安全函数。
使用线程间信号来实现进程内消息总线
整体流程
??消息总线的主要流程如下:
- 主线程注册总线消息处理函数
- 注册总线消息处理函数
- 创建总线消息处理专用线程
- 总线消息处理专用线程等待消息
- 生产者线程产生并发送消息到总线
- 接收并处理消息
- 总线消息处理专用线程接收到消息
- 总线消息处理函数
- 消费者总线消息处理虚函数
- 消费者消息处理实函数
主线程注册总线消息处理函数
注册总线消息处理函数
int main(int argc, char* argv[])
{
int error = S_OK;
if (nullptr != g_Service)
{
...
message_register_handler(message_handler, g_Service);
...
}
message_unregister_handler();
return 0;
}
创建总线消息处理专用线程
int message_register_handler(lpfn_message_handler msgHandler, void *userData)
{
int hr = S_OK;
g_userSig.signum = SIGUSER;
g_userSig.msgHandler = msgHandler;
g_userSig.userData = userData;
if (0 != (pthread_create(&g_userSig.tidp, nullptr, message_thread_function, (void*)&g_userSig)))
{
printf("create message thread error!\n");
hr = E_PTHREAD_CREATE_FAILED;
}
return hr;
}
总线消息处理专用线程等待消息
static void * message_thread_function(void *arg)
{
sigset_t set;
int sig;
siginfo_t info;
user_signal_t *usersig = (user_signal_t *)arg;
/* wait SIGUSER. process must block SIGUSER, otherwise, SIGUSER signal may be processed by other thread*/
sigemptyset(&set);
sigaddset(&set, SIGQUIT);//wait SIGQUIT, to exit thread
sigaddset(&set, SIGUSER);
while (true)
{
sig = sigwaitinfo(&set, &info);
if (sig < 0)
{
if (errno == EINTR)
{
perror("message thread: sigwaitinfo ");
continue;
}
printf("message thread: parent error: %s\n", strerror(errno));
break;
}
if (SIGQUIT == sig)
{
printf("message thread: receive quit signal\n");
break;
}
if (SIGUSER == sig)
{
user_signal_data_t *pdata = nullptr;
pdata = (user_signal_data_t *)info.si_ptr;
if (pdata)
{
if (USER_SIGNAL_CHECK_CODE == pdata->checkCode)
{
if (usersig->msgHandler)
{
usersig->msgHandler(pdata->message, pdata->data1, pdata->data2, g_userSig.userData);
}
free(pdata);
pdata = nullptr;
}
else
{
printf("message thread: receive unknown message data\n");
}
}
else
{
printf("message thread: receive data pointer is null\n");
}
}
}
printf("message thread: exit!\n");
return nullptr;
}
生产者线程产生并发送消息到总线
int message_post(int message, void *data1, void *data2)
{
if (!g_userSig.tidp)
return 0;
int hr = 0;
pid_t pid = getpid();
union sigval val;
user_signal_data_t *sig = (user_signal_data_t *)malloc(sizeof(user_signal_data_t));
memset(sig, 0, sizeof(user_signal_data_t));
sig->checkCode = USER_SIGNAL_CHECK_CODE;
sig->message = message;
sig->data1 = data1;
sig->data2 = data2;
val.sival_ptr = sig;
/* send signal to a specific thread. this can ensure signals are processed correctly. because:
* 1. only one thread processes all signals, so it makes asynchronous signal processing to become synchronously.
* 2. system functions like malloc, printf, etc. will not be interrupted by other signals, so they are reentrant in this prcocess.
*/
hr = pthread_sigqueue(g_userSig.tidp, g_userSig.signum, val);
if (0 != hr)
{
//EAGAIN
printf("message post: hr:%d, errorcode: %d, errorstr: %s\n", hr, errno, strerror(errno));
hr = E_PTHREAD_SIGQUEUE_FAILED;
}
return hr;
}
接收并处理消息
总线消息处理专用线程接收到消息
原理请参考:《Linux-UNIX系统编程手册》22.10 以同步方式等待信号
sig = sigwaitinfo(&set, &info);
...
usersig->msgHandler(pdata->message, pdata->data1, pdata->data2, g_userSig.userData);
总线消息处理函数
int message_handler(int message, void *msgData1, void *msgData2, void *userData)
{
CBaseService *service = (CBaseService *)userData;
service->on_message(message, msgData1, msgData2);
return S_OK;
}
消费者总线消息处理虚函数
int CBaseComsumerService::on_message(int message, void *msgData1, void *msgData2)
{
int hr = E_NOTIMPL;
return hr;
}
消费者消息处理实函数
int CComsumerService::on_message(int message, void *msgData1, void *msgData2)
{
int hr = S_OK;
hr = CBaseComsumerService::on_message(message, msgData1, msgData2);
if (SUCCEEDED(hr))
return hr;
//process messages related to myself only
switch (message)
{
case UM_MESSAGE_A:
on_msg_a(message, msgData1, msgData2);
break;
case UM_MESSAGE_B:
on_msg_b(message, msgData1, msgData2);
break;
default:
hr = E_NOTIMPL;
break;
}
return hr;
}
核心原理
??不可重入函数、异步信号不安全函数,均无法在信号处理函数中安全加以调用。因为这些原因,所以当多线程应用程序必须处理异步产生的信号时,通常不应该将信号处理函数作为接收信号到达的通知机制。相反,推荐的方法如下。
- 所有线程都阻塞进程可能接收的所有异步信号。最简单的方法是,在创建任何其他线程之前,由主线程阻塞这些信号。后续创建的每个线程都会继承主线程信号掩码的一份拷贝。
- 再创建一个专用线程,调用函数sigwaitinfo()、sigtimedwait()或sigwait()来接收收到的信号。当接收到信号时,专有线程可以安全地修改共享变量(在互斥量的保护之下),并可调用并非异步信号安全(non-async-signal-safe)的函数。也可以就条件变量发出信号,并采用其他线程或进程的通讯及同步机制。
??使用线程间信号来实现的进程内消息总线,就是依据此推荐方案来实现的。
sigprocmask阻塞所有异步消息
sigset_t newmask, oldmask;
//block SIGUSER for all threads
sigemptyset(&oldmask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSER);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
pthread_sigqueue发送消息
int message_post(int message, void *data1, void *data2)
{
...
hr = pthread_sigqueue(g_userSig.tidp, g_userSig.signum, val);
...
}
创建总线消息专用线程使用sigwaitinfo接收消息
static void * message_thread_function(void *arg)
{
while (true)
{
sig = sigwaitinfo(&set, &info);
...
usersig->msgHandler(pdata->message, pdata->data1, pdata->data2, g_userSig.userData);
...
}
return nullptr;
}
小结
??使用线程间信号来实现的进程内消息总线,基本实现了与Windows的PostMessage(SendMessage)函数功能类似的进程内消息总线的功能。本文的代码片断,主要用来帮助理解设计的原理和过程。有兴趣的朋友有自己基于这些片断,实现自己的业务功能。
原文地址:https://www.cnblogs.com/augustuss/p/12191189.html