【NPU】Ascend Docker Runtime v26.0.1 之四 hook/ — 超深度逐行分析
hook/ — 超深度逐行分析
两个文件:hook/main.go (67行) + hook/process/process.go (413行) = 480行
核心职责:OCI Prestart Hook实现,在容器namespace已创建但作业未启动时执行设备挂载
一、模块定位
1.1 业务职责
hook/ 模块是 OCI Prestart Hook 的实现。当 runc 在容器创建过程中触发 Prestart Hook 时,此模块被调用。此时容器的 namespace 已创建,但容器内的作业尚未启动,是一个安全的时间窗口,可以:
- 解析容器配置 — 从stdin读取OCI State,获取容器PID和rootfs路径
- 读取挂载配置 — 从
/etc/ascend-docker-runtime.d/读取.list配置文件 - 添加UB挂载 — 自动添加HCCL根信息和拓扑目录
- 解析运行时选项 — NODRV(无驱动模式)、VIRTUAL(虚拟设备模式)
- 解析软链接模式 — 是否允许通过软链接挂载
- 执行CLI —
syscall.Exec替换为ascend-docker-cli(C语言程序)执行实际挂载
1.2 在系统中的位置
1.3 Prestart Hook 时序
二、hook/main.go 逐行解析(67行)
2.1 导入
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"ascend-common/api"
"ascend-common/common-utils/hwlog"
"ascend-docker-runtime/hook/process"
"ascend-docker-runtime/mindxcheckutils"
)
const (
loggingPrefix = api.AscendDockerHook
)
| 导入项 | 作用 |
|---|---|
api |
提供 AscendDockerHook 常量(日志前缀) |
process |
Hook处理包 |
mindxcheckutils |
安全校验 |
loggingPrefix — 设置标准log的前缀,用于区分不同组件的日志。
2.2 main函数
func main() {
defer func() {
if err := recover(); err != nil {
log.Fatal(err)
}
}()
log.SetPrefix(loggingPrefix)
- panic恢复:与runtime/main.go相同的设计,defer recover捕获panic
log.SetPrefix(loggingPrefix)— 设置标准log前缀为api.AscendDockerHook(如"[ascend-docker-hook]")
ctx, _ := context.WithCancel(context.Background())
if err := process.InitLogModule(ctx); err != nil {
log.Fatal(err)
}
logPrefixWords, err := mindxcheckutils.GetLogPrefix()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := mindxcheckutils.ChangeRuntimeLogMode("hook-run-"); err != nil {
fmt.Println("defer changeFileMode function failed")
}
}()
- 与runtime/main.go的模式完全一致:
- 创建context
- 初始化hwlog日志
- 获取日志前缀(uid+tty)
- defer收紧日志权限(前缀为"hook-run-“,区别于runtime的"runtime-run-”)
hwlog.RunLog.Infof("%v docker hook starting, try to setup container", logPrefixWords)
if !mindxcheckutils.StringChecker(strings.Join(os.Args, " "), 0,
process.MaxCommandLength, mindxcheckutils.DefaultWhiteList+" ") {
hwlog.RunLog.Errorf("%v docker hook failed", logPrefixWords)
log.Fatal("command error")
}
if err := process.DoPrestartHook(); err != nil {
hwlog.RunLog.Errorf("%v docker hook failed: %#v", logPrefixWords, err)
log.Fatal(fmt.Errorf("failed in runtime.doProcess: %#v", err))
}
}
逐行解析:
- 记录启动日志
- 参数安全校验:与runtime/main.go相同的StringChecker白名单校验
- 调用
process.DoPrestartHook()— 核心处理 - 错误处理:记录hwlog错误 + log.Fatal终止
设计意图:hook/main.go与runtime/main.go结构几乎完全相同,遵循统一的入口模式:panic恢复→日志→前缀→安全校验→核心逻辑。
三、hook/process/process.go 逐行解析(413行)
3.1 常量定义
const (
runLogPath = api.HookRunLogPath
ascendRuntimeOptions = api.AscendRuntimeOptionsEnv
ascendRuntimeMounts = api.AscendRuntimeMountsEnv
ascendVisibleDevices = api.AscendVisibleDevicesEnv
ascendAllowLink = api.AscendAllowLinkEnv
ascendDockerCli = "ascend-docker-cli"
defaultAscendDockerCli = "/usr/local/bin/ascend-docker-cli"
configDir = api.RunTimeDConfigPath
baseConfig = "base"
configFileSuffix = "list"
hcclRootInfo = "/etc/hccl_rootinfo.json"
topoDirPath = "/usr/local/Ascend/driver/topo"
kvPairSize = 2
MaxCommandLength = 65535
)
| 常量 | 值 | 用途 |
|---|---|---|
runLogPath |
api定义路径 | Hook日志文件路径 |
ascendRuntimeMounts |
ASCEND_RUNTIME_MOUNTS |
挂载配置名环境变量 |
ascendAllowLink |
ASCEND_ALLOW_LINK |
软链接模式环境变量 |
ascendDockerCli |
"ascend-docker-cli" |
CLI程序文件名 |
configDir |
/etc/ascend-docker-runtime.d/ |
挂载配置文件目录 |
baseConfig |
"base" |
默认基础配置名 |
configFileSuffix |
"list" |
配置文件后缀(如base.list) |
hcclRootInfo |
/etc/hccl_rootinfo.json |
HCCL集合通信根信息 |
topoDirPath |
/usr/local/Ascend/driver/topo |
NPU拓扑目录 |
3.2 变量定义
var (
limitReader = io.LimitReader(os.Stdin, limiter.DefaultDataLimit)
doExec = syscall.Exec
ascendDockerCliName = ascendDockerCli
defaultAscendDockerCliName = defaultAscendDockerCli
)
var validRuntimeOptions = [...]string{
"NODRV",
"VIRTUAL",
}
| 变量 | 用途 |
|---|---|
limitReader |
限制stdin读取大小,防止超大输入攻击 |
doExec |
syscall.Exec的变量引用(可被测试mock) |
validRuntimeOptions |
合法运行时选项白名单:NODRV(无驱动模式)、VIRTUAL(虚拟设备模式) |
设计意图:limitReader 防止恶意容器提供超大的OCI State JSON导致内存耗尽。limiter.DefaultDataLimit 是ascend-common库定义的合理上限。
3.3 containerConfig 结构体
type containerConfig struct {
Pid int
Rootfs string
Env []string
}
Pid— 容器进程的Host PID,CLI用此PID进入容器的namespaceRootfs— 容器根文件系统在Host上的路径,CLI需要此路径来挂载文件/目录Env— 容器环境变量,用于解析ASCEND_*变量
3.4 InitLogModule
func InitLogModule(ctx context.Context) error {
const backups = 2
const logMaxAge = 365
const fileMaxSize = 2
runLogConfig := hwlog.LogConfig{
LogFileName: runLogPath,
LogLevel: 0,
MaxBackups: backups,
MaxAge: logMaxAge,
OnlyToFile: true,
FileMaxSize: fileMaxSize,
}
if err := hwlog.InitRunLogger(&runLogConfig, ctx); err != nil {
fmt.Printf("log init failed, error is %v", err)
return err
}
return nil
}
与runtime/process的InitLogModule完全相同的设计,仅日志路径不同(使用api.HookRunLogPath)。
3.5 parseMounts — 挂载配置名解析
func parseMounts(mounts string) []string {
if mounts == "" {
return []string{baseConfig}
}
const maxMountLength = 128
if len(mounts) > maxMountLength {
return []string{baseConfig}
}
mountConfigs := make([]string, 0)
for _, m := range strings.Split(mounts, ",") {
m = strings.TrimSpace(m)
m = strings.ToLower(m)
mountConfigs = append(mountConfigs, m)
}
return mountConfigs
}
逐行解析:
- 如果空 → 返回默认的
["base"] - 长度超过128 → 降级为
["base"](安全限制) - 按逗号分割(如
"base,custom1,custom2"→["base","custom1","custom2"]) - 每个配置名做 TrimSpace + ToLower 标准化
设计意图:ASCEND_RUNTIME_MOUNTS=base,custom 指定要读取哪些.list配置文件。每个配置文件对应一组需要挂载的文件/目录列表。
3.6 parseRuntimeOptions — 运行时选项解析
func isRuntimeOptionValid(option string) bool {
for _, validOption := range validRuntimeOptions {
if option == validOption {
return true
}
}
return false
}
func parseRuntimeOptions(runtimeOptions string) ([]string, error) {
parsedOptions := make([]string, 0)
if runtimeOptions == "" {
return parsedOptions, nil
}
const maxLength = 128
if len(runtimeOptions) > maxLength {
return nil, fmt.Errorf("invalid runtime option, the length exceeds 128 characters")
}
for _, option := range strings.Split(runtimeOptions, ",") {
option = strings.TrimSpace(option)
if !isRuntimeOptionValid(option) {
return nil, fmt.Errorf("invalid runtime option of invalid input value")
}
parsedOptions = append(parsedOptions, option)
}
return parsedOptions, nil
}
逐行解析:
- 空字符串 → 返回空列表
- 长度超过128 → 拒绝
- 按逗号分割
- 每个选项TrimSpace后用白名单校验
- 只允许"NODRV"或"VIRTUAL"
NODRV模式说明:无驱动模式,只挂载设备文件但不加载驱动,用于调试或特殊场景。
3.7 parseSoftLinkMode — 软链接模式解析
func parseSoftLinkMode(allowLink string) (string, error) {
if allowLink == "True" {
return "True", nil
}
if allowLink == "" || allowLink == "False" {
return "False", nil
}
return "", fmt.Errorf("invalid soft link option")
}
| 输入 | 输出 | 说明 |
|---|---|---|
"True" |
"True" |
允许软链接挂载 |
"" |
"False" |
默认禁止 |
"False" |
"False" |
显式禁止 |
| 其他 | error | 非法值 |
设计意图:软链接挂载有安全风险(可能指向任意路径),默认禁止,需要显式开启。
3.8 getContainerConfig — 从stdin读取容器配置
var getContainerConfig = func() (*containerConfig, error) {
state := new(specs.State)
decoder := json.NewDecoder(limitReader)
if err := decoder.Decode(state); err != nil {
return nil, fmt.Errorf("failed to parse the container's state")
}
configPath := path.Join(state.Bundle, "config.json")
if _, err := mindxcheckutils.RealFileChecker(configPath, true, true, mindxcheckutils.DefaultSize); err != nil {
return nil, err
}
ociSpec, err := parseOciSpecFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse OCI spec: %v", err)
}
if len(ociSpec.Process.Env) > MaxCommandLength {
return nil, fmt.Errorf("too many items in spec file")
}
// when use ctr->containerd. the rootfs in config.json is a relative path
rfs := ociSpec.Root.Path
if !filepath.IsAbs(rfs) {
rfs = path.Join(state.Bundle, ociSpec.Root.Path)
}
ret := &containerConfig{
Pid: state.Pid,
Rootfs: rfs,
Env: ociSpec.Process.Env,
}
return ret, nil
}
逐行解析:
specs.State— OCI State结构体,包含Pid(容器进程PID)、Bundle(bundle目录路径)、ID(容器ID)limitReader— 限制stdin读取大小(防DoS)json.NewDecoder(limitReader).Decode(state)— 从stdin解析OCI State JSON- 构造config.json路径:
state.Bundle + "/config.json" RealFileChecker(configPath, true, true, ...)— 安全校验(checkParent=true, allowLink=true)parseOciSpecFile— 解析config.json获取OCI Spec- 环境变量数量校验(≤65535)
- rootfs相对路径处理:当使用containerd(ctr命令)时,rootfs可能是相对路径,需要拼接Bundle路径
OCI State JSON格式:
{
"ociVersion": "1.0.0",
"id": "container-id",
"pid": 12345,
"bundle": "/path/to/bundle",
"status": "created"
}
3.9 readMountConfig — 读取单个.list配置文件
func readMountConfig(dir string, name string) ([]string, []string, error) {
configFileName := fmt.Sprintf("%s.%s", name, configFileSuffix)
baseConfigFilePath, err := filepath.Abs(filepath.Join(dir, configFileName))
if err != nil {
return nil, nil, fmt.Errorf("failed to assemble base config file path: %v", err)
}
fileInfo, err := os.Stat(baseConfigFilePath)
if _, err := mindxcheckutils.RealFileChecker(baseConfigFilePath, true, false,
mindxcheckutils.DefaultSize); err != nil {
return nil, nil, err
}
if err != nil {
return nil, nil, fmt.Errorf("cannot stat base configuration file %s : %v", baseConfigFilePath, err)
}
if !fileInfo.Mode().IsRegular() {
return nil, nil, fmt.Errorf("base configuration file damaged because is not a regular file")
}
f, err := os.Open(baseConfigFilePath)
if err != nil {
return nil, nil, fmt.Errorf("failed to open base configuration file %s: %v", baseConfigFilePath, err)
}
defer f.Close()
fileMountList, dirMountList := make([]string, 0), make([]string, 0)
const maxEntryNumber = 128
entryCount := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
mountPath := scanner.Text()
entryCount = entryCount + 1
if entryCount > maxEntryNumber {
return nil, nil, fmt.Errorf("mount list too long")
}
absMountPath, err := filepath.Abs(mountPath)
if err != nil {
continue
}
mountPath = absMountPath
stat, err := os.Stat(mountPath)
if err != nil {
continue
}
if stat.Mode().IsRegular() {
fileMountList = append(fileMountList, mountPath)
} else if stat.Mode().IsDir() {
dirMountList = append(dirMountList, mountPath)
}
}
return fileMountList, dirMountList, nil
}
逐行解析:
- 构造配置文件路径:
/etc/ascend-docker-runtime.d/base.list - 安全校验:
RealFileChecker(防软链接攻击、文件大小限制) - 常规文件检查:
!fileInfo.Mode().IsRegular()拒绝非普通文件 - 打开文件,defer关闭
- 逐行扫描(bufio.Scanner):
- 每行是一个文件/目录的绝对路径
- 最大128行(安全限制)
- 转为绝对路径
os.Stat检查存在性- 根据类型分类:普通文件→fileMountList,目录→dirMountList
- 错误容忍策略:任何单行解析失败都
continue跳过
base.list 文件示例:
/usr/local/Ascend/driver/lib64/libdcmi.so
/usr/local/Ascend/driver/lib64/libascendcl.so
/usr/local/Ascend/driver/lib64/
/usr/local/Ascend/addins/
3.10 readConfigsOfDir — 读取多个配置文件
func readConfigsOfDir(dir string, configs []string) ([]string, []string, error) {
fileInfo, err := os.Stat(dir)
if err != nil {
return nil, nil, fmt.Errorf("cannot stat configuration directory %s : %v", dir, err)
}
if !fileInfo.Mode().IsDir() {
return nil, nil, fmt.Errorf("%s should be a dir for docker runtime, but now it is not", dir)
}
fileMountList := make([]string, 0)
dirMountList := make([]string, 0)
for _, config := range configs {
fileList, dirList, err := readMountConfig(dir, config)
if err != nil {
return nil, nil, fmt.Errorf("failed to process config %s: %v", config, err)
}
fileMountList = append(fileMountList, fileList...)
dirMountList = append(dirMountList, dirList...)
}
return fileMountList, dirMountList, nil
}
逐行解析:
- 校验配置目录存在且确实是目录
- 遍历配置名列表(如
["base", "custom"]) - 对每个配置名调用
readMountConfig读取对应的.list文件 - 合并所有配置的文件挂载列表和目录挂载列表
设计意图:支持多个.list配置文件的组合,实现声明式挂载配置。例如ASCEND_RUNTIME_MOUNTS=base,vnpu会读取base.list和vnpu.list。
3.11 addUBMount — UB挂载自动添加
func addUBMount(fileMountList []string, dirMountList []string) ([]string, []string) {
ubMountItems := []string{
hcclRootInfo,
topoDirPath,
}
for _, mountItem := range ubMountItems {
mountItemInfo, err := os.Stat(mountItem)
if err != nil {
hwlog.RunLog.Debugf("%s may not exists, error: %v", mountItem, err)
continue
}
if mountItemInfo.Mode().IsDir() {
dirMountList = append(dirMountList, mountItem)
continue
}
fileMountList = append(fileMountList, mountItem)
}
return fileMountList, dirMountList
}
逐行解析:
- 定义需要自动添加的UB相关挂载项:
/etc/hccl_rootinfo.json— HCCL集合通信根信息(文件)/usr/local/Ascend/driver/topo— NPU拓扑信息(目录)
- 对每项检查是否存在:
- 不存在 → Debug日志记录,跳过(非错误)
- 是目录 → 追加到dirMountList
- 是文件 → 追加到fileMountList
设计意图:UB(Ultra-Bandwidth)相关的HCCL和拓扑信息是NPU通信的基础设施,应自动挂载(如果存在),无需用户手动配置。
3.12 getArgs — 组装CLI参数
func getArgs(cliPath string, containerConfig *containerConfig, fileMountList []string,
dirMountList []string, allowLink string) []string {
args := append([]string{cliPath},
"--allow-link", allowLink, "--pid", fmt.Sprintf("%d", containerConfig.Pid),
"--rootfs", containerConfig.Rootfs)
for _, filePath := range fileMountList {
args = append(args, "--mount-file", filePath)
}
for _, dirPath := range dirMountList {
args = append(args, "--mount-dir", dirPath)
}
return args
}
生成的CLI参数格式:
ascend-docker-cli \
--allow-link False \
--pid 12345 \
--rootfs /var/lib/docker/overlay2/xxx/merged \
--mount-file /usr/local/Ascend/driver/lib64/libdcmi.so \
--mount-file /etc/hccl_rootinfo.json \
--mount-dir /usr/local/Ascend/driver/lib64/ \
--mount-dir /usr/local/Ascend/driver/topo \
--options VIRTUAL
| 参数 | 用途 |
|---|---|
--allow-link |
是否允许软链接挂载(True/False) |
--pid |
容器进程PID,CLI用此PID进入容器namespace |
--rootfs |
容器根文件系统路径 |
--mount-file |
需要挂载的文件(可重复多个) |
--mount-dir |
需要挂载的目录(可重复多个) |
--options |
运行时选项(NODRV/VIRTUAL) |
3.13 DoPrestartHook — 核心处理函数
func DoPrestartHook() error {
containerConfig, err := getContainerConfig()
if err != nil {
return fmt.Errorf("failed to get container config: %#v", err)
}
if visibleDevices := getValueByKey(containerConfig.Env, ascendVisibleDevices); visibleDevices == "" {
return nil
}
mountConfigs := parseMounts(getValueByKey(containerConfig.Env, ascendRuntimeMounts))
fileMountList, dirMountList, err := readConfigsOfDir(configDir, mountConfigs)
if err != nil {
return fmt.Errorf("failed to read configuration from config directory: %#v", err)
}
// adapt a5 ub mount
fileMountList, dirMountList = addUBMount(fileMountList, dirMountList)
hwlog.RunLog.Infof("mount files: [%v]", strings.Join(fileMountList, ", "))
hwlog.RunLog.Infof("mount directories: [%v]", strings.Join(dirMountList, ", "))
parsedOptions, err := parseRuntimeOptions(getValueByKey(containerConfig.Env, ascendRuntimeOptions))
if err != nil {
return fmt.Errorf("failed to parse runtime options: %#v", err)
}
allowLink, err := parseSoftLinkMode(getValueByKey(containerConfig.Env, ascendAllowLink))
if err != nil {
return fmt.Errorf("failed to parse soft link mode: %#v", err)
}
currentExecPath, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot get the path of docker-hook: %#v", err)
}
cliPath := path.Join(path.Dir(currentExecPath), ascendDockerCliName)
if _, err = os.Stat(cliPath); err != nil {
return fmt.Errorf("cannot find docker-cli executable file at %s: %#v", cliPath, err)
}
if _, err := mindxcheckutils.RealFileChecker(cliPath, true, false, mindxcheckutils.DefaultSize); err != nil {
return err
}
args := getArgs(cliPath, containerConfig, fileMountList, dirMountList, allowLink)
if len(parsedOptions) > 0 {
args = append(args, "--options", strings.Join(parsedOptions, ","))
}
hwlog.RunLog.Info("docker hook success, will start cli")
if err := mindxcheckutils.ChangeRuntimeLogMode("hook-run-"); err != nil {
return err
}
if err := doExec(cliPath, args, os.Environ()); err != nil {
return fmt.Errorf("failed to exec docker-cli %v: %v", args, err)
}
return nil
}
完整执行流程:
getContainerConfig()— 从stdin读取OCI State + config.json- 检查
ASCEND_VISIBLE_DEVICES— 如果为空,直接返回(非NPU容器) parseMounts— 解析ASCEND_RUNTIME_MOUNTS获取挂载配置名列表readConfigsOfDir— 读取/etc/ascend-docker-runtime.d/下的.list文件addUBMount— 自动添加HCCL和Topo挂载parseRuntimeOptions— 解析ASCEND_RUNTIME_OPTIONSparseSoftLinkMode— 解析ASCEND_ALLOW_LINK- 查找
ascend-docker-cli— 在hook同目录下 RealFileChecker— 安全校验CLI文件getArgs— 组装CLI参数- 如果有运行时选项 → 追加
--options参数 ChangeRuntimeLogMode— 收紧日志权限doExec(syscall.Exec) — 替换进程为ascend-docker-cli
四、关键设计决策
| 决策 | 原因 | 实现 |
|---|---|---|
| stdin读取OCI State | OCI Hook规范要求通过stdin传递State | io.LimitReader(os.Stdin, ...) |
| 限制stdin读取大小 | 防止超大JSON导致内存耗尽 | limiter.DefaultDataLimit |
| .list配置文件 | 声明式挂载,非硬编码 | /etc/ascend-docker-runtime.d/base.list |
| 最大128行限制 | 防止配置文件过大 | maxEntryNumber = 128 |
| 自动添加UB挂载 | HCCL/Topo是NPU通信基础 | addUBMount 检测存在即添加 |
| syscall.Exec替换进程 | Hook执行完毕后CLI需要继续运行 | doExec = syscall.Exec |
| 环境变量解析顺序 | GetValueByKey从前往后 | 第一个匹配的值生效 |
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)