Linux小课堂: 软件安装机制深度解析之以 CentOS 为例的 RPM 包管理与 YUM 工具详解

从 Windows 到 Linux 的软件安装范式转变

在传统 Windows 系统中,用户安装软件通常依赖于 .exe.msi 格式的可执行安装程序

这一过程往往包括以下步骤:

  • 在搜索引擎中查找目标软件
  • 进入官网或第三方下载站
  • 下载安装包(可能附带捆绑软件或广告)
  • 双击运行,按向导一步步完成安装

这种方式存在诸多问题:安全性低、易被植入恶意程序、依赖关系需手动解决(如未安装 Java 环境导致 Eclipse 无法启动)、更新困难等

相比之下,Linux 系统,特别是基于 Red Hat 家族的发行版(如 CentOS),采用了一种更为高效、安全和高度自动化的软件管理机制。其核心在于 软件包管理系统 与 集中式软件仓库(Repository) 的结合使用,其核心优势在于两个关键设计:

  • 软件包的依赖关系自动管理
  • 集中化的软件仓库(Repository)体系

重点强调:

  • Linux 软件安装的本质是"声明需求",而非"手动操作"
  • 系统会自动处理依赖关系、版本兼容性及远程获取,极大提升了安全性与效率

软件包格式与依赖管理机制详解

1 ) 软件包(Package)的概念

1.1 RPM:Red Hat Package Manager 的本质

在 Red Hat 系列发行版(如 RHEL、Fedora、CentOS)中,软件以 .rpm 文件形式分发,全称为 RPM Package Manager(原意为 Red Hat Package Manager),是一种二进制打包格式。它封装了以下内容:

  • 软件的所有可执行文件、配置文件和资源
  • 安装/卸载脚本(pre-install, post-install 等)
  • 元数据信息(版本号、作者、描述、依赖项列表)

示例路径:http://mirrors.aliyun.com/centos/7/os/x86_64/Packages/httpd-2.4.6-97.el7.centos.x86_64.rpm

.rpm 并非简单的压缩包,而是带有元数据和安装逻辑的结构化包体,支持数字签名验证完整性与来源可信性。

1.2 与其他发行版的对比:DEB vs RPM

发行家族 包格式 包管理器 示例系统
Debian 系 .deb apt, dpkg Ubuntu, Debian
Red Hat 系 .rpm yum, dnf, rpm CentOS, RHEL, Fedora

两者功能相似,但底层工具链不同,不能直接互用。

2 ) 依赖关系(Dependency)机制

几乎所有的现代软件都不是孤立运行的。它们依赖其他库文件(libraries)或工具来实现特定功能

示例说明

  • 图像编辑软件 GIMP(GNU Image Manipulation Program)需要图像解码库(如 libjpeg, libpng)才能打开 JPG/PNG 文件。
  • 开发环境 Eclipse 需要 JRE/JDK 才能运行

在 Windows 中,这些依赖常需用户自行安装;而在 Linux 中,每个 .rpm 包都内嵌了依赖声明信息

这一机制由 RPM 包内的元数据驱动,确保所有前置条件满足后才进行安装

RPM 包管理系统如何工作?

示例

bash 复制代码
当你执行:
yum install httpd
 
YUM 会自动分析:
→ httpd 依赖于 apr, apr-util, pcre, systemd
→ apr 又依赖于 glibc
→ 所有缺失的包将被一同下载并安装

当用户请求安装某软件时:

  1. 包管理器读取该 .rpm 包的元数据
  2. 解析其声明的所有依赖项
  3. 自动从配置好的软件仓库中查找并安装缺失的依赖
  4. 若存在冲突或版本不匹配,则提示错误

技术细节凝练总结:

  • 依赖关系形成"依赖树",可能涉及多层间接依赖
  • 包管理器通过 SAT 求解器进行依赖解析,确保一致性
  • 用户无需关心底层细节,只需表达"我要安装什么"

软件仓库(Repository)架构设计原理

1 )什么是 Repository?

软件仓库(Repository) 是集中存放 .rpm 软件包及其元数据的服务器集合,相当于一个"官方认证的应用商店"。

  • 所有合法软件均经过签名验证;
  • 提供统一的索引文件(metadata)供客户端查询;
  • 支持增量更新与缓存机制。

与 Windows 的对比

特性 Windows(传统模式) Linux(YUM/RPM 模式)
分布方式 分散下载 统一仓库
来源可信度 不确定(官网/盗版站混杂) GPG 签名验证
更新机制 手动检查 yum check-update 自动检测
依赖处理 用户负责 系统全自动解决
软件搜索 浏览网页 yum search <keyword> 快速查找

2 ) 镜像站点(Mirror Site)与全球分发网络

若所有 CentOS 用户都访问同一台中央服务器,势必造成网络拥堵甚至宕机。为此,Linux 社区采用了 分布式镜像架构

镜像工作机制

  • 主站(如 http://mirror.centos.org/)定期同步
  • 各国高校、企业(如阿里云、网易、清华)建立本地镜像
  • 用户选择地理上最近的镜像源,提升下载速度

国内常用 CentOS 镜像地址:

urls 复制代码
# 阿里云镜像
http://mirrors.aliyun.com/centos/

# 网易镜像
http://mirrors.163.com/centos/

# 清华大学 TUNA 协会
https://mirrors.tuna.tsinghua.edu.cn/centos/

# 华为云镜像
https://mirrors.huaweicloud.com/centos/

深层机制说明:每个 Repository 提供 repodata/ 目录,内含 XML 格式的元数据(如 primary.xml.gz),描述所有包名、版本、依赖、校验和等信息。YUM 在更新前会拉取这些数据生成本地缓存,确保操作精准无误

切换 CentOS 软件源至国内镜像

默认情况下,CentOS 使用官方源,但国内访问较慢。我们可通过修改 YUM 配置文件切换为高速镜像。

1 ) 关键配置文件路径

bash 复制代码
/etc/yum.repos.d/CentOS-Base.repo

此文件定义了多个软件仓库类别(base, updates, extras 等),每类对应一组 baseurl

YUM 的仓库定义位于 /etc/yum.repos.d/ 目录下,每个 .repo 文件对应一组仓库源。默认使用官方源,但国内访问较慢

2 ) 操作步骤详解(以阿里云为例)

2.1 备份原始配置文件

bash 复制代码
sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 

重要原则:任何系统级修改前必须备份!

2.2 下载阿里云提供的 repo 配置

根据 CentOS 版本选择对应链接。假设使用 CentOS 7:

bash 复制代码
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo 

bash 复制代码
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo \
https://mirrors.aliyun.com/repo/Centos-7.repo

参数说明:

  • -O:指定输出文件名,,-o 同理。两者均可用于重定向下载内容
  • URL 来自 阿里云开源镜像站
  • 注意版本匹配(这里为 CentOS 7)

整理来说

bash 复制代码
# 1. 切换到 root 用户(如尚未登录)
su -
 
# 2. 进入仓库配置目录
cd /etc/yum.repos.d 
 
# 3. 备份原始配置(防止出错可恢复)
mv CentOS-Base.repo CentOS-Base.repo.backup 
 
# 4. 下载阿里云提供的 repo 配置文件
wget -O /etc/yum.repos.d/CentOS-Base.repo \
     https://mirrors.aliyun.com/repo/Centos-7.repo

2.3 清理旧缓存并生成新缓存

bash 复制代码
# 清除旧的元数据缓存
sudo yum clean all
 
# 重建本地缓存(从新源拉取 metadata)
sudo yum makecache

makecache 命令将远程仓库的 repomd.xml 等元数据下载到本地 /var/cache/yum/ 目录,后续操作不再重复联网查询

原理剖析:

  • yum clean all 删除 /var/cache/yum 下所有临时文件
  • yum makecache 主动从当前配置的 repository 下载 repomd.xmlprimary.xml,建立本地索引,提升后续查询效率

2.4 验证源是否生效

bash 复制代码
grep '^baseurl' /etc/yum.repos.d/CentOS-Base.repo

bash 复制代码
# 查看当前启用的仓库列表 
yum repolist enabled

# 输出示例应显示 base, updates, extras 等源来自 mirrors.aliyun.com

# 检查 base repo 的 URL 是否已变更为阿里云
cat /etc/yum.repos.d/CentOS-Base.repo | grep baseurl

输出应类似:

ss 复制代码
baseurl=http://mirrors.aliyun.com/centos/7/os/x86_64/
baseurl=http://mirrors.aliyun.com/centos/7/updates/x86_64/
...

确认域名已替换为 mirrors.aliyun.com

3 )完整 Shell 脚本封装

bash 复制代码
#!/bin/bash

# 切换 CentOS 7 软件源至阿里云镜像 
REPO_FILE="/etc/yum.repos.d/CentOS-Base.repo"
BACKUP_FILE="${REPO_FILE}.backup"
 
echo "正在备份原配置文件..."
if [ -f "$REPO_FILE" ]; then 
    cp "$REPO_FILE" "$BACKUP_FILE"
    echo "✅ 备份成功: $BACKUP_FILE"
else 
    echo "❌ 错误:找不到 $REPO_FILE"
    exit 1
fi
 
echo "正在下载阿里云 repo 配置..."
if wget -q -O "$REPO_FILE" "https://mirrors.aliyun.com/repo/Centos-7.repo"; then
    echo "✅ 下载成功"
else
    echo "❌ 下载失败,请检查网络连接"
    exit 1
fi
 
echo "正在清理并重建 YUM 缓存..."
yum clean all && yum makecache
 
if [ $? -eq 0 ]; then
    echo "🎉 成功切换至阿里云镜像源!"
else
    echo "⚠️ 缓存生成失败,请检查 DNS 或网络设置"
    exit 1 
fi

此脚本可用于自动化部署场景,集成进初始化流程

YUM 包管理命令体系与高级用法

1 ) 核心命令概览

命令 功能说明
yum update 更新所有已安装软件包(保留旧包)
yum upgrade 升级所有软件包(删除旧包)
yum search <keyword> 搜索可用软件包
yum install <package> 安装指定软件包
yum remove <package> 删除指定软件包
yum autoremove 移除无依赖的孤儿包
yum list installed 列出已安装包
yum info <package> 查看包详细信息

2 ) updateupgrade 的深层差异分析

虽然两者在大多数场景下效果一致,但关键区别如下:

特性 yum update yum upgrade
是否删除旧包
是否处理废弃包(obsoletes) 默认开启 显式启用
推荐用途 日常补丁更新 大版本升级

官方文档解释摘录:

"upgrade is equivalent to update with the '--obsoletes' flag enabled."

------ Red Hat Enterprise Linux System Administrator's Guide

因此,在生产环境中建议优先使用 yum update,避免意外移除仍在使用的组件

示例:安装文本编辑器 emacs 并验证

bash 复制代码
搜索是否存在 emacs 包
yum search emacs
 
安装 emacs 编辑器 
yum install emacs -y

输出示例:
--> Resolving Dependencies
--> Running Transaction Check
--> Processing Dependency: libXpm.so.4 for package: emacs-xx.x-x.el7.x86_64
--> Installing: libXpm x86_64 ...
--> Installing: emacs x86_64 ...
Total size: 21 MB
Is this ok [y/N]: y
 
验证是否安装成功 
rpm -q emacs 

输出示例:emacs-24.3-23.el7.x86_64 表示安装成功

系统自动解析并安装所有依赖项,无需人工干预

3 ) 图形化包管理器 vs 命令行工具

CentOS 提供图形界面的"Software"中心(GNOME Software),可通过以下入口启动:

  • Applications → Utilities → Software
  • 或终端输入:gnome-software

局限性:

  • UI 设计陈旧,分类混乱;
  • 不支持批量操作;
  • 无法查看依赖详情;
  • 无法编写脚本自动化。

优势推荐:

始终优先使用命令行工具(YUM/DNF),因其具备:

  • 更强的可控性;
  • 可审计的操作日志(记录于 /var/log/yum.log);
  • 易于集成 CI/CD 流程;
  • 支持静默安装(headless mode)。

功能包括:

  • 浏览分类(音视频、开发工具、办公软件等)
  • 一键安装/卸载
  • 查看已安装程序列表
  • 检查系统更新

但其界面体验较差,且更新滞后,建议优先使用命令行方式

4 ) 本地 RPM 包的安装与管理

对于未收录在仓库中的软件(如私有项目、闭源驱动),可手动下载 .rpm 文件进行安装

两种安装方式对比

方法 命令示例 是否自动解决依赖
使用 rpm 命令 rpm -ivh package-name.rpm ❌ 不处理依赖
使用 yum localinstall yum localinstall ./package-name.rpm -y ✅ 自动解析并安装依赖
  • -i: install
  • -v: verbose
  • -h: hash marks progress

强烈推荐后者,避免因缺少依赖导致"半安装"状态

rpm 示例

bash 复制代码
# 卸载已安装的 rpm 包
rpm -e package-name
 
# 查询某文件属于哪个包
rpm -qf /path/to/file
 
# 列出某包安装的所有文件
rpm -ql package-name

注意:

  • rpm 不解决依赖问题!若缺少依赖,安装将失败
  • yum localinstall 此命令会先分析本地包的依赖需求,再从配置好的 repository 中自动下载补齐,大幅提升成功率

NestJS + TypeScript 实现简易 YUM API 模拟服务

1 )方案1

以下是一个基于 NestJS 和 TypeScript 构建的简易"YUM 仓库查询接口"模拟服务,用于演示依赖解析和服务端响应结构。

项目结构概览

tree 复制代码
yum-simulator/
├── src/
│   ├── app.controller.ts
│   ├── app.service.ts
│   ├── package.entity.ts
│   └── dependency.resolver.ts
├── package.json
└── nest-cli.json

软件包实体定义

typescript 复制代码
// src/package.entity.ts
export class Package {
  constructor(
    public name: string,
    public version: string,
    public arch: string = 'x86_64',
    public size: number,
    public description: string,
    public dependencies: string[] = []
  ) {}
 
  get fullName(): string {
    return `${this.name}-${this.version}.${this.arch}.rpm`;
  }
}

模拟数据库服务

typescript 复制代码
// src/app.service.ts
import { Injectable } from '@nestjs/common';
import { Package } from './package.entity';
 
@Injectable()
export class AppService {
  private packages: Map<string, Package> = new Map();
 
  constructor() {
    this.initMockData();
  }
 
  private initMockData() {
    const gimp = new Package('gimp', '2.8.22', 'x86_64', 256000000, 'GNU Image Manipulation Program', [
      'libjpeg',
      'libpng',
      'gegl',
      'babl'
    ]);
 
    const libjpeg = new Package('libjpeg', '1.5.3', 'x86_64', 1200000, 'JPEG Image Library', []);
    const libpng = new Package('libpng', '1.6.37', 'x86_64', 1500000, 'PNG Image Library', []);
    const gedit = new Package('gedit', '3.38.1', 'x86_64', 8000000, 'GNOME Text Editor', ['gtk3']);
 
    this.packages.set(gimp.name, gimp);
    this.packages.set(libjpeg.name, libjpeg);
    this.packages.set(libpng.name, libpng);
    this.packages.set(gedit.name, gedit);
  }
 
  findAll(): Package[] {
    return Array.from(this.packages.values());
  }
 
  findByName(name: string): Package | undefined {
    return this.packages.get(name);
  }
 
  search(keyword: string): Package[] {
    return this.findAll().filter(pkg =>
      pkg.name.includes(keyword) ||
      pkg.description.toLowerCase().includes(keyword.toLowerCase())
    );
  }
 
  resolveDependencies(target: Package): Package[] {
    const resolved: Package[] = [];
    const visited = new Set<string>();
 
    const traverse = (pkgName: string) => {
      if (visited.has(pkgName)) return;
      const pkg = this.findByName(pkgName);
      if (!pkg) return;
 
      visited.add(pkgName);
      for (const dep of pkg.dependencies) {
        traverse(dep);
      }
      resolved.push(pkg);
    };
 
    traverse(target.name);
    return resolved;
  }
}

控制器提供 REST 接口

typescript 复制代码
// src/app.controller.ts
import { Controller, Get, Param, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { Package } from './package.entity';
 
@Controller('api/yum')
export class AppController {
  constructor(private readonly appService: AppService) {}
 
  @Get('packages')
  getAllPackages(): Package[] {
    return this.appService.findAll();
  }
 
  @Get('search')
  searchPackages(@Query('q') keyword: string): Package[] {
    return this.appService.search(keyword);
  }
 
  @Get('info/:name')
  getPackageInfo(@Param('name') name: string): any {
    const pkg = this.appService.findByName(name);
    if (!pkg) {
      return { error: 'Package not found' };
    }
    return pkg;
  }
 
  @Get('deplist/:name')
  getDependencyTree(@Param('name') name: string): any {
    const pkg = this.appService.findByName(name);
    if (!pkg) {
      return { error: 'Package not found' };
    }
    const deps = this.appService.resolveDependencies(pkg);
    return {
      target: pkg.fullName,
      total_dependencies: deps.length,
      resolution_order: deps.map(p => p.fullName)
    };
  }
}

使用示例(cURL 测试)

bash 复制代码
搜索包含 "gim" 的包 
curl "http://localhost:3000/api/yum/search?q=gim"
 
获取 gimp 的依赖树
curl "http://localhost:3000/api/yum/deplist/gimp"

输出示例:

json 复制代码
{
  "target": "gimp-2.8.22.x86_64.rpm",
  "total_dependencies": 4,
  "resolution_order": [
    "libjpeg-1.5.3.x86_64.rpm",
    "libpng-1.6.37.x86_64.rpm",
    "gegl-0.4.32.x86_64.rpm",
    "babl-0.1.88.x86_64.rpm",
    "gimp-2.8.22.x86_64.rpm"
  ]
}

2 ) 方案2

虽然实际系统级操作仍需 Shell 命令完成,但可通过 Node.js 模拟部分逻辑,加深对包管理机制的理解。

typescript 复制代码
// yum-simulator.service.ts
import { Injectable } from '@nestjs/common';
import * as https from 'https';
import * as fs from 'fs';
 
interface PackageMetadata {
  name: string;
  version: string;
  arch: string;
  requires: string[];
}
 
@Injectable()
export class YumSimulatorService {
  private readonly repoBaseUrl = 'https://mirrors.aliyun.com/centos/7/os/x86_64/repodata/';
  private readonly primaryXmlGz = 'primary.xml.gz';
  private cacheDir = '/tmp/yum-cache';
 
  async fetchRepositoryMetadata(): Promise<string> {
    const url = this.repoBaseUrl + this.primaryXmlGz;
 
    return new Promise((resolve, reject) => {
      https.get(url, (res) => {
        let data = '';
        res.on('data', chunk => data += chunk);
        res.on('end', () => {
          fs.writeFileSync(`${this.cacheDir}/primary.xml.gz`, data);
          resolve(data);
          console.log('✅ 元数据已下载');
        });
      }).on('error', reject);
    });
  }
 
  parsePackagesFromXml(xmlContent: string): PackageMetadata[] {
    // 简化版解析逻辑(实际需使用 sax 或 xml2js)
    const packages: PackageMetadata[] = [];
    // 此处省略完整 XML 解析代码,示意结构即可 
    return packages.map(pkg => ({
      name: pkg['name'],
      version: pkg['version'],
      arch: pkg['arch'],
      requires: pkg['requires']?.split(',') || [],
    }));
  }
 
  async resolveDependencies(targetPackage: string, allPackages: PackageMetadata[]): Promise<string[]> {
    const needed: Set<string> = new Set();
    const stack = [targetPackage];
 
    while (stack.length > 0) {
      const current = stack.pop();
      if (!current || needed.has(current)) continue;
 
      needed.add(current);
 
      const pkg = allPackages.find(p => p.name === current);
      if (pkg && pkg.requires) {
        pkg.requires.forEach(dep => {
          if (!needed.has(dep)) {
            stack.push(dep);
          }
        });
      }
    }
 
    return Array.from(needed);
  }
 
  async simulateInstall(packageName: string): Promise<void> {
    console.log(`🔍 开始模拟安装 ${packageName}...`);
 
    await this.fetchRepositoryMetadata();
    // 假设已完成 XML 解析
    const metadata = this.parsePackagesFromXml('');
    const dependencies = await this.resolveDependencies(packageName, metadata);
 
    console.log(`📦 需要安装的依赖链:`, dependencies.join(' → '));
    console.log('🎉 模拟安装完成!');
  }
}

代码说明:

  • 模拟了 yum makecache 的元数据拉取过程
  • 实现了依赖递归解析算法(拓扑排序思想)
  • 展示了现代语言如何抽象传统系统管理逻辑

3 )方案3

虽然 Node.js 不直接操作 YUM,但在 DevOps 自动化场景中,可通过 child_process 调用系统命令实现集成控制。以下是一个模拟的管理模块设计:

typescript 复制代码
// yum.service.ts
import { Injectable } from '@nestjs/common';
import { execSync } from 'child_process';
 
interface YumResult {
  success: boolean;
  output: string;
  error?: string;
}
 
@Injectable()
export class YumService {
  /
   * 执行 YUM 命令并返回结果
   */
  private execute(command: string): YumResult {
    try {
      const result = execSync(command, { encoding: 'utf-8' });
      return { success: true, output: result };
    } catch (error) {
      return { 
        success: false, 
        output: error.stdout?.toString() || '', 
        error: error.stderr?.toString() || 'Unknown error'
      };
    }
  }
 
  /
   * 更新所有软件包
   */
  updateAll(): YumResult {
    return this.execute('sudo yum update -y');
  }
 
  /
   * 升级系统 
   */
  upgrade(): YumResult {
    return this.execute('sudo yum upgrade -y');
  }
 
  /
   * 安装指定软件包
   */
  install(packageName: string): YumResult {
    return this.execute(`sudo yum install -y ${packageName}`);
  }
 
  /
   * 卸载软件包
   */
  remove(packageName: string): YumResult {
    return this.execute(`sudo yum remove -y ${packageName}`);
  }
 
  /
   * 搜索软件包
   */
  search(keyword: string): YumResult {
    return this.execute(`yum search ${keyword}`);
  }
 
  /
   * 清理缓存并重建
   */
  refreshCache(): YumResult {
    this.execute('yum clean all');
    return this.execute('yum makecache');
  }
 
  /
   * 列出已安装软件 
   */
  listInstalled(): YumResult {
    return this.execute('yum list installed');
  }
 
  /
   * 获取软件详情
   */
  info(packageName: string): YumResult {
    return this.execute(`yum info ${packageName}`);
  }
}

使用示例(Controller 层)

typescript 复制代码
// yum.controller.ts
import { Controller, Get, Post, Param, Query } from '@nestjs/common';
import { YumService } from './yum.service';
 
@Controller('yum')
export class YumController {
  constructor(private readonly yumService: YumService) {}
 
  @Get('update')
  update() {
    return this.yumService.updateAll();
  }
 
  @Post('install/:pkg')
  install(@Param('pkg') pkg: string) {
    return this.yumService.install(pkg);
  }
 
  @Get('search')
  search(@Query('q') keyword: string) {
    return this.yumService.search(keyword);
  }
 
  @Get('refresh')
  refresh() {
    return this.yumService.refreshCache();
  }
}

此模块可用于构建内部运维平台 API,实现远程批量服务器软件管理

为何 Linux 软件管理更具工程价值

维度 Windows 传统方式 Linux(YUM/RPM)
安装流程 手动下载、点击安装 命令行一键触发
依赖处理 用户自行排查 自动解析并补全
来源可信性 不确定(第三方网站) GPG 签名验证
批量管理 困难 支持脚本化、Ansible 集成
审计能力 rpm -qa 可列出全部已装包
回滚支持 依赖卸载程序 yum history undo 可撤销操作

Linux 的包管理系统不仅是工具,更是一种可编程、可审计、可复制的基础设施范式,特别适用于 DevOps、云计算、大规模集群运维等高要求场景

核心要点提炼

  1. 软件包 ≠ 安装程序
    .rpm 是标准化的二进制分发格式,其安装由包管理器统一调度,而非直接执行
相关推荐
想唱rap1 分钟前
Linux开发工具(4)
linux·运维·服务器·开发语言·算法
weixin_537765806 分钟前
【Nginx优化】性能调优与安全配置
运维·nginx·安全
robin591136 分钟前
Linux-通过端口转发访问数据库
linux·数据库·adb
视觉AI40 分钟前
如何查看 Linux 下正在运行的 Python 程序是哪一个
linux·人工智能·python
Lisonseekpan1 小时前
为什么国内禁用docker呢?
运维·docker·容器
扣脚大汉在网络1 小时前
如何在centos 中运行arm64程序
linux·运维·centos
lang201509281 小时前
Linux命令行:cat、more、less终极指南
linux·chrome·less
攒钱植发2 小时前
嵌入式Linux——“大扳手”与“小螺丝”:为什么不该用信号量(Semaphore)去模拟“完成量”(Completion)
linux·服务器·c语言
三五度2 小时前
vmware的ubuntu20.04无网络图标
linux·ubuntu
R-G-B3 小时前
【P1】win10安装 Docker教程
运维·docker·容器