Waydroid 源码分析(二):容器管理与系统初始化
本文深入分析 Waydroid 的容器管理与初始化流程,涵盖 Binder 驱动分配、LXC 配置生成、OverlayFS 挂载策略、OTA 镜像管理,以及硬件属性自动检测的完整实现。
概述
Waydroid 的容器管理层是连接宿主机与 Android 系统的桥梁。它负责:
- 检测硬件环境并分配必要的内核驱动
- 下载和校验 Android 系统镜像
- 生成 LXC 配置文件
- 构建 OverlayFS 分层文件系统
- 启动和管理 LXC 容器的完整生命周期
核心代码分布在以下文件中:
| 文件 | 职责 |
|---|---|
tools/actions/initializer.py | 系统初始化(waydroid init) |
tools/actions/container_manager.py | 容器生命周期管理 |
tools/helpers/drivers.py | Binder/Ashmem 驱动管理 |
tools/helpers/lxc.py | LXC 配置生成与容器操作 |
tools/helpers/images.py | 镜像下载、校验与挂载 |
tools/helpers/mount.py | 挂载操作封装 |
tools/helpers/gpu.py | GPU 检测与驱动映射 |
tools/helpers/protocol.py | AIDL 协议版本设置 |
tools/config/__init__.py | 配置管理与默认值 |
一、Binder 驱动分配
问题背景
Android 依赖三个 Binder 设备进行 IPC:binder、vndbinder、hwbinder。但在标准 Linux 上,这些设备节点并不存在。更复杂的是,宿主机上可能同时运行多个 Android 容器方案(如 Anbox),它们可能已经占用了 /dev/binder 等标准路径。
设备名称优先级
tools/helpers/drivers.py 定义了一个设备名称候选列表,按优先级排列:
BINDER_DRIVERS = [
"anbox-binder", # 兼容 Anbox 命名
"puddlejumper", # 替代名称
"bonder", # 替代名称
"binder" # 标准名称(最后尝试)
]
VNDBINDER_DRIVERS = [
"anbox-vndbinder",
"vndpuddlejumper",
"vndbonder",
"vndbinder"
]
HWBINDER_DRIVERS = [
"anbox-hwbinder",
"hwpuddlejumper",
"hwbonder",
"hwbinder"
]
这个设计的精妙之处在于:优先使用带前缀的名称(如 anbox-binder),避免与宿主机上其他方案的 /dev/binder 冲突。只有在没有找到任何已有设备时,才使用标准的 binder 名称。
setupBinderNodes:设备发现
setupBinderNodes() 在初始化时被调用,负责找到可用的 Binder 设备:
def setupBinderNodes(args):
if args.vendor_type == "MAINLINE":
# 标准 Linux: 先尝试加载驱动,再查找设备
probeBinderDriver(args)
for node in BINDER_DRIVERS:
if os.path.exists("/dev/" + node):
args.BINDER_DRIVER = node
has_binder = True
if not has_binder:
raise OSError('Binder node "binder" for waydroid not found')
# vndbinder, hwbinder 同理...
else:
# HALIUM: 宿主机已有 binder,但跳过最后一个(标准 binder)以避免冲突
for node in BINDER_DRIVERS[:-1]: # 注意 [:-1] — 排除 "binder"
if os.path.exists("/dev/" + node):
args.BINDER_DRIVER = node
has_binder = True
关键区别:HALIUM 环境下,标准的 /dev/binder 等设备已被宿主机的 Android HAL 使用,所以 Waydroid 只搜索带前缀的名称(BINDER_DRIVERS[:-1]),确保不会抢占宿主机的设备。
probeBinderDriver:驱动加载与 Binderfs
probeBinderDriver() 是加载 Binder 驱动的核心函数:
def probeBinderDriver(args):
binder_dev_nodes = []
# 1. 检查哪些设备还不存在
for node in BINDER_DRIVERS:
if os.path.exists("/dev/" + node):
has_binder = True
if not has_binder:
binder_dev_nodes.append(BINDER_DRIVERS[0]) # "anbox-binder"
# vndbinder, hwbinder 同理...
if len(binder_dev_nodes) > 0:
# 2. 尝试传统方式:modprobe 加载内核模块
if not isBinderfsLoaded(args):
devices = ','.join(binder_dev_nodes)
command = ["modprobe", "binder_linux",
"devices=\"{}\"".format(devices)]
tools.helpers.run.user(args, command, check=False)
# 3. 如果内核支持 binderfs,使用 binderfs 方式
if isBinderfsLoaded(args):
command = ["mkdir", "-p", "/dev/binderfs"]
tools.helpers.run.user(args, command, check=False)
command = ["mount", "-t", "binder", "binder", "/dev/binderfs"]
tools.helpers.run.user(args, command, check=False)
# 通过 ioctl 动态创建设备节点
allocBinderNodes(args, binder_dev_nodes)
# 创建符号链接到 /dev/
command = ["ln", "-s"]
command.extend(glob.glob("/dev/binderfs/*"))
command.append("/dev/")
tools.helpers.run.user(args, command, check=False)
这里有两种加载路径:
路径 A:传统 modprobe
modprobe binder_linux devices="anbox-binder,anbox-vndbinder,anbox-hwbinder"
内核模块在加载时创建指定名称的设备节点。
路径 B:Binderfs(现代内核)
Binderfs 是 Linux 5.0+ 引入的特性,允许在运行时动态创建 Binder 设备。allocBinderNodes() 使用 ioctl 实现这一点:
def allocBinderNodes(args, binder_dev_nodes):
# 构造 BINDER_CTL_ADD ioctl 命令
BINDER_CTL_ADD = IOWR(98, 1, 264) # type=98('b'), nr=1, size=264
binderctrlfd = open('/dev/binderfs/binder-control', 'rb')
for node in binder_dev_nodes:
# 打包为 C 结构体: char name[256] + uint32 major + uint32 minor
node_struct = struct.pack('256sII', bytes(node, 'utf-8'), 0, 0)
try:
fcntl.ioctl(binderctrlfd.fileno(), BINDER_CTL_ADD, node_struct)
except FileExistsError:
pass # 设备已存在,忽略
BINDER_CTL_ADD 的 ioctl 编码过程值得注意——它遵循 Linux ioctl 编号规范:
- Direction:
READ|WRITE(双向) - Type:
98(‘b’) - Nr:
1 - Size:
264(256字节名称 + 4字节 major + 4字节 minor)
Binderfs 检测
def isBinderfsLoaded(args):
with open("/proc/filesystems", "r") as handle:
for line in handle:
words = line.split()
if len(words) >= 2 and words[1] == "binder":
return True
return False
通过检查 /proc/filesystems 是否包含 binder 文件系统类型来判断。
Ashmem 驱动
Ashmem 的加载相对简单:
def probeAshmemDriver(args):
if not os.path.exists("/dev/ashmem"):
command = ["modprobe", "-q", "ashmem_linux"]
tools.helpers.run.user(args, command, check=False)
if not os.path.exists("/dev/ashmem"):
return -1
return 0
如果 ashmem 不可用,make_base_props() 中会设置 sys.use_memfd=true,让 Android 回退到标准 Linux 的 memfd_create 系统调用:
if not os.path.exists("/dev/ashmem"):
props.append("sys.use_memfd=true")
二、系统初始化流程
init() 函数
waydroid init 命令调用 tools/actions/initializer.py 中的 init() 函数。完整流程:
def init(args):
# 1. 已初始化且未强制重新初始化 → 跳过
if is_initialized(args) and not args.force:
logging.info("Already initialized")
# 2. 配置硬件环境
if not setup_config(args):
return
# 3. 如果容器正在运行 → 先停止
status = helpers.lxc.status(args)
if status != "STOPPED":
container.Stop(False)
# 4. 下载镜像(非预安装路径)
if args.images_path not in defaults["preinstalled_images_paths"]:
helpers.images.get(args)
else:
helpers.images.remove_overlay(args)
# 5. 创建 overlay 目录
if not os.path.isdir(defaults["rootfs"]):
os.mkdir(defaults["rootfs"])
if not os.path.isdir(defaults["overlay"]):
os.mkdir(defaults["overlay"])
os.mkdir(defaults["overlay"] + "/vendor")
if not os.path.isdir(defaults["overlay_rw"]):
os.mkdir(defaults["overlay_rw"])
os.mkdir(defaults["overlay_rw"] + "/system")
os.mkdir(defaults["overlay_rw"] + "/vendor")
# 6. 加载 ashmem 驱动
helpers.drivers.probeAshmemDriver(args)
# 7. 设置宿主机权限
helpers.lxc.setup_host_perms(args)
# 8. 生成 LXC 配置
helpers.lxc.set_lxc_config(args)
# 9. 生成硬件属性文件
helpers.lxc.make_base_props(args)
# 10. 如果之前在运行 → 重新启动
if status != "STOPPED":
container.Start(session)
setup_config():环境检测
def setup_config(args):
cfg = tools.config.load(args)
# CPU 架构
args.arch = helpers.arch.host() # arm64 或 x86_64
cfg["waydroid"]["arch"] = args.arch
# Vendor 类型
args.vendor_type = get_vendor_type(args) # MAINLINE 或 HALIUM_X
cfg["waydroid"]["vendor_type"] = args.vendor_type
# Binder 设备
helpers.drivers.setupBinderNodes(args)
cfg["waydroid"]["binder"] = args.BINDER_DRIVER
cfg["waydroid"]["vndbinder"] = args.VNDBINDER_DRIVER
cfg["waydroid"]["hwbinder"] = args.HWBINDER_DRIVER
# 检查预安装镜像
for preinstalled_images in defaults["preinstalled_images_paths"]:
# 搜索 /etc/waydroid-extra/images 和 /usr/share/waydroid-extra/images
if os.path.isdir(preinstalled_images):
system_exists = os.path.isfile(system_path) or stat.S_ISBLK(os.stat(system_path).st_mode)
vendor_exists = os.path.isfile(vendor_path) or stat.S_ISBLK(os.stat(vendor_path).st_mode)
if system_exists and vendor_exists:
args.images_path = preinstalled_images
break
注意 stat.S_ISBLK() 的使用——预安装镜像不仅支持普通文件(.img),还支持块设备,这意味着发行版可以将 Android 镜像放在独立的分区上。
Vendor 类型判断
def get_vendor_type(args):
vndk_str = helpers.props.host_get(args, "ro.vndk.version")
ret = "MAINLINE"
if vndk_str != "":
vndk = int(vndk_str)
if vndk > 19:
halium_ver = vndk - 19
if vndk > 31:
halium_ver -= 1 # Android 12L → Halium 12
ret = "HALIUM_" + str(halium_ver)
if vndk == 32:
ret += "L"
return ret
VNDK(Vendor Native Development Kit)版本号直接对应 Android API 级别。如果宿主机有 ro.vndk.version 属性(通过 getprop 读取),说明宿主机本身就有 Android 供应商层(如 Halium/Ubuntu Touch),Waydroid 需要适配这个环境。
VNDK 版本到 Halium 版本的映射逻辑:
- VNDK 28(Android 9)→ HALIUM_9
- VNDK 29(Android 10)→ HALIUM_10
- VNDK 30(Android 11)→ HALIUM_11
- VNDK 32(Android 12L)→ HALIUM_12L
- VNDK 33(Android 13)→ HALIUM_13
三、OTA 镜像下载与校验
下载流程
tools/helpers/images.py 中的 get() 函数负责从 OTA 通道下载镜像:
def get(args):
cfg = tools.config.load(args)
# 1. 获取 system OTA 频道信息
system_ota = cfg["waydroid"]["system_ota"]
# URL 格式: https://ota.waydro.id/system/lineage/waydroid_x86_64/VANILLA.json
system_request = helpers.http.retrieve(system_ota)
system_responses = json.loads(system_request[1].decode('utf8'))["response"]
# 2. 检查是否有更新
for system_response in system_responses:
if system_response['datetime'] > int(cfg["waydroid"]["system_datetime"]):
# 3. 下载 ZIP 包
images_zip = helpers.http.download(args, system_response['url'],
system_response['filename'], cache=False)
# 4. SHA256 校验
with open(images_zip, 'rb') as f:
if sha256sum(f) != system_response['id']:
os.remove(images_zip)
raise ValueError("Downloaded system image hash doesn't match")
# 5. 解压到 images 目录
with zipfile.ZipFile(f, 'r') as zip_ref:
zip_ref.extractall(args.images_path)
# 6. 更新时间戳
cfg["waydroid"]["system_datetime"] = str(system_response['datetime'])
tools.config.save(args, cfg)
os.remove(images_zip)
break
# vendor 镜像同理...
# 7. 清除 overlay(全新开始)
remove_overlay(args)
OTA 通道的 URL 由以下部分构成:
https://ota.waydro.id/system / lineage / waydroid_x86_64 / VANILLA.json
基础 URL ROM类型 架构 系统类型
- ROM 类型:
lineage(LineageOS)或bliss - 系统类型:
VANILLA(纯净版)或GAPPS(含 Google 服务)
SHA256 校验实现
def sha256sum(f):
h = hashlib.sha256()
b = bytearray(128*1024) # 128KB 块读取
mv = memoryview(b)
for n in iter(lambda: f.readinto(mv), 0):
h.update(mv[:n])
f.seek(0) # 重置文件指针,后续 zipfile 可以直接使用
return h.hexdigest()
使用 memoryview 和 readinto 避免在大文件校验时的内存分配开销——这是 Python 中处理大文件哈希的最佳实践。
镜像更新策略
replace() 函数用于系统更新:
def replace(args, system_zip, system_time, vendor_zip, vendor_time):
cfg = tools.config.load(args)
if os.path.exists(system_zip):
with open(system_zip, 'rb') as f:
if validate(args, "system_ota", f): # 验证来自官方通道
with zipfile.ZipFile(f, 'r') as zip_ref:
zip_ref.extractall(args.images_path)
cfg["waydroid"]["system_datetime"] = str(system_time)
os.remove(system_zip)
# vendor 同理...
remove_overlay(args) # 关键: 清除 overlay 确保干净启动
validate() 不仅校验文件完整性,还验证镜像确实来自配置的 OTA 通道:
def validate(args, channel, f):
channel_url = cfg["waydroid"][channel]
channel_request = helpers.http.retrieve(channel_url)
channel_responses = json.loads(channel_request[1].decode('utf8'))["response"]
chksum = sha256sum(f)
for build in channel_responses:
if chksum == build['id']:
return True
return False
overlay 清除
每次镜像更新后都会调用 remove_overlay():
def remove_overlay(args):
if os.path.isdir(tools.config.defaults["overlay_rw"]):
shutil.rmtree(tools.config.defaults["overlay_rw"])
if os.path.isdir(tools.config.defaults["overlay_work"]):
shutil.rmtree(tools.config.defaults["overlay_work"])
这确保新镜像不会被旧的 overlay 修改所干扰。overlay_work 是 OverlayFS 的内部工作目录,也需要一并清理。
四、LXC 配置动态生成
set_lxc_config():配置拼接
tools/helpers/lxc.py 中的 set_lxc_config() 根据 LXC 版本选择合适的配置片段并拼接:
def set_lxc_config(args):
lxc_ver = get_lxc_version(args)
if lxc_ver == 0:
raise OSError("LXC is not installed")
# 根据版本选择配置片段
config_snippets = [config_paths + "base"] # 始终包含基础配置
if lxc_ver <= 2:
config_snippets.append(config_paths + "1") # LXC v1/v2 专用
else:
for ver in range(3, 5):
snippet = config_paths + str(ver)
if lxc_ver >= ver and os.path.exists(snippet):
config_snippets.append(snippet)
# 拼接配置
command = ["sh", "-c", "cat {} > \"{}\"".format(
' '.join('"{0}"'.format(w) for w in config_snippets),
lxc_path + "/config")]
tools.helpers.run.user(args, command)
# 替换架构占位符
command = ["sed", "-i", "s/LXCARCH/{}/".format(platform.machine()),
lxc_path + "/config"]
# AppArmor 配置
if get_apparmor_status(args):
command = ["sed", "-i", "-E",
"/lxc.aa_profile|lxc.apparmor.profile/ s/unconfined/lxc-waydroid/g",
lxc_path + "/config"]
最终配置由以下片段组成:
config_base(始终包含):
lxc.rootfs.path = /var/lib/waydroid/rootfs
lxc.arch = x86_64 # 由 sed 替换
lxc.autodev = 0
lxc.cap.keep = audit_control sys_nice ...
lxc.mount.auto = cgroup:ro sys:ro proc
lxc.include = .../config_nodes
lxc.include = .../config_session
config_3(LXC 3+):
lxc.uts.name = waydroid
lxc.apparmor.profile = unconfined # 或 lxc-waydroid
lxc.seccomp.profile = .../waydroid.seccomp
lxc.no_new_privs = 1
lxc.net.0.type = veth
lxc.net.0.link = waydroid0
lxc.net.0.name = eth0
config_4(LXC 4+):
lxc.seccomp.allow_nesting = 1
generate_nodes_lxc_config():设备节点
这个函数(lxc.py:38)动态生成容器可见的设备列表。它是 Waydroid “设备直通”的核心:
def generate_nodes_lxc_config(args):
nodes = []
def make_entry(src, dist=None, mnt_type="none",
options="bind,create=file,optional 0 0", check=True):
return add_node_entry(nodes, src, dist, mnt_type, options, check)
# /dev 自身用 tmpfs
make_entry("tmpfs", "dev", "tmpfs", "nosuid 0 0", False)
# 基础设备
make_entry("/dev/zero")
make_entry("/dev/null")
make_entry("/dev/full")
make_entry("/dev/ashmem")
make_entry("/dev/fuse")
make_entry("/dev/ion")
make_entry("/dev/tty")
# GPU 设备(直接 passthrough)
make_entry("/dev/kgsl-3d0") # Qualcomm Adreno
make_entry("/dev/mali0") # ARM Mali
make_entry("/dev/pvr_sync") # PowerVR
make_entry("/dev/pmsg0") # Pstore
make_entry("/dev/dxg") # NVIDIA (WSL2)
render, _ = tools.helpers.gpu.getDriNode(args)
make_entry(render) # e.g., /dev/dri/renderD128
# 动态枚举
for n in glob.glob("/dev/fb*"): # 帧缓冲
make_entry(n)
for n in glob.glob("/dev/video*"): # V4L2 摄像头/编码器
make_entry(n)
for n in glob.glob("/dev/dma_heap/*"): # DMA-BUF 堆
make_entry(n)
# Binder: 宿主机名称 → 容器内标准名称
make_entry("/dev/" + args.BINDER_DRIVER, "dev/binder", check=False)
make_entry("/dev/" + args.VNDBINDER_DRIVER, "dev/vndbinder", check=False)
make_entry("/dev/" + args.HWBINDER_DRIVER, "dev/hwbinder", check=False)
# HALIUM 环境: 额外挂载宿主机的 hwbinder
if args.vendor_type != "MAINLINE":
if not make_entry("/dev/hwbinder", "dev/host_hwbinder"):
raise OSError('Binder node "hwbinder" of host not found')
make_entry("/vendor", "vendor_extra", options="rbind,optional 0 0")
# Mediatek 专有媒体设备
make_entry("/dev/Vcodec")
make_entry("/dev/MTK_SMI")
make_entry("/dev/mdp_sync")
make_entry("/dev/mtk_cmdq")
生成的配置条目格式如下:
lxc.mount.entry = /dev/dri/renderD128 dev/dri/renderD128 none bind,create=file,optional 0 0
lxc.mount.entry = /dev/anbox-binder dev/binder none bind,create=file,optional 0 0
lxc.mount.entry = tmpfs dev tmpfs nosuid 0 0
每个条目包含:源路径、容器内目标路径、文件系统类型、挂载选项。optional 标志确保不存在的设备不会导致容器启动失败。
generate_session_lxc_config():会话绑定
每次启动会话时,动态生成用户相关的挂载配置(lxc.py:183):
def generate_session_lxc_config(args, session):
nodes = []
def make_entry(src, dist=None, mnt_type="none",
options="rbind,create=file 0 0"):
# 安全检查: 防止路径注入
if any(x in src for x in ["\n", "\r"]):
logging.warning("User-provided mount path contains illegal character")
return False
# 安全检查: 确认路径属于当前用户
if dist is None and (not os.path.exists(src) or
str(os.stat(src).st_uid) != session["user_id"]):
logging.warning("User-provided mount path is not owned by user")
return False
return add_node_entry(nodes, src, dist, mnt_type, options, check=False)
# XDG 运行时目录
make_entry("tmpfs", defaults["container_xdg_runtime_dir"],
options="create=dir 0 0")
# Wayland socket: 宿主机 → 容器
wayland_host = os.path.realpath(os.path.join(
session["xdg_runtime_dir"], session["wayland_display"]))
wayland_container = os.path.realpath(os.path.join(
defaults["container_xdg_runtime_dir"],
defaults["container_wayland_display"]))
make_entry(wayland_host, wayland_container[1:])
# e.g., /run/user/1000/wayland-0 → run/xdg/wayland-0
# PulseAudio socket
pulse_host = os.path.join(session["pulse_runtime_path"], "native")
pulse_container = os.path.join(defaults["container_pulse_runtime_path"], "native")
make_entry(pulse_host, pulse_container[1:])
# 用户数据目录
make_entry(session["waydroid_data"], "data", options="rbind 0 0")
# ~/.local/share/waydroid/data → /data
注意这里的两个安全检查:
- 路径注入防护: 检查换行符,防止用户通过环境变量注入额外的 LXC 配置行
- 所有权验证: 确保挂载的路径确实属于当前用户,防止越权访问
五、GPU 检测与驱动映射
DRI 节点发现
tools/helpers/gpu.py 负责找到可用的 GPU 渲染节点:
unsupported = ["nvidia"] # NVIDIA 不支持(需要特殊处理)
def getDriNode(args):
# 优先使用配置指定的设备
cfg = tools.config.load(args)
node = cfg["waydroid"].get("drm_device")
if node:
renderDev = os.path.basename(node)
if getKernelDriver(args, renderDev) not in unsupported:
return node, getCardFromRender(args, renderDev)
return "", ""
# 自动发现: 遍历 /dev/dri/renderD*
for node in sorted(glob.glob("/dev/dri/renderD*")):
renderDev = os.path.basename(node)
if getKernelDriver(args, renderDev) not in unsupported:
return node, getCardFromRender(args, renderDev)
return "", ""
getKernelDriver() 通过 sysfs 获取驱动名称:
def getKernelDriver(args, dev):
return helpers.props.file_get(args,
"/sys/class/drm/{}/device/uevent".format(dev), "DRIVER")
# 读取 /sys/class/drm/renderD128/device/uevent 中的 DRIVER= 行
Vulkan 驱动映射
def getVulkanDriver(args, dev):
mapping = {
"i915": "intel", # Intel Gen9+
"xe": "intel", # Intel Xe
"amdgpu": "radeon", # AMD GPU
"radeon": "radeon", # AMD 旧驱动
"panfrost": "panfrost", # ARM Mali (Panfrost)
"msm": "freedreno", # Qualcomm Adreno
"msm_dpu": "freedreno", # Qualcomm Display
"vc4": "broadcom", # Raspberry Pi
"nouveau": "nouveau", # NVIDIA 开源驱动
}
kernel_driver = getKernelDriver(args, dev)
# Intel 旧 GPU 特殊处理
if kernel_driver == "i915":
gen = tools.helpers.run.user(args, ["awk",
"/^graphics version:|^gen:/ {print $NF}",
"/sys/kernel/debug/dri/{}/i915_capabilities".format(...)],
output_return=True, check=False)
if int(gen) < 9:
return "intel_hasvk" # Haswell/Broadwell 使用旧 Vulkan 驱动
return mapping.get(kernel_driver, "")
Intel GPU 的处理尤为精细——Gen9 以下(Haswell/Broadwell)使用 intel_hasvk(HArdware Specific VulKan)驱动,Gen9+(Skylake 及以后)使用标准 intel 驱动。检测方法是读取 debugfs 中的 i915_capabilities。
六、OverlayFS 挂载详解
mount_rootfs():分层挂载
tools/helpers/images.py:166 中的 mount_rootfs() 构建完整的文件系统层次:
def mount_rootfs(args, images_dir, session):
cfg = tools.config.load(args)
# 第一步: system.img → /var/lib/waydroid/rootfs (只读)
helpers.mount.mount(args, images_dir + "/system.img",
defaults["rootfs"], umount=True)
# 第二步: 叠加 OverlayFS
if cfg["waydroid"]["mount_overlays"] == "True":
try:
helpers.mount.mount_overlay(args,
[defaults["overlay"], defaults["rootfs"]], # lower 层
defaults["rootfs"], # 挂载点
upper_dir=defaults["overlay_rw"] + "/system",
work_dir=defaults["overlay_work"] + "/system")
except RuntimeError:
# OverlayFS 失败时优雅降级
cfg["waydroid"]["mount_overlays"] = "False"
tools.config.save(args, cfg)
logging.warning("Mounting overlays failed. The feature has been disabled.")
# 第三步: vendor.img → /var/lib/waydroid/rootfs/vendor
helpers.mount.mount(args, images_dir + "/vendor.img",
defaults["rootfs"] + "/vendor")
# vendor 也有 overlay
# 第四步: 宿主机 EGL 库绑定
for egl_path in ["/vendor/lib/egl", "/vendor/lib64/egl"]:
if os.path.isdir(egl_path):
helpers.mount.bind(args, egl_path, defaults["rootfs"] + egl_path)
# 第五步: ODM 绑定
if helpers.mount.ismount("/odm"):
helpers.mount.bind(args, "/odm", defaults["rootfs"] + "/odm_extra")
else:
if os.path.isdir("/vendor/odm"):
helpers.mount.bind(args, "/vendor/odm", defaults["rootfs"] + "/odm_extra")
# 第六步: 生成运行时属性文件并绑定挂载
make_prop(args, session, args.work + "/waydroid.prop")
helpers.mount.bind_file(args, args.work + "/waydroid.prop",
defaults["rootfs"] + "/vendor/waydroid.prop")
mount_overlay():OverlayFS 实现
tools/helpers/mount.py:154:
def mount_overlay(args, lower_dirs, destination, upper_dir=None,
work_dir=None, create_folders=True, readonly=True):
options = ["lowerdir=" + (":".join(lower_dirs))]
if upper_dir:
options.append("upperdir=" + upper_dir)
options.append("workdir=" + work_dir)
# 内核 4.17+ 需要 xino=off
if kernel_version() >= versiontuple("4.17"):
options.append("xino=off")
mount(args, "overlay", destination, mount_type="overlay",
options=options, readonly=readonly)
xino=off 选项在内核 4.17+ 中关闭 OverlayFS 的 xino(跨设备 inode 编号)特性。这是因为 system.img 使用的 ext4 文件系统与 upper 层的 ext4 可能有 inode 编号冲突,xino=off 避免了这个问题。
最终的 mount 命令大致如下:
mount -t overlay overlay \
-o lowerdir=/var/lib/waydroid/overlay:/var/lib/waydroid/rootfs,\
upperdir=/var/lib/waydroid/overlay_rw/system,\
workdir=/var/lib/waydroid/overlay_work/system,\
xino=off \
/var/lib/waydroid/rootfs
属性文件生成
make_prop() 将基础属性与会话属性合并:
def make_prop(args, cfg, full_props_path):
# 读取初始化时生成的基础属性
with open(args.work + "/waydroid_base.prop") as f:
props = f.read().splitlines()
# 添加会话属性
props.append("waydroid.host.user=" + cfg["user_name"])
props.append("waydroid.host.uid=" + cfg["user_id"])
props.append("waydroid.host_data_path=" + cfg["waydroid_data"])
props.append("waydroid.xdg_runtime_dir=/run/xdg")
props.append("waydroid.wayland_display=wayland-0")
props.append("waydroid.pulse_runtime_path=/run/xdg/pulse")
if which("waydroid-sensord") is None:
props.append("waydroid.stub_sensors_hal=1")
dpi = cfg["lcd_density"]
if dpi != "0":
props.append("ro.sf.lcd_density=" + dpi)
七、AIDL 协议版本
Android 的 Binder IPC 有多个 AIDL 协议版本,不同 Android API 级别使用不同版本。tools/helpers/protocol.py 处理这个映射:
def set_aidl_version(args):
android_api = int(helpers.props.file_get(args,
defaults["rootfs"] + "/system/build.prop",
"ro.build.version.sdk"))
if android_api < 28: # Android 8.1 及更早
binder_protocol = "aidl"
sm_protocol = "aidl"
elif android_api < 30: # Android 9-10
binder_protocol = "aidl2"
sm_protocol = "aidl2"
elif android_api < 31: # Android 11
binder_protocol = "aidl3"
sm_protocol = "aidl3"
elif android_api < 33: # Android 12-12L
binder_protocol = "aidl4"
sm_protocol = "aidl3" # ServiceManager 使用 aidl3
else: # Android 13+
binder_protocol = "aidl3"
sm_protocol = "aidl3"
cfg["waydroid"]["binder_protocol"] = binder_protocol
cfg["waydroid"]["service_manager_protocol"] = sm_protocol
注意 Android 12 的特殊处理:Binder 使用 aidl4,但 ServiceManager 仍使用 aidl3。Android 13+ 则统一回到 aidl3。这些版本号由 gbinder 库使用,确保 Python 工具能正确与容器内的 Android 服务通信。
八、容器生命周期管理
D-Bus 服务架构
container_manager.py 实现了一个 D-Bus 系统服务,暴露容器管理接口:
class DbusContainerManager(dbus.service.Object):
@dbus.service.method("id.waydro.ContainerManager",
in_signature='a{ss}', out_signature='')
def Start(self, session, sender, conn):
# 身份验证
uid = dbus_info.GetConnectionUnixUser(sender)
if str(uid) not in ["0", session["user_id"]]:
raise RuntimeError("Cannot start a session on behalf of another user")
pid = dbus_info.GetConnectionUnixProcessID(sender)
if str(uid) != "0" and str(pid) != session["pid"]:
raise RuntimeError("Invalid session pid")
do_start(self.args, session)
@dbus.service.method("id.waydro.ContainerManager",
in_signature='b', out_signature='')
def Stop(self, quit_session):
stop(self.args, quit_session)
@dbus.service.method("id.waydro.ContainerManager")
def Freeze(self): ...
@dbus.service.method("id.waydro.ContainerManager")
def Unfreeze(self): ...
@dbus.service.method("id.waydro.ContainerManager",
out_signature='a{ss}')
def GetSession(self):
session = self.args.session
session["state"] = helpers.lxc.status(self.args)
return session
Start 方法包含两重身份验证:
- 用户 ID 验证: 只有 root 或会话所有者可以启动
- 进程 ID 验证: 非 root 用户必须使用正确的 PID(防止其他进程冒充)
容器停止
def stop(args, quit_session=True):
services.hardware_manager.stop(args) # 停止硬件服务
if helpers.lxc.status(args) != "STOPPED":
helpers.lxc.stop(args) # lxc-stop -k
while helpers.lxc.status(args) != "STOPPED":
pass # 等待完全停止
# 停止网络
command = [tools.config.tools_src + "/data/scripts/waydroid-net.sh", "stop"]
# 停止传感器守护进程
if which("waydroid-sensord"):
pid = tools.helpers.run.user(args, ["pidof", "waydroid-sensord"], ...)
if pid:
command = ["kill", "-9", pid]
# 卸载 rootfs
helpers.images.umount_rootfs(args)
# 通知会话进程退出
if "session" in args and quit_session:
os.kill(int(args.session["pid"]), signal.SIGUSR1)
del args.session
停止顺序很重要:先停硬件服务 → 停 LXC → 停网络 → 停传感器 → 卸载 rootfs → 通知会话。反序操作可能导致资源泄露。
冻结与解冻
def freeze(args):
if helpers.lxc.status(args) == "RUNNING":
helpers.lxc.freeze(args) # lxc-freeze
while helpers.lxc.status(args) == "RUNNING":
pass
def unfreeze(args):
if helpers.lxc.status(args) == "FROZEN":
helpers.lxc.unfreeze(args) # lxc-unfreeze
冻结功能用于电源管理——当用户锁屏或系统挂起时,Waydroid 可以冻结容器以节省资源,而无需完全停止并重新启动。配置中的 suspend_action 控制此行为:
defaults = {
"suspend_action": "freeze", # 或 "stop"
}
LXC 底层操作
tools/helpers/lxc.py 中的 LXC 操作函数直接调用 LXC 命令行工具:
def start(args):
command = ["lxc-start", "-P", defaults["lxc"],
"-F", "-n", "waydroid", "--", "/init"]
tools.helpers.run.user(args, command, output="background")
wait_for_running(args) # 最多等 10 秒
def stop(args):
command = ["lxc-stop", "-P", defaults["lxc"],
"-n", "waydroid", "-k"] # -k = kill (立即停止)
def status(args):
command = ["lxc-info", "-P", defaults["lxc"],
"-n", "waydroid", "-sH"] # -s = state, -H = human readable
return tools.helpers.run.user(args, command, output_return=True).strip()
lxc-start 的关键参数:
-P /var/lib/waydroid/lxc: LXC 配置路径-F: 前台运行(但 Waydroid 将其放到后台)-n waydroid: 容器名称-- /init: 容器内执行的初始命令(Android 的 init 进程)
shell 访问
waydroid shell 通过 lxc-attach 实现:
def shell(args):
command = ["lxc-attach", "-P", defaults["lxc"],
"-n", "waydroid", "--clear-env"]
command.extend(android_env_attach_options(args)) # 设置 Android 环境变量
if args.COMMAND:
command.extend(args.COMMAND)
else:
command.append("/system/bin/sh")
subprocess.run(command)
android_env_attach_options() 设置了完整的 Android 运行环境:
ANDROID_ENV = {
"PATH": "/product/bin:/apex/com.android.runtime/bin:...",
"ANDROID_ROOT": "/system",
"ANDROID_DATA": "/data",
"BOOTCLASSPATH": "/apex/com.android.art/javalib/core-oj.jar:...",
}
九、设备权限管理
容器启动时,set_permissions() 为关键设备设置宽松权限:
def set_permissions(args, perm_list=None, mode="777"):
if not perm_list:
perm_list = [
"/dev/ashmem",
"/dev/sw_sync",
"/sys/kernel/debug/sync/sw_sync",
"/dev/Vcodec", # Mediatek 视频编解码
"/dev/MTK_SMI", # Mediatek SMI
"/dev/graphics",
"/dev/pvr_sync", # PowerVR
"/dev/ion", # ION 内存分配器
]
perm_list.extend(glob.glob("/dev/dri/renderD*")) # DRI 渲染节点
perm_list.extend(glob.glob("/dev/fb*")) # 帧缓冲
perm_list.extend(glob.glob("/dev/video*")) # 摄像头
perm_list.extend(glob.glob("/dev/dma_heap/*")) # DMA-BUF
for path in perm_list:
if os.path.exists(path):
command = ["chmod", mode, "-R", path]
tools.helpers.run.user(args, command, check=False)
Binder 设备使用更精确的 666 权限(而非默认的 777):
set_permissions(args, [
"/dev/" + args.BINDER_DRIVER,
"/dev/" + args.VNDBINDER_DRIVER,
"/dev/" + args.HWBINDER_DRIVER
], "666")
总结
Waydroid 的容器管理与初始化模块展现了几个值得关注的工程决策:
- 多重 Binder 命名策略: 通过优先级列表和 MAINLINE/HALIUM 分支处理,优雅地解决了多容器方案共存的问题
- Binderfs + 传统 modprobe 双路径: 兼容新旧内核
- OverlayFS 优雅降级: 挂载失败时自动禁用 overlay,保证系统可用
- 安全验证层: 会话配置中的路径注入防护和所有权检查
- SHA256 + OTA 通道双重校验: 确保镜像完整性和来源可信
下一篇预告
在下一篇文章中,我们将深入 图形栈——分析 HWComposer 如何将 Android SurfaceFlinger 的输出桥接到 Wayland,Gralloc 如何通过 GBM 分配 GPU buffer,以及 DMA-buf/SHM/android_wlegl 三种 buffer 传递策略的实现细节。
本文基于 Waydroid 项目源码分析,源码版本 v1.6.2。