在移动设备上,虚拟化的需求正在逐渐增加。其一,移动设备配置越来越高,一些高端配置已和桌面设备接近,这为虚拟化奠定了基础;其二,用户对于移动设备使用场景的多样性与日俱增。现在移动设备不仅用于娱乐日用,还用于工作;其三,安全与隐私问题日益凸显。移动设备上有更多的隐私信息,如各种账号,支付密码等,同时,各种病毒木马正在向移动设备迅速蔓延。这种背景下在一个隔离的环境中运行敏感软件是更加安全的做法;其四,多用户需求的出现。有时手机,尤其平板用户是多个,比如给小孩玩时就希望在一个特定的受限运行环境下。
桌面系统中的虚拟化技术已比较成熟,厂商也都提供了硬件支持,各种虚拟化解决方案也都使用广泛。而移动平台上,一方面由于计算能力相对有限,另一方面移动处理器对虚拟化的支持没有桌面系统中那么成熟完善。于是基于kernel namespace的container技术因其轻量级成为主要研究热点。而移动平台的一大特点就是各种硬件设备种类繁多,而kernel namespace之前主要用于服务器,服务器外设无论是种类还是数量都较少,显示器一般都可以省掉。而手机上各种sensor, camera, wifi, audio, display,
radio, input, LED名目繁多。所以基于kernel namespace的话,一大难题就是解决设备的虚拟化。
在这样的背景下,device namespace在Columbia大学的研究性项目Cell(http://systems.cs.columbia.edu/projects/cells/)中提出,用于实现同一个手机上多个Android系统对设备的并发使用。它本质上是对kernel namespace的扩展。在该方案中,在kernel-level的虚拟化可分为三种方式:1) device driver wrapper, 如framebuffer. 2) namespace-aware subsystem, 如input.
3) namespace-aware device driver, 如binder。另外针对一些闭源的模块,比如RIL和wifi,需要用用户态的device namespace。简言之,就是整个系统启动时会起一个可信的最小初始化环境,称为root namespace。类似Xen里的dom0,它负责真正管理和切换其它虚拟系统,以及真正访问那些闭源模块。其它虚拟系统要访问时,通过IPC向root namespace申请。
随后,Cellrox以商业用途的license发布了device namespace的patch,作为Cellrox的Thinvisor技术一部分(https://github.com/Cellrox/devns-patches/wiki/DeviceNamespace)。Patch分三部分:1)framework主要包含了device namespace的框架,active device namespace的切换处理和对其它开放的API。2)traditional virtualization主要是为了隔离。使不同namespace的进程在操作设备时互不影响。比如binder,
alarm, logger。3)context-aware virtualization主要是为了支持foreground-background模型。比如input, framebuffer, LED, backlight。在这个模型中,系统虽然存在多个虚拟系统,但只有一个是active的。所以一般情况下只有active namespace所在进程对设备的操作会生效,其它的不起作用。这儿又细分为statefull和stateless的device driver。对于stateless的好办,只要在处理时判断下是否active
device namespace,不是的话忽略。而statefull的话就得为每个device namespace保存虚拟状态。
下面以input系统为例来看看device namespace的基本框架和工作原理。首先,需要在已有kernel namespace的基础上加device namespace。具体地,在nsproxy结构中加了dev_namespace结构:
struct nsproxy { ... struct dev_namespace *dev_ns; };
而nsproxy结构作为表示进程的结构task_struct的成员。也就是说,这样,对于一个进程,其nsproxy下的dev_ns代表了它所在的device namespace。nsproxy结构在同个namespace的进程间可以共享,但当这个nsproxy中的其中一个namespace被拷贝或取消共享,nsproxy就会被拷贝,变成所在进程私有。有点COW的意思。
dev_namespace这个核心结构如下:
struct dev_namespace { bool active; ... pid_t init_pid; ... struct blocking_notifier_head notifiers; ... struct dev_ns_info *info[DEV_NS_DESC_MAX]; };
其中active代表它是否是active的device namespace。init_pid是该device namespace下的init进程的pid。info是一个dev_ns_info结构的数组,每个元素代表该device namespace下的一个设备或子系统(为了简便下面统称设备)。notifiers是一个notifier_block链表,它把dev_ns_info结构里的成员nb串起来。它通过Linux notifier chains机制来让active device
namespace切换时可以让每个注册设备调用处理函数。dev_ns_info代表在单个device namespace中的单个设备,它在device namespace中使用设备时创建(如果还没创建)。
struct dev_ns_info { struct dev_namespace *dev_ns; struct list_head list; struct notifier_block nb; atomic_t count; };
其中的list元素用于把不同device namespace的同一设备串在表示该设备的结构dev_ns_desc上。
dev_namespace的初始值为init_dev_ns,它是代表init进程的device namespace。全局变量active_dev_ns指示现在active的device namespace。默认当然是init的device namespace。dev_ns_desc是系统中的全局数组,每个元素表示一个需要用device namespace的设备。
struct dev_ns_desc { char *name; struct dev_ns_ops *ops; struct list_head head; }; static struct dev_ns_desc dev_ns_desc[DEV_NS_DESC_MAX];
其中的dev_ns_ops定义了device namespace framework调用具体device driver的接口。它的实现在device driver中。每个特定设备在初始化时会在dev_ns_desc里面注册一项。在driver-specific的xxx_dev_ns(如evdev_dev_ns)结构中要包含dev_ns_info结构。这样就把common的dev_ns_desc和具体device driver联系起来了。另外,device namespace的framework还通过DEFINE_DEV_NS_INFO定义了一系列的helper函数供driver使用。
#define DEFINE_DEV_NS_INFO(X) _dev_ns_id(X) _dev_ns_find(X) _dev_ns_get(X) _dev_ns_get_cur(X) _dev_ns_put(X)
在每个需要device namespce的内核模块中需要定义该宏,如DEFINE_DEV_NS_INFO(alarm)。以evdev模块为例,DEFINE_DEV_NS_INFO(evdev)会生成以下内容:
static int evdev_ns_id; static inline struct evdev_dev_ns *get_evdev_ns(struct dev_namespace *dev_ns) { struct dev_ns_info *info; info = get_dev_ns_info(evdev_ns_id, dev_ns, 1, 1); return info ? container_of(info, struct evdev_dev_ns, dev_ns_info) : NULL; } static inline struct evdev_dev_ns *find_evdev_ns(struct dev_namespace *dev_ns) { struct dev_ns_info *info; info = get_dev_ns_info(evdev_ns_id, dev_ns, 0, 0); return info ? container_of(info, struct evdev_dev_ns, dev_ns_info) : NULL; } static inline struct evdev_dev_ns *get_evdev_ns_cur(void) { struct dev_ns_info *info; info = get_dev_ns_info_task(evdev_ns_id, current); return info ? container_of(info, struct evdev_ns, dev_ns_info) : NULL; } static inline void put_evdev_ns(struct evdev_dev_ns *evdev_ns) { put_dev_ns_info(evdev_ns_id, &evdev_ns->dev_ns_info, 1); }
其中evdev_ns_id为dev_ns_desc数组和dev_namespace的info数组中对应该设备元素的index。它是在evdev初始化时evdev_init()函数中赋值的。
ret = DEV_NS_REGISTER(evdev, "event dev"); if (ret < 0) { input_unregister_handler(&evdev_handler); return ret; }
本质上调用register_dev_ns_ops()在dev_ns_desc中注册了一个新设备,然后初始化。初始化时比较重要的一步是把模块相关的结构evdev_ns_ops注册到dev_ns_desc中去。这个接口的实现在evdev中,用于日后让device namespace的framework回调evdev子系统。
static struct dev_ns_ops evdev_ns_ops = { .create = evdev_devns_create, .release = evdev_devns_release, };
前面提到过,dev_ns_desc中的一个元素代表一个设备。相当于设备的全局注册表。这里的注册过程是线性搜索第一个空的位置,返回这个位置的index作为evdev_ns_id。这里,设备还没有被真正使用,所以相应的dev_ns_info结构也没有创建,因此元素head中的链表为空。
然后,有那么一天,系统中的某一个进程打开了evdev子系统中的一个设备,然后evdev_open()-> evdev_ns_track_client(client)被调用。
static int evdev_ns_track_client(struct evdev_client *client) { struct evdev_dev_ns *evdev_ns; evdev_ns = get_evdev_ns_cur(); ... client->evdev_ns = evdev_ns; ... list_add(&client->list, &evdev_ns->clients); ... }
这个函数中创建evdev_dev_ns结构。前面提到过,每个需要使用device namespace的设备都要定义这个xxx_dev_ns结构。它是deivce driver与device namespace framework的桥梁。evdev_dev_ns中包含了dev_ns_info结构。每次打开evdev设备会创建一个evdev_client对象。所有同一个device namespace下的evdev_client被串到代表该device namespace中evdev设备的结构evdev_dev_ns的成员clients中。
上面的get_evdev_ns_cur()依次调用get_dev_ns_info_task() -> get_dev_ns_info()。这个函数中会查打开设备进程所在device namespace中是否已注册该设备。有的话就直接返回,否则就调用new_dev_ns_info()新建。但这个dev_ns_info结构是被包在一个driver-specific的xxx_dev_ns结构中的。所以要调用之前注册的回调先初始化外面的结构,这里就是evdev_devns_create()。初始化完evdev_dev_ns后再把它里边的dev_ns_info返回,串到代表该设备的dev_ns_desc数组中去。看一下evdev_dev_ns_create(),其中创建driver-specific的device
namespace结构evdev_dev_ns。然后注册notifier函数,它在切换active device namespace时会被回调。
dev_ns_info->nb = evdev_ns_switch_notifier; dev_ns_register_notify(dev_ns, &dev_ns_info->ns);
这个notifier chain的结构如下:
考虑上面的数据结构,下面是一张总图说明它们之间的大体关系。这个例子中,其中有两个device namespace,考虑两个设备evdev和alarm。evdev在在两个device namespace都有使用,其中一个device namespace中有两个client。alarm只在一个device namespace中使用。
然后,当active device namespace切换时set_active_dev_ns()被调用。active device namespace的切换是通过/proc文件来通知kernel的。当然这仅是作demo之用,真正用时可以改为其它接口。在dev_namespace_init()中,创建/proc/dev_ns/active_ns_pid和/proc/dev_ns/ns_tag。它们的file_operations结构分别为active_ns_fileops和ns_tag_fileops。以active_ns_pid为例,当它被写时,触发proc_active_ns_write()
-> dev_ns_proc_write() -> set_active_dev_ns(),接着它会调用先前注册的notifier函数。这里过程很直观,比如A namespace切到B namespace,先发DEV_NS_EVENT_DEACTIVATE事件到input driver通知A namespace切到后台,然后将active device namespace设为B,最后发DEV_NS_EVENT_ACTIVATE事件到input driver通知B namespace激活了。
void set_active_dev_ns(struct dev_namespace *next_ns) { ... (void) blocking_notifier_call_chain(&prev_ns->notifiers, DEV_NS_EVENT_DEACTIVATE, prev_ns); (void) blocking_notifier_call_chain(&dev_ns_notifiers, DEV_NS_EVENT_DEACTIVATE, prev_ns); ... next_ns->active = true; ... active_dev_ns = next_ns; ... (void) blocking_notifier_call_chain(&next_ns->notifiers, DEV_NS_EVENT_ACTIVATE, next_ns); (void) blocking_notifier_call_chain(&dev_ns_notifiers, DEV_NS_EVENT_ACTIVATE, next_ns); ... }
这里blocking_notifier_call_chain()本质上是调用之前注册的evdev_ns_swtich_callback(),该函数中先根据当前device namespace找到相应的evdev_dev_ns结构。这个evdev_dev_ns中的clients成员指示的链表串了该device namespace的所有设备使用会话,每个用evdev_client表示。前面图中有描述,evdev_client中为device namespace服务的成员有:
struct evdev_dev_ns *evdev_ns; struct list_head list; bool grab;
注意这里的grab是一个虚拟状态,记录着该会话如果在没有多个虚拟系统时是否独占设备,它只代表请求,不代表真正的grab状态,因为还要经过device namespace的逻辑。找到evdev_dev_ns结构后,通过clients成员遍历该device namespace中evdev所有打开的会话,如果处理的消息是DEV_NS_EVENT_ACTIVATE,说明该device namespace切到前台,则如果该会话之前设为独占,就调用evdev_grab()让其真正独占。如果处理的是DEV_NS_EVENT_DEACTIVATE,即该device
namespace切到后台,如果当前会话是真正的独占会话,则调用evdev_ungrab()取消它的grab状态。到这里,把上面的流程总结一下:
有了这些信息后,就可以在读写input event时做一些namespace-aware的逻辑了。如在广播event事件时会检查device namespace,如果不是active的namespace就忽略。对于设备的写处理函数evdev_write()也是类似的。另外,在evdev_do_ioctl()中。对于非active的device namespace,很多时候就不作处理。在EVIOCGRAB的ioctl处理中,对于inactive的device namespace中的会话,将client->grab中的状态设为应该要设的状态,但不真正做事,而是如上节所说等到active
device namespace切换时再做。
作为移动平台容器方案,device namespace还有需要扩展的地方,比如支持多个active的device namespace,但它提供了一种轻量级设备虚拟化的可行方案。其它设备虚拟化方案比如还有systemd中的multi-session(https://dvdhrm.wordpress.com/2013/08/25/sane-session-switching/),它可以免除对kernel的改动。具体使用可以按需求和设备类型结合多种方案。