写内联驱动的port driver时遇到的问题

0. 用C写的内联驱动的port driver运行时嵌入到Eralng虚拟机中,就像是Erlang的一部分,而外部接口是在一个独立的操作系统进程中运行的。
内联驱动的优点是高效,缺点是写的不好会连累Erlang虚拟机,把后者崩溃掉。所以,很危险要小心。

一个比较好的参考例子是[url=http://yaws.hyber.org/download/bfile-1.2.tgz]bfile[/url],bfile实际上是对c的标准文件操作(如fopen(), fread()等)做了包装。

用erl_ddll:loaded_drivers().可以看到缺省情况下erl提供的常见driver:
> erl_ddll:loaded_drivers().
{ok,["efile","tcp_inet","udp_inet","zlib_drv",
"ram_file_drv","tty_sl","GDAL_drv"]}

写driver时可以参考一下。

注意driver中的函数都是静态的,静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
[url=http://hi.baidu.com/gtfcugb/blog/item/57b836dd6e11f5e976c638f1.html]C语言中定义静态函数[/url]的好处:
静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。
关键字“static”,译成中文就是“静态的”,所以内部函数又称静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件。 使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名,因为同名也没有关系。

1. Port Driver共享库的装载与卸载
装载(erl_ddll:load(Path,LibName))指定库文件所在目录和库名字(库文件的名字与ErlDrvEntry结构driver_name字段的名字一致),装载时会自动执行我们在ErlDrvEntry结构init字段中指定的函数(如果指定了),我们可以在这个函数中为driver做一些准备工作;

卸载(erl_ddll:unload(Name)时只需指定要卸载的库名,即ErlDrvEntry结构中的driver_name(也是上面erl_ddll:loaded_drivers列出的库名),如果ErlDrvEntry结构体中指定了finish字段的函数,那么卸载时会自动执行此函数,可以用此函数做一些库的清理工作

Port Driver共享库在Erlang运行时中的存在与转载库的进程有关,如果装载库的进程(即调用erl_ddll:load的进程死掉了,这个进程装载的库也会自动被卸载。可以自己验证一下:
erl_ddll:loaded_drivers().
spawn(fun() ->erl_ddll:load(".", 'YOUR_drv'), timer:sleep(5000) end).
erl_ddll:loaded_drivers().
五秒钟后再
erl_ddll:loaded_drivers().


所以,照看好你的装载共享库的进程,不要让它意外退出了。

2. 端口(Port)的打开与关闭
用open_port启动一个端口时会自动执行ErlDrvEntry结构体中start字段指定的函数;
Port关闭时(如调用unlink关闭)是会指定执行ErlDrvEntry 结构体stop字段指定的函数。
在开启port直到关闭的这段作用域内,我们可能需要共享一些数据以方便对port的操作。该函数会返回一个指针,这个指针是一个ErlDrvData数据类型,实际上指向一个用户自定义的结构体,这个ErlDrvData数据(也就是那个指针)又称为port handle,某种意义上这个指针代表被打开的port,因此在driver的其它回调函数中,ErlDrvData数据(也就是那个指针)总是传入的第一个参数。

例如bfile包装了底层对文件的读写操作,底层实际上是以文件句柄的方式操作文件,每开启一个bfile 端口,就是对应着一个文件的打开(得到一个文件句柄),以后通过端口对文件的操作都是通过该句柄进行的,因此bfile的c实现中需要记录打开的对应文件句柄。这个文件句柄可以在端口打开时记录在ErlDrvPort结构中,以后每次对端口操作时就可以从ErlDrvPort结构体中得到port对应的文件句柄了。

在[b]端口连接进程[/b](即调用open_port打开端口的进程)死掉后,端口会自动关闭。
也可以自己主动关闭端口
    unlink(Port),
catch erlang:port_close(Port).


3. 当在Erlang上调用erlang:port_control时,对应的C driver的control函数将被调用,执行的结果通过connect函数的输出参数rbuf传回给erlang emulator,erl_interface中的ei库会将数据编码成erlang的二进制term,因此在erlang一端可以调用binary_to_term将结果转成term。显然,在C driver中为输出而给rbuf分配的内存应该由erlang emulator负责释放。

4. C程序的编译,连接的时候要小心别遗漏某些库,不然编译连接都通过了,库文件也生成了,运行时却出问题;例如,有一次程序中拼写错误调用了一个不存在的函数,但是编译连接都通过,只是在加载的时候才给出错误。

5. C++的内联驱动
DRIVER_INIT应该声明为extern "C" 的:
extern "C" DRIVER_INIT(MY_drv)


更多的例子参考[url=http://github.com/okeuday/generic-erlang-port--driver-/tree/master]generic erlang port driver[/url],这个项目提供了一个框架,方便了C/C++的外部端口以及内联驱动程序的编写。

6. 内存泄漏的问题
自己用C写内联驱动程序最可能出现的问题就是内存泄漏了,我在linux下使用了一个比较土的检测方法:用一个脚本每十秒钟检测一下erlang虚拟机的内存,具体是调用了ps -o rss命令进行的(单位是KB),这个命令可能不够精确,但也够用了。检测数据都写入erl_mem.log文本文件中,最后一列即是erlang运行时的内存:
 # ERLPID=`pgrep -f 'beam'`; while [ 1 ] ; do MEM=`ps -o rss= -p $ERLPID`; echo -e "`date`\t`date +%s`\t$MEM\t"; sleep 10; done | tee -a erl_mem.log

如果发现内存数量只升不降多半是出现内存泄漏了。
运行这个脚本的机器上不要同时运行两个erlang运行时。


7. 想在C driver里用erl_interface库函数解析二进制数据时出现问题,函数erl_init(NULL, 0)不调用还好,一旦出现,即使没有执行到,控制台上就会大约无穷的:
(no error logger present) error: "erts_poll_wait() failed: einval (22)\n"
真邪门
erl_interface库一般用于外联驱动,或者写C node,为这些程序提供erlang term数据结构的转换支持,这些程序的特点是都独立于Erlang运行,而内联驱动的程序是作为Erlang自身的一部分运行,也许和这个有关,待证实

查官方的例子(在erts/example/目录下)可以看到,写内联驱动的driver进行erlang term的转换时没有用erl_interface,而是使用的ei进行term的编码和解码,ei的抽象程度要比erl_interface低,但是至少提供了将C语言的数据结构转换成对应的Erlang term结构的一个包装。一个生成二元tuple的例子:
ei_x_buff x;  // 将用来表示Erlang term的C数据结构
ei_x_new_with_version(&x);
ei_x_encode_tuple_header(&x, 2); //这是一个二元的tuple
ei_x_encode_atom(&x, "ok"); // tuple的第一个元素是一个atom,值为ok
ei_x_encode_string(&x, "abcdef"); // 第二个元素是字符串abcdef
生成了由ei_x_buff 表示的term,

ErlDrvBinary* bin = driver_alloc_binary(x.index);
memcpy(&bin->orig_bytes[0], x.buff, x.index);
从ei_x_buff提取出二进制binary,在erlang上调用binary_to_term即可得到成Erlang term,最后得到tuple
{ok, "abcdef"}


port driver的官方文档是ERTS User‘s Guide的[url=http://erlang.org/doc/apps/erts/driver.html]第6章 How to implement a driver[/url]
如果driver中的计算比较耗时,可能会阻塞erlang emulator,别的Erlang进程就不能执行了,这是不可接受的,解决办法是实现一个异步driver,在6.5和6.6节对此有介绍
<think>我们正在处理一个关于函数转移到另外文件后编译通过但实际操作设备出现问题的案例。从提供的引用中,我们可以提炼以下关键信息: 引用[1]提到glib.h内联函数重定义的解决办法:将内联函数改为函数声明并声明为弱函数,或者将内联函数的实现转移到C文件中。这提示我们内联函数在头文件中定义可能会导致重定义问题,而将实现移到C文件(即源文件)中是一种解决方案。 引用[3]则展示了内联函数在头文件中的典型用法:在类定义后直接定义内联成员函数。同,引用[3]也提到普通函数可以声明和定义分离,编译阶段只需要声明,链接阶段再关联定义。 用户的问题:将函数转移到另外文件(即从原来的位置移动到单独的源文件)后,编译通过,但实际操作设备出现问题。用户已经包含了必要的头文件。 分析可能的原因: 1. 函数定义转移后,可能破坏了原有的内联特性,导致函数调用方式发生变化,从而引起运行问题。 2. 函数转移后,可能引发了链接问题,比如重复定义或者定义缺失。 3. 函数转移后,可能由于作用域或访问权限的变化,导致函数无法正确访问相关数据。 4. 设备操作问题可能涉及到硬件交互,函数转移后可能破坏了原有的序或内存布局(尤其是内联函数改为普通函数后,调用开销可能影响实性)。 解决思路: 1. 检查函数转移后的定义是否正确,特别是是否添加了必要的链接属性(如static、extern等)以及是否在头文件中保留了声明。 2. 确认转移后的函数是否被正确编译并链接到目标文件中。可以使用nm工具检查目标文件是否包含该函数的符号。 3. 检查设备操作相关的函数是否对执行间敏感。内联函数改为普通函数会增加调用开销,可能导致问题。 4. 检查是否由于函数转移导致优化选项失效,从而影响性能。 具体步骤: 1. 确保在头文件中声明了函数,并且在源文件中定义了函数。例如,原来的内联函数在头文件中,现在改为: ```c // 头文件 example.h #ifndef EXAMPLE_H #define EXAMPLE_H // 声明为普通函数(不再是内联) void my_function(int arg); #endif ``` ```c // 源文件 example.c #include "example.h" void my_function(int arg) { // 函数实现 } ``` 2. 检查链接是否正常。确保包含了该源文件的编译结果(即.o文件)被链接到最终的可执行文件中。 3. 如果函数对执行间要求严格,可以考虑以下方案: - 恢复为内联函数(如果重定义问题无法解决,需考虑其他方法)。 - 使用编译器特定的强制内联指令(如gcc的`__attribute__((always_inline))`),但要注意这可能导致重定义问题,所以需谨慎使用。 - 优化函数实现,减少开销。 4. 如果问题出现在设备操作上,考虑使用调试工具(如gdb)进行单步调试,或者使用日志输出函数执行情况,确认函数是否被正确调用以及参数传递是否正确。 5. 检查是否因为函数转移后,编译器优化选项发生了变化。例如,原来的内联函数在转移后不再内联,导致性能下降。可以尝试调整优化级别(如使用-O2)来让编译器自动内联,或者显式指定内联。 6. 参考引用[1]的方法,如果之前是内联函数,现在移到源文件后,也可以考虑声明为弱符号(weak symbol)以避免重定义,但这通常用于解决库函数冲突,一般应用代码中少见。 示例解决方案: 假设我们有一个内联函数`inline int add(int a, int b) { return a+b; }`原本在头文件,现在移到源文件后出现问题。 我们可以: - 在头文件中保留声明:`int add(int a, int b);` - 在源文件中实现:`int add(int a, int b) { return a+b; }` - 如果该函数调用频繁且对性能敏感,可以尝试在函数定义使用编译器特定的内联指令(但注意,在源文件中定义内联函数需要使用`extern inline`或`static inline`,具体取决于编译器): 对于gcc,可以在源文件中这样: ```c // 在源文件中强制内联(即使没有在头文件中声明为内联) inline __attribute__((always_inline)) int add(int a, int b) { return a+b; } ``` 但是,这种方法可能会导致链接错误(如果该函数在多个源文件中被包含),所以通常建议将这样的函数放在头文件中作为内联函数。因此,我们需要权衡是否必须内联以及是否会出现重定义。 如果上述方法都不能解决问题,可能需要进一步检查设备操作的具体错误表现,例如是否出现内存访问错误、寄存器配置错误等。 由于用户的问题描述中没有具体说明设备操作出现什么问题,我们只能给出一般性的建议。 相关问题: 1. 如何检查链接过程中函数的定义是否被正确包含? 2. 在将内联函数转移到源文件后,如何保持其内联特性? 3. 函数调用开销对实设备操作有哪些影响? 4. 如何调试嵌入式设备上的函数调用问题? </think>### 函数转移后编译通过但运行设备异常的分析与解决 #### 问题分析 当函数转移到单独文件后编译通过但设备操作异常,通常涉及以下核心问题: 1. **符号可见性问题**:函数在源文件中可能未正确定义为全局可见符号(缺少 `extern` 声明或未正确导出符号) 2. **内联函数特性丢失**:原始内联函数转为普通函数后,可能导致: - 性能下降影响实性设备操作(如中断处理) - 多模块包含链接冲突 [^3] 3. **调用约定不一致**:跨文件调用未保持统一的调用约定(如 `__stdcall` vs `__cdecl`) 4. **头文件包含遗漏**:未包含设备操作必需的硬件寄存器定义或驱动声明 #### 解决方案步骤 ##### 1. 检查符号可见性(关键步骤) 在头文件中**显式声明函数**,在源文件中**定义全局符号**: ```c /* 头文件 device_functions.h */ #pragma once extern void device_operation(); // 显式声明全局符号 /* 源文件 device_functions.c */ #include "device_functions.h" void __attribute__((used)) device_operation() { // GCC确保不优化掉 /* 设备操作代码 */ } ``` > 📌 使用 `nm -gC your_binary | grep device_operation` 验证符号是否存在 ##### 2. 处理内联函数特性 若原函数为内联函数(如引用[3]场景): ```diff // 原头文件中的内联函数 - inline void set_level(quint8 level); + /* 移入源文件实现普通函数 */ void set_level(quint8 level); // 声明在头文件 ``` 同添加编译选项保证性能: ```bash # GCC开启链接优化(LTO) CFLAGS += -flto ``` ##### 3. 验证调用约定 对于嵌入式设备,需统一调用约定: ```c /* 设备操作函数使用硬件标准约定 */ void __attribute__((stdcall)) device_operation(); ``` ##### 4. 检查硬件依赖 确保新文件包含所有硬件相关头文件: ```c // device_functions.c #include "hw_registers.h" // 硬件寄存器映射 #include "interrupts.h" // 中断控制器定义 ``` #### 调试手段 1. **反汇编验证**: ```bash arm-none-eabi-objdump -d your_firmware.elf | grep -A 20 device_operation ``` 2. **链接脚本检查**: ```ld /* 确保新.o文件在固件中 */ .text : { *(.text.device_operation*) /* 强制包含函数 */ KEEP(*(.text)) } ``` 3. **实调试**: ```c printf("设备状态: 0x%X\n", HW_REG->STATUS); // 插入状态打印 ``` #### 典型成功案例 某嵌入式项目将GPIO操作函数移至 `gpio_driver.c` 后出现设备无响应: 1. 发现缺失 `volatile` 修饰硬件寄存器访问 2. 修复方案: ```c // gpio_driver.c void gpio_set(volatile GPIO_TypeDef* port) { port->ODR |= 0x01; // 正确使用volatile } ``` 3. 重新编译后设备操作恢复正常 [^1][^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值