CheatSheet

以下是我个人在使用服务器/Linux 发现的一些有用的小玩意。知道这些不会帮助你科研 什么的,答辩老师也不会因为你会这个而欣赏你,但是至少你会觉得很酷,进而产生 自己比较厉害的错觉。

目录

使用 VIM

使用 VIM 是一种信仰,用好了它真的会提高很大效率。

据说好的 VIM 用户绝不和 emacs 用户打架。

本人并不精通 VIM,还要好多东西要学习。

打开与关闭

# 使用 vim 打开文件,如果文件不存在就新建一个
vim <filename>

# 用 diff 模式打开两个文件,方便比较异同
vimdiff file1 file2

# 打开 vim 初学者教程(强烈推荐入门用户学习)
vimtutor

获取帮助

功能太多了,看得眼花缭乱。我怎么知道哪一个是我想要的?

  • 内置文档。很多人可能不知道这是个什么东西。打开 VIM 后,输入 :help 即可 打开内置文档。这里面的功能最全,在阅读文档的时候还可以顺便练习一下 VIM 的 操作。

  • vimtutor。我又一次提到了这东西。对初学者这东西简直是福利啊。在命令行下 输入 vimtutor 即可打开。

  • VIM adventures。有人居然将 VIM 教程做成了游戏。还不快去玩玩。传送门>>

  • Google。好吧,这也是一个不错的选择。但是前提是你得知道怎么搜。千万别相信 百度和它搜索到的辣鸡玩意儿,百度搜到的个人 Blog 的编写者,可能 99% 都不如笔者。

普通模式

VIM 编辑器有多个模式:普通模式,插入模式,VISUAL 模式。进入 VIM 编辑器后, 编辑器处于普通模式,在此模式下,VIM 编辑器会将按键解释成命令。

  • h,j,k,l 移动光标,分别对应:左,下,右,上。不要总去碰键盘右边的方向键, 尽量熟悉用 hjkl 来移动你的光标。

  • w,b,W,B 更快地移动光标到下一个词/上一个词。使用大写字母移动得更快。

  • :q 如果未修改数据,退出

  • :q! 取消所有修改,退出

  • :wq, :x, ZZ 保存并退出,笔者更喜欢用 ZZ,因为只要按三下键盘。

  • :qa! 关闭当前打开的所有文件,退出

  • :<数字> 直接跳转到指定的行

  • :tabe <文件名> 新建或打开一个新文件,并以标签形式显示在编辑器上方

  • gt,gT 切换到下一个/上一个标签

  • x 删除当前位置字符

  • dd 删除当前行(实际是复制到了剪贴板处)

  • <数字>dd 删除指定数目的行数

  • diw 删除当前光标所在单词

  • d$ 删除当前光标所在位置至行尾的内容

  • yy 复制当前行

  • <数字>yy 复制指定数目的行数

  • yw 复制当前光标所在单词

  • y$ 复制当前光标所在位置至行尾的内容

  • p 将剪贴板的内容粘贴至当前位置

  • i,a,I,A 进入插入模式,并将光标放置在 i: 当前字符前,a:当前字符后, I: 本行第一个字符前,A:本行最后一个字符后。

  • o,O 在下方/上方插入空行,并进入插入模式。

  • u,Ctrl-r 撤销上一步的更改/重做上一步的撤销。

  • Ctrl-Z 暂时离开 ViM 编辑器,回到 Shell 界面。在 Shell 中使用 fg 命令即可返回 VIM

  • Ctrl-a,Ctrl-x 将光标所在的数字增加/减少 1。注意:考虑进制,若光标所在的 数字当前为 07,则一次 Ctrl-a 过后将会变成 10(因为 07 是八进制)。

插入模式

在插入模式下,即可插入文本到光标的位置。按下 Esc 键即可返回普通模式。

  • Ctrl-w 删除光标前的一个词。这比按 Backspace 更快。

VISUAL 模式

在普通模式按下 v 可以进入 VISUAL 模式,按下 Ctrl-v 可以进入 VISUAL BLOCK 模式。 两个模式下可对文本进行选择的操作。进而可将普通模式的操作只应用在选择的区域上。

VISUAL BLOCK 模式下批量注释

在没有安插件的情况下,VISUAL BLOCK 模式下可以完成一些简单的批量注释工作。

# 按下 Ctrl-v 进入 VISUAL BLOCK 模式
Ctrl+v

# 选择需要注释的一些行的第一个字符
hjkl

# 按下 I 进入插入模式,注意是大写的 I
I

# 输入注释用的字符,例如 #

# 按下 Esc 键返回普通模式,等待几秒

查找与替换

普通模式下,可进行查找和替换。

  • /foo 从当前位置向后查找 foo 这个关键词。

  • ?foo 从当前位置向前查找 foo 这个关键词。

  • n,N 跳转到下一个/上一个匹配查找的位置。

  • :%s/foo/bar/ 将每行的第一个 foo 替换为 bar。

  • :%s/foo/bar/g 将所有的 foo 替换为 bar。

  • :%s/foo/bar/gc 将所有的 foo 替换为 bar,并在每次替换之前询问。

其他

进入粘贴模式

在设置过自动缩进的 VIM 编辑器下,粘贴模式可以临时取消所有的自动缩进识别。

:set paste

退出这一模式使用

:set nopaste

缩进修复

VIM 可以对完全未缩进的代码进行缩进修复。

# 在普通模式下
gg=G
  • gg 表示跳转到文件第一行。

  • = 表示进行缩进修复。

  • G 表示修复到文件最后一行。

使用 MEX 文件

MEX 文件在 MATLAB & C 混合编程的过程当中占据了重要角色。由于 MATLAB 不擅长处理循环,因此如果使用 MATLAB 做纯循环效率会很低下。 用户可以通过将 C 程序封装成 MATLAB 可执行的文件来为 MATLAB 计算加速。

以下举一个简单的例子来说明 MEX 是如何工作的。 设想我们要实现这样的一个效果:输入一个矩阵 A,而后计算每个元素的 2 倍,然后返回结果。

首先,我们准备一下所需的 C 语言代码。

simple_mult.c
#include "mex.h"

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]){
  int i, m, n;
  double *a, *b;

  if (nrhs != 1){
    mexErrMsgTxt("Usage: B = simple_mult(A)");
    return;
  }

  m = mxGetM(prhs[0]);
  n = mxGetN(prhs[0]);
  a = mxGetPr(prhs[0]);

  plhs[0] = mxCreateDoubleMatrix(m, n, mxREAL);
  b = mxGetPr(plhs[0]);

  for (i = 0; i < m * n; ++i)
    b[i] = 2 * a[i];

}

以上代码中有以下几点是 MEX 文件的约定:

  • 必须包含头文件 mex.h,这个头文件声明了 MEX 所有的函数原型和宏。

  • 作为 MEX 文件执行的主函数,它的函数头必须为例子中的形式。函数名必须为 mexFunction; 返回类型为 void;参数有四个,其中 nlhsnrhs 分别表示返回值数量和参数数量, plhsprhs 分别表示相应数量的 mxArray 指针数组。如果函数头编写不正确则 MEX 编译器无法识别。

  • 不能直接对 mxArray* 类型的数据进行操作,需要用一系列函数将其内部数据暴露出来。

  • MATLAB 中的矩阵存储格式采用 Fortran 方式,将二维矩阵存成一维长向量, 先存第一列,再存第二列,以此类推。

不同 MATLAB 版本支持的 C 编译器不同。例如 MATLAB R2015b 最高只能支持 gcc-4.7 的编译器。因此在执行编译之前,请确保正在使用的 gcc 版本被 MATLAB 支持。 高版本编译器依然可以用,只是会报 warning,我用过高版本的编译器编过几个,貌似 没什么问题。

进入 MATLAB 命令行模式(可使用命令 matn)。在 MATLAB 命令行下,输入

>> mex -setup C

接下来使用

>> mex simple_mult.c

即可编译 MEX 文件,其中 simple_mult.c 为源文件名,必须在当前工作目录之下。

编译成功后,MATLAB 会生成对应的 MEX 可执行文件。在我们的例子里, MATLAB 会生成一个名为 simple_mult.mexa64 的文件。使用此 MEX 函数的方法是

B = simple_mult(A)

小提示

Linux 用户可尝试将本地生成的 mexa64 文件上传至服务器, Windows/Mac用户则需要在服务器上编译 mex 文件。在Windows/Mac系统下生成的 mex 文件无法在 Linux 系统下使用。不过,仍然推荐 Linux 用户在服务器上重新编译 mex 文件, 以防止运行环境变化导致程序无法运行。

设置 Intel MKL 编译选项

使用 Intel MKL 进行计算时,需要使用 gcc/g++ 编译工具,并将 MKL 库链接到你的目标文件上。

编译所需的命令行较为复杂,推荐使用Intel® Math Kernel Library Link Line Advisor来指导编译和链接。

MKL Link Line Advisor 工具需要用户指定选项才可正确完成链接命令的生成。各个选项的含义如下:

  • Select Intel product: Intel MKL 2017,选择这个选项即可。MKL 链接的命令在各个版本中的区别不大。

  • Select OS: 一定要选择 Linux,这是服务器的操作系统。

  • Select usage model of Intel Xeon Phi Coprocessor: 选择 None 即可,我们暂时用不到这个特殊的设定。

  • Select compiler: 根据个人使用的编译器自行选择。例如程序使用 gcc 编译,则此项需要选择 GNU C/C++

  • Select architecture: 选择 Intel 64,我们的系统是 64 位系统。

  • Select dynamic or static linking: 选择库类型,是静态库还是动态库。一般情况下建议选择动态库 dynamic,极少数情况(例如编译 MATLAB 的 MEX 文件)可能需要静态库 static

  • Select interface layer: 选择整数类型。在这里我们选 32-bit integer,即 32 位整数。

  • Select threading layer: 选择多线程类型。这里需要注意自己程序的用途,如果希望 MKL 仅仅占用 1 个 CPU 核心进行运算,那么需要选择 Sequential。如果希望 MKL 自动利用线程进行并行运算,那么需要选择 OpenMP threading

  • Select OpenMP library: 在上一个选项选择 OpenMP threading 之后出现,选择 OpenMP 所使用的库。在这里需要选择编译器对应版本的 OpenMP 库。例如使用 gcc 编译器进行编译,则需要选择 GNU(libgomp)

  • Select cluster library: 如果没有使用分布式并行计算的库,无需勾选任何选项。

  • Select MPI library: 无需设置。

  • Select the Fortran 95 interfaces: 无需设置。

  • Link with Intel MKL libraries explicity: 建议勾选以查看详细的链接命令。

按照如上设置完毕后,即可在下方的输出中看到链接选项和编译选项。直接复制到 Makefile 中即可。

例:使用 MKL 的动态库编译程序,使用多线程的库并使用 GNU OpenMP。

Makefile
CC=gcc
CFLAGS=-m64 -I${MKLROOT}/include
LDFLAGS=-L${MKLROOT}/lib/intel64 -Wl,--no-as-needed -lmkl_intel_lp64 -lmkl_gnu_thread -lmkl_core -lgomp -lpthread -lm -ldl

.PHONY: default clean

default: main.c
        ${CC} -o main $< ${LDFLAGS} ${CFLAGS}

clean:
        rm -rf *.o main

命令行操作

很多初学者可能只会在命令行上用方向键/Home/End 来移动光标,这是 ok 的。不过 当你发现某个很长的命令中间有个词打错了的时候,是选择删掉重新打,还是将光标 移动到出错处更改,这就是个问题了。

  • 左右方向键:左右移动,每次移动一个字符。

  • Ctrl-f, Ctrl-b:左右移动,效果跟方向键一样,但是不会让你的右手频繁移动。

  • 上下方向键:滚动历史曾经执行过的命令。

  • Ctrl-a, Ctrl-e:移动到行首/行尾。这样右手就不用大老远去按 Home/End 了。

  • Alt-f, Alt-b:向右/左移动一个词的位置。

  • Ctrl-w:删除当前词。(实际是删除到光标左边最近的一个空格)

  • Ctrl-u:删除到行首。这会少按好多 Backspace。

  • Shift-PgUp, Shift-PgDn:在 terminal 中滚动。

禁用用户

有时需要禁用服务器某个用户,不让他登录。最好不要用 userdel -r <name> 一删 了之,因为今后可能要重新启用它。

可以通过更改用户默认 shell 的方式让用户无法取得 shell 控制权,自然无法使用 服务器。更改用户 shell 的命令是 chsh

sudo chsh -s /sbin/nologin <username>

/sbin/nologin 是一个特殊的 shell,这个 shell 会显示 This account is currently not available 后直接退出,从而“礼貌拒绝登录”。此时,该用户无法从 tty 或者是使用 ssh 进行登录。

假如该用户可以访问服务器上的其它用户(例如另一个用户是他的朋友,他只需要 临时借用他朋友的账号,这个用户不需要是管理员),那么他是可以通过朋友的账号 拿回自己的 shell 权限的。

# user1 是被禁用账户,user2 是活动账户
[user2@server ~]$ su - user1 -s /bin/bash
Password:                           # << 输入 user1 的密码
[user1@server ~]$                   # << 居然可以登录
[user1@server ~]$ chsh -s /bin/bash # 用 chsh 拿回自己的默认 shell

这里看到,使用 su 命令可以对默认 shell 进行重写,这样我们做的工作全白费了。 使用 su 时需要密码,因此禁用 user1 的密码就可以避免这种方式的越权。

sudo passwd -l user1

上面的命令实际上是修改了 /etc/shadow 文件的密码字段,使其失效。

要恢复账号,只需要执行下面两条命令。

sudo passwd -u <username>          # 解除密码的锁定
sudo chsh -s /bin/bash <username>  # 还原正常的 shell

备注

  • 使用 passwd -l 的原理是禁用用户密码,若用户使用了别的验证方式(例如 ssh 公钥),由于走的不是密码验证,因此禁用密码无效。

  • 不能在远程 ssh 登录的时候复写登录用的 shell。如果能办到的话,很多安全措施 其实并不安全(例如 git-shell)。

GLIBC 版本错误

在旧系统(例如 CentOS 6)上安装一些较新的预编译的库,运行时可能会发生如下 错误

>>> import torch
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/public/home/liuhy/.conda/envs/mytorch/lib/python3.6/site-packages/torch/__init__.py", line 78, in <module>
    from torch._C import *
ImportError: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by /public/home/liuhy/.conda/envs/mytorch/lib/python3.6/site-packages/torch/_C.cpython-36m-x86_64-linux-gnu.so)

原因是系统使用的 GLIBC 版本过低,里面的函数版本未达到要求。

GLIBC 的地位比较特殊,它不像其他的库只需要改动运行时目录就好。这是因为许多系统 的关键命令,例如 ls, cat,都依赖这个库。GLIBC 有许多模块,如果配置错误 导致各个模块的版本不相同,使用 GLIBC 的系统命令就会崩溃,进而整个系统会无法 使用,因此以管理员身份操作 GLIBC 时会非常危险。

传统的解决办法是拿到软件的源码然后重新编译一个,然而有很多因素阻止人们这样做:

  • 重新编译需要解决很多依赖问题,且失败率较高,处理起来很费时。

  • 软件是闭源的,无法得到源码。

  • 软件编译时需要连网,而服务器无法访问某些网络。

下面介绍一种直接修改二进制文件的方式,或许可以解决 GLIBC 的依赖问题。

处理一般的 GLIBC 版本错误是一个很难的问题,这里的方法不能确保一定成功。 笔者对 Linux 的 ELF 文件的理解也十分有限,不能确保下面所说的一定正确。

可执行文件依赖查找顺序

在执行一个可执行二进制文件之前,程序会将控制权交给一个特定的 loader,让它 解析程序文件的 header 并决定要载入哪些库。在 64 位 CentOS 6 系统下, 这个 loader 通常是 /lib64/ld-linux-x86-64.so.2。解析完成并加载程序所需的库 后,控制权回到原来的程序,程序正式开始运行。ld-linux 按照如下顺序查找依赖

  • ELF 文件的 DT_RPATH 字段,通常以 -Wl,-rpath 在连接时指定。已过时。 原因是它的优先级太高,一旦写死在文件里想临时更改并不容易(不过还是很多 人在用)。在 DT_RUNPATH 存在时将会被忽略。

  • LD_LIBRARY_PATH 中的路径。这是一个被“滥用”的环境变量,不过确实好用。 备注:当程序有 SUID 的时候这个变量将会被忽略。

  • ELF 文件的 DT_RUNPATH 字段,以 -Wl,--enable-new-dtags,-rpath 指定。 据说 DT_RPATH 是个错误的设计,因此就出了 DT_RUNPATH。不过这个选项一些旧的 linker 可能不认识。

  • ld.so.cache 里的路径。

  • 默认路径,例如 /lib64, /usr/lib64 等。除非程序编译的时候使用了 -z nodeflibs

因为载入有顺序,同一个库即使被递归地依赖也不会载入两次。如果在一开始就强制载入 高版本的 GLIBC 库,那么之后遇到的 GLIBC 也自动会变成高版本的,这种办法就 可能解决问题。

查看 ELF 文件的依赖

ldd 命令可以检查 ELF 文件的动态库依赖关系(ldd 并不是个二进制程序,它只是 一个脚本,不信你用文本编辑器打开看一下)。例如

$ ldd /bin/ls
	linux-vdso.so.1 =>  (0x00007ffcb7dd9000)
	libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fce3a13e000)
	libcap.so.2 => /lib64/libcap.so.2 (0x00007fce39f39000)
	libacl.so.1 => /lib64/libacl.so.1 (0x00007fce39d30000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fce39963000)
	libpcre.so.1 => /lib64/libpcre.so.1 (0x00007fce39701000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00007fce394fd000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fce3a365000)
	libattr.so.1 => /lib64/libattr.so.1 (0x00007fce392f8000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fce390dc000)

ldd 实际上调用的是 ld-linux-x86-64.so.2,这个程序根据上文中提到的顺序 对库进行查找。那么二进制文件如何找到 ld-linux-x86-64.so.2 这个 loader 呢? 这个 loader 的位置在编译的时候已经写死在二进制程序里了,不用找。 使用 readelf 命令可查看写死在 ELF 文件中的信息。

$ readelf -d -l /bin/ls        # -d 的作用是查看 dynamic 部分
                               # -l 的作用是查看 program header 部分

Elf file type is EXEC (Executable file)
Entry point 0x4043c4
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]   # << 这就是 loader 的指定位置

     (more information ...)

Dynamic section at offset 0x1ada8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libselinux.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libcap.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libacl.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x402228

 (more information ...)

有了 loader,程序的 header,依赖库的名字,程序运行前就能定出来它的依赖。

修改依赖

有了上面的基础我们大致能猜出需要改些什么。在 ELF 文件加上高优先级的 RPATH (已过时),或更改 ELF 文件的 loader。修改库依赖的软件推荐使用 patchelf

先到 patchelf 官网下载 patchelf,编译安装 即可(编译并不是很麻烦)。

几个需要用到的命令写在下面

# 修改 rpath
patchelf --set-rpath RPATH FILENAME

# 修改 interpreter(loader),仅限 program,一般动态库没有 interp
patchelf --set-interpreter INTERP FILENAME

# 查看 rpath/loader
patchelf --print-rpath FILENAME
patchelf --print-interpreter FILENAME

实际操作

以下以 CentOS 6 下安装 pytorch-0.4-cpu 为例。

首先正常安装 pytorch。

conda create -n mytorch          # 创建环境
source activate mytorch
conda install python=3.6         # 安装包
conda install pytorch-cpu torchvision-cpu

直接 import 会出错

>>> import torch
ImportError: /lib64/libc.so.6: version `GLIBC_2.14' not found (required by /public/home/liuhy/.conda/envs/mytorch/lib/python3.6/site-packages/torch/_C.cpython-36m-x86_64-linux-gnu.so)

在这里虽然我们看见是 pytorch 的一个库缺少 GLIBC 的依赖,但是由于 GLIBC 非常 基础,在程序的最开始就已经载入这个库了。前面我们提到,同名的库只会 load 一次。 修改此动态库的字段是没用的。

缺少的库是 GLIBC_2.14,但这个错误只是其中的一个。其他的函数可能需要更高版本的 GLIBC。因此我们考虑安装 GLIBC_2.17。这是因为 CentOS 7 的预装库的版本就是 2.17。

到 GLIBC 官网下载 GLIBC 后,编译,安装。编译时建议使用系统自带的 gcc 和 binutils, 不推荐太新的软件(实际上太新了反而不识别)。

$ tar -xJf glibc-2.17.tar.xz
$ cd glibc-2.17
$ mkdir build && cd build
$ ../configure --prefix=$HOME/glibc    # 注意填写 prefix
$ make
$ make install

编译过程不怎么顺利,遇到问题我也不懂怎么解决,自求多福吧。如果运气好成功安装 完了之后,下一步就是让出错的程序用这个版本的 GLIBC。

调用 pytorch 库的程序是 python,因此修改库的依赖最好直接从 python 本身开始 (这就是为什么需要在虚拟环境下再装一个 python 的原因)。

$ which python                        # 看看 python 在哪
~/.conda/envs/mytorch/bin/python

$ cd ~/.conda/envs/mytorch/bin        # 进入这个路径

$ ldd python                          # 查看依赖
	linux-vdso.so.1 =>  (0x00007ffcb10de000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00002af3e5739000)
	libc.so.6 => /lib64/libc.so.6 (0x00002af3e5957000)
	libdl.so.2 => /lib64/libdl.so.2 (0x00002af3e5ceb000)
	libutil.so.1 => /lib64/libutil.so.1 (0x00002af3e5eef000)
	librt.so.1 => /lib64/librt.so.1 (0x00002af3e60f3000)
	libm.so.6 => /lib64/libm.so.6 (0x00002af3e62fb000)
	/lib64/ld-linux-x86-64.so.2 (0x000000336d600000)

$ patchelf --print-rpath python
$ORIGIN/../lib                     # $ORIGIN 表示 python 所在当前目录

$ patchelf --set-interpreter /public/home/liuhy/glibc/ld-linux-x86-64.so.2
warning: working around a Linux kernel bug by creating a hole of 1835008 bytes in ‘python’  # 出了个 warning,害怕

$ readelf -l python                # 看下 header,确实发生变化。
  (information ignored ...)

  INTERP         0x000000000055f268 0x000000000055f268 0x000000000055f268
                 0x0000000000000032 0x0000000000000032  R      0x1
      [Requesting program interpreter: /public/home/liuhy/glibc/lib/ld-linux-x86-64.so.2]

  (information ignored ...)

最后重新打开 python 测试 pytorch 是否可以

>>> import torch
>>> torch.__version__
'0.4.0'                           # 奇迹般地可以!!!

备注

  • 为什么不去修改 RPATH 而去修改 INTERP 呢?这是因为,程序运行所依赖的 GLIBC 库必须完整配套。python 程序只是依赖 GLIBC 的一部分模块,单独修改这里的 RPATH 会导致 python 所依赖的其他库仍然到原来的位置寻找 GLIBC,强行执行会出 Segment Fault。(以上结论是笔者猜想,不一定对。)