拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从信号到系统调用:School of SRE 中 SRE 必备的 Linux 进程生命周期与排障指南

教程【免费下载链接】school-of-sreAt LinkedIn, we are using this curriculum for onboarding our entry-level talents into the SRE role.项目地址https://gitcode.com/gh_mirrors/sc/school-of-sre点击查看免费下载本文是 LinkedIn「School of SRE」课程体系 Level 102「System Calls and Signals系统调用与信号」模块的总结性技术指南。它以该模块的 conclusion.md 为主线深度融合 signals.md 与 system_calls.md 两个正文章节的全部核心内容系统讲解信号在进程生命周期中的作用、系统调用的工作原理以及如何借助strace定位慢速或挂起的进程。读完本文你将掌握信号分类、信号处理、用户态/内核态切换、write()等核心系统调用以及生产级排障中strace的正确打开方式。为什么 SRE 需要理解信号与系统调用SRE站点可靠性工程师的核心目标之一是提升大规模系统的可靠性。而要做到这一点首先需要对系统的内部工作机制有一个基本层面的理解——这正是本模块见 intro.md存在的意义它不讨论除信号之外的其他中断处理细节也不深入 GNU C 库源码而是聚焦于两个对 SRE 日常工作直接有用的主题信号Signals信号在进程的生命周期中扮演着重要角色。从创建进程到杀死进程几乎每一步操作都离不开信号。对信号的理解尤为关键——如果你能预见到某个会触发信号的事件就可以提前定义一个处理函数handler并告诉操作系统在特定类型的信号到达时执行它。系统调用System Calls系统调用为程序员提供了操作系统内部功能的精确视图也让开发者能深入理解底层实现系统调用的 C 库函数。借助strace命令可以轻松调试慢速slow或挂起hung的 Linux 进程。本模块的完整编排可以在 mkdocs.yml 中看到Introduction → Signals → System Calls → Conclusion它属于 Level 102「Linux Advanced」课程线。按照 intro.md 的说明学习本模块前建议先掌握 Level 101 的 Linux 基础 和 Python 基础——后面的信号处理示例正是用 Python 演示的。信号进程生命周期中的关键角色信号本质上是软件中断中断interrupt是改变程序正常执行流程的事件可由硬件设备或 CPU 自身产生。中断发生时当前执行流被挂起中断处理程序运行运行完毕后恢复之前的执行流。能触发 CPU 中断的事件有三类硬件中断hardware interrupts、软件中断software interrupts和异常exceptions。信号signal就是软件中断它通知某个进程「某个事件已经发生」——这些事件可能是用户的请求如按 CtrlC也可能是系统问题的指示如内存访问错误。每个信号都有唯一的信号编号和一个默认动作default action。进程收到信号后可以以下列三种方式之一做出反应采用操作系统提供的默认方式处理**捕获catch**信号用程序自定义的方式处理完全**忽略ignore**该信号。标准信号与实时信号信号大体分为两大类详见 signals.md标准信号standard signals即传统信号由内核用来向进程通知事件在 Linux 上编号为 1 到 31。实时信号realtime signals由 POSIX.1b 定义用来弥补标准信号的若干局限。相比标准信号实时信号有以下优势信号数量更多标准信号中只有SIGUSR1和SIGUSR2两个可自由用于应用自定义用途而实时信号提供了更大的应用自定义空间支持排队向进程多次发送同一个实时信号信号会被多次投递而标准信号若在进程上已经处于 pending 状态再次发送只会被投递一次可携带数据发送实时信号时可以附带数据一个整数值或指针值接收进程的信号处理器可以取回这些数据投递顺序有保证多个不同类型的实时信号处于 pending 时编号最小的先被投递多个同类型实时信号排队时按发送顺序连同附带数据依次投递。常用信号速查表在 Linux 系统中可以用kill -l命令列出所有可用信号。下表列出了信号 1 到 20 的名称、编号、默认动作与含义这也是日常运维中最常打交道的部分信号名称信号编号默认动作含义SIGHUP1Terminate在控制终端上检测到挂起或控制进程死亡SIGINT2Terminate来自键盘的中断SIGQUIT3Core dump来自键盘的退出SIGILL4Core dump非法指令SIGTRAP5Core dump用于调试的跟踪/断点陷阱SIGABRT / SIGIOT6Core dump异常终止SIGBUS7Core dump总线错误SIGFPE8Core dump浮点异常SIGKILL9Terminate终止信号无法被捕获或忽略SIGUSR110Terminate用户自定义信号 1SIGSEGV11Core dump无效内存引用SIGUSR212Terminate用户自定义信号 2SIGPIPE13Terminate管道破裂向没有读取端的管道写入SIGALRM14Terminate来自alarm()的定时器信号SIGTERM15Terminate进程终止默认优雅退出信号SIGSTKFLT16Terminate数学协处理器上的栈故障SIGCHLD17Ignore子进程停止或终止SIGCONT18Continue若进程被停止则继续执行SIGSTOP19Stop停止进程无法被捕获或忽略SIGTSTP20Stop在 tty 上发出的停止信号如 CtrlZ信号的生成、挂起与投递一个信号由某个事件生成generated之后被**投递delivered到进程进程再做出相应动作。从生成到投递之间的这段时间里信号处于挂起pending**状态。通常情况下挂起的信号会在进程下一次被调度运行时立即投递如果进程正在运行例如进程给自己发送信号则立即投递。但有时需要保证某段代码不被信号投递打断——此时可以把信号加入进程的信号掩码signal mask即一组「投递当前被阻塞」的信号集合。若信号在阻塞期间被生成它会一直保持 pending直到它被从掩码中移除解除阻塞为止。内核提供多种系统调用让进程向掩码中添加或移除信号。信号投递到进程后根据信号的不同进程会执行以下 5 种默认动作之一忽略信号被内核丢弃对进程毫无影响进程甚至不知道它发生过终止进程即异常终止区别于进程通过exit()正常终止生成 core dump 文件并终止进程core dump 文件包含进程虚拟内存的映像可加载到调试器中检查进程终止时的状态停止进程挂起进程的执行恢复执行在进程先前被停止后恢复其执行。为进程设置信号处置disposition除了接受默认动作程序还可以改变信号投递时的行为这称为设置信号的处置disposition。程序可以为某个信号设置以下三种处置之一执行默认动作用于撤销此前把处置改为其他内容的操作忽略信号对默认动作会终止进程的信号尤其有用执行信号处理函数由程序员编写、在信号投递时执行相应任务的函数。例如shell 为SIGINT由中断字符 CtrlC 生成设置了处理器收到后停止当前工作并返回主输入循环让用户重新看到 shell 提示符。把处理函数通知内核的行为通常称为安装installing或建立establishing信号处理器处理器被调用时我们就说该信号已被处理handled或同义地称为捕获caught。需要特别说明的是无法把某个信号的处置直接设置为「终止并转储 core」除非该信号的默认动作就是如此。最接近的做法是安装一个处理器让它在信号到达时调用exit()或abort()——abort()会为进程生成SIGABRT信号使其转储 core 并终止。向进程发送信号的三种方式信号可以通过以下三种途径发送到进程使用kill命令默认发送SIGTERM也可以指定信号编号或名称发送其他信号。例如kill -9 367向 PID 为 367 的进程发送SIGKILL。通过键盘按特定按键即可向运行中的进程发送信号。例如按住 CtrlC 会向进程发送SIGINT从而终止它。通过另一个进程进程可以借助kill()系统调用向另一个进程发送信号。此时信号可以用作同步手段甚至作为一种原始的进程间通信IPC方式进程也可以向自己发送信号。int kill(pid_t pid, int sig)接收两个参数目标进程的pid和期望发送的信号编号。实战用 Python 处理 SIGINT下面通过 Python 的signal库来对比「默认动作」与「自定义处理器」的差异。默认动作下的 SIGINT在 Python 环境中运行一个死循环然后按 CtrlCwhile 1: continue按下 CtrlC 后SIGINT被发送给进程其默认动作是终止进程。控制台输出^CTraceback (most recent call last): File stdin, line 2, in module KeyboardInterrupt进程因收到SIGINT键盘中断而按默认动作终止。自定义信号处理器运行下面的代码为SIGINT安装自定义处理器import signal import sys # Start of signal_handler function def signal_handler(signal, frame): print (You pressed CtrlC!) # End of signal_handler function signal.signal(signal.SIGINT, signal_handler)这是为SIGINT定义自定义信号处理器、覆盖默认动作的典型示例。此时再运行之前的死循环while 1: continue每次按 CtrlC 时输出如下程序并不会终止^CYou pressed CtrlC!也就是说我们通过signal.signal()把SIGINT的默认行为终止进程覆盖为「打印提示信息后继续运行」。若要退出程序可以按 CtrlZ——它发送SIGSTOP信号默认动作是停止进程。这个示例的关键点在于处理函数signal_handler()被调用时带有两个参数信号编号和当前的栈帧对象frame object可能是Nonesignal.signal()的两个参数分别是要捕获的信号编号或名称与信号处理函数的名称它允许为信号定义自定义处理器。信号与系统调用的联动wait() 与 SIGCHLD信号并非孤立存在它与系统调用之间存在着紧密的配合。wait()系统调用是最佳例证——它等待调用进程的某个子进程终止并把该子进程的终止状态写入statusPtr指向的缓冲区。整个流程如下父进程调用wait()系统调用后其执行被挂起直到子进程终止子进程终止时内核生成SIGCHLD信号并投递给父进程——SIGCHLD告诉父进程「子进程有需要收集的信息」父进程收到SIGCHLD后从进程表中**回收reap**子进程的退出状态。注意即使子进程已经终止进程表中仍保留着对应条目存储着进程条目和 PID父进程收集完状态后该条目被删除子进程在系统中留下的全部痕迹被清除。僵尸进程Zombie State如果父进程决定不等待子进程终止而是继续执行后续任务或者读取子进程退出状态失败那么子进程终止后进程表中仍会残留条目——这种状态就是僵尸进程。为避免僵尸进程长期存在需要在创建子进程后编写调用wait()的代码。实践中通常为SIGCHLD信号创建处理器在一个循环中反复调用 wait 系列函数直到没有未收集的子进程数据为止。孤儿进程Orphan State如果父进程先于子进程终止子进程就成为孤儿进程。孤儿进程会被init/systemd收养——它是所有进程的祖先进程 ID 为 1。此后对该进程父 PID 的查询都会返回 1。系统调用进入内核的受控入口什么是系统调用系统调用system call是进入内核的受控入口它允许进程请求内核代表自己执行某个操作。内核通过系统调用应用编程接口API向程序开放一系列服务——例如创建新进程、执行 I/O、创建用于进程间通信的管道等。应用开发者通常不能直接访问系统调用而是通过这个 API 访问它们。系统调用集合是固定的每个系统调用由唯一的编号标识。发起系统调用时处理器状态会从用户态user mode切换到内核态kernel mode这样 CPU 才能访问受保护的内核内存。每个系统调用可能带有一组参数用于在用户空间进程的虚拟地址空间与内核空间之间传递信息。从编程角度看调用系统调用与调用 C 函数非常相似。五类系统调用系统调用主要分为 5 类详见 system_calls.md进程控制Process Control处理与进程相关的任务如进程创建、终止等文件管理File Management对文件进行操作如读/写文件设备管理Device Management处理设备相关事务如读写设备缓冲区信息维护Information Maintenance处理操作系统与用户程序之间的信息及其传递通信Communication用于进程间通信也用于创建和删除通信连接。各类系统调用在 Linux 中的典型示例系统调用类型Linux 示例进程控制fork()、exit()、wait()文件管理open()、read()、write()设备管理ioctl()、read()、write()信息维护getpid()、alarm()、sleep()通信pipe()、shmget()、mmap()用户态与内核态及其切换现代处理器架构通常允许 CPU 至少在两种模式下运行用户态和内核态。相应地虚拟内存区域可以被标记为用户空间或内核空间。在用户态运行时CPU 只能访问标记为用户空间的内存尝试访问内核空间内存会触发硬件异常。任一时刻进程要么在用户态执行要么在内核态执行。能执行哪些指令取决于当前模式而这种限制在硬件层面强制实施。CPU 模式也称处理器模式、CPU 状态、CPU 特权级别会限制 CPU 上某些进程可执行的操作类型和范围。内核本身并不是一个进程而是进程管理器process manager。内核模型假定需要内核服务的进程使用特定的编程构造——也就是系统调用。程序在用户态运行时不能直接访问内核数据结构或内核程序程序在内核态执行时这些限制不再适用程序通常在用户态执行只有在请求内核提供的服务时才切换到内核态应用需要访问系统硬件资源外设、内存、磁盘等时必须发起系统调用这会引发一次从用户态到内核态的上下文切换读写文件等操作都遵循这一流程只有系统调用本身运行在内核态应用的代码并不运行在内核态系统调用完成后进程通过一次反向上下文切换带着返回值回到用户态。除了系统调用内核例程还可以通过以下方式被激活异常exceptionCPU 执行进程时检测到异常状况如非法指令内核代表引发异常的进程处理该异常设备中断interrupt外设向 CPU 发出中断信号通知某个事件如请求关注、状态变化或 I/O 操作完成。每个中断信号由内核程序——中断处理程序interrupt handler——处理。由于外设相对 CPU 是异步工作的中断发生的时间不可预测内核线程执行内核线程运行在内核态其对应程序应视为内核的一部分。上图的流程可以这样理解进程 1在用户态发起系统调用后切换到内核态系统调用被处理随后进程 1 回到用户态继续执行直到定时器中断发生、内核态中的调度程序scheduler被激活发生进程切换进程 2开始在用户态执行直到硬件设备发起中断进程 2 因中断切换到内核态并处理该中断。剖析 write() 系统调用write()系统调用把数据写入一个已打开的文件其声明为# include unistd.h ssize_t write(int fd, void *buffer, size_t count);buffer待写入数据的地址count要从 buffer 写入的字节数fd指向目标文件的文件描述符。write()会把最多count字节从buffer写入fd引用的已打开文件。成功时返回实际写入的字节数可能小于count出错时返回-1。一个重要的事实对磁盘文件执行 I/O 时write()成功返回并不保证数据已经落盘。因为内核会对磁盘 I/O 做缓冲以减少磁盘活动、加快write()调用。它只是在用户空间缓冲区与内核缓冲区缓存buffer cache之间拷贝数据稍后内核才把缓冲区刷新flush到磁盘。如果在此期间另一个进程试图读取这些字节内核会直接从缓冲区缓存提供数据而不是从内容过时的磁盘文件读取。这种设计的目的是让write()保持快速——它不必等待慢速的磁盘操作同时也很高效——减少了内核必须执行的磁盘传输次数。使用 strace 调试 Linux 进程安装 stracestrace是用于跟踪用户进程与 Linux 内核之间切换的工具。使用前先确认系统是否已安装以下命令与输出基于 RHEL/CentOS 7 系列环境从包名中的el7可以识别$ rpm -qa | grep -i strace strace-4.12-9.el7.x86_64如果上面的命令没有任何输出可以通过包管理器安装$ yum install strace库函数与系统调用的关系标准 C 库中的函数被称为库函数library functions其用途五花八门——打开文件、把时间转换为人类可读格式、比较两个字符串等。部分库函数就构建在系统调用之上。通常库函数会提供比底层系统调用更友好的调用者接口。例如printf()提供输出格式化与数据缓冲而write()系统调用只是输出一块字节。Linux 上最常用的标准 C 库实现是 GNU C 库glibc。C 语言提供的printf()让用户能以多种格式写入数据printf()把数据转换为格式化字节序列然后调用write()把这些字节写到输出。我们使用strace观察执行printf %s Hello world时实际发生了什么~]$ strace printf %s Hello world execve(/usr/bin/printf, [printf, %s, Hello world], [/* 47 vars */]) 0 brk(NULL) 0x90d000 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 0x7f8fc672f000 access(/etc/ld.so.preload, R_OK) -1 ENOENT (No such file or directory) open(/etc/ld.so.cache, O_RDONLY|O_CLOEXEC) 3 fstat(3, {st_modeS_IFREG|0644, st_size98854, ...}) 0 mmap(NULL, 98854, PROT_READ, MAP_PRIVATE, 3, 0) 0x7f8fc6716000 close(3) 0 open(/lib64/libc.so.6, O_RDONLY|O_CLOEXEC) 3 read(3, \177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\0\1\0\0\0\20\2\0\0\0\0\0..., 832) 832 fstat(3, {st_modeS_IFREG|0755, st_size2156160, ...}) 0 mmap(NULL, 3985888, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) 0x7f8fc6141000 mprotect(0x7f8fc6304000, 2097152, PROT_NONE) 0 mmap(0x7f8fc6504000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c3000) 0x7f8fc6504000 mmap(0x7f8fc650a000, 16864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) 0x7f8fc650a000 close(3) 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 0x7f8fc6715000 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 0x7f8fc6713000 arch_prctl(ARCH_SET_FS, 0x7f8fc6713740) 0 mprotect(0x7f8fc6504000, 16384, PROT_READ) 0 mprotect(0x60a000, 4096, PROT_READ) 0 mprotect(0x7f8fc6730000, 4096, PROT_READ) 0 munmap(0x7f8fc6716000, 98854) 0 brk(NULL) 0x90d000 brk(0x92e000) 0x92e000 brk(NULL) 0x92e000 open(/usr/lib/locale/locale-archive, O_RDONLY|O_CLOEXEC) 3 fstat(3, {st_modeS_IFREG|0644, st_size106075056, ...}) 0 mmap(NULL, 106075056, PROT_READ, MAP_PRIVATE, 3, 0) 0x7f8fbfc17000 close(3) 0 open(/usr/share/locale/locale.alias, O_RDONLY|O_CLOEXEC) 3 fstat(3, {st_modeS_IFREG|0644, st_size2502, ...}) 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 0x7f8fc672e000 read(3, # Locale name alias data base.\n#..., 4096) 2502 read(3, , 4096) 0 close(3) 0 munmap(0x7f8fc672e000, 4096) 0 open(/usr/lib/locale/UTF-8/LC_CTYPE, O_RDONLY|O_CLOEXEC) -1 ENOENT (No such file or directory) fstat(1, {st_modeS_IFCHR|0620, st_rdevmakedev(136, 1), ...}) 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 0x7f8fc672e000 write(1, Hello world, 11Hello world) 11 close(1) 0 munmap(0x7f8fc672e000, 4096) 0 close(2) 0 exit_group(0) ? exited with 0 逐行解读关键系统调用execve(/usr/bin/printf, [printf, %s, Hello world], [/* 47 vars */]) 0这是进程发起的第一个系统调用它做了三件事操作系统停止父进程的复制进程操作系统加载新程序本例中是printf并启动它execve()用从 printf 可执行文件加载的新内容替换当前进程内存栈中定义进程的相关部分。这一行中第一词execve是正在执行的系统调用名称第一个参数必须是二进制可执行文件或脚本的路径第二个参数是传给新程序的参数字符串数组按惯例第一个字符串应包含被执行文件的文件名第三个参数是环境变量。号后面的数字此处为 0是execve的返回值表示调用成功。open(/usr/lib/locale/UTF-8/LC_CTYPE, O_RDONLY|O_CLOEXEC) -1 ENOENT (No such file or directory)程序尝试open()文件/usr/lib/locale/UTF-8/LC_CTYPE但调用失败返回-1并附带了描述性错误信息ENOENT (No such file or directory)——文件不存在。brk(NULL) 0x90d000/brk(0x92e000) 0x92e000/brk(NULL) 0x92e000brk()系统调用用于增大或减小进程的数据段data segment返回进程数据段将要结束的新地址。open(/lib64/libc.so.6, O_RDONLY|O_CLOEXEC) 3与read(3, \177ELF..., 832) 832这里可以看到一次成功的open()调用随后是read()系统调用。在open()中第一个参数是要使用的文件路径第二个参数定义权限标志O_RDONLY表示文件只读O_CLOEXEC为打开的文件启用 close-on-exec 标志在多线程程序中一个线程打开文件描述符的同时另一个线程执行 exec可以避免竞争条件。返回值 3 是打开文件所用的文件描述符——由于 fd 0、1、2 已被 stdin、stdout、stderr 占用文件描述符表中第一个未使用的就是 3。在read()中第一个参数是文件描述符 3正是open()返回的那个第二个参数是读取数据的缓冲区第三个参数是缓冲区长度返回值 832 是实际读取的字节数。close(3) 0close系统调用让内核关闭文件描述符。对大多数文件系统而言程序通过close系统调用终止对文件系统中某个文件的访问。后面的 0 表示调用成功。write(1, Hello world, 11) 11前面已经介绍过write()系统调用及其参数。我们看到的任何屏幕输出其实都来自文件/dev/tty并通过 fd 1 写到 stdout。第一个参数是文件描述符第二个参数是包含待写信息的缓冲区最后一个参数是字符数量成功时返回实际写入的字节数0 表示什么都没写入本例中为 11。 exited with 0 这表示程序以退出码 0 成功退出。在 Linux 程序中退出码 0 通常表示成功执行与终止。用 strace 定位挂起与慢速进程除了跟踪系统调用strace还能用来检测程序访问了哪些文件。上面的 trace 中open(/lib64/libc.so.6, ...) 3打开了 libc 共享对象——它是各种标准函数的 C 实现也是printf()定义的所在打印 Hello World 所需的函数就在这里。当怀疑某个程序挂起或卡住时strace同样有效拿到 trace 后可以观察程序卡在哪个操作上顺着 trace 往下走还能发现导致挂起/卡住的错误。strace对查找程序性能缓慢的原因也很有帮助。如何查阅系统调用文档你不需要记住所有系统调用及其行为——需要时查阅文档即可。运行man命令前请确保安装了 man-pages 包$ rpm -qa | grep -i man-pages man-pages-3.53-5.el7.noarch然后用如下格式的man命令查看某个系统调用的文档例如execveman 2 execve其中2是 Linux man 手册的章节号专用于系统调用。生产环境的注意事项strace 的性能开销虽然strace有上述诸多用途但在生产环境运行 trace 并不是好选择——它会引入相当可观的开销。根据 Red Hat 高级软件工程师 Arnaldo Carvalho de Melo 所做的性能测试该结论记载于本模块的 system_calls.md被 strace 跟踪的进程运行速度慢了 173 倍这对生产环境来说可能是灾难性的。因此正确姿势是先在预发/测试环境用 strace 定位问题或对生产进程做短时间、低频率的采样避免长时间全量跟踪。结语本指南围绕 School of SRE 的「System Calls and Signals」模块把信号与系统调用这两条线索交织在一起信号以软件中断的形式贯穿进程从创建到终止的全生命周期处理得当可以优雅地控制进程行为系统调用则是进程与内核之间唯一的受控通道理解它才能理解 Linux 上一切 I/O、进程与通信行为的底层原理。对 SRE 而言这套知识既是排查进程挂起、性能退化问题的武器strace也是构建大规模系统可靠性心智模型的地基。你可以继续阅读本模块的 signals.md 与 system_calls.md 原文或顺着 Level 102 课程目录 进入容器化与编排、系统排障与性能优化等更深入的章节。赞分享教程【免费下载链接】school-of-sreAt LinkedIn, we are using this curriculum for onboarding our entry-level talents into the SRE role.项目地址https://gitcode.com/gh_mirrors/sc/school-of-sre点击查看免费下载相关推荐掌握TCPdump与WiresharkSRE必备的网络故障排查终极指南掌握TCPdump与WiresharkSRE必备的网络故障排查终极指南 在软件可靠性工程SRE领域网络故障排查是确保系统稳定运行的关键技能。school教程如何掌握GameEngineFromScratch材质烘焙技术从基础到高级的完整指南如何掌握GameEngineFromScratch材质烘焙技术从基础到高级的完整指南 GameEngineFromScratch材质系统是现代游戏开发中不可或深入理解 Linux 信号机制School of SRE 课程中的进程通信与信号处理实战深入理解 Linux 信号机制School of SRE 课程中的进程通信与信号处理实战 信号Signal是 Linux/Unix 系统中内核与用户进程之教程上一篇极客时间课程下载器打造你的专属离线知识库下一篇2024年前端构建工具终极对比Webpack vs Vite vs Turbopack 性能深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门