参考:extern "C"语句在C++中的作用

一、问题描述

在编译C++程序时,遇到以下问题:

/tmp/cccZeFer.o: In function `main':
main.cpp:(.text+0xf): undefined reference to `****'
collect2: error: ld returned 1 exit status

看到错误的第一直觉是共享库出问题了,因为以前出现这个问题都是因为库没有加进来,但是反复确认过后发现共享库并没有问题。

第一:编译的时候使用-l选项包含了库文件,并且库里面的函数也存在。

第二:库确实存在,不然也不会报上面的错误了,报的错误应该是:

/usr/bin/x86_64-linux-gnu-ld: cannot find -l***
collect2: error: ld returned 1 exit status

试了各种方法都无效,百思不得其解,最后无意间发现竟然是c和c符号表不兼容导致的。

因为库是c编译的,代码是c编译的,c和c的符号表规则不一致,导致编译c时找不到符号,因此编译报错。

二、重现

准备一个库libadd.so和一个源文件main.cpp

> tree
.
├── libadd
│   ├── add.c
│   ├── add.h
│   └── Makefile
├── main.cpp
└── Makefile

1 directory, 5 files

add.hadd.c的内容:

> cat libadd/add.h
int add(int i, int j);

> cat libadd/add.c
#include "add.h"

> cat libadd/Makefile
app:
    gcc add.c -fPIC -shared -o libadd.so

编译libadd.so能够正常编译,然后编译main:

> cat main.cpp 
#include "libadd/add.h"
#include <iostream>

int main() {
    std::cout << add(1, 2) << std::endl;
    return 0;
}

> cat Makefile 
app:
    g++ main.cpp -Llibadd -ladd

此时编译就报错:

/tmp/cccZeFer.o: In function `main':
main.cpp:(.text+0xf): undefined reference to `add'
collect2: error: ld returned 1 exit status

解决方案

在add.h中使用宏定义把函数声明为c导出的函数:

#ifdef __cplusplus
extern "C" {
#endif

int add(int i, int j);

#ifdef __cplusplus
}
#endif

再编译就能通过了。

作者:LeetCode

链接:112. 路径总和

来源:力扣(LeetCode)

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一、题目描述

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明:叶子节点是指没有子节点的节点。

示例:

给定如下二叉树,以及目标和sum = 22

              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1

返回true, 因为存在目标和为 22 的根节点到叶子节点的路径5->4->11->2

二、题解

2.1 dfs深搜(递归)

算法:

利用递归深度优先搜索每个节点,每经过一个节点,sum值减去当前节点的值,继续搜索子节点。直到叶子节点的时候判断sum,为0返回true,否则返回false。

代码:

class Solution {
public:
    bool hasPathSum(TreeNode* root, int sum) {
        if (root == NULL) {
            return false;
        }
        sum -= root->val;

        // 搜索到叶子节点了
        if (root->left == NULL && root->right == NULL) {
            return sum == 0;
        }

        // 计算左右子节点
        return hasPathSum(root->left, sum) || hasPathSum(root->right, sum);
    }
};

imagef9574f9026222914.png

复杂度分析

  • 时间复杂度:我们访问每个节点一次,时间复杂度为O(N),其中N是节点个数。
  • 空间复杂度:最坏情况下,整棵树是非平衡的,例如每个节点都只有一个孩子,递归会调用N次(树的高度),因此栈的空间开销是O(N) 。但在最好情况下,树是完全平衡的,高度只有 log(N),因此在这种情况下空间复杂度只有O(log(N)) 。

2.2 bfs广搜(迭代)

算法:

我们可以用栈将递归转成迭代的形式,深度优先搜索在除了最坏情况下都比广度优先搜索更快。最坏情况是指满足目标和的 root->leaf 路径是最后被考虑的,这种情况下深度优先搜索和广度优先搜索代价是相通的。

使用迭代的思路是:利用队列,每次保存当前节点以及剩余的sum,如果是叶子节点并且剩余的sum为0则返回true。否则,把节点的左右子节点分别压入队列,更新sum值。

所以我们从包含根节点的栈开始模拟,剩余目标和为 sum - root.val,然后开始迭代。弹出当前元素,如果当前剩余目标和为 0 并且在叶子节点上返回 True;如果剩余和不为零并且还处在非叶子节点上,将当前节点的所有孩子以及对应的剩余和压入栈中。

struct NodeSum {
    TreeNode *node; // 当前节点
    int sum; // 剩余的和
};

class Solution {
public:
    bool hasPathSum(TreeNode* root, int sum) {
        queue<NodeSum *> q;
        NodeSum* p;

        TreeNode *node;
        int residualSum;

        if (root == NULL) {
            return false;
        }
        q.push(new NodeSum{ root, sum });

        while (q.empty() == false) {
            p = q.front();
            q.pop();

            node = p->node;
            // 计算剩余需要的sum
            residualSum = p->sum - node->val;
            delete p;
            
            // 存在左子节点,压入队列
            if (node->left) {
                q.push(new NodeSum{ node->left, residualSum });
            }
            // 存在右子节点,压入队列
            if (node->right) {
                q.push(new NodeSum{ node->right, residualSum });
            }
            
            // 叶子节点,sum为0
            if (node->left == NULL && node->right == NULL && residualSum == 0) {
                return true;
            }
        }
        return false;
    }
};

复杂度分析:

  • 时间复杂度:和递归方法相同是O(N)。
  • 空间复杂度:当树不平衡的最坏情况下是O(N) 。在最好情况(树是平衡的)下是 O(log(N))。

一、 问题描述

最近工作中遇到了一个问题:项目需要合入其他部门的模块,但是其中的一个共用共享库被更新了。因为项目很大,如果直接在我们的环境中替换更新这个库,很有可能会影响到其他模块。祖传的代码流传了差不多20年,涉及的模块也十分之多,贸然升级的风险很难评估。但是不替换这个库第三方模块又跑不起来,一度头痛。

刚开始是想到了以下几个方法:

  1. 设置LD\_LIBRARY\_PATH环境变量,修改查找路径的优先级。
  2. 修改so库名

对于第一种方法是有效的:在程序目录下加个lib目录,然后 export LD_LIBRARY_PATH=pwd/lib:$LD_LIBRARY_PATH 把当前路径放到第一搜索顺序,能解决这个问题。但是,环境变量时当前所有程序共享的,其他程序的搜索目录也是这里第一,所以这里的结果就和直接升级库没有区别。同样,修改/etc/ld.so.conf的原理也是一样。

第二种方法本来是认为比较靠谱的,但是测试发现共享库并不是根据文件名来的,修改库名无效。so库内部还有个真实的名字,可以通过 readelf -d lib*.so 来查看:

> readelf /usr/lib/libau.so -d

Dynamic section at offset 0x2c88 contains 29 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000e (SONAME)             Library soname: [libau.so.2]

所以上面两种方法就被派出了,后面查了很久也没有找到合适的办法,崩溃。。。

最后去请教大佬就被告知了可以使用 -Wl,-rpath 选项来解决这个问题,试了一下确实可以。

二、-Wl,-rpath和-Wl,-rpath-link选项

2.1 -Wl,-rpath

加上 -Wl,-rpath 选项的的作用就是指定“程序运行时”的库搜索目录,是一个链接选项,生效于设置环境变量之前。

我们已经知道,共享库的查找顺序为:

  1. LD_LIBRARY_PATH 环境变量的目录
  2. ld.so.conf 高速缓冲文件中的目录
  3. 系统的默认库目录如 /lib, /lib64

-Wl,-rpath 可以让程序在第一步搜索之前先搜索它所指定的目录,通过一个例子来说明:

// add.h
int add(int i, int j);

// add.c
#include "add.h"

int add(int i, int j) {
    return i + j;
}

// main.c
#include <stdio.h>
#include "add.h"

int main() {
    printf("1 + 2 = %d\n", add(1, 2));
    return 0;
}

add.h和add.c用于生成一个so库,实现了一个简单的加法,main.c中引用共享库计算1 + 2:

# 编译共享库
gcc add.c -fPIC -shared -o libadd.so
# 编译主程序
gcc main.o -L. -ladd -o app

编好后运行依赖库:

> ldd app 
    linux-vdso.so.1 (0x00007ffeb23ab000)
    libadd.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007febb7dd0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007febb83d0000
> ./app 
./app: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

可以看到,libadd.so这个库是没有找到的,程序也无法运行,要运行它必须要把当前目录加到环境变量或者库搜索路径中去。

但是如果在链接的时候加上 -Wl,-rpath 选项之后:


> gcc -Wl,-rpath=`pwd` main.o -L. -ladd -o app
> ldd app 
    linux-vdso.so.1 (0x00007fff8f4e3000)
    libadd.so => /data/code/c/1-sys/solib/libadd.so (0x00007faef8428000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faef8030000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faef8838000)
> ./app 
1 + 2 = 3

依赖库的查找路径就找到了,程序能正常运行。

2.2 -Wl,rpath-link

-Wl,rpath-link 是设置编译链接时候的顺序,例如app运行依赖libadd.so,但是libadd.so又依赖libadd\_ex.so,rpath-link 就是指定libadd\_ex.so的路径。和 -Wl,rpath 相比工作的时间不同,一个在链接期间,一个在运行期间。

三、其他相关

程序从编译和链接的过程

linux静态库和动态库的使用方法

‘xxx’: error while loading shared libraries的解决方案

CentOS无法启动,启动分区无法找到,然后就报了个堆栈信息:

ACPI: wmi: Mapper loaded
dracut Warning: No root device "block: /dev/sda4" found
dracut Warning: Boot has failed. To debug this issue add "rdshell" to the kernel command line.

dracut Warning: Signal caught!
dracut Warning: Boot has failed. To debug this issue add "rdshell" to the kernel command line.

kernel Panic - not syncing: Attempted to kill init!
Pid: 1, comm: init Tainted: G I-------2.6.32-358.el6.x86_64 #1
Call Trace:
[<ffffffff8150cfc8>]? panic+0xa7/0x16f
[<ffffffff81073ae2>]? do_exit0x25/0x870
[<ffffffff81182885>]? fput_+0x25/0x30
[<ffffffff81073b48>]? do_group_exit+0x58/0xd0
[<ffffffff81073bd7>]? sys_exit_group+0x17/0x20
[<ffffffff8100b072>]? system_call_fastpath+0x16/x1b
Panic occurred, switching back to text console
*note1*: block device sought is not shown in /dev/fstab.

看样子是磁盘找不到了,想想前不久加了个磁盘装了其他的系统,会不会是影响了分区。

然后进去到另外的ubuntu系统,查看分区表:

image.png

发现分区全部挂在了sdb,然而实际上最开始装系统的时候磁盘应该是sda:

image.png

分析了一下分区信息,其中 sdb1-sdb7 应该就是我的CentOS分区了,50G的sdb4就是根分区,先把它挂载到当前系统。

ma@Y485:~$ sudo mkdir /sdb4
ma@Y485:~$ sudo mount /dev/sdb4 /sdb4/
ma@Y485:~$ cat /sdb4/etc/fstab 
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
# / was on /dev/sda12 during installation
UUID=8c9c0656-bd8a-41e0-8aae-43eaf8938227 /               ext4    errors=remount-ro 0       1
UUID=6b60d3c1-221b-48de-9819-eb41cfbdc0cc /boot           ext4    defaults        0       2
UUID=BAE5-8056  /boot/efi       vfat    umask=0077      0       1
UUID=122bd403-dd15-4616-a5ee-95b3fbeba590 /data           ext4    defaults        0       2

发现所有的分区都是通过ID来标记的,因此基本定位到问题的原因为:添加新磁盘后,之前的磁盘变成了sdb分区,然而系统里面的磁盘ID还是指向开始的sda分区。就导致了分区找不到,系统无法启动。所以最后解决方法就是把所有的UUID都改成当前的分区号:

/dev/sdb4 /                       ext4    defaults        1 1
/dev/sdb2 /boot                   ext4    defaults        1 2
/dev/sdb1 /boot/efi               vfat    umask=0077,shortname=winnt 0 0
/dev/sdb6 /data                   ext4    defaults        1 2
/dev/sdb7 /home                   ext4    defaults        1 2
/dev/sdb5 /usr/local              ext4    defaults        1 2
/dev/sdb3 swap                    swap    defaults        0 0
...

保存重启,然后就好了。

c语言可变长参数传递问题

一、问题描述

C语言中的函数提供了一种可变长参数机制,这个机制使得我们在操作的时候充分自定义自己的功能,例如使用最多的printf函数:

printf("%s: %d", "HelloWorld", 10);

它的函数声明为:

printf(const char *fmt, ...);

其中的...就代表不固定的参数,使用起来十分方便。但是在函数嵌套的时候,不能直接使用...来占位,例如:

#define logerr(s, ...) do { fprintf(stderr, s, ...); } while (0)

编译时就会报错:

va_args.c:4:50: error: expected expression before ‘...’ token
 #define logerr(s, ...) do { fprintf(stderr, "s", ...); } while (0)

如果要嵌套使用,需要通过宏__VA_ARGS__完成:

#define logerr(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

二、参数个数为0的问题

使用上面的方法,参数个数为0的时候编译也会报错:

#define logdbg(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

int main() {
    logdbg("HelloWorld\n");
    return 0;
}

编译报错:

> gcc va_args.c  -o debug/va_args
va_args.c: In function ‘main’:
va_args.c:5:59: error: expected expression before ‘)’ token
 #define logdbg(s, ...) do { fprintf(stderr, s, __VA_ARGS__); } while (0)

原因是因为参数个数零,预编译后main函数里面的代码变成了:

> gcc -E va_args.c | tail 
int main() {
    const char *msg = "HelloWorld";

    do { fprintf(
# 10 "va_args.c" 3 4
   stderr
# 10 "va_args.c"
   , "HelloWorld\n", ); } while (0);
    return 0;
}

可以看到:fprintf函数的最后是"HelloWorld", );,最后一个逗号和括号之间没有数据,语法不通过。

解决方案

__VA_ARGS__前面加上##,例如:

#include <stdio.h>
#include <stdarg.h>

#define logerr(s, ...) do { fprintf(stderr, s, ##__VA_ARGS__); } while (0)
#define logdbg(s, ...) do { fprintf(stderr, s, ##__VA_ARGS__); } while (0)

int main() {
    const char *msg = "HelloWorld";
    logerr("%s\n", msg);
    logdbg("HelloWorld\n");
    return 0;
}

编译运行:

> make va_args
gcc va_args.c -o debug/va_args
> ./debug/va_args 
HelloWorld
HelloWorld

一、关于gitbook

gitbook是一款写书软件,可以很方便的把一系列markdown文本整合成一个书籍网站发布。

以下是一个预览页面:

gitbook的名字中虽然有git,但是实际上和git没有任何关系。就像java和javascripts一样。

二、安装gitbook

gitbook实际上是一个node.js工具,因此使用前要先安装node.js,或者直接安装npm工具:

sudo apt install npm

确认nodejs和npm命令可用:

> node -v
v10.16.0
> npm -v
6.9.0

安装gitbook:

sudo npm install gitbook-cli -g

三、使用gitbook生成第一本书

在要写书的目录内,执行gitbook init即可初始化一本书:

image.png

默认会生成两个文件:README.mdSUMMARY.md。其中README.md文件是对书籍整体的介绍,而SUMMARY.md中记录了章节目录信息。

发布第一本书

使用gitbook serve可发布书籍信息,执行后默认在本地搭起一个服务端监听4000端口:

在浏览器访问4000端口即可预览:

执行gitbook serve后会在当前目录下生成一个_book的文件夹,文件夹里面保存了发布书籍的静态文件资源。

> ll _book/
total 12
drwxrwxr-x. 10 maqian maqian  270 Sep 13 21:04 gitbook
-rw-rw-r--.  1 maqian maqian 6172 Sep 13 21:04 index.html
-rw-rw-r--.  1 maqian maqian  568 Sep 13 21:04 search_index.json

静态文件也可以直接使用nginx或者其他web服务器来发布,gitbook serve实际上是先生成静态文件,然后再托管这些文件作为web服务器。

如若不想使用gitbook serve提供的服务,可以直接使用gitbook build编译出静态文件:

imagedbe8c5cede9c3529.png

三、目录结构

3.1 SUMMARY.md

默认情况下SUMMARY.md中的内容:

# Summary

* [Introduction](README.md)

3.1.1 添加子目录

效果:

imagee5e99079a08eed2b.png

SUMMARY.md:

# Summary

* [Introduction](README.md)
* [Css](css/README.md)
    * [css1](css/css1.md)
    * [css2](css/css2.md)
* [Javascripts](js/README.md)
    * [js1](js/js1.md)
    * [js2](js/js2.md

3.1.2 section分块

效果:

imagebc967a125dd2b3a8.png

SUMMARY.md:

# Summary

* [Introduction](README.md)

## Part I

* [Css](css/README.md)
    * [css1](css/css1.md)
    * [css2](css/css2.md)

## Part II

* [Javascripts](js/README.md)
    * [js1](js/js1.md)
    * [js2](js/js2.md)

一、nginx目录索引

nginx中内置了目录索引命令 auto_index ,十分方便就能给目录生成web索引:

location /ftp/ {
    alias /data/html;
    autoindex on;
}

效果如下:

PIC20180902_214954.png

两个可选的命令是 autoindex_exact_sizeautoindex_localtime ,分别表示是否精确显示文件大小(以字节方式)和是否显示本地时间,两个都 不开启 的情况下是这样的:

PIC20180902_215504.png

二、使用fancyindex索引

nginx自带的索引功能很单一,界面也很原始。后面有人做了个fancyindex的插件用来强化这个功能,已经被官方采用。官方文档地址,fancyindex代码地址:ngx-fancyindex

2.1 编译fancyindex

安装fancyindex要重新编译nginx,首先下载fancyindex源码

git clone https://github.com/aperezdc/ngx-fancyindex

解压nginx进入目录,重新执行 configure 命令,指定参数 --add-module 添加fancyindex模块,nginx安装的具体流程可以参考源码编译安装nginx

./configure --user=www --group=www \
    --prefix=/usr/local/nginx-1.12.2 \
    --with-http_stub_status_module \
    --with-http_ssl_module \
    --add-module=../ngx-fancyindex # 添加fancyindex模块

执行make和make install即可完成安装。

2.2 使用fancyindex

fancyindex的指令:

location /ftp/ {
    alias /data/software/nginx/;
    fancyindex on; # 使用fancyindex
    fancyindex_exact_size off; # 不显示精确大小
}

其效果如下:

fancyindex.png

点击表头的 File NameFile Size 或者 Date 能对文件进行排序。

2.3 其他用法

fancyindex提供了自定义页头和页脚,分别通过指令 fancyindex_headerfancyindex_footer 完成。

例如在页脚加上一个超链接到百度首页:

location /ftp/ {
    alias /data/software/nginx/;
    fancyindex on;
    fancyindex_exact_size off;
    fancyindex_footer "fancy_footer.html";
}

然后在 网站根目录下 加上一个 fancy_footer.html

<div id="footer">
    <a href="https://www.baidu.com">百度一下<a>
</div>

重新载入后的页面:

PIC20180902_221620.png

安装esxi途中遇到了找不到网卡驱动的问题:

20180902184932.jpg

这是因为iso文件中本身没有添加当前设备网卡的驱动,如要安装,需要手动导入属于自己网卡的的驱动。

第一步先找到自己的网卡型号:

[ma@centos ~]$ lspci -tv
-[0000:00]-+-00.0  Advanced Micro Devices, Inc. [AMD] Family 15h (Models 10h-1fh) Processor Root Complex
           +-01.0  Advanced Micro Devices, Inc. [AMD/ATI] Richland [Radeon HD 8650G]
           +-01.1  Advanced Micro Devices, Inc. [AMD/ATI] Trinity HDMI Audio Controller
           +-02.0-[01]----00.0  Advanced Micro Devices, Inc. [AMD/ATI] Thames [Radeon HD 7670M]
           +-04.0-[02]----00.0  Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller
           +-05.0-[03]----00.0  Broadcom Limited BCM4313 802.11bgn Wireless Network Adapter

我这里有两块网卡,一个有线网卡 Realtek 8111/8168/8411和一个无线网卡Broadcom Limited BCM4313,不考虑使用无线,就只导入8111驱动。驱动可以在V-Front VIBSDepot wiki中寻找,这里面不仅包含了驱动,还有各种其他相关的工具组件:

PIC20180902_183042.png

进入ESXI package 搜索自己的网卡型号8111,点进去之后选择下载.vib包:

PIC20180902_183233.png

得到net55-r8168-8.045a-napi.x86_64.vib后,要把它加到原始的安装包里去,要用到另一个软件叫ESXi-Customizer,下载地址:v7.2版本下载地址,主界面如下:

ESXi-Customizer-v2.7.2-GUI.png

第一个选择官方的原始镜像,第二个是下载的驱动目录,第三个就是输出文件夹,选好后点RUN就能一键完成,很方便。

关于ESXi-Customizer的使用

ESXi-Customizer下载完成后,双击打开会自动解压出来得到以下文件:

PIC20180902_183835.png

后缀名为 .cmd 的就是启动文件,是一个脚本,默认情况下这个脚本是只能支持的win8.1,win10用户打开会报错:

PIC20180902_144756.png

从错误信息上不难看出原因,以一个程序员的经验来说这里一定是脚本校验不通过了, 搜索找到以下位置:

PIC20180902_184252.png

虽然不知道这是什么语言,但从代码结构来看肯定是这里版本校验不通过了,当前的版本为 10.0 ,走到了倒数第三句就退出了。

所以为了避免走到这里就直接在前面获取版本的时候写死成 6.3 ,以win8方式运行(实际上这里也只是打了一个提示语句而已,并不是以win8模式运行):

image.png

果然,重新启动就能运行了,虽然有点显示异常,不过不影响操作:

image.png

一、备份数据库

mysql自带了数据库备份工具mysqldump可以很方便的对数据库进行备份:

mysqldump -u root -p --all-database > db.sql

以上命令就完成了一次数据备份,备份后的数据保存在文件 db.sql ,参数 --all-databases 是指备份所有数据库。

如果只想备份特定的数据库,通过参数 --database, -B 指定即可,也可以直接加在命令后面:

mysqldump -u root -p test > test.sql

这条命令就只备份test数据库,生成的test.sql文件即为数据库。

二、恢复数据库

恢复数据库使用mysql命令就可以完成,要注意的地方是恢复到数据库之前要求数据库必须存在:

mysql -u root -p test < test.sql

以上命令就表示把备份的数据库文件导入到数据库test中,如果test数据库不存在,会报错:

root@35c000f43aa6:/backup# mysql -u root -p test< test.sql 
Enter password: 
ERROR 1049 (42000): Unknown database 'test'

三、mysqldump用户权限问题

使用mysqldump进行数据备份时依赖账户密码和数据库的访问权限,如果使用正常的业务账号容易导致账号密码被泄露。根据权限最小化原则,一般建议为mysqldump建立单独的用户身份。

一个单独的mysqldump用户应该包含以下权限:

  1. 只有只读权限,不能修改数据库内容
  2. 只能本地用户登陆

创建一个符合以上条件的dumper用户:

create user dumper@'127.0.0.1' identified by '123456';
grant select on test.* to dumper@'127.0.0.1';
grant show view on test.* to dumper@'127.0.0.1';
grant lock tables on test.* to dumper@'127.0.0.1';
grant trigger on test.* to dumper@'127.0.0.1';

一、镜像和容器

docker中的镜像和容器对应linux环境中的程序和进程,不运行时是一个静态的二进制文件,运行就成了系统中的一个进程。docker运行时被称作容器,静态的文件则被称作镜像。

镜像是docker三大核心中最为重要的,因为运行docker首先得要有镜像。而镜像的来源有多种,可以从官方仓库获取,也可以手动制作。默认情况下启动一个镜像,如果不存在于本地,会从镜像仓库获取。

1.1 获取镜像

如果想要从镜像仓库获取镜像,使用 docker pull 命令就可以完成,例如:

ma@Y485:~$ docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
9db2ca6ccae0: Pull complete 
Digest: sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc
Status: Downloaded newer image for hello-world:latest

仓库中的镜像包含两部分:镜像名和版本,版本字段可以省略,默认使用latest。
例如我们要获取ubuntu 14.04镜像,可以使用下面的命令完成:

ma@Y485:~$ docker pull ubuntu:14.04
14.04: Pulling from library/ubuntu
8284e13a281d: Pull complete 
26e1916a9297: Pull complete 
4102fc66d4ab: Pull complete 
1cf2b01777b2: Pull complete 
7f7a2d5e04ed: Pull complete 
Digest: sha256:71529e96591eb36a4100cd0cc5353ff1a2f4ee7a85011e3d3dd07cb5eb524a3e
Status: Downloaded newer image for ubuntu:14.04

1.1 查看本地镜像

查看本地已有的镜像可以通过命令 docker imagesdocker image ls 完成:

ma@Y485:~$ docker images 
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
tvial/docker-mailserver   latest              dac8816ec078        3 days ago          517MB
hello-world               latest              2cb0d9787c4d        5 weeks ago         1.85kB
shiningrise/hustoj        latest              bbbf10eeee29        8 weeks ago         490MB
ma@Y485:~$ docker image ls
REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
tvial/docker-mailserver   latest              dac8816ec078        3 days ago          517MB
hello-world               latest              2cb0d9787c4d        5 weeks ago         1.85kB
shiningrise/hustoj        latest              bbbf10eeee29        8 weeks ago         490MB

显示的结果中包含了镜像名,版本号,镜像ID,创建时间和镜像大小等,镜像ID是用来标识镜像的唯一ID。

1.3 搜索仓库中的镜像

使用docker search命令可以在远程仓库中搜索想要的镜像:

ma@Y485:~$ docker search hello-world
NAME                                       DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
hello-world                                Hello World! (an example of minimal Dockeriz…   624                 [OK]                
kitematic/hello-world-nginx                A light-weight nginx container that demonstr…   108                                     
tutum/hello-world                          Image to test docker deployments. Has Apache…   53                                      [OK]
....

默认会显示镜像名,镜像描述和star等信息,OFFICIAL表示是否为官方镜像,如果标有[OK]则表示是官方的,最后的AUTOMATED表示是否为自动创建的镜像。

search中提供了一些筛选的选项:

  • -s, --stars=n : 老版本中用来筛选star用的,表示只显示star大于等于n的镜像。新版docker用 --filter=start=n 来筛选。
  • --no-trunc=truc|false : 是否截断描述信息,默认情况下描述超多长度之后会被截断成...显示。
# 查找star大于500的hello-world镜像,且不截断描述信息。
ma@Y485:~$ docker search hello-world --stars=500 --no-trunc
Flag --stars has been deprecated, use --filter=stars=3 instead
NAME                DESCRIPTION                                          STARS               OFFICIAL            AUTOMATED
hello-world         Hello World! (an example of minimal Dockerization)   624                 [OK]                

1.4 镜像标签

镜像标签类似于linux中的 alias ,相当于给镜像取了别名:

ma@Y485:~$ docker tag ubuntu:14.04 myubunt
ma@Y485:~$ docker image ls | grep ubuntu
myubuntu                  latest              971bb384a50a        4 weeks ago         188MB
ubuntu                    14.04               971bb384a50a        4 weeks ago         188MB

新创建的标签镜像和实际的镜像ID都是相同的,指向了同一个镜像。

1.5 删除镜像

docker rmi 可以删除本地已经存在的镜像,删除镜像可以使用 镜像名+标签镜像id 完成。

例如当前 hello-world 镜像的id为 2cb0d9787c4d ,把它删除:

ma@Y485:~$ docker rmi 2cb0d9787c4d
Untagged: hello-world:latest
Untagged: hello-world@sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc
Deleted: sha256:2cb0d9787c4dd17ef9eb03e512923bc4db10add190d3f84af63b744e353a9b34
Deleted: sha256:ee83fc5847cb872324b8a1f5dbfd754255367f4280122b4e2d5aee17818e31f5

值得注意的是:如果容器镜像已经被运行了,即使容器处于停止的状态,镜像也是无法删除的。

ma@Y485:~$ docker rmi 2cb0d9787c4d
Error response from daemon: conflict: unable to delete 2cb0d9787c4d (must be forced) - image is being used by stopped container 5549f206097

这里提示镜像无法删除,应为有一个容器 5549f2060977 正在使用它,查看此时运行的容器:

ma@Y485:~$ docker ps # 查看所有正在运行中的容器
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
ma@Y485:~$ docker ps -aq # 查看运行中或者已经停止的容器,-q表示只显示容器id
85d2d2aa258a
d383073e1d32
5549f2060977

可以看到有一个已经停止了的容器 5549f2060977 ,它在占用 hello-world 镜像,删除经向前要把这个容器也删除了才行,删除容器使用 docker rm

ma@Y485:~$ docker rm 5549f2060977 # 删除容器
5549f2060977
ma@Y485:~$ docker rmi 2cb0d9787c4d # 删除镜像
Untagged: hello-world:latest
Untagged: hello-world@sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc
Deleted: sha256:2cb0d9787c4dd17ef9eb03e512923bc4db10add190d3f84af63b744e353a9b34
Deleted: sha256:ee83fc5847cb872324b8a1f5dbfd754255367f4280122b4e2d5aee17818e31f5

当镜像存在多个标签的时候,删除镜像只是删除了镜像的当前标签,实际的镜像并没有删除:

ma@Y485:~$ docker image ls | grep ubuntu # 当前有两个ubuntu镜像,有一个是镜像标签,实际都是一个镜像
myubuntu                  latest              971bb384a50a        4 weeks ago         188MB
ubuntu                    14.04               971bb384a50a        4 weeks ago         188MB
ma@Y485:~$ docker rmi myubuntu # 删除myubuntu标签
Untagged: myubuntu:latest
ma@Y485:~$ docker image ls | grep ubuntu 
ubuntu                    14.04               971bb384a50a        4 weeks ago         188MB

最后能看到,删除 myubuntu 之后,镜像并没有删除,原来的ubuntu和镜像依旧存在。

1.6 查看镜像信息

每个镜像都有自己的详细信息,包括作者、版本以及镜像大小等相关信息,使用命令 docker inspect 即可查看:

ma@Y485:~$ docker inspect hello-world
[
    {
        "Id": "sha256:2cb0d9787c4dd17ef9eb03e512923bc4db10add190d3f84af63b744e353a9b34",
        "RepoTags": [
            "hello-world:latest"
        ],
        "RepoDigests": [
            "hello-world@sha256:4b8ff392a12ed9ea17784bd3c9a8b1fa3299cac44aca35a85c90c5e3c7afacdc"
        ],
        "Parent": "",
        "Comment": "",
        "Created": "2018-07-11T00:32:08.432822465Z",
        "Container": "6b6326f6afc81f7850b74670aad2bf550c7f2f07cd63282160e5eb564876087f",
        "ContainerConfig": {
            "Hostname": "6b6326f6afc8",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/hello\"]"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:6bc48d210ad4c6bbb74e02e6196a9133b57107033c09e92cac12616cad30ebcf",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "DockerVersion": "17.06.2-ce",
        "Author": "",
        "Config": {
            "Hostname": "",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/hello"
            ],
            "ArgsEscaped": true,
            "Image": "sha256:6bc48d210ad4c6bbb74e02e6196a9133b57107033c09e92cac12616cad30ebcf",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": null
        },
        "Architecture": "amd64",
        "Os": "linux",
        "Size": 1848,
        "VirtualSize": 1848,
        "GraphDriver": {
            "Data": {
                "MergedDir": "/var/lib/docker/overlay2/96168c171bf2d3370860383cb4be12c68141940410bdd823a0ca04e19c77d0ee/merged",
                "UpperDir": "/var/lib/docker/overlay2/96168c171bf2d3370860383cb4be12c68141940410bdd823a0ca04e19c77d0ee/diff",
                "WorkDir": "/var/lib/docker/overlay2/96168c171bf2d3370860383cb4be12c68141940410bdd823a0ca04e19c77d0ee/work"
            },
            "Name": "overlay2"
        },
        "RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:ee83fc5847cb872324b8a1f5dbfd754255367f4280122b4e2d5aee17818e31f5"
            ]
        },
        "Metadata": {
            "LastTagTime": "0001-01-01T00:00:00Z"
        }
    }
]

docker history 命令可以查看镜像的历史信息:

ma@Y485:~$ docker history hello-world
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2cb0d9787c4d        5 weeks ago         /bin/sh -c #(nop)  CMD ["/hello"]               0B                  
<missing>           5 weeks ago         /bin/sh -c #(nop) COPY file:3c3ca82dfdb40d30…   1.85kB              

二、镜像仓库

2.1 Docker Hub

docker镜像仓库是一个集中存放镜像的地方,docker pull 就是直接在这个仓库获取的镜像。

docker官方维护了一个公共的镜像仓库Docker Hub,里面有许多优质的镜像。

除了下载镜像,这里还能上传个人制作的个人镜像。

2.1 阿里云镜像加速器

docker安装后默认的仓库地址Docker Hub,对于国内用户来说速度比较慢,可以考虑替换为国内的镜像仓库。

国内的镜像加速器比较好的有:DaoCloud, 时速云, 阿里云等,这里介绍阿里云镜像加速器的用法。

登陆后,阿里云会分配私有的加速器地址,里面有加速器在不同系统下的使用方法,按照教程设置即可。

Ubuntu下的替换教程:

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://aketvpgu.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

其他系统可以查阅: