☰
Linux diff与patch命令详解:从生成补丁到应用补丁的完整流程
2026/10/2 1:43:58 网站建设 项目流程

搞 Linux 这行,几乎没人能绕开diff和patch这对组合。不管是给内核提交补丁、给某个开源项目修 bug,还是自己在服务器上维护一堆配置文件,本质上都是“对比新旧差异、把差异打包、再应用到别处”这一套逻辑。diff负责找出差异,patch负责把差异打进去,两个命令配合起来就是一条完整的补丁工作流。

今天这篇就围绕这个主题,把diff生成补丁、patch应用补丁的完整流程拆开讲清楚。适合刚接触 Linux 命令行的新手,也适合平时在用但没系统整理过这套流程的人。我会从命令参数、补丁格式、路径处理讲到实际演练和排错经验,尽量把每一步背后的原因也交代清楚,而不是只给一串“照着敲就行”的命令。

1. diff 与 patch 这套流程到底在解决什么问题

1.1 为什么不用直接传整个文件

先想一个场景:你在维护一个几十万行的源码项目,改了两三个文件,每个文件改了几行。如果你想把这个改动交给别人,最笨的办法是把整个项目压缩包发过去。但这样做问题很明显——包大、浪费时间、而且对方很难一眼看出你到底改了什么。

补丁机制的核心思路是:只传递变化,不传递完整内容。diff生成的补丁文件里,记录的是“哪一行被删了、哪一行被加了、哪一行保持不变作为上下文”。这个补丁文件通常只有几 KB,哪怕你的项目是几百 MB 也没关系。对方拿到补丁后,用patch命令就能把自己的副本改成和你一模一样的状态。

这个思路放在今天看可能觉得理所当然,但在没有版本控制系统的年代,这就是协作开发的命脉。即便现在 Git 已经普及,diff/patch这套流程依然没有被淘汰——很多内核开发、嵌入式移植、软件打包的场景里,补丁文件仍然是最标准、最通用的交流格式。

1.2 整个流程只有三步

这套工作流总结下来就三个动作:生成补丁、传输补丁、应用补丁。生成补丁是开发者在自己的工作目录里完成的,传输补丁可以靠邮件、聊天工具或者挂在代码仓库里,应用补丁是接收方在自己机器上做的。

  • 生成补丁:diff -u 旧文件 新文件 > 修改.patch
  • 应用补丁:patch -p1 < 修改.patch
  • 回退补丁:patch -R -p1 < 修改.patch

看起来简单,但实际运用中涉及不少细节:目录层级怎么处理、新增文件怎么识别、二进制文件怎么办、补丁应用失败怎么排查。这些我都会在后面展开讲。先把核心思想记住:diff 是减法思维,patch 是加法操作,一个是“找出区别”,一个是“应用区别”。

2. diff 命令参数和输出格式拆解

2.1 三种常见的 diff 输出格式

diff命令有很多输出格式,日常接触最多的有三种。如果你看过 Git 的提交记录,其实你已经见过其中一种了。

先说普通格式(normal),也就是不加任何格式参数时的默认输出。它用<和>来标识“旧文件里的内容”和“新文件里的内容”,靠a、d、c这三个字母表示添加、删除、修改操作。这个格式信息密度低、还带着方向符号,实际使用中基本只适合人眼简单对比两个小文件。

再说上下文格式(context),加上-c参数启用。它会输出变化的行,以及变化前后各若干行作为参照,用!标记发生变化的行。这种格式比普通格式好读不少,但体积大,因为每个变化点都要重复输出一大段上下文。

最后是合并格式(unified),加上-u参数启用。它把前后的上下文压缩到一个区域里,删除的行用-开头,新增的行用+开头,变化点用一个@@行来定位。今天的 Git diff、补丁文件基本上都用这个格式。它的体积小、可读性好、最重要的是patch应用起来最可靠。

2.2 高频参数逐个说

diff的参数非常多,但实际工作中高频出现的就这几个,我按使用频率排个序:

  • -u:输出 unified 格式,生成补丁时必加,否则 patch 工具可能无法识别格式。
  • -r:递归对比目录。没有它,diff对目录只会报“它们是目录”而不会深入对比内部文件。
  • -N:把不存在的文件当作空文件来处理。没有它,新增文件或删除文件根本不会出现在 diff 结果里。生成补丁时不加-N,对方打补丁后不会多出新文件,也不会帮你删掉该删的文件。
  • -a:把所有文件当作文本文件处理。默认情况下,遇到二进制文件diff会提示“binary files differ”,有了-a它就会强行做逐字节文本比较,但输出的内容可能是乱码。
  • -x 模式:排除匹配的文件或目录。比如-x '*.log'就能忽略所有日志文件。如果想排除整个目录,用-x .git之类。
  • --strip-trailing-cr:忽略行尾的\r。如果你在两台系统之间倒腾文件(比如 Windows 和 Linux),行尾符不一致导致 diff 结果稀奇古怪,这个参数能救命。

常用组合是diff -uNr 旧目录 新目录 > 改动.patch。这个组合意味着:递归遍历、输出 unified 格式、把新增文件当作从空文件开始的修改。这样打出来的补丁是完整可用的。

2.3 补丁文件头里的数字到底什么意思

很多人第一次看到diff -u的输出会蒙圈。这里用一个小例子说明。

假设旧文件old.txt内容为:

apple banana cherry

新文件new.txt内容为:

apple banana kiwi

运行diff -u old.txt new.txt,输出大致是:

--- old.txt 2024-01-01 12:00:00.000000000 +0800 +++ new.txt 2024-01-01 12:00:00.000000000 +0800 @@ -1,3 +1,3 @@ apple banana -cherry +kiwi

第一第二行是文件路径和时间戳,---对应旧文件,+++对应新文件。第三行@@ -1,3 +1,3 @@是 hunk 的定位信息:-1,3表示旧文件的第 1 行开始、连续 3 行;+1,3表示新文件的第 1 行开始、连续 3 行。后面的@@在同一行上的额外内容(如果有)是所在函数或上下文信息。

3. 生成补丁的实操方法与路径处理

3.1 单文件补丁的生成

单文件补丁是最简单的情况。比如你改了系统里的一个配置文件,想把改动保留下来或发给同事,就这么写:

diff -u /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak > nginx-config.patch

等一下,顺序别搞错。diff -u 旧文件 新文件,旧文件在前,新文件在后。如果你写反了,生成的补丁内容会把新增行和删除行对调,对方应用后会把新版改回旧版,方向完全反了。我见过不止一次因为这个顺序问题导致补丁应用后代码状态不符合预期的,重点是这类错误还不报错,很难察觉。

3.2 整个目录树的补丁生成

一个项目通常不止一个文件改动。这时候直接对比两个目录,一次性生成整棵树的补丁:

diff -uNr original/ modified/ > project-update.patch

这里original/是原始未改动的代码,modified/是你改完之后的代码。输出会把所有差异文件都汇总到project-update.patch里。注意,目录对比时如果某个文件在modified/里是新增的,务必确保加了-N参数,否则这个文件不会出现在补丁中。同理,如果你删除了某个文件,不加-N的话接收方也不会看到删除操作。

还有一个细节:对比目录时路径分隔符和斜杠。patch应用补丁时是通过补丁文件里的文件名来定位目标文件的,所以生成补丁时的“起始目录名字”会影响对方应用的命令。比如diff -uNr original/ modified/生成的补丁里,文件路径会写成original/xxx.c和modified/xxx.c。对方把这个补丁放到他项目的根目录下,需要用patch -p1剥掉第一层目录名(也就是original/)。

3.3 路径层级和 p 参数的对应关系

patch的-p参数是用来“剥掉路径层数”的,这个必须理解清楚。补丁文件里写着a/src/main.c和b/src/main.c(a/、b/前缀常见于git diff生成的结果),patch -p1的意思就是“忽略第一段路径”,于是a/src/main.c变成了src/main.c,然后拿这个相对路径去当前目录下找文件。

如果补丁里的路径是original/src/main.c,而你当前就在项目根目录下,同样需要-p1来剥掉original/。如果补丁里直接就是src/main.c,那就不需要剥层,用patch -p0。如果补丁是在项目根目录的上一级生成的,路径是两个目录的完整相对路径,可能得-p2甚至更多。

记住这条经验:拿到任何补丁,先打开看一眼文件路径,再决定用-p几。老手也不是靠猜,都是先看补丁内容再敲命令的。

4. patch 打补丁全流程与关键参数

4.1 先干跑一下,不要直接应用

patch支持“预测模式”,也就是先模拟应用一遍,不实际修改任何文件。这是我认为这套流程里最值得养成的习惯:

patch --dry-run -p1 < project-update.patch

如果这条命令输出的结果是“patching file xxx”并且没有FAILED字样,说明补丁可以顺利应用。如果某个文件报错说 “can't find file to patch”,那你就要检查路径层级或者当前目录是不是对了。

干跑模式的意义在于,它能把代价降到零。正式应用一旦失败,工作区里会残留.rej(reject,拒绝应用)文件,运气不好还会改掉一半文件,留给你一堆半成品。先干跑、后正式,这个顺序能帮你挡掉 90% 的意外。

4.2 正式应用、备份与回退

正式应用只需去掉--dry-run:

patch -p1 < project-update.patch

默认情况下,patch处理每个文件时如果发生冲突,会把无法匹配的行写入后缀为.rej的文件,被修改前的原始内容则会存成.orig文件。这两个文件往往意味着“这个补丁不能直接应用,你最好手动处理冲突”。

如果应用完发现效果不对,或者补丁方向搞反了,可以用-R参数回退:

patch -R -p1 < project-update.patch

-R会把补丁里的+和-对调,相当于撤销这次修改。前提是你应用补丁后没有继续手动改动那些文件,否则回退也会失败。

还有一个实用参数是-b,它在应用补丁前会自动备份被修改的文件,文件名后面多一个.orig后缀。和默认生成的.orig作用类似,但-b的逻辑更明确:保留应用补丁前的状态,方便出问题随时还原。

注意:我强烈建议在任何可能出乱子的项目上,先cp -r 项目目录 项目目录.bak做一份完整备份。patch -b只能备份被修改的单个文件,不能备份整个项目结构。

4.3 patch 的其他高频参数

除了-p和-R,还有几个参数在特定场景下非常有用。-d 目录可以在执行patch前先切换到指定目录,适合你人不在项目根目录但补丁里的路径是相对路径的情况。-E在应用补丁后删除空文件,如果补丁里删除了某个文件的全部内容,这个参数能保证文件本体也被移除。--fuzz=N设置匹配行时的模糊度,patch在找 hunk 位置时允许上下文有少量不匹配,默认值是 2,调大能让某些因为微小平移导致的失败变成成功,但也有可能找错位置,属于“有风险就撞运气”的方案,不推荐新手动。

5. 实战演练:从配置文件修改到补丁应用

5.1 场景设定

假设你在维护一个小型 Web 服务,服务器上有两份 Nginx 配置目录。一份是当前正在用的nginx_old/,一份是你调优后的nginx_new/。两个目录里都有一堆.conf文件,其中server.conf改了几个参数,upstream.conf是新增的文件。现在要把这些改动从本机同步到另一台服务器上。

目录结构大致是这样:

nginx_old/ nginx.conf server.conf mime.types nginx_new/ nginx.conf server.conf upstream.conf mime.types

5.2 执行命令与结果解读

第一步,生成完整补丁:

diff -uNr nginx_old/ nginx_new/ > nginx-tuning.patch

第二步,查看补丁内容,确认关键路径:

cat nginx-tuning.patch

文件里应该能看到类似这样的段落:

diff -uNr nginx_old/server.conf nginx_new/server.conf --- nginx_old/server.conf 2024-06-01 10:00:00.000000000 +0800 +++ nginx_new/server.conf 2024-06-02 15:30:00.000000000 +0800 @@ -20,7 +20,7 @@ server_name example.com; listen 80; - worker_connections 1024; + worker_connections 2048; keepalive_timeout 65; gzip on; }

upstream.conf因为是新增文件,还会有一段从/dev/null到新文件的 diff,可能长这样:

diff -uNr nginx_old/upstream.conf nginx_new/upstream.conf --- nginx_old/upstream.conf 1970-01-01 08:00:00.000000000 +0800 +++ nginx_new/upstream.conf 2024-06-02 15:30:00.000000000 +0800 @@ -0,0 +1,12 @@ +upstream backend { + server 10.0.0.1:8080 weight=5; + server 10.0.0.2:8080 weight=3; +}

然后把这个补丁文件传到目标服务器上,在项目根目录(也就是nginx_old/所在的父目录)执行:

patch --dry-run -p1 < nginx-tuning.patch

如果输出里有两行patching file nginx_old/server.conf和patching file nginx_old/upstream.conf,没有FAILED,就可以正式应用:

patch -p1 < nginx-tuning.patch

注意这里我用的是-p1,因为补丁里的路径带着nginx_old/这个前缀,而目标目录就叫nginx_old。如果你在nginx_old/目录里面执行,就得用-p1剥掉nginx_old/,这样它才能去找server.conf。

5.3 验证应用结果

应用完成后,验一遍很重要。最直接的办法是再次执行 diff,看差异是否已经消失:

diff -uNr nginx_old/ nginx_new/

如果没有任何输出,说明两个目录现在完全一致,补丁应用成功。如果想更谨慎一点,还可以检查有没有生成.rej或.orig残留文件:

find . -name '*.rej' -o -name '*.orig'

没有任何输出就是干净状态。这个习惯我建议保持,.rej文件不会自己消失,留久了很容易被误提交,造成下一代补丁的混乱。

6. 常见问题与排查技巧实录

6.1 遇到这些报错别慌

打补丁失败的场景,基本都在下面这个表里。逐个对照排查,大部分都能解决。

报错或现象可能原因解决办法
can't find file to patch当前目录不对,或-pN层级不对查看补丁头部的文件路径,到正确的目录下用正确的-p参数
Reversed (or previously applied) patch detected补丁方向反了,或者已经应用过了检查diff参数顺序;若已应用过,无需再次执行
FAILED: xxx上下文不匹配,目标文件和生成补丁时的基准文件差异太大打开.rej详情,手动比对合并
补丁应用成功但文件内容不对上下文匹配到错误位置(fuzz 过度)查看 hunk,确认@@定位信息,必要时手动修改
行尾符不一致Windows 换行\r\n与 Linux 换行\n混用用--strip-trailing-cr参数,或统一换行符

最常栽的是第一种。项目目录没对上、-p1误写成-p0、补丁文件是在另一个完全不同的仓库根目录下生成的,都会导致can't find file to patch。排查方法很简单:打开补丁顶部,看+++后边写的路径是什么,然后问自己“我现在所在的目录下能不能通过这个相对路径找到文件”。能,就用-p0,不能,就剥一层再找。

6.2 日常维护补丁的几个经验

说几个我在实际中总结出来的习惯,希望对你有帮助。

第一,补丁文件的命名尽量带上日期和功能描述,比如20240602-nginx-worker-connections.patch。时间一长你就知道,什么fix.patch、update.patch这种名字隔一个月就完全不认识了。补丁文件不是一个一次性的临时文件,它可能要在多个环境里传播、归档、追溯,名字就是它的门面。

第二,能用git diff的时候尽量用git diff。现在大部分项目都在 Git 仓库里,你改了文件没提交,git diff > 改动.patch就能生成非常规范的补丁。Git 生成的补丁格式天然适合patch -p1应用,路径前缀固定是a/和b/,减少了很多路径上的麻烦。如果你不想带a/、b/前缀,可以git diff --no-prefix > 改动.patch,这样对方可以用-p0更直接地应用。

第三,如果你提交的补丁是要给内核或者大型开源项目用的,记得先跑一遍项目自带的检查脚本,内核里就是scripts/checkpatch.pl。它能帮你检查补丁有没有格式问题、多余空白、长行之类的毛病。很多维护者非常在意这些细节,补丁格式都不规范的话,技术内容再好也容易被拒。

第四,不要轻易用--fuzz去强行应用一个本来匹配不上的补丁。模糊匹配的工作原理是忽略少量上下文错误、尽可能把 hunk 放到看似“差不多”的位置上。如果目标文件和补丁的基准版本差异比较大,模糊匹配很容易把修改放到错误的函数甚至错误的文件区域里,而且不报错。补丁应用后编译不报错不代表它真的放对了位置。宁可让补丁失败、手动解决冲突,也不要靠调大 fuzz 来“赌一把”。

第五,二进制文件不适合走 diff/patch 这条流程。虽然diff -a能强行比较并生成补丁,但二进制格式一旦有了细微变化,生成的补丁基本上是一堆不可读的乱码,体积也不一定小。二进制文件的更新,老老实实直接传完整文件,或者用rsync这类工具按块同步,别硬套补丁流程。

这套 diff/patch 的玩法并不复杂,核心就是“差异即补丁、补丁即差异”。会用diff -uNr生成补丁,会用patch -p1应用补丁,再掌握--dry-run先试、-R回退、.rej排错这几个关键技巧,日常工作里九成以上的补丁场景都能稳稳拿捏。理解它背后“先对比、再传输、后合并”的协作思维,你以后遇到 Git、Gerrit、邮件列表里的各种补丁交互,思路也会清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询