Robot Operating System——远程修改日志等级

news2024/9/17 7:30:39

大纲

  • 日志输出Node
  • 修改其他Node日志等级的Node
    • 获取日志等级
    • 设置日志等级
  • 测试
    • 默认等级
    • Debug等级
    • Warn等级
    • Error等级
  • 完整代码

在任何系统中,日志功能的重要性都是不容忽视的。日志功能为开发者、维护者甚至用户提供了一个关键的工具,以监控、理解和调试系统的行为。所以我们数量掌握日志功能非常必要。

本文我们将结合demo_nodes_cpp/src/logging/use_logger_service.cpp来熟悉如何在ROS2中,远程修改日志等级,以方便工作中问题的排查。

在use_logger_service中,我们将在一个叫做TestNode的Node中查询/设置另外一个叫LoggerServiceNode的Node的日志等级,然后观察LoggerServiceNode的日志输出情况。

日志输出Node

和以往例子中创建的Node不同,LoggerServiceNode在构造函数中指定了要通过rclcpp::NodeOptions().enable_logger_service(true)来开启查询/设置日志等级的Service。这样TestNode才可以远程修改LoggerServiceNode的等级。

在LoggerServiceNode的构造函数中,它创建了一个监听"output"主题的订阅者。当收到发布者的消息时,它会使用RCLCPP_DEBUG、RCLCPP_INFO、RCLCPP_WARN和RCLCPP_ERROR打印消息。在本例中TestNode修改完LoggerServiceNode的日志等级后,会作为发布者发布"output"主题消息。这样我们就可以观察不同日志等级时,RCLCPP_DEBUG、RCLCPP_INFO、RCLCPP_WARN和RCLCPP_ERROR的输出情况。

// This demo program shows how to enable logger service and control logger level via logger service.
// Class LoggerServiceNode enable logger service and create a subscription. The callback of
// subscription output received message by different log functions.
// Class TestNode can set/get logger level of LoggerServiceNode and send message to it.

class LoggerServiceNode : public rclcpp::Node
{
public:
  explicit LoggerServiceNode(const std::string & node_name)
  : Node(node_name, rclcpp::NodeOptions().enable_logger_service(true))
  {
    auto callback = [this](std_msgs::msg::String::ConstSharedPtr msg)-> void {
        RCLCPP_DEBUG(this->get_logger(), "%s with DEBUG logger level.", msg->data.c_str());
        RCLCPP_INFO(this->get_logger(), "%s with INFO logger level.", msg->data.c_str());
        RCLCPP_WARN(this->get_logger(), "%s with WARN logger level.", msg->data.c_str());
        RCLCPP_ERROR(this->get_logger(), "%s with ERROR logger level.", msg->data.c_str());
      };

    sub_ = this->create_subscription<std_msgs::msg::String>("output", 10, callback);
  }

private:
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);

  const std::string node_name = "LoggerServiceNode";
  auto logger_service_node = std::make_shared<LoggerServiceNode>(
    node_name);

修改其他Node日志等级的Node

在本例中,我们使用一个自定义类TestNode来实现相关功能。

  auto test_node = std::make_shared<TestNode>(node_name);

在构造函数中,会把各种需要的的工具对象都创建好。其中包括:

  • 通知LoggerServiceNode打印日志的Topic发布者。
  • LoggerServiceNode暴露出来的Service(“/set_logger_levels"和”/get_logger_levels")的客户端。用于设置和获取LoggerServiceNode的日志等级。
class TestNode : public rclcpp::Node
{
public:
  explicit TestNode(const std::string & remote_node_name)
  : Node("TestNode"),
    remote_node_name_(remote_node_name)
  {
    pub_ = this->create_publisher<std_msgs::msg::String>("output", 10);
    logger_set_client_ = this->create_client<rcl_interfaces::srv::SetLoggerLevels>(
      remote_node_name + "/set_logger_levels");
    logger_get_client_ = this->create_client<rcl_interfaces::srv::GetLoggerLevels>(
      remote_node_name + "/get_logger_levels");
  }
  ……
private:
  const std::string remote_node_name_;
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
  rclcpp::Client<rcl_interfaces::srv::SetLoggerLevels>::SharedPtr logger_set_client_;
  rclcpp::Client<rcl_interfaces::srv::GetLoggerLevels>::SharedPtr logger_get_client_;
};

获取日志等级

我们通过上一步创建的返回日志等级Service(rcl_interfaces::srv::SetLoggerLevels)的客户端调用async_send_request查询LoggerServiceNode的日志等级。

可以看到async_send_request是一个异步方法,它需要我们调用rclcpp::spin_until_future_complete进行同步等待。只有等消息成功返回时,我们才从返回结果中提取日志等级。

  bool get_logger_level_on_remote_node(uint32_t & level)
  {
    if (!logger_get_client_->wait_for_service(2s)) {
      return false;
    }

    auto request = std::make_shared<rcl_interfaces::srv::GetLoggerLevels::Request>();
    request->names.emplace_back(remote_node_name_);
    auto result = logger_get_client_->async_send_request(request);
    if (rclcpp::spin_until_future_complete(shared_from_this(), result) !=
      rclcpp::FutureReturnCode::SUCCESS)
    {
      return false;
    }

    auto ret_result = result.get();
    level = ret_result->levels[0].level;
    return true;
  }

设置日志等级

这一步我们通过之前创建的设置日志等级的Service(rcl_interfaces::srv::SetLoggerLevels)的客户端调用async_send_request来设置LoggerServiceNode的日志等级。

  bool set_logger_level_on_remote_node(
    rclcpp::Logger::Level logger_level)
  {
    if (!logger_set_client_->wait_for_service(2s)) {
      return false;
    }

    auto request = std::make_shared<rcl_interfaces::srv::SetLoggerLevels::Request>();
    auto set_logger_level = rcl_interfaces::msg::LoggerLevel();
    set_logger_level.name = remote_node_name_;
    set_logger_level.level = static_cast<uint32_t>(logger_level);
    request->levels.emplace_back(set_logger_level);

    auto result = logger_set_client_->async_send_request(request);

    if (rclcpp::spin_until_future_complete(this->shared_from_this(), result) !=
      rclcpp::FutureReturnCode::SUCCESS)
    {
      return false;
    }

    auto ret_result = result.get();
    if (!ret_result->results[0].successful) {
      RCLCPP_ERROR(
        this->get_logger(), "Failed to change logger level: %s",
        ret_result->results[0].reason.c_str());
      return false;
    }
    return true;
  }

测试

然后我们分别查看各种日志等级的日志的输出情况。

默认等级

  // Output with default logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with default logger level:");
  {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 1";
    test_node->get_pub()->publish(std::move(msg));
  }
  std::this_thread::sleep_for(200ms);

可以看到,默认等级时,DEBUG级别日志是不输出的。INFO、WARN、ERROR级别日志会输出。
在这里插入图片描述

Debug等级

  RCLCPP_INFO(test_node->get_logger(), "Output with debug logger level:");
  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Debug)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 2";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set debug logger level via logger service !");
  }

可以看到,DEBUG等级时,所有级别的日志都会输出。
在这里插入图片描述

Warn等级

  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Warn)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 3";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set warn logger level via logger service !");
  }

可以看到,WARN等级时,INFO和DEBUG级别日志不会输出,但是WARN、ERROR级别日志会输出。
在这里插入图片描述

Error等级

  // Output with error logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with error logger level:");
  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Error)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 4";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set error logger level via logger service !");
  }

可以看到,ERROR等级时,只有ERROR级别日志会输出,而INFO、DEBUG、WARN级别日志不会输出。
在这里插入图片描述

完整代码

// Copyright 2023 Sony Group Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <chrono>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"

#include "std_msgs/msg/string.hpp"

#include "rcl_interfaces/srv/get_logger_levels.hpp"
#include "rcl_interfaces/srv/set_logger_levels.hpp"

using namespace std::chrono_literals;

// This demo program shows how to enable logger service and control logger level via logger service.
// Class LoggerServiceNode enable logger service and create a subscription. The callback of
// subscription output received message by different log functions.
// Class TestNode can set/get logger level of LoggerServiceNode and send message to it.

class LoggerServiceNode : public rclcpp::Node
{
public:
  explicit LoggerServiceNode(const std::string & node_name)
  : Node(node_name, rclcpp::NodeOptions().enable_logger_service(true))
  {
    auto callback = [this](std_msgs::msg::String::ConstSharedPtr msg)-> void {
        RCLCPP_DEBUG(this->get_logger(), "%s with DEBUG logger level.", msg->data.c_str());
        RCLCPP_INFO(this->get_logger(), "%s with INFO logger level.", msg->data.c_str());
        RCLCPP_WARN(this->get_logger(), "%s with WARN logger level.", msg->data.c_str());
        RCLCPP_ERROR(this->get_logger(), "%s with ERROR logger level.", msg->data.c_str());
      };

    sub_ = this->create_subscription<std_msgs::msg::String>("output", 10, callback);
  }

private:
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;
};

class TestNode : public rclcpp::Node
{
public:
  explicit TestNode(const std::string & remote_node_name)
  : Node("TestNode"),
    remote_node_name_(remote_node_name)
  {
    pub_ = this->create_publisher<std_msgs::msg::String>("output", 10);
    logger_set_client_ = this->create_client<rcl_interfaces::srv::SetLoggerLevels>(
      remote_node_name + "/set_logger_levels");
    logger_get_client_ = this->create_client<rcl_interfaces::srv::GetLoggerLevels>(
      remote_node_name + "/get_logger_levels");
  }

  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr get_pub()
  {
    return pub_;
  }

  bool set_logger_level_on_remote_node(
    rclcpp::Logger::Level logger_level)
  {
    if (!logger_set_client_->wait_for_service(2s)) {
      return false;
    }

    auto request = std::make_shared<rcl_interfaces::srv::SetLoggerLevels::Request>();
    auto set_logger_level = rcl_interfaces::msg::LoggerLevel();
    set_logger_level.name = remote_node_name_;
    set_logger_level.level = static_cast<uint32_t>(logger_level);
    request->levels.emplace_back(set_logger_level);

    auto result = logger_set_client_->async_send_request(request);

    if (rclcpp::spin_until_future_complete(this->shared_from_this(), result) !=
      rclcpp::FutureReturnCode::SUCCESS)
    {
      return false;
    }

    auto ret_result = result.get();
    if (!ret_result->results[0].successful) {
      RCLCPP_ERROR(
        this->get_logger(), "Failed to change logger level: %s",
        ret_result->results[0].reason.c_str());
      return false;
    }
    return true;
  }

  bool get_logger_level_on_remote_node(uint32_t & level)
  {
    if (!logger_get_client_->wait_for_service(2s)) {
      return false;
    }

    auto request = std::make_shared<rcl_interfaces::srv::GetLoggerLevels::Request>();
    request->names.emplace_back(remote_node_name_);
    auto result = logger_get_client_->async_send_request(request);
    if (rclcpp::spin_until_future_complete(shared_from_this(), result) !=
      rclcpp::FutureReturnCode::SUCCESS)
    {
      return false;
    }

    auto ret_result = result.get();
    level = ret_result->levels[0].level;
    return true;
  }

private:
  const std::string remote_node_name_;
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
  rclcpp::Client<rcl_interfaces::srv::SetLoggerLevels>::SharedPtr logger_set_client_;
  rclcpp::Client<rcl_interfaces::srv::GetLoggerLevels>::SharedPtr logger_get_client_;
};


int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);

  const std::string node_name = "LoggerServiceNode";
  auto logger_service_node = std::make_shared<LoggerServiceNode>(
    node_name);
  auto test_node = std::make_shared<TestNode>(node_name);

  rclcpp::executors::SingleThreadedExecutor executor;

  executor.add_node(logger_service_node);

  std::thread thread([&executor]() {
      executor.spin();
    });

  auto get_logger_level_func = [&test_node] {
      uint32_t get_logger_level = 0;
      if (test_node->get_logger_level_on_remote_node(get_logger_level)) {
        RCLCPP_INFO(test_node->get_logger(), "Current logger level: %u", get_logger_level);
      } else {
        RCLCPP_ERROR(
          test_node->get_logger(),
          "Failed to get logger level via logger service !");
      }
    };

  // Output with default logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with default logger level:");
  {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 1";
    test_node->get_pub()->publish(std::move(msg));
  }
  std::this_thread::sleep_for(200ms);

  // Get logger level. Logger level should be 0 (Unset)
  get_logger_level_func();

  // Output with debug logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with debug logger level:");
  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Debug)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 2";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set debug logger level via logger service !");
  }

  // Get logger level. Logger level should be 10 (Debug)
  get_logger_level_func();

  // Output with warn logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with warn logger level:");
  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Warn)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 3";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set warn logger level via logger service !");
  }

  // Get logger level. Logger level should be 30 (Warn)
  get_logger_level_func();

  // Output with error logger level
  RCLCPP_INFO(test_node->get_logger(), "Output with error logger level:");
  if (test_node->set_logger_level_on_remote_node(rclcpp::Logger::Level::Error)) {
    auto msg = std::make_unique<std_msgs::msg::String>();
    msg->data = "Output 4";
    test_node->get_pub()->publish(std::move(msg));
    std::this_thread::sleep_for(200ms);
  } else {
    RCLCPP_ERROR(test_node->get_logger(), "Failed to set error logger level via logger service !");
  }

  // Get logger level. Logger level should be 40 (Error)
  get_logger_level_func();

  executor.cancel();
  if (thread.joinable()) {
    thread.join();
  }

  rclcpp::shutdown();
  return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1958128.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Windows Docker desktop搭建pwn环境

安装虚拟机很重&#xff0c;占空间&#xff0c;影响速度。 今天试了下用Windows下的Docker搭建pwn做题环境&#xff0c;搭配MobaXterm真的很好! 一、Windows下安装Docker desktop 网上参考文章很多&#xff0c;不赘述。 说明&#xff1a;去https://www.docker.com/products/d…

PHP经销商订货管理系统小程序源码

经销商订货管理系统&#xff1a;重塑供应链效率的利器 &#x1f680; 开篇&#xff1a;解锁供应链管理的新纪元 在竞争激烈的商业环境中&#xff0c;经销商作为供应链的关键一环&#xff0c;其订货效率直接影响到整个供应链的流畅度和响应速度。传统的订货方式往往繁琐、易出…

【C++深度探索】深入解析AVL树的底层实现机制

&#x1f525; 个人主页&#xff1a;大耳朵土土垚 &#x1f525; 所属专栏&#xff1a;C从入门至进阶 这里将会不定期更新有关C/C的内容&#xff0c;欢迎大家点赞&#xff0c;收藏&#xff0c;评论&#x1f973;&#x1f973;&#x1f389;&#x1f389;&#x1f389; 前言 AV…

学习大数据DAY26 简单数据清洗练习和 Shell 脚本中的数据库编程

目录 上机练习 14 mysql 命令 sql 语句实现步骤 shell 脚本导入 csv 格式文件到 mysql 数据库 secure-file-priv 特性 把文件拷贝到 mysql 指定目录下 上机练习 15 mysqldump 命令 上机练习 16 上机练习 14 运用上一节课学的 Shell 工具完成 1. 清洗数据《infotest.t…

黑马头条Day12-项目部署_持续集成

一、今日内容介绍 1. 什么是持续集成 持续集成&#xff08;Continuous integration&#xff0c;简称CI&#xff09;&#xff0c;指的是频繁地&#xff08;一天多次&#xff09;将代码集成到主干。 持续集成的组成要素&#xff1a; 一个自动构建过程&#xff0c;从检出代码、…

Markdown 语法大全详解

Markdown 语法大全详解 Markdown是一种轻量级标记语言&#xff0c;排版语法简洁&#xff0c;让人们更多地关注内容本身而非排版。它使用易读易写的纯文本格式编写文档&#xff0c;可与HTML混编&#xff0c;可导出 HTML、PDF 以及本身的 .md 格式的文件。因简洁、高效、易读、易…

langchain 入门指南 - 实现一个多模态 chatbot

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 在前面的文章中&#xff0c;我们学会了如何通过 langchain 实现本地文档库的 QA&#xff0c;又或者通过 langchain 来实现对话式的问答系…

2024下半年,前端的技术风口来了

“ 你近期有体验过哪些大模型产品呢&#xff1f; 你有使用大模型API做过一些实际开发吗&#xff1f; 在你日常开发中&#xff0c;可以与大模型相关应用结合来完成工作吗&#xff1f; ” **最近&#xff0c;一直在和同事聊&#xff0c;关于前端可以用大模型干点啥&#xff…

Vue2和Vue3实战代码中的小差异(实时更新)

目录 前言1. 未使用自闭合标签2. 事件名连字符3. 换行符4. 弃用.sync5. 弃用slot 前言 以下文章实时更新&#xff0c;主打记录差异 1. 未使用自闭合标签 104:7 error Require self-closing on Vue.js custom components (<el-table-column>) vue/html-self-closing✖…

【华为OD机考】2024D卷最全真题【完全原创题解 | 详细考点分类 | 不断更新题目】

可上 欧弟OJ系统 练习华子OD、大厂真题 绿色聊天软件戳 od1441了解算法冲刺训练&#xff08;备注【CSDN】否则不通过&#xff09; 文章目录 相关推荐阅读栈常规栈单调栈 队列&#xff08;题目极少&#xff0c;几乎不考&#xff09;哈希哈希集合哈希表 前缀和双指针同向双指针 贪…

我与C语言二周目邂逅vlog——6.文件操作

1. 为什么使⽤⽂件&#xff1f; 如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失 了&#xff0c;等再次运⾏程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进⾏持久…

从区块链到股票市场的全方位布局,广辉团队创新引领共创财富未来!

广辉团队作为一家涉足互联网投资领域的团队&#xff0c;在短短几年内迅速崛起&#xff0c;成为行业中的佼佼者。这支团队汇聚了来自各行各业的商业精英&#xff0c;并在互联网金融领域创造了巨大的财富。业务范畴涵盖了资产管理、资本市场、消费金融、保险市场、零售银行及财富…

SSM项目实战

项目实战一 这里实战的是我Javaweb项目实战&#xff08;后端篇&#xff09;的改写 Javaweb项目实战用到的技术是servletvue3 这里用到的是springspringmvcmybatisvue3 项目结构 步骤一:导入需要依赖 <!--mybatis核心--><dependency><groupId>org.mybatis<…

Intel12代处理器在虚拟机中安装Windows98SE

最近想把以前写的那个Windows98开始菜单完善一下&#xff0c;装个Windows98来参考参考。 项目地址&#xff1a;GitHub - zhaotianff/WindowsX: windows toolsets 路过的小伙伴可以帮忙点个star。 这里把安装过程分享一下。 本文以VMware17虚拟机为例&#xff0c;介绍如何在1…

阿里玄铁处理器涉及的相关技术居然有PHP

其实跟PHP没啥关系&#xff0c;也可以说有点关系 指令集说明&#xff1a; RISC-V 指令集是由美国加州大学伯克利分校&#xff08;University of California, Berkeley&#xff09;的研究人员开发的。该项目主要由Krste Asanović教授领导&#xff0c;并且得到了计算机体系结构…

Java面试八股之JDK 动态代理和 CGLIB 动态代理的区别

JDK 动态代理和 CGLIB 动态代理的区别 JDK 动态代理和 CGLIB 动态代理都是在 Java 中实现动态代理的两种常见方式。它们各自有不同的特点和适用场景。下面详细介绍一下这两种动态代理的区别&#xff1a; 1. 代理机制 JDK 动态代理: 实现原理: JDK 动态代理基于 Java 的反射…

微信小程序开发中如何通过正确的步骤和调试方法来解决问题

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[2435024119@qq.com] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? 专栏导…

Docker容器基础篇

一.Docker容器简要介绍 Docker 是一个开源项目&#xff0c;旨在提供轻量级的应用容器化解决方案。它允许开发者打包应用及其所有依赖项到一个标准化的单元中&#xff0c;称为容器。这些容器可以在开发人员的工作环境中构建&#xff0c;然后轻松地在不同的计算机、服务器或云平…

论文阅读:面向自动驾驶场景的多目标点云检测算法

论文地址:面向自动驾驶场景的多目标点云检测算法 概要 点云在自动驾驶系统中的三维目标检测是关键技术之一。目前主流的基于体素的无锚框检测算法通常采用复杂的二阶段修正模块,虽然在算法性能上有所提升,但往往伴随着较大的延迟。单阶段无锚框点云检测算法简化了检测流程,…

Linux安装vmware tools(vmware tools软件包来源ESXI8.0.3)

一、默认正常安装(也可以下载文章顶部资源上传linux服务器解压安装&#xff0c;免去挂载光驱的步骤) ##挂载cdrom到/mnt目录 [rootlocalhost /]# mount /dev/cdrom /mnt mount: /dev/sr0 is write-protected, mounting read-only [rootlocalhost /]# ##切换至/mnt目录 [rootlo…