最近想给RPi写个监控应用,打算用node.js写成Web端,方便在笔记本上用浏览器来查看。 监控的内容不仅仅是RPi上Linux的运行状况,还要有RPi硬件的状态信息,如果CPU温度、时钟频率、电压等。RPi的固件中提供了一个程序来获取这些硬件的状态数据——/opt/vc/bin/vcgencmd,此命令的详细用法见这个说明页面。 如果要在node.js程序里获取这些数据,可以通过child_process模块的spawn或exec方法来开启一个新的进程,并运行vcgencmd,进而读取其标准输出流的内容。这样,每次需要一个数据都要新建一个进程,感觉资源耗费比较大。毕竟单核的ARM,只有700MHz的处理速度,还是要省着用。况且监控程序更不应该消耗如此大的计算资源。 于是想通过node.js的addon方式来获取硬件的状态信息。幸好,vcgencmd是在RPi开源的VideoCore接口上编写的,raspberrypi的github上提供了源码。通过查看源码,去掉其中使用的互斥锁及事件的API后,发现vcgencmd其实是调用VideoCore的VCHI。该接口其实是通过消息队列实现的。客户程序给特定的消息队列发送命令,即运行vcgencmd时使用的参数,之后再从队列中读取消息,便所需要的硬件状态数据了。从开放的源码中不能获知具体的实现方式,不过可以推测,发送消息和接收消息应该是两个不同的队列。VideoCore的服务程序应该是从消息队列A中读取命令,经过处理后,再放到消息队列B让客户程序读取的。 既然看不到具体的实现细节就算了,只要有接口就可以开始编码了。突然很惊喜地在vchi.h文件中发现VCHI的API中有设置回调函数的选项,即SERVICE_CREATION_T结构体的callback属性,便有了以异步方式调用VCHI的想法,配合node.js的事件驱动模式,感觉运行效率会比创建新进程运行vcgencmd要好很多。但这毕竟是理论上的想法,实际上可否实现还得等代码写出来运行后才知道,因为VCHI与v8的接口之间能否相互相调用还是一个问题。 VCHI回调函数中的reason参数的值代表是了调用此回调函数的事件,可用的事件列表及相应的说明被定义在vchi-common.h文件的VCHI_CALLBACK_REASON_T枚举类型中。其中的VCHI_CALLBACK_MSG_AVAILABLE是消息可用事件,即可以通过vchi_msg_dequeue函数来读取命令运行结果的消息体了。 在只用VCHI编写的测试程序中,发现回调函数中获取消息体是可行的,而且也是真正的异步处理。但不知道为什么把它写入Node,js的addon代码后,运行到回调函数时会Segment fault。也许真的像我之前所预料的,v8和VCHI之间还是不太兼容对方,而且特别是VCHI函数中用了一句 HandleScope scope;所带来的效果是无法预料的,因为它的作用是在当前栈中分配很多本地v8的类型控制器。经过调试,基本能够确定程序是运行到这句时出现Segment fault的。 可能是个人喜欢追求完美的性格问题,总是喜欢做这些不太合适的cross over,而且常常在这些边界位置上纠结。这次也不例外,出现问题后很想找到原因。但才发现无从下手,因为没有VCHI的实现代码,而且对v8的运行机制也不熟悉。最后只好放弃了! 除了创建进程外,还剩下的选择还有 - 同步调用vchi_msg_queue和vchi_msg_dequeue
- 使用libuv的work queue在新线程中调用vchi_msg_queue和vchi_msg_dequeue实现异步调用
权衡了一下,VCHI应该是比较接近硬件底层的接口,与系统调用应该差不多了,而且可能是运行在内核态,所以运行的效率应该很快。虽然同步调用vchi_msg_dequeue需要一直循环到接收到消息为止,但阻塞的时间应该不长。如果在新线程中实现异步调用,一来创建线程会消耗资源外,异步回调的程序写起来是比较麻烦。对于这种只是获取一个很少量的数据的调用而言感觉没有必要。所以选择了同步调用的方式。 其实也要想过直接使用vcgencmd所使用的接口,即vc_vchi_gencmd.h文件中提供的接口,但查看了其实现文件vc_vchi_gencmd.c,发现其中用了互斥锁和IPC事件信号的API,而且提供的接口可用于创建多个VCHI连接的。虽然不知道过中的缘由,但感觉自己写的监控程序中这些东西都是不需要的。(其实是追求完美而钻牛角尖的老毛病又犯了。)不管怎么样,跟着感觉走吧,发现问题再改也不迟,反正也不是什么重要的东西,经得起折腾。 node.js调用VCHI的实现的addon的代码如下: - #include <node.h>
- #include <v8.h>
- #include <string>
- #include <interface/vchi/vchi.h>
- #include <interface/vmcs_host/vchost.h>
- #include <interface/vmcs_host/vc_gencmd_defs.h>
- #include <interface/vcos/vcos_types.h>
- #include <vcinclude/common.h>
- #define GENCMD_MAX_LENGTH 1024
- using namespace v8;
- using namespace std;
- static VCHI_INSTANCE_T vchi_instance;
- static VCHI_CONNECTION_T vchi_connection;
- static VCHI_SERVICE_HANDLE_T vchi_service_handle;
- static string getString(Local<Value> arg) {
- Local<String> value = arg->ToString();
- int len = value->Length();
- char buf[len + 1];
- memset(buf, 0, len + 1);
- value->WriteAscii(buf);
- string str(buf);
- return str;
- }
- Handle<Value> Exec(const Arguments& args) {
- HandleScope scope;
- if (args.Length() < 1) {
- ThrowException(Exception::Error(String::New("must specify command")));
- return scope.Close(Undefined());
- }
- if (!args[0]->IsString()) {
- ThrowException(Exception::Error(String::New("command must be a string")));
- return scope.Close(Undefined());
- }
- string cmd = getString(args[0]);
- if (vchi_initialise(&vchi_instance) != 0) {
- ThrowException(Exception::Error(String::New("VCHI initialization failed")));
- return scope.Close(Undefined());
- }
- if (vchi_connect(NULL, 0, vchi_instance) != 0) {
- ThrowException(Exception::Error(String::New("VCHI connection failed")));
- return scope.Close(Undefined());
- }
- SERVICE_CREATION_T gencmd_parameters;
- gencmd_parameters.version = VCHI_VERSION(VC_GENCMD_VER);
- gencmd_parameters.service_id = MAKE_FOURCC("GCMD");
- gencmd_parameters.connection = &vchi_connection;
- gencmd_parameters.rx_fifo_size = 0;
- gencmd_parameters.tx_fifo_size = 0;
- gencmd_parameters.callback = NULL;
- gencmd_parameters.callback_param = NULL;
- gencmd_parameters.want_unaligned_bulk_rx = VC_FALSE;
- gencmd_parameters.want_unaligned_bulk_tx = VC_FALSE;
- gencmd_parameters.want_crc = VC_FALSE;
- if (vchi_service_open(vchi_instance, &gencmd_parameters, &vchi_service_handle) != 0) {
- ThrowException(Exception::Error(String::New("VCHI service open failed")));
- return scope.Close(Undefined());
- }
- if (vchi_msg_queue(vchi_service_handle, cmd.c_str(), cmd.length() + 1, VCHI_FLAGS_BLOCK_UNTIL_QUEUED, NULL) != 0) {
- ThrowException(Exception::Error(String::New("fail to send message accross VCHI service")));
- vchi_service_release(vchi_service_handle);
- return scope.Close(Undefined());
- }
- char buffer[GENCMD_MAX_LENGTH];
- uint32_t actual_buffer_size;
- while (vchi_msg_dequeue(vchi_service_handle, buffer, sizeof (buffer), &actual_buffer_size, VCHI_FLAGS_NONE) != 0);
- int ret_code = VC_VTOH32(*(int *)buffer);
- if (ret_code < 0) {
- string err_msg = "fail to receive message from VideoCore ";
- err_msg += "(Error code: ";
- err_msg += ret_code;
- err_msg += ")";
- ThrowException(Exception::Error(String::New(err_msg.c_str())));
- while (vchi_service_close(vchi_service_handle) != 0);
- return scope.Close(Undefined());
- }
- actual_buffer_size -= sizeof (int);
- char response[actual_buffer_size + 1];
- memset(response, 0, actual_buffer_size + 1);
- memcpy(response, buffer + sizeof (int), actual_buffer_size);
- while (vchi_service_close(vchi_service_handle) != 0);
- return scope.Close(String::New(response));
- }
- void init(Handle<Object> target) {
- target->Set(String::NewSymbol("exec"), FunctionTemplate::New(Exec)->GetFunction());
- }
- NODE_MODULE(gcmd, init);
复制代码 由于addon中调用了VCHI,所以编译时需要包含VCHI的头文件路径及链接时需要用于的动态链接库文件/opt/vc/lib/libbcm_host.so,node-gyp的构建配置文件binding.gyp如下:- {
- "targets": [
- {
- "target_name": "gcmd",
- "sources": [ "src/gcmd.cc" ],
- "include_dirs": [
- "/opt/vc/include",
- "/opt/vc/include/interface/vcos/pthreads",
- "/opt/vc/include/interface/vmcs_host/linux"
- ],
- "libraries": [
- "/opt/vc/lib/libbcm_host.so"
- ],
- "cflags": [
- "-std=c++11"
- ]
- }
- ]
- }
复制代码经过测试,没发现什么问题。在node.js层面上还将从VCHI获取到的硬件状态数据加工了一下,即把字符串转换成数值,并用js类封装好再返回给调用者。 最后还是没有罢休,google了一下关于v8的HandleScope的内容,找到了一篇分析v8与node.js整合机制的文章。从文章的内容了解到v8的工作方式,推断VCHI的回调函数中使用HandleScope出现问题的原因可能是在于HandleScope的分配出了问题,至于是什么问题就没有进行深入调试,日后有时间再继续深究一下。
本文转自:http://gutspot.com/
|