Dockerfile 编写经验：优化大小与效率 Dockerfile 通用的技巧总结 1. 使用多阶段构建 2. 最小化层数 3. 彻底清理 4. 选择合适的基镜像 5. 仅安装必要的依赖 6. 利用构建缓存 常见陷阱 总结 Dockerfile 编写经验：优化大小与效率 创建高效的 Docker 镜像对于加速构建、减少存储需求和提升部署性能至关重要。在实践中，通过不断改进 Dockerfile，可以总结出编写优化 Dockerfile 的一些关键技巧。 Dockerfile 通用的技巧总结 1. 使用多阶段构建 利用多阶段构建分离构建和运行时环境，仅将必要的产物（二进制文件、库、配置）复制到最终阶段，这通过排除构建工具和中间文件保持运行时镜像精简。 示例：仅从构建阶段复制编译后的二进制文件和特定资源 WORKDIR /device-openvino # Copy binaries and resources from the builder stage COPY --from=builder /device-openvino/device-openvino-object-classification-c /device-openvino/device-openvino-object-classification-c # Copy the CSDK library (custom dependency built by build_deps.sh) COPY --from=builder /opt/iotech/iot/1.5/lib /opt/iotech/iot/1.5/lib COPY --from=builder /device-openvino/deps/device-sdk-c/build/release/_CPack_Packages/Linux/TGZ/csdk-0.0.0 /device-openvino/csdk # copy device service configuration file COPY res/configuration.yaml /device-openvino/res/configuration.yaml COPY res/profiles /device-openvino/res/profiles COPY res/devices /device-openvino/res/devices 2. 最小化层数 将相关命令（例如，apt-get update && apt-get install ）合并到单个 RUN 指令中，以减少层数。每个 RUN 都会创建一个新层，增加镜像大小。 使用&&和\保持可读性，同时串联命令。 # Configure the image source and install runtime dependencies RUN sed -i 's/archive.ubuntu.com/mirror.nju.edu.cn/g' /etc/apt/sources.list && \ sed -i 's/security.ubuntu.com/mirror.nju.edu.cn/g' /etc/apt/sources.list && \ apt-get update && apt-get install -y curl gnupg && \ # Configure OpenVINO repository curl -fsSL https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | apt-key add - && \ echo "deb https://apt.repos.intel.com/openvino/2025 ubuntu22 main" | tee /etc/apt/sources.list.d/intel-openvino-2025.list && \ # Configure Intel GPU repository curl -fsSL https://repositories.intel.com/gpu/intel-graphics.key | \ gpg --yes --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \ echo "deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/gpu/ubuntu jammy unified" | \ tee /etc/apt/sources.list.d/intel-gpu-jammy.list && \ # Install runtime dependencies apt-get update && apt-get install -y \ # base dependencies dumb-init \ libyaml-dev \ uuid-dev \ libpaho-mqtt-dev \ libcbor-dev \ ... 3. 彻底清理 在包安装后始终移除临时文件，例如APT缓存（/var/lib/apt/lists/*）。使用apt-get clean和--no-install-recommends避免不必要的依赖。 示例： # cleanup --no-install-recommends && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* 4. 选择合适的基镜像 选择满足需求的最小基镜像（例如，ubuntu:22.04 或更小的 debian:slim ）。除非必要，避免使用臃肿的镜像。 明确指定平台（例如，--platform=linux/amd64 ）以确保兼容性 # including its dependencies ARG BASE=ubuntu:22.04 FROM --platform=linux/amd64 ${BASE} as builder LABEL license='SPDX-License-Identifier: Apache-2.0' \ ... 5. 仅安装必要的依赖 精确指定运行时依赖。避免在最终镜像中安装开发包（例如，libopencv-dev），除非必要。新版Dockerfile选择性安装OpenCV模块是一个很好的示例。 使用apt-cache search或包文档确定确切的依赖。 6. 利用构建缓存 按从最不可能更改到最可能更改的顺序排列指令（例如，先安装包，再复制源代码）。这最大化构建缓存的复用。 新版Dockerfile在 make 之前复制源代码，确保早期层（例如依赖安装）被缓存，能大大节省构建时间！ # build device service COPY src /device-openvino/src COPY Makefile /device-openvino/Makefile RUN make clean build # Stage 2: Create the final runtime image FROM --platform=linux/amd64 ${BASE} 常见陷阱 过度安装依赖：在运行时镜像中安装广泛的包（例如，libopencv-dev）会增加体积。始终验证运行时需求。 忘记清理：不移除APT缓存或临时文件会在镜像中留下不必要的数据。 冗余指令：重复环境变量设置或存储库配置会增加层数和复杂性。 未固定版本：不指定包版本可能导致构建不一致。 广泛的COPY命令：不加筛选地复制整个目录会包含未使用的文件，增加大小。 总结 本文展示了如何通过合并命令、清理缓存和选择最小依赖等小而精心的更改，显著减少 Dockerfile 构建镜像的大小、开发过程的效率以及发布后的稳定性。通过遵循上述技巧，可以创建精简、安全且高效的Docker镜像，使应用更易于部署和维护。
使用 OpenVINO Model Zoo 下载和转换模型 前提条件 1. 安装 pip 及 OpenVINO 开发包 2. 安装 Protocol Buffers 3. 验证安装 4. 检查 Model Zoo 5. 下载并转换 AlexNet 模型 结论 使用 OpenVINO Model Zoo 下载和转换模型 OpenVINO 是由英特尔开发的一个开源工具包，用于优化和部署深度学习模型。它特别适合在英特尔硬件上运行推理，但也支持其他平台。OpenVINO 的一个关键功能是 Model Zoo，这是一个包含预训练深度学习模型的集合，涵盖了对象检测、分类、图像分割等多种任务。 笔者将详细介绍在 Linux 系统上使用 OpenVINO Model Zoo 的步骤。我们将涵盖安装必要工具、探索可用模型、下载并转换 AlexNet 模型的完整过程。这些步骤基于 OpenVINO 2023.3 版本，并使用命令行工具完成。 前提条件 在开始之前，请确保您的 Linux 系统满足以下要求： Python 3 和 pip：需要 Python 3 以及 pip 来管理 Python 包。 OpenVINO 开发工具：这些工具是与 Model Zoo 交互的必需品。 互联网连接：用于下载工具和模型。 以下是我们将使用的关键命令： 命令 描述 apt install python3-pip 安装 Python 3 的 pip 包管理器 pip install openvino-dev==2023.3 安装 OpenVINO 开发工具（2023.3 版本） pip install protobuf==3.20.1 安装与 OpenVINO 兼容的 Protocol Buffers pip list 列出已安装的 Python 包以验证安装 omz_downloader -h 显示 omz_downloader 工具的帮助信息 omz_downloader --print_all 列出 Model Zoo 中的所有可用模型 omz_downloader --name alexnet 下载 AlexNet 模型 omz_converter --name alexnet 将 AlexNet 模型转换为 OpenVINO IR 格式 1. 安装 pip 及 OpenVINO 开发包 如果您的系统尚未安装 Python 3 的 pip，可以通过以下命令安装： apt install python3-pip 此命令确保可以安装和管理 Python 包。OpenVINO 开发包包含与 Model Zoo 交互所需的工具，例如 omz_downloader 和 omz_converter。安装 2023.3 版本的开发包： pip install openvino-dev==2023.3 确保版本号与您使用的 OpenVINO 版本匹配，以避免兼容性问题。 2. 安装 Protocol Buffers 某些模型可能需要 Protocol Buffers（protobuf）来处理模型文件，例如转换格式等。安装与 OpenVINO 2023.3 兼容的版本： pip install protobuf==3.20.1 此步骤确保您可以处理需要 protobuf 的模型。 3. 验证安装 为了确认所有工具都已正确安装，运行以下命令列出已安装的 Python 包： pip list 在输出中查找以下包，如果这些包出现在列表中，说明安装成功： 包名 版本 openvino-dev 2023.3 protobuf 3.20.1 4. 检查 Model Zoo OpenVINO Model Zoo 包含超过 200 个预训练模型，涵盖了对象检测、分类、图像分割、手写识别、文本到语音、姿势估计等多种任务。这些模型分为两类： 英特尔模型：已转换为 OpenVINO 格式。 公共模型：需要使用 OpenVINO 模型转换 API 进行转换。 要了解 omz_downloader 工具的用法，运行： omz_downloader -h usage: omz_downloader [-h] [--name PAT[,PAT...]] [--list FILE.LST] [--all] [--print_all] [--precisions PREC[,PREC...]] [-o DIR] [--cache_dir DIR] [--num_attempts N] [--progress_format {text,json}] [-j N] options: -h, --help show this help message and exit --name PAT[,PAT...] download only models whose names match at least one of the specified patterns --list FILE.LST download only models whose names match at least one of the patterns in the specified file --all download all available models --print_all print all available models --precisions PREC[,PREC...] download only models with the specified precisions (actual for DLDT networks); specify one or more of: FP16-INT8,FP32-INT1,FP32-INT8,FP16,FP32,FP16-INT1 ... -j N, --jobs N how many downloads to perform concurrently 此命令将显示工具的选项和用法，例如指定模型名称、下载目录等。要查看 Model Zoo 中所有可用模型的列表，运行： omz_downloader --print_all 此命令将输出一个详细的模型列表，您可以根据任务需求选择合适的模型： Sphereface aclnet aclnet-int8 action-recognition-0001 ... yolo-v2-ava-sparse-35-0001 yolo-v2-ava-sparse-70-0001 yolof yolox-tiny 5. 下载并转换 AlexNet 模型 在本例中，我们将下载 AlexNet 模型，这是一个用于图像分类的经典卷积神经网络。运行以下命令： omz_downloader --name alexnet 此命令会将 AlexNet 模型下载到默认目录（通常是用户主目录下的 open_model_zoo_models）。您可以通过 --download_dir 选项指定自定义下载路径。下载的模型需要转换为 OpenVINO 的中间表示（IR）格式，以便在 OpenVINO 应用程序中进行高效推理。使用 omz_converter 工具完成此操作： omz_converter --name alexnet 此命令会将 AlexNet 模型转换为 IR 格式，生成两个文件：一个 .xml 文件（描述模型结构）和一个 .bin 文件（包含模型权重）。转换后的模型可以直接用于 OpenVINO 支持的推理任务。 结论 通过以上步骤，笔者成功在 Linux 系统上安装了 OpenVINO 工具，探索了 Model Zoo，下载并转换了 AlexNet 模型。这些步骤为使用 OpenVINO Model Zoo 中的其他模型奠定了基础，使读者能够轻松集成预训练模型到深度学习应用中。
基于 C 语言的线程安全的图像缓冲队列
线程安全的图像缓冲队列 1. 介绍 2. 关键代码 2.1 数据结构 2.2 入队出队核心函数 3. 内存管理注意事项 3.1 c_mat_t 和 mat_data 的生命周期 3.2 队列销毁时的内存清理： 4. 总结 线程安全的图像缓冲队列 在这篇博客中，笔者将探讨一个最近实现的基于 C 语言的线程安全图像缓冲循环队列，重点分析其内存管理的细节和注意事项。该队列用于在图像处理后入队，供后续 AI 推理时出队使用，特别适用于实时视频处理或 AI 推理流水线。以下笔者将聚焦内存管理的实现细节、潜在风险及优化建议。 1. 介绍 该图像缓冲队列是一个固定大小的线程安全循环队列，基于 OpenCV 的 Mat 结构存储图像数据，采用生产者-消费者模型。内存管理是笔者重点考虑的细节，需要确保队列在高负载场景下的稳定性和高效性： 生产者：预处理图像（如调整大小、归一化）并入队。 消费者：出队图像并送入 AI 模型进行推理。 队列的关键特性包括： 使用 POSIX 线程（ pthread ）的互斥锁和条件变量实现线程安全。 通过循环缓冲区优化内存使用。 支持图像元数据（名称、路径）和 OpenCV 图像数据的管理。 2. 关键代码 2.1 数据结构 c_mat_t：OpenCV Mat 的封装，存储图像数据及其元数据。 typedef struct c_mat { unsigned char* mat_data; int mat_data_size; int mat_width; int mat_height; int mat_channels; int mat_type; } c_mat_t; 内存管理要点：mat_data 是一个指针，指向动态分配的实际图像数据。调用者需负责分配和释放内存，队列本身不管理 mat_data 的生命周期。 ImageInfo 是存储图像元数据和 c_mat_t 指针; ImageQueue 是循环队列结构，包含 ImageInfo 数组和同步原语: typedef struct { char name[MAX_NAME_LEN]; char path[MAX_NAME_LEN]; c_mat_t *img; } ImageInfo; typedef struct { ImageInfo items[MAX_QUEUE_SIZE]; int front; int rear; int size; pthread_mutex_t mutex; pthread_cond_t not_full; pthread_cond_t not_empty; } ImageQueue; 内存管理要点：items 是一个固定大小的数组（MAX_QUEUE_SIZE = 10），在栈或全局内存中分配，无需动态分配。mutex 和条件变量由 POSIX 线程库管理。 2.2 入队出队核心函数 从该队列设计上讲：调用者手动管理每一张图片的内存创建与释放，因此初始化队列和销毁队列的设计比较简单且常规，不再赘述。 入队图像： int enqueue(ImageQueue *queue, const char *name, const char *path, const c_mat_t *img) { pthread_mutex_lock(&queue->mutex); while (queue->size == MAX_QUEUE_SIZE) { pthread_cond_wait(&queue->not_full, &queue->mutex); } queue->rear = (queue->rear + 1) % MAX_QUEUE_SIZE; strncpy(queue->items[queue->rear].name, name, MAX_NAME_LEN - 1); strncpy(queue->items[queue->rear].path, path, MAX_NAME_LEN - 1); queue->items[queue->rear].img = img; queue->size++; pthread_cond_signal(&queue->not_empty); pthread_mutex_unlock(&queue->mutex); return 0; } 内存管理： name 和 path 使用 strncpy 复制到固定大小的字符数组，MAX_NAME_LEN - 1 确保留空间给终止符，避免缓冲区溢出。 img 指针直接赋值，队列不负责分配或释放 c_mat_t 或其 mat_data，该操作由创建实际参数时负责。 注意：笔者调用前已经确保 img 和 mat_data 在入队期间保持有效，且在队列使用期间不被释放。 出队图像: int dequeue(ImageQueue *queue, ImageInfo *info) { pthread_mutex_lock(&queue->mutex); while (queue->size == 0) { pthread_cond_wait(&queue->not_empty, &queue->mutex); } *info = queue->items[queue->front]; queue->front = (queue->front + 1) % MAX_QUEUE_SIZE; queue->size--; pthread_cond_signal(&queue->not_full); pthread_mutex_unlock(&queue->mutex); return 0; } 内存管理： info 接收 ImageInfo 的副本，name 和 path 的字符数组被复制，img 指针被复制。 注意：出队后，info->img 指向的 c_mat_t 和 mat_data 仍由调用者管理。此时笔者通常会才使用完ImageInfo *info后（如 AI 推理）手动释放，如下所示：（这样的好处在于只是用该张图片的原来存储的堆空间，便于内存管理，以及避免深拷贝带来的时间成本） if (info.img->mat_data) { image_free(info.img); info.img->mat_data = NULL; } 3. 内存管理注意事项 3.1 c_mat_t 和 mat_data 的生命周期 c_mat_t和其 mat_data 由调用者分配和释放，队列仅存储指针。这要求调用者在入队和出队时确保内存的有效性。这意味着调用者需要在调用入队前分配，出队后释放，这一点很重要。 3.2 队列销毁时的内存清理： destroy_queue 仅释放 POSIX 资源，未清理 items 中残留的 img 指针。 问题：如果队列销毁时仍有图像未出队，c_mat_t 和 mat_data 可能泄漏。 改进办法： 在 destroy_queue 中遍历 items，释放有效的 img 和 mat_data（需调用者提供释放函数）。 或者记录所有入队的 c_mat_t，在销毁时统一释放。 4. 总结 该图像缓冲队列通过固定大小的循环缓冲区和线程同步机制，实现了高效的图像数据管理。然而，内存管理的细节至关重要，尤其是在 c_mat_t 和 mat_data 的生命周期管理、字符数组截断、以及队列销毁时的清理等方面。遵循上述注意事项和优化建议，可以显著提高队列的稳定性和可维护性。
Linux运维笔记：修复 ThinLinc 会话数据库损坏问题 ThinLinc 在多用户 Linux 工作站中的重要性 问题发现：会话数据库损坏 解决： 1. 备份数据 2.删除损坏的数据库文件 3.重启服务 4.重新检查 5.验证修复 总结 Linux运维笔记：修复 ThinLinc 会话数据库损坏问题 ThinLinc 是一个优秀的远程桌面解决方案，它让用户能够方便地访问工作站，运行复杂计算任务或进行数据分析。然而，最近我们遇到一个问题：ThinLinc 客户端无法成功连接至工作站。排查发现是 ThinLinc 的核心服务 vsmserver.service 启动失败，导致用户无法登录。经过进一步排查，发现问题源于会话数据库损坏，并成功解决了它。 ThinLinc 在多用户 Linux 工作站中的重要性 实验室的工作站通常需要配置高性能硬件，运行专业软件，但用户分布在不同地点。ThinLinc 通过虚拟会话管理（VSM）提供流畅的远程访问体验，让用户无需物理接触工作站即可完成工作。VSM 服务器（vsmserver.service）是 ThinLinc 的核心组件，负责协调用户会话。如果它出现故障，整个远程访问系统就会瘫痪。 最近，我们注意到用户反馈无法连接到工作站。运行 systemctl list-units --type=service 检查后，发现 vsmserver.service 状态为 loaded failed failed，详细情况如下所示： root@ccccs-Z790-UD-AX:/home/ccccs# systemctl list-units --type=service ... vsmagent.service loaded active running ThinLinc VSM agent vsmserver.service loaded failed failed ThinLinc VSM server whoopsie.service loaded active running crash report submission daemon ... 此时通过 systemctl 命令尝试重启该服务，发现仍不能成功启动... 问题发现：会话数据库损坏 首先查看了系统日志： sudo journalctl -u vsmserver.service 日志显示服务在启动后立即退出，状态码为 status=1/FAILURE，但没有具体错误详情。接着转向 ThinLinc 的专用日志文件： sudo cat /var/log/vsmserver.log 2025-04-14 12:26:52 INFO vsmserver: VSM Server version 4.15.0 build 3358 started 2025-04-14 12:26:52 ERROR vsmserver: Error loading session database: Error unpacking /var/lib/vsm/sessions (invalid load key, '\x00'.) 2025-04-14 12:26:52 ERROR vsmserver: Session database needs manual recovery. 2025-04-14 12:26:52 ERROR vsmserver: Exiting 发现是 /var/lib/vsm/sessions 文件损坏，导致 VSM 服务器无法加载会话数据库并退出。 这个错误提示 invalid load key, '\x00' 通常表示文件内容不完整或被意外修改，可能是磁盘问题、异常关机或软件 bug 导致。(工作站会偶尔发生断电或人为重启) 解决： 1. 备份数据 sudo cp -r /var/lib/vsm/sessions /var/lib/vsm/sessions.bak 2.删除损坏的数据库文件 ThinLinc 文档指出，损坏的 /var/lib/vsm/sessions 文件可以安全删除，服务器会在下次启动时自动重新创建。我们执行： sudo rm /var/lib/vsm/sessions 注意：删除会话数据库会清除当前会话记录，可能导致用户丢失未保存的会话状态。在我们的场景中，实验室用户主要运行短期任务，影响较小，但也要确保此时每人连接。 3.重启服务 sudo systemctl start vsmserver.service 4.重新检查 systemctl list-units --type=service # 启动成功 vsmserver.service loaded active running ThinLinc VSM server 5.验证修复 让用户通过他们本地 ThinLinc 客户端登录工作站，确认远程桌面功能完全恢复。 总结 定期备份：虽然 ThinLinc 的会话数据库可以重建，但备份 /var/lib/vsm/ 文件夹能降低风险。可每周自动备份。 监控磁盘健康：数据库损坏可能与磁盘错误有关。运行 df -h /var/lib/vsm 和 fsck 检查文件系统是个好习惯。 检查日志：journalctl 和 /var/log/vsmserver.log 是诊断 ThinLinc 问题的关键。熟悉这些工具能大大加快故障排除。 远程桌面系统的稳定性对用户体验至关重要。通过系统化的诊断和谨慎的操作，大多数问题都可以迎刃而解。
C 语言应用开发中的注意事项 1.全局变量 2.源代码文件夹中 .c 模块文件的划分 3.多线程编程注意的问题 线程安全和互斥锁 线程间通信 4.深浅拷贝问题 总结 C 语言应用开发中的注意事项 C 语言作为一门经典的编程语言，广泛应用于系统编程、嵌入式开发和高性能计算等领域。然而，在进行 C 语言应用开发时，开发者需要关注一系列问题，以确保代码的正确性、性能和可维护性。 笔者在基于 C 语言开发 EdgeX Foundry 设备服务实践之中，遇到了一些问题，如全局变量的使用、.c 模块文件的划分、多线程编程注意事项、深浅拷贝问题以及编译链接时的多重定义问题。本文将对其展开讨论，并提供具体的代码示例和实用建议。 1.全局变量 全局变量是在函数外部定义的变量，其作用域从定义处开始到文件结束。它们在程序整个生命周期内存在，通常不会被轻易时放掉。可被程序中的任何函数访问。它能够方便多个函数共享数据，并减少函数参数传递的开销，但同时也不利于模块化编程和代码重用，增加调试难度。 以下展示的 global_pp 全局变量，在 EdgeX Foundry 设备服务中用于保存设备协议的详细信息，为部分关键代码： #include "device_openvino.h" #include "circular_buffer.h" /* protocols */ typedef struct { char *input_model; char *input_image_path; /* Uri */ char *device_name; /* CPU, GPU[0,1,...], NPU */ char *Score; } protocol_properties; protocol_properties *global_pp; // malloc a protocol_properties void func() { if (!global_pp) { iot_log_debug(driver->lc, "malloc a global_pp (protocol_properties)"); global_pp = (protocol_properties *)malloc(sizeof(protocol_properties)); global_pp->input_model = NULL; global_pp->input_image_path = NULL; global_pp->device_name = NULL; global_pp->Score = NULL; } } int main() { func(); return 0; } 注意事项： 尽量减少全局变量的使用，优先考虑局部变量或函数参数。 使用有意义的命名（如 g_config），避免冲突。 在多线程环境中，使用锁机制确保线程安全。 2.源代码文件夹中 .c 模块文件的划分 模块化编程将程序分解为独立的模块，每个模块负责特定功能。这种方法能提高代码的可读性、可维护性和可重用性，同时便于团队协作开发。划分 .c 文件的指导思想有以下几点： 每个 .c 文件应包含一组相关功能的实现，例如字符串处理或网络通信。 避免单个 .c 文件过大，建议控制在 1000 行以内。 使用头文件 (.h) 声明对外接口，.c 文件实现具体逻辑。 头文件用于声明函数、宏、类型和全局变量，供其他模块使用。 以下是 EdgeX Foundry 设备服务中环形 buffer 缓冲区的模块设计： //circular_buffer.h #pragma once #include <stddef.h> // 包含 size_t 的定义 // Circular buffer structure struct circular_buffer { struct infer_result* buffer; // storing data size_t capacity; // Maximum buffer capacity size_t size; // The current number of stored elements size_t head; // Head index (write location) size_t tail; // Tail index (read position) }; struct circular_buffer* create_circular_buffer(size_t capacity); ... //circular_buffer.c #include "circular_buffer.h" #include <stdlib.h> #include "edgex_common.h" // Initialize circular buffer struct circular_buffer* create_circular_buffer(size_t capacity) { //malloc a circular_buffer struct circular_buffer* cb = (struct circular_buffer*)malloc(sizeof(struct circular_buffer)); if (cb == NULL) { return NULL; } cb->buffer = (struct infer_result*)malloc(sizeof(struct infer_result) * capacity); if (cb->buffer == NULL) { free(cb); return NULL; } //Init cb->capacity = capacity; cb->size = 0; cb->head = 0; cb->tail = 0; return cb; } 要点： 使用 #ifndef 和 #define 或 #pragma once 防止头文件重复包含。 头文件仅声明，不定义变量或函数。 保持头文件简洁，避免引入不必要的依赖。 3.多线程编程注意的问题 线程安全和互斥锁 在多线程程序中，多个线程可能同时访问共享资源，导致数据不一致。互斥锁（mutex）可用于保护临界区，确保同一时间只有一个线程操作共享数据。在 EdgeX Foundry 设备服务 SDK 设计中，服务实例结构体包含有互斥锁指针，在设备服务初始化时一并初始化 mutex 以便于在整个设备服务声明周期中进行资源同步管理： /** * @brief Structure representing an OpenVINO driver instance for managing inference operations. */ typedef struct openvino_driver { iot_logger_t *lc; pthread_mutex_t mutex; // for synchronization pthread_t inference_thread; // 推理线程句柄 bool stop_inference; // 控制推理线程停止的标志 } openvino_driver; viod *openvino_inference_thread (){ ... // Thread-safe storage of results pthread_mutex_lock(&driver->mutex); push_result(global_cb, *results); pthread_mutex_unlock(&driver->mutex); } 线程间通信 线程间可通过共享内存、信号量或消息队列通信。例如，使用条件变量实现生产者-消费者模型。在 EdgeX Foundry 设备服务的后续开发中也会频繁出现。 4.深浅拷贝问题 浅拷贝： 只复制基本数据类型和指针，指针指向的内存不复制。 深拷贝： 复制所有数据，包括指针指向的内存。 在 EdgeX Foundry 设备服务开发中，协议资源 global_pp; 在 openvino_create_addr 中由深拷贝赋值，以此避免global_pp;在 SDK 回调函数中被错误释放资源： /** * @brief Creates an address structure for the OpenVINO device service. * @details This callback function is used by the EdgeX Foundry framework to create and populate a protocol_properties * structure with OpenVINO-specific configuration data extracted from the provided protocols. * @param impl Pointer to the implementation-specific data (cast to openvino_driver). * @param protocols Pointer to the protocol data containing OpenVINO configuration. * @param exception Pointer to store any exception data (not used in this implementation). * @return devsdk_address_t A pointer to the populated protocol_properties structure, cast to the EdgeX address type. */ static devsdk_address_t openvino_create_addr(void *impl, const devsdk_protocols *protocols, iot_data_t **exception) { ... const iot_data_t *props = devsdk_protocols_properties(protocols, "Openvino"); //从设备获取协议资源 if (props) { result = iot_data_string_map_get_string(props, "Uri"); pp->input_image_path = result; result = iot_data_string_map_get_string(props, "model"); pp->input_model = result; result = iot_data_string_map_get_string(props, "CPU_GPU"); pp->device_name = result; result = iot_data_string_map_get_string(props, "Score"); pp->Score = result; } // Allocate and populate new global protocol properties // 深拷贝 global_pp->input_model = malloc(strlen(pp->input_model) + 1); strcpy(global_pp->input_model,pp->input_model); global_pp->input_image_path = malloc(strlen(pp->input_image_path) + 1); strcpy(global_pp->input_image_path,pp->input_image_path); global_pp->device_name = malloc(strlen(pp->device_name) + 1); strcpy(global_pp->device_name,pp->device_name); global_pp->Score = malloc(strlen(pp->Score) + 1); strcpy(global_pp->Score,pp->Score); return (devsdk_address_t)pp; } 深拷贝的原因： 对象包含动态分配的内存。 需要独立副本，避免共享内存导致的问题。 总结 在 C 语言应用开发中，开发者需要综合考虑全局变量的合理使用、模块化设计的规范性、多线程编程的线程安全性、深浅拷贝的适用场景以及编译链接的正确性。 通过谨慎使用全局变量、合理划分模块、确保线程安全、正确处理拷贝和避免多重定义问题，可以编写出健壮、高效且易于维护的 C 语言程序。
EdgeX Foundry 开发设备服务的几个关键点 1.设备服务工作流 2.设备服务示例模板 2.1 SDK 相关工作流 2.2 template.c 的细节 3.删除设备的处理逻辑 4.增加设备 4.1 增加设备的方式 4.2 增加设备的处理逻辑 总结 EdgeX Foundry 开发设备服务的几个关键点 EdgeX Foundry 设备服务是与传感器/设备或物联网对象 (“物”) 交互的边缘连接器，它需要与各种设备交互，将数据传输到边缘计算系统，并根据需求执行控制命令。EdgeX Foundry 通过其模块化设计和丰富的工具集，使得设备服务的开发变得更加简单高效。我们还可以使用设备服务 SDK 来创建您自己的 EdgeX 设备服务，本文重点介绍使用 C-SDK 开发设备服务源代码的几个必须明确的关键点 。 1.设备服务工作流 以 设备服务 template.c 为例，作为一个虚拟设备服务，它可以将数据发送到核心数据微服务，用户可以通过命令和控制微服务发送命令并获得响应，虚拟设备服务的这些功能在执行功能或性能测试时不需要任何实际设备就可以使用。 注意点： 虚拟设备服务对核心数据和元数据微服务具有依赖性，因为初始化过程需要检查或注册配置文件，设备和值描述符，因此，核心数据和元数据服务必须在设备服务初始化之前完全启动。 服务启动时，服务会通过从 YAML 文件加载“值描述符”,“设备服务”,“设备描述”来初始化，并登记在元数据微服务中，YAML文件默认存在，也可以自行创建。虚拟设备服务为元数据微服务提供回调API来管理设备实例。 2.设备服务示例模板 2.1 SDK 相关工作流 EdgeX Foundry 的 C-SDK 提供了一个设备服务模板（位于 examples/template.c 强烈推荐先看一遍代码），它提供了一套规范化的开发示例，包括自定义回调函数、设备服务启动关闭、内存开辟释放等，是我们初次接触设备服务开发的良好开端。设备服务工作流在上一节有了一个大致的介绍，结合官方的指导文档 servicewritersguide.md (强烈推荐先阅读一遍)，这里进一步描述设备服务内部 SDK 相关的工作流，展示如下： 从根本上说，设备服务由多个回调组成，上图中的 SDK 内部工作流均在一系列回调函数中作处理。一个设备服务只有一个设备服务实例结构体指针 Implementation 每次调用回调时，该指针都会被传递，如初始化回调函数的第一个参数 (template_driver *) impl。 /* --- template.c ---- */ /* Initialize performs protocol-specific initialization for the device * service. */ static bool template_init ( void *impl, struct iot_logger_t *lc, const iot_data_t *config ) { template_driver *driver = (template_driver *) impl; iot_log_debug (lc, "Template Init."); return true; } 2.2 template.c 的细节 一个设备服务必须要自定义好的回调函数如下：每一个回调函数的实现正如上面提到的 template_init 那样。 /* Device Callbacks */ devsdk_callbacks *templateImpls = devsdk_callbacks_init ( template_init, template_get_handler, template_put_handler, template_stop, template_create_addr, template_free_addr, template_create_resource_attr, template_free_resource_attr ); 同时设备服务 SDK 还提供了一些非必要的回调函数，以便于开发者实现更灵活的需求，在 servicewritersguide.md 的 Optional handlers 部分有提到。举一个例子，一个设备服务实例可以实现 device_added、device_updated 和 device_removed 处理程序，然后它将收到服务管理的设备范围的更改通知。如果需要特殊的初始化/关闭操作来优化设备的使用，这可能会有所帮助。只需要将该函数实现，并注册进设备服务的回调初始化函数即可： //device_added 的具体实现 static void device_added( void *impl, const char *devname, const devsdk_protocols *protocols, const devsdk_device_resources *resources, bool adminEnabled) { openvino_driver *driver = (openvino_driver *)impl; iot_log_info(driver->lc, "New device added: %s", devname); } ... //主函数内的回调初始化 devsdk_callbacks *templateImpls = devsdk_callbacks_init ( template_init, template_get_handler, template_put_handler, template_stop, template_create_addr, template_free_addr, template_create_resource_attr, template_free_resource_attr ); //注册自定义的设备服务回调 devsdk_callbacks_set_listeners(templateImpls, device_added, NULL, NULL); 3.删除设备的处理逻辑 每删除一个设备，一定依次进入的回调函数是：template_free_resource_attr 和 template_free_addr 。 如果自定义回调 device_removed 已经初始化，则会优先进入该回调，然后再进入上面两个默认的释放内存处理回调之中。 4.增加设备 4.1 增加设备的方式 通过 EdgeX Foundry 官方 UI 界面（如下图所示）增加一个具体的设备，通过界面可视化引导，很容易完成以下步骤： SelectDeviceService SelectDeviceProfile DevicePrimary CreateAutoEvent CreateDeviceProtocol 注意点：CreateDeviceProtocol 为必须设置项，且需要跟自己的设备服务所规定的协议保持一致。 通过 API 控制命令，直接向 core-metadata 发送 POST 请求，即可一行命令生成新设备，如下所示： curl http://localhost:59881/api/v3/device -H "Content-Type: application/json" -X POST \ -d '[ { "apiVersion" : "v3", "device": { "name": "Device1", "description": "An Example Device", "adminState": "UNLOCKED", "operatingState": "UP", "serviceName": "device-template", "profileName": "template", "protocols": { "Others": { "key1": "xxxx", "key2": "yyyy", } } } } ]' 4.2 增加设备的处理逻辑 每增加一个设备，会先调用自定义回调 device_add （如果已经初始化） 然后在第一次获取设备资源，也就是 GET 请求时，会依次调用 template_create_resource_attr 、 template_create_addr 和 template_get_handler。 以后的 GET 请求只会直接调用 template_get_handler。 总结 本文深入探讨了使用 EdgeX Foundry C-SDK 开发设备服务的几个关键点，为开发者提供了一个清晰的指导框架。通过分析设备服务的工作流、模板代码的细节以及设备增删的处理逻辑，我们可以更高效地构建自定义设备服务。 首先，设备服务的工作流强调了其与核心数据和元数据微服务的依赖性，以及通过 YAML 文件初始化配置的重要性。其次，C-SDK 的模板（template.c）提供了一个规范化的开发起点，包含必备回调函数（如 init、get、put、stop）和可选回调（如 device_added），开发者可根据需求灵活扩展。再次，删除设备的逻辑涉及内存释放回调（如 template_free_addr），而增加设备则通过 UI 或 API 触发，并涉及初始化和资源获取的回调链。 总的来说，掌握这些关键点——理解工作流、熟悉模板结构、明确设备管理的回调逻辑——能够帮助开发者快速上手 EdgeX Foundry 设备服务开发，无论是构建虚拟服务还是连接真实设备。
GCC 链接顺序问题总结 1.问题描述 2.根本原因 3.解决方法 4.注意事项 5.总结 GCC 链接顺序问题总结 1.问题描述 在编译 C/C++ 程序时，使用 GCC 链接库时可能会遇到 undefined reference to ... 的错误，即使正确指定了库（例如 -lopenvino_c）。例如，命令： gcc -I/usr/include -L/usr/lib -lopenvino_c -o main main.c 可能会报错： /usr/bin/ld: main.c:(.text+0x2a): undefined reference to `ov_get_openvino_version' 原因是 GCC 的链接顺序不符合要求。 2.根本原因 GCC 的链接器行为：GCC 使用 ld 作为链接器，ld 在解析符号时是单向扫描的，从左到右处理输入文件和库。 符号解析规则： 链接器在遇到目标文件（如 main.c 编译生成的 .o 文件）时，会记录其中未定义的符号（例如 ov_get_openvino_version）。 只有在后续扫描到的库文件中找到这些符号的定义，链接器才会解析成功。 如果库（-lXXX）出现在源文件或目标文件之前，链接器在处理库时还不知道需要哪些符号，因此不会从中提取定义，导致符号未解析。 正确的链接顺序 规则：GCC 要求源文件或目标文件在前，依赖库在后。 正确示例： gcc -I/usr/include/openvino -L/usr/lib -o main main.c -lopenvino_c ​ - main.c 在前：链接器先处理 main.c，记录未定义的符号。 ​ - -lopenvino_c 在后：链接器随后扫描 libopenvino_c.so，找到符号定义并完成链接。 3.解决方法 调整命令顺序： 始终将源文件（main.c）或目标文件（main.o）放在依赖库（-lXXX）之前。 标准格式：gcc [编译选项] 源文件 -l库名 [链接选项]。 验证符号： 使用 nm -D /path/to/lib.so | grep 函数名 检查库中是否包含所需符号，确保问题确实出在顺序而非库本身。 完整示例： gcc -I/usr/include/openvino -L/usr/lib -o main main.c -lopenvino_c 4.注意事项 多文件情况：如果有多个源文件或目标文件，确保它们都在库之前，例如： gcc -o main file1.c file2.c -lopenvino_c 库之间的依赖：如果库之间有依赖关系，也需要按从被依赖到依赖的顺序排列，例如： gcc -o main main.c -lopenvino_c -lopenvino CMake 自动处理：在使用 CMake 时，链接顺序通常由 target_link_libraries 自动管理，无需手动调整。 5.总结 GCC 的链接顺序要求是“源文件在前，依赖库在后”，这是由链接器 ld 的单向扫描机制决定的。遵循这个规则可以避免 undefined reference 错误，确保符号正确解析。
分享-如何在 K3S 集群上部署自定义 DNS，方便内部使用。
自定义 DNS 配置 部署自定义 DNS 服务 暴露 DNS 服务给集群外部使用 测试 自定义 DNS 配置 自定义一些解析，custom-dns-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: custom-dns-config namespace: default data: Corefile: | .:53 { errors health # 自定义解析规则 rewrite name regex (.*)\.t\.yiqisoft\.cn t.yiqisoft.cn hosts { 192.168.1.10 example.com # 自定义域名解析 192.168.1.11 test.example.com # 更多自定义解析 } log # 可选：记录查询日志 cache 30 # 可选：缓存响应 reload } 部署自定义 DNS 服务 使用 K3S 内置的 CoreDNS 进行部署，custom-dns-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: custom-dns namespace: default spec: replicas: 1 # 可根据需要增加副本 selector: matchLabels: app: custom-dns template: metadata: labels: app: custom-dns spec: containers: - name: coredns image: rancher/mirrored-coredns-coredns:1.10.1 args: [ "-conf", "/etc/coredns/Corefile" ] volumeMounts: - name: config-volume mountPath: "/etc/coredns" readOnly: true ports: - containerPort: 53 name: dns protocol: UDP - containerPort: 53 name: dns-tcp protocol: TCP volumes: - name: config-volume configMap: name: custom-dns-config items: - key: Corefile path: Corefile 暴露 DNS 服务给集群外部使用 使用 LoadBalancer 暴露给外部， custom-dns-service.yaml apiVersion: v1 kind: Service metadata: name: custom-dns namespace: default spec: selector: app: custom-dns ports: - name: dns port: 53 targetPort: 53 protocol: UDP - name: dns-tcp port: 53 targetPort: 53 protocol: TCP type: LoadBalancer 测试 假设集群两台服务器：192.168.123.135/136 nslookup > server 192.168.123.135 Default server: 192.168.123.135 Address: 192.168.123.135#53 > example.com Server: 192.168.123.135 Address: 192.168.123.135#53 Name: example.com Address: 192.168.1.10 > > server 192.168.123.136 Default server: 192.168.123.136 Address: 192.168.123.136#53 > test.example.com Server: 192.168.123.136 Address: 192.168.123.136#53 Name: test.example.com Address: 192.168.1.11 >
前提 浏览器提示 CORS 问题 设置相关 主网站 被动网站 前提 当我们访问网站：demo.a.com 时，会顺便访问 *.t.a.com 时，会造成 CORS 问题。 浏览器提示 CORS 问题 不用多说，CORS 问题一直存在，现在必须要要解决掉。 设置相关 假设，两个网站都是使用 Nginx 作为反向代理。 主网站 server { root /var/www/html/demo.a.com; server_name: demo.a.com; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } # CORS 配置 add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always; ... } 被动网站 server { root /var/www/html/t.a.com; server_name .t.a.com; location / { # 处理预检请求 if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'Authorization, DNT, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } # CORS 配置 add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, DNT, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Range' always; add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always; proxy_pass http://localhost:8888; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ... }
在 ubuntu22.04 中 安装 EdgeX 并运行 device-sdk-c sample
在 ubuntu22.04 中 安装 EdgeX 并运行 device-sdk-c sample 1.克隆源码-EdgeX 2.安装工具 3.编译运行-EdgeX 4.克隆源码-SDK-C 5.添加第三方依赖库 6.编译-C SDK 7.编译运行 random 设备服务示例 总结 在 ubuntu22.04 中 安装 EdgeX 并运行 device-sdk-c sample EdgeX Foundry 作为一个开源的边缘计算框架，为开发者提供了一个灵活、可扩展的平台，用于构建和管理边缘设备及服务。然而，对于初次接触 EdgeX 的用户来说，从零开始安装并运行其相关组件（如 device-sdk-c 示例）可能会面临不少挑战。 无论是环境的配置、依赖的安装，还是编译与调试的过程，每一步都需要细致的操作和清晰的思路。本文将以 Ubuntu 22.04 为基础平台，详细记录如何从克隆 EdgeX 源码到成功运行 device-sdk-c 的 sample 示例的全过程。希望通过这篇博客，不仅能为新手提供一份实用的操作指南，也能让有经验的开发者在遇到问题时找到解决思路。 1.克隆源码-EdgeX 在开始安装 EdgeX 之前，我们需要先获取其源码，而 GitHub 是获取最新版本的最佳途径。为了让后续操作更加有条理，我们首先在用户主目录下创建一个专属的工作目录，用于存放 EdgeX 相关的文件。以下是具体操作： 打开终端，输入以下命令创建一个名为 edgex 的文件夹，并进入该目录： mkdir edgex && cd edgex 接下来，我们需要从 GitHub 上克隆 EdgeX 的 Compose 文件仓库，这是运行 EdgeX 服务的核心配置文件集合。执行以下命令： git clone https://github.com/edgexfoundry/edgex-compose.git cd edgex-compose 结果显示如下： root@ubuntu:/home/ubuntu# mkdir edgex && cd edgex root@ubuntu:/home/ubuntu/edgex# git clone https://github.com/edgexfoundry/edgex-compose.git Cloning into 'edgex-compose'... remote: Enumerating objects: 5826, done. remote: Counting objects: 100% (3563/3563), done. remote: Compressing objects: 100% (319/319), done. remote: Total 5826 (delta 3504), reused 3250 (delta 3244), pack-reused 2263 (from 3) Receiving objects: 100% (5826/5826), 1.59 MiB | 1.76 MiB/s, done. Resolving deltas: 100% (4949/4949), done. root@ubuntu:/home/ubuntu/edgex# ls edgex-compose 输出信息显示了克隆进度，最终 ls 命令确认目录中已存在 edgex-compose 文件夹，说明这一步成功完成。 2.安装工具 EdgeX 的部署离不开 Docker，因为它通过容器化技术简化了服务的管理和运行。因此，我们需要先在 Ubuntu 系统中安装 Docker。以下是详细的安装步骤： 使用官方脚本安装 Docker，并指定阿里云镜像源以加速下载： curl -fsSL https://get.docker.com -o get-docker.sh sh get-docker.sh --mirror Aliyun docker -v 第一条命令通过 curl 下载安装脚本并保存为 get-docker.sh 文件；第二条命令执行脚本，--mirror Aliyun 参数指定使用阿里云镜像源，避免默认源下载缓慢；最后通过 docker -v 检查版本，确保安装成功。安装过程的输出类似： root@ubuntu:/home/ubuntu# curl -fsSL https://get.docker.com -o get-docker.sh root@ubuntu:/home/ubuntu# sh get-docker.sh --mirror Aliyun # Executing docker install script, commit: 4c94a56999e10efcf48c5b8e3f6afea464f9108e ... Client: Docker Engine - Community Version: 28.0.1 API version: 1.48 ... root@ubuntu:/home/ubuntu# docker -v Docker version 28.0.1, build 068a01e 如果下载速度过慢，可能是镜像源的问题。这时可以调整 Ubuntu 的软件源以提高效率。首先检查系统版本： root@ubuntu:/home/ubuntu# more /etc/os-release PRETTY_NAME="Ubuntu 22.04.5 LTS" NAME="Ubuntu" VERSION_ID="22.04" VERSION="22.04.5 LTS (Jammy Jellyfish)" VERSION_CODENAME=jammy ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-poli cy" UBUNTU_CODENAME=jammy 确认版本为 jammy 后，访问南京大学镜像站（https://mirror.nju.edu.cn/mirrorz-help/ubuntu/）, 获取对应源配置并编辑 /etc/apt/sources.list： # vim /etc/apt/sources.list deb https://mirror.nju.edu.cn/ubuntu/ jammy main restricted universe multiverse deb https://mirror.nju.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse deb https://mirror.nju.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted universe multiverse 启动 docker 并查看状态： root@ubuntu:/home/ubuntu# systemctl restart docker root@ubuntu:/home/ubuntu# systemctl status docker ● docker.service - Docker Application Container Engine Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset> Active: active (running) since Thu 2025-03-13 12:21:05 CST; 6s ago TriggeredBy: ● docker.socket Docs: https://docs.docker.com Main PID: 28525 (dockerd) Tasks: 10 Memory: 22.1M CPU: 1.228s CGroup: /system.slice/docker.service └─28525 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/con> Active: active (running) 表示 Docker 已正常运行，为后续 EdgeX 部署奠定了基础。 3.编译运行-EdgeX 有了 Docker 支持后，我们可以开始部署 EdgeX 服务。EdgeX 使用 Docker Compose 管理多个容器，因此需要进入 compose-builder 目录执行构建和运行命令： 进入目录并运行无安全模式的 EdgeX： cd /home/ubuntu/edgex/edgex-compose/compose-builder/ make run no-secty echo MQTT_VERBOSE= MQTT_VERBOSE= docker compose -p edgex -f docker-compose-base.yml -f add-postgres.yml -f add-mqtt-messagebus.yml -f add-mqtt-broker-mosquitto.yml convert --output docker-compose.yml.gen if [ "0" = "0" ]; then rm -rf ./gen_ext_compose; fi echo "# Generated with: Docker Compose version v2.33.1" > docker-compose.yml cat docker-compose.yml.gen >> docker-compose.yml rm docker-compose.yml.gen docker compose -p edgex up -d [+] Running 12/12 ✔ Container edgex-ui-go Running 0.0s ✔ Container edgex-postgres Running 0.0s ✔ Container edgex-mqtt-broker Running 0.0s ✔ Container edgex-core-keeper Running 0.0s ✔ Container edgex-core-metadata Running 0.0s ✔ Container edgex-core-common-config-bootstrapper Running 0.0s ✔ Container edgex-app-rules-engine Running 0.0s ✔ Container edgex-support-scheduler Running 0.0s ✔ Container edgex-core-data Running 0.0s ✔ Container edgex-kuiper Running 0.0s ✔ Container edgex-support-notifications Running 0.0s ✔ Container edgex-core-command Started 0.4s 这表明所有容器已启动，包括 UI、数据库和核心服务等。 查看容器运行状态，显示如下，每个容器显示 Up 状态，说明 EdgeX 已成功运行： docker compose ps NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS edgex-app-rules-engine nexus3.edgexfoundry.org:10004/app-service-configurable:latest "/app-service-config…" app-rules-engine 7 minutes ago Up 7 minutes 48095/tcp, 127.0.0.1:59701->59701/tcp edgex-core-command nexus3.edgexfoundry.org:10004/core-command:latest "/core-command --reg…" core-command 7 minutes ago Up 5 minutes 127.0.0.1:59882->59882/tcp edgex-core-common-config-bootstrapper nexus3.edgexfoundry.org:10004/core-common-config-bootstrapper:latest "entrypoint.sh /core…" core-common-config-bootstrapper 7 minutes ago Up 7 minutes edgex-core-data nexus3.edgexfoundry.org:10004/core-data:latest "/core-data --regist…" core-data 7 minutes ago Up 7 minutes 127.0.0.1:59880->59880/tcp edgex-core-keeper nexus3.edgexfoundry.org:10004/core-keeper:latest "/core-keeper" core-keeper 7 minutes ago Up 7 minutes 127.0.0.1:59890->59890/tcp edgex-core-metadata nexus3.edgexfoundry.org:10004/core-metadata:latest "/core-metadata ... 4.克隆源码-SDK-C 为了运行 device-sdk-c 的示例，我们还需要获取其源码。device-sdk-c 包含了丰富的 C 语言示例，是开发自定义设备服务的好起点： 在 /edgex 目录下克隆仓库： git clone https://github.com/edgexfoundry/device-sdk-c.git root@ubuntu:/home/ubuntu/edgex/edgex-compose/compose-builder# cd ../.. root@ubuntu:/home/ubuntu/edgex# ls edgex-compose root@ubuntu:/home/ubuntu/edgex# git clone https://github.com/edgexfoundry/device-sdk-c.git Cloning into 'device-sdk-c'... remote: Enumerating objects: 5641, done. remote: Counting objects: 100% (527/527), done. remote: Compressing objects: 100% (213/213), done. remote: Total 5641 (delta 419), reused 337 (delta 307), pack-reused 5114 (from 4) Receiving objects: 100% (5641/5641), 1.93 MiB | 1.70 MiB/s, done. Resolving deltas: 100% (3210/3210), done. 5.添加第三方依赖库 编译 device-sdk-c 需要一系列依赖库，参考官方 README（https://github.com/edgexfoundry/device-sdk-c/blob/main/README.md） A Linux build host A version of GCC supporting C11. CMake version 3 or greater and make. Development libraries and headers for: curl (version 7.56 or later) microhttpd (version 0.9) libyaml (version 0.1.6 or later) libcbor (version 0.5) paho (version 1.3.x) libuuid (from util-linux v2.x) IOTech's C utilities package (version 1.5) apt-get install libcurl4-openssl-dev libmicrohttpd-dev libyaml-dev libcbor-dev libpaho-mqtt-dev apt-get install lsb-release apt-transport-https curl gnupg curl -fsSL https://iotech.jfrog.io/artifactory/api/gpg/key/public | gpg --dearmor -o /usr/share/keyrings/iotech.gpg echo "deb [signed-by=/usr/share/keyrings/iotech.gpg] https://iotech.jfrog.io/iotech/debian-release $(lsb_release -cs) main" | tee -a /etc/apt/sources.list.d/iotech.list apt-get update apt-get install iotech-iot-1.5-dev apt install uuid-dev 6.编译-C SDK 在C SDK 顶层目录下, 运行 make，最终结果显示如下： root@ubuntu:/home/ubuntu/edgex/device-sdk-c# make ... [100%] Built target device-file 7.编译运行 random 设备服务示例 编译前的准备工作，参考https://github.com/edgexfoundry/device-sdk-c/blob/main/src/c/examples/random/README.md 此示例设备服务模拟了一个具有两个随机数生成器和一个可读取和写入状态的开关的设备。先配置环境变量，注意自己的路径： export CSDK_DIR=/home/logan/edgex/device-sdk-c/build/release/_CPack_Packages/Linux/TGZ/csdk-0.0.0 export LD_LIBRARY_PATH=$CSDK_DIR/lib:/opt/iotech/iot/1.5/lib 涉及头文件和库文件的环境变量很重要，并且运行程序的时候也需要用到。 在/random 目录下执行编译命令： gcc -I$CSDK_DIR/include -I/opt/iotech/iot/1.5/include -L$CSDK_DIR/lib -L/opt/iotech/iot/1.5/lib -o device-random device-random.c -lcsdk -liot 将编译好的可执行文件 device-random 复制到以下源文件目录，因为源文件目录提供了/res 里面包含预设的配置文件： cp device-sdk-c/build/release/c/examples/random/device-random device-sdk-c/src/c/examples/random/ cd /home/ubuntu/edgex/device-sdk-c/src/c/examples/random/ 执行以下命令：-o 表示覆盖默认配置，-cp 指定配置提供者 ./device-random -o -cp=keeper.http://localhost:59890 如果遇到报错，按以下方法解决： 安全 Token 报错：设置环境变量禁用安全服务： root@ubuntu:/home/ubuntu/edgex/device-sdk-c/src/c/examples/random# ./device-random -o -cp=keeper.http://localhost:59890 level=INFO ts=2025-03-13T06:44:41Z app=device-random msg="iot_threadpool_alloc (threads: 8 max_jobs: 0 default_priority: -1 affinity: -1)" level=INFO ts=2025-03-13T06:44:41Z app=device-random msg="iot_scheduler_alloc (priority: -1 affinity: -1)" level=ERROR ts=2025-03-13T06:44:41Z app=device-random msg="vault: Token file /tmp/edgex/secrets/device-random/secrets-token.json doesn't parse as JSON" Error: 4: Configuration is invalid 解决：需要关闭Edges安全服务： 参考https://docs.edgexfoundry.org/4.0/microservices/configuration/CommonEnvironmentVariables/#edgex_security_secret_store export EDGEX_SECURITY_SECRET_STORE="false" Curl 解析失败：编辑 /etc/hosts： level=DEBUG ts=2025-03-13T06:45:41Z app=device-random msg="mqtt: subscribing to edgex/configs/edgex/v4/core-common-config-bootstrapper/all-services/#" level=DEBUG ts=2025-03-13T06:45:41Z app=device-random msg="mqtt: subscribing to edgex/configs/edgex/v4/core-common-config-bootstrapper/device-services/#" level=ERROR ts=2025-03-13T06:45:42Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" level=ERROR ts=2025-03-13T06:45:43Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" level=ERROR ts=2025-03-13T06:45:44Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" level=ERROR ts=2025-03-13T06:45:45Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" level=ERROR ts=2025-03-13T06:45:46Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" level=ERROR ts=2025-03-13T06:45:47Z app=device-random msg="Curl failed with code 6 (Couldn't resolve host name)" 解决：在/etc/hosts 中添加127.0.0.1 edgex-core-metadata： /etc/hosts 127.0.0.1 localhost 127.0.0.1 edgex-core-metadata # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts 健康检查间隔缺失：修改 res/configuration.yml： level=DEBUG ts=2025-03-13T06:50:39Z app=device-random msg="HTTP response: 404" level=DEBUG ts=2025-03-13T06:50:39Z app=device-random msg="HTTP response: 400" level=ERROR ts=2025-03-13T06:50:39Z app=device-random msg="Register service failed: {"apiVersion":"v3","message":"AddRegistrationRequest.Registration.HealthCheck.Interval field is required","statusCode":400} level=ERROR ts=2025-03-13T06:50:39Z app=device-random msg="Unable to register service in registry" Error: 21: HTTP request failed level=DEBUG ts=2025-03-13T06:50:39Z app=device-random msg="Scheduler thread stopping" 解决：在res/configration.yml 中添加 HealthCheckInterval: 10s： Writable: LogLevel: DEBUG Service: Host: localhost Port: 59999 StartupMsg: Example random device service started HealthCheckInterval: 10s ​ 或者在/etc/hosts 中添加127.0.0.1 edgex-mqtt-broker,然后在执行命令 ./device-random -o -cp=keeper.http://localhost:59890 中去掉 -o： /etc/hosts 127.0.0.1 localhost 127.0.0.1 edgex-core-metadata 127.0.0.1 edgex-mqtt-broker # The following lines are desirable for IPv6 capable hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts 如果程序运行成功，将显示如下： ... level=INFO ts=2025-03-13T07:36:45Z app=device-random correlation-id=c26ef1f7-755c-4952-8091-5c7c436e039b msg="AutoEvent: RandomDevice1/SensorOne" level=INFO ts=2025-03-13T07:36:50Z app=device-random correlation-id=dc222997-3113-465b-9c5c-26829ce24901 msg="AutoEvent: RandomDevice1/SensorTwo" level=INFO ts=2025-03-13T07:36:55Z app=device-random correlation-id=049d560d-896a-4e5a-8101-f3364c2767f1 msg="AutoEvent: RandomDevice1/SensorOne" 打开浏览器进入：localhost:4000 可发现一个设备服务，可成功读取随机数，如下： 总结 通过以上步骤，我们成功地在 Ubuntu 22.04 上完成了 EdgeX 的安装，并运行了 device-sdk-c 的 random 设备服务示例。从源码的克隆到 Docker 环境的搭建，再到依赖库的安装和配置文件的调整，每一个环节都环环相扣，考验着我们的耐心和解决问题的能力。 过程中可能遇到的报错，如安全性 token 问题、Curl 解析失败或健康检查间隔配置缺失，也提醒我们在操作复杂系统时，细节的重要性不容忽视。 最终，当我们在浏览器中看到 localhost:4000 界面上显示的设备服务和随机数据时，那份成就感无疑是对所有努力的最好回报。这不仅是一个技术实现的过程，更是一次对 EdgeX 架构和边缘计算理念的深入学习之旅。未来，我们还可以基于此基础，进一步扩展功能、优化性能，让边缘计算的潜力在更多场景中得以释放！