5

Linux 实操

·30 分钟

1. chmod 600 是什么意思?

Linux 的文件权限分三组:属主、属组、其他人,每组三位分别是读(r=4)、写(w=2)、执行(x=1)。三位加起来就是一个八进制数字,600 拆开是 6(rw-) 0(---) 0(---),意思是属主可读可写,属组和其他用户完全没有权限。

含密钥的配置文件必须设成 600,比如 alertmanager.yml 里带告警通知的密钥、SSH 私钥,因为同一台机器上的其他用户能读到 644 的文件。SSH 尤其严格,私钥权限只要超过 600 就直接拒绝使用并报 UNPROTECTED PRIVATE KEY FILE——这不是它挑剔,而是私钥一旦被别人读走,整个认证机制就失去意义了。

几个常见值对照着记:644 是普通文件的默认(自己可写、别人只读),755 是目录和可执行文件的默认(别人能进入和执行但不能改),600 是私密文件,700 是私密目录。

目录上这三个权限的含义跟文件不一样:r 是能列出目录内容,w 是能在里面新建和删除文件,x 是能进入这个目录以及访问里面的文件。所以一个目录只有 r 没有 x,ls 能看到文件名却读不了任何一个文件。

改权限用 chmod,改属主属组用 chown。除了数字写法还有符号写法,chmod u+x script.sh 只给属主加执行权限,不用心算。

2. ps、top、kill 怎么用?kill -9 和 kill -HUP 有什么区别?

ps aux 看当前所有进程的快照,ps -ef | grep xxx 用来找特定进程;top(或更好用的 htop)是动态刷新的,按 CPU 或内存排序看谁在吃资源。两者的区别是快照和实时——排查"现在为什么这么卡"用 top,脚本里拿 PID 用 ps。

kill 这个名字有误导性,它不是"杀死",而是"发送信号",默认发的是 SIGTERM。

命令信号效果
kill PIDSIGTERM (15)请求进程退出。进程可以捕获它,先关连接、写完日志、清理临时文件再退出
kill -9 PIDSIGKILL (9)由内核直接终止,进程无法捕获也无法忽略,没有任何清理机会
kill -HUP PIDSIGHUP (1)按惯例表示"重新加载配置",多数服务收到后重读配置文件而不重启进程

日常应该先用 kill 发 SIGTERM,给进程一个体面退出的机会,只有它不响应时才升级到 kill -9。直接上 -9 的风险是数据可能还在缓冲区没落盘、锁文件和临时文件不会被清理,下次启动可能报"已在运行"。

有一种情况 kill -9 也杀不掉:进程处于 D 状态(不可中断睡眠),通常是卡在磁盘或网络文件系统的 IO 上。信号要等进程回到用户态才能被处理,而它压根没回来。这时只能等 IO 超时或者重启机器。

3. su 和 sudo 有什么区别?

su 是切换用户身份,之后的所有命令都以新身份运行,直到 exit 退回来,需要输入的是目标用户的密码。sudo 是以另一个用户(默认 root)的身份执行单条命令,执行完就回到原身份,输入的是自己的密码。

差别不只是方便程度,主要在安全模型上。su root 要求所有需要提权的人都知道 root 密码,密码一旦泄露或者有人离职就得全员更换;而且切过去之后干了什么,日志里都记成 root,追溯不到具体是谁。sudo 不需要分发 root 密码,可以在 /etc/sudoers 里按用户、按命令精细授权(比如只允许运维组重启 nginx,别的一概不能碰),而且每次 sudo 都会记进日志,谁在什么时候执行了什么一清二楚。

所以生产环境的规范做法是禁用 root 直接登录,所有人用自己的账号登录、按需 sudo。

再补个细节:su usersu - user 不一样。加了横杠会加载目标用户的完整登录环境——切换 HOME、重新读 .bash_profile、重置 PATH,不加则沿用当前的环境变量。很多"用 su 切过去之后命令找不到"的问题,根源就是漏了这个横杠。

4. systemctl 的 start/stop/restart/reload 有什么区别?enable 是干什么的?

startstop 启停服务,restart 等于先停再起,进程 PID 会变,服务有一段不可用的窗口。reload 不重启进程,只让服务重新加载配置,底层通常是给进程发 SIGHUP,连接不中断。

能用 reload 就不用 restart,尤其是 Nginx 这类持有大量长连接的服务,restart 会把所有在途请求打断。但 reload 只对配置类的改动有效,改了监听端口、换了运行用户这种需要重新初始化的改动,还是得 restart。有些服务不支持 reload,systemd 提供了 reload-or-restart 兜底。

enablestart 是两件正交的事,这是最容易混的点:start 是"现在启动",enable 是"开机自启"。只 startenable,机器一重启服务就没了——新部署的服务在服务器重启后集体失联,典型原因就是这个。所以部署时应该用 systemctl enable --now xxx,一条命令把两件事都办了。

排查时常用的是 systemctl status xxx 看运行状态和最近几行日志,journalctl -u xxx -f 跟踪完整日志,journalctl -u xxx --since "10 min ago" 看指定时间段。服务起不来时先看 status 里的 Active 状态和退出码,再翻 journalctl 里的具体报错,基本就能定位。

5. Shell 的管道和重定向怎么用?

管道 | 把前一个命令的标准输出接到后一个命令的标准输入上,让每个命令只干一件事、串起来完成复杂任务。重定向是把输入输出接到文件上:> 覆盖写,>> 追加,< 从文件读。

关键要分清三个标准流:stdin(0)、stdout(1)、stderr(2)。2>&1 的意思是"把标准错误重定向到标准输出当前指向的地方",所以 cmd > log 2>&1 才能把正常输出和报错都存进文件;写成 cmd 2>&1 > log 顺序反了,错误还是会打到终端——因为重定向是从左往右依次生效的,执行 2>&1 时 stdout 还指着终端。

管道默认只传 stdout,所以 cmd | grep xxx 是筛不到报错信息的,要写成 cmd 2>&1 | grep xxx。排查时这是很常见的困惑:明明屏幕上有报错,grep 却什么都没匹配到。

几个常用组合:

ps -ef | grep nginx | grep -v grep
journalctl -u prometheus --since today | grep -i error
du -sh /var/log/* | sort -h | tail -5
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head

最后一条是运维里的经典流水线——取出所有访问 IP,排序后统计每个出现多少次,再按次数倒序取前几名。uniq -c 只能合并相邻的重复行,所以前面必须先 sort,这个顺序省不掉。

另外 &&|| 是按上一条命令的退出码决定要不要执行下一条(成功才执行右边 / 失败才执行右边),; 则是不管成败都往下走。

6. SIGHUP 是什么?为什么发一个信号就能热加载?

SIGHUP 原本的含义是 hang up——终端断开时,内核会给这个终端上的前台进程组发这个信号,默认动作是终止进程。这也是为什么 SSH 一断,后台跑的命令会跟着死掉,得用 nohup 或者 screen/tmux 把它跟终端解绑。

守护进程没有关联的终端,永远不会因为终端断开而收到 SIGHUP,这个信号对它们就"空出来了"。于是形成了一个惯例:守护进程把 SIGHUP 重新定义为"重新加载配置"。这不是内核规定的语义,纯粹是约定俗成,但 Nginx、Prometheus、rsyslog、sshd 都遵守它,systemctl reload 底层往往就是在发这个信号。

热加载的实现是这样的:进程启动时注册一个 SIGHUP 的处理函数。信号到达时内核中断进程的正常执行流,转去执行这个处理函数,函数里重新读配置文件、用新配置替换掉内存里的旧配置,然后返回原来的执行点继续跑。整个过程进程没有退出,PID 不变,已经建立的连接和内存里的状态都还在,所以服务不中断。

对比几个信号的可捕获性就更清楚了:SIGTERM 和 SIGHUP 都能被捕获,所以能自定义处理逻辑;SIGKILL 和 SIGSTOP 内核禁止捕获,这是为了保证系统总有办法强制干预一个失控的进程。热加载只能建立在可捕获的信号上。

对 Prometheus 这类服务,kill -HUP 让它重读配置和告警规则,比 restart 好在不丢失内存里的状态、不断开正在抓取的目标。

7. inode 是什么?软链接和硬链接有什么区别?

文件系统把文件分成两部分存:inode 存元数据(权限、属主、大小、时间戳,以及数据块的位置),数据块存内容。文件名不在 inode 里——文件名和 inode 号的对应关系存在目录项里。也就是说,目录本质上是一张"文件名 → inode 号"的映射表。

理解了这一点,两种链接的区别就清楚了。硬链接是在目录里新增一条指向同一个 inode 的记录,两个文件名完全平等、没有主次之分,inode 里的链接计数加一;删除其中一个只是计数减一,只有减到 0,内容才真正被释放。软链接是一个独立的文件,它的内容就是目标文件的路径字符串,访问时由内核解析这个路径再去找目标。

硬链接软链接
本质同一个 inode 的另一个名字一个存着路径的新文件,有自己的 inode
跨文件系统不行,inode 号只在单个文件系统内唯一可以
链接目录不允许,会造成目录环可以
目标被删不受影响,内容还在变成断链,访问报错

运维里几乎只用软链接,因为它能跨分区、能链目录,而且 ls -l 里带箭头,一眼就能看出这是个链接。典型用法是版本切换:/opt/app/current 软链到 /opt/app/v1.2.3,发新版本时改一下链接指向就完成切换,出问题改回去就是回滚。

还有个实际的坑跟 inode 直接相关:df -h 显示磁盘还有空间,却创建不了文件,这是 inode 用完了。inode 的数量在格式化时就固定了,海量小文件(没清理的 session 文件、切得太碎的日志)会先把它耗尽,得用 df -i 才看得出来。

8. df 和 du 有什么区别?磁盘满了怎么排查?

df 从文件系统的角度统计,读的是超级块里记录的已用和空闲块数,很快,看的是整个分区的真实占用。du 从目录树的角度统计,递归遍历每个文件把大小加起来,慢,而且只能统计到它能遍历到的文件。

两者对不上是很常见的现象,最典型的原因是有进程还占着已删除的文件:文件从目录里删掉了,du 遍历不到它,但只要还有进程持有这个文件的描述符,inode 和数据块就不会释放,df 仍然算它占着空间。日志文件被 rm 掉但服务没重启,就是这个情况——用 lsof | grep deleted 能找出来,重启对应服务,或者用 > /path/to/log 清空(而不是 rm)来释放。

磁盘满了的排查顺序:先 df -h 确认是哪个分区满了;再 du -sh /* 从根目录往下逐层收窄,找到占用大户;找不到大文件就 df -i 看是不是 inode 耗尽;两者都正常但空间对不上,就查 lsof | grep deleted

日常预防比事后排查更重要:日志要配 logrotate 按大小或时间轮转并限制保留份数;监控上配磁盘使用率告警,阈值设在 80% 左右,留出处理时间。等到 100% 才发现,很多服务已经因为写不了日志或临时文件而挂了,连排查本身都会变得困难。

9. ss、netstat、curl、ping、traceroute 分别用来干嘛?

这几个命令对应网络排查的不同层次,按从底层往上的顺序用最有效率。

ping 测的是网络层通不通(走 ICMP),只能说明 IP 可达,跟目标端口上有没有服务无关。而且很多机器出于安全禁 ping,所以 ping 不通不等于网络不通。

traceroute(或者更好用的 mtr)看数据包经过了哪些路由跳,用来定位是在哪一跳断的、哪一跳延迟高,排查跨机房跨运营商的问题时有用。

ss -tlnp 看本机监听了哪些端口(-t 是 TCP、-l 是监听状态、-n 不做名称解析、-p 显示对应进程)。服务连不上时第一件事就是在服务端跑这个,确认进程到底起没起、监听在哪个地址上。监听在 127.0.0.1:9090 只有本机能连,要外部访问必须监听 0.0.0.0:9090——这是排查时最常撞见的原因之一。看当前建立的连接用 ss -tn state established

netstatss 的老版本,功能类似但在连接数多时明显更慢(它逐个读 /proc,而 ss 直接走内核的 netlink 接口),新系统上优先用 ss。

curl 是应用层的验证,能真正发一个 HTTP 请求看服务怎么响应。curl -I 只看响应头,curl -v 看完整的连接和握手过程。如果只想验证 TCP 端口通不通、不涉及应用层协议,用 nc -zv host porttelnet host port

10. 服务连不上怎么排查?抓包主要看什么?

先按层次逐段排除:服务端 ss -tlnp 确认进程确实在监听 → 服务端本机 curl 127.0.0.1:端口 确认服务本身正常 → 客户端 ping 确认网络可达 → 客户端 nc -zv 确认端口可达,这一步不通基本就是防火墙或安全组。

这套命令能定位到是哪一层不通,抓包解决的则是"为什么不通"——它是唯一能看到线上真实报文的手段。

tcpdump -i any -nn 'tcp port 9090 and host 1.2.3.4' -w cap.pcap

-nn 不解析域名和端口名,避免抓包时反查 DNS 干扰结果;-w 存成文件再用 Wireshark 打开,比在终端里看方便得多。要实时看就去掉 -w

看的核心是三次握手卡在哪一步,不同的卡点直接指向不同的故障:

只看到客户端发出 SYN,服务端那侧一个包都没收到——包在路上被丢了,通常是中间的防火墙或云安全组拦截。

服务端收到了 SYN 但回的是 RST——包到了机器,但那个端口上没有进程监听,或者被本机防火墙以 reject 方式拒绝。回 RST 说明对方主机是活的,只是不接受这个连接。

服务端收到 SYN 却完全不回,客户端不断重传 SYN——典型是防火墙以 DROP 方式静默丢弃,或者服务端的 accept 队列满了。DROP 和 REJECT 的区别在抓包上就体现为"没有任何响应"和"明确回一个 RST"。

握手成功但应用层没有数据往来——问题不在网络,转去看应用日志。

这个方法真正的价值在于,它把"连不上"这个笼统的现象切分成了客户端、中间网络、服务端三个明确的责任区间。在两端同时抓包对比,就能确定包究竟是在哪一段丢的。

11. iptables 怎么放行一个端口?安全组是什么?

iptables 按"表—链—规则"组织。最常用的是 filter 表的三条链:INPUT(进本机的包)、OUTPUT(出本机的包)、FORWARD(经本机转发的包)。规则从上往下匹配,命中就执行对应动作(ACCEPT/DROP/REJECT),全都没命中就走链的默认策略。

iptables -I INPUT -p tcp --dport 9090 -j ACCEPT

-I 是插到链的最前面,-A 是追加到最后,这个区别很关键:如果链尾已经有一条"拒绝所有"的兜底规则,用 -A 加的放行规则永远轮不到匹配。另外 iptables 改完不会自动持久化,重启就没了,要 iptables-save,或者用 firewalld、ufw 这类前端来管理。

现在 CentOS/RHEL 一般用 firewalld(firewall-cmd --add-port=9090/tcp --permanent 之后 --reload),Ubuntu 用 ufw(ufw allow 9090/tcp),底层都还是 netfilter。

安全组是云厂商在虚拟网络层面提供的防火墙,作用在实例的网卡之前,包还没进操作系统就被过滤掉了。它跟 iptables 是两层独立的关卡,两层都放行了才通——排查云服务器端口不通,绝大多数情况是安全组没开,因为它在控制台上,容易被忽略。安全组默认拒绝所有入站、放行所有出站,规则是白名单式的,而且是有状态的:放行了入站请求,对应的响应自动放行,不用再单独配出站规则。

实际排障时可以这样区分:如果在服务器本机 curl 127.0.0.1:9090 通、但外部连不上,问题就在这两层防火墙上,先看安全组(更常见),再看本机 iptables。

12. 正向代理和反向代理有什么区别?Nginx 做反代怎么配?

两者的技术动作是一样的——接收请求再转发出去,区别在于代理站在谁那一侧、为谁服务。

正向代理站在客户端这边,客户端知道自己在用代理并主动配置它,服务端不知道真实的客户端是谁。用途是访问控制和统一出口:公司里所有终端通过 Squid 上网,就能集中做白名单、缓存和审计,对外也只暴露一个出口 IP。

反向代理站在服务端这边,客户端完全不知道后面还有别的服务器,以为代理就是目标服务器本身。用途是负载均衡、TLS 终结、统一入口和隐藏内部结构。

一个最小可用的 Nginx 反代配置:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

那几个 proxy_set_header 不是可有可无的装饰。不加的话,后端拿到的 Host127.0.0.1:3000,据此生成的跳转链接和 Cookie 域名全会出错;拿到的客户端 IP 也全是代理的 IP,日志统计和限流直接失效。X-Forwarded-For 会把每一层代理经过的 IP 追加进去形成一条链,如果外层还有 CDN,取真实 IP 要注意取的是哪一段,而且不能无条件信任这个头——它可以被客户端伪造。

WebSocket 还需要额外的升级头,否则连接会被降级成普通 HTTP 请求:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

改完配置先 nginx -t 做语法检查,通过之后再 systemctl reload nginx。先检查再 reload 是必须养成的习惯——配置有错时 reload 会失败,但旧配置还在跑,服务不会中断。

13. FRP 内网穿透是怎么工作的?为什么要用反向隧道?

问题的根源是 NAT。内网机器没有公网 IP,它能主动访问外网,但外部无法主动连它——NAT 的映射表是在内网机器发起连接时才建立的,外面的包进来时找不到对应的映射,路由器不知道该转给内网的哪一台机器。

FRP 的解法是把连接方向反过来。frps 部署在有公网 IP 的服务器上,frpc 部署在内网机器上,由 frpc 主动向 frps 发起并一直保持一条长连接——这个方向是 NAT 允许的。之后外部用户访问公网服务器的某个端口,frps 就通过这条已经建好的隧道把流量转给 frpc,frpc 再转给内网的本地服务,响应沿原路返回。

关键就在于"谁先发起"。既然外面连不进来,那就让里面的人先把线接出去,然后一直挂着不断——反向隧道的本质,是用一条内网主动发起的长连接,换来一个外部可达的入口。

SSH 也能做同样的事,ssh -R 8080:localhost:3000 user@公网服务器 就是反向端口转发,把公网服务器的 8080 映射到内网的 3000。FRP 相比之下更适合长期运行:有配置文件统一管理多个服务、断线自动重连、支持 HTTP/HTTPS 按域名分流、有鉴权和加密选项。

安全上必须注意,这等于在防火墙上开了个洞,把内网服务暴露到了公网。所以要配 token 鉴权,只穿透确实需要外部访问的端口,敏感服务再加一层认证,公网侧最好套上 TLS。

14. 容器和虚拟机有什么区别?docker-compose 是什么?

虚拟机在硬件之上虚拟出完整的机器,每个虚拟机跑一套自己的操作系统内核;容器共享宿主机的内核,只是用 namespace 做资源隔离(进程、网络、文件系统各自看到独立的一份),用 cgroup 做资源限制(CPU、内存配额)。

所以容器启动是秒级的,本质就是起一个受限的进程,镜像是 MB 级;虚拟机启动要分钟级,得走完整的内核引导,镜像是 GB 级。代价是隔离性:虚拟机是硬件级隔离,容器共享内核,内核漏洞可能被用来逃逸,而且容器里跑不了跟宿主机不同的内核,Linux 宿主机上起不了 Windows 容器。

docker-compose 用一个 YAML 文件描述多个容器的组合——镜像、端口映射、数据卷、环境变量、依赖顺序、网络——然后一条 docker compose up -d 全部拉起来。它解决的是多容器应用手敲一长串 docker run 参数难以维护、难以复现的问题;配置文件进了 Git,整个环境就可以版本化。

部署时有个容易踩的坑:docker 和 systemd 都能管理服务的生命周期,混用会打架。比如用一个 systemd unit 去调 docker compose up,容器崩溃后 docker 自己的 restart 策略和 systemd 的 Restart 配置会互相干扰,或者 systemd 认为服务还活着、其实容器早退了。原则是同一个服务只交给一个管理者:要么完全用 docker 的 restart: always 交给 docker daemon 管,要么用 systemd 管一个前台运行的进程,中间不要再套一层。