You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 + 事件唤醒的方案,性能最优且无兼容性问题,实现步骤如下:

  1. JNI层新增唤醒机制:使用eventfd创建一个唤醒事件描述符,用poll同时监听VPN的文件描述符和唤醒描述符的可读事件。
  2. 新增停止JNI方法:供Java侧主动调用,往eventfd写入数据触发唤醒,让poll立即返回,主动退出循环。
  3. 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层修改

  1. 主动关闭VPN的逻辑:
// 关闭VPN前先唤醒原生线程
Jni.stopVPN();
try {
    lt.close();
} catch (IOException e) {
    Log.i(logTag,"关闭fd异常: "+e);
}
  1. VpnService的onRevoke回调添加唤醒逻辑:
@Override
public void onRevoke() {
    super.onRevoke();
    Jni.stopVPN();
    // 其他清理逻辑
}

内容的提问来源于stack exchange,提问作者user1772004

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 01:15:08