本文目录

[[toc]]

常见目录

  • /: 根目录
  • /root: root 用户的 Home
  • /home/${username}: 普通用户的 Home
  • /etc: 配置文件目录
  • /bin: 命令目录
  • /sbin: 管理命令目录
  • /usr/bin / /usr/sbin: 系统预装的其他命令

常用命令

部分常用命令

# 修改主机名
hostnamectl set-hostname 主机名

# 清空文件内容,并将 'content' 写入到 file 中
echo "content" > file

# 将 /dev/sr0 的磁盘制作成镜像,放到 /data/xxx.iso
dd if=/dev/sr0 of=/data/xxx.iso

# 将 /dev/sr0 的磁盘挂载到 /mnt/cd
mount /dev/sr0 /mnt/cd

# 查看 CPU 信息
lscpu

# 创建文件 outFile ,内容为 inFile 读取 4M 内容,操作重复 10 次,并且大小为 40M
dd if="${inFile}" bs=4M count=10 of="${outFile}"

# 创建空洞文件 outFile ,申请 20 * 4M 的空白空间再从 inFile 读取 4M 内容,操作重复 10 次,最终文件大小为 120M ,实际内容 40M
dd if="${inFile}" bs=4M count=10 seek=20 of="${outFile}"

# 以 code 1 返回
exit 1

# 如果上面的代码有错误, exit 返回最后一个错误码
# 如果没有错误,返回 0
exit

# 统计命令运行时间
time ls

# 遍历 1-100 ,写入 lines.txt 文件
seq 1 100 > lines.txt

# 输出文件内容,每行内容前面追加行号
cat -n lines.txt

# 查看当前登录的用户是谁
whoami

查看命令帮助信息

man

使用 man 可以查看命令的帮助信息,分为 9 种类型:

  1. 可执行程序或命令
  2. 系统调用(由内核提供的函数)
  3. 库函数调用(程序库中的函数)
  4. 特殊文件(通常位于 /dev 目录下)
  5. 文件格式和约定,例如 /etc/passwd
  6. 游戏
  7. 杂项(包括宏包和约定),例如 man(7), groff(7)
  8. 系统管理命令(通常仅限 root 用户使用)
  9. 内核例程,已废弃

可以使用 man ${command} 获取命令帮助,如果出现重名的时候,可以通过数字指定获取的是什么帮助,比如 man 5 passwdman 1 passwd 就是不同的。

默认情况下,查询的都是 1 的类型

如果不确定查询什么类型的话,可以用 man -a ${command} 查询 1-9 所有同名的帮助信息。

type

如果希望查询,执行的内容属于什么,比如别名、函数、命令、可执行程序等等,可以使用 type ${command} 查看

help

如果是内部命令 ( shell 执行器自带的命令 ) ,可以使用 help ${command} 查询帮助信息,比如 help cd

如果是外部命令 ( 非内部命令 ) ,可以使用 ${command} --help 查询帮助信息,比如 ls --info

info

info 可以查看比 help 更详细的文档,一般作为 help 的补充,比如 info ls

ls

  • ls -a: 查看全部,包括隐藏文件
  • ls -l: 展示详细信息,统计文件大小时,仅统计文件内容大小
  • ls -r: 逆序展示
  • ls -t: 按照时间排序
  • ls -R: 递归展示所有文件
  • ls -h: 将文件大小格式化展示

cd

  • cd -: 返回上次访问的目录,类似浏览器的回退
  • cd ~: 返回当前用户的 Home
  • cd /etc: 进入绝对路径
  • cd etc: 进入相对路径

mv / cp

可以使用通配符 ?*:

  • ?: 匹配一个任意字符
  • *: 匹配任意个任意字符

查看文本命令

cat

cat: 在终端打印文件信息

head

head 查看文件开头,默认将前 10 行打印到终端上,可以通过 head -${line} ${file} 指定打印前几行,比如 head -2 ~/.zsh_history

tail

tail 查看文件结尾,默认将后 10 行打印到终端上,可以通过 tail -${line} ${file} 指定打印前几行,比如 tail -2 ~/.zsh_history

tail -f: 文件更新后,同步展示更新,此时会进行交互阻塞,需要通过 ctrl c 终止,查看日志文件时必备。

wc

wc 可以统计文件内容信息

  • wc -l ${file}: 展示文件行数
  • wc -c ${file}: 统计字节数
  • wc -m ${file}: 统计字符数
  • wc -L ${file}: 最长的那一行有多长
  • wc -w ${file}: 统计文件内有多少单词

more / less

  • more 可以对文件分页展示,通过空格翻页,只能一页一页翻
  • less 可以对文件分行展示,类似 vim 的状态,可以用方向键、 PageUp 、 PageDown 控制

tar

tar 可以打包、解包、压缩、解压文件

打包、压缩

  • tar c: 打包文件,将文件合并
  • tar f: 指定操作类型为文件
  • tar v: 查看操作过程

一般 cf 是一起使用的,也就是 tar cf ${打包后的文件名} ${需要打包的目录} ,文件名一般以 .tar 结尾,表示通过 tar 打包

但是这么打包,是不会对文件进行压缩的,为了方便传输,一般还需要通过 gzipbzip2 压缩, tar 内部已经集成了压缩能力,可以通过 z 声明需要使用 gzip 压缩, j 声明使用 bzip2 压缩,也就是 tar czf ${打包后的文件名} ${需要打包的目录} 或者 tar cjf ${打包后的文件名} ${需要打包的目录}

为了区分压缩与未压缩,一般压缩文件使用 .tar.gz 表示使用 gzip 压缩,使用 .tar.bz2 表示使用 bzip2 压缩。

由于双扩展名比较长,一般使用 .tgz 表示 .tar.gz.tbz2 表示 .tar.bz2

bzip2 压缩率更高,但是处理时间更长,按需选择即可。

解压、解包

  • tar x: 解包文件,将包还原为文件 / 目录
  • tar f: 指定操作类型为文件
  • tar z: 使用 gzip 解压
  • tar j: 使用 bz2 解压

vi

vimvi 扩展版本,向上兼容, vi 存在四种模式:

  • 正常模式
  • 插入模式
  • 命令模式
  • 可视模式

正常模式

  • i: 进入插入模式,光标在当前位置

  • I: 进入插入模式,光标在当前行头部

  • a: 进入插入模式,光标在当前位置的下一个位置

  • A: 进入插入模式,光标在当前行尾部

  • o: 进入插入模式,在下方插入一行,并移动光标到新行

  • O: 进入插入模式,在上方插入一行,并移动光标到新行

  • y: 复制

    • yy: 复制当前行
    • ${line}yy: 复制 ${line} 行内容
    • y$: 复制从当前位置到结尾的所有内容
  • d: 剪切

    • dd: 剪切当前行
    • d$: 剪切从当前位置到当前行结尾的所有内容
  • p: 粘贴

  • u: 撤销操作

  • ctrl r: 撤销撤销操作

  • x: 删除当前字符

  • r${char}: 当前字符替换为指定字符

  • 移动光标

    • h / j / k / l: 左 / 下 / 上 / 右 移动光标
    • ${line}shift g: 移动到低 ${line} 行开头
    • gg: 到达第一行
    • G: 到达最后一行
    • ^: 到达行开头
    • $: 到达行末尾
  • :: 进入命令模式

  • v: 进入字符可视模式

  • V: 进入行可视模式

  • ctrl v: 进入块可视模式

命令模式

  • :w ${file}: 另存为文件,省略 ${file} 则是保存
  • :q: 退出 vi
  • :q!: 退出并且不保存
  • :!${shell}: 执行 ${shell} 命令,临时退出 vi ,命令回显退出后回到 vi
  • :/${string}: 查找匹配内容, n 会查找下一个, ctrl n 查找上一个
  • 文本替换
    • :s/${old}/${new}: 文本替换,将旧值替换为新值,只会替换当前行的内容,如果需要全部替换,需要使用 :s/${old}/${new}/g
    • :%s/${old}/${new}: 文本替换,将旧值替换为新值,替换范围为整个文件,如果需要全部替换,需要使用 :%s/${old}/${new}/g
    • :${startLine},${endLine}s/${old}/${new}: 文本替换,将旧值替换为新值,替换范围为 ${startLine}${endLine} ,如果需要全部替换,需要使用 :${startLine},${endLine}s/${old}/${new}/g
  • :set: 临时修改设置
    • :set nu: 设置行号
    • :set nonu: 取消行号
    • :set hlsearch: 搜索高亮
    • :set nohlsearch: 取消搜索高亮

可视模式

  • v: 字符可视,从当前位置,按照移动选择连续字符
  • V: 行可视,从当前位置,按照移动逐个选择行
  • ctrl v: 块可视,从当前位置,按照移动逐个选择块,块即为矩形区域

选中后,通过 ctrl i + 两次 esc 可以快速为块中每一行都添加指定字符

权限管理

用户权限

用户管理

  • useradd ${username}: 新增用户,会同步创建 Home 目录,并记录到 /etc/passwd/etc/shadow ,未指定组时,创建用户同名组
    • useradd -g ${group} ${username}: 新增用户,并放到指定用户组,而不是创建新同名组
  • id ${username}: 确认是否存在指定用户
  • passwd ${username}: 修改用户密码,如果没有输入 ${username} ,则修改当前用户密码
  • userdel ${username}: 删除用户,不删除用户数据,保留 Home ,并 Home 会被设置为无主权限,只能由 root 访问
    • userdel -r ${username}: 彻底删除用户以及用户所有数据,包括 Home
  • usermod [options] ${username}: 修改用户属性
    • usermod -d /home/xxx ${username}: 修改用户的 Home 目录为 /home/xxx
    • usermod -g ${group} ${username}: 将用户放到指定用户组中
  • chage: 修改密码过期信息,比如密码有效期、修改密码的间隔时间等等

用户组管理

  • groupadd ${group}: 新增用户组
  • groupdel ${group}: 删除用户组

用户切换

  • su: 切换用户
    • su ${username}: 切换用户身份,但保留在当前终端
    • su - ${username}: 切换用户身份,会登录到对应的用户终端,并切换到对应用户的 Home
  • sudo: 使用其他用户身份执行命令,需要进行对应用户的密码验证
    • visudo: 设置能够使用 sudo 的用户(组),以及能够执行哪些权限,需要使用当前用户命令进行验证

用户配置文件

vi /etc/passwd 打开用户配置文件

root:x:0:0:root:/root:/bin/bash
  1. root: 用户名
  2. x: 是否需要密码验证, x 为需要
  3. 0: 用户 uid 。 Linux 是根据 uid 标识用户的,如果将普通用户的 uid 修改为 0 ,普通用户也会变成 root 用户,普通用户一般从 1000 开始自增
  4. 0: 用户所在的用户组 id 。
  5. root: 注释信息
  6. /root: 用户的 Home 目录
  7. /bin/bash: 用户登录后打开的命令解释器

用户密码配置文件

vi /etc/shadow 打开用户密码配置文件

he110:$6$TmibBbIpSjJs1Q8V$gMyULqUA0J65TKuHCSzR6123456/5Q2v.J/wAwFDnc7W08atsoe/6tx0GmYNKar3R/Q5fhAqRAg9CS3ZiwPWi.:19935:0:99999:7:::
  1. he110: 用户名
  2. $6$TmibBbIpSjJs1Q8V$gMyULqUA0J65TKuHCSzR6123456/5Q2v.J/wAwFDnc7W08atsoe/6tx0GmYNKar3R/Q5fhAqRAg9CS3ZiwPWi.: 用户加密过的密码,哪怕相同密码,加密后也不同。

用户组配置文件

vi /etc/group 打开用户组配置文件

test:x:0:root
  1. test: 组名
  2. x: 是否需要密码验证 x 为需要
  3. 0: 组 ID
  4. root: 当前组属于哪个组, root 指属于 root

文件权限

查看文件权限

通过 ls -l 可以查看文件权限

drwxr-xr-x  3  username  groupname  filesize  createdTime  filename

drwxr-xr-x 即为访问权限信息

  • d: 文件类型
    • -: 普通文件
    • d: 目录文件
    • b: 块特殊文件,也就是设备
    • c: 字符特殊文件
    • l: 符号链接
    • f: 命名管道
    • s: 套接字文件
  • r: 是否有读权限,没有为 - , 数字表示为 4
  • w: 是否写入权限,没有为 - , 数字表示为 2
  • x: 是否执行权限,没有为 - , 数字表示为 1

rwx 为一组,重复三次

  1. 第 1 组 rwx文件所属用户 username 的操作权限
  2. 第 2 组 rwx文件所属用户组 groupname 的操作权限
  3. 第 3 组 rwx其他用户 的操作权限

对于目录来说

  • x: 进入目录
  • r: 显示目录文件名,必须先有 x 才行
  • w: 修改目录内的文件名,必须先有 x 才行

修改文件权限

  • chmod: 修改文件、目录权限
    • chmod +x ${file}: 为文件所属用户、所属用户组、其他用户都添加可执行权限
    • chmod u+x ${file}: 为 文件所属用户 添加 可执行权限
    • chmod g-x ${file}: 为 文件所属用户组 移除 可执行权限
    • chmod o=r ${file}: 为 其他用户 设置 为 可读权限
    • chmod a=rw ${file}: 为文件所属用户、所属用户组、其他用户都 设置 为 可读写权限
    • chomd 700 ${file}: 只有所属用户可以读取、写入、执行文件, 三位数字代表三组 rwx 的值之和
    • umask: 一般为 022 ,默认权限为 777 - umask ,所以默认权限就是 644
  • chown: 修改所属用户、所属用户组
    • chown ${username} ${file}: 修改 ${file} 文件为 ${username} 所属
    • chown :${groupname} ${file}: 修改 ${file} 文件为 ${groupname} 所属
    • chown ${username}:${groupname} ${file}: 同时修改 ${file} 文件所属用户名与用户组
  • chgrp: 单独修改所属用户组
    • chgrp ${groupname} ${file}: 修改 ${file} 文件为 ${groupname} 所属

当文件所属用户与文件所属用户组,权限冲突时,优先匹配所属用户,再匹配所属用户组,最后匹配其他用户。

比如 对于 group1user1 没有任何权限,但是 group1 有读写权限。

user1 将无法读写文件, group1 中的其他用户可以读写文件。

特殊权限

  • SUID: 用于二进制可执行文件,执行命令的时候,自动获取文件所属用户的权限,不需要密码,比如 修改文件命令 ( /usr/bin/passwd )
    • chmod 4755 ${file}: 第一个 4 即为设置 SUID 权限
  • SGID: 用于目录,在该目录下创建新文件与目录,权限自动修改为该目录所属用户组,用于文件共享
    • chmod 2755 ${file}: 第一个 2 即为设置 SUID 权限
  • SBIT: 用于目录,该目录下新建的文件和目录,仅 root 与自己可以删除
    • chmod 1755 ${file}: 第一个 1 即为设置 SUID 权限

网络管理

网络状态查看

老版本 Linux 使用 net-tools ,新版本使用的是 iproute

net-tools 包括

  • ifconfig:
    • lo: 本地环回
    • eth1: 板载网卡
    • ens33: PCI-E 网卡,老版本 Linux 没有
    • enp0s3: 无法获取物理信息的 PCI-E 网卡,老版本 Linux 没有
    • eth0: 以上都不匹配使用 eth0 ,指第一块网卡,多个网卡会递增。
  • route: 查看网关,会将 IP 解析为主机名,比较慢,推荐使用 route -n
  • netstat: 查看当前的网络监听信息。

iproute 包括

  • ip
  • ss

ipconfig

回显中常用参数含义如下:

  • inet: 网络 IP
  • netmask: 网络掩码
  • ether: 网卡 MAC
  • RX packets: 接收了多少数据包
  • RX errors: 接收了多少错误数据包
  • TX packets: 发送了多少数据包
  • TX errors: 发送了多少错误数据包

修改网卡名称

vim /etc/default/grub 编辑网卡配置可以批量修改网卡名称

# 传递给 Linux 内核的参数
GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet"

# 修改为以下配置,可以让网卡从 eth0 自增,不区分网卡
GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet biosdevname=0 net.ifnames=0"

让 grub 配置生效可以使用 grub2-mkconfig -o /boot/grub2/grub.cfg

然后重启即可

网关配置

  • ifconfig ${eth} ${ip} [netmask ${mask}]: 设置网卡 IP 地址、掩码等

  • ifup ${eth}: 启用网卡

  • ifdown ${eth}: 禁用网卡

  • route add default gw ${gateway}: 添加默认网关, add 修改为 del 即为删除

  • route add -host ${IP} gw ${gateway}: 添加明细路由,访问 IP 时,使用指定的 gateway

  • route add -net ${网段} netmask ${mask} gw ${gateway}: 访问网段时,使用指定的 gateway

  • ip addr ls: 等同于 ifconfig

  • ip link set dev ${eth} up: 等同于 ifup ${eth}

  • ip link set dev ${eth} down: 等同于 ifdown ${eth}

  • ip addr add 1.1.1.1/24 dev ${eth}: 等同于 ifconfig ${eth} 1.1.1.1 netmask 255.255.255.0

  • ip route add 1.1.1.1/24 via 192.168.0.1: 等同于 route add -net 1.1.1.1 netmask 255.255.255.0 gw 192.168.0.1

网络排障

  • ping: 查看主机联通状态
  • traceroute: 跟踪每一跳路由,可以看数据在哪里丢失,如果中间主机不支持追踪,会展示 *
    • traceroute -w 1 www.baidu.com: 超时时间为 1s ,跟踪访问百度
  • mtr: 集成了 pingtraceroute ,展示内容更丰富
  • nslookup: 查看 dns 解析
  • telnet: 检查端口是否联通,比如 telnet www.baidu.com 80
  • tcpdump: 抓包工具,比如 tcp -i any -n host 1.1.1.1 and port 80:
    • -i: 指定抓包的网卡
    • -n: 会将域名解析为 IP 地址
    • host: 指定目的 IP
    • port: 指定目的端口
    • and: 查询条件联合,如果单条件不需要 and
  • netstat: 查看端口监听状态,比如 netstat -ntpl
    • -n: 会将域名解析为 IP 地址
    • -t: 查看 TCP 协议
    • -p: 打印信息带上进程名
    • -l: stateLISTEN
  • ss: netstat 替代,例如 ss -ntpl
    • -n: 会将域名解析为 IP 地址
    • -t: 查看 TCP 协议
    • -p: 打印信息带上进程名
    • -l: stateLISTEN

网络框架

Linux 内核 2.4 版本开始,内核引入了一套通用的过滤框架 —— Netfilter ,允许外界对网络数据包在内核协议栈流转过程中进行代码干预。

Linux 系统中的各类网络功能,如地址转换、封包处理、地址伪装、协议连接跟踪、数据包过滤、透明代理、带宽限速和访问控制等,都是基于 Netfilter 提供的代码拦截机制实现的。可以说, Netfilter 是整个 Linux 网络系统最重要(没有之一)的基石。

Netfilter 围绕网络协议栈(主要在网络层)埋下了 5 个钩子(也称 hook ),用来干预 Linux 网络通信。

内核中的其他模块(如 iptablesIPVS 等)向这些钩子注册回调函数。当数据包进入内核网络协议栈并经过这些钩子时,注册的回调函数自动触发,从而对数据包进行相应的干预和处理。

这 5 个钩子的名称与含义如下:

  • PREROUTING: 只要数据包从设备(如网卡)进入协议栈,就会触发该钩子。当我们需要修改数据包的 Destination IP 时,会使用到该钩子,即 PREROUTING 钩子主要用于目标网络地址转换( DNATDestination NAT )。
  • FORWARD: 顾名思义,指转发数据包。前面的 PREROUTING 钩子并未经过 IP 路由,不管数据包是不是发往本机的,全部照单全收。如果发现数据包不是发往本机,则会触发 FORWARD 钩子进行处理。此时,本机就相当于一个路由器,作为网络数据包的中转站, FORWARD 钩子的作用就是处理这些被转发的数据包,以此来保护其背后真正的“后端”机器。
  • INPUT: 如果发现数据包是发往本机的,则会触发本钩子。 INPUT 钩子一般用来加工发往本机的数据包,当然也可以做数据过滤,保护本机的安全。
  • OUTPUT: 数据包送达到应用层处理后,会把结果送回请求端,在经过 IP 路由之前,会触发该钩子。 OUTPUT 钩子 一般用于加工本地进程输出的数据包,同时也可以限制本机的访问权限。比如,将发往 www.example.org 的数据包都丢弃掉。
  • POSTROUTING: 数据包出协议栈之前,都会触发该钩子,无论这个数据包是转发的,还是经过本机进程处理过的。 POSTROUTING 钩子 一般用于源网络地址转换(SNATSource NAT)。

处理网络包流程

  1. 外部数据包到达主机时,首先由网卡 eth0 接收。
  2. 网卡通过 DMA ( Direct Memory Access, 直接内存访问)技术,将数据包拷贝到内核中的 RingBuffer (环形缓冲区)等待 CPU 处理。 RingBuffer 是一种首尾相接的环形数据结构,它作为缓冲区,缓解网卡接收数据的速度快于 CPU 处理数据的速度问题。
  3. 接着,网卡产生 IRQ ( Interrupt Request, 硬件中断),通知内核有新的数据包到达。
  4. 内核调用中断处理函数,标记新数据到达。接着,唤醒 ksoftirqd 内核线程,执行软中断( SoftIRQ )处理。
  5. 软中断处理中,内核调用网卡驱动的 NAPI ( New API ) poll 接口,从 RingBuffer 中提取数据包,并转换为 skb ( Socket Buffer )格式。 skb 网络子系统中用于描述网络数据包的核心数据结构。数据包的发送、接收还是转发,内核都会通过 skb 来处理。
  6. skb 被传递到内核协议栈,在多个网络层次间处理:
    • 网络层( L3 Network layer ):根据主机中的路由表,判断数据包路由到哪一个网络接口( Network Interface )。这里的网络接口可能是稍后介绍的虚拟设备,也可能是物理网卡 eth0 接口。
    • 传输层( L4 Transport layer ):处理网络地址转换( NAT )、连接跟踪( conntrack )等。
  7. 内核协议栈处理完成后,数据包被传递到 socket 接收缓冲区。应用程序利用系统调用(如 Socket API )从缓冲区读取数据。至此,整个收包过程结束。

传输成本问题

分析 Linux 系统处理网络数据包的过程,我们注意到潜在问题:数据包的处理流程过于冗长。

处理流程涉及到多个网络层协议栈(如数据链路层、网络层、传输层和应用层),网络层协议栈之间需要封包/解包,还有频繁的上下文切换( Context Switch ),都让 Linux 内核的瓶颈不可忽视。

对于设计网络密集型系统,优化内核参数是不可或缺的一环。

除了想办法优化内核参数,另外一批人抱着它不行就绕开它想法,提出了一种“内核旁路“( Kernel bypass )思想的技术方案。其中, DPDKXDP 是主机内“内核旁路”思想的代表技术, RDMA 是主机之间“内核旁路”思想的代表技术。

DPDK ( Data Plane Development Kit, 数据平面开发套件)

  • 传统内核网络 (图左侧): 网络数据包自网络接口卡( NIC )出发,经过驱动程序,内核协议栈,最后通过 Socket 接口传递至业务逻辑。
  • DPDK 加速网络 (图右侧): 在该方案中,网络数据包利用 用户空间 I/OUIO )技术,直接绕过内核协议栈,从网卡转移到 DPDK 基础库,然后传递至业务逻辑。也就是说 DPDK 绕过了 Linux 内核协议栈对数据包的处理过程,在用户空间直接对数据包进行收发与处理。

爱奇艺开源的 DPVSDPDK 技术在负载均衡领域的成功应用。从每秒转发数据包数量( Packet Per Second, PPS )指标来看, DPVS 的性能表现比 LVS 高出 300%

eBPF 和 快速数据路径 XDP ( eXpress Data Path, 快速数据路径)

DPDK 技术完全绕过内核,直接将数据包透传至用户空间处理。 XDP 正好相反,它在内核空间根据用户的逻辑处理数据包。

在内核执行用户逻辑的关键在于 BPFBerkeley Packet Filter ,伯克利包过滤器)技术 —— 一种允许在内核空间运行经过安全验证的代码的机制。 Linux 内核 2.5 版本起, Linux 系统就开始支持 BPF 技术了,但早期的 BPF 主要用于网络数据包的捕获和过滤。到了 Linux 内核 3.18 版本,开发者推出了一套全新的 BPF 架构,也就是我们今天所说的 eBPFExtended Berkeley Packet Filter )。与早期的 BPF 相比, eBPF 的功能不再局限于网络分析,它几乎能访问所有 Linux 内核关联的资源,逐渐发展成一个多功能的通用执行引擎。

XDP 本质是 Linux 内核在网络路径上埋下的钩子,该钩子位于网卡驱动层内,数据包进入网络协议栈之前。如果 XDP 钩子挂载了 eBPF 程序,就能在 Linux 系统收包早期阶段介入处理,避免数据包“循规蹈矩”的进入内核,带来的额外开销。

XDP 执行完 eBPF 逻辑之后,用“返回码”作为输出,它代表对数据包应该做什么样的最终裁决。 XDP 支持的 5 种返回码名称及含义如下:

  • XDP_ABORTED: 表示 XDP 程序处理数据包时遇到错误或异常。
  • XDP_DROP: 在网卡驱动层直接将该数据包丢掉,通常用于过滤无效或不需要的数据包,如实现 DDoS 防护时,丢弃恶意数据包。
  • XDP_PASS: 数据包继续送往内核的网络协议栈,和传统的处理方式一致。这使得 XDP 可以在有需要的时候,继续使用传统的内核协议栈进行处理。
  • XDP_TX: 数据包会被重新发送到入站的网络接口(通常是修改后的数据包)。这种操作可以用于实现数据包的快速转发、修改和回环测试(如用于负载均衡场景)。
  • XDP_REDIRECT: 数据包重定向到其他的网卡或 CPU ,结合 AF_XDP 可以将数据包直接送往用户空间。

通过下面的步骤,进一步了解 eBPF 程序是如何被 Linux 内核加载、验证并执行:

  1. 编写的 eBPF 程序,经过编译器,编译为 eBPF 字节码。
  2. 编译好的代码,会被 eBPF 所对应的高级编程语言库程序加载,并由高级语言进行系统调用处理。目前 eBPF 支持 GolangPythonC / C++Rust 等。
  3. 通过系统调用陷入内核后,首先由内核 eBPF 程序进行验证( verify ),这一步确保程序本身无误:不会崩溃,不会出现死循环,没有权限异常;然后编译为 eBPF 伪代码的程序再转换为具体的机器指令集,最终挂载到对应的钩子处(或称追踪点)。
  4. 内核在处理某个追踪点时,刚好有 eBPF 程序,则触发事件,并由加载的 eBPF 程序处理

eBPF 能够在不修改内核源码的情况下,动态加载和执行用户定义的代码,在 Linux 内核的多个子系统,如网络、跟踪和 Linux 安全模块( LSM )中广泛应用。基于 eBPF 技术的开源项目也层出不穷,如 Facebook 的高性能网络负载均衡器 Katran 、内核跟踪调试工具 BCCbpftrace ,以及 Isovalent 的容器网络方案 Cilium 等。

Cilium 为例,它在 eBPFXDP 钩子(也有其他的钩子)基础上,实现了一套全新的 conntrackNAT 机制。并以此为基础,构建出如 L3 / L4 负载均衡、网络策略、观测和安全认证等各类高级功能。

由于 Cilium 实现的底层网络功能现独立于 Netfilter ,因此它的连接追踪数据和 NAT 规则数据不会存储在 Linux 内核默认的 conntrack 表和 NAT 表中。常规的 Linux 命令 conntracknetstatsslsof 等,无法查看 NAT 规则和连接追踪数据。得使用 Cilium 提供的查询命令才行,例如:

cilium bpf nat list ## 列出 Cilium 中配置的 NAT 规则。
cilium bpf ct list global## 列出 Cilium 中的连接追踪条目

RDMA ( Remote Direct Memory Access, 远程直接内存访问)

传统的以太网在网络延迟、吞吐量和 CPU 资源消耗方面存在先天不足。此背景下,广泛应用于高性能计算领域的 RDMA ( Remote Direct Memory Access ,远程直接内存访问)技术脱颖而出。

RDMA 设计灵感来源于 DMA (Direct Memory Access,直接内存访问),是一种允许主机之间直接访问彼此内存的技术。

DMA 技术中,无需 CPU 参与,主机内部的设备(如硬盘或网卡)能够直接与内存交换数据; RDMA 的工作原理如图所示,应用程序通过专用的接口(RDMA Verbs API)绕过主机操作系统和 TCP / IP 协议栈,达到了直接访问远程主机内存的效果。

RDMA 网络的协议实现分为三类: InfinibandiWARPRoCE ,它们的含义及区别如下:

  • Infiniband (无限带宽),是一种专门为 RDMA 而生的技术,由 IBTA ( InfiniBand Trade Association, InfiniBand 贸易协会)在 2000 年提出,因其极致的性能(能够实现小于 3 μs 时延和 400Gb/s 以上的网络吞吐),在高性能计算(HPC)领域中备受青睐。 但注意的是,构建 Infiniband 网络需要配置全套专用设备,如专用网卡、专用交换机和专用网线,限制了其普及性。其次,它的技术架构封闭,不兼容现有的以太网标准。这意味着,绝大多数通用数据中心都无法兼容 Infiniband 网络。

    尽管存在上述缺陷,但 Infiniband 因其卓越的性能仍然是某些领域是首选。例如,全球流行的人工智能应用 ChatGPT 背后的分布式机器学习系统就是基于 Infiniband 网络构建的。

  • iWRAP ( Internet Wide Area RDMA Protocol, 互联网广域 RDMA 协议),这是一种将 RDMA 封装在 TCP/IP 协议内的技术。 RDMA 网络为了高性能而生,而 TCP/IP 协议为了可靠性而生,它的三次握手、拥塞控制等机制让 iWRAP 失去了绝大部分 RDMA 技术的优势。所以,先天设计缺陷让 iWRAP 逐渐被业界抛弃。\

  • 为了降低 RDMA 技术的使用成本,并使其应用于通用数据中心领域, 2010 年, IBTA 发布了 RoCE (RDMA over Converged Ethernet, 融合以太网的远程直接内存访问)技术,将 Infiniband 的数据标准(IB Payload)“移植”到以太网。只需配备支持 RoCE 的专用网卡和标准以太网交换机,即可享受 RDMA 技术带来的高性能。

    • RoCEv1 基于二层以太网,局限于同一子网,无法跨子网通信。
    • RoCEv2 基于三层 IP 网络,支持跨子网通信。

RoCEv2 解决了 RoCEv1 无法跨子网的局限,凭借其低成本和兼容性优势, RoCE 技术开始广泛应用于分布式存储、并行计算等通用数据中心场景。根据云计算平台 Azure 公开的信息,至 2023 年,Azure 整个数据中心 70% 的流量已经是 RDMA 流量了

RDMA 网络对丢包极为敏感,任何数据包的丢失都可能导致大量重传,降低传输性能。 Infiniband 网络依靠专用设备来确保网络可靠性,而 RoCE 网络则基于标准以太网实现 RDMA ,这要求基础设施必须具备无损以太网功能,以避免丢包对性能造成严重影响。

目前,大多数数据中心使用 DCQCN (微软和 Mellanox 提出)或者 HPCC (阿里巴巴提出)算法为 RoCE 网络提供可靠性保障。

软件包管理

包管理器

CentOS 、 RedHat 使用 yum 作为管理器,软件包格式为 rpm ,包格式为 ${软件名}-${版本号}.${系统版本}.${平台}.rpm

rpm 常见命令如下:

  • rpm -q ${software}: 查询是否安装了 ${software}
  • rpm -qa | more: 查询安装了哪些软件包,并且分页展示
  • rpm -i ${xxx.rpm}: 手动安装指定软件包, rpm 不能自动安装依赖包,需要手动处理缺失依赖
  • rpm -e ${software}: 手动卸载指定软件包

yum 可以自动识别依赖,并自动下载依赖包,软件源配置存放在 /etc/yum.repos.d/CentOS-Base.repo

  • yum makecache: 更新源后使用,清空软件包缓存,重新下载源数据
  • yum install ${software}: 安装 ${software}
  • yum remove ${software}: 移除 ${software}
  • yum list: 查看软件包
  • yum grouplist: 查看软件包
  • yum update [${software}]: 升级(指定)软件包

Ubuntu 、 Debian 使用 apt 作为管理器,软件包格式为 deb

dpkg 常见命令如下:

  • dpkg-query ${software}: 查询是否安装了 ${software}
  • dpkg-query -l | more: 查询安装了哪些软件包,并且分页展示
  • dpkg -i ${xxx.dpkg}: 手动安装指定软件包, rpm 不能自动安装依赖包,需要手动处理缺失依赖
  • dpkg -r ${software}: 手动卸载指定软件包

编译安装

包管理器中的版本通常较低,需要使用最新版可以采用二进制安装或者编译安装,部分软件包也并没有提供包管理器安装方案,只能手动安装。

一般编译安装的过程如下:

# 声明版本号,避免重复输入
$version=1.15.8.1

# 下载源码
wget "https://openresty.org/download/openresty-${version}.tar.gz"

# 解压源码
tar -zxf "openresty-${version}.tar.gz"

# 进入解压后 源码目录
cd "openresty-${version}/"

# 按照运行环境进行配置,比如设置安装路径等等
./configure--prefix=/usr/local/openresty

# 编译源码为可执行程序 -j2 指使用 CPU 两个核心用于编译
# 在编译时可能出现各种问题,比如 gcc 库版本不匹配、依赖软件包未安装等,可以参考报错信息或者官方文档解决。
make -j2

# 安装软件包
makeinstall

不同软件的安装方式有所差异,具体需要看软件提供的安装说明。

内核升级

# 查看内核版本
uname -r
# 升级内核版本
yum install "kernel-${version}"
# 更新软件包
yum update

内核编译安装

# 安装编译依赖包
yum install gcc gcc-c++ make ncurses-devel openssl-devel elfutils-libelf-devel

# 下载需要在官方 https://www.kernel.org 手动下载

# 源码存放路径
$source='/usr/src/kernals'

# 解压内核
tar xvf linux-5.1.10.tar.xz -C $source

# 进入源码目录
cd "${source}/linux-5.1.10"

# === 以下配置方式任选其一 === #

# 通过菜单进行配置
make menuconfig
# 全选内核功能
make allyesconfig
# 最小内核
make allnoconfig
# 使用当前系统的内核编译配置
cp /boot/config-kernelversion.platform "${source}/linux-5.1.10/.config"

# === 配置完成 === #

# 编译配置的全部内容,使用 2 个 CPU 核心进行编译
make -j2 all

# 先安装内核支持的模块
make modules_install
# 再安装内核
make install

# 查询 Linux grub 配置,查找有几个内核程序
grep ^menu /boot/grub2/grub.cfg

# 设置默认选择第几个内核, 0 就是第一个,也就是更新的内核
grub2-set-default 0

grub 配置文件存放在 /etc/default/grub

# 为 Linux 添加哪些配置, Linux 启动时,通过 e 可以看到下述参数被追加到 Linux 内核启动命令中
# - rhgb: 指在启动时使用图形界面
# - quiet: 指在启动时只输出必要信息
# - single: 遗忘密码时,可以直接跳过密码登录
# - rd.break:
GRUB_CMDLINE_LINUX="rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet"

忘记密码重置

忘记密码时,可以通过启动时按 e ,进入 Linux 引导程序,配置启动参数 ( linux16 开头的就是 )

追加 singlerd.break ( CentOS 7 及以上需要 ) ,通过 ctrl x 使用该参数启动

启动后会进入一个虚拟的根目录,通过 mount -o remount,rw /sysroot 将磁盘挂载,能够对文件进行读写

修改根目录 ( chroot /sysroot )

设置新密码 echo ${password} | passwd --stdin

设置后需要确保 SELinux 是关闭状态,否则会被安全拦截,通过 vim /etc/selinux/config 可以打开 SELinux 配置文件,将 SELINUX=enforcing 修改为 SELINUX=disabled

配置完成通过 exit 回到虚拟目录,再 reboot 重启

进程管理

进程是操作系统 分配资源的最小单位 。 进程结束的时候不一定会正常结束,也可能通过 ctrl c 或者 single 强制结束,也就是 abort

进程可以创建子进程,所以进程也是一棵树。

线程是操作系统 任务调度的最小单位 。 CPU 在分配时间片时,是按照线程来分配的,同一进程创建的线程,将共享资源。

查看进程

  • ps: 查看当前终端能查到的进程, PID 是进程的唯一标识, CMD 是执行的命令
    • ps -e: 查看不同终端的所有进程
    • ps -f: 查看结果携带 UIDPPID ,即哪个用户启动的,这个 UID 是可以修改的,不一定准确, PPID 是父进程的 ID 。
    • ps -L: 查了线程
    • ps -Z: 查看 SELinux 给打的标签
  • pstree: 按照 PPID 聚合成树形结构展示。
  • top: 动态展示进程信息,包括资源使用情况,进程默认每 3 秒更新一次,可以通过 s 编辑更新时间
    • up xxx,: 开机到现在运行了多少时间
    • xx users,: 有几个用户登录了
    • load average: xx, xx, xx: 当前系统负载,对应 1 分钟 、 5 分钟 、 15 分钟的资源使用情况。
    • Tasks: 有多少进程在运行,有多少进程在休眠,有多少进程被停止
    • Cpu(s): CPU 使用率,多个 CPU 展示平均值,通过 1 切换逐个 CPU 展示还是平均值
      • us: 用户计算
      • ni: nice ,即优先级
      • sy: 进程状态交互
      • id: 空闲状态
      • wa: 等待状态
    • KiB Mem: 内存使用率
    • KiB Swap: Swap 使用率。 Swap 是内存淘汰后临时存放的磁盘空间, Swap 使用率低说明内存负载低,如果内存与 Swap 都满了,会 随机杀掉占用内存较大的进程
  • top -p ${PID}: 只查看指定进程状态

阻塞进程、非阻塞进程

阻塞进程

指正在运行的进程因等待某些事件(如 I/O 操作、资源获取或数据到达)而无法继续执行,主动放弃 CPU 进入阻塞状态的过程。此时,进程会调用阻塞原语将自己挂起,并加入阻塞队列,直到事件触发后被唤醒。

由于每个进程都需要被唤醒,才能知道是否还在阻塞,所以当阻塞的进程数较多时,会出现频繁切换进程的情况,此时会出现大量的性能开销,对应的就是 Apache 、 Tomcat 等服务。

非阻塞进程

非阻塞进程通过异步控制,在资源准备完成后主动唤醒进程,避免了频繁的进程切换开销。

在进程发起非阻塞操作时,系统调用会返回 EAGAN 错误,而不是阻塞进程,这样可以在软件层面通过主动轮询或者事件驱动,在资源准备完毕后才去唤醒进程,典型事例有 nginx 。

进程控制

优先级控制

  • nice -n ${value} ${file}: 设定文件运行时的优先级。 ${value} 范围为 [-20, 19] ,值越小优先级越高。
  • renice -n ${value} ${PID}: 设置运行中的进程的优先级。

作业控制

  • &: 放到命令末尾,命令会放到到后台运行。
  • ctrl z: 暂停当前命令,并将当前运行的命令切换到后台。
  • jobs: 查看所有后台运行的进程。
  • fg ${value}: 将后台进程切换到前台, ${value}jobs 获取,最前面的数字即为 ${value}

进程间通信

  • kill -l: 查看当前支持的所有信号
  • SIGINT: 中断程序,对应 -2
  • SIGKILL: 无条件立即结束,对应 -9

守护进程( daemon )

  • 守护进程: 完全脱离终端控制(即使启动时依赖终端),​​不接收终端信号​​(如 SIGHUP ),且​​不向终端输出信息​​,通常随着操作系统长期运行。
  • nohup 进程: 仅​​忽略 SIGHUP 信号​​(终端挂断信号),但​​未完全脱离终端​​。

nohup

nohup 会忽略 HANGUP 信号,终端关闭了也不会关闭进程。

一般会在后台启动,也就是配合 & 使用,例如: nohup tail -f /var/log/messages & ,期间所有输出会放到 ./nohup.out

nohup 启动后,关闭终端,进程对应的父进程会被杀死,此时进程会变成孤儿进程,被 1 号进程收留,所以 PPID 也会随之改变。

查看守护进程

/proc 目录会在被访问时,读取内存信息,映射到本地文件中,通过这种方式可以读取守护进程的信息。

通过 /proc/${PID} 可以查看内存映射出来的进程内容。

  • 查看进程的文件操作可以直接看 ls -l fd
  • 查看进程的执行根目录可以直接看 ls -l cwd

系统日志

系统日志一般存放在 /var/log 目录下

  • /var/log/message: 系统实时输出的信息
  • /var/log/dmesg: 内核启动时打印的信息
  • /var/log/secure: 安全日志
  • /var/log/cron: 计划任务日志

安全策略

  • MAC: 强制访问控制,即操作系统内部强制校验,为进程、文件、用户打标签,判断是否匹配,如果不匹配就禁止访问,所以重置密码的时候需要关闭 SELinux ,也就是 MAC
  • DAC: 自主访问控制,即用户通过权限、用户组等方式自行实现的访问控制能力

内存与磁盘

查看信息

查看内存信息

  • free: 查看内存与 Swap 分区使用情况
    • free -m: 格式化单位,单位为 MB ,小数部分会被舍弃
    • free -g: 格式化单位,单位为 GB ,小数部分会被舍弃
    • total: 所有内存
    • used: 已被使用的内存
    • free: 空闲内存
    • buff/cache: 缓存区内存
    • available: 由于 Linux 进程会尽可能多的占用空闲内存,所以有一部分是申请了但是没有使用的,就是 available
  • top: 实时查看内存占用以及其他信息

查看磁盘信息

  • fdisk: 查看分区
    • fdisk -l: 查看所有磁盘,磁盘命名一般为 /dev/sda/dev/sdb ,可以查看磁盘大小、扇区、 I/O 速度等。等同于 parted -l
    • ls /dev/sd?? -l: 查看所有磁盘的所有分区,磁盘命名一般为 /dev/sda1/dev/sdb1
  • df: 查看磁盘挂载点
    • df -h: 查看磁盘挂载点,输出格式化后的内容
  • du: 查看文件占用多少空间,包含空洞。

文件系统

文件系统类型

  • ext4: CentOS 6 以及更早版本默认使用
  • XFS: CentOS 7 以及更晚版本默认使用
  • NTFS: 需要额外安装软件包,有版权限制,否则只读

ext4 包含以下内容:

  • 超级块: 记录分区中包含的文件以及文件总数, df 就是直接读的这个信息
  • 超级块副本: 数据备份,恢复数据时就是将副本内的数据恢复到超级块
  • i 节点: 记录每个文件的信息,包括大小、权限、内容等,但是不包括文件名,文件名存放在上级目录中
  • 数据块: 储存 i 节点的数据, i 节点可能不止对应一个数据块,看节点的数据大小

文件操作

  • touch ${file}: 创建文件
  • echo 123 > ${file}: 写入文件,会为 inode 挂载数据块,一个数据块最少为 4kb ,可以通过 du -h ${file} 验证
  • cp ${fromFile} ${toFile}: 拷贝文件
  • mv ${fromFile} ${toFile}: 同目录修改的话,只修改目录中存储的文件名,跨目录移动需要迁移数据块,耗时长
  • vim ${file}: vim 本质上是复制了一份数据块与 inode ,放到 ${file}.swap ,在保存时修改目录的文件名指针指向 ${file}.swap ,这样可以避免编辑过程中宕机等事故,导致脏数据污染源文件。
  • rm ${file}: 仅断开文件名与 inode 链接
  • ln ${file} ${newFile}: 将新文件名指向 ${file} 对应的 inode ,只能同分区使用
  • ln -s ${file} ${newFile}: 创建符号链接,新文件名对应的不是 inode ,而是源文件的路径,适合跨磁盘、跨文件系统使用

文件访问控制

facl 可以提供比 rwx 更精细的权限控制能力

  • getfacl ${file}: 查看文件的 facl 策略
  • setfacl -m u:${user}:r ${file}: 新增 facl 策略,添加 指定用户 读权限
  • setfacl -m g:${group}:w ${file}: 新增 facl 策略,添加 指定用户组 写权限
  • setfacl -s u:${user}:r ${file}: 新增 facl 策略,收回 指定用户 读权限

磁盘分区

如果磁盘小于 2T 可以使用 fdisk 分区

  • fdisk /dev/sda: 磁盘分区,一块磁盘最多有 4 个主分区,其余为扩展分区。
    • d: 删除分区
    • n: 创建分区
    • q: 退出分区并且不保存
    • w: 保存分区方案并退出
    • p: 打印当前的分区方案
  • mkfs.ext4 /dev/sda1: 设置分区的文件系统类型为 ext4 ,并进行分区
  • mount -t ext4 /dev/sda1 /mnt/sda1: 手动将磁盘挂载到文件系统上,未挂载不能直接访问
    • -t ext4: 指定文件系统为 ext4
    • -t auto: 文件系统自动检查,如果没有传递 -t 默认就是自动检测

mount 只能手动挂载,重启设备后就丢失,需要重新挂载,可以将磁盘信息写入 /etc/fstab ,即可自动挂载,例如在 /etc/fstab 新增如下一行

# 磁盘   挂载地址   文件格式   磁盘权限   磁盘是否需要备份   开机自检时,检查顺序, 0 为不检查
/dev/sda1 /mnt/sda1 ext4 defaults 0 0

超过 2T 需要使用 parted 分区,基本流程相同,语法有所不同

用户磁盘配额

xfs 文件系统的用户磁盘配额 quota

  • fdisk /dev/sdb
  • mkfs.xfs /dev/sdb1: 使用 xfs 格式化磁盘分区,如果格式化前后磁盘分区是不同文件系统,则需要使用 -f
  • mkdir -p /mnt/disk1: 如果目录不存在,则自动创建目录
  • mount -o uquota,gquota /dev/sdb1 /mnt/disk1: 挂载磁盘, uquota 是用户配额限制, gquota 是用户组配额限制
  • chmod 1777 /mnt/disk1: 修改磁盘权限,方便处理,非必要
  • xfs_quota -x -c 'report -ugibh' /mnt/disk1: 查看磁盘报告
    • -u: 查看用户配额
    • -g: 查看用户组配额
    • -i: 查看 inode 信息
    • -d: 查看 数据块 信息
    • -h: 格式化展示信息
  • xfs_quota -x -c 'limit -u isoft=5 ihard=10 userl' /mnt/disk1: 添加磁盘配额限制
    • -u: 限制用户配额
    • -g: 限制用户组配额
    • isoft: inode 软限制,实际允许超过,但是会提示
    • ihard: inode 硬限制,绝对不允许超过
    • bsoft: 数据块软限制,实际允许超过,但是会提示
    • bhard: 数据块硬限制,绝对不允许超过

Swap 分区管理

创建 Swap 分区流程:

  • fdisk /dev/sdc
  • mkswap /dev/sdc1: 格式化为 Swap 分区,实际为打上 Swap 标签
  • swapon /dev/sdc1: 打开 Swap 分区,此时就会追加到 Swap 中了
  • swapoff /dev/sdc1: 关闭 Swap 分区,此时就会从 Swap 中移除了

使用文件作为 Swap 分区:

  • dd if=/dev/zero bs=4M count=1024 of=/swapfile: 创建 4G 大的空洞文件作为 Swap 分区
  • mkswap /swapfile: 格式化为 Swap 分区,实际为打上 Swap 标签,建议权限改为 600 ,否则可能会报错

如果需要永久生效,也需要写入 /etc/fstab:

# 磁盘   挂载地址   文件格式   磁盘权限   磁盘是否需要备份   开机自检时,检查顺序, 0 为不检查
/swapfile swap swap defaults 0 0

磁盘 RAID

RAID 即为磁盘组合使用,类似集群的概念,有专用的 RAID 磁盘,分为以下四个级别:

  • RAID 0 striping 条带模式,数据 1 分为 N ,写入不同磁盘,每个磁盘存储不同的内容
  • RAID 1 mirroring 镜像模式,数据同时写入不同磁盘,即磁盘备份
  • RAID 5 奇偶校验,一般至少 3 块硬盘,前 2 块硬盘为 RAID 0 ,后一块硬盘写入奇偶校验,通过其中一块硬盘 + 奇偶校验可以还原另一个硬盘数据
  • RAID 10 至少 4 块硬盘, 2 块 RAID 1 、 2块 RAID 0

逻辑卷管理

名词:

  • LVM: 逻辑卷管理器
  • PV: 物理卷, LVM 的最底层存储单元,可以是磁盘、分区、 RAID 设备等。
  • VG: 多个物理卷组合成的卷组
  • LV: 逻辑卷,从卷组中划分出来的虚拟分区,最终被格式化为文件系统,支持动态扩容

创建逻辑卷过程:

  • pvcreate /dev/sd[a,b,c]1: 将 sda1sdb1sdc1 初始化为 PV
  • pvs: 查看 PV 信息
  • vgcreate vg1 /dev/sda1 /dev/sdb1: 创建卷组 vg1 ,并将 sda1sdb1 加入卷组中
  • vgs: 查看 VG 信息
  • lvcreate -L 100M -n lv1 vg1: 在 vg1 上创建一块逻辑卷 lv1 ,大小为 100M
  • lvs: 查看 LV 信息
  • mkfs.xfs /dev/vg1/lv1: 格式化为 xfs 文件系统
  • mount /dev/vg1/lv1 /mnt/lv: 挂载到系统磁盘

扩容现有磁盘

  • mount | grep ${dir}: 查看目录在哪块卷组上
  • vgextend ${vg} ${pv}: 将 PV 集成到 VG 中
  • lvextend -L +100M -n ${lv}: 给 LV 扩容
  • xfs_growfs ${lv}: 通知文件系统已经扩容,更新磁盘空间

系统诊断

  • sar: 系统监控,一般内置
    • sar -u 1 10: 每间隔 1 秒对 CPU 采样,采样 10 次
    • sar -r 1 10: 每间隔 1 秒对 内存 采样,采样 10 次
    • sar -b 1 10: 每间隔 1 秒对 磁盘 I/O 采样,采样 10 次
    • sar -d 1 10: 每间隔 1 秒对 每块磁盘 采样,采样 10 次
    • sar -q 1 10: 每间隔 1 秒对 进程 采样,采样 10 次
  • iftop -P: 网络监控,需要安装软件包。 类似 top 一样的展示效果,实时监控网络情况。

shell 脚本

Linux 启动过程

graph TB
BIOS --> MBR
MBR --> BootLoader["BootLoader ( grub )"]
BootLoader --> kernel
kernel --> systemd['systemd ,即 1 号进程,低版本为 init']
systemd --> 系统初始化
系统初始化 --> shell

MBR

MBR 即为主引导记录

  • dd if=/dev/sda of=mbr.bin bs=446 count1: 导出主引导记录
  • dd if=/dev/sda of=mbr.bin bs=512 count1: 导出主引导记录 + 分区表
  • hexdump -C mbr.bin: 读取二进制文件,并展示

BootLoader

BootLoader 即 grub 程序,将引导 Linux 内核启动。

  • /boot/grub2/grub2-editenv list: 查看引导内核版本

kernel

启动内核,并运行 1 号进程,低版本 Linux 是 init ,高版本为 systemd

systemd

systemd 会按照当前的启动级别,启动不同的进程

shell 语法

shell 文件格式

shell 文件格式如下

#!/bin/bash

# shell 命令

当使用 shell 解析器如 bashsh 等工具解析时,会将第一行认为是注释,忽略

当通过 ./test.sh 执行时,会根据第一行注释定义的解析器去执行 shell

#!/bin/bash 也被称为 Sha-Bang

shell 不同执行方式的区别

  • bash test.sh: 启动子进程去执行,对当前 shell 无影响,如果文件无可执行权限也能被执行。
  • ./test.sh: 启动子进程去执行,对当前 shell 无影响,必须要有 rx 权限。
  • source ./test.sh: 在当前 shell 执行,执行结果会对当前 shell 产生影响,如果文件无可执行权限也能被执行。
  • . ./test.sh: 在当前 shell 执行,执行结果会对当前 shell 产生影响,如果文件无可执行权限也能被执行。

内建命令与外部命令

  • 内建命令: shell 执行器自带的命令,不会创建子进程执行
  • 外部命令: 不是内建命令的命令,会创建子进程执行

管道

管道可以将不同命令连接起来,将上一个命令的输出,作为下一个命令的输入,持续组合。

| 即为管道符,也称匿名管道,管道符左右两边的命令会创建子进程去执行,并将输入输出进行连接,也即是进程间通信。

由于会创建子进程去执行,所以 内建命令也无法影响到当前 shell

重定向

一个进程默认会打开标准输入、标准输出、错误输出三个文件描述符

  • <: 输入重定向。 ${command} < ${file}, 将 file 的内容作为 command 的输入
  • >: 输出重定向。 ${command} > ${file}, 将 command 的输出写入 file (覆盖源文件)
  • >>: 输出重定向。 ${command} >> ${file}, 将 command 的输出写入 file (追加到文件末尾)
  • 2>: 错误重定向。 ${command} 2> ${file}, 将 command 的错误信息写入 file (覆盖源文件)
  • 2>>: 错误重定向。 ${command} 2>> ${file}, 将 command 的错误信息写入 file (追加到文件末尾)
  • &>: 输出重定向。 ${command} &> ${file}, 将 command 的所有信息写入 file (覆盖源文件)
  • &>>: 输出重定向。 ${command} &>> ${file}, 将 command 的所有信息写入 file (追加到文件末尾)

变量

命名规则:

  • 字母、数字、下划线
  • 不能以数字开头

变量赋值:

  • a=123: 变量名 = 变量值, = 两端不能有空格,否则会被视为命令
  • let a=10+20: 定义变量的时候支持对变量进行计算
  • l=ls: 可以将命令设置为变量
  • letc=$(ls -l /etc): 将命令输出赋值给变量,也可以使用反引号替代 $()

变量作用域只在当前 shell ,不能跨 shell 使用,打开新终端时变量会自动失效。

通过 export ${变量名} 可以导出变量,让子进程也能访问该变量

unset ${变量名} 可以删除变量

环境变量

  • env | more: 查看当前定义的环境变量
  • PATH=$PATH:/xxx: 创建一个变量,值为全局 PATH 变量 + /xxx ,该变量需要被 export ,才能在创建新终端后也生效
  • echo $?: 上条命令的结束代码, 0 为没有错误,或者表示条件判断为 true
  • echo $$: 输出当前进程 PID
  • echo $0: 输出当前进程名称
  • echo $1: 输出进程接受的第 1 个参数
  • echo ${2-default}: 输出进程接受的第 2 个参数,如果参数值为空,设置为 default
  • echo $*: 输出所有参数列表,为字符串拼接形式
  • echo $@: 输出所有参数列表,为数组形式
  • echo $#: 添加到 shell 的参数个数

以下文件会被自动执行,同时对环境变量进行初始化:

  • /etc/profile: 全局环境变量,对所有用户生效
  • /etc/profile.d/: 存放全局环境变量的目录,自动加载,对所有用户生效
  • /etc/bashrc: 存放全局环境变量的文件, bash 自动加载,对所有用户生效
  • ~/.bash_profile: 全局环境变量,仅对当前用户生效
  • ~/.bashrc: 存放全局环境变量的文件, bash 自动加载,仅对当前用户生效

通过 su - ${user} 切换用户时,自动加载 shell 的执行顺序为:

  1. /etc/profile
  2. ~/.bash_profile
  3. ~/.bashrc
  4. /etc/bashrc

通过 su ${user} 切换用户时,自动加载 shell 的执行顺序为:

  1. ~/.bashrc
  2. /etc/bashrc

通过 bash 创建新终端,自动加载 shell 的执行顺序为:

  1. ~/.bashrc
  2. /etc/bashrc

数组

  • list=(1 2 3): 定义数组
  • echo ${list}: 输出数组第 1 个元素
  • echo ${list[@]}: 输出数组所有元素
  • echo ${#list[@]}: 输出数组元素数量
  • echo ${list[1]}: 输出数组第 2 个元素

运算符

  • expr 1 + 1: 计算 1 + 1 的结果,只能计算整数,中间必须有空格
  • let a=1+1: 可以实现与 expr 一样的计算效果,并且支持小数,中间不能有空格
  • (( a++ )): let 的语法糖,里面的表达式可以插入空格

引号

  • ': 不会解析变量
  • ": 会解析变量
  • `: 执行命令

括号

  • ( ${command} ): 创建一个子 shell 执行命令
  • ( 1 2 ): 创建数组
  • (( 1 + 2 )): 执行计算
  • $(ls): 执行 ls 命令
  • [ 1 -gt 2 ]: 进行条件判断,等同于 test 1 -gt 2 ,两边必须有空格
  • [[ 1 = 2 ]]: 进行条件判断, 1 是否等于 2 ,支持符号写法,也就是 >< ,两边必须有空格
  • <: 输入重定向
  • >: 输出重定向
  • {0..9}: 输出 0-9
  • cp -v /etc/passwd{,.bak}: 简化写法,等同于 cp -v /etc/passwd /etc/passwd.bak

判断语句

可以使用 test 命令与 [[]] 进行条件判断, [[]]test 做了扩充,支持符号写法,比如 >< 等等。

除此之外, shell 中的判断还支持对文件的判断,毕竟 Linux 一切皆文件:

  • -b ${file}: 文件存在,并且是特殊块
  • -c ${file}: 文件存在,并且是特殊字符
  • -d ${file}: 文件存在,并且是目录
  • -e ${file}: 文件存在
  • -f ${file}: 文件存在,并且是常规文件
  • -r ${file}: 文件存在,并且是有可读权限
  • -w ${file}: 文件存在,并且是有可写权限
  • -x ${file}: 文件存在,并且是有可执行权限

test 执行后,可以通过 $? 获取判断结果,不一定需要把结果保存为单独变量。

条件语句

# 只是对测试条件的 $? 判断,一般会使用 test 命令作为 if 判断条件
if [ -x /tmp/test.sh ]
# 如果 then 单独一行, if 后可以不跟 `;` ,否则一定要跟 `;`
then
  # 测试条件返回为 0 时执行
fi

# 使用 echo 命令作为判断条件,由于 echo 1 没有错误,所以此时 $? 是 0 ,那就会输出 'true'
if echo 1; then
  # 这里可以执行多条命令,块已经被 then 到 else/fi 包裹,不需要额外处理
  echo 'true'
  echo 'true'
  echo 'true'
else
  # 测试条件返回不为 0 时执行
  echo 'false'
fi

# 判断是不是 root
# $USER 和 root 的 引号 可加可不加,两者都能识别。
# 如果判断的值不确定的话,推荐都加上,避免出现奇怪的问题
if [[ $USER = root ]]; then
  echo 'root 用户'
# 可以通过 elif 在上面规则不满足时,继续判断, elif 后也需要跟 then
elif [[ "$USER" = 'he110' ]]; then
  echo 'He110 用户'
else
  echo 'false'
fi

分支语句

shell 中可以用 case 实现 JS 中的 switch 分支语句效果, case 基本格式如下:

case "$变量" in
  "值 1" )
    # 这里需要两个 ;
    # 第一个 ; 结束分支内执行的命令
    # 第二个 ; 才是结束分支
    ;;

  # 可能有多个值匹配同一分支的情况,直接用 | 连接即可
  "值 2" | "值 3" )
    ;;

  # 通配符,等同于 switch 中的 default
  * )
    ;;
esac

循环语句

# 使用 {start..end} 可以生成 [start, end] 区间内的整数数组
# 这里可以替换为其他数组,比如参数数组 `$*` 、 `"$@"`
# 遍历的时候每个数组元素会放到 item 变量中
for item in {1..9}
# 通过 do 语句定义循环体
do
  # 循环体内部执行的命令
  echo "print $item"
# 通过 done 表示循环体定义完成
done

批量重命名:

# 从命令中读取列表
for filename in `ls *.mp3`
do
  # 通过 $() 执行命令,获取文件名
  # 重命名为 xxx.mp4
  mv $filename $(basename $filename .mp3).mp4
done

# 另一种写法,可以直接遍历文件
for filename in ./*.mp3
do
  mv $filename $(basename $filename .mp3).mp4
done

C 语言风格的遍历(不推荐, shell 不适合做计算,效率不高)

for (( i = 1; i <= 10; i++ ))
do
  echo "print $i"
done

while / until 循环:

# while 判断条件为 true 时执行
while [ 2 -gt 1 ]
do
  echo '死循环了'
done

# 死循环的简洁构建方式,比如用于输出交互式菜单
while :
do
  echo '死循环了'
done

# while 判断条件为 false 时执行
until [ 1 -gt 2 ]
do
  echo '死循环了'
done

break / continue 可以控制循环退出:

for num in {1..9}
do
  if [ $num -eq 5 ]; then
    # 跳过这次循环
    continue
  elif [ $num -eq 8 ]; then
    # 结束循环
    break
  else
    echo $num
  fi
done

遍历参数的 N 种方式:

# for 循环遍历
for pos in $*
do
  # 这里最好加上 "" ,确保进行字符串比较,避免出现一些参数类型导致的问题
  if [ "$pos" -eq 'help' ]; then
    echo "$pos"
  fi
done

# while + shift 参数左移
# 判断参数个数 > 1 就继续循环
while [ $# -ge 1 ]
do
  if [ "$1" -eq 'help' ]; then
    echo "$1"
  fi
  # 将参数出队
  # shift 后可以跟整数,表示一次出队多少个参数
  # 适用于 -a xxx -b xxx 的情况
  shift
done

函数

基本结构如下:

# 定义函数, function 可省略
function funcName() {
  # a 在函数被调用时创建
  # 调用结束自动销毁
  # 局部变量可以避免对外部 shell 环境污染
  local a=2
  echo $a
}

# 执行函数
funcName

系统内置的函数库位于 /etc/init.d/functions ,定义了操作系统内置的一些函数。

脚本控制

脚本控制是为了避免用户死循环占用资源,比如递归创建子进程,最终对所有用户产生影响。

使用 ulimit -a 可以看到当前终端的使用限制,比如 CPU 限制、进程数限制、锁限制等等。

fork 炸弹

fork 炸弹指不断递归创建子进程,最终将资源占满的恶意代码,例如:

# 定义了一个 func , func 会递归在后台启动 func
# 最终后台被大量进程占满,导致资源不足
func() { func | func& }; func

精简命名后,可以更短,例如:

.(){.|.&};.

一般通过资源监控可以发现此类程序,并通过 renice 调整优先级,或者直接 kill -9 终止。

信号

信号广泛用于进程间通信,是 Linux 的底层通信机制之一。

  • kill 可以给指定进程发送信号,默认发送 15 号信号。
  • ctrl c 会发送 2 号信号。
  • kill -9 会强制终止,不可捕获,不可阻塞。

进程捕获信号示例:

# trap 捕获后的操作 捕获的信号码
trap "echo signal" 15

# ctrl + c 也是可以被捕获的,并修改为不被终止,可以用于备份脚本等后台进程
trap "echo '不可被 ctrl + c 制裁'" 2

计划任务

计划任务分为一次性与周期性

一次性计划任务

一次性计划任务只会执行一次,使用 at ${date} 创建计划任务,输入脚本后按 ctrl + d 完成创建。

使用 atq: 查询所有一次性计划任务。

由于定时任务执行时 shell 环境不可测,推荐不依赖环境写法,比如使用绝对路径。

定时任务执行的时候不依赖终端,所以也没有标准输出,获取输出需要依靠 > 重定向。

周期性定时任务

周期性定时任务每隔一段时间执行一次, Linux 提供了 cron 实现定时任务,最小单位为 1 分钟,如果使用秒级定时器、毫秒级定时器,需要使用第三方依赖包。

  • crontab -e: 创建周期性定时任务
  • crontab -l: 列出所有周期性定时任务

如下是一个配置自动清理缓存的脚本:

# 分钟   小时   天   月   星期   需要执行的命令
# * 匹配任意时间
# */5 即为每五分钟运行一次
# 如果需要指定时间,直接写时间数字即可
# 需要指定时间段可以使用 1-5
# `/usr/bin/sync`: 会将内存中的缓存持久化(如果需要的话),避免清理缓存时导致数据丢失
# `echo 1 > /proc/sys/vm/drop_caches`: 清理内存中的缓存
*/5 * * * * /usr/bin/sync; echo 1 > /proc/sys/vm/drop_caches

# 每周一的凌晨 3:30 定时执行备份任务
30 3 * * 1 /root/backup.sh

延时计划任务

anacon 机制,即延时计划任务,当到达定时任务执行时间,但是系统不可用(如关机等),可以在系统可用后立即执行

anacron 通过检查 /var/spool/anacron/ 目录下的时间戳文件,判断任务是否在预定周期内执行过。若因系统关机导致任务未执行,会在下次开机后自动补执行

通过编辑 /etc/anacrontab 可以创建延时计划任务,内置的延时定时任务如下:

# 全局参数
RANDOM_DELAY=45           # 最大随机延迟时间(分钟)
START_HOURS_RANGE=3-22    # 允许执行的时间段(小时范围)

# 任务定义格式
# 任务周期   基础延迟时间   任务唯一标识符   实际需要执行的任务或者脚本命令
period_in_days delay_in_minutes job_identifier command
1       5       cron.daily      nice run-parts /etc/cron.daily
7       25      cron.weekly     nice run-parts /etc/cron.weekly
@monthly 45     cron.monthly    nice run-parts /etc/cron.monthly

每个任务可以设置基础延迟时间与随机延迟范围

  • flock -xn "/tmp/task.lock" -c "/root/task.sh"
    • flock: 为指定任务创建锁文件,只要锁文件存在,再次使用该锁文件执行任务就会被限制。
    • -x: 独占锁。只要被锁定了,不能再重复运行其他任务。
    • -s: 共享锁。允许多个进程一起持有锁,但是只能读,不能写。 如果已经被独占锁锁住,即使共享锁也无法读。
    • -n: 非阻塞模式。如果被锁住了就直接结束,而不是持续等待锁释放。
    • -w: 阻塞模式最长等待多少秒。
    • "/tmp/task.lock": 锁文件存放位置
    • -c "/root/task.sh": 配置需要执行的任务/命令

搜索

正则

基本与 JS 中的正则相同

内容查找

grep 可以对文件内容进行查找,基本命令格式为: grep ${keyword} ${待查找的内容或者文件名} 。 例如: grep html ./index.html

${keyword} 可以使用正则表达式,例如 grep "<ht.*" ./index.html

使用正则表达式时,最好将正则表达式使用 " 包裹,避免 shell 解析时识别将部分元字符解析为其他 shell 关键字。

文件查找

find 可以对文件进行查找,例如 find ${需要查找的文件名} 。 这将在当前目录下查找指定文件名,这里的文件名需要精确匹配,支持 shell 通配符,不支持正则表达式。

比较常见的基本命令格式为: find ${需要查找的目录} ${查找条件} 。 例如:

  • find /var/log -name nginx: 将会查找 /var/log 目录下所有文件名包含 nginx 的字符的任意文件。
  • find /var/log -regex .*nginx.*: 将会查找 /var/log 目录下所有文件名包含 nginx 的字符的任意文件,使用正则匹配查找。
  • find /var/log -type f -regex .*nginx.*: 将会查找 /var/log 目录下所有文件名包含 nginx 的字符的普通文件,使用正则匹配查找。
  • find /var/log -atime 8: atime 对应文件被访问的时间,这里查找的是 8 小时内被访问过的文件。可以使用 stat ${file} 查看 atime
  • find /var/log -ctime 8: ctime 对应 inode 的更新时间,例如文件创建、数据块更新、权限变更等。可以使用 stat ${file} 查看 ctime
  • find /var/log -mtime 8: mtime 在内容被修改时更新,这里查找的事 8 小时内被更新过内容的文件。可以使用 stat ${file} 查看 mtime
  • find /var/log -user root: 查找归属于 root 用户的文件。
  • find /var/log -uid 0: 查找归属于 root 用户的文件。
  • find *log* -exec ${command}: 查找所有包含 log 的文件,并对这些文件执行 ${command},例如 find *log* -exec rm -v {} \;
    • {}: 在 ${command} 中,可以使用 {} 引用遍历出来的每个文件
    • \;: 表示传给 -exec 的值传完了,必须要加

文本操作

cut 可以将文件、内容进行切分,并按照需要返回对应的内容块。

常见用法如: cut -d ${separator} -f 1 ${file} 。 这个命令将指定内容按照 ${separator} 分割,并获取其中的第 1 个部分。

传入文件后,将按照换行符,每一行文件都进行切分。

例如: cut -d ":" -f 7 /etc/passwd: 查询所有用户访问的终端。

sort 可以对内容进行排序,例如 cut -d ":" -f 7 /etc/passwd | sort 会对所有终端进行排序, sort -r 会反向排序

uniq -c 将会对文本进行统计,合并相邻的相同文本,并计算次数。

所以统计所有用户使用的终端,并按照使用次数排序,就是 cut -d ":" -f 7 /etc/passwd | sort | uniq -c | sort -r

先分割所有的终端,并将终端排序,确保相同终端一定相邻,再合并终端名,并计次,最终倒序输出,确保次数高的在前面。

行文本编辑器

  • vim 是全文本编辑器,主要用于编辑文件,一般是交互式的。
  • sedawk 是行文本编辑器,逐行处理文本,一般是非交互式的。

sed

sed 一般用于文本替换,例如 sed '${寻址空间}s/${查找文本}/${替换文本}/' ${file1} [ ${file2} ] ,寻址空间和查找文本可以是正则表达式,寻址空间用于对待匹配文本进行过滤,可以指定行号或者指定满足正则匹配的行。

sed 的基本工作方式是:

  • 将文件以行为单位读取到内存(模式空间)
  • 使用 sed 的每个脚本对该行进行操作
  • 处理完成后输出该行

文本替换

多个替换可以使用 -e 组合,例如 sed -e 's/{old1}/${new1}/' -e 's/{old2}/${new2}/' ${file}

需要将替换的内容写回文件可以使用 -i ,例如: sed -i 's/{old1}/${new1}/' ${file}

需要使用扩展正则表达式可以使用 -r ,例如: sed -r 's/{扩展正则}/${new1}/' ${file} 使用推荐直接开 -r ,如捕获组之类的功能都属于扩展功能。

# 创建测试文件,只有一行,内容为 'a a a'
echo a a a > testFile

# 输出 `aa a a` ,替换每行仅发生一次
sed 's/a/aa/' testFile

# 输出 `aa aa aa` ,替换每行仅发生一次,但是每次都替换全部匹配项
sed 's/a/aa/g' testFile

# -n 可以阻止 sed 默认的输出行为
# p 表示将匹配的行,替换后输出
# 这条语句将只输出替换成功的行
sed -n 's/a/aa/p' testFile

# `w ${file}` 将替换成功的行写入新文件
sed -n 's/a/aa/w wFile' testFile

# 当替换 / 时,可以将原本的分隔符替换成别的字符,类似 JS 正则
sed 's!/!aa!' testFile

# 输出 `bb a a` , -e 参数依次执行,第二个 -e 可以拿到第一个 -e 返回的 aa
sed -e 's/a/aa/' -e 's/aa/bb/' testFile

# 输出 `bb a a` , 等价于上一条,简略写法
sed 's/a/aa/;s/aa/bb/' testFile

# 没有输出,直接将 `bb a a` 写入源文件中对应行
sed -i 's/a/aa/;s/aa/bb/' testFile

# 另存为一个新文件,内容为 `bb bb a`
sed 's/a/aa/;s/aa/bb/' > otherTestFile

# 删除前面三个字符,输出 `a a`
sed 's/...//' testFile
# 输出 'bbbb a a'
sed -r 's/(bb)/\1\1/' testFile

# 保存替换模式到文件
echo 's/...//' > sedscript
# 从文件中加载替换模式
sed -f sedscript filename

寻址空间

寻址空间的使用示例如下:

# 创建一个多行的文件
echo 1 2 3 > testFile
echo a b c >> testFile

# 匹配第一行的 a 并替换为 b
# 这里不会发生替换,因为第一行没有 a
sed '1s/a/b/' testFile

# 匹配第 1 行到第 2 行的 a 并替换为 b
# 这里会发生替换,因为第 2 行有 a
sed '1,2s/a/b/' testFile

# 匹配第 1 行到最后 1 行的 a 并替换为 b
# 这里会发生替换,因为第 2 行有 a
sed '1,$s/a/b/' testFile

# 匹配行内存在 \w 也就是字母的行,并执行替换
sed '/\w/s/a/b/' testFile

# 匹配行内存在 \w 也就是字母的行,并执行多次替换
sed '/\w/{s/a/b/;s/b/aa/}' testFile

其他功能

除了替换之外, sed 还支持其他能力:

  • 删除行:
    • sed '/a/d' testFile: 使用寻址空间匹配含有 'a' 的行,并将其删除,并忽略其后跟着的命令
    • sed '/a/d;s/a/b/' testFile: 使用寻址空间匹配含有 'a' 的行,并将其删除,后续的替换命令被忽略,不会执行
  • 追加行:
    • sed '/a/i xxx' testFile: 使用寻址空间匹配含有 'a' 的行,并在其上一行插入 'xxx'
    • sed '/a/a xxx' testFile: 使用寻址空间匹配含有 'a' 的行,并在其下一行插入 'xxx'
  • 修改匹配行:
    • sed '/a/c xxx' testFile: 使用寻址空间匹配含有 'a' 的行,将这一行修改为 'xxx'
  • 读写文件:
    • sed '/a/r templateFile' testFile: 使用寻址空间匹配含有 'a' 的行,将这一行的内容修改为 templateFile 的文件内容
    • sed -n 's/a/aa/w wFile' testFile: 替换后的文件写入指定文件,可用于多文件合并
  • 打印:
    • sed '/a/=' testFile: 使用寻址空间匹配含有 'a' 的行,并在其上一行插入该行对应的行号,一般要配合 -n
    • sed '/a/p' testFile: 使用寻址空间匹配含有 'a' 的行,并输出该行,一般要配合 -n
  • 提前退出:
    • sed '10q' testFile: 只打印前十行,然后直接退出,性能更高
  • 多行匹配:
    • sed 'N;s/\n//' testFile: 将下一行文本一起读入,然后将换行符替换为空字符串
    • P: 打印
    • D: 删除

多行匹配实例

多行匹配实例:

# 创建一个多行文本,两个 hello bash 拆分成 3 行
cat > testFile << EOF
hell
o bash hel
lo bash
EOF

# 1. 读入 1-2 行
# 2. 删除 \n
# 3. 匹配 `\s?hello bash` 并替换为 `hello zsh`
# 4. 打印替换结果
# 5. 将输出的内容删除掉
# 6. 读入第 3 行,并与剩余字符串合并,重新执行 2
# 最终输出:
# hello zsh
# hello zsh
sed -r 'N;s/\n//;s/\s?hello bash/hello zsh\n/;P;D' testFile

保持空间

保持空间可以保持处理中的文本,以便下次使用,而不需要一直 P;D

保持空间默认会有一行空白行,也就是 \n

  • hH 将模式空间内容存放到保持空间,小写是覆盖模式,大写是追加模式
  • gG 将保持空间内容取出到模式空间,小写是覆盖模式,大写是追加模式
  • × 交换模式空间和保持空间内容
# 需求: 将 /etc/passwd 前 5 行倒序输出

# 命令写法如下,可以用 tac 替换 sort -r
head -5 /etc/passwd | cat -n | sort -r

# 使用 sed 处理命令如下
# 由于 sed 每次只读一行,之前的行会被丢弃,所以需要利用交换模式,每次读取前,先从保持空间获取之前读过的内容。
# 处理逻辑如下:
# 1. 使用 `1h` 将第一行放入保持空间,覆盖掉原来的空白行
# 2. 使用 `1!G` 表示除了第一行,每次模式空间读取新行后,都从保持空间获取历史值,并追加到模式空间中,也就是逆序排列
# 3. 使用 `$!x` 表示除了最后一行,否则读取后都将模式空间与保持空间内容交换
# 4. 使用 `$p` 表示只有最后一行才会输出
head -5 /etc/passwd | cat -n | sed -n '1h;1!G;$!x;$p'

# 另一套处理思路为追加模式,每次都将内容追加进保持空间,而不需要交换
# 处理逻辑如下:
# 1. 使用 `1!G` 表示除了第一行(此时保持空间有一行空白行),其他行读取新行后,都从保持空间获取历史值,并追加到模式空间中,也就是逆序排列
# 2. 使用 `h` 将结果保存到保持空间中
# 3. 使用 `$p` 表示只有最后一行才会输出
head -5 /etc/passwd | cat -n | sed -n '1!G;h;$p'

awk

awk 一般用于对文本内容进行统计,并按照格式输出,通常要求文本具备一定的规范性,如果不规范的话可以先用 sed 处理后再统计,例如: awk -F ':' '{print $NF}' /etc/passwd 将统计 /etc/passwd 中定义的所有终端

名词解释

awk 中常用名词:

  • 记录: 每一行就是一项记录
  • 字段: 使用分隔符分隔开的内容,就是字段。默认分隔符为 空格与制表符。 可以使用 -F 指定字段分隔符: awk -F '${新的分隔符,支持正则}'
  • 主输入循环: { } 内的内容,其中的代码会对每个记录生效
  • 开始语句: BEGIN { } 内的内容,其中的代码会在主循环开始之前执行一次, 大小写敏感
  • 结束语句: END { } 内的内容,其中的代码会在主循环开始执行完成后执行一次, 大小写敏感
# 输出安装的内核版本
# 通过 /^menu/ 过滤行,只取 menu 开头的行给 awk 处理
awk -F "'" '/^menu/{ print $2 }' /boot/grub2/grub.cfg

# 输出安装的内核版本,并添加序号
# x 为变量,未定义会初始化为 0
# x++ 会在每次循环时自增
awk -F "'" '/^menu/{ print x++,$2 }' /boot/grub2/grub.cfg

表达式

  • 赋值操作符:
    • var1 = 'name'
    • var2 = 'hello' 'world', 自动连接多个字符,变成 'helloworld'
    • var3 = $1, 分隔符分割后,切割出来的第一个字段
    • num++
    • num--
    • num += 1
    • num -= 1
    • num *= 1
    • num /= 1
    • num %= 1
    • num ^= 1
  • 算数操作符:
    • +
    • -
    • *
    • /
    • %
    • ^
  • 系统变量: awk 相当于一门编程语言,也有自己的系统变量,就像 NodeJSprocess 一样
    • FS: 输入的字段分隔符。输入时会按照 FS 对记录进行分割。
      • awk 'BEGIN{FS=":"}{print $NF}' /etc/passwd: 统计 /etc/passwd 中定义的所有终端
    • OFS: 输出的字段分隔符。输出时会按照 FS 对字段进行拼接。
      • awk 'BEGIN{FS=":";OFS="---"}{print $1,$NF}' /etc/passwd: 统计 /etc/passwd 中定义的所有用户与终端,用户与终端使用 --- 连接
    • RS: 记录分隔符,默认为换行符
    • NR: 行数。多个文件时,会持续累计。
      • awk '{print NR,$0}' /etc/passwd /etc/passwd
    • FNR: 行数。多个文件时,新文件第一行重置为 1
      • awk '{print FNR,$0}' /etc/passwd /etc/passwd
    • NF: 字段数量。直接输出对应字段数量,取最后一个字段通常使用 $NF
      • awk 'BEGIN{FS=":"}{print $NF,NF}' /etc/passwd: 统计 /etc/passwd 中定义的所有终端,并在后面跟上字段数量,也就是 7
  • 关系操作符
    • >
    • <
    • <=
    • >=
    • ==
    • !=
    • ~: 匹配正则表达式
    • !~: 不匹配正则表达式
  • 布尔操作符:
    • &&
    • ||
    • !

判断和循环

awk 中的判断和循环,与 JS 语言风格是一致的,一模一样:

  • if
  • while
  • do-while
  • for

数组

awk 的数组类似 JS ,数组下标可以是字符串,没有数组方法的概念。

# 初始化文件
echo user1 1 1 1 > test.txt
echo user2 2 2 2 >> test.txt

# 遍历文件行,输出每行的数字之和,即:
# user1 3
# user2 6
awk '{ sum = 0; for (col = 2; col <= NF; col++) { sum += $col; } list[$1] = sum } END { for (user in list) { print user, list[user] } }' test.txt

# awk 同样可以使用 -f 加载代码块,并且可以读取 awk 的命令行参数
# ARGC 是参数数量
# ARGV 是参数值数组
cat > args.awk << EOF
BEGIN {
  for (x = 0; x < ARGC; x++) {
    print x, ARGV[x]
  }
}
EOF

# 输出结果如下:
# 0 awk
# 1 111
# 2 222
# 3 333
awk -f args.awk 111 222 333

函数

awk 的函数分为 3 类:

  • 算数函数: 处理数字的。可以通过 man awk 查看所有支持的函数。
    • sin(): 正弦函数
    • cos(): 余弦函数
    • int(): 取整
    • rand(): 伪随机数, 0-1 之间,每次取的值都一样
    • srand(): 安全的随机数, 0-1 之间,每次取的值都不一样
  • 字符串函数: 处理字符串的。可以通过 man awk 查看所有支持的函数。
    • sub(old, new, str): 替换第一个匹配项 old 支持字符串或者正则表达式
    • gsub(old, new, str): 全局替换, old 支持字符串或者正则表达式
    • index(str, subStr): 查找子串在父串中出现的位置
    • length(str): 返回字符串长度
    • match(str, regexp, arr): 正则匹配, arr 可省略,用于存储匹配分组的数组,如果需要捕获自组则需要传入
    • split(str, arr, sep): 将 strsep 分割,分割后存入 arr
    • substr(str, pos, len): 截取子字符串。 从 pos 开始截取,取 len 个字符, pos1 开始
  • 自定义函数: 自定义的处理项。跟 JS 定义函数一样,但是必须定义到 BEGIN{}{}END{} 外面。

防火墙

分类

按照运行环境分为:

  • 软件防火墙: 主要处理数据包过滤、地址转换等
  • 硬件防火墙: 主要面向 DDos 等网络攻击场景,也能兼顾处理一部分包过滤,

按照服务的网络层分为:

  • 包过滤防火墙: 从传输层控制数据包过滤、地址转换等
  • 应用层防火墙: 应用层能获取更多的信息,比如按登录用户过滤

CentOS 6 默认使用 iptables ( 底层为 netfilter ) CentOS 7 默认使用 firewallD ( 会转换成 iptables 处理,简化配置 )

iptabls

名词解释

  • 规则表: 按照不同的操作类型进行规则配置,比如 filter 表、 NAT 表、 mangle 表、 raw 表,规则表内
    • filter 表: 数据包过滤
    • nat 表: 地址转换
    • mangle 表: 对数据包进行修改
    • raw 表: iptables 默认开启对数据包的连接追踪,此表配置哪些数据包不追踪。
    • security 表: 此表用于强制访问控制 (MAC) 网络规则,例如由 SECMARK 和 CONNSECMARK 目标启用的规则,主要用于 SELinux ,不常用。
  • 规则链: 按照网络传输链路过程进行规则配置,比如 INPUTOUTPUTFORWARDPREROUTINGPOSTROUTING 。 更具体的说,规则链是按照 netfilter 不同 hook 监听来聚合规则的,比如都监听 INPUT ,所以规则链也是支持用户自定义的。

基本语法

iptables 常用语法为 iptables -t ${规则表} ${command} ${规则链} ${规则}

  • -t ${规则表}: 针对哪个规则表进行操作,如果没有指定则默认为 filter 表
  • ${command}: 选择需要执行的命令,例如查看规则、新增规则、删除规则、修改规则
    • -A / --append: 添加规则,追加到规则表末尾
    • -C / --check: 检查是否匹配规则
    • -D / --delete: 删除规则/规则链
    • -I / --insert: 在规则链的指定位置插入一个或多个规则
    • -R / --replace: 替换指定的规则
    • -P / --policy: 配置规则链的默认策略
    • -L / --list: 列出规则,如果没有指定规则规则链,将列出表中所有的规则
    • -N / --new-chain: 创建自定义的规则链
    • -X / --delete-chain: 删除自定义的规则链
    • -E / --rename-chain: 重命名自定义的规则链
    • -n: 不根据 IP 反向索引主机名,直接展示 IP
    • -v: 查看详细信息
  • ${规则链}: 在传输链路的哪个位置进行操作
  • ${规则}: 具体进行什么操作,比如允许通过、不允许通过
    • -4 / --ipv4: 规则用于 IPv4
    • -6 / --ipv6: 规则用于 IPv6
    • -p / --protocol: 规则针对指定通信协议生效
    • --dport: 规则针对指定端口生效
    • -s / --source: 规则针对指定源地址,支持 ip[/mask]
    • -d / --destination: 规则针对指定目的地址,支持 ip[/mask]
    • -i / --in-interface: 规则针对指定接收数据包的网卡
    • -o / --out-interface: 规则针对指定发送数据包的网卡
    • -m / --match: 加载扩展模块,用于复杂匹配,比较少用
    • -j / --jump: 用于指定数据包匹配规则后的 ​最终处理动作​​,或跳转到一个 ​​用户自定义链​ ​继续匹配规则,匹配完毕后回到本链继续执行后续规则。
      • -j ACCEPT: 允许数据包通过
      • -j DROP: 丢弃数据包。不会发送任何响应,客户端会持续等待直到超时,同时也可以避免暴露自身存在,比如拒绝一些 IP 、 端口探测。
      • -j REJECT: 拒绝数据包通过。 返回错误响应。
      • -j my_chain: 跳转到自定义链
    • -g / --goto: 仅用于跳转到用户自定义链​​,且 跳转后​​不会返回原链

常见命令

  • iptables -vnL: 查看所有的 filter 表规则,不反向索引主机名,展示详细信息
  • iptables -t nat -L: 查看所有的 nat 表规则
  • iptables -t filter -A INPUT -s 1.1.1.1 -j ACCEPT
    • -t filter: 指定添加到 filter 表,即配置包过滤功能
    • -A INPUT: 配置 INPUT 链,即接受数据时触发规则
    • -s 1.1.1.1: 指定源地址,当源地址为 1.1.1.1 时,触发规则
    • -j ACCEPT: 规则设置为允许通过
  • iptables -P INPUT DROP: 配置 INPUT 链,默认阻止所有访问,仅配置了 ACCEPT 的规则能通过

iptables 匹配的时候会从链头开始匹配,先匹配到 -j ACCEPT 就直接返回了,因为 -j 指定了 最终处理动作 ,所以后续的 DROP 被忽略了。

iptables -t filter -A INPUT -s 1.1.1.1 -j ACCEPT
iptables -t filter -A INPUT -s 1.1.1.1 -j DROP

NAT 表

  • iptables -t nat -A PREROUTING -i eth0 -d 1.2.3.4 -p tcp --dport 80 -j DNAT --to-destination 192.168.0.111: 目的地址映射。可以将发送给外网的请求映射到内网网关,进行数据采集、转发等工作。
    • -t nat: 配置 nat 表
    • -A PREROUTING: 注册路由处理之前的转发规则
    • -i eth0: 配置监听的网卡
    • -d 1.2.3.4: 配置监听目的 IP 为 1.2.3.4 的数据包
    • -p tcp: 配置监听 TCP 数据包
    • --dport 80: 配置监听发送到 80 端口的数据包
    • -j DNAT: 进行地址转换
    • --to-destination 192.168.0.111: 转换后的目的 IP
  • iptables -t nat -A POSTROUTING -i eth1 -s 192.168.0.111/24 -p tcp --dport 80 -j SNAT --to-source 1.2.3.4: 源地址映射。可以将内网转发的请求映射伪装成外网发送的。
    • -s 192.168.0.111/24: 指定数据包的源地址字段
    • -j SNAT: 进行源地址转换
    • --to-source 1.2.3.4: 转换后的源地址

持久化规则

iptables 通过命令行添加规则的时候,只会在内存中添加,不会持久化处理,如果需要持久化,可以使用 service iptables save 将当前配置持久化到 /etc/sysconfig/iptables ,如果是 CentOS 7 需要手动安装 iptables-services

service iptables save 会调用 iptables-save 获取内存中的规则配置,并保存到临时文件中,再将临时文件重命名为 /etc/sysconfig/iptables 覆盖原配置

firewallD

firewallD 引入了 zone 的概念,将规则链的配置项,比如网段、可访问的服务、网口等信息进行聚合。

在管理时,以 zone 为单位,不需要关注处理的是哪个规则表、哪个规则链,只需要针对不同的 zone 进行管理即可。

ssh

sshd

ssh 是安全远程命令行服务。

早期使用的是 telnet 进行远程命令行,但是由于 telnet 是明文传输,会导致管理员密码泄露,所以才替换为 ssh 。

主要的配置文件在 /etc/ssh/sshd_config ,需要关注的配置有:

  • Port 22: sshd 监听的端口,默认为 22 ,有些服务器为了安全起见会更改默认端口
  • PermitRootLogin yes: 是否允许 root 用户从 ssh 登录,默认允许
  • AuthorizedKeysFile: sshd 登录的密钥文件放在哪里

配置在修改后需要重启 sshd 才会生效,例如 systemctl restart sshd.service

scp

基于 ssh 协议,扩展出来了文件传输功能,也就是 scp , scp 传输时,命令与文件是使用同一链路传输,这与 FTP 有所区别。

  • scp ${localFile} ${user}@${server}:${path}: 文件上传。需要交互式的输入密码,或者使用内置密钥
  • scp -i ${keyPath} ${localFile} ${user}@${server}:${path}: 文件上传。指定登录密钥
  • scp ${user}@${server}:${path} ${localFile}: 文件下载。需要交互式的输入密码,或者使用内置密钥
  • scp -r ${localFile} ${user}@${server}:${path}: 目录上传。需要交互式的输入密码,或者使用内置密钥

ftp

简介

FTP 需要使用不同链路传输命令与文件,需要占用至少两个端口:

  • 命令传输: 一般使用 20/21 端口进行,比如建立链接,协商文件传输链路端口等。
  • 文件传输: 一般使用 > 1024 的端口号,需要通过命令传输将端口号传递给客户端。

命令传输时,分为主动模式与被动模式:

  • 主动模式: 建立 FTP 连接后,服务端主动与客户端发送消息,建立文件传输链路
  • 被动模式: 建立 FTP 连接后,服务端被动响应客户端消息,建立文件传输链路

可以使用 yum install vsftpd ftp 安装 ftp 功能,并在 systemctl start vsftpd.service 启用 vsftpd 服务。

其中 vsftpd 一般作为服务端, ftp 是客户端。

在安装完成后,可以使用 ftp ${server} 进行 ftp 连接,例如 ftp localhost

默认可以使用 ftp 账号登录,密码为空。也支持使用本地账号登录,密码为用户密码。

在连接上 ftp 后,可以使用 !${command} 执行命令,例如 !ls

通过 put ${file} 可以将本地的同名文件上传到服务端,文件名保持不变。

通过 get ${file} 可以将服务端的同名文件下载到本地,文件名保持不变。

vsftpd

常见配置文件:

  • /etc/vsftpd/vsftpd.conf: 主配置文件
    • anonymous_enable=YES: 是否允许匿名用户登录,也就是 ftp 用户, YES / NO 必须大写
    • local_enable=YES: 是否允许使用本地账号登录。
    • write_enable=YES: 是否允许写文件,也就是写入服务端。
    • connect_from_port_20=YES: 是否开启主动传输模式,主动传输一般是 20 端口。
    • userlist_enable=NO: 是否开启用户黑白名单功能。
    • userlist_deny=YES: 为 YES 时, userlist 代表黑名单;为 NO 代表白名单。
  • /etc/vsftpd/ftpusers: 记录禁止登录的用户,一般是权限较高的用户,避免文件泄露。
  • /etc/vsftpd/user_list: 用户黑白名单配置

虚拟用户

使用本地用户登录 ftp 容易泄露信息,而且在登录时的验证阶段会比较慢,所以往往 ftp 会启用虚拟用户的功能,使用 ftp 专用的账号进行登录。

  • guest_enable=YES: 开启虚拟用户功能
  • guest_username=vuser: 配置虚拟用户的对应的本地用户,所有 ftp 账号都会使用该账号进行操作,禁止配置为 root 用户
  • user_config_dir=/etc/vsftpd/vuserconfig: 针对每个虚拟用户单独进行权限控制。可以通过此配置修改配置文件存放地址,使用用户名对应配置文件名。
  • allow_writeable_chroot=YES: 允许虚拟用户上传文件
  • pam_service_name=vsftpd.vuser: 修改验证模块名称,使用自定义的验证模块

配置虚拟用户的步骤如下:

# 初始化 ftp 虚拟账号
# 单行为用户名,双行为密码
cat > vuser.temp << EOF
user1
password1
user2
password2
EOF

# 转换为 ftp 数据库文件
db_load -T -t hash -f ./vuser.temp /etc/vsftpd/vuser.db
# 需要更改为 600 权限
chmod 600 /etc/vsftpd/vuser.db

# 修改验证模块,支持使用 FTP 虚拟账号登录
# 需要配置权限验证与登录验证两条记录
# 由于没有保留之前的用户,这种方式验证只能进行虚拟账号登录,无法使用本地账号登录
# 如果需要兼容本地用户,可以在原来的 /etc/pam.d/vsftpd 上修改
cat > /etc/pam.d/vsftpd.vuser << EOF
auth sufficient /lib64/security/pam_userdb.so db=/etc/vsftpd/vuser
account sufficient /lib64/security/pam_userdb.so db=/etc/vsftpd/vuser
EOF

# 初始化用户权限配置目录
mkdir -p /etc/vsftpd/vuserconfig

# 为 user1 配置权限
# local_root: 登录后打开的目录
# write_enable: 是否可写
# anon_umask: 文件权限掩码,用户上传的文件会被设置为 `0777 - anon_umask` 的权限
# anon_world_readable_only: 是否为只读
# anon_upload_enable: 是否可上传
# anon_mkdir_write_enable: 是否允许新建目录
# anon_other_write_enable: 扩展写权限,是否允许支持删除、重命名、覆盖文件等
# download_enable: 是否允许下载
cat > /etc/vsftpd/vuserconfig/user1 << EOF
local_root=/data/ftp
write_enable=YES
anon_umask=022
anon_world_readable_only=NO
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES
download_enable=YES
EOF

用户态、内核态

用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括 CPU 资源、存储资源、 I/O 资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口,即系统调用。

简单来说:

  • 内核态:运行在内核空间的进程的状态
  • 用户态:运行在用户空间的进程的状态

区分两种状态的原因在于: 部分 CPU 指令有较大的安全风险,只能在内核中使用,即运行在内核空间,处于内核态; 部分风险不高的指令可以直接暴露给用户,即运行在用户空间,即处于内核态。