Kernel 不支持 timerfd 使用 timeout

VPS 内核版本较低,低到还没实现 timerfd。由于是 OpenVZ 的方案,基本上是无法更新 Kernel 的。有一个必要应用正好要使用 HevLib 库的 HevEventSourceTimeout,此 source 是基于 timerfd 的,临时的解决方法是使用 HevEventSourceSignal 监听 SIGALRM 信号, 使用 alarm 或 ualaram 设置定时器:

#include <stdio.h>
#include <signal.h>
#include <hev-lib.h>
 
static bool
signal_handler (void *data)
{
        printf ("timeout signal\n");
 
        alarm (1);
 
        return true;
}
 
int
main (int argc, char *argv[])
{
        HevEventLoop *loop = NULL;
        HevEventSource *signal = NULL;
 
        loop = hev_event_loop_new ();
 
        signal = hev_event_source_signal_new (SIGALRM);
        hev_event_source_set_callback (signal, signal_handler, NULL, NULL);
        hev_event_loop_add_source (loop, signal);
        hev_event_source_unref (signal);
 
        alarm (1);
 
        hev_event_loop_run (loop);
 
        hev_event_loop_unref (loop);
 
        return 0;
}

Over!

Android 自动启动服务

最近在将 HevSocks5Client 移植到 Android 上了,在经过增加 signalfd 和 timerfd 相关的系统调用支持后,就可以直接使用 NDK 编译出 executable 了。直接的 native exectuable 在 Android 系统总还是不太方便用哦。还是做成一个 apk 吧,暂定只写一个 service 并开机自动启用,无 activity 的。

Java 中调用 native 程序我选择使用 JNI 方式,直接在 JNI_OnLoad 方法中调用 pthread_create 创建个线程跑原来的 main 就行啦。

...
#if defined(ANDROID)
#include <jni.h>
#include <pthread.h>
#endif
 
int
main (int argc, char *argv[])
{
    ...
}
 
#if defined(ANDROID)
static void *
thread_handler (void *data)
{
    main (0, NULL);
    return NULL;
}
 
jint
JNI_OnLoad (JavaVM *vm, void *reserved)
{
    pthread_t thread;
    pthread_create (&thread, NULL, thread_handler, NULL);
    return JNI_VERSION_1_4;
}
#endif

Android 服务
服务主要是加载 JNI 接口的 hev-socks5-client 库,使服务跑起来。

package hev.socks5;
 
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
 
public class MainService extends Service {
 
        static {
                System.loadLibrary("hev-socks5-client");
        }
 
        public IBinder onBind(Intent intent) {
                return null;
        }
}

BroadcastReceiver
ServiceReceiver 的功能就是监听系统上的 BOOT_COMPLETED 事件,用于实现自动启动服务。

package hev.socks5;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
 
public class ServiceReceiver extends BroadcastReceiver {
 
        @Override
        public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
                        Intent i = new Intent(context, MainService.class);
                        context.startService(i);
                }
        }
}

AndroidManifest.xml
最后,要在 Manifest 中注册 Service 和 Receiver,增加上访问 Internet 和 Boot completed 的权限。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="hev.socks5"
          android:versionCode="1"
          android:versionName="1.0">
        <application android:label="@string/app_name" >
                <service android:name=".MainService">
                        <intent-filter>
                                <action android:name="hev.socks5.MainService" />
                        </intent-filter>
                </service>
                <receiver android:enabled="true" android:name=".ServiceReceiver">
                        <intent-filter>
                                <action android:name="android.intent.action.BOOT_COMPLETED" />
                        </intent-filter>
                </receiver>
        </application>
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
</manifest>

Tips
此方法仅在 Android 2.3.3 及之前版本有效,之后版本如果此应用在安装后从没运行过,Broadcast Receiver 将接收不到 boot completed 的 action,解决方法是使用命令手动启动一下 service。

am startservice hev.socks5/.MainService

Over!

变量运算溢出问题

最近犯了一个低级错误呀,记录一下这个教训吧,大概如下:

struct timespec {
    time_t tv_sec;
    long tv_nsec;
};
struct timespec ts;
uint64_t nsec = ts.tv_sec * 1000000000 + ts.tv_nsec;

也就是将 ts 换算成 nanoseconds,在 64-bit 系统上工作的没有问题,但 32-bit 系统上就杯具了。

这个地方可能会有两个问题:
1. 认为目标变量是 64-bit 就足够了,但乘法运算实际上是 (time_t) (tv.tv_sec * 1000000000),结果有可能溢出了。
2. 认为 time_t 类型在所有环境下都是足够的。(我就是认为它都是 64-bit 的)

Over!

移植 HevLib 到 Android 系统

打算将 HevSocks5Client 移植到 Android 系统,替换原来的 HevTLSTunnel 实现。先要移植 HevLib,原以为只要增加两个文件 Application.mk 和 Android.mk 就可以了。但编译到 hev-event-source-signal.c 的时候就出错了,报 sys/signalfd.h 文件找不到。于是 android-ndk 目录里搜索了一下,还真没有这个文件的。这才意识到有可能 Android 的用户态目前还没开放这些 API。

如果仅仅是用户态没有导出问题应该不太,关键内核要实现了这些功能。还好我的系统中存在 /proc/config.gz 文件,解压查询确认 SIGNALFD、TIMERFD 及 EVENTFD 都是支持。下面的事情就好办了,就是增加几个系统调用的实现问题了。进一步确认了间接系统调用 syscall 函数是实现了的,连汇编都不用写了。

diff --git a/src/hev-event-source-signal.c b/src/hev-event-source-signal.c
index daf3e96..01f0088 100644
--- a/src/hev-event-source-signal.c
+++ b/src/hev-event-source-signal.c
@@ -11,7 +11,12 @@
 #include <unistd.h>
 #include <signal.h>
 #include <sys/epoll.h>
+#if defined(ANDROID)
+#include <fcntl.h>
+#include <asm/unistd.h>
+#else /* GENERIC */
 #include <sys/signalfd.h>
+#endif
 
 #include "hev-event-source-signal.h"
 
@@ -25,6 +30,31 @@ struct _HevEventSourceSignal
 	int signal_fd;
 };
 
+#if defined(ANDROID)
+#define SFD_NONBLOCK	O_NONBLOCK
+
+struct signalfd_siginfo
+{
+  uint32_t ssi_signo;
+  int32_t ssi_errno;
+  int32_t ssi_code;
+  uint32_t ssi_pid;
+  uint32_t ssi_uid;
+  int32_t ssi_fd;
+  uint32_t ssi_tid;
+  uint32_t ssi_band;
+  uint32_t ssi_overrun;
+  uint32_t ssi_trapno;
+  int32_t ssi_status;
+  int32_t ssi_int;
+  uint64_t ssi_ptr;
+  uint64_t ssi_utime;
+  uint64_t ssi_stime;
+  uint64_t ssi_addr;
+  uint8_t __pad[48];
+};
+#endif
+
 static HevEventSourceFuncs hev_event_source_signal_funcs =
 {
 	.prepare = NULL,
@@ -33,6 +63,14 @@ static HevEventSourceFuncs hev_event_source_signal_funcs =
 	.finalize = hev_event_source_signal_finalize,
 };
 
+#if defined(ANDROID)
+static int
+signalfd (int fd, const sigset_t *mask, int flags)
+{
+	return syscall (__NR_signalfd4, fd, mask, _NSIG / 8, flags);
+}
+#endif
+
 HevEventSource *
 hev_event_source_signal_new (int signal)
 {
diff --git a/src/hev-event-source-timeout.c b/src/hev-event-source-timeout.c
index 57a9b00..cacd196 100644
--- a/src/hev-event-source-timeout.c
+++ b/src/hev-event-source-timeout.c
@@ -11,10 +11,20 @@
 #include <errno.h>
 #include <unistd.h>
 #include <sys/epoll.h>
+#if defined(ANDROID)
+#include <fcntl.h>
+#include <asm/unistd.h>
+#else /* GENERIC */
 #include <sys/timerfd.h>
+#endif
 
 #include "hev-event-source-timeout.h"
 
+#if defined(ANDROID)
+#define TFD_NONBLOCK	O_NONBLOCK
+#define TFD_TIMER_ABSTIME 1
+#endif
+
 static bool hev_event_source_timeout_prepare (HevEventSource *source);
 static bool hev_event_source_timeout_check (HevEventSource *source, HevEventSourceFD *fd);
 static void hev_event_source_timeout_finalize (HevEventSource *source);
@@ -35,6 +45,22 @@ static HevEventSourceFuncs hev_event_source_timeout_funcs =
 	.finalize = hev_event_source_timeout_finalize,
 };
 
+#if defined(ANDROID)
+static int
+timerfd_create (int clockid, int flags)
+{
+	return syscall (__NR_timerfd_create, clockid, flags);
+}
+
+static int
+timerfd_settime (int fd, int flags,
+			const struct itimerspec *new_value,
+			struct itimerspec *old_value)
+{
+	return syscall (__NR_timerfd_settime, fd, flags, new_value, old_value);
+}
+#endif
+
 HevEventSource *
 hev_event_source_timeout_new (unsigned int interval)
 {

Over!

重新实现了 Socsk5

可能你会想实现那么多的 Socsk5 能吃吗?其实我只是想研究一下 I/O 模型,拿它来练习一下,如果效果不错也可以拿来用用。本次的实现没有使用 GLib,或许我真的想再也不用 GLib 啦?应该是这样的吧,至少我已经开始在实现我需要的东西。HevLib 就是我的一个初级替代品吧,目前仅仅包含最基本的数据结构、EventSource、EventLoop 等等。更多的数据结构与算法还有类型和对象系统就在以后有需要的时候再实现,一步一步来吧。

重写的 Socks5 Proxy 就是使用了 HevLib 的事件循环机制(Epoll,优先级排序),目前是单线程异步事件驱动的,感觉异步事件驱动实现起来还是有点麻烦的,和内核的进程调度一样,要上下文切换,实际来说 Session 中已经存储了上下文了,要做说也就是流程控制,我是简单的使用了一个状态变量控制的(可能不是太好,有好的方法可以告诉我哦)。

另外,在 DNS 解析上遇到一点麻烦、gethostbyname 虽然工作的没问题,显然这个阻塞的方法并不适合我现在的模型,又看了一下 getaddrinfo_a 和 c-ares 这个异步 DNS 查询库,结果发现想套到当前的事件循环机制中都有一些麻烦。后来就直接基于 UDP 实现了一个简单的异步的标准A记录查询器,简单又好用。

现在服务已经在 VPS 上部署了,RAM 的使用由原来的 300+M 直接下降到 80+M,功能还和原来一样的。;)

代码仓库:https://gitcafe.com/heiher/hev-socks5-proxy

Over!