runtime/process/process.go — 超深度逐行分析

文件行数:807行 | 核心职责:拦截create命令、修改OCI Spec、注入NPU设备与Hook
这是整个项目最核心、最复杂的文件


一、模块定位

1.1 业务职责

runtime/process/process.go 是 ascend-docker-runtime 的核心处理引擎。当 Docker 调用 ascend-docker-runtime 时,此文件负责:

  1. 解析命令行参数 — 提取 --bundle 路径和 create 命令
  2. 读取OCI Spec — 解析容器的 config.json
  3. 检测NPU需求 — 解析 ASCEND_VISIBLE_DEVICES 环境变量
  4. 选择NPU驱动 — V1(旧版910/310)或V2(新版910B/A5)
  5. 注入Hook — 向Spec添加Prestart和Poststop Hook
  6. 创建虚拟设备 — 如果是VIRTUAL模式,切分vNPU
  7. 挂载设备 — 向Spec添加davinci设备+管理设备+UB设备
  8. 写回Spec — 将修改后的config.json写回磁盘
  9. 执行runc — 透传给runc完成容器创建

1.2 在系统中的位置

调用链

runtime/main.go

DoProcess
入口函数

getArgs
解析参数

modifySpecFile
修改Spec

readSpecFile
读取config.json

processDevicesAndHooks
设备+Hook处理

writeSpecFile
写回config.json

execRunc
执行runc

checkVisibleDevice
检测NPU需求

GetMatchingNpuWorker
选择驱动

addHook
注入Hook

addDevice
挂载设备


二、常量与变量定义(第28-110行)

2.1 常量定义

const (
    runLogPath          = api.RunTimeRunLogPath
    hookDefaultFilePath = "/usr/local/bin/ascend-docker-hook"
    MaxCommandLength    = 65535
    hookCli             = "ascend-docker-hook"
    destroyHookCli      = "ascend-docker-destroy"
    dockerRuncFile      = "docker-runc"
    runcFile            = "runc"
    envLength           = 2
    kvPairSize          = 2
    borderNum           = 2

    useAscendDocker      = api.AscendDockerRuntimeEnv + "=True"
    ascendVisibleDevices = api.AscendVisibleDevicesEnv
    ascendRuntimeOptions = api.AscendRuntimeOptionsEnv

    void = "void"
)
常量 用途
runLogPath api定义的路径 日志文件路径
hookDefaultFilePath /usr/local/bin/ascend-docker-hook Hook默认路径(未使用,仅声明)
MaxCommandLength 65535 命令行参数最大长度,用于安全校验
hookCli "ascend-docker-hook" Hook程序文件名
destroyHookCli "ascend-docker-destroy" Destroy程序文件名
dockerRuncFile "docker-runc" Docker版runc文件名
runcFile "runc" 标准runc文件名
envLength 2 环境变量key=value对的长度
kvPairSize 2 SplitN的分段数
borderNum 2 设备范围"0-3"的边界数(左+右)
useAscendDocker "ASCEND_DOCKER_RUNTIME=True" 标识容器使用ascend-docker-runtime
ascendVisibleDevices "ASCEND_VISIBLE_DEVICES" NPU设备可见性环境变量名
ascendRuntimeOptions "ASCEND_RUNTIME_OPTIONS" 运行时选项环境变量名
void "void" 表示不需要挂载NPU

2.2 变量定义

var (
    hookCliPath     = hookCli
    hookDefaultFile = hookDefaultFilePath
    dockerRuncName  = dockerRuncFile
    runcName        = runcFile
    deviceRegx      = fmt.Sprintf(`^(?:%s(%s|%s|%s|%s)|%s)-(\d+)$`, api.Ascend, api.Ascend910No,
        api.Ascend310BNo, api.Ascend310PNo, api.Ascend310No, api.NPULowerCase)

    ascend910A5ManagerDevices = []string{hisiHdc}
    defaultManagerDevices     = []string{devmmSvm, hisiHdc}

    managerDevicesMap = map[string][]string{
        Ascend910A5: ascend910A5ManagerDevices,
    }
)

deviceRegx 正则表达式解析

正则 ^(?:Ascend(910|310B|310P|310)|npu)-(\d+)$ 用于匹配设备名格式:

匹配格式 示例 说明
Ascend910-N Ascend910-0 910芯片第N个设备
Ascend310B-N Ascend310B-1 310B芯片
Ascend310P-N Ascend310P-0 310P芯片
Ascend310-N Ascend310-2 310芯片
npu-N npu-0 通用NPU命名

设计意图:使用变量(非常量)是因为这些值可能在测试中被覆盖。

2.3 设备类型常量(第90-130行)

const (
    Atlas200ISoc = "Atlas 200I SoC A1"
    Atlas200     = "Atlas 200 Model 3000"
    Ascend310    = api.Ascend310
    Ascend310P   = api.Ascend310P
    Ascend310B   = api.Ascend310B
    Ascend910    = api.Ascend910
    Ascend910A5  = api.Ascend910A5
    ascend       = api.Ascend
    npu          = api.NPULowerCase

    devicePath           = "/dev/"
    davinciName          = "davinci"
    virtualDavinciName   = "vdavinci"
    davinciManager       = "davinci_manager"
    davinciManagerDocker = "davinci_manager_docker"
    devmmSvm             = "devmm_svm"
    hisiHdc              = "hisi_hdc"
    // ... 310B管理设备列表
    svm0, tsAisle, upgrade, sys, vdec, vpc, pngd, venc, logDrv, acodec, ai, ao, vo, hdmi
    uburma, ummu
)

设备文件说明

设备文件 用途
/dev/davinciN NPU计算设备,N是设备ID
/dev/vdavinciN 虚拟NPU设备(vNPU切分后)
/dev/davinci_manager NPU管理设备
/dev/davinci_manager_docker Docker专用管理设备(容器内重映射)
/dev/devmm_svm 设备内存管理(SVM=Shared Virtual Memory)
/dev/hisi_hdc 华为HDC(Host Device Channel)通信通道
/dev/uburma UB(Ultra-Bandwidth)内存访问
/dev/ummu UB内存管理单元

三、核心类型定义

3.1 args结构体

type args struct {
    bundleDirPath string
    cmd           string
}
  • bundleDirPath:OCI bundle目录路径,包含config.json
  • cmd:命令类型,只有"create"需要处理,其他直接透传给runc

args

+string bundleDirPath

+string cmd


四、核心函数逐行解析

4.1 GetDeviceTypeByChipName(第113-127行)

func GetDeviceTypeByChipName(chipName string) string {
    if strings.Contains(chipName, api.Ascend310BNo) {
        return Ascend310B
    }
    if strings.Contains(chipName, api.Ascend310PNo) {
        return Ascend310P
    }
    if strings.HasPrefix(chipName, api.Ascend910A5Prefix) {
        return Ascend910A5
    }
    if strings.Contains(chipName, api.Ascend310No) {
        return Ascend310
    }
    if strings.Contains(chipName, api.Ascend910No) {
        return Ascend910
    }
    return ""
}

逐行解析

  1. 如果芯片名包含"310B"字符串 → 返回 Ascend310B
  2. 如果芯片名包含"310P" → 返回 Ascend310P
  3. 注意:A5用HasPrefix而非Contains,因为A5芯片名有特定前缀模式 Ascend950XX
  4. 如果包含"310" → 返回 Ascend310
  5. 如果包含"910" → 返回 Ascend910
  6. 都不匹配 → 返回空字符串

匹配优先级设计意图:310B和310P必须在310之前检查,因为"310B"也包含"310"子串。这是字符串匹配的经典陷阱。

chipName输入

包含310B?

返回 Ascend310B

包含310P?

返回 Ascend310P

前缀是Ascend950?

返回 Ascend910A5

包含310?

返回 Ascend310

包含910?

返回 Ascend910

返回空字符串

4.2 getArgs(第129-144行)

func getArgs() (*args, error) {
    args := &args{}

    for i, param := range os.Args {
        if param == "--bundle" || param == "-b" {
            if len(os.Args)-i <= 1 {
                return nil, fmt.Errorf("bundle option needs an argument")
            }
            args.bundleDirPath = os.Args[i+1]
        } else if param == "create" {
            args.cmd = param
        }
    }

    return args, nil
}

逐行解析

  1. 创建空 args 结构体
  2. 遍历 os.Args(命令行参数数组)
  3. 遇到 --bundle-b:检查下一个参数是否存在,存在则设为 bundleDirPath
  4. 遇到 create:设为 cmd
  5. 返回解析结果

设计意图:OCI Runtime调用格式为 ascend-docker-runtime create --bundle /path,需要从中提取bundle路径和命令类型。

4.3 InitLogModule(第147-160行)

func InitLogModule(ctx context.Context) error {
    const backups = 2
    const logMaxAge = 365
    const fileMaxSize = 2
    runLogConfig := hwlog.LogConfig{
        LogFileName: runLogPath,
        LogLevel:    0,       // DEBUG级别
        MaxBackups:  backups, // 最多2个备份
        MaxAge:      logMaxAge, // 保留365天
        OnlyToFile:  true,    // 仅写文件不输出stdout
        FileMaxSize: fileMaxSize, // 单文件2MB
    }
    if err := hwlog.InitRunLogger(&runLogConfig, ctx); err != nil {
        fmt.Printf("hwlog init failed, error is %v", err)
        return err
    }
    return nil
}

日志配置详解

配置项 含义
LogFileName api.RunTimeRunLogPath 日志文件路径
LogLevel 0 DEBUG级别(最详细)
MaxBackups 2 最多保留2个轮转备份
MaxAge 365 日志最大保留天数
OnlyToFile true 仅写文件,不输出到stdout
FileMaxSize 2 单个日志文件最大2MB

设计意图OnlyToFile=true 防止日志干扰stdout,因为OCI Runtime的stdout会被runc捕获。

4.4 execRunc(第162-181行)

var execRunc = func() error {
    tempRuncPath, err := exec.LookPath(dockerRuncName)
    if err != nil {
        tempRuncPath, err = exec.LookPath(runcName)
        if err != nil {
            return fmt.Errorf("failed to find the path of runc: %v", err)
        }
    }
    runcPath, err := filepath.EvalSymlinks(tempRuncPath)
    if err != nil {
        return fmt.Errorf("failed to find realpath of runc %v", err)
    }
    if _, err := mindxcheckutils.RealFileChecker(runcPath, true, false, mindxcheckutils.DefaultSize); err != nil {
        return err
    }

    if err := mindxcheckutils.ChangeRuntimeLogMode("runtime-run-"); err != nil {
        return err
    }
    if err = syscall.Exec(runcPath, append([]string{runcPath}, os.Args[1:]...), os.Environ()); err != nil {
        return fmt.Errorf("failed to exec runc: %v", err)
    }

    return nil
}

逐行解析

  1. exec.LookPath("docker-runc") — 在PATH中查找docker版runc
  2. 如果找不到,降级查找 runc
  3. filepath.EvalSymlinks — 解析符号链接获取真实路径
  4. RealFileChecker — 安全校验runc文件(防篡改)
  5. ChangeRuntimeLogMode — 收紧日志文件权限
  6. syscall.Exec替换当前进程为runc(不是fork/exec,是execve)

关键设计syscall.Exec 会将当前进程的内存镜像替换为runc,PID不变。这意味着ascend-docker-runtime进程"变成"了runc进程。传递的参数是 os.Args[1:](去掉第一个参数,即程序名本身),环境变量原样传递。

找到

未找到

找到

未找到

不通过

通过

成功

失败

execRunc启动

LookPath docker-runc

EvalSymlinks解析符号链接

LookPath runc

返回错误

RealFileChecker安全校验

返回错误

ChangeRuntimeLogMode收紧日志权限

syscall.Exec替换进程为runc

进程变为runc,不会返回

返回错误

4.5 addAscendDockerEnv(第183-188行)

func addAscendDockerEnv(spec *specs.Spec) {
    if spec == nil || spec.Process == nil || spec.Process.Env == nil {
        return
    }
    spec.Process.Env = append(spec.Process.Env, useAscendDocker)
}
  • 向容器的环境变量列表追加 ASCEND_DOCKER_RUNTIME=True
  • 三重nil检查防止空指针panic
  • 设计意图:让容器内的应用能检测到自己运行在ascend-docker-runtime环境中

4.6 addHook(第190-228行)

func addHook(w dcmi.WorkerInterface, spec *specs.Spec, deviceIdList *[]int) error {

这是最复杂的函数之一,负责注入Prestart Hook和创建虚拟设备

    if deviceIdList == nil {
        return nil
    }
    currentExecPath, err := os.Executable()
    if err != nil {
        return fmt.Errorf("cannot get the path of docker-runtime: %v", err)
    }
  • nil检查保护
  • os.Executable() 获取当前二进制文件路径(如 /usr/local/bin/ascend-docker-runtime
    hookCliPath = path.Join(path.Dir(currentExecPath), hookCli)
    if _, err := mindxcheckutils.RealFileChecker(hookCliPath, true, false, mindxcheckutils.DefaultSize); err != nil {
        return err
    }
    if _, err = os.Stat(hookCliPath); err != nil {
        return fmt.Errorf("cannot find docker-hook executable file at %s: %v", hookCliPath, err)
    }
  • 构造hook路径:与runtime同目录下的 ascend-docker-hook
  • 双重校验:RealFileChecker(安全校验)+ os.Stat(存在性检查)
    if spec.Hooks == nil {
        spec.Hooks = &specs.Hooks{}
    }
    needUpdate := true
    if len(spec.Hooks.Prestart) > MaxCommandLength {
        return fmt.Errorf("too many items in Prestart ")
    }
    for _, hook := range spec.Hooks.Prestart {
        if strings.Contains(hook.Path, hookCli) {
            needUpdate = false
            break
        }
    }
    if needUpdate {
        spec.Hooks.Prestart = append(spec.Hooks.Prestart, specs.Hook{
            Path: hookCliPath,
            Args: []string{hookCliPath},
        })
    }
  • 初始化Hooks结构(如果不存在)
  • 安全限制:Prestart数量不超过65535
  • 幂等性检查:遍历已有Prestart Hook,如果已包含ascend-docker-hook则不重复添加
  • 添加Hook:Path为hook二进制路径,Args仅包含路径本身
    if len(spec.Process.Env) > MaxCommandLength {
        return fmt.Errorf("too many items in Env ")
    }

    if strings.Contains(getValueByKey(spec.Process.Env, ascendRuntimeOptions), "VIRTUAL") {
        return nil
    }

    vdevice, err := dcmi.CreateVDevice(w, spec, *deviceIdList)
    if err != nil {
        return err
    }
    hwlog.RunLog.Infof("vnpu split done: vdevice: %v", vdevice.VdeviceID)

    if vdevice.VdeviceID != -1 {
        if err = updateEnvAndPostHook(spec, vdevice, deviceIdList); err != nil {
            return fmt.Errorf("update evn and post hook failed: %v ", err)
        }
    }

    return nil
}
  • 如果环境变量包含"VIRTUAL" → 已是虚拟模式,跳过vNPU创建
  • 调用 dcmi.CreateVDevice 创建虚拟设备
  • 如果创建成功(VdeviceID != -1),更新环境变量并注入Poststop Hook

成功

失败

addHook入口

nil检查

获取当前二进制路径

构造hook路径
同目录下的ascend-docker-hook

安全校验hook文件

初始化Spec.Hooks

Prestart已包含
ascend-docker-hook?

跳过添加

追加Prestart Hook

环境变量含VIRTUAL?

跳过vNPU创建
直接返回

CreateVDevice
创建虚拟设备

updateEnvAndPostHook
更新Env+注入Poststop

返回错误

返回nil

4.7 parseDevices / getDeviceListFromVisibleValue(第248-291行)

func parseDevices(visibleDevices string) ([]int, error) {
    devices := make([]int, 0)

    for _, value := range strings.Split(visibleDevices, ",") {
        deviceFromValue, err := getDeviceListFromVisibleValue(value)
        if err != nil {
            return nil, err
        }
        devices = append(devices, deviceFromValue...)
    }

    sort.Slice(devices, func(i, j int) bool { return i < j })
    return removeDuplication(devices), nil
}
  • 按逗号分割设备列表(如 “0,1,2” → [“0”,“1”,“2”])
  • 对每个值调用 getDeviceListFromVisibleValue 解析(支持范围"0-3")
  • 排序后去重
func getDeviceListFromVisibleValue(visibleValue string) ([]int, error) {
    maxDevice := 128
    devices := make([]int, 0)
    visibleValue = strings.TrimSpace(visibleValue)
    if strings.Contains(visibleValue, "-") {
        // 范围解析: "0-3" → [0,1,2,3]
        borders := strings.Split(visibleValue, "-")
        if len(borders) != borderNum {
            return nil, fmt.Errorf("invalid device range: %s", visibleValue)
        }
        left, err := strconv.Atoi(borders[0])
        if err != nil || left < 0 {
            return nil, fmt.Errorf("invalid left boarder range parameter: %s", borders[0])
        }
        right, err := strconv.Atoi(borders[1])
        if err != nil || right > maxDevice {
            return nil, fmt.Errorf("invalid right boarder range parameter: %s", borders[1])
        }
        if left > right {
            return nil, fmt.Errorf("left boarder (%d) should not be larger than the right one(%d)", left, right)
        }
        for n := left; n <= right; n++ {
            devices = append(devices, n)
        }
        return devices, nil
    }
    // 单个设备: "5" → [5]
    n, err := strconv.Atoi(visibleValue)
    if err != nil {
        return nil, fmt.Errorf("invalid single device parameter: %s", visibleValue)
    }
    devices = append(devices, n)
    return devices, nil
}

支持的格式

输入格式 输出 说明
"0" [0] 单个设备
"0,1,2" [0,1,2] 多个设备
"0-3" [0,1,2,3] 范围设备
"0-3,7,9-11" [0,1,2,3,7,9,10,11] 混合格式

边界校验:左边界≥0,右边界≤128,左≤右。

4.8 parseAscendDevices(第293-311行)

func parseAscendDevices(visibleDevices string) ([]int, error) {
    devicesList := strings.Split(visibleDevices, ",")
    devices := make([]int, 0, len(devicesList))

    for _, d := range devicesList {
        matchGroups := regexp.MustCompile(deviceRegx).FindStringSubmatch(strings.TrimSpace(d))
        if matchGroups == nil {
            return nil, fmt.Errorf("invalid device format: %s", d)
        }
        n, err := strconv.Atoi(matchGroups[2])
        if err != nil {
            return nil, fmt.Errorf("invalid device id: %s", d)
        }
        devices = append(devices, n)
    }

    sort.Slice(devices, func(i, j int) bool { return i < j })
    return removeDuplication(devices), nil
}
  • 使用正则 ^(?:Ascend(910|310B|310P|310)|npu)-(\d+)$ 匹配
  • matchGroups[2] 是第二个捕获组(设备ID数字)
  • 排序+去重

与parseDevices的区别parseDevices 处理纯数字格式(“0,1,2”),parseAscendDevices 处理命名格式(“Ascend910-0,npu-1”)。

4.9 getValueByKey / getValueByDeviceKey(第313-345行)

func getValueByKey(data []string, name string) string {
    for _, envLine := range data {
        words := strings.SplitN(envLine, "=", kvPairSize)
        if len(words) != kvPairSize {
            hwlog.RunLog.Error("environment error")
            return ""
        }
        if words[0] == name {
            return words[1]
        }
    }
    return ""
}
  • ["KEY1=val1", "KEY2=val2"] 格式的数组中查找指定key的值
  • SplitN限制分割数为2,防止值中包含"="时出错
func getValueByDeviceKey(data []string) string {
    res := ""
    for i := len(data) - 1; i >= 0; i-- {
        words := strings.SplitN(data[i], "=", kvPairSize)
        if len(words) != kvPairSize {
            hwlog.RunLog.Error("environment error")
            return ""
        }
        if words[0] == ascendVisibleDevices {
            res = words[1]
            break
        }
    }
    // ...
    return res
}

关键差异getValueByDeviceKey 从后往前遍历。设计意图:当环境变量中存在多个 ASCEND_VISIBLE_DEVICES 时,取最后一个生效的值(后写覆盖前写,模拟Docker环境变量的覆盖语义)。

4.10 addDeviceToSpec(第360-375行)

func addDeviceToSpec(spec *specs.Spec, dHostPath string, dContainerPath string) error {
    device, err := oci.DeviceFromPath(dHostPath)
    if err != nil {
        return fmt.Errorf("failed to get %s info : %v", dHostPath, err)
    }

    device.Path = dContainerPath

    spec.Linux.Devices = append(spec.Linux.Devices, *device)
    newDeviceCgroup := specs.LinuxDeviceCgroup{
        Allow:  true,
        Type:   device.Type,
        Major:  &device.Major,
        Minor:  &device.Minor,
        Access: "rwm",
    }
    spec.Linux.Resources.Devices = append(spec.Linux.Resources.Devices, newDeviceCgroup)
    return nil
}

逐行解析

  1. oci.DeviceFromPath(dHostPath) — 获取Host路径对应的设备信息(类型/主设备号/次设备号)
  2. device.Path = dContainerPath — 设置容器内路径(可能与Host路径不同,如vdavinci重映射)
  3. 追加到 Spec.Linux.Devices — runc会根据此列表在容器namespace中创建设备节点
  4. 创建cgroup规则 — 允许容器访问该设备的 主:次 设备号,权限"rwm"(读写+mknod)

设计意图:OCI Spec的设备注入需要两步:1)声明设备(Linux.Devices),2)允许访问(Linux.Resources.Devices cgroup规则)。缺少任何一步都无法正常访问设备。

dHostPath
/dev/davinci0

oci.DeviceFromPath
获取设备类型+主次号

设置containerPath
/dev/davinci0或vdavinci0

追加到Spec.Linux.Devices
声明设备节点

追加到Spec.Linux.Resources.Devices
cgroup允许访问

runc读取Spec

容器namespace中创建设备
+cgroup授权

4.11 addManagerDevice(第408-455行)

func addManagerDevice(w dcmi.WorkerInterface, spec *specs.Spec) error {
    chipName, err := w.GetChipName()
    // ...
    devType := GetDeviceTypeByChipName(chipName)
    // ...
    if devType != "" && devType != Ascend910A5 {
        dPath := devicePath + dvppCmdList
        if err := addDeviceToSpec(spec, dPath, dPath); err != nil {
            hwlog.RunLog.Warnf("failed to add dvpp_cmdlist to spec : %v", err)
        }
    }
    if devType == Ascend310B {
        return addAscend310BManagerDevice(spec)
    }
    // 添加 davinci_manager
    dPath := devicePath + davinciManager
    if err := addDeviceToSpec(spec, dPath, dPath); err != nil {
        return fmt.Errorf("add davinci_manager to spec error: %v", err)
    }

    productType, err := w.GetProductType()
    // ...
    switch productType {
    case Atlas200ISoc, Atlas200:
        // 这两种产品不需要额外管理设备
    default:
        if err = addCommonManagerDevice(spec, devType); err != nil {
            return fmt.Errorf("add common manage device error: %v", err)
        }
    }
    return nil
}

逐行解析

  1. 获取芯片名称 → 确定设备类型
  2. 如果非空且非A5 → 添加 dvpp_cmdlist(数字视觉预处理命令列表设备)
  3. 如果是310B → 调用专门的310B管理设备添加函数(14个设备)
  4. 添加 davinci_manager(所有芯片都需要)
  5. 获取产品类型
  6. Atlas 200I SoC / Atlas 200 → 不添加额外管理设备
  7. 其他产品 → 添加通用管理设备(devmm_svm + hisi_hdc,A5仅hisi_hdc)

addManagerDevice

GetChipName
获取芯片名

GetDeviceTypeByChipName
确定设备类型

类型为空或A5?

添加 dvpp_cmdlist

类型是310B?

addAscend310BManagerDevice
添加14个管理设备

添加 davinci_manager

GetProductType
获取产品类型

Atlas 200I SoC
或 Atlas 200?

不添加额外设备

addCommonManagerDevice
添加 devmm_svm + hisi_hdc

4.12 checkVisibleDevice(第500-522行)

func checkVisibleDevice(spec *specs.Spec) ([]int, error) {
    if spec.Process == nil {
        return nil, errors.New("empty process info")
    }
    visibleDevices := getValueByDeviceKey(spec.Process.Env)
    if visibleDevices == "" || visibleDevices == void {
        return nil, nil
    }

    if strings.Contains(visibleDevices, ascend) || strings.Contains(visibleDevices, npu) {
        devices, err := parseAscendDevices(visibleDevices)
        // ...
        return devices, err
    }
    devices, err := parseDevices(visibleDevices)
    // ...
    return devices, err
}

逐行解析

  1. nil检查 spec.Process
  2. 从环境变量从后往前查找 ASCEND_VISIBLE_DEVICES
  3. 空或"void" → 返回空列表(不挂载NPU)
  4. 如果包含"Ascend"或"npu" → 使用命名格式解析(parseAscendDevices
  5. 否则 → 使用纯数字格式解析(parseDevices

4.13 addDevice(第524-545行)

func addDevice(w dcmi.WorkerInterface, spec *specs.Spec, deviceIdList []int) error {
    deviceName := davinciName
    if strings.Contains(getValueByKey(spec.Process.Env, ascendRuntimeOptions), "VIRTUAL") {
        deviceName = virtualDavinciName
    }
    for _, deviceId := range deviceIdList {
        dPath := devicePath + deviceName + strconv.Itoa(deviceId)
        dContainerPath, err := getMountPath(dPath, deviceName)
        // ...
        if err := addDeviceToSpec(spec, dPath, dContainerPath); err != nil {
            return fmt.Errorf("failed to add davinci device to spec: %v", err)
        }
    }

    if err := addUBDevice(spec); err != nil {
        // ...
    }

    if err := addManagerDevice(w, spec); err != nil {
        // ...
    }

    return nil
}

逐行解析

  1. 默认设备名为"davinci",如果VIRTUAL模式则改为"vdavinci"
  2. 遍历设备ID列表,对每个设备:
    • 构造Host路径:/dev/davinci0/dev/vdavinci0
    • getMountPath 获取容器内路径(vdavinci会重映射为davinci+N)
    • 添加到Spec
  3. 添加UB设备(uburma/ummu目录下的设备)
  4. 添加管理设备

4.14 updateEnvAndPostHook(第547-581行)

func updateEnvAndPostHook(spec *specs.Spec, vdevice dcmi.VDeviceInfo, deviceIdList *[]int) error {

此函数在vNPU创建成功后调用,负责更新环境变量和注入Poststop Hook

    newEnv := make([]string, 0, len(spec.Process.Env)+1)
    needAddVirtualFlag := true
    *deviceIdList = []int{int(vdevice.VdeviceID)}
  • 关键操作:将设备ID列表替换为虚拟设备ID(vdevice.VdeviceID
  • 后续 addDevice 会使用这个新的ID来挂载 /dev/vdavinci{VdeviceID}
    for _, line := range spec.Process.Env {
        words := strings.Split(line, "=")
        if len(words) == envLength && strings.TrimSpace(words[0]) == ascendRuntimeOptions {
            needAddVirtualFlag = false
            if strings.Contains(words[1], "VIRTUAL") {
                newEnv = append(newEnv, line)
                continue
            } else {
                newEnv = append(newEnv, strings.TrimSpace(line)+",VIRTUAL")
                continue
            }
        }
        newEnv = append(newEnv, line)
    }
    if needAddVirtualFlag {
        newEnv = append(newEnv, fmt.Sprintf(api.AscendRuntimeOptionsEnv+"=VIRTUAL"))
    }
    spec.Process.Env = newEnv
  • 遍历环境变量,找到 ASCEND_RUNTIME_OPTIONS
  • 如果已有VIRTUAL → 保持不变
  • 如果没有VIRTUAL → 追加",VIRTUAL"
  • 如果完全不存在 → 添加 ASCEND_RUNTIME_OPTIONS=VIRTUAL
  • 设计意图:确保Hook进程知道这是虚拟设备模式
    currentExecPath, err := os.Executable()
    // ...
    postHookCliPath := path.Join(path.Dir(currentExecPath), destroyHookCli)
    // ...安全校验...
    
    spec.Hooks.Poststop = append(spec.Hooks.Poststop, specs.Hook{
        Path: postHookCliPath,
        Args: []string{postHookCliPath, fmt.Sprintf("%d", vdevice.CardID), fmt.Sprintf("%d", vdevice.DeviceID),
            fmt.Sprintf("%d", vdevice.VdeviceID)},
    })
    return nil
}
  • 构造destroy程序路径(与runtime同目录下的 ascend-docker-destroy
  • 安全校验
  • 注入Poststop Hook,参数为 cardID、deviceID、vdeviceID
  • 设计意图:容器停止时自动销毁虚拟设备,防止资源泄漏

4.15 modifySpecFile / readSpecFile / writeSpecFile(第583-640行)

func modifySpecFile(path string) error {
    if err := mindxcheckutils.CheckPath(path, true); err != nil {
        return err
    }
    spec, err := readSpecFile(path)
    if err != nil {
        return err
    }
    if err := processDevicesAndHooks(spec); err != nil {
        return err
    }
    return writeSpecFile(path, spec)
}

三步操作:安全校验路径 → 读取并修改 → 写回。

func readSpecFile(path string) (*specs.Spec, error) {
    stat, err := os.Stat(path)
    // ...
    jsonFile, err := os.OpenFile(path, os.O_RDWR, stat.Mode())
    // ...
    defer jsonFile.Close()
    if err = mindxcheckutils.CheckFileInfo(jsonFile, mindxcheckutils.DefaultSize); err != nil {
        return nil, err
    }
    jsonContent, err := ioutil.ReadAll(jsonFile)
    // ...
    var spec specs.Spec
    if err = json.Unmarshal(jsonContent, &spec); err != nil {
        return nil, err
    }
    return &spec, nil
}
  • CheckPath — 路径安全校验(防../穿越、防软链接)
  • CheckFileInfo — 文件安全校验(大小限制、权限检查、UID检查、防setuid/setgid)
  • json.Unmarshal — JSON反序列化为 specs.Spec 结构体
func writeSpecFile(path string, spec *specs.Spec) error {
    jsonFile, err := os.OpenFile(path, os.O_RDWR, 0)
    // ...
    jsonOutput, err := json.Marshal(spec)
    // ...
    if err = jsonFile.Truncate(0); err != nil {
        return err
    }
    if _, err = jsonFile.WriteAt(jsonOutput, 0); err != nil {
        return err
    }
    return nil
}
  • Truncate(0) — 清空文件内容(因为修改后的JSON可能比原来短)
  • WriteAt(jsonOutput, 0) — 从文件开头写入
  • 设计意图:Truncate+WriteAt确保文件不会残留旧数据

4.16 processDevicesAndHooks(第642-658行)

func processDevicesAndHooks(spec *specs.Spec) error {
    devices, err := checkVisibleDevice(spec)
    if err != nil {
        return fmt.Errorf("failed to check %v parameter, err: %v", api.AscendVisibleDevicesEnv, err)
    }

    if len(devices) != 0 {
        npuWorker, err := dcmi.GetMatchingNpuWorker()
        if err != nil {
            return err
        }
        if err = addHook(npuWorker, spec, &devices); err != nil {
            return fmt.Errorf("failed to inject hook, err: %v", err)
        }
        if err = addDevice(npuWorker, spec, devices); err != nil {
            return fmt.Errorf("failed to add device to env: %v", err)
        }
    }
    addAscendDockerEnv(spec)
    return nil
}

核心编排函数,执行顺序:

  1. checkVisibleDevice — 检测是否需要NPU
  2. 如果需要 → GetMatchingNpuWorker 选择驱动
  3. addHook — 注入Hook + 创建vNPU
  4. addDevice — 挂载设备
  5. addAscendDockerEnv — 添加标识环境变量(无论是否需要NPU都添加)

4.17 DoProcess(第795-807行)

func DoProcess() error {
    args, err := getArgs()
    if err != nil {
        return fmt.Errorf("failed to get args: %v", err)
    }

    if args.cmd != "create" {
        return execRunc()
    }

    if args.bundleDirPath == "" {
        hwlog.RunLog.Warn("get bundleDirPath is empty,try get current working dir from pwd ")
        args.bundleDirPath, err = os.Getwd()
        if err != nil {
            return fmt.Errorf("failed to get current working dir: %v", err)
        }
    }

    specFilePath := args.bundleDirPath + "/config.json"

    if err = modifySpecFile(specFilePath); err != nil {
        return fmt.Errorf("failed to modify spec file %s: %v", specFilePath, err)
    }

    return execRunc()
}

逐行解析

  1. getArgs() — 解析命令行参数
  2. 如果不是"create"命令 → 直接execRunc透传(如delete/kill/start等命令)
  3. 如果bundle路径为空 → 尝试用当前工作目录
  4. 构造config.json路径
  5. modifySpecFile — 读取→修改→写回
  6. execRunc — 执行runc

失败

成功

成功

失败

DoProcess

getArgs解析参数

cmd == create?

execRunc直接透传

bundleDirPath为空?

os.Getwd获取当前目录

构造config.json路径

modifySpecFile
安全校验+读取+修改+写回

返回错误

execRunc
syscall.Exec替换为runc

进程变为runc
不会返回


五、完整调用关系图

DoProcess

getArgs

execRunc

modifySpecFile

CheckPath
安全校验

readSpecFile

processDevicesAndHooks

writeSpecFile

CheckFileInfo
文件安全校验

json.Unmarshal

checkVisibleDevice

GetMatchingNpuWorker

addHook

addDevice

addAscendDockerEnv

getValueByDeviceKey
从后往前遍历

parseDevices
纯数字格式

parseAscendDevices
命名格式

getDeviceListFromVisibleValue

removeDuplication

deviceRegx正则匹配

os.Executable

RealFileChecker

dcmi.CreateVDevice

updateEnvAndPostHook

getValueByKey
检查VIRTUAL

addDeviceToSpec
循环每个设备

addUBDevice

addManagerDevice

oci.DeviceFromPath

Spec.Linux.Devices

Spec.Linux.Resources.Devices

os.Stat uburma

os.Stat ummu

addDevicesInDir

GetChipName

GetDeviceTypeByChipName

addAscend310BManagerDevice

addCommonManagerDevice

GetProductType

LookPath docker-runc

LookPath runc

EvalSymlinks

RealFileChecker

ChangeRuntimeLogMode

syscall.Exec

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐