新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux进程创建全解析:fork、exec与system的底层原理与实战

发布时间:2026/9/29 2:36:24来源:尧图网络
Linux进程创建全解析:fork、exec与system的底层原理与实战
1. 为什么“创建进程”是Linux学习绕不开的一关不管你是刚接触Linux的新手还是在写后台服务、搞嵌入式、维护服务器的老手进程这个概念都躲不掉。很多初学者学到进程这块第一反应是“我平时就在终端里敲命令也没手动创建过进程啊”其实不是这样。你在终端里执行的每一条命令、你双击启动的每一个服务、你用systemctl拉起来的每个daemon背后都是一次进程创建的过程。只不过平时操作系统帮你把这些事都干了你没意识到而已。搞清楚“一个进程到底是怎么被创建出来的”其实是在搞懂三件事父进程和子进程的关系、程序如何被加载成进程、以及系统调用和库函数之间那层薄纱。这篇文章我会用三种最直接的方式来演示创建进程——fork()、exec族函数、system()函数把每种方式的原理、代码、执行效果和坑都梳理清楚。同时也照顾一下刚入门的读者所以会把一些底层的机制比如写时拷贝、僵尸进程、PID分配用尽量直白的语言讲明白。适合谁来读正在学Linux系统编程的学生、刚接触Linux运维想补底层知识的人、以及写C/C但一直停留在“调API”层面、没深究过进程本质的开发者。看完这篇文章你至少能做到三件事能读懂最常见的进程创建代码、能在终端里用命令验证进程的父子关系和状态、在面试被问“进程有几种创建方式”时能有理有据地展开。2. 方式一fork()——所有进程创建的“地基”2.1 一个看似简单却暗藏玄机的系统调用fork()是Linux系统里最经典的进程创建方式也是很多初学者第一个接触的“看不懂”的系统调用。它的函数签名极其简单#include sys/types.h #include unistd.h pid_t fork(void);没有参数返回值却有两个。对于调用者来说fork()执行一次却返回两次在父进程中返回子进程的PID在子进程中返回0。如果返回-1说明创建失败常见原因是进程数达到系统上限或者内存不足。我第一次学fork()的时候最大的困惑就是“返回值为什么能有两个”。后来才明白它不是普通的函数调用而是“复制”了整个进程。fork()被调用后内核会创建一个新的进程新进程是调用进程的副本拥有独立的地址空间、独立的文件描述符表、独立的进程控制块task_struct。复制完成之后父进程和子进程各自从fork()的返回点继续往下执行——只不过一个拿到的返回值是子进程的PID一个是0。这里要强调一个关键点子进程不是从main函数重新开始执行的而是从当前代码里fork()调用的下一行开始执行。这个特性导致了无数初学者在写多进程程序时踩坑后面我会专门演示一个例子。2.2 进程表的“复制”机制——写时拷贝早期Unix的fork()是直接把父进程的地址空间完整复制一份给子进程代价非常大。现代Linux用的是**写时拷贝Copy-On-WriteCOW**技术fork()时并不真正复制地址空间而是让父子进程共享同一份物理内存页并且把这些页标记为只读。任何一方尝试写入时内核才真正分配新页并复制内容。用生活化的类比来说fork()不是把一本书复印一份给你而是先给你同一个书架上的同一本书贴上“共享阅读”的标签。只有当你想在书上做笔记、涂改内容的时候系统才临时抽出另一本书重新复印一份保证你改的不影响别人看的。这个设计的好处是显而易见的创建进程的开销被大幅降低因为大多数情况下子进程创建后马上就会去执行别的程序配合exec族函数根本不需要把父进程的内存全部复制一遍。2.3 动手实验写一个最简单的衍生进程程序先来看一段真实的代码我建议你自己动手跑一遍#include stdio.h #include unistd.h #include sys/types.h int main() { printf(父进程开始PID %d\n, getpid()); pid_t pid fork(); if (pid 0) { perror(fork失败); return -1; } else if (pid 0) { // 子进程执行到这里 printf(我是子进程PID %d我的父进程 PID %d\n, getpid(), getppid()); } else { // 父进程执行到这里 printf(我是父进程PID %d我的子进程 PID %d\n, getpid(), pid); } printf(这行代码父进程和子进程都会执行当前 PID %d\n, getpid()); return 0; }编译运行gcc fork_demo.c -o fork_demo ./fork_demo输出效果类似这样实际PID会因为系统不同而不同父进程开始PID 12345 我是父进程PID 12345我的子进程 PID 12346 这行代码父进程和子进程都会执行当前 PID 12345 我是子进程PID 12346我的父进程 PID 12345 这行代码父进程和子进程都会执行当前 PID 12346注意观察两个细节第一最后一行printf执行了两次这正是“fork()返回后父子进程各自继续执行”的体现。这种情况下进程的执行顺序是不确定的谁先谁后取决于内核的调度策略。所以我多次强调多进程编程里绝不能依赖代码书写顺序来推断执行顺序。第二getppid()返回父进程的PID。如果父进程先退出子进程会被initPID为1的进程收养再调用getppid()时看到的就是1。这个现象在写守护进程daemon时比较常见后面我单独讲。2.4 这行代码背后的操作系统工作流程很多人学到这里会问“fork()里到底发生了什么”。我按常见的内核处理逻辑梳理一下用户在调用fork()时触发系统调用陷入内核态。内核检查当前进程的资源限制确认是否允许创建新进程。内核为新进程分配一个进程控制块task_struct并把父进程的task_struct内容大部分复制过去。新进程获得一个新的PID并把自己的父进程指针指向调用fork()的那个进程。内核复制父进程的地址空间映射但物理内存页暂时共享并设置为只读触发写时拷贝机制。子进程被加入系统进程队列等待调度器分配CPU时间。fork()返回两次内核在父进程上下文中返回子进程PID在子进程上下文中返回0。从进程生命周期来看fork()创建的子进程一开始处于“就绪态”获得CPU时间片后进入“运行态”。整个过程非常快现代Linux上做一次fork()通常在毫秒到微秒级别。3. 方式二exec族函数——让新进程“改头换面”3.1 fork解决了“复制”问题但没解决“替换”问题单纯调用fork()只能得到一个父进程的副本。如果你想创建一个完全不同的进程——比如写一个shell用户在终端输入lsshell先fork()出来一个子进程然后子进程马上把自己替换成ls程序——这时候就需要用到exec族函数。exec族函数的作用是在当前位置加载并执行一个新程序替换掉当前进程的正文段、数据段、堆和栈。注意它没有创建新进程而是“变化”了当前进程。PID不变进程的身份没变但做的事情完全变了。所以真实的创建流程通常是fork()exec()搭配使用。先复制一个新的进程出来再在新进程里加载目标程序。这也就是经典shell的工作原理。3.2 exec族六个函数的关系一张表理清楚exec族一共有六个函数它们本质上都调用了同一个系统调用execve区别在于参数的传递方式和程序查找路径的方式不同函数名程序路径格式参数传递方式环境变量来源常见用途execl绝对路径逐个列出参数继承当前环境程序路径固定的场景execv绝对路径参数数组继承当前环境参数动态构建execlp仅文件名按PATH搜索逐个列出参数继承当前环境直接调用系统命令execvp仅文件名按PATH搜索参数数组继承当前环境模拟shell执行命令execle绝对路径逐个列出参数自定义环境变量需要修改环境变量execve绝对路径参数数组自定义环境变量系统调用底层实现记忆技巧函数名里有l的代表参数以列表形式一个一个传有p的代表会用PATH环境变量去寻找程序有e的代表可以自己指定环境变量数组。3.3 实操演示shell子进程如何“接管”外部程序下面这段代码演示了fork()后子进程立即用execlp()替换成外部命令的过程#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { perror(fork失败); return -1; } else if (pid 0) { // 子进程执行这里替换成 ls 程序 printf(子进程即将调用 exec当前 PID %d\n, getpid()); execlp(ls, ls, -l, NULL); // 如果 execlp 成功下面这行代码永远不会执行 perror(execlp执行失败); exit(-1); } else { // 父进程等待子进程结束 int status; waitpid(pid, status, 0); printf(父进程等待结束子进程 PID %d 已退出\n, pid); } return 0; }运行效果会是先打印子进程调exec前的一句话然后输出ls -l的结果最后父进程打印等待结束的信息。这里有一个极重要的概念exec成功时不返回值失败时返回-1。也就是说如果execlp成功了它根本没有机会返回子进程已经被整容成ls了。如果它返回了只意味着失败了。所以每次调用exec之后必须马上检查错误并退出否则子进程会继续执行后面本不该执行的代码。3.4 使用exec时容易犯的三个低级错误第一个坑路径写错。execl(/bin/ls, ls, NULL)如果写成execl(ls, ls, NULL)就会失败因为不带p的函数不会去PATH里搜索必须给绝对路径。同理execlp偶尔也会因为PATH环境变量被改动而找不到程序。第二个坑第一个参数里的程序名和第二个参数的理解。第二个参数是argv[0]也就是程序自己看到的进程名。很多人把第二个参数写成-l当成选项传给ls结果反应不过来为什么程序显示异常。记住第一个参数是为了让程序知道自己被叫做什么不影响实际功能但很多程序会基于argv[0]做不同行为比如busybox。第三个坑忘了处理fork()出来的子进程。如果你的子进程调用了exec后没有做失败处理一旦exec失败子进程会继续执行原本进程的代码导致两个进程干相同的事。排查十分痛苦所以一定要养成exec后面紧跟错误处理和exit的习惯。4. 方式三system()函数——一条路走到黑的“懒人方法”4.1 一个库函数内部却集成了fork、exec、wait前两种方式都是系统调用层面的操作日常开发中其实还有更省事的system()函数。这个函数出自标准C库程序员只要传一个字符串进去系统就会在子shell环境中执行这条命令。#include stdlib.h int system(const char *command);如果你传的是NULLsystem()只会检查shell是否存在存在返回非0。如果传的是命令字符串它会这样工作调用fork()创建一个子进程。子进程中调用execl(/bin/sh, sh, -c, command, NULL)也就是启动一个shell来执行你传进去的命令字符串。父进程用waitpid()等待子进程结束并收集状态。所以system()的本质是“forkexecwait”三件套的组合封装。它要求系统中有/bin/sh才能工作几乎所有Linux发行版都满足这个条件。我在实际项目里看到过很多人在业务代码里直接用system()调外部命令比如system(tar -czf /backup/data.tar.gz /data);这种写法在快速实现原型时确实很爽几行代码就把压缩备份的事干了。但它的安全性和可控性存在明显隐患下面细说。4.2 返回值不是你想的那样system()的返回值有三个层次很多人只看它是不是0这是最常见的误用如果command为NULL返回非0表示shell可用。如果fork()失败返回-1。如果fork()成功但exec失败shell启动失败返回127。否则返回的是shell的退出状态码需要用WEXITSTATUS()这类宏来解析。shell正常执行的命令退出0调用失败返回1命令不存在返回127。所以你写system(some_command)想通过返回值是否为0来判断some_command是否成功在命令本身确实存在且执行成功时没问题但一旦命令写得有语法错误或者程序崩溃你得到的返回值可能跟你想的完全不一样。更好的做法是结合WIFEXITED和WEXITSTATUS宏去解析或者干脆用前两种方式自己控制子进程的退出状态。4.3 什么时候适合用system什么时候千万不能用适合用的场景快速写脚本工具或一次性测试程序不需要对子进程做精细控制。手动构造命令行比直接调用库函数更简单比如调用复杂的命令行组合grep、awk、管道联用。对性能不是特别敏感的运维脚本辅助程序。千万要谨慎的场景对安全性要求高的程序。system()传入的字符串如果包含用户输入极容易造成命令注入。比如用户输入; rm -rf /之类的构造shell会直接解析执行。轻则逻辑出错重则系统被破坏。对性能要求高的程序。system()每次都要启动一个shellshell再去解析执行命令开销比直接用forkexec大不少。需要精细管理进程资源的程序。比如需要设置子进程的CPU亲和性、进程组、信号处理时system()把这些细节全隐藏了你根本控制不了。我的建议写代码时优先考虑forkexec组合只有在快速原型或简单脚本场景才用system()。4.4 实操对比system和forkexec的执行效果下面的代码演示了同一个任务——执行echo命令——分别用system和forkexecvp来实现#include stdio.h #include unistd.h #include sys/wait.h #include stdlib.h int main() { printf( 方式1system() \n); int ret system(echo hello from system); printf(system返回值为%d\n, ret); printf(\n 方式2fork execvp \n); pid_t pid fork(); if (pid 0) { char *argv[] {echo, hello from forkexec, NULL}; execvp(echo, argv); perror(execvp); exit(-1); } else { int status; waitpid(pid, status, 0); if (WIFEXITED(status)) { printf(子进程正常退出退出码%d\n, WEXITSTATUS(status)); } } return 0; }两个方式表面上干了一样的事但system()的工作流程里多了一个shell的参与命令字符串要经过shell解析。想清楚这一层差异你在做进程管理和权限控制时就不容易出偏差。5. 在终端里验证进程创建比你想的简单但也不简单5.1 用ps命令追踪父子进程和进程状态写完代码跑起来怎么能确认进程真的被创建了我平时最常用的就是ps。比如你在终端跑一个会fork()出子进程的程序紧接着用ps -ef --forest看一下进程树ps -ef --forest可以看到那种“树状”的缩进关系父进程在上一层子进程缩进一层。如果想要更细的信息用ps -l看进程的状态列STAT比如R是运行态、S是睡眠态、Z是僵尸态、T是停止态。有时你会碰巧发现某个进程状态是Z这就是僵尸进程。僵尸进程是子进程先退出、父进程还没来得及通过wait()/waitpid()回收其退出状态时进程表里残留的一项。它不占CPU资源却占一个PID无法用kill -9清除。干掉它的唯一方法是让父进程退出然后由init进程接管并回收。5.2 用pstree看进程家族的“族谱”如果觉得ps的森林视图不够直观可以试试pstreepstree -p 12345这个命令会把指定PID的进程以及它的所有子进程以树形图展示出来一目了然。结合我们前面写过的fork示例你运行时能看到当前shell分支出一个新的子进程简直像在看真实的“家族谱系”。这个工具在排查“某个进程是谁拉起来的”“为什么会有这么多个同名的子进程”时特别有用。5.3 通过/proc文件系统查看进程的细节Linux的/proc文件系统是一个虚拟文件系统每个正在运行的进程都对应一个以PID命名的目录。查看某个进程的信息不要依赖top一个工具可以直接钻进底层看cat /proc/12345/status cat /proc/12345/statstatus文件里能看到进程的状态、PPID、UID、内存使用等。这是深入理解进程信息最可靠的手段因为ps和top本质上也是从这个文件系统读数据的。用这种方法学习你会对进程有更有实感的认识——每个进程在系统里真的有一个“文件夹”存在文件里的内容都是实时更新的既直观又底层。5.4 实践中验证“写时拷贝”的最简单方法前面讲了写时拷贝的原理如果你想在实验中看到它可以写一个程序fork()之前定义一个大数组并打印数组的内存地址fork()之后让父子进程分别打印同一个变量的地址。如果不做特殊处理你看到的地址是相同的——这印证了共享物理内存一旦子进程修改这个变量再打印时父进程和子进程看到的值就不同了地址依然相同。地址相同但值不同这正是虚拟内存机制和写时拷贝相互作用的结果。6. 三种方式的核心差异与选型建议6.1 对比表从多个维度看三种方式的取舍对比维度fork()exec族函数system()是否创建新进程是否替换当前进程是内部通过fork创建是否替换程序映像否是是先fork后exec加载shell典型配合关系单独使用或与exec搭配常与fork搭配内部已封装forkexecwait参数传递无参数直接复制进程通过列表或数组传参通过字符串传shell命令安全性取决于后续代码可控性高不易被注入有命令注入风险性能开销低COW优化低高多一次shell启动进程控制能力最基础全部可控全部可控基本不可控只等结束使用场景并发服务、守护进程加载目标程序快速调用Shell命令6.2 同样是创建进程为什么有时候感觉“像”有时候“不像”很多人把这三个概念混在一起其实误解出在“创建”这个词上。fork()创建的新进程和父进程长得一模一样exec严格来说不是创建而是一种“变身”进程还是那个进程但程序被换掉了system()是典型的“复制一个人去干一件新事情”——它先复制当前进程然后在复制品里启动一个全新程序。实际项目中最常见的组合是fork()execvp()——比如写一个简易Shell。先fork()出子进程再让子进程去exec用户输入的命令。而单用fork()的场景通常是需要两个进程协作完成不同任务的场合比如服务端accept()到一个连接后fork()出子进程去处理这个连接父进程继续监听新连接。6.3 选型时我会优先问自己的三个问题第一个问题我要不要精确控制子进程的输入输出、信号、环境变量如果需要用forkexec不要用system()。第二个问题子进程要不要和父进程共享数据如果需要共享并做同步fork()之后在父子进程之间用管道、信号量等IPC机制比较合适exec会清空内存共享数据就别想了。第三个问题这段代码里传入的命令字符串是否包含外部输入包含的话坚决不用system()老老实实用带参数数组的execvp()天然规避命令注入。7. 常见问题与排查技巧实录7.1 子进程变成僵尸进程怎么排查和解决“僵尸进程”这个名字很吓人但真相其实很简单子进程已经执行完毕但它的退出状态还没被父进程取走所以进程表里保留了它的“尸体”。用ps -aux | grep Z看到一堆Z状态的进程就是常见现象。排查思路第一步看代码里有没有调用wait()或waitpid()第二步看父进程是不是一直没有调用这些函数第三步看父进程是不是自己早就退出了——如果父进程退出这些僵尸进程会被init收养并回收所以僵尸进程通常出现在“父进程长期存活却不回收子进程”的场景。解决办法父进程中waitpid()等待子进程结束或者显式给子进程设置信号处理函数捕获SIGCHLD信号后在处理函数里调用waitpid()如果对子进程的结束时间不关心也可以用signal(SIGCHLD, SIG_IGN)忽略掉这个信号内核会自动回收子进程资源这是很多服务端程序的常用做法。7.2 fork()调用失败常见原因是什么fork()返回-1一般有三种原因一是系统的进程数达到上限用cat /proc/sys/kernel/pid_max看最大PID用ps -eLf | wc -l看当前线程数二是当前用户的进程数受限用ulimit -u查看三是内存不足分配新的task_struct失败。排查时先看dmesg有没有“out of memory”的记录再看当前进程数量通常都能定位到具体原因。我在一台低配开发机上经常遇到fork()失败原因就是某个同事写了个无限循环创建子进程的服务把用户进程数限额打满了。这种问题用pkill清理后再看看ulimit -u基本就能恢复。7.3 exec执行成功却没看到预期结果有一种很隐蔽的情况exec执行成功了但你期望的程序输出没出现在屏幕上。这通常是因为程序的输出被重定向了或者环境变量变化导致某些程序行为不同。比如用execle传入了一个精简的环境变量数组环境里缺失LANG、PATH等关键变量很多命令会报错或不输出了。我的经验是除非确实需要定制环境变量否则优先用execlp/execvp这类继承当前环境的函数。需要自定义环境时至少把PATH和HOME带上否则你会在调试这种问题的时候浪费大量时间。顺手也提醒一句exec后没马上fflush()缓冲区的话之前用printf打印的输出可能不会显示因为exec会直接丢弃用户态缓冲区里的内容。这是非常典型的坑——fork后子进程里的printf没带换行符然后立即exec结果那行printf的输出丢了。7.4 system()返回127但命令明明存在前面讲过system()返回127是shell启动失败或命令找不到。但有时候你手动在终端执行同样的命令明明可以用system()调用却返回127常见原因就是代码运行环境里的PATH和shell环境不一致。system()是启动/bin/sh -c去执行它的PATH可能被改得很精简找不到你传进去的命令。解决办法调用system()时传入绝对路径比如system(/bin/ls -l)或者先确认当前环境PATH的值再传给system中的命令使用。因为system()内部会按shell的默认规则处理环境变量尽量保持环境干净可控是最好的做法。7.5 多线程进程里调用fork()需要特别小心现代Linux服务端程序里多线程场景很普遍。如果你在一个多线程程序里调用fork()子进程只会保留调用线程的副本其他线程全部消失。这本身不是大问题但这些线程持有的锁、堆内存状态、文件描述符等资源可能处于不一致状态。子进程如果去操作一个它在“上层线程”里并不拥有完整状态的资源很容易死锁或者数据损坏。安全做法除非你完全清楚后果否则尽量避免在多线程程序里fork()尤其是随后再调用exec之外的非异步信号安全函数。如果必须在多线程环境用forkexec那一套在fork()之后、exec()之前子进程里只能调用异步信号安全函数比如write()绝对不能调用malloc()、printf()这类函数因为它们内部用锁锁的状态可能已经损坏了。8. 写在最后一点踩坑后的个人体会我在刚开始学进程这块时总觉得fork、exec、system这些东西离实际工作很远。后来在负责一个后台服务模块时遇到服务端同时处理大量并发请求需要频繁创建子进程才真正感受到理解这三个方式的差异有多重要。当时因为用了system()去调用外部脚本导致服务响应慢得离谱后来改成forkexecvp整个模块的性能立刻上一个台阶。如果你现在刚接触这些内容建议不要只看概念一定要亲手编译运行一下文中的代码再用ps、pstree、/proc去观察系统状态。把代码里的数字、命令变成屏幕上能看到的进程变化那种“原来如此”的感觉会非常强烈。进程管理是Linux系统编程的中枢地带把这三种创建方式吃透后面再学线程、进程间通信、守护进程、信号处理时你会发现很多知识都是连起来的。多年下来我养成的习惯是每写一个涉及进程的代码跑完都会顺手用ps -ef --forest看一眼进程树是否和我的预期一致。这个习惯帮我排查了大量难以复现的并发问题。今天分享的这三种方式既是基础也是实际开发和运维里最常用的三板斧掌握了它们你在Linux世界里的“自由度”又会高出一截。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

揭开3G开关:如何将32位.NET应用内存上限从2GB提升至4GB 2026/9/29 3:27:37

揭开3G开关:如何将32位.NET应用内存上限从2GB提升至4GB

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
高可靠芯片烧录零缺陷:六大失效路径与全流程管控方法 2026/9/29 3:27:37

高可靠芯片烧录零缺陷:六大失效路径与全流程管控方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
绝区零一条龙(ZenlessZoneZero-OneDragon)通知配置体系全解析:二维配置语义、运行时读取链路与旧版迁移实现 2026/9/29 3:27:31

绝区零一条龙(ZenlessZoneZero-OneDragon)通知配置体系全解析:二维配置语义、运行时读取链路与旧版迁移实现

桌面应用RPA计算机视觉 【免费下载链接】ZenlessZoneZero-OneDragon 绝区零 一条龙 | 全自动 | 自动闪避 | 自动每日 | 自动空洞 | 支持手柄 项目地址: https://gitcode.com/gh_mirrors/ze/ZenlessZoneZero-OneDragon 点击查看 免费下载 本文以 OneDragon 项目&…

阅读更多 →
序列化 与 反序列化 解读 2026/9/29 3:27:31

序列化 与 反序列化 解读

前言:做工具开发的小伙伴,对序列化和反序列化应该很熟悉,几乎每天打交道。本文,我们一起入个门。 目录 一、概念 二、为什么需要序列化 三、代码示例 一、概念 序列化是这样一个过程:将内存中的数据或对象转化为可…

阅读更多 →
axure9.0中“每项加载时”替代方法 2026/9/29 3:27:31

axure9.0中“每项加载时”替代方法

方案一:使用“添加筛选”“移除筛选”(推荐,最简单)利用“添加筛选”和“移除筛选”来强制中继器重新计算,从而触发“每项加载时”事件。操作步骤:在“上一步”按钮的“鼠标单击时”用例中,在“…

阅读更多 →
phpcms v9 后端全新升级,支持php8.3 2026/9/29 3:27:31

phpcms v9 后端全新升级,支持php8.3

phpcms v9 全新升级,支持Apache/2.4.37 PHP/8.3.31,界面也做全面升级,前端代码跟以前一样的方法调用,全面支持目录伪静态

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉