系统概念类
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1.8
1.9
1.10
1.11
设置类
模块任务类
3.1
3.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.10
系统概念类
1.1 什么是 ZOS ?
ZOS 是 Zero Operating System 的简称,是菊风公司的软件开发基础平台,所有协议栈和业务组件都是基于 ZOS 开发而成。
通过 ZOS 对多种操作系统和处理器的支持,使得基于 ZOS 的软件产品能够独立于操作系统和处理器环境。同时,ZOS 抽象了协议栈和业务组件需要的通用操作能力,通过功能丰富的接口使得上层软件的开发能够提高效率、增强软件设计的一致性和实现代码复用率。
1.2 ZOS 跟操作系统的区别?
ZOS 不是一个操作系统,ZOS 的运行完全依赖操作系统提供的线程驱动机制,同时也依赖操作提供的重要输入输出、网络传输等功能。而且,ZOS 没有独立的编译环境,确切的说, ZOS 是操作系统上的一个应用程序。
操作系统有多种多样的,不同的操作系统的能力都是不尽相同,比如 Windows 和 Linux 下的临界区操作的接口函数就不一样,消息对列机制、计时器功能也是大不相同,因此如果要开发运行在多操作系统上运行的程序,势必考虑多系统的兼容性问题。
ZOS 作为系统开发平台,能够很好的屏蔽多种操作系统的不同点,比如在不同的操作系统,可以使用完全相同的消息队列接口实现线程间的通信。同时,ZOS 提供了很多操作系统没有提供的操作接口,尤其是针对通信协议栈和业务组件的软件特性。比如 ZOS 提供了丰富的缓冲区操作接口,业务状态机接口等功能。
1.3 ZOS 是否就是操作系统的适配层?
正如 1.2 所述,ZOS 不仅仅是提供了操作适配的功能,还提供了大量了针对通信协议栈和业务组件开发的功能接口,比如丰富的缓冲区管理、计时器管理等功能。
ZOS 的大部分功能(80%以上)都不是简单对操作系统相关的接口封装,很多功能接口在大多数操作系统中都是不存在的,是菊风公司的多年软件设计的经验总结。
在 ZOS 的500左右的接口中,只有100个不到属于操作系统的兼容封装。即使如此,这些接口都是在满足功能目的的前提下,代码也是自主实现的。
1.4 ZOS 提供哪些功能?
ZOS 实现了如下功能:
• 操作系统的兼容接口(包括 Mutex, Semaphore, Task, Time, Socket,File 等)
• 内存池管理(包括 Bucket Pool、Power Pool 等)
• 缓冲区管理(包括 Data Buffer, Pipe Buffer, Encode Buffer, Aggregation Buffer 等)
• 计时器管理(包括 Queue Timer, Ring-Matrix Timer)
• 任务管理(包括 Task, Module)
• 消息队列
• 状态机管理
• 资源跟踪管理(Memory Dump, Buffer Dump, Fsm Dump等)
• 其他常用接口(包括List, Hash, String 等)
1.5 ZOS 是否可以定制化?
ZOS 可以根据业务需要进行适当的定制化,比如有些测试目的的程序,可能并不需要任务、消息对接、计时器等功能。用户可以根据业务需要,通过 Zos_SysInit 或 Zos_SysInitX 来启动 ZOS 系统。
同时 ZOS 提供了大量配置接口,使得用户可以根据业务需求设置运行参数,比如线程的默认堆栈大小、内存池排列等。
1.6 ZOS 移植到操作系统上方便吗?
ZOS 对于大多数通信业务而言对操作系统的依赖已经将为最少的要求,在操作系统上主要需要保证一下功能是否满足:
• 支持 Thread
• 支持 Mutex 和 Semaphore
• 支持 Socket 或者其他网络传输接口
• 支持获取高精度时间等时间操作
• 支持文件操作(如果应用软件有文件操作需求)
• 支持内存堆的申请和释放
以上几点是操作系统移植的关键要求,在此基础上,通过比较《ZOS 可移植性问讯表》确认其他次要功能。
总体来说,ZOS 对于操作系统的可移植性工作量并不大,还是蛮方便的。
1.7 ZOS 系统能安全的退出?
ZOS 在实现的时候已经考虑了安全退出,不仅保证线程的退出,同时也能完整的释放从操作系统中申请的资源(如内存、互斥锁、信号量等)。
在内存资源有限的操作系统,尤其是在一些需要应用层显示的释放资源的系统,比如一些手机的应用开发,就需要应用软件能够安全的退出,这样的操作甚至是非常频繁的。
ZOS 是通过 Zos_SysDestroy 接口退出的。对于任务退出,要求用户死循环运行的任务显式的退出,否则 ZOS 在退出的时候,等待一段时间后会强行关闭任务,在一些操作系统中是不建议如此的。
1.8 ZOS 系统能完整的释放内存?
ZOS 能完整的释放从操作系统中申请的资源,包括内存资源。基于 ZOS 的应用软件,如果都是通过 ZOS 提供的内存管理接口,ZOS 都完整的记录下来,在退出的时候会安全而且完整的释放。
1.9 ZOS 系统包括哪些线程?
ZOS 系统如果是通过 Zos_SysInit 启动,会存在 TIMER 线程。如果 Log 的线程配置为运行状态的情况下,ZOS 还会启动 Log 线程,以周期性的时间把日志写入文件中。
通过 Zos_SysInitX 启动则不会任何线程。
1.10 如何获取错误码?
ZOS 所属的系统接口基本上都提供了失败错误码管理,用户在操作失败的时候,通过 Zos_ErrnoGet 来获取失败的错误码,通过 Zos_ErrnoPrint 就可以把错误码打印出来。
1.11 什么是 ZPLATFORM 宏?
ZPLATFROM 是 ZOS 的编译宏,是为了区分不同的操作系统。用户在编译是,需要指定具体的宏值,比如 ZPLATFORM = ZPLATFOMR_WIN32。
ZOS 定了的操作系统宏值有以下几种:
| /* operating system type */ |
| #define ZPLATFORM_WIN32 |
1 |
/* windows */ |
| #define ZPLATFORM_WINCE |
2 |
/* windows ce */ |
| #define ZPLATFORM_VXWORKS |
3 |
/* vxworks */ |
| #define ZPLATFORM_THREADX |
4 |
/* threadx */ |
| #define ZPLATFORM_LINUX |
5 |
/* linux */ |
| #define ZPLATFORM_FREEBSD |
6 |
/* freebsd */ |
| #define ZPLATFORM_SOLARIS |
7 |
/* solaris */ |
| #define ZPLATFORM_ARENA |
8 |
/* arena */ |
设置类
2.1 如何获取错误码?
ZOS 参数配置接口定义在 zos_syscfg.h 文件中。主要是分系统启动前生效和动态生效配置接口。
动态生效的配置接口比较,主要是以下等接口:
• os_SysCfgSetIsSuptAssert
• os_SysCfgSetPrintDisp
• os_SysCfgSetLogDisp
• os_SysCfgSetLogLevel
• os_SysCfgSetLogPrint
• os_SysCfgSetDftStackSize
其他接口即使能动态生效,但是可能会系统存在潜在风险,是不建议在 ZOS 系统运行后设置的。比如 Zos_SysCfgSetHeapMalloc 和 Zos_SysCfgSetHeapFree,如果设置的操作系统堆内存申请和释放接口跟 ZOS 启动时设置不一样,会导致启动分配的资源无法安全释放,甚至会引起系统崩溃。
对于系统启动前生效的配置接口,用户需要按照在 Zos_SysInit 或 Zos_SysInitX 系统启动前设置。比如以下代码设置 Log 文件的大小:
| /* reset the log parameters */ |
| Zos_SysCfgSetLogFile(“myapp.log”); |
/* log file name */ |
| Zos_SysCfgSetLogBufSize(10240); |
/* log inner buffer size */ |
| Zos_SysCfgSetLogFileSize(102400); |
/* log file limit size */ |
| /* start the zos system */ |
| Zos_SysInit(); |
2.2 如何配置内存块?
ZOS 可以配置的内存池块主要有以下几种:
• 通用内存池 (通过 Zos_SysCfgSetMemBkt 设置)
• 消息队列专用内存池 (通过 Zos_SysCfgSetMsgBkt设置)
• 数据缓冲区专用内存池 (通过 Zos_SysCfgSetDbufBkt设置)
• 幂内存池(特殊内存池,通过 Zos_SysCfgSetPMemBkt)
另外,各个内存池可以通过配置接口设置动态释放的能力,这样能够保证最小的内存需要,
但是可能会引起系统过多的内存碎片(尤其是小块内存比较多的情况)。
ZOS 提供了一些手段可以分析系统在运行尖峰时刻对内存开销的情况,通过这些统计数字,可以最优化配置这些内存资源。关于这些分析接口主要有:
• VOID Zos_MemDbgShow(); (通用内存池的资源统计信息)
• VOID Zos_MsgDbgShow(); (显示消息队列专用内存池的资源统计信息)
• ZVOID Zos_DbufDbgShow(); (通用数据缓冲区专用内存池的资源统计信息)
• VOID Zos_PMemDbgShow(); (通用幂内存池的资源统计信息)
下面是一个电话机程序的内存自定义内存池的例子:
/* jpda memory bucket config info group */ |
static ST_ZOS_BKT_INFO m_astJpdaMemBktInfoGrp[] = |
{ |
/* size, |
maximum count, |
increment count */ |
{32, |
128, |
128}, |
{64, |
32, |
64}, |
{128, |
32, |
32}, |
{256, |
20, |
16}, |
{512, |
36, |
8}, |
{1024, |
16, |
4}, |
{2048, |
20, |
2}, |
{4096, |
16, |
1}, |
{8192, |
5, |
1}, |
}; |
/* jpda message bucket config info group */ |
static ST_ZOS_BKT_INFO m_astJpdaMsgBktInfoGrp[] = |
{ |
/* size, |
maximum count, |
increment count */ |
{32, |
0, |
128}, |
{64, |
0, |
64}, |
{128, |
0, |
32}, |
{256, |
0, |
16}, |
{512, |
0, |
8}, |
}; |
/* jpda dbuf bucket config info group */ |
static ST_ZOS_BKT_INFO m_astJpdaDbufBktInfoGrp[] = |
{ |
/* size, |
maximum count, |
increment count */ |
{128, |
0, |
32}, |
{256, |
32, |
16}, |
{512, |
0, |
8}, |
{1024, |
16, |
4}, |
{2048, |
0, |
2}, |
{4096, |
0, |
1}, |
{8192, |
0, |
1}, |
}; |
/* jpda power memory bucket config info group */ |
static ST_ZOS_BKT_INFO m_astJpdaPMemBktInfoGrp[] = |
{ |
/* size, |
maximum count, |
increment count */ |
{256, |
0, |
16}, |
{512, |
0, |
270}, |
{1024, |
0, |
160}, |
{2048, |
0, |
2}, |
{4096, |
0, |
1}, |
{8192, |
0, |
1}, |
}; |
/* jpda resource config */ |
ZINT Jpda_ResCfg() |
{ |
/* set the memory block config */ |
Zos_SysCfgSetMemBkt(ZOS_GET_TABLE_SIZE(m_astJpdaMemBktInfoGrp), |
m_astJpdaMemBktInfoGrp); |
/* set the message block config */ |
Zos_SysCfgSetMsgBkt(ZOS_GET_TABLE_SIZE(m_astJpdaMsgBktInfoGrp), |
m_astJpdaMsgBktInfoGrp); |
/* set the dbuf block config */ |
Zos_SysCfgSetDbufBkt(ZOS_GET_TABLE_SIZE(m_astJpdaDbufBktInfoGrp), |
m_astJpdaDbufBktInfoGrp); |
/* set the power memory block config */ |
Zos_SysCfgSetPMemBkt(ZOS_GET_TABLE_SIZE(m_astJpdaPMemBktInfoGrp), |
m_astJpdaPMemBktInfoGrp); |
return ZOK; |
} |
ZINT main() |
{ |
/* resource config */ |
Jpda_ResCfg(); |
/* zos system init */ |
if (Zos_SysInit() != ZOK) |
return -1; |
/* run zos shell */ |
if (Zsh_Run(ZNULL) != ZOK) |
{ |
Zos_SysDestroy(); |
return -1; |
} |
/* destroy the system */ |
Zos_SysDestroy(); |
return 0; |
} |
2.3 如何设置打印重定向?
ZOS 的打印主要有两种,一个是 Log 打印,另外一个就是 Zos_Printf 打印,也就是通过 Log 和 Zos_Printf 都可以把所需相关的内容显示在屏幕(如 Windows Console)上,其默认方式都是通过最后的 printf 标准函数打印出来。
Log 和 Zos_Printf 都是通过 printf 打印的,但是这种默认方式可以通过控制来关闭甚至重定向。
Log 可以通过 Zos_SysCfgSetLogDisp 设置打印重定向,比如图形窗体(Dialog)中无法显示 printf 的内容,但是可以通过重定向把打印内容发消息给指定的窗体,然后通过窗体程序的接口显示在窗体中。同样,Zos_SysCfgSetPrintDisp 可以设置 Zos_Printf 的打印重定向。
在重定向回调中,建议不要使用 ZOS 的接口来传递打印内容,因为在异常情况下(比如内存资源严重不足),此时使用 ZOS 的一些接口(如 Zos_Malloc)会导致失败,而失败后如果进行 Zos_Printf 打印,,这样就引起恶性循环。当然这种情况并不常见,但是必须要注意。
下面是一个在 Window MFC 中显示打印信息的回调例子:
/* jwphone print */ |
ZINT Jwpe_Print(ZCHAR *pcStr) |
{ |
ZCHAR *pcStrBuf; |
ZULONG dwLen; |
/* get the length of string */ |
dwLen = Zos_StrLen(pcStr); |
/* malloc the message memory */ |
pcStrBuf = malloc(dwLen); |
if (!pcStrBuf) return ZFAILED; |
/* copy message into buffer */ |
memcpy(pcStrBuf, pcStr, dwLen); |
/* send message to user page */ |
if (PostMessage(JWPE_HDLGDBG, WM_APP_DBG_PRINT, |
(WPARAM)pcStrBuf, (LPARAM)dwLen) == 0) |
{ |
free(pcStrBuf); |
return ZFAILED; |
} |
return ZOK; |
} |
/* set print redirect functions */ |
Zos_SysCfgSetPrintDisp(Jwpe_Print); |
Zos_SysCfgSetLogDisp(Jwpe_Print); |
2.4 配置中的很多任务 ID 是干什么的?
在 zos_syscfg.h 文件中定了了很多模块和任务的常量宏(关于模块和任务参见第三章),如下:
| /* zos modules definition, default instance number is 5 */ |
| #define ZMODID_SYS |
0 |
/* system module id */ |
| #define ZMODID_NET1 |
1 |
/* net 1 module id */ |
| #define ZMODID_NET2 |
2 |
/* net 2 module id */ |
| #define ZMODID_PROTO1 |
3 |
/* protocol 1 module id */ |
| /* zos system task id */ |
| #define ZTASKID_ROOT |
ZTASKID_MAKE(ZMODID_SYS, 0) |
| #define ZTASKID_TIMER |
ZTASKID_MAKE(ZMODID_SYS, 1) |
| #define ZTASKID_LOG |
ZTASKID_MAKE(ZMODID_SYS, 2) |
| /* zos network task id */ |
| #define ZTASKID_UTAL |
ZTASKID_MAKE(ZMODID_NET1, 0) |
| #define ZTASKID_DNS |
ZTASKID_MAKE(ZMODID_NET1, 1) |
| /* zos protocol task id */ |
| #define ZTASKID_SIP |
ZTASKID_MAKE(ZMODID_PROTO1, 0) |
| #define ZTASKID_H323 |
ZTASKID_MAKE(ZMODID_PROTO1, 1) |
这些宏值其实都为菊风的协议栈和业务组件准备的,比如 ZMODID_PROTO1 是协议栈模块,ZTASKID_SIP 则是协议栈模块中的成员任务。对于 ZOS 而言,其实只需要一下模块和任务就可以了:
| #define ZMODID_SYS |
0 |
/* system module id */ |
| /* zos system task id */ |
| #define ZTASKID_ROOT |
ZTASKID_MAKE(ZMODID_SYS, 0) |
| #define ZTASKID_TIMER |
ZTASKID_MAKE(ZMODID_SYS, 1) |
| #define ZTASKID_LOG |
ZTASKID_MAKE(ZMODID_SYS, 2) |
其他对于 ZOS 来说的都是多余的。至于为何其他无关的内容定义于此处,因为往往应用都是需要跟协议栈和业务组件结合在一起,尤其是对于那些使用这些库的用户,因此把菊风组件的相关任务 ID 定义于此就可以通用了。
对于大多数应用来说(业务应用是基于协议栈上直接开发),ZMODID_APP1、ZMODID_FRAME1、ZMODID_DEV1、ZMODID_MGMT1 这些模块相关的都是可以从 zos_syscfg.h 中去除的,然后可以自定义应用相关的模块 ID 和任务 ID。
如果使用 ZOS 的任务,ZMODID_SYS 是不可去除的。如果使用 UTAL(通用传输层), DNS 等模块,ZMODID_NET1/2 是不可去除的,同样对于对应的协议栈也是如此。如果越是后面定义的模块和任务,之前的信息越是要小心谨慎去除,因为如果往往协议栈跟网络模块是存在依赖关系的。
其实,为什么谈到用户自定义 zos_syscfg.h 的模块和任务,目的是有时候能节约一些内容。因为系统是在启动时,根据 Zos_SysCfgSetModCount 和 Zos_SysCfgSetInstNum 设置的值来分配资源的,默认是16个模块,每个模块5个实例,即 60 个任务资源。而有些简单的应用,不需要这么多模块和任务,这样可以少浪费一些资源。
如果用自定义的模块 ID 和任务中的实例 ID 超过了 Zos_SysCfgSetModCount 和 Zos_SysCfgSetInstNum 所设置的值,则使用任务或模块相关的接口都会失败。
模块任务类
3.1 什么是模块和任务?
ZOS 为了统一管理所有由操作系统创建的线程,并且根据其应用目的,以模块和任务的形式来管理。每个模块可以存在多个任务实例,任务表示某个模块中的某个实例,也就是说实例本质上就抽象为任务。
模块通过一个 ID 值(整数值)来标识,称为模块 ID,同样,任务有任务 ID,实例有实例 ID。任务 ID 是模块 ID 和 实例 ID 可以通过 ZTASKID_MAKE 来组合产生。模块 ID 和实例 ID 可以从任务 ID 中通过 ZTASKID2MODID 和 ZTASKID2INSTID 来分解。
3.2 为什么采用模块和任务的管理方式?
首先一个业务应用是可以分解成具体的功能模块,而且这些模块都有其功能范围,这说明任何应用的模块设计都是可控的、可设计的;其次,模块之间往往需要进行业务通信,比如 UTAL 和 SIP 之间需要传递 SIP 传输的报文,因此对于任务通信来说,互相了解身份就非常重要;最后,从产品角度来看,不仅要能合理管理模块,同时也要对模块的重要资源进行分析,比如业务通信的流量、计时器的使用情况等信息。因此采用模块和任务的方式来管理线程是一种非常理想的管理手段。
另外,从操作系统的角度看,操作系统为了通用性,不仅没有提供此种管理方式,同时线程无论是其标识,还是其他信息,都很难以模块化的方式来管理,因此软件设计师往往需要其他方式来管理这些线程。
3.3 如何定义模块和任务?
模块和任务都是通过模块 ID, 任务 ID 来标识,菊风提供的业务组件都是定义在 zos_syscfg.h 中。
用户可以根据业务所需的实际软件规划,通过定义 模块 ID,然后使用 ZTASKID_MAKE 来产生任务 ID。如下面的例子:
| #define ZMODID_MYAPP |
12 |
/* my application module id */ |
| #define ZTASKID_MYTASK1 |
ZTASKID_MAKE(ZMODID_MYAPP, 0) |
| #define ZTASKID_MYTASK2 |
ZTASKID_MAKE(ZMODID_MYAPP, 1) |
3.4 任务和线程有什么区别?
任务是通过操作系统的线程创建的,因此任务本质上等于线程(但是这也完全取决于跟操作系统的移植情况)。但是任务不仅仅是一个运行中的线程,同时也是一个应用可管理的业务概念。而且,任务的驱动入口和线程的驱动入口实际上不同的,而从用户角度则是完全感觉不到。
3.5 一般任务和模块任务有什么区别?
ZOS 的任务有两种,一种是一般任务,另外一种就是模块任务。一般任务实际上就是把任务的驱动入口直接线程驱动,并且由用户负责循环使用(往往都是死循环)。而模块任务是用户只需关心收到的消息,完全感觉不到线程的运行,如果没有收到任务消息,任务处于休眠状态。
从系统退出的角度,一般任务因为是用户维护死循环,因此往往需要用户显示的关闭任务,否则系统退出的时候,会等待一段时间,然后强行关闭任务(这会存在未知的风险)。而模块任务,完全受控于 ZOS,会安全及时的退出。
从业务应用角度来说,如果模块处于软件架构中的中上层次,而且其业务功能是受其他业务模块驱动的,那么设计成模块任务是一种非常理想的选择。如果模块的功能比较单一话,需要连续不断的处理,比如传输、媒体采样等处理,这时候采用一般任务,以死循环加一些休眠手段就是比较理想的选择。
3.6 如何创建和销毁任务?
ZOS 的任务可以动态的创建和销毁。一般任务需要通过 Zos_TaskSpawn 来创建。而模块任务首先需要通过 Zos_ModReg 或 ZOS_MOD_START 来开始注册,然后通过 Zos_ModStart 或 ZOS_MOD_START 来完成启动过程的。模块任务在启动的时候会 PFN_ZINSTINIT 所指向的回调函数来初始化用户资源,如果用户初始化函数失败,则创建任务就失败了。
对于任务的销毁,一般任务最好用户主动退出线程,这是安全和最佳的方式,否则 Zos_TaskDelete 删除时,等待一段时间后(系统设置为 2秒钟),系统会强行删除任务。对于模块任务,Zos_ModDereg 或 ZOS_MOD_DEREG 会安全的删除任务,当然,模块任务也可以通过 Zos_TaskDelete 删除。 模块任务在销毁中,最好在 Zos_ModReg 创建时在指定的销毁回调函数,在销毁过程中就会释放掉启动任务时申请的系统资源(如内存、文件句柄等)。
3.7 任务能动态的创建和销毁吗?
ZOS 的任务可以动态的创建和销毁。对于动态创建,需要保证任务 ID 所指的任务没有已创建的任务,同时还要保证任务 ID 是有效范围的值。
3.8 一般使用什么任务优先级?
ZOS 任务创建时优先级是由用户指定,一般建议用 ZTASK_PRIORITY_NORMAL,对于不怎么重要的,而且不怎么运行,实时性要求不高的,可以考虑使用 ZTASK_PRIORITY_MIN;如果实时性要求非常高,为多个模块服务器的可以考虑 ZTASK_PRIORITY_MAX 优先级。用户完全可以通过对应用程序多次试验,然后确定理想的优先级值。
在 ZOS 中,像 Log 任务,因为每隔1分钟(默认值)才运行一次,而且只是负责把缓冲区中的日志写入文件,然后就进入休眠状态,所以默认设置为 ZTASK_PRIORITY_MIN。
3.9 一般使用多大的堆栈大小?
ZOS 任务创建时堆栈大小是由用户指定的,如果传入的值为0,则会采用系统配置的值(默认值是 8K)。用户可以通过 Zos_SysCfgSetDftStackSize 设置默认的堆栈大小。
堆栈的大小设计取决于以下条件:
• 是否存在较深的递归函数或处理过程?
• 是否存在需要大块局部数组的函数?
• 是否存在需要单个或累加比较大的局部数据结构?
对于一般的程序来说,如果上面3个条件都要求不大的,8K 基本上是足够的。检验堆栈大小是否足够的方法就是通过运行检验,同时保留一定的余地。
对于堆栈要求比较大的,尤其是局部数组比较大的,建议采用动态或者静态内存来替代。
3.10 一般使用多长的消息队列?
对于一般任务而言,其实消息队列是可有无,也就是往往处于发送者的角度,接受其他模块消息的情况比较少。
模块任务都是需要消息队列,因为没有消息队列也就失去了模块任务存在的意义了。如果没有指定消息对列长度,任务虽然不会失败,但也是会创建长度为1的消息队列。
至于多长的消息队列比较合适,主要是跟任务其他任务的通信量,选择一个通信量最大的值,同时保留一定的余地。一个任务消息队列使用情况可以在运行过程中通过 Zos_TaskDbgShow 函数来打印相关信息,这是有效分析消息队列需求的手段。