我们知道 USB 是主从模式(Host/Slave)的,有趣的是,Android 手机既可以被当做从设备连接到我们的 PC 被识别成为一个 USB 设备,也可以当做主设备识别外部的 USB 设备。这篇东东主要着眼于后者。
我之前一直有一个疑惑,那就是 Android 上层与 Linux 内核底层究竟是如何通讯的。之前我知道了可以通过 sysfs ,或者是通过 uevent 来打通内核与上层。现在,我学习了 Android USB Host 之后,发现了还有一种新的方法 —— Android JNI 。
首先声明,我对 Android 这一块知道的确实不多,所以下面的描述肯定是会有这样那样的错误,还待日后更加深入学习。
Android JNI
关于 Java JNI 的我写了另外的一篇东东,就不再赘述了。下面着重说明 Android JNI 与 Java JNI 的区别。
Android 中使用了一种不同于传统 Java JNI 的方法来定义其 native 函数。其中很重要的区别就是 Android 使用了一种 Java 和 C 函数的映射表数组,并在其中表述了函数的参数和返回值。这个数组的类型就是 JNINativeMethod ,定义如下:
typedef struct {
const char *name;
const char *signature;
void *fnPtr;
} JNINativeMethod;
其中,第一个变量是 Java 中的函数名,第二个变量是一个描述返回值和参数的字符串,第三个是指向 native 实现的函数指针。
比较难以理解的第二个参数,具体可以参照这篇博客。
发现 USB 设备流程
首先,我先使用自顶向下来说明一下整个流程:
在 UsbHostManager 中创建一个线程来监控(注意,这里用“监控”可是很有讲究的,因为当 Android 设备处于 Host 模式时,当有 USB 设备插拔时,首先相应的是 Linux 内核,然后内核再将设备变动的信息传递给上层,而 Android 根据这些信息在进行相应的动作,所以这里使用了“监控”二字。这跟 Android 设备处于 Slave 模式下的 UsbDeviceManager 是完全不同的)设备的连接状态。该线程执行的是 JNI 层的函数,而 JNI 层则是通过调用 libusb 库的函数,利用内核提供的 inotify 机制来监控 /dev/bus/usb 下文件的变动来判断是否有新的设备的插拔。下面的图示很好地展现了这种自顶向下的路线:
(Source:Unboxing Android USB)
接下来,我再以自底向上的方法来详细阐述整个流程:
首先,USB 插入设备之后,内核会在 /dev/bus/usb 这个目录下建立代表该文件的设备文件入口(device file entry)(当然,个中细节还需要花时间深入理解,这里先假设其成立),然后 Android 上层使用 libusb 这个库来监控这个目录下的文件变动,其中用到的技术就是 inotify 。关于 inotify 我也写了一篇东东来简要地说明它,可以先去看看。下面将通过分析 libusb 的源代码(具体位置在 system/core/libusb/usbhost.c)来详细说明:
首先,先初始化 inotify ,获得文件描述符,此后的所有事件都是通过读取该文件描述符中的数据来判断:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct usb_host_context *usb_host_init()
{
struct usb_host_context *context = calloc(1, sizeof(struct usb_host_context));
if (!context) {
fprintf(stderr, "out of memory in usb_host_context\n");
return NULL;
}
// 初始化,获取文件描述符
context->fd = inotify_init();
if (context->fd < 0) {
fprintf(stderr, "inotify_init failed\n");
free(context);
return NULL;
}
return context;
}
这里要说明一下 struct usb_host_context 这个数据结构:
1
2
3
4
5
6
7
8
9struct usb_host_context {
int fd; // inotify 返回的文件描述符,通过读取其中的数据来判断事件
usb_device_added_cb cb_added; // 当有 USB 设备插入是调用的回调函数,在 JNI 层赋值,后面会提到
usb_device_removed_cb cb_removed; // 同上,当有 USB 设备拔出是的回调函数
void *data; // 调用者给上面两个回调函数的参数
int wds[MAX_USBFS_WD_COUNT]; // /dev/bus/usb 下各个子目录对应的 watch descriptor
int wdd; // /dev 对应的 watch descriptor
int wddbus; // /dev/bus 对应的 watch descriptor
};
然后,就是要开始添加要监控的目录了(即我们这里的 /dev/bus/usb):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void usb_host_run(struct usb_host_context *context,
usb_device_added_cb added_cb,
usb_device_removed_cb removed_cb,
usb_discovery_done_cb discovery_done_cb,
void *client_data)
{
int done;
done = usb_host_load(context, added_cb, removed_cb, discovery_done_cb, client_data);
while (!done) {
done = usb_host_read_event(context);
}
} /* usb_host_run() */
主要的工作是在 usb_host_load() 中完成的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31int usb_host_load(struct usb_host_context *context,
usb_device_added_cb added_cb,
usb_device_removed_cb removed_cb,
usb_discovery_done_cb discovery_done_cb,
void *client_data)
{
int done = 0;
int i;
context->cb_added = added_cb;
context->cb_removed = removed_cb;
context->data = client_data;
/* watch for files added and deleted within USB_FS_DIR */
context->wddbus = -1;
for (i = 0; i < MAX_USBFS_WD_COUNT; i++)
context->wds[i] = -1;
/* watch the root for new subdirectories */
// 如上面说说,wdd 是用来发现 /dev 下是否有子目录创建或者删除的
context->wdd = inotify_add_watch(context->fd, DEV_DIR, IN_CREATE | IN_DELETE);
watch_existing_subdirs(context, context->wds, MAX_USBFS_WD_COUNT);
/* check for existing devices first, after we have inotify set up */
done = find_existing_devices(added_cb, client_data);
if (discovery_done_cb)
done |= discovery_done_cb(client_data);
return done;
} /* usb_host_load() */
其中的 watch_existing_subdirs() 就是添加 /dev/bus/usb 下的文件变动的监控的。函数不难理解,主要说明一下,就是 context->wds 这个数组:wds[0] 代表的是整个 /dev/bus/usb 下子目录的变动,而 wds[] 数组中其他的代表了真正设备的变动,因为内核会在每个新设备插拔之后在 /dev/bus/bus 这个目录下增加设备对应的文件如:/dev/bus/usb/001/001 等。
在完成了文件监控之后,接下来就可以来读数据了,调用了上面的 usb_host_read_event() ,这个函数重复的地方很多,下面我只选取了关键的 USB 设备的部分来说明:
1
2
3
4
5
6
7
8
9
10for (i = 1; (i < MAX_USBFS_WD_COUNT) && !done; i++) {
if (wd == context->wds[i]) {
snprintf(path, sizeof(path), USB_FS_DIR "/%03d/%s", i, event->name);
if (event->mask == IN_CREATE) {
done = context->cb_added(path, context->data);
} else if (event->mask == IN_DELETE) {
done = context->cb_removed(path, context->data);
}
}
}
如果传来的数据是跟真正 USB 设备相关的(通过 watch descriptor 来判断,而且结合上面对 context->wds[] 数组的说明),那么就通过调用相应的回调函数来通知上层相应的设备变动。
好,底层的这部分说明完了,接下来就要 JNI 层了。JNI 层负责给调用上面提到的函数,指定合适的回调函数,具体位置在:frameworks/base/services/core/jni/com_android_server_UsbHostManager.cpp:
1
2
3
4
5
6
7
8
9
10static void android_server_UsbHostManager_monitorUsbHostBus(JNIEnv* /* env */, jobject thiz)
{
struct usb_host_context* context = usb_host_init();
if (!context) {
ALOGE("usb_host_init failed");
return;
}
// this will never return so it is safe to pass thiz directly
usb_host_run(context, usb_device_added, usb_device_removed, NULL, (void *)thiz);
}
在联系我们在 libusb 上提到的 usb_host_init() 和 usb_host_run() ,有没有一种 connected 的感觉呢?
我们前面说过,JNI 层的函数是被 UsbHostManager 调用的,我们再来看看 UsbHostManager 中具体的监控进程,位置在 frameworks/base/services/usb/java/com/android/server/usb/UsbHostManager.java:
1
2
3
4
5
6
7
8
9
10
11
12public void systemReady(){
synchronized (mLock) {
// Create a thread to call into native code to wait for USB host events.
// This thread will call us back on usbDeviceAdded and usbDeviceRemoved.
Runnable runnable = new Runnable() {
public void run(){
monitorUsbHostBus();
}
};
new Thread(null, runnable, "UsbService host thread").start();
}
}
那么如何打通这两者呢?哈,我们上面提到的 Android JNI 就扮演着连接者的角色。看看下面的代码你就明白了:
1
2
3
4static JNINativeMethod method_table[] = {
{ "monitorUsbHostBus", "()V", (void*)android_server_UsbHostManager_monitorUsbHostBus },
......
};
至此,Android 发现外部 USB 设备的整个流程我们就说明完了。
EOF