Android VpnService.Builder文件描述符在原生代码中read阻塞问题咨询
问题根因
- 阻塞IO的系统调用特性:你当前JNI中使用的是阻塞模式的
read系统调用,Linux POSIX标准规定,关闭一个正在被其他线程阻塞读取的文件描述符,不会主动唤醒该阻塞的系统调用,只有当有新数据到达、收到信号或者发生IO错误时read才会返回。 - Java层封装的差异:原来的Java实现中,
FileInputStream.read经过ART虚拟机封装,调用ParcelFileDescriptor.close()时,虚拟机会主动中断该文件流上的IO操作线程,抛出InterruptedIOException,而原生系统调用没有这个逻辑,也不会响应Java层的线程中断信号。 - VPN revoke的特殊行为:当系统触发
VpnService.onRevoke回调时,VPN对应的tun接口会被系统直接回收,该文件描述符不会再产生任何IO事件,也不会主动返回错误,因此阻塞的read会永久挂起。
修复方案
推荐使用非阻塞IO + 事件唤醒的方案,性能最优且无兼容性问题,实现步骤如下:
- JNI层新增唤醒机制:使用
eventfd创建一个唤醒事件描述符,用poll同时监听VPN的文件描述符和唤醒描述符的可读事件。 - 新增停止JNI方法:供Java侧主动调用,往
eventfd写入数据触发唤醒,让poll立即返回,主动退出循环。 - Java侧触发时机:在主动关闭VPN前、或者
onRevoke回调触发时,先调用停止JNI方法唤醒原生线程,再执行关闭文件描述符的操作。
兼容提示:如果需要支持Android 4.3及以下版本(API < 19),可以用普通管道(pipe)替代eventfd,逻辑完全一致:创建管道后将读端加入poll监听,唤醒时往写端写入1字节即可。
代码示例
JNI层修改
#include <unistd.h> #include <android/log.h> #include <sys/poll.h> #include <sys/eventfd.h> #include <errno.h> #include <stdint.h> #include <string.h> // 实际生产建议将wake_fd和VPN实例绑定,避免多实例冲突,此处用全局变量简化演示 static int g_wake_fd = -1; JNIEXPORT void JNICALL Java_com_example_Jni_doVPN(JNIEnv *env, jclass cls, jint vpn_fd) { char buf[2000]; g_wake_fd = eventfd(0, EFD_NONBLOCK); struct pollfd fds[2] = { {.fd = vpn_fd, .events = POLLIN}, {.fd = g_wake_fd, .events = POLLIN} }; for(;;) { int poll_ret = poll(fds, 2, -1); // 被信号中断的话重试 if (poll_ret < 0 && errno == EINTR) { continue; } if (poll_ret < 0) { __android_log_print(ANDROID_LOG_ERROR, "foo", "poll失败: %s", strerror(errno)); break; } // 收到主动唤醒信号,退出循环 if (fds[1].revents & POLLIN) { __android_log_print(ANDROID_LOG_INFO, "foo", "收到停止信号,退出VPN处理线程"); break; } // VPN有可读数据 if (fds[0].revents & POLLIN) { int read_len = read(vpn_fd, buf, sizeof(buf)); if (read_len <= 0) { __android_log_print(ANDROID_LOG_INFO, "foo", "VPN读取结束,返回值:%d", read_len); break; } __android_log_print(ANDROID_LOG_VERBOSE, "foo", "收到数据包,长度:%d", read_len); // 此处添加你的数据包处理逻辑 } } // 清理资源 close(g_wake_fd); g_wake_fd = -1; } // 新增停止方法,供Java层调用 JNIEXPORT void JNICALL Java_com_example_Jni_stopVPN(JNIEnv *env, jclass cls) { if (g_wake_fd != -1) { uint64_t wake_flag = 1; write(g_wake_fd, &wake_flag, sizeof(wake_flag)); } }
Java层修改
- 主动关闭VPN的逻辑:
// 关闭VPN前先唤醒原生线程 Jni.stopVPN(); try { lt.close(); } catch (IOException e) { Log.i(logTag,"关闭fd异常: "+e); }
VpnService的onRevoke回调添加唤醒逻辑:
@Override public void onRevoke() { super.onRevoke(); Jni.stopVPN(); // 其他清理逻辑 }
内容的提问来源于stack exchange,提问作者user1772004
相关产品推荐
相关产品推荐

