嵌入式C开发中编程模型
风波驱动
风波驱动构架(Event-DrivenArchitecture)是一种用于设计应用的软件构架和模型,程序的执行流由外部风波来决定,它的特征是包含一个风波循环,当外部风波发生时使用反弹机制来触发相应的处理。主要包括4个基本组件:
许多现代应用设计都是由风波驱动的,风波驱动应用可以用任何一种编程语言来创建,由于风波驱动本身是一种编程技巧,而不是一种编程语言。
风波驱动构架可以最大程度降低耦合度,因而是现代化分布式应用构架的理想之选。
深入理解风波驱动
异步处理和主动轮训,要理解风波驱动和程序,就须要与非风波驱动的程序进行比较。实际上,现代的程序大多是风波驱动的,例如多线程的程序,肯定是风波驱动的。初期则存在许多非风波驱动的程序,这样的程序,在须要等待某个条件触发时,会不断地检测这个条件,直至条件满足,这是很浪费cpu时间的。而风波驱动的程序,则有机会释放cpu因而步入睡眠态(注意是有机会,其实程序也可自行决定不释放cpu),当风波触发时被操作系统唤起,这样才能愈发有效地使用cpu。IO模型,风波驱动框架通常是采用Reactor模式或则Proactor模式的IO模型。
Reactor模式其中特别重要的一环就是调用函数来完成数据拷贝,这部份是应用程序自己完成的,内核只负责通知监控的风波到来了,所以本质上Reactor模式属于非阻塞同步IO。
Proactor模式,利用于系统本身的异步IO特点,由操作系统进行数据拷贝,在完成以后来通知应用程序来取就可以,效率更高一些,并且底层须要依靠于内核的异步IO机制来实现,可能利用于DMA和Zero-Copy技术来实现,理论上性能更高。
当前Windows系统通过IOCP实现了真正的异步I/O,而在Linux系统的异步I/O还不健全,例如Linux中的boost.asio模块就是异步IO的支持linux查看进程,然而目前Linux系统还是以基于Reactor模式的非阻塞同步IO为主。风波队列,风波驱动的程序必将会直接或则间接拥有一个风波队列,用于储存无法及时处理的风波,这个风波队列,可以采用消息队列。风波串联,风波驱动的程序的行为,完全受外部输入的风波控制,所以风波驱动框架中,存在大量处理程序逻辑,可以通过事件把各个处理流程关联上去。次序性和原子化,风波驱动的程序可以根据一定的次序处理队列中的风波,而这个次序则是由风波的触发次序决定的,这一特点常常被用于保证个别过程的次序性和原子化。
风波驱动的缺点
常用的风波驱动框架
消息驱动
消息驱动和风波驱动很类似,都是先有一个风波,之后形成一个相应的消息,再把消息装入消息队列,由须要的项目获取。她们只是一些细微区别,通常都采用相同框架,细微的区别:
现代软件系统是跨多个端点运行并通过小型网路联接的分布式系统。比如,考虑一位民航公司顾客通过Web浏览器订购机票。该订单可能会通过API,之后通过一系列返回结果的过程。那些来回通讯的一个术语是消息传递。在消息驱动构架中,这种API调用看上去十分像一个函数调用:API晓得它在调用哪些,期盼某个结果并等待该结果。
消息驱动的优点
/** Structure used for queue entries that can be either simple messages, conditional or timed. */
typedef struct AppMessage
{
struct AppMessage *next; /**< point to next msg in message list */
uint32 due; /**< Millisecond time to deliver this message */
union
{
Task task; /**< Receiving task (if unicast) - typedef void (*Handler)(Task t, MessageId id, Message m); */
Task *tlist; /**< Ptr to receiving task list (if multicast) */
} t;
void *message; /**< Pointer to the message payload */
const void *condition_addr; /**< Pointer to condition value */
uint16 id; /**< Message ID */
CONDITION_WIDTH c_width; /**< Width of condition value(16bit or 32bit or unuse) */
unsigned int multicast:1; /**< If multicast, task is a null-terminated list */
} AppMessage;
/**
* Send a message after a time delay
* @param task Pointer to task(s) to deliver the message to
* @param multicast Whether 'task' is a pointer to a list or not
* @param id ID of the message
* @param message The message contents
* @param delay Number of milliseconds to wait before delivering the message
* @param c Optional pointer to a condition which must be zero for the
* message to be delivered.
* @param c_width Whether to test the condition as a 16 or 32-bit variable
* or @c CONDITION_WIDTH_UNUSED if @c c is NULL.
*/
void message_send_later(Task *task, bool multicast, uint16 id, void *message, uint32 delay, const void * c, CONDITION_WIDTH c_width)
{
uint32 timenow = get_milli_time();
a = pnew(AppMessage);
......
a->due = delay + timenow;
......
if(insert(a))
{
event_trigger();
}
......
}
/**
* Call from the scheduler with a message posted to this scheduler queue to
* cause a message to be delivered from the message queue. This function
* posts a new scheduler message if there is more work to be done (messages in the queue).
* ppriv Unused context
*/
void api_sched_msg_handler(void **ppriv)
{
AppMessage **p = &vm_message_queue;
/* Find the first message which isn't blocked on a condition */
while((a = *p) != 0)
{
const void * c = a->condition_addr;
if(c==0 || get_message_condition_value(c, a->c_width) == 0)
{
break;/* No condition or it's satisfied */
}
p = &a->next;
}
if(a)
{
/* Found a message */
uint32 now = get_milli_time();
int32 delta = VM_DIFF(a->due, now);
......
if(delta == 0)
{
......
/* Unlink the message from the queue */
*p = a->next;
/* Deliver the message to the handler(s) */
if (!a->multicast)
{
if(a->t.task && a->t.task->handler)
{
VALIDATE_FN_PTR(a->t.task->handler);
a->t.task->handler(a->t.task, a->id, a->message);
}
}
......
pfree(a);
event_trigger();
}
}
}
常用的消息驱动框架
风波驱动vs消息驱动
消息驱动的方式与风波驱动的方式一样有好多优点和缺点,但每种方式都有自己最适宜的情况。
数据驱动
数据驱动核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应当尽可能的将设计的复杂度从程序代码转移至数据。
事例:
假定有一个程序,须要处理其他程序发送的消息,消息类型是字符串,每位消息都须要一个函数进行处理。第一印象,我们可能会这样处理:
里面的消息类型取自sip合同(不完全相同,sip合同借鉴了http合同),消息类型可能就会降低。看着经常的流程可能有点累,检查一下中间某个消息有没有处理也比较费力,并且,每降低一个消息,就要降低一个流程分支。
根据数据驱动编程的思路,可能会这样设计:
下边这些思路的优势:
可读性更强,消息处理流程一目了然。更容易更改,要降低新的消息,只要更改数据即可,不须要更改流程。重用,第一种方案的好多的elseif虽然只是消息类型和处理函数不同,而且逻辑是一样的。下边的这些方案就是将这些相同的逻辑提取下来,而把容易发生变化的部份提及外边。
好多设计思路背后的原理似乎都是相通的,蕴涵在数据驱动编程背后的实现思想包括:
控制复杂度。通过把程序逻辑的复杂度转移到人类更容易处理的数据中来,进而达到控制复杂度的目标。隔离变化。像前面的事例,每位消息处理的逻辑是不变的,并且消息可能是变化的linux 事件触发,那就把容易变化的消息和不容易变化的逻辑分离。机制和策略的分离。和第二点很像,本书中好多地方谈到了机制和策略。上例中,我的理解,机制就是消息的处理逻辑,策略就是不同的消息处理。
数据驱动编程可以拿来做哪些
1.表驱动法(Table-Driven)
去除重复代码,考虑一个消息(风波)驱动的系统linux 事件触发雨林木风linux,系统的某一模块须要和其他的几个模块进行通讯。它收到消息后,须要依照消息的发送方,消息的类型,自身的状态,进行不同的处理。比较常见的一个做法是用三个级联的switch分支实现通过硬编码来实现:
switch(sendMode)
{
case:
}
switch(msgEvent)
{
case:
}
switch(myStatus)
{
case:
}
这些方式的缺点:
用表驱动法来实现
按照定义的三个枚举:模块类型,消息类型,自身模块状态,定义一个函数跳转表:
typedef struct __EVENT_DRIVE
{
MODE_TYPE mod;//消息的发送模块
EVENT_TYPE event;//消息类型
STATUS_TYPE status;//自身状态
EVENT_FUN eventfun;//此状态下的处理函数指针
}EVENT_DRIVE;
EVENT_DRIVE eventdriver[] = //这就是一张表的定义,不一定是数据库中的表。也可以使自己定义的一个结构体数组。
{
{MODE_A, EVENT_a, STATUS_1, fun1}
{MODE_A, EVENT_a, STATUS_2, fun2}
{MODE_A, EVENT_a, STATUS_3, fun3}
{MODE_A, EVENT_b, STATUS_1, fun4}
{MODE_A, EVENT_b, STATUS_2, fun5}
{MODE_B, EVENT_a, STATUS_1, fun6}
{MODE_B, EVENT_a, STATUS_2, fun7}
{MODE_B, EVENT_a, STATUS_3, fun8}
{MODE_B, EVENT_b, STATUS_1, fun9}
{MODE_B, EVENT_b, STATUS_2, fun10}
};
int driversize = sizeof(eventdriver) / sizeof(EVENT_DRIVE)//驱动表的大小
EVENT_FUN GetFunFromDriver(MODE_TYPE mod, EVENT_TYPE event, STATUS_TYPE status)//驱动表查找函数
{
int i = 0;
for (i = 0; i < driversize; i ++)
{
if ((eventdriver[i].mod == mod) && (eventdriver[i].event == event) &&
(eventdriver[i].status == status))
{
return eventdriver[i].eventfun;
}
}
return NULL;
}
月天校准:对给定年份和月份的天数进行校准(需分辨平年和闰月)
#define MONTH_OF_YEAR 12 /* 一年中的月份数 */
/* 闰年:能被4整除且不能被100整除,或能被400整除 */
#define IS_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))
/* 平年中的各月天数,下标对应月份 */
INT8U aDayOfCommonMonth[MONTH_OF_YEAR] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
INT8U ucMaxDay = 0;
if((OnuTime.Month == 2) && (IS_LEAP_YEAR(OnuTime.Year)))
ucMaxDay = aDayOfCommonMonth[1] + 1;
else
ucMaxDay = aDayOfCommonMonth[OnuTime.Month-1];
if((OnuTime.Day < 1) || (OnuTime.Day > ucMaxDay)
{
CtcOamLog(FUNCTION_Pon,"Month %d doesn't have this Day: %d(1~%d)!!!n",
OnuTime.Month, OnuTime.Day, ucMaxDay);
retcode = S_ERROR;
}
名称构造:按照WAN插口承载的业务类型(Bitmap)构造业务类型名称字符串。
/* 获取var变量第bit位,编号从右至左 */
#define GET_BIT(var, bit) (((var) >> (bit)) & 0x1)
const CHAR* paSvrNames[] = {"_INTERNET", "_TR069", "_VOIP", "_OTHER"};
const INT8U ucSvrNameNum = sizeof(paSvrNames) / sizeof(paSvrNames[0]);
VOID SetServerType(CHAR *pszSvrType, INT16U wSvrType)
{
INT8U ucIdx = 0;
for(; ucIdx < ucSvrNameNum; ucIdx++)
{
if(1 == GET_BIT(wSvrType, ucIdx))
strcat(pszSvrType, paSvrNames[ucIdx]);
}
}
版本控制:控制OLT与ONU之间的版本协商。ONU本地设置三比特控制字,其中bit2(MSB)~bit0(LSB)分别对应0x21、0x30和0xAA版本号
pstSendTlv->ucLength = 0x1f;
if (gOamCtrlCode == 0)
{
vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);
pstSendTlv->aucVersionList[3] = 0x30;
vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);
pstSendTlv->aucVersionList[7] = 0x21;
vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
pstSendTlv->aucVersionList[11] = 0x20;
vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);
pstSendTlv->aucVersionList[15] = 0x13;
vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);
pstSendTlv->aucVersionList[19] = 0x01;
vosMemCpy(&(pstSendTlv->aucVersionList[20]), ctc_oui, 3);
pstSendTlv->aucVersionList[23] = 0xaa;
}
else if (gOamCtrlCode == 1)
{
vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);
pstSendTlv->aucVersionList[3] = 0x30;
vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);
pstSendTlv->aucVersionList[7] = 0x21;
vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
pstSendTlv->aucVersionList[11] = 0x20;
vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);
pstSendTlv->aucVersionList[15] = 0x13;
vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);
pstSendTlv->aucVersionList[19] = 0x01;
}
//此处省略gOamCtrlCode == 2~6的处理代码
else if (gOamCtrlCode == 7)
{
vosMemCpy(&(pstSendTlv->aucVersionList), ctc_oui, 3);
pstSendTlv->aucVersionList[3] = 0x20;
vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);
pstSendTlv->aucVersionList[7] = 0x13;
vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
pstSendTlv->aucVersionList[11] = 0x01;
}
消息处理:终端输入不同的复印命令,调用相应的复印函数,以控制不同级别的复印
typedef struct{
OAM_LOG_OFF = (INT8U)0,
OAM_LOG_ON = (INT8U)1
}E_OAM_LOG_MODE;
typedef FUNC_STATUS (*OamLogHandler)(VOID);
typedef struct{
CHAR *pszLogCls; /* 打印级别 */
E_OAM_LOG_MODE eLogMode; /* 打印模式 */
OamLogHandler fnLogHandler; /* 打印函数 */
}T_OAM_LOG_MAP;
T_OAM_LOG_MAP gOamLogMap[] = {
{"all", OAM_LOG_OFF, noanylog},
{"oam", OAM_LOG_OFF, nologOam},
//... ...
{"version", OAM_LOG_OFF, nologVersion},
{"all", OAM_LOG_ON, logall},
{"oam", OAM_LOG_ON, logOam},
//... ...
{"version", OAM_LOG_ON, logVersion}
};
INT32U gOamLogMapNum = sizeof(gOamLogMap) / sizeof(T_OAM_LOG_MAP);
VOID logExec(CHAR *pszName, INT8U ucSwitch)
{
INT8U ucIdx = 0;
for(; ucIdx < gOamLogMapNum; ucIdx++)
{
if((ucSwitch == gOamLogMap[ucIdx].eLogMode) &&
(!strcasecmp(pszName, gOamLogMap[ucIdx].pszLogCls));
{
gOamLogMap[ucIdx].fnLogHandler();
return;
}
}
if(ucIdx == gOamLogMapNum)
{
printf("Unknown LogClass(%s) or LogMode(%d)!n", pszName, ucSwitch);
return;
}
}
这些技巧的益处:
Unix设计原则中的“分离原则”和“表示原则”
在《Unix编程艺术》和《代码大全2》中均觉得人类阅读复杂数据结构远比复杂的控制流程容易,即相对于程序逻辑,人类更擅长于处理数据。
数据驱动编程可以应用于:
函数级设计,如本文示例。程序级设计,如用表驱动法实现状态机。系统级设计,如DSL。2.基于数据模型编程数据驱动思索总结
本文大部份节选嵌入式中重要的编程模型,少量自己更改整合而至!