Skip to content
Go back

Waydroid 源码分析(二):容器管理与系统初始化

Waydroid 源码分析(二):容器管理与系统初始化

本文深入分析 Waydroid 的容器管理与初始化流程,涵盖 Binder 驱动分配、LXC 配置生成、OverlayFS 挂载策略、OTA 镜像管理,以及硬件属性自动检测的完整实现。

概述

Waydroid 的容器管理层是连接宿主机与 Android 系统的桥梁。它负责:

  1. 检测硬件环境并分配必要的内核驱动
  2. 下载和校验 Android 系统镜像
  3. 生成 LXC 配置文件
  4. 构建 OverlayFS 分层文件系统
  5. 启动和管理 LXC 容器的完整生命周期

核心代码分布在以下文件中:

文件职责
tools/actions/initializer.py系统初始化(waydroid init
tools/actions/container_manager.py容器生命周期管理
tools/helpers/drivers.pyBinder/Ashmem 驱动管理
tools/helpers/lxc.pyLXC 配置生成与容器操作
tools/helpers/images.py镜像下载、校验与挂载
tools/helpers/mount.py挂载操作封装
tools/helpers/gpu.pyGPU 检测与驱动映射
tools/helpers/protocol.pyAIDL 协议版本设置
tools/config/__init__.py配置管理与默认值

一、Binder 驱动分配

问题背景

Android 依赖三个 Binder 设备进行 IPC:bindervndbinderhwbinder。但在标准 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 编号规范:

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 版本的映射逻辑:


三、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类型    架构              系统类型

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()

使用 memoryviewreadinto 避免在大文件校验时的内存分配开销——这是 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

注意这里的两个安全检查:

  1. 路径注入防护: 检查换行符,防止用户通过环境变量注入额外的 LXC 配置行
  2. 所有权验证: 确保挂载的路径确实属于当前用户,防止越权访问

五、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 方法包含两重身份验证:

  1. 用户 ID 验证: 只有 root 或会话所有者可以启动
  2. 进程 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 的关键参数:

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 的容器管理与初始化模块展现了几个值得关注的工程决策:

  1. 多重 Binder 命名策略: 通过优先级列表和 MAINLINE/HALIUM 分支处理,优雅地解决了多容器方案共存的问题
  2. Binderfs + 传统 modprobe 双路径: 兼容新旧内核
  3. OverlayFS 优雅降级: 挂载失败时自动禁用 overlay,保证系统可用
  4. 安全验证层: 会话配置中的路径注入防护和所有权检查
  5. SHA256 + OTA 通道双重校验: 确保镜像完整性和来源可信

下一篇预告

在下一篇文章中,我们将深入 图形栈——分析 HWComposer 如何将 Android SurfaceFlinger 的输出桥接到 Wayland,Gralloc 如何通过 GBM 分配 GPU buffer,以及 DMA-buf/SHM/android_wlegl 三种 buffer 传递策略的实现细节。


本文基于 Waydroid 项目源码分析,源码版本 v1.6.2。


Share this post on:

Previous Post
Waydroid 源码分析(一):整体架构与设计哲学
Next Post
Waydroid 源码分析(三):图形栈 — HWComposer 与 Gralloc