Category Archives: Laboratory

实时操作系统杂谈

实时操作系统 (RTOS) 在工业控制、航空航天和电力系统中有大量应用。我自己接触实时操作系统已有几年时间,虽然实际的项目使用经验不多,然而耳濡目染还是积累了一些经验和知识。在这里整理和分享给大家。

实时操作系统(Real-time Operating System, RTOS)是针对有实时性要求的应用而设计的操作系统。这些应用通常包括汽车引擎控制、轨道交通、工业机器人、飞行器控制系统等。实时操作系统一般提供抢占式调度机制,重要的高优先级任务可以剥夺低优先级任务对CPU的使用权;同时,任务在等待使用资源时,RTOS可以将其CPU的使用权释放给其他就绪的任务,从而使得系统的总体响应速度更快。

1. 常见的RTOS

目前市场上常见的商用实时操作系统有:

  • uCosII / uCosIII | Micrium
  • FreeRTOS
  • Nucleus RTOS | Mentor Graphics
  • RTLinux (需要MMU支持)
  • QNX (需要MMU支持)
  • VxWorks | WindRiver
  • eCos
  • RTEMS

其中除了FreeRTOS, RTEMS和RTLinux是免费的之外,其余RTOS都是需要商业授权的。uCos II和FreeRTOS是平时接触比较多的RTOS,相关资料比较多。而VxWorks是安全性公认最佳的,用于航空航天、轨道交通和卫星的应用。如果系统中需要使用复杂的文件、数据库、网络等功能,那么以Linux为基础的RTLinux是比较好的选择;但是如果系统对实时性和确定性的要求非常高,那么可以使用较为简单的RTOS(如 uCosII),再根据需要开发通信协议或者软件包。总体上来说,操作系统的复杂性是与应用软件的复杂性一致的。同时,功能上更复杂的RTOS对硬件系统资源的需求也会更高。

2. RTOS的功能

一般的RTOS会提供以下全部或部分功能:

  • 基于静态优先级(fixed-priority)的抢占式(preemptive)任务调度;
  • 进程间通信(基于消息,消息邮箱,管道);
  • 基于信号量(semaphore)的进程间同步;
  • 任务的创建、暂停、删除;
  • 资源访问控制(并发控制与防止互锁);
  • 临界区(critical section)控制;
  • 驱动程序的管理与接口;
  • MMU内存管理、内存动态申请与分配;
  • 其他功能:如GUI用户界面和TCP/IP相关功能。

3. 选用指标

一般来说,实时操作系统的主要参数指标有:

  • 支持的优先级数量,如64、128或256;
  • 使用的任务调度算法:时间片轮转调度,加权轮转调度(weighted round-robin),先入先出(FIFO),优先级调度;任务调度算法决定了任务响应时间的可分析性;
  • 中断响应速度,即从中断产生到进入中断服务程序的时间;
  • 上下文切换时间,即任务切换时间;
  • 操作系统大小以及资源使用(footprint, ROM及RAM的占用);
  • 授权费用与授权方式,是按产品型号计费、产品数量计费还是一次性授权。

其他的选择指标主要就是文档的完整程度,是否有GUI支持,团队对该OS的了解程度,所支持的CPU型号以及需求功能的规模,以及相应的技术支持。从更专业的角度上来说,还有是否支持防死锁(deadlock)和优先级反转(priority inversion)等提高系统可靠性的功能,操作系统自身服务程序占用的时间大小。对于时间关键性应用,操作系统需要具备相对确定的执行时间(deterministic execution time)。从调试的角度来说,操作系统是否具有调试功能(尤其是多线程、多核)以及支持的调试工具也是重要的指标。

4. 关于RTOS的误区

最后说一下关于实时操作系统的两个误区:

1、用了实时系统后,系统响应速度一定更快。不一定。因为实时操作系统本身引入了执行开销,所以对于小型应用来说,有RTOS的性能也许不如无操作系统的情况。实时操作系统的优势最能体现在中大型系统中,当任务间存在复杂的耦合和依赖关系,并且应用程序经常要长时间等待外部资源时。

2、用实时操作系统就可以保证实时性。不一定。相对来说,使用实时系统可以改善系统的实时性。但是实时操作系统只是作为工具存在的,如果需要提供实时性保障,还需要使用实时系统理论对任务的可调度性和响应时间进行分析,才可以得到科学、系统的响应性保障。

修订历史

  • 2018-05-22 (v1.1) 修改了部分内容,为知乎专栏调整了版式
  • 2015-08-17 (v1.0) 初版,在云飞机器人实验室发表

Linux系统全方位调试与维护工具整理

俗话说:“工欲善其事,必先利其器。”

在多年维护和开发Linux服务器的过程中,我接触了一些常用的开发和调试工具。本文将这些工具整理在一起,便于指导读者选择正确的工具。文中对于工具只有简单的介绍,需要详细的用法可以--help或自行Google。

本文针对的环境是CentOS/RedHat服务器 (需要epel库)。其他发行版应该也有相应的命令,但我没有一一测试。

系统管理

  • ssh: 远程登录
  • last, lastlog: 查看用户登录历史
  • cron: 设置周期运行的定时任务
  • at: 设置运行一次的定时任务
  • screen/nohup: 在后台运行程序,程序不会随着shell关闭而退出
  • rsync: 文件夹同步
  • pv: 文件拷贝(重定向方法,含进度条,但会丢失文件的权限信息)
  • lshw: 显示硬件信息
  • lscpu: 显示CPU信息
  • dmesg: 显示系统日志

资源监控

通过查看系统资源使用,快速定位性能瓶颈与系统异常。

  • 内存
    • free: 当前内存使用情况
  • 硬盘
    • df: 硬盘空间使用情况
    • iotop: 磁盘IO使用实时监视
    • ncdu: 分析文件夹使用的磁盘大小
  • CPU
    • 见进程调试
  • 网络
    • nethogs: 监控当前活跃的网络连接
    • nload: 主机上传/下载流量监控
  • 综合
    • glances: 系统资源、进程查看工具(我的最爱)
    • linux-dash/pyDash: 系统资源监控(基于网页)

内核调试

  • perf
  • kgdb: 内核调试工具
  • lsmod: 显示所有被使用的module
  • vmstat

进程调试

  • ps: 查看当前进程
  • top: 查看当前进程以及进程资源占用情况(动态更新)
  • htop: top替代工具,信息丰富,可以查看树状进程信息
  • pidof: 按进程名查PID
  • skill: kill的升级版,通过名字(而不是PID)kill进程

文件调试

  • tree: 将文件夹按树形显示
  • lsof: 显示当前程序文件(包括虚拟文件)打开情况
  • mc: Midnight Commander知名文件管理器

网络调试

  • curl: 发送HTTP, REST请求
  • tcpdump: 抓包工具,通过自带的与或非逻辑可以组合成复杂的过滤条件
  • wireshark: 类似tcpdump的老牌抓包神器
  • nmap: 网络开放端口扫描
  • ip: 网络管理工具集
  • fping: ping升级版
  • netstat
  • nicstat
  • iptraf

程序调试

  • gdb: gnu项目下的代码调试器
  • strace: 打印出进程调用了哪些系统调用。
  • ldd: 查看程序依赖库。
  • objdump: 打印目标码和反汇编结果。
  • xargs: 读取输入流并将读取的文本作为指定命令行工具的命令行参数。
  • ipcs: 显示进程间通信设施状态

其他工具

  • vim/nano: 文本编辑器
  • grep, sed: 文本信息处理
  • awk: 表处理,很强大的二维表处理功能,可以对格式化文本输出进行处理
  • jq: 命令行的json查询和格式化工具,适合查看REST接口的输出

References

[1] 用十条命令在一分钟内检查Linux服务器性能, http://www.infoq.com/cn/news/2015/12/linux-performance
[2] Linux Performance, Brendan D. Gregg, http://www.brendangregg.com/linuxperf

【C语言深入】陷阱:数组溢出导致内存被意外修改

C语言的指针在提供编程便利的同时,却带来了很多潜在的内存安全问题。见以下例子:

<br />
#include &lt;stdio.h&gt;</p>
<p>int main() {</p>
<p>	char string_buff[12];<br />
	unsigned int i_not_zero = 0xFF;</p>
<p>	sprintf(string_buff, &quot;Hello,world!&quot;);</p>
<p>	printf(&quot;i = %x\r\n&quot;, i_not_zero);</p>
<p>	return 0;<br />
}<br />

该程序(不正确)的输出为:

<br />
i = 0<br />

上述代码使用sprintf修改了string_buff指针所指向的char型数组。但是由于在申明数组长度的时候,没有考虑到字符串结束符’\0’,所以实际写入时不慎篡改了下一位内存地址的内容(此例中为i_not_zero, 使用MinGW gcc)。在实际程序中,此类bug一般很难被发现,尤其是还有其他程序在正常修改该值时,一般先会去排查和该变量有关的程序。

这只是一个因为不慎所导致的内存溢出问题,而在一些极端的黑客代码中,经常会见到通过内存变量和函数的指针地址反向访问、修改堆栈,从而获得系统的权限。可见指针作为C语言的一个重要(但是晦涩的)组成部分,无形中降低了系统的可靠性和安全性,需要挑战programmer的debug能力。

【C语言深入】陷阱:数组指针作为函数参数返回

再来看一个指针问题,同样的来自一个本科生的代码。这段代码想要实现将一个全是小写字母的字符串转换成对应的大写字母字符串:

<br />
char *covert_to_upper_case(char *string) {<br />
    char p[100];<br />
    int i = 0;</p>
<p>    for(; i &lt; strlen(string); i++ ) {<br />
            p[i] = string[i] - ('a' - 'A');<br />
    }<br />
    p[i] = '&#92;&#48;';</p>
<p>    return p;</p>
<p>}<br />

然而这段代码没有能实现期望的功能。原因如下:

  1. 主程序调用convert_to_upper_case()函数后,堆栈为p分配了内存空间;
  2. 函数体正确修改了p对应字符数组的内容,并将p的首地址作为指针返回;
  3. 函数返回后,所有临时变量从堆栈中弹出,包括p[100];
  4. 主程序得到返回的指针,对其进行解析。然而指针指向的字符数组此时已经从堆栈中弹出,解析后的数据无法被定义。

要想正确实现对应的功能,应该将目标指针作为额外参数传递给该函数,并由上层调用者提供内存空间的创建。当然也可以使用malloc()将内存分配在堆中,但是需要注意使用对应的free()释放空间,否则会有内存泄露的问题。

【C语言深入】指针的一个错误赋值

关于指针总是有说不完的故事。

最近给本科的学生带Embedded System课程设计,遇到了一个非常奇怪的bug。有一段代码需要实现I2C通信,核心代码已经由软件库提供了,学生只需要设置结构体后调用API即可。一个学生的代码是这样的:

<br />
struct I2C_CONFIG {<br />
  // ...<br />
  char *i2c_buff;<br />
  int length;<br />
  // ...<br />
};</p>
<p>struct I2C_CONFIG cfg;<br />
char *i2c_buff;</p>
<p>void I2C_init()<br />
{<br />
  // ...<br />
  cfg.buff = i2c_buff;<br />
  cfg.length = sizeof(buff);<br />
  // ...<br />
}</p>
<p>void I2C_send(new_buff)<br />
{<br />
  // ...<br />
  i2c_buff = new_buff;<br />
  I2C_MasterTransferData(LPC_I2C1, cfg);<br />
  // ...<br />
}<br />

初看一下没有什么问题:在I2C_init()函数中首先对结构体cfg进行初始化,而在I2C_send()函数中设置了需要发送的数据指针,之后使用I2C的API发送数据。

因为代码一直无法实现期望的功能,我又仔细看了一下其中的蹊跷。我注意到,这段代码中使用了一个中间变量:char *i2c_buff。在I2C_init()中虽然将cfg.buff指向了i2c_buff,但是因为cfg.buff本身也是指针变量,而非”指向指针的指针”,所以这里只实现了简单的按值传递,即将i2c_buff的值 (初始值为0) 赋给了cfg.buff。之后虽然在I2C_send()中修改了临时变量i2c_buff指向的位置,但却没有影响到cfg.buff中的内容,cfg.buff依然指向之前i2c_buff初始化时指向的内存地址,所以需要发送的缓冲指针new_buff其实并没有传递给之后的I2C_MasterTransferData()函数!为了解决这个问题,必须将更改后的i2c_buff的值再次赋给cfg.buff,即:

<br />
void I2C_send(new_buff)<br />
{<br />
  // ...<br />
  i2c_buff = new_buff;<br />
  cfg.buff = i2c_buff;<br />
  I2C_MasterTransferData(LPC_I2C1, cfg);<br />
  // ...<br />
}<br />

另外这段代码还有一个不容易注意的bug,就是在I2C_init()中使用了sizeof()来判断buffer的大小。因为sizeof()函数得到的只是数据类型的大小,所以对于指针char *i2c_buff来说,sizeof(i2c_buff) = 4,而不会返回buffer的实际大小。指针的大小并不等于指针指向缓冲的大小!

【RPi树莓派使用指南】树莓派官方7寸屏入门指南

1. 引子

在树莓派官方触摸屏发布之前,市场上可用的屏幕有以下三种:

  • 直接和GPIO插口对接的屏幕,使用SPI与CPU进行通信。需要特殊的驱动程序将framebuffer的内容发送到LCD控制器上,一般带有触屏功能,大小以3.5寸为主流。受限于SPI通信速度,刷新速率不高;
  • 专用USB接口的屏幕,如RoboPeak Mini USB Display。这类屏幕通过USB连接,需要本地运行驱动程序;
  • 通用LCD屏幕,通过HDMI和树莓派连接。因其通用性不需要特殊的驱动程序,但是很多都不支持触屏功能,而且都需要额外的转接板,体积较大;

rpilcd-front-with-base
▲ 图.  树莓派官方7寸屏实拍

我自己的需求是将树莓派作为信息显示中心,在屏幕上显示我的HP服务器的运行信息,另外提供一些快捷的传感器监控和控制操作接口。最初一直在官方屏幕和HDMI屏幕之间犹豫,最后还是选择了官方触摸屏。归结起来主要有几个原因:

  • 官方屏的LCD模组最有保证,淘宝上的HDMI LCD一般成像质量不高;
  • 官方屏的触摸功能在所有方案中是支持的最好的,有十点电容触摸(目前Raspbian还只支持单点,以后会升级),且不需要额外驱动。而HDMI接口的LCD如果有触摸功能,都需要额外接一根USB用于提供触摸控制;
  • 官方触屏和树莓派3可以直接通过铜柱物理连接,无需额外的驱动电路板。连线也非常少,只需要一根DSI软排线和供电接口即可。

总体上来说,虽然官方屏的价格高了一些,但是却是所有方案中最可靠、简洁的,所以最后也没有多犹豫就从网上下单了。rpilcd-front-without-base
▲ 图. 树莓派官方LCD屏实拍 – 正面

Read more »

【C语言深入】C/C++变量命名规范

目前主流的C/C++命名风格有两种:一种是Windows风格的匈牙利命名法,主要是采用类型前缀 + 变量名首字母大写,另一个就是Unix/Linux命名习惯。我自己采用的是基于Unix的变种,融合了匈牙利命名法的一些优点,在这里分享给大家。

变量名的组成:(模块名) +  (作用域) + (类型前缀) + 变量名 + (变量名后缀),解释如下:

  • 变量名 以小写的英文字母构成,词与词之间用下划线连接,如key_value, data_src; 不可使用数字,不混用大小写;
  • 模块名 声明该变量属于的模块,防止模块与模块的命名冲突。如timer_prescalar_value, DMA_channel_name等;
  • 作用域前缀 (Scope Prefix) 标注变量的作用域,提高代码可读性:
    g_: 全局变量;
    n_: 局部变量;
    t_: 中间变量;
    s_: static静态变量;
  • 类型前缀 (Type Prefix) 指明变量的数据类型:
    ptr_: 指针变量,在程序中临时需要使用指针时,也常简写为p_,如*p_src;
    h_: 句柄,如h_file;
    n_: 整形,s_: 短整形,l_: 长整形, u_: 无符号整型,可增加数据位数,如u32;
    ch_: 字符型变量;
    f_: 浮点,d_: 双精度浮点;
    b_: boolean;
    by_: byte字节型(关注数据的位特性,需要位操作的情况下使用);
    reg_: 表示寄存器;
  • 后缀 (Suffix) 指明变量的性质:
    _src: 源,_dst: 目的;
    _str: 字符串;
    _t: 在声明数据类型时使用,表示为自定义的数据类型,如u32_t;
    _st: 表示为结构体;
    _buff: 数据缓冲, msg_buff;
    _arr, _a, _m: 数组或矩阵;

变量名的取名规则:

  • 循环控制变量 i, j, k, m, n,除循环控制外应避免使用这些变量名称;
  • 函数名 使用(模块名 + )动词 + 名词的形式,同样小写 + 下划线:sys_find_file(), IO_get_data(). 后者因为IO为专用名词故破例使用大写;
  • 类名或结构体名 使用首字母大写加下划线连接:如Mystring, Datetime_type;
  • 私有类成员 Private使用下划线_前缀,如_data_src_ptr, _init_module();
  • 宏定义或常量 使用全部大写:如MAX_NUMBER, LOOP_NUMBER;
  • 缩写 使用能广泛接受的缩写:如add, ans, avg, chk, cnt, col, ctrl, def, del, dst, disp, err, freq, idx, init, len, min, max, mid, msg, num, opt, pos, ptr, recv, res, ret, src, str, sub, num, ts (timestamp), val等。

本网站的所有实例代码和项目程序都将按此命名规范进行编写。

【RPi树莓派使用指南】树莓派3代介绍及历代树莓派比较

树莓派自从12年02月最初发布之后,目前已突破800万的总销量。作为树莓派的早期支持者,云飞实验室也一直在关注着它的发展。如今在经历了4年的设计迭代之后,树莓派于16年02月推出了最新一代的树莓派3。树莓派3的本次发布与之前的2代只相差了整整一年时间,但是得益于目前芯片行业的快速发展,树莓派3的性能将会有很大的提升。这主要表现在以下几点:

  • 更高的处理速度。树莓派3首次采用了64位处理器:基于Cortex-A53的博通BCM2837。BCM2837为四处理器核心,主频也由树莓派2的900MHz提高到了1.2GHz。根据官方提供的数据,这将使树莓派3的处理速度较2代提高50%。如果和1代的700MHz单核相比,提升大约在3 – 4倍。更高的CPU速度使得树莓派可以胜任更大负荷的运算工作:如科学计算,机器人路径规划等。
  • 更高的互联性。树莓派3使用了集成蓝牙4.0和WiFi的设计。集成通信的设计的意义是多方面的。首先,使用者无需再购买额外的USB设备,从一定程度上来说,鼓励了用户在自己的设计中使用这些通信功能;其次,集成的通信模块可以进行更好的功耗管理,同时IO吞吐的性能也会得到提高;最后,可以更进一步的优化内核,只针对板载的芯片专门进行优化。避免可能出现的兼容性或者未优化的驱动导致通信性能下降问题。

rasp-pi-3-board
图1. 树莓派3外观

rasp-3_Model_B
图2. 树莓派3外观 (设计渲染图) 

从这两点来看,树莓派3代将很有可能再次扩展自己的使用领域,同时在物联网和机器人中得到应用。同类产品如果还是单从硬件角度进行提升,已经无法再与之竞争。因为性能的显著提升且维持原价,在树莓派2代出来时没有入手的玩家,这一次也难免蠢蠢欲动。同时因其性价比的纯粹提升,树莓派1代与2代的销量会大幅下降。因为2代只发布了1年,所以很可能成为绝版,如果出于收藏的目的可以尽早买之。总体上来说,这次树莓派3已经具备了IoT所需要的基本条件(性价比,互联性,体积与功耗)。今后的方向可能是增加更多的IO功能,如PWM和ADC等,是被大多数玩家需要却还没有被支持的。

以下是我对历代树莓派版本的整理与对比:

表1. 发布时间及主要特点

型号 发布时间 主要特点
PI 1 Model B 2012年02月 第一代树莓派。Model A不含以太网。
PI Compute Module 2014年04月 模块化设计,使用SODIMM大小的金手指接口。
PI 1 Model B+ 2014年07月 增加了2个USB接口,增加了9个GPIO:26脚->40脚。
使用MicroSD卡。
PI 2 Model B 2015年02月 升级处理器:四核900MHz Cortex-A9。升级为1GB RAM。
PI Zero 2015年11月 无网络通信功能,廉价,小尺寸。
PI 3 2016年02月 升级处理器:64bit四核1.2GHz Cortex-A53。
内置蓝牙4.0和WiFi。

 

表2. 树莓派历代版本硬件比较

型号 处理器 主频 内存 GPIO 互联性 功耗级别
PI 1 Model B BCM2835
(ARM11)
700MHz 512MB 26 2 USB
HDMI
10/100M Ethernet
700mA (3.5W)
PI Compute Module BCM2835
(ARM11)
700MHz 512MB 0 无。需要配合扩展板使用。 200mA (1W)
PI 1 Model B+ BCM2835
(ARM11)
700MHz 512MB 40 4 USB
HDMI
10/100M Ethernet
600mA (3W)
PI 2 Model B BCM2836
(Cortex-A9 四核)
900 MHz 1GB 40 4 USB
HDMI
10/100M Ethernet
800mA (4W)
PI Zero BCM2835
(ARM11)
1 GHz 512MB 40 (无排针) 1 Micro-USB
Mini HDMI
无板载网卡
160mA (0.8W)
PI 3 BCM2837
(Cortex-A53 64位四核)
1.2 GHz 1GB 40 4 USB
HDMI
Bluetooth 4.1
WiFi 802.11n
10/100M Ethernet
800mA (4W)

 

Read more »

【C语言深入】C语言的函数指针

函数指针是C语言指针中的一个分支:函数指针是指向函数地址的指针。和一般的指针一样,函数指针可以大大增强编程时的灵活性。这篇博文根据我的理解,简单介绍了自己对于函数指针的理解。

一、函数名的本质

在介绍函数指针之前,我们先来理解一下究竟什么是函数。以下一段代码定义了名为fun1(),接受int并返回int参数的函数:

<br />
int fun1(int x) {	// fun1是函数名<br />
	return x + 1;	// 函数体<br />
}<br />

从一般的理解角度来看,fun1是函数的函数名。之所以要定义函数名,是因为在程序的其他位置要调用该函数时,可以直接使用这个别名。这种使用别名的方法和变量相似,但又不完全一样。对一个变量x而言,其意义如下:

物理地址 数值  说明
0x20000000        30 <– x是该数值单元的别名

x是内存单元0x20000000所对应的内存单元。此处x = 30,而x的地址&x = 0x20000000.

那么对一个函数而言,函数名又代表什么呢?假设之前的fun1函数在编译后被放置在0x08000200地址,则fun1对应的内存结构如下所示:

物理地址 数值 说明
fun1 (0x08000200) fun1_entry <– fun1是函数的入口地址

在这里,函数名代表一个记录了函数入口地址的存储单元的物理地址。可见,函数名的本质是地址。在编译阶段,函数名被转化成为对应的地址。在使用 xxxx() 函数调用的语法时,该地址被载入程序计数器PC,函数参数及当前现场被弹入堆栈。最后进行函数的实际跳转和执行。

二、函数指针是什么

对于一般变量而言,指针可以指向变量的地址,并修改变量的内容:

物理地址 数值  说明
0x20000000         30 <– x是整形变量
0x20000004  0x20000000 <– p是指向x的指针

这里p是指向x的指针,(即p = &x)。此时p的内存单元所存储的是x单元的物理地址,通过 *p 解析地址之后就可以访问或修改x单元的内容。同样的,函数也有其对应的指针 – 函数指针。函数指针是一种特殊的指针,其指向的对象不是变量而是函数。函数指针指向目标函数的入口地址(首地址)。这里我们定义一个指向fun1函数的函数指针pf:

<br />
int fun1(int);		// fun1是一个(含int输入参数和int返回参数的)函数<br />
int (*pf)(int);		// pf是(指向返回int型,含int参数函数的)函数指针<br />
pf = &amp;fun1;</p>
<p>/* 也可以直接写成: */<br />
int (*pf)(int) = &amp;fun1;</p>
<p>/* 如果需要调用函数指针对应的函数,可以写为:*/<br />
(*pf)();		// 等价于fun1()<br />

到这里,我们回顾上一节中说到的函数名。其实函数名也可以理解成为const型的函数指针。所以在c语言中,以下调用也是合法的:

<br />
(*fun1)()		// 等价于fun1()<br />
pf = fun1		// 等价于pf = &amp;fun1;<br />
/* 但是fun1的值不能被修改 */<br />
fun1 = fun2		// 错误!fun1是const类型的指针<br />

函数指针的声明较为冗长,如果需要定义多个同类型的函数指针。可以通过typedef定义一个函数指针类型,从而进行简化:

<br />
typedef int (*PF)(int); // 声明PF是一个函数指针类型<br />
PF pf1 = fun1;<br />
PF pf2 = fun2;<br />

三、函数指针的应用场景

以上说明了如何定义函数指针,下面介绍函数指针的应用场景。函数指针最常见的应用还是作为回调函数的参数。一般在事件驱动的程序框架中,当对应事件发生时,需要触发对应的处理函数。以下代码实现了在初始化阶段,将事件与对应的处理函数关联(假设set_event_callback()是实现该功能的系统函数):

<br />
typedef int (*CALLBACK)(int);</p>
<p>#define EVENT_1		(0x01)<br />
#define EVENT_2		(0x02)</p>
<p>int set_event_callback(const int e, CALLBACK);</p>
<p>void init_callbacks() {<br />
	CALLBACK pf1, pf2;<br />
	set_event_callback(EVENT_1, pf1);<br />
	set_event_callback(EVENT_2, pf2);<br />
}<br />

在set_event_callback中,函数指针pf作为参数传递给函数使用。在基于事件编程的框架中(如一般的GUI库),函数指针经常以作为回调函数的方式出现。函数指针的另一个应用就是,根据当前程序进程的不同,要在不同条件下调用不同的处理函数:

<br />
void change_function(int nEvent, PF *ppf) {</p>
<p>	switch (nEvent) {<br />
		case 0:<br />
			*ppf = fun1;<br />
			break;<br />
		case 1:<br />
			*ppf = fun2;<br />
			break;<br />
		default:<br />
			*ppf = fun3;<br />
	}<br />
}<br />

这里利用了函数指针指向的函数是可以改变的(而非函数名是const型的)。change_function() 将函数指针的地址(指向函数指针的指针)作为参数传入,并根据当前nEvent的情况更改原函数指针指向的函数。

函数指针的另一个应用场景,是允许程序在只知道函数物理地址(但不知道具体的函数名)的情况下进行函数跳转。相信大家都还记得这个经典的C语言面试题:

<br />
(*(void(*)(void))0)();<br />
// 或<br />
((void(*)(void))0)();<br />

此语句的本质就是将0内存位置强制转换成为了函数指针,并调用了该函数指针指向的函数。以下代码解释了此处是如何将0地址转换成函数指针的:

<br />
typedef void (*PF)(void);</p>
<p>// (*(void(*)(void))0)() 等价于<br />
(*(PF)0)()<br />
// 或<br />
((PF)0)()<br />

函数指针的更高级用法就是组成函数指针数组,或和指向函数指针的指针配合使用。在此不再继续介绍,感兴趣的读者可自行研究。

参考资料

[1] Kenneth A.Reek, C和指针(第二版), 2008, 人民邮电出版社

[2] Brian W. Kernighan / Dennis M. Ritchie, The C Programming Language (Second Edition), 1989, Prentice Hall

[3] C语言中文网,C语言函数名与函数指针详解http://c.biancheng.net/cpp/html/496.html

Smart Home | HP MicroServer Gen8 不专业评测

今年三月份,我在英国Amazon下单买下了现在这台HP ProLiant MircoServer Gen8。说到为什么要买它,回想起来,其实中间有很长的一段渊源。

记得2010年,我对DIY智能家居系统开始感兴趣。因为当时手上有友善之臂的mini2440开发板,我最初选择的实现方案是基于ARM(S3C2440)+ Linux。不过当时对Linux系统还不熟悉,花了一些精力调通了摄像头远程拍照后,就不了了之了。后来有了树莓派,又进一步研究了IO控制、语音播报和简单图像识别的实现。虽然功能上都可以实现,但是树莓派的CPU和IO性能实在捉襟见肘。后来又尝试过pcDuino v3,也并没有太多改善。究其原因是,这些嵌入式控制板毕竟追求的是低功耗和高集成性,在性能上难免会有妥协。有过这些失败的经验后,我决定还是买一个全功能的服务器平台。

当时我对服务器的印象还是:高IO性能,价格昂贵(万元以上),体积大,7/24工作,需要良好散热环境。在经过一些调查后,我意外发现了惠普的塔式服务器系列。我之前从未有过选购服务器的经验,但是这款Gen8还是让我眼前一亮,仔细研究了两天性能和兼容性之后我就下单了。Gen8让我动心的特点主要有:

1、体积:体积小巧,在家里随意摆放,不占空间;
2、价格:英镑折算2000元人民币,配上西数3TB红盘900,总价只要3000左右,远远低于标准服务器的价格;
3、性能:RAID硬盘控制器,千兆以太网,x86架构;
4、外观设计:铝合金面板,蓝色健康指示灯,外观设计美观大方;
5、多OS支持:RHEL / Windows Server / VMware ESXi;
6、可玩性好:接口多 – USB 3.0 / USB 2.0 / VGA / 双千兆网口,升级空间 – 内存最大可升级16GB,硬盘最大支持16TB,CPU支持升级Xeon,PCIe接口可插独立显卡。

当然了,Gen8的主要目的还是作为智能家居框架的中心:智能家用中心服务器(Smart Home Central Server,SHCS)。这个系统的主要功能有:
1、智能家居主站:智能节点数据采集与管理(与节点的通信方式:RF无线/WLAN/LAN);
2、智能家庭服务:天气播报,日历事件提醒,环境检测;
3、摄像头监控与入侵检测;
4、NAS数据存储:照片、多媒体、文件备份;
5、流媒体服务(DLNA):音乐,视频播放服务器;
6、BT文件下载器;
7、提供网页管理前台(基于Apache):当前服务器运行情况、智能节点数据浏览、资源使用情况、功能管理等;
8、开发调试:Linux应用程序开发,或作为数据库服务器。

有了Gen8强大的硬件支持,我的项目又有了新的希望。接下来的几篇文章我会介绍Gen8的硬件结构、配置和可升级空间。尽请期待。