尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

ShellLab实验指南:从零实现Unix Shell,掌握进程控制与信号处理

ShellLab实验指南:从零实现Unix Shell,掌握进程控制与信号处理
📅 发布时间:2026/8/3 23:21:36

1. 实验背景与核心价值:为什么每个CS学生都应该亲手做一次ShellLab?

如果你是一名计算机科学或相关专业的学生,或者是一位对操作系统底层交互感兴趣的开发者,那么“ShellLab”这个名字你一定不陌生。它通常是《计算机组成原理》或《深入理解计算机系统》(CS:APP)课程中一个标志性的实验项目。这个实验的核心任务,是让你亲手实现一个简化版的Unix Shell(命令行解释器),比如tsh(Tiny Shell)。

很多人初看这个实验,会觉得:“不就是写个能跑命令的程序吗?有什么难的?” 或者认为这只是一个简单的字符串解析练习。但当你真正沉下心来,一行代码一行代码地去构建它时,你会发现,这个看似简单的“外壳”,实际上是你窥探操作系统核心机制——进程控制——的一扇绝佳窗口。它绝不是一个玩具,而是一个将书本上抽象的“进程”、“作业控制”、“信号”、“I/O重定向”等概念,转化为指尖可感、可调试的实体的练兵场。

我当年做这个实验时,花了整整一周时间与各种诡异的bug作斗争,但正是这个过程,让我对“一个命令是如何从输入到执行完毕”的完整生命周期有了刻骨铭心的理解。这种理解,是单纯阅读教材或听讲座无法获得的。ShellLab的价值在于,它强迫你去思考:当你按下回车键后,系统底层究竟发生了什么?父进程如何创建子进程?子进程如何被前台或后台执行?Shell如何知道一个后台作业何时结束?信号如何像中断一样,优雅或粗暴地打断正在运行的进程?

通过实现一个功能完整的Shell,你将系统性地实践以下核心知识点:

  • 进程的创建与执行:深入理解fork()、exec()系列函数、waitpid()的用法和区别。
  • 作业控制:实现前台、后台作业的调度与管理,理解进程组(Process Group)和会话(Session)的概念。
  • 信号处理:编写稳健的信号处理程序(Signal Handler),处理SIGINT(Ctrl+C)、SIGTSTP(Ctrl+Z)、SIGCHLD(子进程终止或停止)等信号,这是理解异步事件处理的基石。
  • I/O重定向:实现<、>、>>等操作,理解文件描述符(File Descriptor)的复制与重定向(dup2)。
  • 管道:实现|操作,理解进程间通信(IPC)最基本的形式。

可以说,成功完成ShellLab,意味着你已经打通了操作系统入门学习的“任督二脉”。接下来,我们将从零开始,一步步拆解这个实验的完整实现路径与核心陷阱。

2. 实验环境准备与基础框架解析

在开始编码之前,搭建一个稳定、可复现的调试环境至关重要。很多同学实验做不下去,一半的原因在于环境配置混乱或对基础代码框架理解不透。

2.1 实验环境搭建要点

通常,ShellLab实验会提供一个初始的代码包(如tsh.c),里面包含了一个极其简陋的、只能执行内置命令quit的Shell框架。你的任务就是填充这个框架。

1. 操作系统选择:强烈推荐使用Linux原生环境或macOS(其终端本质也是Unix Shell)。Windows用户务必使用WSL2(Windows Subsystem for Linux),它提供了一个近乎原生的Linux内核环境,能完美支持实验所需的所有系统调用和信号行为。纯Windows下的MinGW或Cygwin环境在信号处理和进程控制上可能与标准Unix存在细微差异,容易引入难以排查的幽灵bug。

2. 编译器与调试器:使用gcc编译,并务必加上-g选项生成调试信息(如gcc -g -o tsh tsh.c)。调试将是本实验的主旋律,gdb是你的最佳伙伴。学会使用gdb的断点、单步执行、查看变量、附着到进程(attach)等功能,尤其是调试多进程和信号处理时,gdb不可或缺。

3. 参考工具:实验通常会提供一个参考Shell可执行文件,比如tshref。你的Shell输出行为(尤其是作业列表的格式、错误信息)必须与参考实现完全一致,这是评分的关键。一个实用的技巧是:编写一个自动化测试脚本,同时向你的tsh和tshref输入相同的命令序列,然后对比两者的输出。这能极大提高自测效率。

2.2 理解基础代码框架

拿到tsh.c后,不要急于写代码。先花半小时通读一遍,理解它的骨架。一个典型的框架包含以下部分:

/* 全局变量 */ char prompt[] = “tsh> “; // 提示符 int verbose = 0; // 调试模式开关 /* 作业(Job)链表:用于跟踪所有由本shell启动的进程 */ struct job_t { pid_t pid; // 进程ID int jid; // 作业ID(Shell内部分配,从1开始) int state; // 状态:前台运行(FG)、后台运行(BG)、已终止(TERMINATED)、已停止(STOPPED) char cmdline[MAXLINE]; // 命令行字符串 struct job_t *next; }; struct job_t *job_list = NULL; /* 函数原型 */ void eval(char *cmdline); // 核心:解析并执行命令行 int builtin_cmd(char **argv); // 判断并执行内置命令(如quit, jobs, fg, bg) void do_bgfg(char **argv); // 实现fg和bg命令 void waitfg(pid_t pid); // 等待前台作业结束 void sigchld_handler(int sig); // SIGCHLD信号处理函数 void sigint_handler(int sig); // SIGINT (Ctrl+C) 处理函数 void sigtstp_handler(int sig); // SIGTSTP (Ctrl+Z) 处理函数 /* 作业链表操作辅助函数 */ void addjob(struct job_t *job_list, pid_t pid, int state, char *cmdline); void deletejob(struct job_t *job_list, pid_t pid); struct job_t *getjobpid(struct job_t *job_list, pid_t pid); struct job_t *getjobjid(struct job_t *job_list, int jid); int pid2jid(pid_t pid); void listjobs(struct job_t *job_list); /* Main函数 */ int main(int argc, char **argv) { // 解析参数,设置信号处理函数,进入主循环 while (1) { // 打印提示符,读取用户输入(使用fgets) // 调用eval函数处理输入 } return 0; }

你的主战场就是填充eval、builtin_cmd、do_bgfg、waitfg和三个信号处理函数。框架已经为你定义了数据结构(作业链表)和辅助函数,你的任务是让它们协同工作。

注意:信号处理函数的安装(使用sigaction而非简单的signal)必须在main函数初期完成,并且要设置SA_RESTART标志吗?这是一个需要仔细斟酌的细节。对于Shell,像read这样的慢速系统调用在等待终端输入时,如果被信号中断,我们通常希望它不要自动重启,因为信号(如Ctrl+C)本身就是要中断当前操作的。这一点后面会详细讨论。

3. 核心引擎:eval函数的实现逻辑与并发陷阱

eval函数是Shell的心脏。它接收一整行命令字符串,负责解析、判断、创建进程并执行。其逻辑流程图看似简单,但每一步都暗藏玄机。

3.1 基本执行流分解

  1. 解析命令行:使用strtok或更安全的strsep按空格分割命令行,得到参数数组argv。需要特别处理&(后台执行)、<、>、>>、|等特殊元字符。第一步就要判断命令是否以&结尾,这决定了作业是前台还是后台启动。

  2. 判断内置命令:调用builtin_cmd(argv)。如果是quit、jobs、fg、bg等内置命令,直接在该函数内处理并返回1,eval也随之返回。否则返回0,继续执行外部命令。

  3. 屏蔽信号(关键!):在创建子进程之前,必须使用sigprocmask屏蔽SIGCHLD信号。这是整个实验最易出错、也最重要的并发控制点。

    • 为什么?考虑以下时序:父进程(Shell)调用fork()创建子进程后,在将子进程添加到作业链表(addjob)之前,子进程可能已经执行完毕并终止,立刻向父进程发送SIGCHLD信号。如果此时SIGCHLD处理函数sigchld_handler被调用,它会尝试从作业链表中删除这个子进程(deletejob)。但此时父进程的addjob还没有执行!这就导致deletejob失败(找不到该PID的作业),而随后addjob又将一个已经终止的进程加入链表。从此,你的作业链表里就多了一个“僵尸”条目,永远无法被清理。
    • 正确做法:在fork()前屏蔽SIGCHLD,在addjob完成后(并且,如果是前台作业,在调用waitfg之前)再解除屏蔽。这样可以确保addjob和deletejob这两个对共享数据结构(作业链表)的访问是原子的,不会被打断。
  4. 创建子进程:调用fork()。

    • 在子进程中: a.恢复信号掩码:子进程继承了父进程被屏蔽的信号,需要先用sigprocmask解除对所有信号的屏蔽,恢复默认处理方式。否则子进程可能无法响应如SIGINT等信号。 b.设置进程组:调用setpgid(0, 0),将子进程放入一个新的进程组。这一步对于作业控制至关重要,它使得Shell能够向整个作业(可能是一个管道命令序列)发送信号。 c.处理I/O重定向和管道:在调用execvp之前,根据解析出的元字符,使用open、dup2等系统调用重定向标准输入、输出。如果是管道,则需要创建管道(pipe),并正确连接前后命令的输入输出。 d.执行程序:调用execvp(argv[0], argv)。如果执行失败,应打印错误信息并调用exit(EXIT_FAILURE)终止子进程。
    • 在父进程中: a.添加作业到链表:根据是否是后台作业(&),调用addjob将子进程PID添加到作业链表,状态设为BG(后台)或FG(前台)。 b.解除信号屏蔽:调用sigprocmask恢复之前的信号掩码。 c.前台作业等待:如果是前台作业,调用waitfg(pid)函数,该函数会循环等待,直到这个特定的前台作业不再是前台状态(被终止或停止)。注意:waitfg内部不能使用waitpid,而应该用一个基于global variable或作业状态的忙等待或sigsuspend。因为waitpid会回收任意一个已终止的子进程,而我们需要等待的是特定的前台作业。 d.后台作业通知:如果是后台作业,则打印类似[1] 12345的提示信息,表示作业ID和进程ID已加入后台运行。

3.2 eval函数伪代码示例

void eval(char *cmdline) { char *argv[MAXARGS]; // 参数数组 char buf[MAXLINE]; // 命令行副本 int bg; // 是否为后台作业 pid_t pid; sigset_t mask_all, mask_one, prev_one; strcpy(buf, cmdline); bg = parseline(buf, argv); // parseline是框架提供的函数,解析argv并返回是否为后台 if (argv[0] == NULL) return; // 空行 if (!builtin_cmd(argv)) { // 不是内置命令 // 1. 设置信号屏蔽 sigfillset(&mask_all); sigemptyset(&mask_one); sigaddset(&mask_one, SIGCHLD); sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // 阻塞SIGCHLD // 2. 创建子进程 if ((pid = fork()) == 0) { // 子进程 sigprocmask(SIG_SETMASK, &prev_one, NULL); // 恢复信号掩码 setpgid(0, 0); // 设置新的进程组 // 处理重定向和管道(如果有) if (handle_redirection(argv) < 0) exit(EXIT_FAILURE); if (execvp(argv[0], argv) < 0) { printf(“%s: Command not found\n”, argv[0]); exit(EXIT_FAILURE); } } // 父进程 // 3. 添加作业(仍在SIGCHLD阻塞中) int state = bg ? BG : FG; addjob(job_list, pid, state, cmdline); // 4. 解除SIGCHLD阻塞 sigprocmask(SIG_SETMASK, &prev_one, NULL); // 5. 等待前台作业或打印后台作业信息 if (!bg) { waitfg(pid); } else { printf(“[%d] %d %s”, pid2jid(pid), pid, cmdline); } } return; }

4. 信号处理:异步世界的秩序维护者

信号处理是ShellLab的难点和精华所在。信号是异步的,可能在任何时刻打断主程序的执行流。处理不当,轻则输出混乱,重则导致作业链表状态不一致,甚至死锁。

4.1 必须处理的三种信号

  1. SIGCHLD - 子进程状态变更:当子进程终止(exit)或停止(收到SIGTSTP等信号)时,内核会向父进程发送此信号。它的处理函数sigchld_handler是Shell的“清洁工”,负责回收僵尸进程、更新作业状态。
  2. SIGINT - 中断信号:通常由Ctrl+C产生。Shell本身应该忽略它(因为Ctrl+C是发给前台进程组的),但Shell需要将此信号转发给当前的前台作业进程组。
  3. SIGTSTP - 停止信号:通常由Ctrl+Z产生。同样,Shell本身忽略,但需要转发给前台作业进程组,使其停止运行。

4.2 稳健的SIGCHLD处理函数实现

sigchld_handler的逻辑必须非常严谨,因为它会在异步环境下操作全局作业链表。

void sigchld_handler(int sig) { int old_errno = errno; // 保存errno,防止被信号处理函数修改 pid_t pid; int status; // 使用WNOHANG | WUNTRACED循环回收所有状态已改变的子进程 while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { if (WIFEXITED(status)) { // 子进程正常退出 deletejob(job_list, pid); } else if (WIFSIGNALED(status)) { // 子进程被信号终止 printf(“Job [%d] (%d) terminated by signal %d\n”, pid2jid(pid), pid, WTERMSIG(status)); deletejob(job_list, pid); } else if (WIFSTOPPED(status)) { // 子进程被信号停止 printf(“Job [%d] (%d) stopped by signal %d\n”, pid2jid(pid), pid, WSTOPSIG(status)); struct job_t *job = getjobpid(job_list, pid); if (job) job->state = ST; // 更新状态为STOPPED } // 注意:WIFCONTINUED 在本实验通常不需要处理 } if (pid < 0 && errno != ECHILD) { // 错误处理,但ECHILD(没有子进程)是正常的 unix_error(“waitpid error”); } errno = old_errno; // 恢复errno return; }

关键点解析:

  • 使用while循环和WNOHANG:因为信号不排队,如果同时有多个子进程结束,可能只收到一个SIGCHLD信号。必须用while循环配合waitpid(..., WNOHANG, ...)来回收所有已终止的子进程,避免僵尸进程堆积。
  • 使用WUNTRACED:这个选项使得waitpid也能返回那些被停止(STOPPED,如Ctrl+Z)的子进程信息,这对于更新作业状态至关重要。
  • 区分退出原因:WIFEXITED、WIFSIGNALED、WIFSTOPPED宏用于判断子进程状态变化的原因,并采取相应动作(删除作业或更新状态)。
  • 保存和恢复errno:信号处理函数中可能会调用修改errno的函数(如printf),而errno是全局变量。如果不保存恢复,可能会破坏主程序中依赖errno的逻辑。

4.3 SIGINT和SIGTSTP处理函数:信号的转发

这两个处理函数的逻辑类似:它们不是用来处理Shell自身收到的信号,而是当Shell收到这些信号时,应该将其转发给当前的前台作业进程组。

void sigint_handler(int sig) { pid_t fg_pid = fgpid(job_list); // 需要实现fgpid,返回当前前台作业的PID if (fg_pid > 0) { kill(-fg_pid, SIGINT); // 向整个前台进程组发送SIGINT // 注意是 -fg_pid,负的PID表示向整个进程组发送信号 } return; } void sigtstp_handler(int sig) { pid_t fg_pid = fgpid(job_list); if (fg_pid > 0) { kill(-fg_pid, SIGTSTP); // 向整个前台进程组发送SIGTSTP } return; }

重要细节:在main函数中安装这些处理函数时,应使用sigaction并不设置SA_RESTART标志。因为对于Shell,当它在read系统调用中等待用户输入时,如果用户按下Ctrl+C,我们期望read被中断并返回错误,从而让Shell有机会重新打印提示符并等待下一个命令。如果设置了SA_RESTART,read会自动重启,导致Shell无法及时响应中断。

5. 作业控制命令:fg、bg与jobs的实现

内置命令jobs、fg、bg是用户与Shell管理的作业进行交互的接口。

5.1 jobs命令

这个最简单,直接调用框架提供的listjobs函数,打印出作业链表中的所有非“已终止”作业,显示它们的JID、状态(运行中/已停止)、命令行等。

5.2 fg和bg命令的实现

do_bgfg函数处理fg %1或bg %2这样的命令。其核心逻辑是:

  1. 参数解析:判断参数是以%开头的作业ID(JID)还是直接的进程ID(PID)。
  2. 查找作业:根据JID或PID,调用getjobjid或getjobpid从作业链表中找到对应的作业结构体。
  3. 发送信号:
    • 对于bg命令:向该作业的进程组发送SIGCONT信号(kill(-job->pid, SIGCONT)),将其状态从STOPPED改为BG,并打印提示信息。
    • 对于fg命令:同样先发送SIGCONT信号(如果它是停止的),然后将其状态改为FG,并调用waitfg(job->pid),等待这个作业重新变成前台作业并运行直至结束(或再次停止)。

一个关键并发问题:在do_bgfg中发送SIGCONT信号后,该作业的进程会继续执行,并可能很快终止,触发SIGCHLD处理程序。SIGCHLD处理程序可能会在do_bgfg更新作业状态为FG并调用waitfg之前,就删除了该作业。这会导致waitfg等待一个不存在的PID,或者访问已释放的内存。

解决方案:这又回到了共享数据(作业链表)的同步问题。一种常见的做法是,在do_bgfg中,在发送信号和修改作业状态/调用waitfg的这段代码前后,也使用sigprocmask屏蔽SIGCHLD信号,形成一个临界区,确保操作的原子性。这与在eval中屏蔽信号的初衷是一致的。

6. 高级功能与边界条件处理

在实现了基本功能后,你的Shell还需要正确处理一些边界情况和高级功能,才能算得上健壮。

6.1 I/O重定向的实现

在子进程的代码块中,在调用execvp之前,需要扫描argv,处理<、>、>>。

  • 找到<,则其后的参数是输入文件。用open打开该文件(只读),并用dup2(fd, STDIN_FILENO)将标准输入重定向到这个文件描述符,然后关闭原文件描述符。
  • 找到>或>>,则其后的参数是输出文件。用open打开(O_WRONLY|O_CREAT|O_TRUNC或O_WRONLY|O_CREAT|O_APPEND),并用dup2(fd, STDOUT_FILENO)重定向标准输出。
  • 重要:重定向符号和文件名本身应该从argv中移除,避免被当作参数传递给要执行的程序。通常的做法是,在解析时将它们设为NULL,这样execvp遇到NULL就会停止。

6.2 管道的实现

管道(cmd1 | cmd2)是ShellLab的一个常见扩展要求。实现它需要更复杂的进程管理和文件描述符操作。

  1. 解析命令行,找到|的位置,将命令分割成两部分(或更多)。
  2. 调用pipe(p)创建管道,得到读端p[0]和写端p[1]。
  3. fork()第一个子进程(执行cmd1):
    • 关闭管道的读端p[0]。
    • 使用dup2(p[1], STDOUT_FILENO)将标准输出重定向到管道的写端。
    • 关闭p[1](重定向后,原描述符应关闭)。
    • 执行cmd1。
  4. fork()第二个子进程(执行cmd2):
    • 关闭管道的写端p[1]。
    • 使用dup2(p[0], STDIN_FILENO)将标准输入重定向到管道的读端。
    • 关闭p[0]。
    • 执行cmd2。
  5. 在父进程中,必须关闭管道两个端点的所有描述符(close(p[0]); close(p[1]);),否则管道的读端不会收到EOF。
  6. 父进程需要等待两个子进程(或整个进程组)结束。这需要更精细的作业管理和waitfg逻辑。

6.3 内存管理与错误处理

  • 僵尸进程防御:确保sigchld_handler在任何情况下都能正确回收所有终止的子进程。使用while循环和WNOHANG是关键。
  • 作业链表一致性:任何对全局作业链表的修改(addjob,deletejob, 修改状态)都必须考虑与信号处理函数的并发访问。合理使用信号屏蔽是保证一致性的主要手段。
  • 系统调用错误检查:对fork,execvp,waitpid,kill,dup2,open等所有系统调用进行错误检查,并输出有意义的错误信息。
  • 内存泄漏:虽然实验规模小,但良好的习惯是:在deletejob中不仅要移除链表节点,还应释放为该节点分配的堆内存(如果框架是动态分配的)。

7. 调试技巧与常见“巨坑”盘点

即使逻辑完全正确,第一次运行也几乎必然充满bug。以下是我在调试ShellLab时积累的血泪经验。

1. 使用strace和ps命令:

  • strace -f ./tsh:可以跟踪Shell及其所有子进程的系统调用,非常有助于理解进程创建、信号传递、文件描述符操作的实际过程。
  • 在另一个终端运行ps a -o pid,pgid,state,cmd,可以实时查看所有进程的PID、进程组ID、状态和命令,帮助你确认作业是否被正确添加到进程组,状态是否正确。

2. 处理“进程仍在运行”的错觉:有时你会发现,一个前台作业结束后,Shell没有打印新的提示符,好像卡住了。这通常是因为waitfg函数的实现有问题。waitfg应该循环检查特定前台作业的PID是否还在作业链表中且状态为FG。它不能调用waitpid,因为waitpid会阻塞并回收任意一个子进程,可能回收的是其他后台作业,导致waitfg永远等不到它想等的那个前台作业结束(因为那个作业已经被sigchld_handler回收了)。正确的waitfg应该是一个忙等待或使用sigsuspend的循环。

3. 信号处理函数中不要调用不可重入函数:例如,避免在信号处理函数中调用printf、malloc等。虽然在本实验的简单场景下使用printf问题不大(因为我们的处理逻辑很短,且主要向终端输出),但在严谨的编程中,这可能导致不可预知的行为。更安全的做法是,在信号处理函数中只设置一个全局的volatile sig_atomic_t标志,在主循环中检查这个标志并执行相应的打印或逻辑。不过,实验框架通常允许在handler中使用printf。

4. 关于SA_RESTART的纠结:如前面所述,在安装SIGINT和SIGTSTP的handler时,不要设置SA_RESTART。但对于SIGCHLD,设置SA_RESTART通常是安全的,甚至是有益的,可以避免某些慢速系统调用(如read在某些情况下)被SIGCHLD意外中断。但这需要结合你的具体实现和测试来判断。

5. 测试用例的覆盖:不要只测试简单的ls、sleep命令。构造复杂的测试场景:

  • 连续快速启动多个后台作业,测试sigchld_handler的回收能力。
  • 在前台作业运行中按Ctrl+Z停止它,然后用bg使其后台继续,再用fg调回前台。
  • 测试fg一个不存在的JID/PID,或fg一个已经是前台的作业,你的Shell应该给出清晰的错误信息。
  • 测试管道和重定向的组合:ls -l | grep “.c” > output.txt。
  • 测试有问题的命令,如执行一个不存在的程序,Shell应报告“Command not found”然后继续运行。

完成ShellLab的过程,就像在精心搭建一个多米诺骨牌阵,任何一个环节的微小失误都可能导致整个系统行为异常。但当你最终看到自己编写的Shell能够像bash一样稳健地处理前台、后台作业,响应各种信号时,那种成就感是无与伦比的。这不仅仅是一个课程实验,更是一次对计算机系统底层交互机制的深刻洗礼。我强烈建议你在实现基本功能后,尝试挑战管道和重定向,那会让你对进程间通信和文件描述符的理解再上一个台阶。

相关新闻

  • QQ截图独立版:免费开源的全能屏幕工具完整指南
  • 差分隐私 vs 同态加密 vs 安全多方计算:AI企业数据合规选型决策树,附3家头部公司落地ROI对比表
  • 探索GPLGPU的3D渲染管线:从三角形光栅化到纹理映射全解析

最新新闻

  • 2026 年邳州优秀的乳胶护脊床垫订做厂家哪家专业,睡了十年塌腰腰,直到碰了它,才懂好床垫要护脊还不闷汗 - 企业推荐管【认证】
  • 吕梁除甲醛公司甲醛检测推荐选择:康之居除甲醛标准、流程、避坑指南 - CMA甲醛检测中心
  • DLSS Swapper完整指南:5步解决游戏画质与性能的终极方案
  • 3分钟搭建你的专属象棋AI教练:告别手动输入,拥抱智能对弈新时代
  • 2026年新西兰本科留学机构推荐榜单:奥克兰名校/预科留学院校/国际留学预科辅导深度解析,专科高中预科留学精选指南 - 优企名品
  • 咖啡店选什么咖啡豆?这几款前街咖啡豆,出杯稳定、回头客多,闭眼入! - 咖评官方推荐

日新闻

  • 5分钟快速搭建智能数字人:Live2D虚拟形象终极部署指南
  • 告别繁简字幕转换烦恼:这款开源工具让你一键搞定影视字幕处理 [特殊字符]
  • GPT-5.4传闻背后:大模型永久记忆与极限推理的技术演进与挑战

周新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号