Java 这门语言与生俱来的显著特性就是“一次编译，到处运行”，这种功能得益于 JVM 平台的支持，Java 程序通常通过将其打包为 JAR 或 WAR 包，并依赖 JVM 和 Servlet 容器来运行。其底层运行时 JVM 采用 JIT（即时编译）模式来执行程序代码，JVM 会在运行时进行编译优化和动态执行代码，这通常会导致较高的内存占用。这样的好处是采用 JIT 可以热更新和热部署程序，并且 JVM 可以在运行期间对程序进行动态分析，来实时优化程序以达到最好的性能状态。Cloud Native 面临的问题随着云计算的发展，服务端程序的运行和部署方式发生了巨大变化，特别是在云原生（Cloud Native）场景下，服务器程序被打包成一个个容器运行。在这种环境下，传统方式容器化的 Java 程序面临着打包后的镜像文件过大的问题。镜像文件中需要包含 JRE 和各种第三方库，导致镜像体积庞大，占用更多的磁盘空间。目前云计算的计费方式也发生了改变，采用按需分配的实时计费方式，根据程序的内存占用和运行时间计费。这与传统 IDC 自托管式机房中长时间运行服务器程序形成鲜明对比，按需分配的计费方式显然更具性价比，因此优化 Java 程序在容器环境中的运行方式变得尤为重要。Serverless 面临的问题传统的 JVM 工作原理要求程序员先将编写的 Java 程序打包为 JAR 或 WAR 包，这是普通 Java 开发者所能接触到的程序编译和构建阶段。然而当程序运行起来之后，底层的 JVM 会进行许多复杂的操作，包括内存管理、垃圾回收、即时编译等。需要先启动 JRE 中的 JVM 来加载 Tomcat 服务器所需的字节码，再加载 WAR 包中的字节码文件到 Servlet 容器中运行服务，这个过程非常耗时。下图为整个传统 Java 程序从源代码到程序运行各个阶段的工作流程：下图为传统的 Java 运行时执行程序的生命周期，首先应用的开发者将 *.java 源代码编译为 bytecode 和 jar 依赖包通过 maven 构建打包为一个单独的 jar 包。当执行 java -jar xxx.jar 命令时，首先要启动 JRE 中的 JVM 程序，因为 JVM 本身也是一款单独程序对应着 $JAVA_HOME\bin 目录中的 java ，当 JVM 启动之后会在系统中创建一个进程，将要运行的 jar 程序包和 JRE 标准库中 class 文件加载入 JVM 内存中，并且找到 main 方法开始解释执行。当程序运行一段时间之后 JVM 充分获取了 Java 程序的一些执行数据信息，开始对程序进行 JIT 及时编译优化程序，当程序被 JIT 优化之后的程序才能达到和 C/C++ 这种直接编译为 Native Code 程序的性能。在云计算推崇的 Serverless 架构场景下，Java 的 JIT 模式并不适合，Serverless 应用程序需要能够快速启动以响应事件驱动的请求，并且在空闲时自动关机销毁，释放在服务请求期间所使用的内存。然而，这种需求与传统的 Java 程序运行模式背道而驰，使得 Java 在 Serverless 场景中显得不适用，这就是被业界称为的 冷启动 问题。为了解决这一痛点，Oracle Labs 推出一款新的 JDK 产品 GraalVM JDK ，相比传统的 JDK 该产品内置 Java 静态编译工具和一款新的虚拟机，和传统的 JVM 不一样 GraalVM 是支持多语言进行混合编程的虚拟机，这也是它一大亮点。GraalVM 中的静态编译工具不仅是支持将 Java 编译为二进制可执行文件，还支持 JavaScript 和 Python 等其他语言的编译优化功能，本文只关注于 GraalVM 对 Java 程序进行 AOT（Ahead-Of-Time）编译的解决方案。使用 GraalVM 对 Java 程序进行 AOT 编译的解决方案，可以显著缩短启动时间并降低内存占用，使 Java 程序更适合在 Serverless 环境中运行，通过 AOT 编译，编译器生成一个 Native Code 二进制文件，即对应平台架构的机器代码程序。这种方式无需像 JIT 那样依赖于 JVM 运行，在内存和磁盘占用上有显著改善。下图为 AOT 编译工作流程，在编译阶段会程序进行静态分析，通过它内置工具分析 Java 源代码中依赖关系，将其所有依赖和代码执行逻辑都进行提前编译为机器代码，缺点也很明显可能会失去在 JIT 模式下的动态反射功能。如上图，在 AOT 模式下将在运行时的过程放到在程序构建阶段，构建阶段会对 Java 程序执行静态代码分析和依赖可达性分析，将其所有的依赖软件包都编译生产所属平台对应的可执行二进制文件。AOT 编译优化对程序进行 AOT 编译特别适合云应用程序，被 AOT 编译优化之后的程序启动速度足够快，从而缩短启动时间并更直接地水平扩展云服务，内存占用相比 JVM 模式下要少很多，这对于在云中运行的容器初始化的微服务尤其有益。AOT 前提是完全封闭的运行空间，因为它消除了各种代码注入的可能性，例如 2021 年震惊网络的 Log4j 漏洞就是由于利用了 Java 中的动态类加载机制而发生的，在 AOT 编译之后程序就不会发生类似于漏洞。下面这张蜘蛛图详细展示了不同程序运行方式的区别：介于 Java 程序部署平台和主流的容器技术都是基于 Linux 系统，所以这里以 Ubuntu 发行版本作为基础 AOT 编译和运行打包环境。要对 Java 程序进行 AOT 编译的话首先要安装 GraalVM JDK 的发行版本，这个可以去它的官方网站 graalvm.org 上下载和安装，安装步骤和传统基于 OpenJDK 的发行版本步骤一致。在 Linux 中要是使用 GraalVM 提供的 AOT 静态编译功能，首先需要在操作系统中安装一些 C/C++ 编译器的工具链，在 Ubuntu 和其他基于 Debian 的系统中执行下面命令：sudo apt-get install build-essential libz-dev zlib1g-dev其中的 build-essential 是一个用于 C 语言的开发包，包含了 gcc make gdb 和 libc 函数库等很多工具，这些工具和库对于编译 C 和 C++ 程序是必不可少的。libz-dev 和 zlib1g-dev 是用于压缩和解压缩的开发库，这两个软件包提供了开发 zlib 库的头文件和开发工具，允许您在编写 C/C++ 程序时链接和使用 zlib 压缩库，GraalVM 的 AOT 编译依赖于这些工具来构建本地映像。默认情况下，GraalVM 的 AOT 编译在链接时通常使用系统的标准 glibc 库，但在小型嵌入式设备或对内存要求极低的环境中，glibc 并未针对这些场景做出特别优化。相比之下目前有另外一种专有的 musl 标准库实现，musl 被开发出来就是专注在内存受限的设备上表现优异，使用 musl 生产的二进制文件通常比使用 glibc 生产的文件体积更小，所以更推荐直接使用 musl 替代 glibc 去做编译链接优化。在 GraalVM 的 AOT 编译工具中提供自定义选项，允许开发者去自定义使用 musl 去做生成二进制文件优化。部分 Linux 发行版本系统可能没有内置 musl 库需要提前安装，通过源代码安装的方式可以去 musl.libc.org 官方网站下载源代码包编译安装。但这种方式的需要自己手动配置和编译步骤比较繁琐，建议使用下面这种方式，通过别人已经编译好的 x64 位的 x86_64-linux-musl-native 程序，下载压缩包到指定位置就可以使用。基于 musl 编译的前置条件是依赖于 musl 工具链，但在 GraalVM JDK 中并未提供相关的工具链的支持，需要提前下载 x86_64-linux-musl-native 和 zlib 文件，创建一个用于存放 musl 工具链的目录，将其解压到这个目录中，并且将 x86_64-linux-musl-gcc 和 zlib 进行整合编译安装，步骤如下：# 创建一个工具存储目录 mkdir /usr/local/graal-aot-tools # 进入到这个目录 cd /usr/local/graal-aot-tools # 下载 x86_64-linux-musl 工具链 wget http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz # 解压下载到的 musl 工具链包 tar -zxvf x86_64-linux-musl-native.tgz # 复制文件到工作根目录 cp -r x86_64-linux-musl-native/* . # 下载 zlib 依赖 wget https://zlib.net/current/zlib.tar.gz # 解压 zlib 依赖源码包 tar -zxvf zlib.tar.gz通过一条特定 TOOLCHAIN_DIR 环境变量来指定 musl 工具链安装路径，graalvm 的 aot 编译器会通过此变量来找到 x86_64-linux-musl 的位置：export TOOLCHAIN_DIR="/usr/local/graal-aot-tools" export PATH="$TOOLCHAIN_DIR/bin:$PATH" export CC="$TOOLCHAIN_DIR/bin/gcc"最后把 zlib 和 TOOLCHAIN_DIR 整合进行编译安装，进入 zlib 源代码安装目录中执行： ./configure --prefix=$TOOLCHAIN_DIR --static make make install配置完成之后可以通过 x86_64-linux-musl-gcc 命令检查 musl 工具链是否安装成功。如果使用的是 maven 来构建自动化项目，可以使用 pom 文件中配置 AOT 相关构建参数，来自动化来构建二进制文件： <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>${native.maven.plugin.version}</version> <extensions>true</extensions> <executions> <execution> <id>build-native</id> <goals> <goal>build</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <skip>false</skip> <imageName>${bin.file.name}</imageName> <mainClass>${app.main.class}</mainClass> <buildArgs> <buildArg>--gc=G1</buildArg> <buildArg>--static</buildArg> <buildArg>--libc=musl</buildArg> <buildArg>--no-fallback</buildArg> <buildArg>-march=compatibility</buildArg> </buildArgs> </configuration> </plugin>在项目的根目录中执行 mvn native:build 命令就可以调用位于 $JAVA_HOME/bin/native-image 工具执行静态分析并且生成二进制可执行文件。AOT 编译对计算机硬件资源的要求非常高，取决于项目的大小，大型项目编译时需要占用很多 GB 的内存和大量的 CPU 使用率，不过这会使用高性能的 CI/CD 服务器来完成自动化构建工作。通过 x86_64-linux-musl-gcc 和 musl 做链接编译生成的二进制可执行文件，完全使用的是静态链接的方式，从而提高了可移植性。在不同的 x86_64 的 Linux 系统上运行这些静态链接的二进制文件时，不需要任何额外的库，这对于部署和分发二进制文件非常有用。从编译日志输出可以看到，AOT 编译器启用了 PGO: ML-inferred 优化，这个默认在其他平台是不会启动的，只要在 Linux 平台下使用 x86_64-linux-musl-gcc 才会开启的，由于使用的完全静态链接的二进制文件，所以可以使用 upx 针对这个二进制文件进行压缩，压缩之后的二进制文件可以小到 3 MB 大小。这对于将其打包为 docker 容器镜像文件非常有利，意味着不需要任何其他基础镜像，直接打包可执行二进制文件放到容器中执行，基于 scratch 基础镜像打包之后镜像大小和二进制文件大小一致。# 使用一个轻量的基础镜像 FROM scratch # 复制静态链接的二进制文件到根目录 COPY target/vasedb / # 设定工作目录，这是可选的 WORKDIR / # 默认的启动命令 CMD ["/vasedb"]采用 AOT 和 JIT 模式通过这些数据，形成鲜明的对比，优势也展示出来了，以往采用 JIT 模式运行的 Java 被容器化之后镜像大小往往都是上百 MB 大小的。个人认为有 Serverless 更激进优化方案，在 AOT 编译之后程序会内嵌入一个微型的 SubstrateVM 运行时，这个运行时负责 Java 程序在运行阶段一些多线程和内存分配、垃圾回收功能，默认 AOT 编译使用的 serial 垃圾回收器是一种单线程垃圾回收器，适用于单线程应用或有少量线程的应用，另外一个可选的垃圾回收器是 G1 垃圾回收器，G1 用于多核处理器和大内存机器的高性能垃圾回收器。但是在 Serverless 应用场景中往往应用程序运行时间很短，在程序在运行一段时间之后就主动销毁了，这有点类似于在 AOT 编译阶段的编译程序本身步骤，当编译器编译完程序就销毁了，是一种一次性应用程序。熟悉 JVM 多个垃圾回收器实现应该知道其中有一个叫 Epsilon 垃圾回收器，该垃圾回收器不会执行任何垃圾回收操作，它只进行内存分配而不释放内存。所以我个人认为在某些特定的 Serverless 应用场景中完全可以使用 Epsilon 内存分配器的方案，因为部分场景程序运行时长还没有等到 GC 回收器触发的时间点就被底层基础设施销毁了，程序运行所分配的内存会被 OS 主动回收，而不需要执行垃圾回收器进行回收，去掉 SubstrateVM 中的垃圾回收器部分，打包应用程序体积会更小。引用资料文件Oracle (The Art of) (Java) Benchmarking PPTAOT vs. JIT Compilation in JavaGraalVM Native Image 快速入门
[scode type="lblue"]传统的 Linux 安全机制靠着默认的用户权限、所属组、其他组、 ACL 等功能来实现权限控制，而目前还有另外一套安全机制 SELinux 机制来管理系统安全性，相比默认的权限控制 SELinux 的机制更为强制，默认的用户权限是靠着用户或者超级管理员设置和管理，而 SELinux 强制访问控制机制是由操作系统内核强制实施的，从而使系统更加安全。如果开启了 SELinux 每当操作的时候，SELinux 都会检测是否进程系统调用和网络套节字合法，如果不符合 SELinux 默认的权限就会被拒绝和终止。[/scode]SELinux 提供了一种安全的策略语言，管理员可以使用它来定义系统安全策略。这种策略语言是相当复杂的，但是可以通过使用预定义的策略模块进行简化。这些策略模块是一组预定义的规则集，可以保护常见的应用程序和服务，如 Apache Web 服务器、MySQL 数据库等。总的来说，SELinux 可以帮助管理员保护系统免受恶意攻击和安全漏洞的影响，从而提高系统的安全性和稳定性。然而，由于 SELinux 是一种高级的安全机制，其配置和管理需要一定的技能和经验。一般情况下很少自己起编写 SELinux 的安全策略文件，只会临时或者配置 SELinux 默认的安全策略级别信息，SELinux 配置文件路径 /etc/selinux/config 中，目前很多发行版本已经默认启动该功能。级别说明enforcing默认启动，强制实行安全策略，如果不符合要求就会被禁止permissive该级别会提示用户操作不符合安全策略，但是不会拒绝操作disabled直接禁止功能，因此也不会占用系统资源而策略文件存放在 /etc/selinux/targeted/policy 中，是针对系统的安全策略进行定义的文件。SELinux 有两大策：域（domain）的概念允许定义哪些进程可以访问某些文件和目录；上下文 (context) 默认会给每个系统资源文件设置一个标签，而 SELinux 安全功能就要根据策略文件来检测进程和上下文的关系，从而来控制哪些进程可以访问什么资源。# 查看进程域 ps -Z # 查看资源上下文 ls -Z # 查看目录 ls -Zd /xxx/dir # 查看 selinux 状态 sestatus[scode type="yellow"]在 SELinux 上下文包括以下字段： user（用户）、role（角色）、type（类型）和 security level（安全级别），例如有一个策略规则允许 Apache作为 httpd_t 运行的 Web 服务器进程，访问通常位于 /var/www/html/ 和其他 Web 服务器目录中上下文的文件和目录 httpd_sys_content_t ，但是策略中没有允许规则适用于通常位于 /mysql 和 /data/msyql/ 中的文件，因此不允许访问，即使 Apache 被破坏，一个恶意的脚本可以访问它，也无法访问 /data/msyql/ 目录 。[/scode]因为有默认安全规则配置文件的存在，创建新文件或者移动文件修改文件会导致进程域和上下文不相符的情况，SELinux 默认就会拒绝此类操作导致不符合预期的效果，例如把 nginx 默认的网站目录设置为其他目录导致无法正常运行。此时就要使用下面命令修改进程上下文，使得 nginx 能正常访问到文件：semanage fcontext -a -t httpd_sys_content_t /wwwroot semanage fcontext -a -t httpd_sys_content_t /wwwroot/* # 如果是网络描述符资源，例如端口上下文 semange fcontext -a -t http_port_t -p tcp 8080 # 生效刚刚到修改 restorecon -Rv /wwwroot # 查找符合 http 端口 semanage port -l | grep http[scode type="yellow"]SELinux 默认提供多个命令可以协助帮助设置权限和查看 SELinux 目前运行状态和参数信息：[/scode]命 令作用说明setenforce临时修改 SELinux 状态getenforce获取当前系统 SELinux 状态getsebool [-a]查询 SElinux 策略内各项规则的布尔值setsebool [-P]设置策略并且永久生效修改之后 nginx 进程就能成功访问到 /wwwroot 目录中的文件了， SELinux 没有阻挡读取文件的请求，SELinux 修改的日志相信会存储在 /var/log/audit/audit.log 中，下面开始介绍如何使用 journalctl 查看系统日志信息。juornalctl 日志调试上面介绍了 SELinux 在 Linux 起到作用，并且运行过程中出现问题也会被记录到日志中，方便后期维护审查系统。这里要介绍一款日志审查工具 journalctl ，该工具可以快速帮助管理员检索到需要的日志信息，目前 Linux 中日志系统的日志消息级别有：报错 、警告 、提示 、其他 进行标注，分别对应着不同日志文件：文件作用/var/log/boot.log系统开机自检日志和引导日志/var/log/lastlog用户登录成功的时间，终端名称和 IP 地址/var/log/btmp登录失败的用户时间和终端和 IP 地址/var/log/messages系统各个服务的运行和报错信息/var/log/secure系统安全相关的信息/var/log/wtmp系统启动和关机相关的信息[scode type="yellow"]日志所以类型分为 3 大类：系统日志 、用户日志 、程序日志；系统日志一般都是有系统自动生成的，而用户日志主要就是记录一些用户登录和登出行为信息，程序日志是程序自己生产的，部分系统也会记录一份。常用的参数列表：[/scode]参数作用-k内核日志-b启动日志-u指定服务-n显示行数-p指定类型-f强制输出-since启示时间-until终止时间下面是一些使用例子，注意点事 -p 参数有很多类型：# 只显示错误、冲突和重要告警信息 journalctl -p err..alert # 显示从某个日期 ( 或时间 ) 开始的消息 journalctl --since="2012-10-30 18:17:16" # 显示从某个时间 ( 例如 20分钟前 ) 的消息 journalctl --since "20 min ago" # 显示最新信息，并且显示多少行 journalctl -f -n 10 # 查看特定进程 journalctl /usr/lib/systemd/systemd # 如果知道进程名称查看指定进程名称 journalctl -u sshd # 通过进程 PID 进行 journalctl _PID=1 # 清理两周之前日志 journalctl --vacuum-time=2weeks # 清理日志使总大小小于 100M journalctl --vacuum-size=100M # 查看 SELinux 日志信息 sudo journalctl -t setroubleshoot -n 50 # 等价于这条命令 sudo dmesg | grep -i selinux[scode type="lblue"]-p 参数对应的是日志输出的类型，目前日志类型列表：[/scode]类型作用emerg系统出现严重故障，比如内核崩溃alert应立即修复的故障，比如数据库损坏crit危险比较高的故障，硬盘损坏导致程序运行失败err危险性一般的故障，比如某个服务启动和运行失败warning警告信息比如某个服务参数或者功能出错notice不严重的一般故障，只是需要抽空处理的情况info通用性消息用于提示一些有用的信息debug调试程序所产生的信息none没有优先级，不进行日志记录其他资料How To Use Journalctl to View and Manipulate Systemd LogsSecurity-Enhanced Linux（SELinux）的基本和高级配置Linux Command DocsLinux 命令必知必会
大多数开发者或多或少都使用过 Java 这门编程语言，Java 这门编程语言最大的特性就是一次编写到处运行，这就使得开发者在编写了 Java 源代码之后通过 Java Compiler 编译出来的程序就可以在不同平台架构上运行。不管是 x86 还是 ARM 的 CPU 都运行正常无误，熟悉计算机 CPU 架构都知道不同 CPU 核心使用的不同架构和指令集，如果想要编写一次程序到处运行那就得需要使用交叉编译来完成，但是 Java 打破这一点，这得益于 JVM 的存在，Java 源代码不是将源代码直接编译成为对应平台机器码，而是自己定义一套指令集规则也就是对应的 Java Bytecode 标准，再通过将其字节码文件放到 JVM 上运行，最终 JVM 充当一个翻译员将其通过 JIT 的方式运行程序。[scode type="green"]JVM 有自己完善的硬体架构，如处理器、堆栈、寄存器等，还具有相应的指令系统。JVM 屏蔽了与具体操作系统平台相关的信息；本质上就是对现有的物理硬件的软件抽象的实现，可以作为一种编程语言的虚拟机，实际上不只是专用于 Java 语言，只要生成的编译文件符合 JVM 对加载编译文件格式要求，任何语言都可以由 JVM 编译运行。目前实现这些标准虚拟机有很多，例如 HotSpot 、 JRockit 、 IBM OpenJ9 、 GraalVM 这些都是相关的虚拟机实现产品。[/scode]Java 跨平台原理编写 Java 程序的源文件都是以 .java 为后缀名的文件，下面则为一个 Java 源代码文件要想运行它必须通过 Java 编译器来生成对应的字节码 .class 文件，从而让不同平台的 JVM 加载 .class 文件来运行程序：这里值得注意的是 Java 现在每 6 个月就有一次更新，每次都会有新的语言特性添加，不同版本的 Java 编译器可能生成的代码文件在不同 JVM 有不同效果，例如低版本的 JVM 不能运行高版本的 Java 编译器生成的 .class 文件，反过来则是可以的，不过这些都是小问题可以通过 AOT 编译成对应平台机器码运行，也可以使用一些第三方提供程序版本代码迁移工具 EMT4J 来完成。在 JDK 整个开发工具包中其实有两套编译器，大多数程序员所使用的 javac 编译器，此编译器只是将编写的 java 源文件编译生成为 JVM 能认识的字节码；而在 JVM 中又有一套 JIT 编译器，针对的是更高性能程序场景使用的，会把字节码再次编译成为机器码。JVM 主体架构当 Java 程序需要运行的时，此时就得需要 JVM 了，JVM 内部大致会和下图接近，首先前面通过 Java Compiler 生成的 .class 文件会把加载到虚拟机内存中，首先头一次执行是通过 JVM 内置 Interpreter 解析执行代码，这个过程 JVM 的解析器会把字节码里面指令翻译成对应平台的指令进行运行。另外 JVM 中内置另外 2 个 JIT Compiler 分别为 C1 和 C2，这两个即时编译器是针对长时间运行的热点代码进行优化的编译器，JVM 的设计者在设计的就考虑到一些程序代码运行效率的问题，前面提到的通过解析器执行代码性能肯定不能和 C 这种原生编译成机器码的程序性能相比，所以设计者针对热点代码会采用 C1 和 C2 编译器进行编译较为底层次的机器码来执行。[scode type="yellow"]使用解释器执行时，当某一行代码被执行了几次，这行代码会被打上 warm 的标签，当某一行代码被执行了很多次，这行代码会被打上 Hot🔥 的标签成为热点代码，这和 JVM GC 分代设计方式类似，通过分代来解决不频繁 GC 放到老年代。[/scode]在运行时将热点函数编译为汇编代码，当程序再次运行到经过实时编译的函数时，就可以执行经过编译和优化的汇编代码，而不再需要解释执行了。由于编译是在运行时进行的，因此 JIT 编译器可以获得代码实际运行的路径、热点和变量值等信息，基于此可以做出非常激进的编译优化，从而获得执行效率更高的代码。何为热点代码？例如下面有一段代码就为热点代码，当然现实中程序热点代码可能更为复杂：public class HotSpotExample { public static void main(String[] args) { long startTime = System.currentTimeMillis(); int iterations = 100000000; for (int i = 0; i < iterations; i++) { // 热点代码块 int result = compute(i); if (result % 2 == 0) { System.out.println("Even"); } else { System.out.println("Odd"); } } long endTime = System.currentTimeMillis(); System.out.println("Time taken: " + (endTime - startTime) + " ms"); } // 模拟一个计算函数，这里只是一个简单的示例 private static int compute(int input) { return input * 2 + 1; } }在这个示例中 HotSpot 编译器会观察程序的运行情况，当 compute 方法被多次调用后，它会优化这个方法的执行，将其编译为本地机器代码，以提高执行效率。C1 编译器优化较少，但是编译所消耗资源也较少；C2 编译得到的代码性能最好，但是编译消耗的资源也较多，但是目前在新版本的 JDK 中采用了 Graal 替代了 C2，新版本的 Graal 采用的是 Java 编写，因为是 Java 某些方面能做出的优化比 C2 要强，C2 编译器是使用 C++ 编写的难以维护并且某些特性实现不了，例如函数的局部逃逸分析的优化上，这样也是 Oracle 官方的将其 JVM 命名为 Hotspot Virtual Machine 原因。JVM 核心组件当 JVM 被启动它所做的工作就是为 .class 服务，跑起来是一个进程会加载 jar 包或者 war 包，这里的 war 包是基于 Servlet 容器的，例如 Tomcat 容器，这些容器本身也是 Java 程序也是跑在 JVM 进程之上的，当不同请求过来会被 JVM 内部的线程进行调度，下面为 JVM 内部结构主要架构图：当 JVM 开始执行 Java 程序时，会找到程序入口 main 方法并且创建一个线程开始执行，也称之为主线程；如果在类中创建了其他对象内存会被分配到 Heap 中；当运行一个函数时此时就需要每个线程的执行栈，函数都在自己的栈空间里执行，栈空间里存储的是运行时产生的局部变量，当线程执行到那一条字节码指令时，这个信息指令会被存放在程序计数寄存器中。[scode type="yellow"]需要注意的是方法区，方法区中主要存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等的共享的区域，方法区和堆类似是不连续存储区域，JVM 启动的时候被创建；方法区的大小决定了系统可以保存多少个类，如果系统定义了太多的类，导致方法区溢出，虚拟机同样会抛出内存溢出的错误；例如启动一个 Tomcat 大量加载 jar 依赖和使用大量动态反射技术导致内存溢出崩溃。[/scode]在程序中有一部分内存是在执行过程中可以被多个线程共享的，例如 Heap 内存，在 JVM 中的 方法区 和 堆 的内存都是可以被共享的。这样就会使得多个线程能访问该区域的内存数据，只要涉及到共享数据就产生数据竞争问题，在编写 Java 程序同样要考虑的这些多线程数据竞争问题，如果了解 JVM 这些内部组件的关系就可以使得编写代码时能够避免一些数据竞争的问题。[scode type="yellow"]方法区不一定每个虚拟机都有，得看具体虚拟机实现。方法区和堆内存的数据是被共享着的，只要涉及共享数据就会发生数据竞争，在 Jakarta 中的 Servlet 做法是没有成员变量，多个线程共享访问 Servlet 就没有成员变量也不会发生数据冲突区情况，在运行时就不用考虑线程安全问题这种就为无状态对象。[/scode]JVM 的字节码中程序执行依靠着栈，JVM 实现的指令执行都是依靠着栈结构来设计的，和传统的直接面向 CPU 硬件指令寄存器不一样。因为 JVM 作为一层物理机器抽象层必须要适配很多不同架构的 CPU ，所以如果使用寄存器的架构就必须和本机的平台架构绑定到一起；而 JVM 基于栈的实现可以在多个不同硬件平台上执行程序，但是基于栈指令比基于寄存器的指令要多，传统寄存器只需要 2 条指令完成的操作，而基于栈的可能需要多个指令操作，下面为一个计算两数之和的 Java 源代码对应的字节码指令：当字节码指令被执行时，JVM 中的 Program Counter Register 就会保存着每个线程栈该执行对应字节码指令的位置地址。在多线程环境下 CPU 具体如何去调用这些线程是不确定的，线程之间是可以来回切换的，如果发生了切换线程操作，当线程被切换到 CPU 上继续执行时就可以从 PC 中获取对应指令地址；在每次执行一次字节码指令之后 PC 的值会对应将其加指令地址操作，具体增加多少的得看指令大小偏移量，此时 PC 就会保存着下一条需要被 JVM Engine 执行的字节码指令的地址。下图为执行大致的过程：栈是在运行时的单位，而堆是存储的单位，当程序被执行起来时每个线程都有相应的栈，对象和方法最后都是为字节码指令会被依次压入栈中进行操作，在栈顶的栈帧为正在执行的栈帧，当栈帧上栈帧被执行完成之后会销毁，依次执行直到栈为空程序结束执行。[scode type="red"]执行栈的退出方式有 2 种，第一种为程序正常 return 返回退出，另外一种则为遇到未知错误和异常的情况导致栈帧退出；JVM 的执行栈大小可以被设置，具体参数例如 -Xss128k ，如果运行时递归调用函数可能就会导致 StackOverflowError ，如果 Java 虚拟机堆栈可以动态扩展，如果没有足够的内存可用于为新线程创建初始 Java 虚拟机堆栈，则 Java 虚拟机机器抛出一个 OutOfMemoryError ，这些都是属于堆栈运行时错误问题。[/scode]在栈中的变量所使用的内存是不需要被 Garbage Collection) 所管理的，当栈帧销毁时变量所使用的内存就会被释放掉，这里针对的是基础数据类型 byte 、 short 、 int 、long 、float 、 double 、 char 、boolean 所分配的空间，而针对的自定义复合数据类型 Class 在使用时就被称之为引用类型，当使用 new 关键字创建对象时此时它所占用内存是在堆上分配，而在栈上也就是一个指向堆的内存地址引用，内存布局为下图： 当在不同的方法之间传递对象的时，对象的信息会在每个方法栈中，传递依靠着是局部变量表，局部变量表是 JVM 设计一个特殊的表格，会有 slot 槽位，槽位中存储的各个变量在栈帧中的信息，可以根据这些信息找到对应分配空间，例如在栈帧中操作堆内存上的数据。局部变量表的大小会影响的栈帧的大小，如果局部变量表很大的话可能会占用更多空间，而相应的能创建栈帧数就会减小，局部变量表还有重要的作用就是和垃圾回收相关的，它是垃圾回收器扫描时的根节点，如果局部变量表中的变量还在被使用，这个变量所占用的内存就不会被回收掉。垃圾回收前面介绍 JVM 内存部分了解到程序在运行过程中会不断的创建新的类对象，每个的对象都需要内存分配，而这些内存会从堆内存中分配，如果频繁的不断创建对象很有可能导致堆内存被占用满。计算机的内存不是无限的这时 JVM 的设计就要想办法收回一些不再使用的对象占用的内存空间，这里将不再使用的对象称之为垃圾，所谓的垃圾就是不再使用的对象引用，垃圾回收器核心功能就要找出存在堆中不再被使用的对象，清理掉占用的空间，再让这些空间再被二次使用。[scode type="yellow"]垃圾回收器核心算法就是要找出堆空间中不被程序所引用的对象，并将这些对象的内存空间回收，这里的回收在不同的 GC 算法实现有所不同，目前主要的 GC 算法还是通过查找引用并且标记对象，被标记的对象所占用的空间就为可以被二次使用，这是整个 GC 算法的核心功能，如上图所示在一段内存空间来找出那些内存片段是不被使用的，而那些内存片段是还在被使用的，这与操作系统的内存分页管理方式很类似。[/scode]目前在很多自动回收内存的编程语言中，都有垃圾回收器算法的踪影，这个取决于各种的垃圾回收器算法的实现大同小异，常见的垃圾回收器分为 3 种：串行垃圾回收器： 每当垃圾回收器的线程工作的时候，所有正在执行其他计算任务的线程必须停止工作，如果不停止其他线程会导致垃圾回收器标记的垃圾会引发混乱，当这些线程被停止之后就会引发程序停止的状态，这个过程也称之为 STOP THE WORLD 即世界被暂停了。并行垃圾回收器： 相较于串行垃圾回收器的工作方式稍微做了一些优化，采用多个垃圾回收器线程工作的方式进行垃圾扫描标记和清理工作，可以利用多核 CPU 的优势提高整个垃圾回收过程效率。并发垃圾回收器： 这种垃圾回收工作线程和其他计算任务线程可以并发的运行，我称之为渐进式垃圾回收算法，这和前面我博文中讲到的 Wisckey存储 扫描标记的方式类似，先对某块内存进行扫描划分区域性扫描，渐进式扫描整个堆空间直到完成。上图为 JVM 中的堆结构内存布局，JVM 的内存布局采用的分区的方式，分为新生代和老年代两种堆区域，这种分区的方式和执行引擎中针对热点的代码的方式很类似，如果某些代码被频繁的执行就会被进一优化成底层机器码。而 JVM 中堆内存布局也有这种设计方式，绝大多数的 Java 对象的生命周期一般都很短，对应这种对象都是归于新生代区域管理，垃圾回收器针对于新生代的堆空间 GC 可能比较频繁，但是不会去扫描整个堆区域的内存，从而提高 GC 效率。当年轻一代填满时，这会导致一次小的垃圾收集过程，假设年轻代区域的对象死亡率很高，就可以进行一次小规模的垃圾清理过程，在此过程中一些幸存的对象会老化并最终移至老年代，进行这样的一次过程被称之为 Minor GC。而老年代中则存放的存活时间较长的对象，长期存活的对象与经过多次 Minor GC 依然存活下来的对象，在老年代内存被占满时就会进行垃圾回收，而老年代进行垃圾回收过程称之为 Major GC 这个过程可能相较于 Minor GC 执行时间更长。新生代设计较为复杂，本分为了 3 个不同的区域分别为 Eden 和 S0 、S1 三个区域，Minor GC 会先扫描整个 Eden 区域如果有未引用的对象，会直接清理掉，而针对还有被引用的对象则会被移动复制到幸存者空间 S0 中。这样反复 Minor GC 之后如果还会存活，一旦所有幸存的对象都被移动到 S1，最后会直接移动到老年代上，新生代整个区域将会被清理掉。当数轮次要 Minor GC 之后当老化的对象达到某个年龄阈值时，它们会从年轻代提升到老年代，最终将对老年代执行主要 Major GC ，清理并压缩该空间，这就是整个 JVM 的垃圾回收器设计，通过分代的方式进行垃圾回收器清理这和 LSM-Tree 存储引擎的垃圾回收器的设计有很相似之处，非常精妙！最后永久代包则含 JVM 所需的元数据用于描述应用程序中使用的类和方法，由 JVM 在运行时根据应用程序使用的类填充，此外 JavaSE 的标准库类和方法可能存储在这里。Summary作为一名 Java 开发人员了解 JVM 相关的原理对于编写健壮的 Java 程序有很多帮助，当然 JVM 也不止适用于某个特定的编程语言，了解 JVM 也是帮助提升对计算机底层技术的认知，在 JDK 中包含了很多用于调式 JVM 的工具，例如查看内存情况的 jmap 和 垃圾回收器状态的 jstat 工具，还有一个单独的 VisualVM 项目，这些都是官方提供好的工具能帮助开发者在实际开发中解决问题，了解 JVM 架构之后当程序出现问题了之后就知道从哪里入手解决问题，不会出现束手无策的情况。其他资料What Does a Modern JVM Look Like, And How Does It Work?GraalVM + AWS Lambda or solving Java cold start problemAOT vs. JIT Compilation in JavaDeep Dive Into the New Java JIT Compiler – GraalCategories of Java HotSpot VM OptionsJava Language and Virtual Machine SpecificationsJava | Multithreading Part 1: Java Memory ModelJava Garbage Collection BasicsMinor GC vs Major GC vs Full GCOpenJDK vs. Dalvik/ART Virtual MachineJava Developer's Introduction to GraalVM / 郑雨迪浅析JAVA虚拟机结构与机制CORE JAVA WEEK 2022 | 面向云原生现代化Java实践与演进-李三红
最近打算把博客基础环境迁移到 typecho ，其实在 2017 年为的博客就是使用 typecho 搭建的，但是 2019 年考虑到动态网站并且还是基于 php 的博客系统的安全性问题，我就迁移到了静态博客系统 hexo 上，最后 2020 年又迁移到 hugo 上，虽然静态博客系统可以抵抗 xss 反射和 xss 持久化攻击，但是部分功能目前满足不了我，决定迁移会到 typecho 系统上，本篇博客记录一下如何配置 php 运行环境。php-fpm 环境作为一个基本的动态的 web 服务来说必须有要数据库和 http 服务器支持，所以这里就诞生了一些基于开源软件的解决方案，例如 LNMP 的架构，php 本身只是一个动态类型的脚本语言不能作为 http 服务器，这里通过将 php-fpm 和 nginx 整合到一起来提供动态网站的服务。nginx 和 postgresql 服务器采用通过 rpm 软件包的方式安装，相比于通过源代码编译安装的方式要简单多一点，也不需要等待很长的编译时间，而 php-fpm 为了稳定性采用源代码的方式进行编译安装，首先运行下面命令：# 安装 nginx yum install -y nginx相对于 nginx 而 postgresql 数据库安装要复杂，postgresql 目前有很多版本，安装要去官方网站查询对应的版本，并且还需要设置最新的 yum 仓库信息：# 安装 postgresql # Install the repository RPM: sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm # Disable the built-in PostgreSQL module: sudo dnf -qy module disable postgresql # Install PostgreSQL: sudo dnf install -y postgresql15-server # Optionally initialize the database and enable automatic start: sudo /usr/pgsql-15/bin/postgresql-15-setup initdb sudo systemctl enable postgresql-15 sudo systemctl start postgresql-15nginx 和 postgresql 配置完成之后，接下来将需要进行编译安装 php-fpm 环境，先要解决相关依赖包问题，如何下载 php8 最新版本源码包：# 安装依赖的软件包 sudo yum groupinstall "Development Tools" # 下载 php8 的源码包 wget https://www.php.net/distributions/php-8.2.3.tar.gz # 解压源代码和进入目录 tar zxf php-8.2.3.tar.gz && cd php-8.2.3接下来要对 php-fpm 进行源代码编译安装，首先我们要对进行参数设置支持数据库模块，这里要支持 psql 模块，如果使用的是 mysql 使用 --with-mysql 参数，需要指定 psql 的安装目录 :./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-pdo-pgsql=/usr/pgsql-15 --with-pgsql=/usr/pgsql-15 --with-freetype --with-jpeg --with-zlib --enable-bcmath --enable-shmop --enable-sysvsem --with-curl --enable-mbstring --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-ldap-sasl --with-xmlrpc --enable-soap --with-gettext --enable-fpm --with-zip --enable-gd --with-webp在此过程中会不断检测依赖软件关系和版本，如果遇到版本较低软件可以手动安装和更新，不知道包在 yum 仓库的完整包名可以使用 search 查看：如果默认 yum 不能正常搜索到软件包，可以尝试手动下载对应的软件包安装，第三方软件包查看地址 pkgs.org ，当 configure 全部跑完之后会提示你欢迎使用 php 说明整个环境配置没有问题了，如下图：接下来执行 make 命令，make 命令会根据改成的 configure 文件规则来生产二进制文件，最后使用 make install 命令进行安装：# 生产二进制文件 make # 安装到配置的目录中 make install最后要配置 php 一些环境文件，例如 php.ini 文件可以配置运行环境时参数开启和不开启什么样的功能，在安装目录中是没有这个文件的，需要从源代码包中的模版文件中拷贝；php-fpm 全名是 PHP FastCGI 进程管理器，php-fpm.conf 是用来配置进程的文件，php-fpm 启动后会先读 php.ini ，然后再读相应的 conf 配置文件，conf 配置可以覆盖 php.ini 的配置；另外最后为控制 web 网站的文件，可以配置网站连接性能相关参数：# 配置 ini cp php.ini-development /usr/local/php/lib/php.ini # 创建php-fpm进程管理文件 mv /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf # 配置网站文件 mv /usr/local/php/etc/php-fpm.d/www.conf.default /usr/local/php/etc/php-fpm.d/www.conf最后一步需要把 php-fpm 的服务加入到启动项中，当重新启动服务器会自动加载生效：# 加入启动项，某些系统是 init.d 文件夹 cp sapi/fpm/init.d.php-fpm /etc/init/php-fpm由于 web 服务器在公网上提供服务难免遇到恶意用户情况，并且 php 上一种动态脚本语言，这里需要禁止一些危险的的内置函数，需要在 /usr/local/php/lib/php.ini 的 disable_fucntion 字段添加危险函数列表：disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv最后在还需要将 php-fpm 服务整合到 nginx 中，这样才能使得基于 php 环境网站能正常服务，在 nginx 配置文件中加入下列信息：server { listen 443 ssl http2; #listen [::]:443 ssl http2; server_name www.ibyte.me; root /var/wwwroot/www.ibyte.me; ssl_certificate "/var/wwwroot/ssl/www.ibyte.me.cer"; ssl_certificate_key "/var/wwwroot/ssl/www.ibyte.me.key"; ssl_session_cache shared:SSL:1m; ssl_session_timeout 10m; ssl_ciphers PROFILE=SYSTEM; ssl_prefer_server_ciphers on; # security headers add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; add_header Permissions-Policy "interest-cohort=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # logging 设置访问日志 access_log /var/wwwroot/log/nginx_access.log combined buffer=512k flush=1m; error_log /var/wwwroot/log/nginx_error.log warn; # gzip 开启压缩功能 gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; location / { # 设置默认索引 index index.php index.html index.htm; } # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # 这里配置 php-fpm 让 *.php 的请求都去 fastcgi_param 上处理 location ~ .*\.php(\/.*)*$ { # 这里必须和SCRIPT_FILENAME一直，巨坑！ root /var/wwwroot/www.ibyte.me; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffers 8 16k; fastcgi_buffer_size 32k; } } server { listen 80; server_name www.ibyte.me; return 301 https://$server_name$request_uri; }推荐默认不要早默认的 nginx.conf 中修改，应该在 conf.d 目录中单独新建一个域名 conf 文件，配置完成之后使用 systemctl restart nginx 启动，如果启动过程中出现了权限问题添加对应权限即可，我使用机器默认开启了 SELinux 安全功能，这时可能导致读取不到 SSL 文件，关闭 SELinux 功能 setenforce 0。SELinux 功能的配置文件路径 /etc/selinux/config 如果要永久关闭此功能，修改配置为禁止使用，但是这样对于生产系统的安全性就会降低，推荐还是在启动 nginx 和 php-fpm 临时关闭，或者将 php-fpm 加入到白名单中，输入下面命令：# 查看 SElinux 状态 sestatus # 加入到白名单中 setsebool -P httpd_read_user_content 1 setsebool -P httpd_can_network_connect 1到此基础软件运行环境配置完成，为了测试环境是否正常运行可以新建一个 phpinfo.php 测试，内容如下：<?php echo phpinfo(); ?>如果能正常显示环境信息说明正常，接下来要去初始化 postgresql 数据库：# 初始化数据库 postgresql-15-setup initdb # 修改默认密码 sudo -u postgres psql # 输入 \password 修改密码最好开启远程登录功能，相关的数据库配置文件在 /var/lib/pgsql/15/data/ 路径下：# 允许远程登录 vim postgresql.conf listen_addresses = '*' # 最后面修改 pg_hba.conf 添加一行 host all all all md5 # 创建新的数据库 create user blog with password 'xxxxx'; create database blog_db owner blog; GRANT ALL PRIVILEGES ON DATABASE blog_db to blog;最后设置防火墙开放一些端口允许远程访问，推荐使用基于内核的 netfilter 的 iptables 工具：# 添加 ssh http https psql iptables -I INPUT -p tcp --dport 22 -j ACCEPT iptables -I INPUT -p tcp --dport 80 -j ACCEPT iptables -I INPUT -p tcp --dport 443 -j ACCEPT iptables -I INPUT -p tcp --dport 5432 -j ACCEPT iptables -P INPUT DROP # 持久化化规则 iptables-save >/etc/iptables-script # 设置开启自动恢复规则 echo '/sbin/iptables-restore /etc/iptables-script' >>/etc/rc.d/rc.local另外一种方式为 firewalld 工具，老版本的系统不会安装需要自己手动安装：yum install -y firewalld firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --permanent --zone=public --add-port=22/tcp firewall-cmd --permanent --zone=public --add-port=5432/tcp firewall-cmd --reload systmctl enable firewalld由于使用的是 typecho 系统生成的数据配置文件在网站根目录中，安全起见设置成为不允许所有人通过 http 访问： # 禁止访问 config.inc.php 文件 location =/config.inc.php { return 403; }至此配置完成，重启 nignx 和 php-fpm 。其他资料PHP disable_functions Executable HandlingChapter 5. Working with SELinuxPostgreSQL新手入门
传统保存备份数据的方式是把某个数据文件拷贝到多个磁盘上保存多个副本，或者采用云盘的方式存储，而在 Linux 中通过一个类似于的功能，可以将多块物理磁盘组成一个逻辑的磁盘单位使用，RAID 代表独立磁盘冗余阵列或廉价磁盘冗余阵列，RAID 的目的是将文件的数据分布在多个磁盘上，这样单个磁盘故障就不会丢失该数据。RAID 技术在计算机中最重要的就是数据，而数据在持久化之后就会被存储到硬盘上，此时对硬件质量和安全性就提出更高要求，例如如果硬盘出现故障之后数据就无法被正常读取导致丢失，RAID 技术就简单来说就是把多块硬盘组成一个整体，从而提升数据可靠性和读写效率问题，缺点明显就是浪费多空间。目前实现 RAID 技术有两种方案，一种软件的方式来实现，另外一种通过硬件的方式，很多在线商城可以购买到支持 RAID 硬盘阵列盒子 有物理设计支持；共同的特点是就是需要多块磁盘，最低需要为 2 块磁盘，不同的 RAID 方案有不同磁盘个数要求。RIAD 0 级别最少需要 2 块硬盘，安全性低，数据依次写入到所有的硬盘中，不具备备份数据和容错能力，其中一块出现异常整个数据就会受到影响，数据可以并行读取和写入不同磁盘中。RIAD 1 级别最少需要 2 块硬盘，安全性高，数据同步写入到所有的硬盘中，数据被冗余存储在多个硬盘中，有一块硬盘可用数据就不会受到影响，以 2 块硬盘为例空间利用率只有 50% ，但是读取数据性能会提升。RIAD 5 级别最少需要 3 块硬盘，和前面方式不同的是采用一种经过数学上一种奇偶数校验来到达数据的完备性，会把真实数据依次存储在不同硬盘中，并且将数据的奇偶数信息存储在不同的硬盘中，当某块硬盘出现问题时，可以通过奇偶数校验反向还原出来原始的数据信息，此种方案能有效提高硬盘利用率和读写效率，也是数据安全性高的。RIAD 1-0 级别是组合了 RIAD 0 和 RIAD 1 两个级别的产物，先对数据采用 RIAD 1 的方式来存储，最后再将其使用 RIAD 0 的方式组合，形成了高可读写和高数据完备性的方案，只要不是损坏的同一阵列的硬盘，那么最高可允许 50% 硬盘损坏而还能正常提供服务。RIAD 的级别远不止这么多，但是最为常用也就这几个，目前使用最多还是 RIAD 5 和 RIAD 10 级别能提供读写性能也能提高数据的安全性，至于怎么创建磁盘阵列可以查看 mdadm 命令。其他资料Virtual Data OptimizerStandard RAID levels软件 RAID 配置 | 储存管理指南 | SUSE
我个人接触 Linux 比较早，最开始喜欢研究网络安全相关的话题，所以第一次使用的 Linux 系统是 Kali Linux，虽然现在有很多的 Linux 发行版本，但大多都是还是使用命令行进行操作的。和 MacOS 不同 Linux 大部分软件生态还是没有 Apple MacOS 完整，这里只的是图形界面和第三方公司开发的软件，而且大部分软件都是一些命令行下的软件，作为一名开发者都有必要掌握一些基础的 Linux 操作命令，这些都是类 Unix 系统，大部分基础命令是相通的，所以介绍同样常用的命令。代替鼠标的命令在 Windows 很少有人使用的 DOS 命令行进行操作，但是在 Linux 下最为常见的是使用命令行，当然 Windows 也可以完全使用的 CMD 命令进行操作来控制电脑，但是大部分人还是使用视窗界面和鼠标。如果计算机操作系统没有图形化界面那么估计到现在还没有普及起来呢。最为常见为目录切换和文件查看，文件和目录创建、文件移动和文件复制、文件下载和文件编辑等操作，下面介绍：# 使用 cd 可以切换目录 cd /home/auula # 返回上一层目录 cd .. # 使用相对路径切换 cd ../../xxx # 快速返回到家目录 cd ~创建文件和创建目录：# 创建一个文件 touch Hello.java # 更新一些文件最后访问时间 touch -d Hello.java # 创建一个目录 mkdir /home/xxxx # 递归创建目录 mkdir -p /home/a/b/c文件移动和创新命名都为同一个 mv 命令：# 将目录a到b中 mv /aaaxx/a /bbbxx/b # 重新命名 mv a.tar b.tar文件复制操作：# 将a.txt文件复制到b目录中 cp /xxxx/a.txt /xxx/b # 使用递归复制 cp -r /xxx/aaa/*.txt /xxx/bbb/从远程的网络服务器上下载程序压缩包，可以有很多种方式，最常见上 wget：# wget 下载文件 -b 后台运行 -c 支持断点续传 wget -b -c https://blog.leonding.me/mirror/trojan-install.sh查看当前工作目录的位置，和当前命令下有什么文件：# 当前位置信息 pwd # 查看当前目录下有什么文件，包括隐藏文件 ls -al查看某个文件内容，如果文件内容太多可以分页查看：# 查看某个文件的内容 cat /xxx/Hello.java # 通过分页查看某个文件的内容 more /xxx/Hello.java当在一个多层次顶层目录中，查看当前目录下的所有子目录：# 查看当前目录，打印目录结构树 tree .查看文件属性信息和文件夹属性信息：# 查看文件属性信息 file Hello.java # stat 查看文件夹和文件信息 stat directory对文本文件内容高级操作：# 查看文件的行数和字节树 wc -l /etc/passwd # 替换文件内容的字符串 cat Hello.java | tr S X对文件内容进行匹配过滤，和比较两个文件内容差异：# 查看符合批评 root 字符串的内容 cat /etc/shadow | grep root # 查看两个文件内容的差异 diff -brief a.java b.java在 Linux 系统中所有的东西都被抽象成了文件，所以如果要删除某个文件得注意！删除文件和目录命令如下：# 删除某个目录，如果目录中有其他文件夹和文件会交互式删除 rm /xxxx # 强制删除，通配符 rm -rf /xxxx/*.go在 Linux 中安装软件传统的方式上通过源代码的方式进行编译安装，这种方式要求管理员自己解决各个软件包的依赖关系，还有一种方式采用的不同 Linux 发行版本中提供软件包管理工具来管理软件。目前为两大主流的发行版本，基于 Debian 和 Redhat 版本的，对于量大不同的包管理工具 deb 和 rpm，这里为主要介绍 rpm 的方式：# 安装某个 rpm 软件包 rpm -ivh xxxx.rpm # 查找安装目录 rpm -ql package | grep bin # 查找某个全包名 rpm -qa | grep java # 卸载某个软件包 rpm -y remove package 在命令行模式没有图形化界面来查看时间，如果需要查看当前系统时间，可以使用 date 命令：# 格式化输出当前系统时间 date "+%Y-%m-%d %H:%M:%S %p" # 当前已经过去多少天 date "+%j" # 设置当前系统时间 date -s "20221101 8:30:00" # 设置时区 timedatectl set-timezone Asia/Shanghai在磁盘上查找某个文件，有时候不知道文件存放在哪里，或者只记得文件的某个名称：# 在某个目录下查询一个以Match开头的.java 源文件 find /xxx -name "Match*.java"此种查询方式效率很低，另外一种方式上通过 locate 命令，此命令会先对目录和文件建立索引，在进行查询：# 在运行 locate 命令之前先要建立索引 updatedb # 在使用 locate 命令查找 locate wheris在 Linux 中的软件都是带有命令本体和软件的使用文件的，要查询使用文档可以使用：# whereis 查询 man 的命令的源代码和帮助文件 whereis man如果需要查询某个命令本体安装路径：# 查询某个程序的路径 which java快速判断一个文件夹和文件是否存在，是否具备读写权限：# 判断目录是否存在 [ -d /directory ] && echo "dir exist" # 判断是否文件 [ -e /xxx/file.java ] && echo "file.java exist" # 判断当前用户是否有写权限 [ -w /xxx/file.java ] && echo "file.java writeable" # 判断当前用户是否有读权限 [ -r /xxx/file.java ] && echo "file.java readable" # 判断当前用户是否有执行权限 [ -x /xxx/xxx.sh ] && echo "xxx.sh executable"在括号中 [ expr ] 可以说其他支持 与、或、非 运算的表达式，例如判断一个当前登录的账号是不是超级管理员：[ ! $USER = root ] && echo "user not administrator"除此之外，还有支持大小比较运算：# 等于 [ 10 -eq 10 ] && echo "equals" # 不等于 [ 100 -ne 10 ] && echo "not equals" # 小与 [ 10 -lt 100 ] && echo "less than" # 大于 [ 100 -gt 10 ] && echo "greater than" # 小于或者等于 [ 10 -le 10 ] && echo "less than & equal" # 大于或者等于 [ 100 -ge 10 ] && echo "greater than & equal" 要判断一个字符串是否为空：# 判断字符串是否为空 [ -z $str ] && echo "is empty"管道和重定向符号，将前面的程序的输出传入到下一个程序的输入：# 将前面一个程序的输出输入下一个程序输入 cat /etc/passwd | grep root # 将标准输出重定向到一个文件中 echo "Hello Shell" > text.txt # 将标准错误和标准输出同时追加重定向到一个文件中 ls -l auula &>> tee.txt # 将文本内容重定向到另外程序标准输入中 ssh root@xxxxxx < deploy.sh查看系统进程和系统状态信息可以使用 PS 命令，大部分命令行软件运行在后台，如果想要停止运行某个软件就需要对应软件 PID ，如下：# 显示系统中全部的进程信息 ps aux # 以进程树的形式显示 pstree # 返回 nginx 的任务 ID pidof nginx # 列出系统支持的全部信号列表： kill -l # 通过信号的方式控制一个进程，强制关闭进程 kill -9 1518定时任务说完最基本的代替鼠标操作的命令之后，介绍一下定时任务，Linux 系统主要的应用场景还是服务器端，服务器大部分不间断运行并且提供对外的服务，某些时候例如定期备份数据库，人工定期备份很麻烦这时可以使用定时任务来使用程序自动化完成。at 命令可以用于在指定时间执行命令，at 只能适用于一种单一任务的定时器，会以 atd 守护进程的形式在后台模式运，会从任务队列里取出任务来执行。at命令的特点就是计划任务的一次性特征，一旦设置的计划任务被执行，则该任务就会被从任务列表库中删除，仅用于执行一次的工作：# 指定一个脚本文件，并且设置执行时间 at -f task.sh 07:35 # 查看已经创建的任务 at -l # 移除某个任务 at -d task_id # 设置在2023年5月18日准时执行某脚本文件 at -f task.sh 05/18/2023 # 使用计划任务设置25分钟后执行某指定脚本文件 at -f task.sh now+25 min另外一种为周期定时器，对应着某个时间段和某个时间点重复做某件事情，在物理上时间都直线运动，一直每分每秒前进，那就要设计一套规则来计算时间和时间单位。在圣经中上帝创造万物用了 7 天，所以现在用 7 天为一个星期的周期，每天 24 小时，一小时为 60 分钟，一分钟为 60 秒... 一年看 2 月份是润年还是平年，组成一个周期。周期定时器的时间设定就以此为单位，来重复某个任务，在 Linux 中有一个为 crond 命令提供此功能，crond 命令在设置的时候要提供日期占位符号： # 每1分钟执行一次 task.sh * * * * * task.sh # 每小时的第3和第15分钟执行 3,15 * * * * task.sh # 每隔两天的上午8点到11点的第3和第15分钟执行 3,15 8-11 */2 * * myCommand # 每月1、10、22日的 4 : 45 重启smb 45 4 1,10,22 * * /etc/init.d/smb restart权限管理大家都说 Linux 系统比 Windows 安全，哪这里的安全体现在哪里？Linux 最初的版本是类似于 Unix 系统设计的，而 Unix 是一个多用户的操作系统，在同一时刻有多个用户可以登录到操作系统中完成任务，那么多个用户如何保证各自的文件安全性呢？ 最初的设计者将引入了权限系统概念，权限系统可以帮助 Root 超级管理员用户管理其他用户的权限，在 Linux 系统中所有的东西皆为文件，对这些文件设置权限，就可以让多个用户之间保持某个规则来访问。Linux 权限系统设计为：用户权限、所属组权限、其他权限；这 3 组权限都属于权限位都是一样的，分别为： R 可读 、W 可写 、 X 可执行，对应目录来说如果没有可执行权限，用户不能进入该目录，有读权限也只能文件列表。权限位对应数字R4W2X1这是最基础的权限位，例如一个文件所有权限位为 rwx-r-x-rwx 换成数字表示为 757，下面介绍基于这些的身份权限操作：# 查看当前用户是谁 who # 查看当前用户的信息 id user_id # 向系统中添加一个名为 user1 用户，并且设置默认的家目录和默认用户ID useradd -d /home/directory_u1 user1 -u 8888 # 修改某个用户信息，将 user1 加入 root 扩展组，并且禁止用户登陆 usermod -G root -L user1 # 修改某个用户的密码 echo "New Passwd" | passwd --stdin user1 # 锁定用户，解锁 -u passwd -l user1 # 查询密码状态 passwd -S user1 # 删除一个用户，并且删除用户家目录 userdel -r user1以上为最基础的用户管理命令，默认创建的用户都有一个默认的与用户名同名的组名，一个用户可以是多个组称之为扩展组，如何创建一个组？默认使用 groupadd 命令即可：# 创建一个组 groupadd www # 将 nignx 用户添加到 www 组 usermod -G www nginx # 更改用户组 ID groupmod -g 6666 www # 删除一个用户组 groupdel www # 查看用户组是否被删除 cat /etc/group | grep www # chown 和 chmod 都属于文件权限控制命令，chown 更能快捷修改文件所有者和所属组 chown nignx:www /wwwroot除此之外还有还有超级权限位，默认的用户、所属组、其他组的权限位无法满足一些需求的时候就会使用到超级权限位，例如 SUID 、 SGID 、SBIT 权限位。SUID 权限位可以让一个原本不属于该用户的文件能拥有属于该文件的权限，例如 ls -l /etc/passwd 让普通用户自己可以修改自己的密码。SGID 权限位可以让用户临时获得文件所属组的权限，例如创建新文件会自动将其归属到对应目录到所属组中。SBIT 权限位，很多第三方翻译成了 粘滞 Sticky Bit 位这是不正确的！应该叫保护位，设置了该权限位的目录，下的文件是无法被其他人删除的只能被所有者删除，例如 /tmp 目录。这三组最高权限位的关系是：SUID = 2^2 、SGID = 2^1 、SBIN = 2^0 ，操作命令：# 设置 SUID chmod u+s xxx # 设置 SGID chmod g+s xxx # 设置 SBIN chmod o+t xxx # 全部组应用规则 chmod a+x xxx超级权限位在第一位，例如一个文件的权限是 1755 那么表示 rwxr-xr-t ，而 rwsrws-w- 表示 6772 ；另外如果一个目录没有 x 权限只有 r 权限，则表示只能读取目录里的内容，不能进入到目录中。除此之外还有一些隐藏更深的隐藏权限属性，chattr 命令其功能是用于更改文件隐藏属性，可用于针对目录和文件的隐藏权限做控制。# 进追加的方式，并且修改之后立即同步到磁盘上 chattr +aS logging.log # 递归设置目录，其中的子文件内容而不能新建或删除文件 chattr -R +i /root以上权限管理命令只能针对的是用户、用户组、其他组来做权限控制，不能准确到某个特定用户身上和用户组上，针对此类需求可以使用文件访问控制列表策略，也称之为 ACL ，通过该技术可以更加精准的控制权限的分配。# 只允许某个用户对目录有读写执行权限 setfacl -Rm u:nginx:rwx /www/wwwroot # 允许某个组对目录有读和访问权限 setfacl -Rm g:www:rx /www/wwwroot # 获取某个目录到 ACL 控制权限信息 getfacl -R /root > dir_root_backup.acl # 从控制信息文件恢复 ACL setfacl --restore dir_root_backup.acl # 取消某个用户的独立权限，-d 是全部清理掉 setfacl -x u:nginx /www/wwwroot除以权限控制规则以外，默认普通用户除用户 ID 为 000 是超级管理员用户之外，其他用户在执行 sbin 系统级的命令时需要提权操作，例如 fdiks 命令对硬盘进行分区操作，这是要管理员用户才有权限执行的命令。如果普通用户要执行此类操作必须临时使用 sudo 命令获得超级权限来执行，另外一种方式为通过 visudo 命令进行配置，指定某个用户能获取什么样的命令权限。# 例如规则: 用户名 允许操作的网段=临时拥有的身份用户 允许操作命令路径 auula 192.168.31.0/255=(root) /usr/bin/cat,/usr/sbin/fdisk # 检查配置 /etc/sudoers 文件配置项是否合规 visudo -c # 配置提权时不需要输入密码 auula ALL=(ALL) NOPASSWD: ALL # 配置组规则，允许对应组旗下人员有高级权限 %www ALL=(ALL) ALL文件系统在所有的操作系统中文件系统都属于一层对于底层硬件磁盘设备的抽象，在 Linux 内核中也是这样的，内核为用户提供一层为 Virtual File System 简称为 VFS 接口，用户操作文件时实际上操作的是操作这层接口。在日常使用 Windows 和 MacOS 时感觉不到硬件变动对于文件系统影响，例如插入一个 USB 设备到电脑中，文件系统会默认将其连接上，而在 Linux 上需要人工手动分区和格式化再将其挂载到指定目录中使用。# 挂载指定硬件设备到指定目录 mount /dev/sda3 /backup # 查看某设备到 UUID blkid /dev/sr0 # 也可以通过硬件设备到唯一 ID 进行挂载 mount UUID=c6f831b1-5ede-4e06-9dd3-eecb42cc4934 /backup # umount 可以用于卸载已经挂载的设备 umount /dev/sdb2 # 查看各个硬件设备和分区挂载的关系 lsblk # 查看当前磁盘和分区使用状态 df -h # 查看某个目录占用内存大小 du -sh大部分设备一段挂载是不会变动的，但是某些情况下服务器重启会导致设备挂载也失效，这时候需要配置开机主动挂载，需要编辑 /etc/fstab 文件中配置信息。以上是针对已经分区并且格式化好的文件系统，如果服务器新加了一块磁盘，此时就需要进行对磁盘进行分区然后使用文件系统格式化，最后挂载到指定目录才能被正常使用。分区会用 fdisk 工具进行分区，此工具目前只能适用于 MBR 的分区表，最多支持 2TB 磁盘，相当于普通服务器完全就够用的；分区完成之后会进行文件系统格式化，这里 Linux 默认提供了 mkfs 命令，次命令有很多子命令例如 mkfs.ext2 mkfs.ext3 mkfs.xfs 文件系统格式，当格式化完成之后使用 mount 挂载到指定目录即可。挂载交换区另外一个文件系统为交换区，可以帮助操作系统在物理内存不够情况下创建新的硬盘交换区，以此充当扩展内存使用，但是磁盘和内存读写速度有明显差距，创建 swap 分区的方式有很多，例如 fdisk 命令，也可以提供 fallocate 命令和 mkswap 命令，注意使用 fdisk 时得需要将其 partition type 修改为 82 表示为交换区格式。# 格式化分区 mkswap /dev/sda3 # 挂载交换区 swapon /dev/sda3 # 设置开机自动挂载 echo "/dev/sda3 none swap defaults 0 0" >> /etc/fstab网络防火墙在 Linux 中防火墙功能可以控制网络数据包数据包的交换，也是保障服务器数据安全性方面最重要的功能，往往服务器上允许部分应用不允许被外界所访问到，这是就可以使用防火墙规则来控制了。 Linux 中的防火墙自身的流量进行过滤，防火墙底层实现基于内核的 netfilter 和 nftables 包过滤器的，而对于普通用户使用的 iptables 和 firewalld 命令只是一个便于配置过滤器规则的工具而已。在网络层次 OSI 模型中，iptables 属于数据链路层的服务，会对网络上的流量按照规则：源地址 、 目的地址 、端口、传输协议 、服务类型信息从上至下进行规则匹配，如果匹配到了则会按照预先设置的规则来处理。# 查看已经配置的策略 iptables -L # 清理掉配置策略 iptables -F # 设置把INPUT链设置默认规则为 DROP iptables -P INPUT DROP # 允许 INPUT 链的 ICMP 流量进入，例如允许 PING 命令样有响应 iptables -I INPUT -p icmp -j ACCEPT # 删除刚刚给 INPUT 添加的规则，指定链和第几条 iptables -D INPUT 1 # 允许某个网段访问并且制定协议和端口 iptables -I INPUT -s 192.168.31.0/24 -p tcp --dport 22 -j ACCEPT # 开放 HTTP 的 80 端口 iptables -I INPUT -p tcp --dport 80 -j ACCEPT # 只接受某个网卡接口的流量 iptables -A INPUT -i eth0 -j ACCEPT # 过滤掉转发的流量，IP 到 IP 到数据包 iptables -A FORWARD -s 192.168.1.0/24 -d 10.0.0.1/24 -j DROP # 禁止某个端口范围进行访问 iptables -A INPUT -p udp -dport 1024:65535 -j REJECT # 永久生效添加策略规则 iptables-saveiptabls 命令功能远不止这么多，最常用是基于 filter 功能对 chain 上的数据包进行过滤，另外一款最新的服务器设置工具是 firewall-cmd 命令，提供 GUI 和命令行版本，并且还有自动化命令参数补全功能，可以查看 firewall-cmd命令 。命令行很酷在类 Unix 系统下有很多的有趣第三方软件，例如 sl 可以跑火车，Linux 是一款类似 Unix 操作系统很多东西都是通用的，很多的第三方开发者也实现不少的命令行软件，这里我介绍几个可以能提升开发效率的命令行软件。第一款是 cheat.sh 这是一款基于在线网络的命令行和编程案例查询工具，可以基于 curl 发起一个 http 请求到达 cheat 服务器，cheat 服务器会返回相应的结果，例如查询 kotlin 语言中的 for 循环语句怎么使用？或者查询某个 Linux 命令不知道也什么参数和如何使用？第二款是 wttr.in 也是基于网络服务的命令行工具，同样要使用 curl 发起一个 http 请求查询数据并且展示到终端上，例如查询上海的未来天气情况?第三款 branch.sh 同样也是基于网络服务的命令行工具，可以使用以下命令来查看您的 Linux 系统信息，还可以测试网络带宽及硬盘读写速率，机器性能如何？第四款为命令行下日历程序，在没有 GUI 界面下绘制日历表格，命令为 cal ：其他资料Linux 命令完全手册Linux Cast 视频Introduction to Linux在 Linux 中使用 shell 脚本自动创建/移除并挂载交换文件A Deep Dive into Iptables and Netfilter Architecture
大部分商用服务器都使用类似于 Redhat Enterpris Linux 版本的 Linux 系统作为服务器运行环境，Linux 是允许多个用户远程登录操作的系统。部分企业的 Linux 服务器可能运行时长很长，数年之久，而大多数时采用是普通用户进行管理运维的，而相对于 Root 超级管理员用户则是配置系统关键环境和更新系统、安装更新软件包时所需要用到，如果时间长或者前一任管理员离职了，Root 超级管理员用户密码没有对接或者忘记就需要重置 Root 超级管理员用户的密码。部分 Linux 发行版本切换到 Root 超级管理员用户会提示一句话: 权限越多，责任越大！ 可见 Root 超级管理员用户重要性。目前重置 Root 用户的密码方案适用于类 Redhat Enterpris Linux 发行版本的操作系统，例如 CentOS 系统。重置密码使用如下命令，查看系统是否支持此博文介绍的重置密码方法：[root@localhost ~]# cat /etc/redhat-release CentOS Stream release 9 [root@localhost ~]#如果你是在物理服务器机架上，可以把显示器和键盘接上服务器设备上，而对应部分使用的公有云服务商的 VPS 可以在管理页面启动 VNC 进行连接管理。重启服务器在停留在开机页面时，按下键盘上的 E 键，会进入内核信息修改页面，找到 linux 开头哪一行的末尾处加 rd.break 参数， 然后按住 Ctrl + X 组合键确认修改。系统过几秒钟就会刷新进入到紧急救援模式，在命令行中输入一下命令：mount -o remount,rw /sysroot chroot /sysroot passwd touch /.autorelabel整个过程如下截图，会提示输入新的 Root 账号的新密码，如果密码字符串组合很简单，会意思密码过于简单，不允许被设置。最后连续按住键盘上的 Ctrl + D 组合按键，系统会退出重启，最后输入新密码登录到系统完成重置。
作为一名极客肯定会使用 Linux 操作系统，并且作为一名合格的程序员应该必须会使用 Linux 操作系统，Linux 是泛指一些基于 Linux Kernel 二次开发的操作系统，目前当今世界上有很多的 Linux 发行版本，那么小白程序员如何选择一个适合自己的 Linux 操作系统呢？这篇文章笔者就会重点介绍一下。作为一名后端程序员的会我推荐是使用基于 RedHat 衍生出来的发行版本，RedHat 是当今世界上最成功的开源软件公司，其最初的发家核心软件产品就是 RedHat Enterprise Linux ，这是基于 Linux Kernel 内核开发出常用商用操作系统，很多的商用公司都在使用这款操作系统，该系统有稳定保证还有配套一些商用技术服务支持的，也有 RedHat 公司整个软件生态的支持。红帽公司为了提升他们产品在 Linux 发行版本中独特性，还设计一套完善认证课程体系，例如：初级的 RHCSA 、中级的 RHCE 、 高级的 RHCA ，如果作为一名程序员能考取这方面的职业技能证书，那么就可以证明自己在 Linux 运维管理方面有很高的技术技能，相应的在职场上技术能力高度最好体现。发行版本首先介绍就是 Oracle Enterprise Linux 发行版本，这个是由 Oracle 公司维护并且提供商业化技术支持 Linux 企业发行版本，和 RedHat Enterprise Linux 软件生态完全兼容。CentOS 是另外一个社区免费企业级 Linux 发行版本，因为 Linux Kernal 是使用 GNU 通用公共许可证（GPL） 发布源代码，因此任何人都可以运行、研究、修改和重新分发源代码，而 CentOS 则是按照相关法律条文合法合规的基于 RedHat Enterprise Linux 源代码发行的社区免费版本，主要就是移除一些 RedHat 的商标，软件生态和 Oracle Linux 和 RedHat Enterprise Linux 相互兼容。另外一个企业级为德国 SUSE 公司提供的 OpenSUSE Enterpris Linux 版本，也是提供的企业级 Linux 技术支持的服务的公司，SUSE Linux Enterprise 产品比上游 openSUSE 接受了更严格的测试社区产品，目的是只有成熟、稳定的包含组件的版本才能进入发布的企业产品。Ubuntu 是基于 Debian 以桌面应用为主的 Linux 发行版，也有 Server 版本但是大部分还是以桌面版本为主，主要产品面向个人电子产品的，有智能手机和智能电视系统，也提供一些付费的技术服务支持业务。这里 Ubuntu 主打的个人 PC 设备桌面操作系统，而上面的 RedHat Enterpris Linux 和 Oracle Enterpris Linux 、 CentOS Enterpris Linux 、 SUSE Enterpris Linux 都针对大型商用服务器市场的，面向都是数据中心的超级计算机的操作系统，和 Ubuntu 最大区别就是软件安装的方式，Ubuntu 有 GUI 的应用商店来下载安装应用程序，而后者企业级大部分是不使用图形界面进行操作的，而是终端设备连接到服务器进行运维管理。如果是个人作为开发机使用的推荐使用 Ubuntu 发行版本，而如果企业级推荐 Enterpris Linux 因为这些包管理工具和软件大部分都是兼容的，在 Linux 系统上早期版本软件的方式是通过软件源代码进行编译安装的，要解决很多依赖关系问题，而针对这些大部分企业发行版本采用的是基于 RPM 的 YUM 、DNF) 软件包管理工具，不同发行版本修改其软件源即可。其他资料如何选择适合的 Linux 发行版？什么是 Linux？ | Linux 新手课程为什么很多大佬在使用Linux？
什么是 Shell？Shell 这里指的是 Uinx 中 Shell 解释器，而不是某跨国公司品牌的 shell 名称，这才使得人搞混了。在常用的 Linux 系统中 Shell 是一种介于系统内核和用户之间的命令解释器程序，用户可以通过输入命令通过 Shell 解析器执行命令从而达到操作计算机内核作用。如果经常使用类 UNIX 系统的话，如果掌握 Shell Script 会大大提高一下工作效率，常用的系统资源管理自动化操作，都可以使用编写 Shell 脚本来完成，而且好处是大多数操作系统默认都有自带的解释器。每个操作系统的默认自带的解释器都有很多种，可以通过下面这个命令来查看支持的解释器列表：$: cat /etc/shells # List of acceptable shells for chpass(1). # Ftpd will not allow users to connect who are not using # one of these shells. /bin/bash /bin/csh /bin/ksh /bin/sh /bin/tcsh /bin/zsh #我默认使用的这个，功能强大于普通bash /usr/local/bin/zsh最常用的解释器也就是/bin/bash，下面可以编写一个简单的脚本，内容如下：#!/bin/bash echo "Hello Shell Script！" # 执行会打印这句话变量的使用在 Shell 脚本中也有变量的概念，也内置一些变量方便编写脚本的时候使用，例如$HOME、$USER、$PWD、$SHELL... 在 Shell 脚本中的变量也简单，没有类型系统所有数据都字符串类型，无法参入运算，但是有区分只读静态变量，如下：#!/bin/bash variable = "这是一个普通的变量" echo $variable readonly immutable = 1024 # 只读不可变 zsh: read-only variable: immutable unset variable # 取消设置的值 echo $variable如果需要把当前脚本文件里面的变量导出给其他脚本使用的话，那就需要将变量导出为全局变量，使用关键字 export ：#!/bin/bash GLOBAL = "全局变量" export GLOBAL这样在其他的 Shell 环境变量就可以访问到这个变量了。特殊变量 $n 这里的 n 表示数字，从 0 开始的可以是很多个，但是 1-9 可以使用 $n ，但是超过之后访问其值必须加上大括号 ${10} 这样才能访问到，一般这使用与脚本文件外部传入参数获取使用。另外一个如果需要得到外部传入参数个数需要使用 $# 它返回就是参数个数。如果需要获取全部输入的参数直接使用 $@ ，它就获取到所有传入的参数。#!/bin/bash echo "args length: $#" echo "$0 $1 $2" args=("$@") echo "first : ${args[0]}" echo "second : ${args[1]}"output:$: sh shell-args.sh 1 2 3 args length: 3 shell-args.sh 1 2 first : 1 second : 2还有一个比较特殊的 $? ，它可以检查上次执行命令是否出错，它会检查上次命令执行返回结果，如果结果为 0 则表示正常运行退出，反之则认为是程序非正常退出，当然退出错误码可以自定义，例如修改一个不可变变量：#!/bin/bash readonly immutable=1024 echo $? #0 immutable=2048 echo $? #1运算符上面介绍 Shell 脚本中变量的使用，但是变量的缺点就是没有类型系统，所有的变量都是字符串如果需要进行计算那么就不可能的。为了解决数值运算的问题，在 Shell 中运算使用的关键字 expr 命令，例如 expr n + x 加法计算。expr 命令是一个手工命令行计数器，用于在 UNIX/LINUX 下求表达式变量的值，一般用于整数值也可用于字符串，运算的时候用空格隔开每个项，特殊字符需要转义的使用 \ ，对包含空格和其他特殊字符的字符串要用引号括起来。#!/bin/bash expr 10 + 10 #加法计算 expr 20 - 10 #减法计算 expr 10 \* 10 #乘法计算 expr 50 / 5 #除法计算 expr 8 % 7 #取余计算特殊的字符串运算：#!/bin/bash expr length "text content" # 计算长度 12 expr substr "this is text" # 截取 is is expr index "hello" e #第一次出现的位置2当然如果想纯做数值计算，使用 $[运算表达式] 如下：#!/bin/bash echo "10 + 10 = $[10 + 10]" # 10 + 10 = 20 expr `expr 2 + 3` \* 4 # 20流程控制shell 作为脚本语言同样也有条件语句，只要支持条件流程控制才能算是一门脚本语言，shell 脚本支持条件判断、流程控制、循环这些下面是一些例子。IF 语句条件判断表达式，条件语句需要放置在 [ 条件 ] 里面，条件相关的关键字有：-ge是否大于或等于-le是否小于或等于-lt小于-gt小于检测是否有某种权限关键字：-w是否有写权限-r是否有读权限-e文件夹或者文件是否存在#!/bin/bash x=$1 y=$2 if [ $x -le $y ]; then echo "x less than y" elif [ $x -gt $y ]; then echo "y grater than x" else echo "other result" fi CASE 语句熟悉其他编程语言的应该都使用过 Case 语句来匹配处理多个条件，在 shell 中也存在 case 语句，如下：#!/bin/bash language=$1 case $language in Java) echo "Java 语言";; Go) echo "Go 语言";; Rust) echo "Rust 语言";; esac FOR 循环for循环可以根据某种条件让程序重复执行块逻辑，下面是普通循环的例子：#!/bin/bash sum=0 for ((i=0;i<=100;i++)) do sum=$[$sum+$i] done echo $sum for loop in 1 2 3 4 5;do echo "number: $loop" done WHILE循环while循环也是一种循环的，在循环里面设置一个条件，直到该条件满足为止，如下：#!/bin/bash counter=0 while [ $counter -le 5 ]; do counter=`expr $counter + 1` echo $counter done 函数编程在shell脚本里面也有函数的概念，函数可以把一些重复执行逻辑块单独拿出来，通过传递参数到达不同结果，在shell脚本里面有内置函数，也可以自定义函数，本节将介绍内置函数和自定义函数的使用。有时候编写的脚本需要接受外部的输入，这时我们就要失业内置的read函数了，read函数可以从控制台读入值，并且还能设置等待用户输入的超时时间，下面就是一个例子：#!/bin/bash read -t 8 -p "input your name:" NAME echo "Hello $NAME~"basename主要用于显示文件路径名不包括目录部分后的显示文件名，如果指定了后缀参数suffix那么返回就是文件名，如下：basename /Users/ding/jmeter.log .log dirname从给定的文件路径，去掉文件部分返回文件路径部分，文件路径如下：dirname /Users/ding/jmeter.log /Users/ding其实内置函数也就是一个一个内置命令，当然除了内置的一些函数以外，还可以在脚本里面自定义函数，但是函数必须定义在调用者前面，因为脚本是顺序执行的，如下是一个标准的函数定义：#!/bin/bash function sum() { read -t 5 -p "请输入加数:" x read -t 5 -p "请输入加数:" y return $[$x + $y] } sum echo "和是：$?"需要注意的是函数返回值，如果没有使用return关键字，那么返回值则使用最后一行作为返回值。CUT命令cut是内置的命令，主要用于在文件中负责剪切数据用的，cut可以在文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出。例如下面通过ifconfig命令的内容，要把网卡的本地ip截取出来，内容如下：$: ifconfig lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP> inet 127.0.0.1 netmask 0xff000000 inet6 ::1 prefixlen 128 inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 nd6 options=201<PERFORMNUD,DAD> gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280 stf0: flags=0<> mtu 1280 XHC20: flags=0<> mtu 0 XHC0: flags=0<> mtu 0 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 options=400<CHANNEL_IO> ether 8c:85:90:c4:67:da inet6 fe80::ca2:121e:899c:3504%en0 prefixlen 64 secured scopeid 0x6 inet 192.168.31.221 netmask 0xffffff00 broadcast 192.168.31.255 nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active en1: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 options=460<TSO4,TSO6,CHANNEL_IO> ether 82:87:9a:86:cc:01 media: autoselect <full-duplex> status: inactive en2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 options=460<TSO4,TSO6,CHANNEL_IO> ether 82:87:9a:86:cc:00 media: autoselect <full-duplex> status: inactive bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 options=63<RXCSUM,TXCSUM,TSO4,TSO6> ether 82:87:9a:86:cc:01 Configuration: id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 ipfilter disabled flags 0x0 member: en1 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 7 priority 0 path cost 0 member: en2 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 8 priority 0 path cost 0 nd6 options=201<PERFORMNUD,DAD> media: <unknown type> status: inactive现在需要截取出来eno的ip地址：ifconfig en0 | grep "inet " | cut -d " " -f 2 192.168.31.221SED流式编辑器sed是内置的一款流式编辑器，它可以把内容读取到内存里面缓冲区里，然后在对每行内容进行操作，然后把结果输出到标准输出，不会影响到原来的文件，如果需要转存可以使用重定向输出，常用的一些操作有：public class HelloWorld { public static void main(String[] args) { System.out.println("HelloWorld!"); } }现在有一块Java源代码文件，需要使用sed对该文件的一些操作，如下：#替换HelloWorld为中文 $: sed -e 's/HelloWorld!/您好，世界！/' HelloWorld.java public class HelloWorld { public static void main(String[] args) { System.out.println("您好，世界!"); } }好现在把源代码添加多一点方便，后面测试，现在内容如下：[root@aliyun-centos7 developer]# cat /root/helloworld.java public class helloworld { public static void main(String[] agrs) { // 让程序输出双引号中的字符串 System.out.println("Hello World!"); System.out.println("Hello World!"); System.out.println("Hello World!"); System.out.println("Hello World!"); System.out.println("Hello World!"); System.out.println("Hello World!"); System.out.println("Hello World!"); } }下面是删除第三行，并且在第二行添加新的内容：$: sed -e "3d" -e '2a\\t\t\// 修改了第3行注释' /root/helloworld.java public class helloworld { public static void main(String[] agrs) { // 修改了第3行注释 System.out.println("Hello World!"); } }sed '5,9d' /root/helloworld.java删除文件的第九行然后把内容输出到标准输出上sed '4a\\t\tSystem.out.println("您好！世界！");' /root/helloworld.java 在某一行向后插入内容nl /root/helloworld.java | sed '5,9d' 列出文件行号，并且从第5行删到第9行！[root@aliyun-centos7 developer]# nl /root/helloworld.java | sed '5,9d' 1 public class helloworld { 2 public static void main(String[] agrs) { 3 // 让程序输出双引号中的字符串 4 System.out.println("Hello World!"); 10 System.out.println("Hello World!"); 11 System.out.println("Hello World!"); 12 } 13 }在第五行之前插入内容i前 a后[root@aliyun-centos7 ~]# nl helloworld.java | sed '5i\\t\t\tSystem.out.println("这是通过sed命令插入的内容！！");' | nl 1 1 public class helloworld { 2 2 public static void main(String[] agrs) { 3 3 // 让程序输出双引号中的字符串 4 4 System.out.println("Hello World!"); 5 System.out.println("这是通过sed命令插入的内容！！"); 6 5 System.out.println("Hello World!"); 7 6 System.out.println("Hello World!"); 8 7 System.out.println("Hello World!"); 9 8 System.out.println("Hello World!"); 10 9 System.out.println("Hello World!"); 11 10 System.out.println("Hello World!"); 12 11 System.out.println("Hello World!"); 13 12 } 14 13 } 修改内容c[root@aliyun-centos7 ~]# nl helloworld.java | sed '5c \\t\t\tSystem.out.println("通过sed命令修改的内容！！");' | nl 1 1 public class helloworld { 2 2 public static void main(String[] agrs) { 3 3 // 让程序输出双引号中的字符串 4 4 System.out.println("Hello World!"); 5 System.out.println("通过sed命令修改的内容！！"); 6 6 System.out.println("Hello World!"); 7 7 System.out.println("Hello World!"); 8 8 System.out.println("Hello World!"); 9 9 System.out.println("Hello World!"); 10 10 System.out.println("Hello World!"); 11 11 System.out.println("Hello World!"); 12 12 } 13 13 } AWK 分析工具一款强大的文本分析工具，把文本逐行读入，以空格为默认的方式分隔将其为切片，切开的部分进行分析处理，一般要配合正则表达式使用。#获取Java版本号 $: java -version 2>&1 | sed '1!d' | sed -e 's/"//g' | awk '{print $3}'大部分内置的函数和命令在使用的时候可以去查文档，没有必要记住那么多，不过了解 shell 脚本编写是有必要的！