尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
深入Linux fork:从写时复制到僵尸进程与多线程陷阱的完整链路
见过不少朋友在解释 fork 时把答案停在“父进程拿到子进程 PID子进程拿到 0然后代码分叉”就交差了。这个说法没有错但它只回答了“调用长什么样”没回答“调用之后系统里发生了什么”。线上真正让你头疼的那些现象——printf 打了两次、子进程变成摸不掉的僵尸、多线程程序 fork 后莫名卡死——全部藏在这个问题的更深处。这一篇我打算把 fork 之后的完整链路捋一遍内核在入口处做了什么、内存是怎么“复制”的、父子进程从哪一刻真正分家、为什么几乎所有程序都要在 fork 之后立刻 exec、僵尸和孤儿到底怎么处理、以及多线程程序里 fork 会踩到什么隐蔽的坑。这篇文章适合两类人刚写完第一个 fork 程序、想知道系统全貌的学习者以及被线上诡异 bug 逼着回来补课的开发者。1. 一次调用两次返回fork 在内核入口处做了哪些事fork 在 C 语言里只是一个pid_t fork(void)但你每次调用它实际上是在向内核发起一个系统调用。用户态的 glibc 做了一层薄封装真正干活的是内核里与进程复制相关的整条逻辑链。这条链的核心是copy_process无论内核版本怎么演进、调用的路径叫什么名字核心要做的事始终是下面这一串。首先内核会为子进程分配一个新的task_struct。你可以把它理解成 Linux 的“进程档 案”里面记录了 PID、PPID、进程状态、打开的文件、内存描述符指针、信号处理方式、资源限制、调度信息等等。task_struct是 fork 最根本的产物没有它后面所有“子进程”的概念都不存在。分配完这个结构之后内核还要给子进程准备一份独立的内核栈因为在返回用户态之后父子进程要共享同一份代码、但各自维护独立的执行现场。接下来是核心资源的复制我整理成一张闯关清单步骤复制/新建的内容说明1进程描述符与内核栈新的 task_struct 和新内核栈子进程由此诞生2PID 与进程关系链分配新的 PID建立父子关系加入进程树3地址空间描述符 mm_struct为子进程创建独立的 mm_struct为写时复制做准备4文件描述符表复制 fd 表但底层的 file 结构体引用计数加 15信号相关结构复制或共享信号处理函数与信号掩码6定时器与统计信息复制相关计时器状态调度统计归零留意第三步它非常关键子进程拿到的是一个“新的 mm_struct”但这个新结构在最初几乎完全指向父进程的物理内存页。也就是说fork 之后父子进程共享着同一批物理内存只是页表级别做了手脚。具体怎么做的下一节详细展开。还有一个容易被忽视的点这次系统调用为什么能“两次返回”原因在于 fork 会把用户态的寄存器上下文完整复制一份给子进程同时内核在返回路径上做了手脚——父进程在系统调用返回时拿到的返回值是子进程的 PID子进程拿到的返回值是 0。这就是“一次调用返回两次”的真相它不是玄学而是内核在两条返回路径上分别改了返回值。理解了这一步你就能回答另一个常见问题为什么子进程返回 0 而不是返回自己的 PID我觉得这个设计非常符合 C 语言的使用习惯——子进程一出生就靠这个 0 跟父进程分流到完全不同的代码分支而父进程拿到子进程 PID 是为了后续可以waitpid精准回收它。如果你不需要精准回收wait(-1, ...)照样能兜底。需要提醒的是fork 不是百分百成功的。进程数达到上限、内存不足时fork 会返回 -1 并设置 errno常见的是 EAGAIN。我见过很多新手代码不检查返回值就直接拿负数 PID 去 waitpid结果在低内存环境上线后警报乱响。写 fork 的第一原则是调用后立即判断三分支。2. 复制内存的真相写时复制与“谁先写谁承担成本”关于 fork 有一个流传很广的误解fork 会把父进程的整块内存复制一份子进程拥有独立副本。这个理解放在上世纪某些 Unix 实现里是对的但放在现代 Linux 上就完全偏离实际了。现代内核用的是写时复制Copy-on-WriteCOW这个机制我建议你把它当作理解 fork 一切后续行为的基点。fork 发生时子进程的页表会指向父进程已经映射好的那些物理页不重新拷贝任何物理页的内容。内核要做的只是把共享的那些页表项标记为只读。读是所有人都能读的写则不行。之后不管是父进程还是子进程只要有人想往某个页里写CPU 就会触发缺页异常内核在异常处理路径里检查页面的映射情况如果这个物理页只有一个进程引用直接把页表项改成可写就行如果还有多个进程引用就分配一个新物理页把旧页内容拷贝过去让写进程拿到自己的私有副本同时把两边页表项的映射计数减掉。这个设计带来两个实实在在的好处。第一fork 本身变得极快。父进程占用 10GB 内存fork 也几乎是一瞬间完成因为没有任何物理页拷贝。只有真正发生写入的页才会在那一刻付出一次性的拷贝代价。第二内存占用大幅下降。父子进程如果共享一段只读代码段那这段代码在整个系统里只需要一份物理页。我在接触过的一个数据处理服务里验证过这个收益进程启动后加载了数百 MB 的词典数据fork 出去一批 worker所有 worker 共享同一份词典物理页整体 RSS 并没有随着 worker 数量线性疯涨。假如每次 fork 都全量复制这种架构根本跑不起来。观察 COW 的办法很多比较简单的是看/proc/pid/smaps里的Shared_Clean和Private_Dirty这些字段。Shared 表示当前映射还没被私有化Private 表示已经因为写入被单独拷贝了。我自己写过一个验证 demo #include stdio.h #include stdlib.h #include unistd.h #include string.h int main(void) { char *buf malloc(64 * 1024 * 1024); memset(buf, a, 64 * 1024 * 1024); /* 先写一遍建立父进程的私有页 */ pid_t pid fork(); if (pid 0) { perror(fork); exit(1); } if (pid 0) { /* 子进程只读所有页不触发写时复制 */ long sum 0; for (int i 0; i 64 * 1024 * 1024; i) sum buf[i]; sleep(3); /* 子进程开始写入逐个页触发写时复制 */ memset(buf, b, 64 * 1024 * 1024); sleep(3); _exit(0); } wait(NULL); return 0; }这个程序在子进程里先读后写配合 smaps 或者简单的 RSS 监控可以直观看到读阶段不会改变共享/私有的比例写阶段共享页大幅减少。这也解释了为什么有人会说 COW 的代价“谁先写谁承担”——写操作触发缺页异常异常处理带着内存拷贝开销那些从不写入的页则始终共享着。有一个历史细节也值得提早期 Linux 为了减少 fork 之后父进程紧接着写页、白白触发 COW 的概率内核在 fork 后会让子进程优先进入调度。现代的调度逻辑已经不像当年那么直接但道理还是一样的——新 fork 出来的进程生命周期通常很短要么很快 exec 替换掉旧映像要么很快退出让新进程先跑比让父进程先跑划算得多。3. 返回之后的分岔路变量、文件偏移和缓冲区继承fork 返回之后父子进程的程序计数器PC指向同一条指令的下一行寄存器内容几乎一模一样所有变量在当前时刻的值也完全相同。但这个“完全相同”只是一个起点。用户态的变量、栈、堆这些内存区域在写完之前都是共享的物理页一旦某个进程动手修改某个页写时复制立刻把那个页单独切开。所以在你的直觉里应该把 fork 后的父子进程理解成“你现在有一个自己还有一个刚分裂出来的克隆人你们俩共享记忆但从某一方动手改东西的那一刻起你们就开始长出各自不同的记忆空间”。子进程里x 100不会影响父进程的x反之亦然两个进程从同一初值出发走向各自不同的状态。不过有几样东西的“共享方式”经常让人误解。第一个坑是文件偏移量。父子进程各有自己的一份文件描述符表但这两张表里的每个条目指向的是同一个内核 file 对象。file 对象里存着当前读写位置offset。所以父进程往文件里写了一个字节文件偏移量前进一格子进程再往同一描述符写时会接着父进程写到的位置继续写而不是从开头重新写。如果你没意识到这一点很容易写出“明明两个进程写得是同一块逻辑顺序却来回穿插”的诡异日志。要避免这种错乱要么父子各开各的文件描述符要么用pread/pwrite这种带偏移的系统调用。第二个坑我踩过不止一次是标准 I/O 缓冲区继承。请看这个经典场景printf(hello before fork\n); pid_t pid fork();如果这里打印的内容后面没有换行或者输出对象被重定向到了文件那么hello before fork可能还躺在用户态缓冲区里没进入内核。fork 会复制用户态地址空间自然也会把这份缓冲内容复制给子进程。等到父进程刷新缓冲区输出一条子进程刷新缓冲区输出一条。结果一句话变成了两句话而且看起来就像是 fork 让 printf 执行了两次。我当时排查类似问题时的修复方式很简单fork 之前调用fflush(NULL)把所有标准流缓冲区全部刷掉再考虑 fork 的事。养成这个习惯之后这类重复输出基本绝迹。另外还有一批“继承项”值得记住当前工作目录、环境变量、umask、nice 值、信号处理函数的设置方式、资源限制这些都会在 fork 时原样继承。其中信号处置的细节尤其容易踩坑父进程忽略某个信号子进程也会忽略父进程为某个信号注册了自己的 handler子进程的 handler 也被复制过去。还有一件事经常被问fork 之后父子进程到底谁先执行答案是不保证。调度器按优先级、时间片和 CPU 空闲情况自己决定。我建议永远不要在业务逻辑里赌“这一刻父进程还没跑子进程一定能先完成初始化”。需要顺序控制就用管道、信号或者共享内存加原子操作做同步而不是靠 sleep 碰运气。4. 从 fork 到 exec为什么每个进程几乎都要走这一步fork 复制出来的子进程仍然在执行父进程的代码段。意思是它出生那一刻就是一个会走完父进程后续逻辑的“分身”。但实际业务里我们 fork 完子进程之后绝大多数情况是想让子进程去执行另一个程序——比如 shell 要执行你敲的ls比如服务进程要拉起一个外部工具。这时候就必须 exec。exec 系列有 execl、execlp、execle、execv、execvp、execvpe 这些名字但它们全部是库函数封装最终都要落到execve这个系统调用。带p的会自动从 PATH 环境变量里搜索可执行文件带e的可以显式传递自定义环境变量数组其他差异基本在参数组织和查找方式上。exec 做的事情可以概括为把当前进程的地址空间整个替换掉重新载入一个新程序。具体来说内核会先校验要执行的文件、权限和格式。拿最常见的 ELF 文件举例内核读取 ELF 头识别出这是动态链接的可执行文件之后并不直接加载所有共享库而是先加载程序解释器也就是动态链接器通常是ld.so这个角色把控制权交给它。链接器再去读取可执行文件依赖的共享库完成符号重定位最后跳到程序的入口点。如果你执行的是静态链接程序就少掉动态链接器这一层内核本身就能完成加载。在装载新程序的整个过程中旧进程的代码段、数据段、堆、栈全部被替换之前 fork 继承下来的地址空间内容烟消云散。但有几种状态会保留下来PID 和 PPID 不变进程依然维持同一个身份没有设置FD_CLOEXEC的文件描述符会继续保留新程序可以继续使用这些 fd被捕获的信号处理函数会重置为默认动作但被忽略的信号继续保持忽略信号掩码、环境变量里的 PATH、内核统计时间这些也在。这也是为什么 fork 之后要立刻判断 exec 是否成功#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main(void) { pid_t pid fork(); if (pid 0) { perror(fork); exit(1); } if (pid 0) { execlp(ls, ls, -l, NULL); perror(execlp); _exit(127); } int status; waitpid(pid, status, 0); return 0; }在这个示例里子进程同时承担两件事尝试用ls替换自己以及处理 exec 失败的情况。exec 成功的话当前地址空间被替换perror之后那几行代码永远不会执行exec 失败才会返回 -1函数调用者必须处理这个错误。真正生产环境里几乎总是建议 exec 失败后立刻_exit(127)退出子进程而不是让它带着父进程的状态继续存活。原因就是前面说的缓冲区继承——如果调用的是exit它会刷新 stdio 缓冲区可能把父进程还没来得及 flush 的输出重复刷出去_exit直接进内核回收进程根本不碰那些缓冲区。我后来理解到fork 和 exec 的组合其实是 Unix 进程设计的一个精妙之处创建进程fork和替换程序exec被拆成了两个独立操作组合出无穷灵活性。你可以在 exec 之前顺手设置重定向、切换工作目录、修改信号处置把它们全部固化到子进程里再让 exec 把新程序请进来。这也意味着fork 之后不是非 exec 不可——如果你 fork 只是想得到一个执行相同代码的副本比如守护进程派生出多个 worker完全可以不 exec。但如果你 fork 之后要去调用高层库函数、拿锁、做 I/O请先翻到第六节看看多线程的坑。5. 僵尸不等于消失wait、SIGCHLD 与孤儿收养子进程退出之后并不会立刻从系统中彻底消失。内核会把退出状态码记录在子进程残留的 task_struct 里向父进程发送 SIGCHLD 信号然后保留这个结构直到父进程来“收尸”。如果父进程迟迟不来这个进程就进入了 Z 状态也就是僵尸进程。我遇到不少新手会把僵尸进程当成病毒或者内存泄漏。其实僵尸进程在 ps 里显示Z它不占用 CPU也不占用物理内存它占用的主要是两个东西一个是 PID 槽位一个是内核里的进程描述符。PID 不是无限的系统默认的 pid_max 通常是 32768如果大量僵尸堆积总有一天会 fork 失败报 EAGAIN。我曾经处理过一个定时任务服务父进程循环 fork 子进程但从不 wait跑了一周之后日志里开始出现“cannot fork”的报错检查ps -eo stat,ppid,pid,cmd | awk $1Z出来几百行僵尸。回收子进程的正规手段就是 wait 系列系统调用。#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main(void) { pid_t pid fork(); if (pid 0) { exit(42); } int status; waitpid(pid, status, 0); if (WIFEXITED(status)) { printf(exit code: %d\n, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf(killed by signal: %d\n, WTERMSIG(status)); } return 0; }wait等价于waitpid(-1, ...)回收任意一个已退出的子进程waitpid(pid, ...)可以针对某个特定子进程。第三个参数传WNOHANG时如果没有已退出的子进程立即返回 0不会阻塞调用者。这是写非阻塞事件循环时非常重要的一种方式。status 里面其实是编码过的信息不要直接拿它当退出码用。标准做法是用WIFEXITED、WEXITSTATUS、WIFSIGNALED、WTERMSIG这些宏去解码。WIFEXITED判断是否正常退出WEXITSTATUS取退出码WIFSIGNALED判断是否被信号杀死WTERMSIG取信号编号。清了这一点你才不会在排查“为什么子进程退出码是 139”的时候一头雾水——139 对应的是 128 11也就是被 SIGSEGV 信号杀掉了。实际项目里事件循环模型通常不会阻塞在一个 waitpid 上而是靠信号驱动。常见做法是把 SIGCHLD 的处置设成自定义 handler在 handler 里循环调用waitpid(-1, status, WNOHANG)static void sigchld_handler(int sig) { int status; while (waitpid(-1, status, WNOHANG) 0) { /* 处理退出状态 */ } }用这个模式有一点要注意信号处理函数里只能调用 async-signal-safe 的函数waitpid、_exit这一档没问题printf、malloc 这一类就不要放了。还有另一个取巧方案是直接把 SIGCHLD 设为SIG_IGNLinux 上这样会透传给内核“子进程退出后自动回收”省得写 wait 逻辑。代价是你拿不到子进程的退出状态所以到底用哪种方案取决于你需不需要关心那个退出码。孤儿进程是另一个容易混淆的概念。如果父进程先于子进程退出子进程会被自动收养收养方默认是 1 号进程也就是常说的 init 进程。现代 Linux 里还引入了一个叫 subreaper 的机制进程可以通过prctl(PR_SET_CHILD_SUBREAPER, 1)声明自己是“sub-reaper”这时它的子孙进程如果变成孤儿会先被它收养。这套设计在容器场景里非常常见。注意孤儿进程不会变成僵尸因为收养它的 init 会持续 wait 清理真正会变僵尸的是那些父进程自己退出后没人负责回收的子进程。关于 wait 还有一个容易忽略的细节多个子进程同时退出时wait会回收其中任意一个而且不保证是哪一个。如果你关心特定子进程的退出码就传它的 PID 用waitpid。在批量任务派发场景里我的习惯是维护一个“待回收子进程 PID 表”配合 SIGCHLD handler 或主循环里的非阻塞 waitpid 逐个回收一个都不漏。6. 多线程里的 fork 陷阱以及 vfork、clone 的取舍多线程程序里调用 fork是踩坑率最高的场景而且这些坑往往要到生产环境才炸。核心事实是一个多线程进程调用 fork 之后子进程不会拥有父进程的全部线程而是只保留调用 fork 时停在那里的那一个线程。其他线程在子进程里等于凭空消失了。听起来好像没什么但你自己想一想如果另一个线程当时正拿着某个锁呢那把锁在子进程的视角里依然处于“已持有”状态但持有锁的线程已经不存在了。子进程如果再去拿同一把锁就会死锁。这绝对不是理论问题。malloc 在分配内存时内部有锁printf 的缓冲区操作有锁很多日志库、连接池、ORM 都有自己的锁。多线程服务程序在 fork 之后如果调用这些库函数随时可能重启性卡死。我在排查一个“偶尔上线后几小时无响应”的问题时最终就是落在这种锁继承上——父进程的一个后台线程正拿着日志缓冲锁主线程 fork 出来的子进程尝试写日志直接锁死。因此多线程程序的 fork 有一条铁律如果你要在子进程里执行新程序请立刻 exec如果 fork 之后还要做任何复杂操作先问自己能不能只调用 async-signal-safe 函数。最安全的一句话总结是fork 之后能安全做的事就是 exec或者_exit。如果你确实必须在 fork 之后、不 exec 的情况下做一些受保护操作POSIX 提供了pthread_atfork这个机制它允许你注册三个回调函数prepare 回调在 fork 真正发生前执行用于把所有相关锁都锁上parent 回调在 fork 完成后、父进程环境里执行用于把锁解开child 回调在 fork 完成后、子进程环境里执行同样负责解开锁。这样保证子进程拿到的锁状态是干净的。pthread_mutex_t my_mutex PTHREAD_MUTEX_INITIALIZER; static void prepare_handler(void) { pthread_mutex_lock(my_mutex); } static void parent_handler(void) { pthread_mutex_unlock(my_mutex); } static void child_handler(void) { pthread_mutex_unlock(my_mutex); } /* 初始化时注册 */ pthread_atfork(prepare_handler, parent_handler, child_handler);但我要泼点冷水pthread_atfork能解决的问题有限。如果程序里同时存在十把、几十把锁跨多个模块注册一堆 atfork 回调很容易在复杂调用关系里出现漏锁或者死锁。最稳妥的方案仍然是“fork 之后立刻 exec”。这也是为什么我后来在服务设计里尽量避免“多线程主进程直接 fork 做事”而是派生出独立进程或者直接用线程。说完 fork 的坑顺便把 vfork 和 clone 也讲清楚。vfork 的历史语义是“子进程借用父进程地址空间父进程阻塞直到子进程 exec 或退出”它的原始动机是避免 fork 早期全量复制内存的开销。但现代 Linux 的 fork 已经基于 COW创建开销大幅下降vfork 的收益主要变成了“少做一点页表复制和多余工作”。它有个代价很大的风险子进程在 exec 前直接写父进程的地址空间会污染父进程的数据。如今很多高性能场景里真正需要它时标准库也往往会替你把它包装进posix_spawn这类函数里。posix_spawn的存在价值就是让你用一个统一接口完成 forkexec 组合内部自动选择更优的实现路径普通业务代码建议直接用它。clone 则是 Linux 上更底层的系统调用fork 和 pthread_create 最终都建立在它之上。clone 最大的特点是可以通过 flags 精确控制父子进程共享哪些资源CLONE_VM共享地址空间CLONE_FILES共享文件描述符表CLONE_FS共享文件系统相关信息CLONE_SIGHAND共享信号处理器CLONE_THREAD则把新进程放进同一个线程组。看明白这套 flag你就能理解为什么 Linux 里线程被叫做“轻量级进程”——它就是通过 clone 选择了共享大部分资源的进程。反过来如果你想创建共享内存但独立执行流的结构也可以在 clone 层自己做组合。我个人的取舍习惯是普通场景直接 fork exec wait要在同一个程序里并发执行同一个代码路径用 pthread_create 而不是 fork需要拉起外部可执行文件又不关心复杂子进程操作优先posix_spawn只有做容器、namespace 这类极底层的系统编程才直接碰 clone。这套习惯帮我避掉了不少线上事故。把这几节内容串起来看fork 之后发生的事远不止“返回一个 0 一个 PID”那么简单。真正重要的是那一条贯穿始终的线索fork 出来的子进程继承的是“引用”而不是“状态”——内存页是引用文件对象是引用锁的状态也是引用只有当某个进程真正动手去改写写时复制才会把那个页彻底切开。谁先动手谁承担成本而你能不能管好这些共享引用决定了你的进程控制代码是稳稳跑起来还是留下一堆让人挠头的僵尸和死锁。最后再分享一个我自己长期使用的习惯写任何 fork 相关的程序都默认在 fork 前把 stdio 缓冲 flush 掉fork 后每条路径都显式处理 -1、0、正数三种情况任何子进程路径都要有_exit兜底。这三板斧看着简单已经帮我挡掉过绝大多数进程控制的低级事故。
RELATED

相关推荐

MATLAB在智能电网通信仿真中的高保真建模与工程验证

MATLAB在智能电网通信仿真中的高保真建模与工程验证

简介:本资源是一份面向高校电气工程、智能电网及相关专业师生的MATLAB/Simulink仿真实验指导手册,聚焦风电机组在智能电网通信与运行场景下的动态响应建模与分析。手册系统设计四个递进式实验:前两例基于定速风电机组,分别仿真风速…

📅 2026/10/11 15:36:41
Linux基础IO实战:文件描述符、缓冲与阻塞非阻塞详解

Linux基础IO实战:文件描述符、缓冲与阻塞非阻塞详解

在Linux下写程序,IO是躲不过去的一关。我见过不少写了几年业务代码的朋友,能用printf和fread混着把功能跑通,但一旦遇到“为什么先打印的日志后落盘”“为什么read到EOF了数据还不对”这类问题,就开始抓瞎。这篇笔记没有高深理论&…

📅 2026/10/11 15:36:41
Hadoop日志按日期统计:时间解析、分区设计与MR工程化实践

Hadoop日志按日期统计:时间解析、分区设计与MR工程化实践

简介:本资源是一份面向大数据初学者与Hadoop实践者的完整日志分析项目,聚焦于使用MapReduce实现网站访问日志的按日期统计功能,解决真实场景中PB级日志数据的分布式聚合需求。压缩包共33个文件,含4个核心Java源码(含Ma…

📅 2026/10/11 15:36:41
MORE NEWS

更多资讯

📰

自动侧推定位机构中的接近开关:让工件靠边更准确

自动侧推定位机构常用于装配前校正、检测前靠边、输送线转位和小型工件姿态调整。工件从输送线进入定位区后,通常需要由侧推板或气缸将其推向基准面。如果侧推距离不足,工件可能没有真正贴紧定位边;如果回位不完整,又会影响下一个…

📰

排序算法选择排序全解析:逻辑、稳定性、复杂度与工程取舍

讲个真实场景:我见过不少刚接触算法的同事,写出来的第一个排序代码,其实都是选择排序。倒不是因为他们背过这个算法,而是因为人天生就喜欢"从一堆东西里挑最小的,放到最前面"——这个动作太符合直觉了。但选…

📰

快照时间线分析:用历史快照还原目标网站的演变

快照时间线分析:用历史快照还原目标网站的演变 【免费下载链接】Legendary_OSINT A list of OSINT tools & resources for (fraud-)investigators, CTI-analysts, KYC, AML and more. 项目地址: https://gitcode.com/GitHub_Trending/le/Legendary_OSINT …

📰

一个软件工程大一新生的C语言学习感悟

我的C语言学习之路作为一名软件工程的大一新生,在这个暑假里开始学习C语言,我想分享一下我的感受。我之前接触计算机很少,但也会一点基本的操作。在得知我是软件工程专业时,我便询问了豆包关于这个专业相关的内容,于是…

📰

AI-For-Beginners 实战指南:基于 Hugging Face Transformers 的实验、文本生成与 Notebook 整理

教程人工智能机器学习深度学习 【免费下载链接】AI-For-Beginners 12 Weeks, 24 Lessons, AI for All! 项目地址: https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners 点击查看 免费下载 本指南围绕课程《AI-For-Beginners》第 18 课的课后任务(…

📰

Spring Boot+Vue前后端分离旅游订票系统实战:从库存防超卖到订单状态机

上个季度我完整做了一个“旅游线路展示 在线订票”的前后端分离项目:Spring Boot 做后端接口,Vue 做前端页面,整个系统包含线路浏览、景点详情、日期团期选择、订单提交、支付状态回跳、后台线路维护这些核心环节。项目不大,但业…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬