周日晚上翻完这一周的聊天记录,发现光是技术问题就攒了几十个。我们是一所高校里的Linux兴趣小组,外号圈内人一般叫XYLinux Group,这周是小组正式运转的第一周,做了不少事,也踩了不少坑。我打算从这期开始,把每周值得记录的内容整理成公开的周报,既是留给后来成员的资料,也是给外面同样在折腾Linux的朋友一份参考。所以这期讲的东西不追求多高深,但保证都是真跑过、真报过错的东西。
1. 过去一周小组整体在做什么
1.1 小组的定位和第一周目标
很多校园技术社团的问题不是没人,而是聚在一起不知道干什么。XYLinux Group最开始也就是几个平时在宿舍折腾Linux的人,装上系统以后就开始互相安利发行版、比谁桌面配置得花哨。后来人多了才发现,光靠聊天记录沉淀不了东西,今天说的命令明天就找不到了。所以第一周我们先把目标定了下来:不搞排场、不折腾发行版攀比,只做两件事——把基础打牢,把踩过的坑记下来。
带着这个目标,小组这周的活动分成几块:新成员引导、第一次内核模块实验、常见软件源问题整理,以及一次网络抓包排查。每一项都有一个负责人,周末统一把记录交给我。说实话第一周能安排这么多已经有点超预期,因为大部分人是第一次在命令行下完成完整任务。最终基本都跑通了,但过程里暴露出来的问题也很有代表性,后面统一复盘。
1.2 新成员基础摸底
小组里不止有老手,也有几个刚接触Linux的同学。摸底的方式很简单:每人发一份清单,上面列了五个基础操作,要求在终端里完成并截图反馈。内容包括:用命令查看系统内核版本、找出一台机器的CPU信息、创建一个带空格的目录并进入、把一个文件从当前目录移动到上级目录、查看历史命令记录。这些操作看起来简单,但能很快筛出“会开终端”和“真的理解命令”之间的差距。
摸底结果比预想的要好一些,大部分人都能完成前三个,真正卡住的是“带空格的目录”。不少人在创建目录时直接输入mkdir test dir,结果生成了两个目录,然后追问才知道需要用引号或转义符处理空格。我们当时没有直接给答案,而是让每个人先跑一遍man mkdir和info bash,再去查引号的用法。后来这个点也成了第一期入门培训的重点。
2. 内核模块编译的第一课
2.1 为什么第一课选内核模块
小组技术分享第一课选了“内核模块”,不是因为这东西日常用得多,而是因为它能一次性带出好几个重要的基本功:内核版本、工具链、Makefile、模块加载卸载、日志查看。平时写C程序都在用户态,系统调用上面的东西很少碰,内核模块能让你直接看到内核态和用户态的区别。最友好的一点是,编译失败并不会把系统搞坏,最多就是insmod失败,非常适合作为新手的第一道硬菜。
另外,内核模块编译的过程本身就能暴露很多环境问题。比如头文件没装、内核版本和源代码不一致、Makefile缩进错了、权限不够、Secure Boot拦截加载,这些问题在真实项目里都会遇到,与其以后手忙脚乱,不如一开始就踩一遍。
2.2 环境准备和完整编译流程
第一步是确认内核版本,我们用的命令是:
uname -rDebian/Ubuntu系需要安装构建工具和对应的内核头文件包:
sudo apt update sudo apt install build-essential linux-headers-$(uname -r)如果提示找不到linux-headers-$(uname -r),先确认是不是刚升级过内核但没重启。/lib/modules/$(uname -r)/build这个目录是内核模块编译要用的软链接,指向头文件和Makefile所在位置,如果这个目录不存在,就说明headers没装好。
然后写一个最简模块,文件名就叫hello.c:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init hello_init(void) { printk(KERN_INFO "hello: module loaded\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO "hello: module unloaded\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");对应的Makefile也很简单:
obj-m += hello.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean这里有两个关键点。第一,KDIR指向的是当前内核的build目录,不是源码目录,因为我们是编译外部模块,只需要头文件和编译规则。第二,Makefile里的缩进必须是一个Tab,不能是四个空格,否则make会报missing separator。我们小组至少有两个人栽在这一步。
编译:
make正常会生成hello.ko。然后加载和查看日志:
sudo insmod hello.ko dmesg | tail lsmod | grep hello sudo rmmod hello dmesg | tail如果一切正常,dmesg里能看到hello: module loaded和hello: module unloaded。建议加载完模块后立刻用lsmod确认,避免后面排查时以为加载了但其实没有。
2.3 模块加载失败的两类原因
第一类原因是权限,报错通常是Operation not permitted。如果当前内核启用了模块签名验证,并且在物理机上开启了Secure Boot,未签名或未注册密钥的模块会被直接拒绝。我们内部统一建议:做这类实验尽量在虚拟机里做,省掉很多签名相关的折腾。不是说不可以在物理机上学,而是新手还没到需要处理密钥链的阶段,没必要一上来就和硬件信任链搏斗。
第二类原因是版本不匹配。有人编出来的.ko文件名带了版本号,比如hello.ko,但加载时提示版本magic不对。这种情况通常是因为源码或Makefile里的KDIR指向了错误的内核版本,或者头文件包和正在运行的内核不是同一个版本。检查命令就是看uname -r和ls /lib/modules/下的目录名是否一致。
3. 从软件源加速到网络问题排查
3.1 软件源镜像替换的正确姿势
小组里几乎每隔几天就有人问:为什么apt install慢到没法用?这周我们花了一个晚上整理软件源加速的办法。核心思路很简单:把系统默认的软件源地址换成离自己近的镜像站点。但这里要特别注意,不要随便在网上找一段sed命令就往系统里贴,先备份,再修改,最后验证。
以Debian系为例,先备份原始文件:
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak然后替换源地址。假设要把默认的http://archive.ubuntu.com/ubuntu替换为某个内部镜像站https://mirrors.example.com/ubuntu,可以用:
sudo sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.example.com/ubuntu|g' /etc/apt/sources.list执行完以后一定要先看一遍修改结果:
cat /etc/apt/sources.list确认没有把路径替换成乱七八糟的东西后,再运行:
sudo apt update如果系统启用了第三方源的签名验证,新增镜像站可能需要导入对应的GPG公钥。这里不推荐直接禁用签名验证,因为那是拿整个包管理器的安全在赌。正确做法是去镜像站文档里找到对应的公钥安装步骤。
3.2 tcpdump抓包定位一个真实网络问题
周三的时候A同学反馈,开发板ping网关不稳定,时通时不通。第一反应是网线或无线信号问题,但他换了几根线都一样。后来我们用tcpdump在开发板上抓包:
tcpdump -i eth0 icmp结果很有意思,请求包确实发出去了,但一直没有回复。然后到网关侧用同样的命令抓包,发现网关也收到了ICMP请求,但回包被防火墙策略丢掉了。检查了一下,发现某条规则把来自这个网段的回包拦了。调整规则之后,开发板立刻恢复正常。
这次排查最大的收获不是解决了问题,而是让组员记住了一个原则:网络不通的时候,先明确“在哪一层断了”,再决定用什么工具。tcpdump最常用的是先抓ICMP和DNS,因为这两个能快速判断链路和域名解析是否正常。保存抓包结果可以用:
tcpdump -i any port 80 -w http.pcap然后用Wireshark打开看细节。平时抓包不要直接抓所有流量,一定要加过滤条件,不然文件会涨得飞快,分析时反而找不到关键数据。
3.3 顺手写了一个批量重命名脚本
整理资料的时候发现有一批图片扩展名是.jpg,但实际文件格式是JPEG,想统一成.jpeg。这种批量操作最适合写个Shell循环。脚本内容很简单:
#!/bin/bash for f in *.jpg; do mv -- "$f" "${f%.jpg}.jpeg" done重点是${f%.jpg}这个写法,意思是去掉变量f末尾的.jpg,再拼上新的扩展名。很多人第一次看到会把它和${f%%.jpg}搞混,两者区别在于匹配范围:单个%去掉最短匹配,双%去掉最长匹配。在这个场景下结果一样,但理解原理比记住命令更重要。
mv --里的--表示“选项结束”,防止文件名以-开头时被当成参数。文件名用双引号包起来,也是防止路径里带空格时被拆成多个单词。这种细节在真实环境里非常有用,因为文件命名往往不受你控制。
4. 踩坑复盘与排查速查
4.1 内核头文件版本不匹配
实验课上有人在内核升级后没有重启,直接编译模块,结果make时报错说找不到/lib/modules/xxx/build。一开始以为是headers没装,后来发现是当前运行的内核还是旧版本,头文件已经装成了新版本。重启以后问题就消失了。这类问题最好一开始就自查两个命令:
uname -r ls /lib/modules/$(uname -r)/build如果第二个命令提示目录不存在,那就不是版本不一致,是真的缺头文件。
4.2 脚本执行权限与解释器问题
另一个高频问题是写了脚本但运行时报Permission denied。不用怀疑是系统坏了,多数情况下只是文件没有执行权限。解决办法是:
chmod +x script.sh ./script.sh或者不赋予执行权限,直接用解释器运行:
bash script.sh如果脚本开头写了#!/usr/bin/env bash,但内容是用Windows编辑器改过的,可能还会出现\r字符导致的诡异报错。这时可以用sed -i 's/\r$//' script.sh把回车符去掉。我们小组有位成员就是因为脚本是从网上复制下来保存的,卡了差不多半小时。
4.3 本周问题速查表
把这周出现过的典型问题整理成了一张表,方便以后快速检索。
| 问题场景 | 现象 | 可能原因 | 快速排查/解决 |
|---|---|---|---|
| apt更新 | 报无法解析域名 | 没有网络或DNS配置不对 | 先ping网关地址,再检查/etc/resolv.conf |
| 编译模块 | 找不到内核头文件 | 未安装对应版本headers | 执行apt install linux-headers-$(uname -r) |
| 加载模块 | Operation not permitted | 权限不足或签名被拦截 | 确认加sudo;实验环境建议用虚拟机 |
| dmesg | 看不到模块打印 | printk日志级别限制 | 尝试dmesg -n 8或检查模块是否真的加载 |
| make | missing separator | Makefile缩进用了空格而不是Tab | 把缩进改成Tab字符 |
| 运行脚本 | Permission denied | 文件没有执行权限 | 执行chmod +x或改用bash script.sh |
4.4 日志和文档的好习惯
这周最大的感受是,很多人遇到问题第一反应是截图发群里,而不是自己先去看日志。其实Linux已经把所有线索都写在日志里了,只是我们平时不习惯翻。系统日志一般在/var/log目录下,systemd管理的服务可以用journalctl -u 服务名查看。内核日志直接看dmesg。
我们内部立了一个规矩:提问的时候必须带上自己执行过的命令和对应的报错输出,谁先看到日志谁先试着解释,解释不了再上升到群里讨论。这个规矩执行了一周之后,群里重复问题的数量明显少了,因为很多人自己在看日志的时候就发现问题了。
5. 下一周计划与入门建议
5.1 下一周计划
第一周周报写到这里,下周的安排也基本定了。我们准备聊Git协作工作流,因为以后大家要共同维护小组的知识库和脚本仓库,不会用Git根本没法合作。同时会把这次内核模块的实验整理成一份带错误记录的文档,放到小组的文档仓库里。后面还计划安排一次容器基础分享,让新成员知道怎么用容器跑一个隔离环境,降低折腾系统的心理负担。
内容形式也打算调整一下:技术分享不再只用屏幕共享讲,而是提前给一份任务卡,现场一起做,做不出来就现场报错现场排查。这样记录下来的问题才是最真实的。
5.2 给Linux新手的三个建议
第一,不要着急换发行版。一定要先在虚拟机或者容器里试,等自己真的知道想要什么了再装物理机。第二,遇到不懂的命令,先用man和--help,不要立刻去搜索引擎复制答案。很多命令的坑,文档里写着,只是没有耐心读。第三,养成记笔记的习惯。不用记得漂亮,哪怕是截图加两行字,过两周再回看都值钱。我们小组这周的周报,本质上也是把零散记录整理成结构化的东西。
有一次我在修一个编译问题时,本来只差一条环境变量,但因为当时没有记录,第二天重新面对同样的报错时又花了一个小时。后来我不管多小的操作都往笔记里丢一条,这个习惯帮我省下的时间远比当时写笔记的时间多。
写周报最累的不是整理内容,而是回忆当时为什么卡住。写到后半部分的时候我还在想,这些在别人看来可能很简单,但对我们来说,每一个坑都是实实在在的长进。第一期的内容不算多,但胜在都是真跑过的。下周见。