linux编译器和自动化构建工具(gcc与Makeile)

1.linux编译器

Linux 系统下有多种编译器可供选择,最常用的是 GCC 和 Clang,它们能够编译 C、C++ 等语言并且性能优越。编译器的选择依赖于具体需求,例如编译速度、错误提示等。

1.1 常见的linux编译器

(1)GCC (GNU Compiler Collection):

  • Linux 上最常用的编译器,支持 C、C++、Objective-C、Fortran、Ada 等多种语言。

  • 通常 Linux 系统默认安装,可通过命令 `gcc` 或 `g++` 调用。

(2)Clang:

  • 基于 LLVM 的轻量级编译器,支持 C、C++ 等语言。

  • 错误信息提示更加友好,编译速度快。

(3)Ninja:

  • 小巧高效的构建系统,专为并行构建设计,常配合 CMake 使用以提高构建速度。

自动化构建工具:

Make:

  • Make 是一种构建自动化工具 ,虽然不是编译器,但用于管理编译过程

  • 使用 `Makefile` 组织编译规则,支持大型项目的构建。

CMake:

  • 跨平台构建系统生成器,生成适用于多种编译器的构建文件,常与 Make 或 Ninja 配合使用。

1.2 gcc常见选项

gcc选项

bash 复制代码
-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S  编译到汇编语言,不进行汇编和链接
-c:只编译为目标文件(.o),不链接生成可执行文件。
-o <file>:指定输出文件名称。
-std=<标准>:指定标准版本(例如 `-std=c99`、`-std=c++11`)。
cpp 复制代码
-static 此选项对生成的文件采用静态链接
-g 生成调试信息。GNU 调试器可利用该信息。
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-O0, -O1, -O2, -O3:优化等级,从不优化(O0)到最大优化(O3)。
-w  不生成任何警告信息。
-Wall 生成所有警告信息。
bash 复制代码
-I<目录>:指定头文件搜索路径,例如 `-I /path/to/includes`。
-L<目录> 和 -l<库名>:指定库搜索路径和链接库,例如 `-L /path/to/lib -lmylib`。
-D<宏定义>:在编译时定义宏,例如 `-DDEBUG`。

1.3 gcc如何完成

GCC 的编译过程主要分为四个阶段:预处理、编译、汇编、链接

  1. 预处理:处理 `#include`、`#define` 等预处理指令,生成预处理后的代码文件。

可以使用 `gcc -E file.c -o file.i` 查看预处理结果。

  1. 编译:将预处理后的代码转为汇编代码。可以使用 `gcc -S file.i -o file.s` 查看生成的汇编代码。

  2. 汇编:将汇编代码转为目标文件(机器码),可以使用 `gcc -c file.s -o file.o` 查看目标文件。

  3. 链接:将目标文件与库文件链接生成可执行文件。最终执行 `gcc file.o -o program`,生成 `program` 可执行文件。

(1) 预处理

gcc -E code.c -o code.i

-E:从现在开始进行程序的编译,一旦预处理做完,就停下来

-o:表明指定输出文件的名称

code.i存的是code.c预处理后的结果

展开宏定义,头文件展开 ,删除注释。,生成纯粹的 C 代码文件等。

(2) 编译

gcc -S code.i -o code.s

-S:开始进行程序翻译,编译做完,形成汇编,就停下来

(3) 汇编(生成机器可识别代码)

gcc -c code.s -o code.o

-c:开始进行程序翻译,汇编完成就停下来

code.o:重定位目标二进制文件,无法执行。

(4)链接(生成可执行文件或库文件)

gcc code.o -o code

我们的.o库文件进行连接->可执行

直接操作:

gcc file.c:生成目标文件的可执行程序

gcc -c :生成目标文件的同名.o文件

1.4 动静态库简介

在 Linux 上,静态库和动态库的创建和使用方式有所不同,以下是如何生成和链接这两种库的详细步骤。

在Linux中,ldd命令用于显示一个可执行文件或共享库 所依赖的共享库及其路径。file命令用于识别文件的类型。它通过检查文件的内容而不仅仅是文件扩展名来确定文件类型。

这里的dynamically表示a.out文件是动态链接的。

gcc默认的可执行程序是动态链接的。

1.4.1. 动态库

动态库在程序运行时加载,不会将库的内容嵌入可执行文件。动态库的文件扩展名通常为 `.so`(shared object)。

生成动态库时需要使用 `-fPIC` 和 `-shared` 选项。假设我们仍然使用 `file1.c` 和 `file2.c`。

  1. 编译源文件:使用 `-fPIC` 选项生成位置无关代码(Position-Independent Code)。
cpp 复制代码
   gcc -fPIC -c file1.c -o file1.o
   gcc -fPIC -c file2.c -o file2.o

在Linux中,ldd命令用于显示一个可执行文件或共享库所依赖的共享库及其路径。

  1. 创建动态库:使用 `-shared` 选项将目标文件打包成动态库。
cpp 复制代码
   gcc -shared -o libmylib.so file1.o file2.o

1.4.2 静态库

静态库是一种在编译时将库代码嵌入到可执行文件中的库。生成静态库的文件扩展名通常为 `.a`。

gcc -static

这里就变成了(statically linked)静态链接

Linux静态库安装命令:

sudo yum install glibc-static(c静态库) libstdc++-static(c++静态库) -y

1.4.3. 静态链接和动态链接的区别

  • 静态链接:在编译时将库直接嵌入可执行文件生成的可执行文件不依赖库文件 。缺点是生成的文件较大 ,更新库后需要重新编译。

  • 动态链接:在运行时加载库文件节省了可执行文件的体积 ,并且库更新后无需重新编译即可生效 。缺点是程序运行时需要能够访问到库文件,动态库是共享的,一旦丢失,所有依赖动态库的程序都会运行出错

2.自动化构建工具 -make/Makefile

在 Linux 环境下,`make` 和 `Makefile` 是常用的自动化构建工具。`make` 是一种构建工具,而 `Makefile` 则是其构建规则的定义文件。以下是它们的基础知识和用法:

  • make:`make` 工具读取并执行 `Makefile` 中的规则,以自动化处理项目的构建和管理流程。它主要用于编译代码,但也可以用于执行其他任务。

  • Makefile:`Makefile` 是一个文本文件,包含了构建规则、依赖项和命令。每次运行 `make` 时,它会依据 `Makefile` 中的规则自动化执行各种构建步骤。

2.1 Makefile 的基础结构

一个 `Makefile` 通常包含三个主要部分:

  • 目标(target):即要生成的文件或执行的任务,例如可执行文件。

  • 依赖项(dependencies):生成目标文件所需的文件或资源,如源代码文件。

  • 命令(commands):每当依赖项更新时,用于生成目标的命令。

我们只需make一下,便可执行对目标文件的依赖关系执行依赖方法

在命令行中使用 `make` 命令时可以指定目标,例如:

make # 默认执行第一个目标,即生成 main

make clean # 执行 clean 目标,清理构建文件

2.2 Makefile 中的伪目标

伪目标(phony target)是没有文件依赖的目标用于定义清理、安装等操作 。可以使用 `.PHONY` 来声明伪目标:

cpp 复制代码
.PHONY: clean
clean:
    rm -f *.o main   //*.o表示所有以.o为扩展名的文件

删除mytest

make clean:删除mytest

make会自动向下扫描makefile文件,默认形成第一个目标文件

在我们将clean移动到最上面后,默认执行clean

2.3 文件的时间属性

接上面,如果我们多次使用make就会出现以下情况。这是因为编译器会对比源文件和可执行文件的修改时间,如果源文件未被修改,则不能也没必要继续make,提高了编译效率。

那么这个修改时间的对比是怎么判定的呢?其实是通过文件的时间属性来比较的。

在 Linux 中,文件的三个时间属性分别是:

  1. Access Time (atime):最后一次访问文件的时间。任何读取文件内容的操作(如 `cat`、`less` 等)都会更新 `atime`,但并不会修改文件内容。

  2. Change Time (ctime):最后一次更改文件属性的时间。当文件的元数据(如权限、所有者)发生变化时,`ctime` 会更新。更改文件内容(从而影响 `mtime`)也会更新 `ctime`。

  3. Modify Time (mtime):最后一次修改文件内容的时间。当文件内容发生更改(例如编辑文件内容并保存)时,`mtime` 就会更新。

可以使用`stat` 命令查看文件的这些时间属性:该命令会输出文件的详细信息,包括 `Access`、`Modify` 和 `Change` 时间。

下面修改了test.c的内容,增加删除文件的内容会使文件内存发生改变,所以Change改变了,文件 = 内容 + 属性。

make是否执行对比的就是**mtime,**mtime改变了才可以继续make。

这里我们用touch更新一下文件的三个时间属性,可以执行make。

2.4 make的推导原则

makefile会进行依赖关系的推导,直到依赖文件是存在的

make 会递归检查并推导每个目标文件的依赖关系,一层层往下直到找到存在的文件为止。从最初的目标 `mytest` 开始,`make` 会检查所有依赖文件是否存在或需要更新,并在每一步中执行对应的命令来生成缺少的文件或更新需要重新生成的文件。

cpp 复制代码
1 mytest: test.o
2     gcc test.o -o mytest

3 test.o: test.s
4     gcc -c test.s -o test.o

5 test.s: test.i
6     gcc -S test.i -o test.s

7 test.i: test.c
8     gcc -E test.c -o test.i

(1)执行过程分析

当运行 `make mytest` 时,`make` 会依次按照以下步骤进行依赖关系的递归推导:

  1. 检查目标 mytest:
  • 目标 `mytest` 依赖于 `test.o`。

  • 如果 `test.o` 不存在或比 `mytest` 更加新,`make` 会继续推导生成 `test.o` 的依赖关系。

  1. 检查目标 `test.o`:
  • 目标 `test.o` 依赖于 `test.s`。

  • 如果 `test.s` 不存在或比 `test.o` 更加新,`make` 会继续推导生成 `test.s` 的依赖关系。

  1. 检查目标 `test.s`:
  • 目标 `test.s` 依赖于 `test.i`。

  • 如果 `test.i` 不存在或比 `test.s` 更加新,`make` 会继续推导生成 `test.i` 的依赖关系。

  1. 检查目标 `test.i`:
  • 目标 `test.i` 依赖于 `test.c`。

  • 如果 `test.c` 存在且是最新的,`make` 将执行生成 `test.i` 的命令。

(2)每个步骤的命令执行

  • 第 8 行命令:`gcc -E test.c -o test.i`

  • 执行此命令生成 `test.i`。

  • 第 6 行命令:`gcc -S test.i -o test.s`

  • 一旦生成了 `test.i`,`make` 会回到上一级的依赖关系,执行生成 `test.s` 的命令。

  • 第 4 行命令:`gcc -c test.s -o test.o`

  • 生成 `test.s` 后,`make` 会继续执行生成 `test.o` 的命令。

  • 第 2 行命令:`gcc test.o -o mytest`

  • 最后,生成 `test.o` 后,`make` 执行生成 `mytest` 的命令,得到最终的可执行文件。

(3)总结

`make` 从目标 `mytest` 开始,递归查找每个依赖文件的生成规则,直到找到现有文件为止(在这个例子中,最终是 `test.c`)。

2.5 使用变量

`Makefile` 中的变量用于减少重复、简化代码编写,并提高可读性。通过定义和使用变量,你可以在 `Makefile` 中更容易地更改编译选项、文件路径等。

(1)常用变量

  1. 用户自定义变量:
  • BIN:用来表示生成的可执行文件或二进制文件的名称或路径。

  • `CC`:编译器,例如 `gcc`、`g++`。

  • `CFLAGS`:编译器选项,例如 `-Wall`、`-O2`。

  • `LDFLAGS`:链接器选项,例如 `-lm`、`-lpthread`。

  • `SRC`:源文件列表。

  • `OBJ`:目标文件列表。

  • `TARGET`:生成的可执行文件名称。

cpp 复制代码
   - `CC := gcc`:使用 `gcc` 作为编译器。
   - `CFLAGS := -Wall -g`:定义编译选项,开启所有警告(`-Wall`)和调试信息(`-g`)。
   - `SRC` 和 `OBJ` 分别定义源文件和目标文件列表。
   - `TARGET` 定义最终的可执行文件名称。
  1. 自动化变量:
  • $(自定义变量):访问自定义变量。

  • `$@`:当前的目标文件。

  • `$<`:第一个依赖文件,即当前的 `.c` 文件。

  • `$^`:所有依赖文件,不包含重复项。

  • `$+`:所有依赖文件,包含重复项。

  • `$?`:比目标文件更新的依赖文件列表。

  • `$*`:目标文件的基名,不含后缀。

  • `$$`:表示一个字面上的 `$` 符号(例如在 shell 命令中使用)。

cpp 复制代码
   - `$(CC)`、`$(CFLAGS)` 等通过 `$()` 语法引用变量。
   - `$(TARGET)` 表示最终生成的目标文件,`$(OBJ)` 表示所有的目标文件对象。

在 Makefile 中,自动化规则(也称为模式规则)使用 % 符号来匹配任意文本,这样可以定义一类文件的生成方式,而不必为每个文件单独写一条规则。自动化规则通常用于编译多个源文件,减少重复代码,提高 Makefile 的简洁性。

3.自动化规则的语法

cpp 复制代码
%.o: %.c
    <命令>

%表示匹配任意内容。%.o 是目标模式,表示任何以 .o 结尾的目标文件。%.c 是依赖模式,表示任何与目标文件同名但以 .c 结尾的依赖文件。<命令> 是生成目标文件所需的命令。

  1. 预定义变量:
  • `MAKE`:`make` 命令自身,常用于递归调用。

  • `MAKEFLAGS`:传递给 `make` 的选项和参数。

  • `SHELL`:指定使用的 shell,一般是 `/bin/sh`。

(2)赋值操作

  1. 简单赋值(`:= `):立即求值。
  • 定义时就计算变量的值,适用于希望变量值立即固定的情况。
  1. 递归赋值(`= `):延迟求值。
  • 直到变量被使用时才会计算其值,适用于依赖其他变量的动态内容。
  1. 条件赋值(`?=`):只有在变量未定义时才赋值。
  • 如果变量已被赋值,则保持原值。
  1. 追加赋值(`+=`):向已有变量添加内容。
  • 常用于给现有变量添加新的值或选项。

(3)实例解析

这个Makefile的目的是通过`gcc`编译器来编译和链接一个C程序。它定义了目标文件、源文件和清理指令的规则。

变量定义部分

cpp 复制代码
 BIN=mytest      # 可执行文件名,生成的二进制文件名为mytest
 SRC=test.c      # 源文件名,这里是test.c
 OBJ=mytest.o    # 对象文件名,即编译test.c生成的对象文件为mytest.o
 CC=gcc          # 编译器指定为gcc
 RM=rm -f        # 删除命令,`rm -f`用于删除文件(不会提示确认删除)

规则定义部分

cpp 复制代码
 $(BIN):$(OBJ)   # 目标是可执行文件$(BIN),依赖于对象文件$(OBJ)
     gcc $(OBJ) -o $(BIN)    # 链接命令:将对象文件链接为可执行文件mytest
  • 这一部分的规则表示,`(BIN)\`(即 \`mytest\`)的生成依赖于\`(OBJ)`(即`mytest.o`)。当`$(OBJ)`发生变化时,就会重新执行第8行的链接命令生成可执行文件。

  • `gcc (OBJ) -o (BIN)` 这一行表示当 `(BIN)\`(即 \`mytest\`)需要更新时,会调用 \`gcc (OBJ) -o (BIN)\`,其中 \`(OBJ)` 是对象文件 `mytest.o`,最终生成可执行文件 `mytest`。

cpp 复制代码
 $(OBJ):$(SRC)   # 目标是对象文件$(OBJ),依赖于源文件$(SRC)
     gcc -c $(SRC) -o $(OBJ)    # 编译命令:将源文件编译成对象文件

这里定义了如何从源文件生成对象文件。当 `(OBJ)\`(即 \`mytest.o\`)需要更新时,执行 \`gcc -c (SRC) -o $(OBJ)`。`-c` 表示编译而不链接,仅生成对象文件。

清理规则

cpp 复制代码
 .PHONY:clean   # 声明clean为伪目标
 clean:         # clean目标的定义
     $(RM) $(BIN) $(OBJ)    # 执行清理操作,删除可执行文件和对象文件

这里定义了一个"伪目标" `clean`,用于清理生成的文件。执行 `make clean` 时,会调用 `rm -f mytest mytest.o` 来删除可执行文件和对象文件。`.PHONY: clean` 表示 `clean` 只是一个命令,不是实际的文件目标。

总结

这个Makefile的执行流程是:

  1. `make`:首先检查 `mytest` 是否需要更新。如果 `mytest` 不存在或 `mytest.o` 有更改,则会重新编译和链接。

  2. `make clean`:删除生成的文件 (`mytest` 和 `mytest.o`)

2.6 通过变量是makefile更加通用

2.6.1 获取当前目录下所有的.c文件

(1)SRC=$(shell ls *.c) :从当前目录中获取所有.c文件的列表并将其赋值给SRC变量。

- shell:shell 是 Makefile 中的一个内置函数,在 Makefile 中用于执行一个 Shell 命令,并将其输出结果作为字符串返回

- ls *.c :这个命令列出当前目录下的所有以 .c 结尾的文件。

(2)SRC=$(wildcard *.c):获取当前目录下所有的 .c 文件并将其赋值给SRC变量

wildcard是一个函数,用于获取匹配特定模式的文件名列表

下面echo打印了两次是由于命令的回显。@符号用于抑制命令的回显。具体来说,当你在Makefile中定义一个规则时,如果在命令前面加上@,这个命令在执行时不会在终端中显示出来。这样可以使输出结果更加干净,特别是在你不想显示具体的命令时。

2.6.2 编译

(1)编译成.o文件

(2)生成可执行程序

  • `$@`:当前的目标文件列表。(也就是下面的BIN)

  • `\^\`:依赖关系中的所有依赖文件,不包含重复项。((OBJ))

  • `\<\`:第一个依赖文件,即当前的 \`.c\` 文件。(下面的<表示将.c文件一个一个的生成.o文件)

  • %表示匹配任意内容。%.o 表示任何以 .o 结尾的目标文件。%.c 表示任何与目标文件同名但以 .c 结尾的依赖文件。

cpp 复制代码
$(BIN):$(OBJ)
     $(CC) $^ -o $@
等价于
$(BIN):$(OBJ)
     gcc $(OBJ) -o $(BIN)
cpp 复制代码
%.o:%.c
    $(CC) -c $<
mytest.o:test.c                                                                                   
    gcc -c test.c -o mytest.o

一个文件

多个文件

make:将所有文件自动编程.o文件

清理:

再来一个示例:

添加一百个文件,直接make即可生产所有文件的.o文件。这样是不是makefile是不是更加通用了?

上面为了让大家更加清晰的看到命令的运行,所以没有隐藏命令的回显。使用@隐藏命令回显

使用 `make` 的好处

  • 自动化构建:当源文件发生更改时,`make` 可以自动构建项目。

  • 增量构建:`make` 仅会重新编译发生变化的文件,节省时间。

  • 模块化和可维护性:通过 `Makefile`,项目的构建过程更加清晰、易于维护。

3. 缓冲区

在Linux系统中,行缓冲区是一种输入输出缓冲方式,主要影响标准输入输出流的处理方式。行缓冲的特点是:当缓冲区收到一个完整的行(通常是遇到换行符 `\n`)时,才会将缓冲的内容进行处理。以下是一些关于行缓冲的详细信息:

(1)缓冲区的类型

在Linux中,标准输入输出流(`stdin`、`stdout` 和 `stderr`)通常有三种缓冲模式:

  1. 行缓冲:每当接收到换行符 `\n`,或缓冲区满了 ,数据才会被刷新。这种模式通常应用于终端交互的标准输入和标准输出。(stdin和stdout的默认缓冲模式

  2. 全缓冲:数据会在缓冲区满时才进行输出刷新,适用于文件等不需要频繁刷新的情况。

  3. 无缓冲:数据一旦写入就会立即输出,这通常应用于 `stderr`(标准错误输出),以确保错误信息能即时反馈。

我们主要介绍行缓冲模式。

(2)行缓冲的工作原理
当使用行缓冲时,数据会先暂存在缓冲区中 ,直到满足以下条件之一时 才会将数据输出或处理:

  • 遇到换行符 `\n`

  • 缓冲区已满

  • 手动调用 `fflush(stdout)` 来刷新缓冲区

1. 使用\n,立即输出内容

  1. 两秒后输出

  1. 手动调用 `fflush(stdout)` 来刷新缓冲区

(3)利用缓冲区实现倒计时

  1. 每输出一个数,刷新一下缓冲区。(/r是回车符,使光标移动到行开头)
  1. 输出多位倒计时

多位数要使用%-2d才能得到我们想要的效果

4. 实现进度条

main.c

cpp 复制代码
  1 #include "process.h"
  2 #include <unistd.h>
  3 #include <string.h>
  4
  5 #define SIZE 101
  6 #define STYLE '='
  7 //V1:展示进度条的基本功能
  8 void process()
  9 {
 10   int rate = 0;
 11   char buffer[SIZE];
 12   const char *lable = "|/-\\";
 13   memset(buffer, 0, sizeof(buffer));
 14   int len = strlen(lable);
 15 
 16   while (rate <= 100)
 17   {
 18     printf("[%-100s][%d%%][%c]\r", buffer, rate, lable[rate%len]);
 19     fflush(stdout);
 20     buffer[rate] = STYLE;
 21     rate++;
 22     usleep(50000);
 23   }
 24   printf("\n");
 25 }
 26 
 27 //V2
 28 void FlushProcess(double total, double current)
 29 {
 30   const char *lable = "|/-\\";                                                                                                                                                                                                   
 31   int len = strlen(lable);
 32   static int index = 0;
 33   char buffer[SIZE];
 34   memset(buffer, 0, sizeof(buffer));
 35 
 36   double rate = current*100.0/total;
 37   int num = (int)rate;
 38 
 39   int i = 0;
 40   for(i = 0; i < num; i++)
 41     buffer[i] = STYLE;
 42 
 43   printf("[%-100s][%.1lf%%][%c]\r", buffer, rate, lable[index++]);
 44   fflush(stdout);
 45   index %= len;
 46 
 47   if (num >= 100) printf("\n");
 48 }

process.c

cpp 复制代码
    1 #include "process.h"
    2 #include <unistd.h>
    3 #include <time.h>
    4 #include <stdlib.h>
    5                                                                                                                                                                                                                                
    6 double total = 1024.0;
    7 double speed[] = {1.0, 0.1, 0.2, 0.01, 0.5, 10.0};
    8 
    9 void download(double total)
   10 {
   11     srand(time(NULL));
   12     double current = 0.0;
   13     while (current <= total)
   14     {
E> 15       FlushProcess(total, current);
   16       if(current>=total) break;
   17       int random = rand() % 6;
   18       usleep(5000);
   19       current += speed[random];
   20       if(current>=total) current = total;
   21     }
   22   }
   23 int main()
   24 {
   25     download(1024.0);
   26     printf("download 1024.0MB done\n");
   27     download(512.0);
   28     printf("download 512.0MB done\n");
   29     download(256.0);
   30     printf("download 256.0MB done\n");
   31     download(128.0);
   32     printf("download 128.0MB done\n");
   33     return 0;
   34 }
  ~

process.h

cpp 复制代码
  1 #pragma once
  2 #include <stdio.h>
  3 #include <unistd.h>
  4 void process();
  5 void FlushProcess();         
相关推荐
冼紫菜15 分钟前
[特殊字符]CentOS 7.6 安装 JDK 11(适配国内服务器环境)
java·linux·服务器·后端·centos
Chuncheng's blog1 小时前
RedHat7 如何更换yum镜像源
linux
爱莉希雅&&&1 小时前
shell脚本之条件判断,循环控制,exit详解
linux·运维·服务器·ssh
wei_work@2 小时前
【linux】Web服务—搭建nginx+ssl的加密认证web服务器
linux·服务器·ssl
扶尔魔ocy2 小时前
【Linux C/C++开发】轻量级关系型数据库SQLite开发(包含性能测试代码)
linux·数据库·c++·sqlite
Sylvan Ding3 小时前
远程主机状态监控-GPU服务器状态监控-深度学习服务器状态监控
运维·服务器·深度学习·监控·远程·gpu状态
慢一点会很快3 小时前
【vscode】解决vscode无法安装远程服务器插件问题,显示正在安装
服务器·ide·vscode
追赶sun3 小时前
Ubuntu 添加系统调用
linux·ubuntu·操作系统·系统调用
北漂老男孩4 小时前
在 Linux 上安装 MATLAB:完整指南与疑难解决方案
linux·运维·matlab
Why not try?!4 小时前
Centos7 中 Docker运行配置Apache
运维·docker·容器