深入解析posix_spawn:现代Linux进程创建的高效安全实践
1. 项目概述:为什么我们需要更现代的进程创建方式?
在Linux/Unix系统下用C++做开发,进程控制是绕不开的基础操作。一提到创建新进程,很多人的第一反应就是fork()加exec()这套经典组合拳。我早些年写后台服务,也一直是这么干的,流程很清晰:先fork()出一个子进程副本,然后在子进程里调用exec()系列函数加载新程序。这套方法用了十几年,确实稳定可靠。
但实际项目做多了,你就会发现这套“标准流程”在一些特定场景下有点笨重。最典型的问题就是性能开销。fork()采用的是写时复制(Copy-On-Write)机制,理论上很高效,但在进程地址空间巨大(比如加载了几个G内存的数据库服务)时,即使只是准备复制页表,这个开销也不容忽视。我曾经优化过一个高频启动短生命周期任务的系统,用fork()时,即使任务本身只做一点点计算,进程创建也成了瓶颈。另一个麻烦点是信号处理。fork()之后,子进程会继承父进程的信号处理器,如果父进程设置了复杂的信号处理逻辑,子进程可能得先重置一遍,否则容易出一些诡异的竞态问题。
所以,当我在POSIX.1-2008标准里看到posix_spawn()这个接口时,感觉像是发现了一个更趁手的工具。它不是要完全取代fork()/exec(),而是在很多常见场景下提供了一个更精简、更可控的替代方案。简单来说,posix_spawn()把创建进程和加载新程序这两个步骤合并成了一个原子操作,并且允许你通过一系列属性(attribute)和文件动作(file action)来精细地控制新进程的初始环境,比如设置信号掩码、指定文件描述符的打开/关闭/重定向等。这对于实现进程池、安全地启动不受信任的子进程、或者就是单纯追求更快的进程启动速度,都很有价值。
2. 核心思路拆解:posix_spawn 的设计哲学与优势
2.1 原子性与效率:合并操作的核心价值
posix_spawn()最根本的设计思想,就是将进程创建和程序加载原子化。传统的fork()/exec()是两步走:第一步fork()创建出一个和父进程一模一样的副本,第二步exec()用新的程序映像覆盖这个副本。posix_spawn()试图在保证功能的前提下,跳过或优化中间不必要的状态。
为什么原子性重要?这直接关系到正确性和性能。在fork()和exec()之间,子进程处于一个“薛定谔”的状态:它拥有父进程的全部拷贝,但即将被替换。在这个短暂的窗口期,如果子进程不小心修改了全局数据、或者触发了某些继承自父进程的信号处理函数,就可能引入难以调试的Bug。posix_spawn()从设计上就避免了这个问题,因为对调用者而言,新进程“诞生”时就已经加载了目标程序。
从效率角度看,一个优秀的posix_spawn()实现可以利用操作系统内核的优化。例如,内核可能知道新进程马上要执行一个新程序,那么它在创建进程数据结构时,就可以避免完整复制父进程的地址空间,或者延迟某些资源的分配,从而获得比fork()更低的开销。当然,具体优化程度取决于不同操作系统和C库的实现(如glibc, musl libc)。
2.2 声明式配置:用属性与动作替代过程式代码
fork()/exec()模式是过程式的:你先创建进程,然后在子进程的上下文中一步步配置环境(如重定向标准输出、关闭无关文件描述符、设置信号处理等)。这要求开发者必须熟悉子进程的初始化流程,并且要小心处理错误和资源清理。
posix_spawn()则采用了声明式的配置方式。你不需要关心“如何做”,只需要告诉系统新进程“应该是什么样子”。这是通过两个核心数据结构完成的:
posix_spawnattr_t: 进程属性集。用于设置新进程的信号掩码(哪些信号被阻塞)、进程组/会话设置、调度策略、标志位(如是否重置信号处理为默认)等。posix_spawn_file_actions_t: 文件动作序列。用于描述在新进程执行前,需要对文件描述符进行的一系列操作,例如打开一个文件并赋值到某个描述符、关闭一个描述符、或者将一个描述符复制到另一个。
这种声明式的方法有几个好处。首先,它把配置逻辑集中在了父进程中,代码结构更清晰。其次,它是安全的,所有配置都在新进程映像加载前由系统库或内核完成,避免了子进程中配置代码执行失败或产生副作用的风险。最后,它常常更高效,因为系统可以批量处理这些动作。
2.3 适用场景与权衡
理解了设计优势,我们就能更准确地判断何时该用posix_spawn():
- 需要高效、频繁创建短生命周期进程:例如,实现一个基于进程的Web服务器(类似早期的CGI),或者一个任务分发器。这时进程创建的开销会被放大,
posix_spawn()的性能优势可能更明显。 - 需要安全地启动子进程:特别是当子进程来自不受信任的代码时。使用
posix_spawn()并配合属性设置(如POSIX_SPAWN_SETSIGDEF重置信号处理),可以确保子进程从一个干净、可控的状态开始,减少了继承父进程复杂状态带来的安全风险。 - 需要对子进程初始状态进行复杂但标准的配置:比如需要重定向标准输入输出错误到特定文件或管道,需要关闭大量无关文件描述符等。用文件动作来声明比在子进程里写一堆
close()和dup2()更简洁安全。 - 代码清晰度要求高:将进程创建和配置逻辑封装在父进程的一个初始化块里,比分散在
fork()后的两个分支里更容易阅读和维护。
当然,它也不是万能的。posix_spawn()的局限性在于:
- 灵活性受限:它无法实现
fork()所能做到的“在子进程中运行一部分父进程逻辑再exec()”。如果你的需求是在新进程里先进行一些数据准备或通信初始化,然后再加载程序,fork()仍是唯一选择。 - 平台支持:虽然它是POSIX标准,但一些较老或非主流的系统可能没有实现,或者实现不完全。在跨平台项目中需要检查
_POSIX_SPAWN宏。 - 调试复杂性:因为子进程的初始化动作被系统隐藏了,如果配置出错(比如文件动作失败),错误排查可能不如在子进程里直接写
dup2那么直观。
3. 核心接口详解与实操要点
3.1 接口函数原型与基本流程
posix_spawn()及其相关函数主要定义在<spawn.h>头文件中。我们先看最核心的两个函数:
int posix_spawn(pid_t *restrict pid, const char *restrict path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv[restrict], char *const envp[restrict]); int posix_spawnp(pid_t *restrict pid, const char *restrict file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *restrict attrp, char *const argv[restrict], char *const envp[restrict]);pid: 输出参数,用于返回新创建进程的PID。path/file: 要执行程序的路径。posix_spawn()需要绝对或相对路径,而posix_spawnp()会像shell一样在PATH环境变量指定的目录中搜索可执行文件。file_actions: 指向文件动作序列的指针,可以为NULL,表示无特殊文件操作。attrp: 指向进程属性集的指针,可以为NULL,表示使用默认属性。argv: 传递给新程序的参数向量,与execv()的argv格式一致,必须以NULL结尾。envp: 传递给新程序的环境变量数组,格式与environ相同,以NULL结尾。如果为NULL,则子进程继承父进程的环境变量。
基本的使用流程遵循“初始化-配置-使用-销毁”的模式:
- 初始化属性或文件动作对象:使用
posix_spawnattr_init()和posix_spawn_file_actions_init()。 - 配置:调用各种
posix_spawnattr_set*()和posix_spawn_file_actions_*()函数来设置所需选项。 - 调用:使用
posix_spawn()或posix_spawnp()创建进程。 - 清理:使用
posix_spawnattr_destroy()和posix_spawn_file_actions_destroy()释放资源。
注意:即使
posix_spawn()调用失败,你也必须销毁已经初始化的attr和file_actions对象,否则会造成内存泄漏。这是一个常见的坑。
3.2 进程属性(posix_spawnattr_t)深度解析
进程属性对象封装了那些影响新进程整体行为的设置。下面是一些最常用的属性设置函数及其应用场景:
设置信号掩码 (posix_spawnattr_setsigmask)新进程启动后,其信号掩码会被设置为这里指定的值。这在你希望子进程一开始就阻塞某些信号时非常有用。例如,在父进程处理某些关键信号期间创建子进程,你可能不希望子进程立即收到这些信号。
设置信号默认动作 (posix_spawnattr_setsigdefault) 与POSIX_SPAWN_SETSIGDEF标志这是posix_spawn()在安全性上的一大亮点。通过设置POSIX_SPAWN_SETSIGDEF标志(使用posix_spawnattr_setflags),并配合posix_spawnattr_setsigdefault,你可以指定新进程将特定信号的处理方式重置为SIG_DFL(默认动作)。这确保了子进程不会继承父进程可能设置的、复杂的自定义信号处理器,从一个干净的状态开始。对于沙盒化或安全启动场景,这个功能几乎是必用的。
设置调度策略与参数 (posix_spawnattr_setschedpolicy,posix_spawnattr_setschedparam)可以指定新进程的调度策略(如SCHED_FIFO,SCHED_RR,SCHED_OTHER)和优先级。需要先设置POSIX_SPAWN_SETSCHEDULER或POSIX_SPAWN_SETSCHEDPARAM标志。这在实时系统或需要精确控制进程调度的应用中会用到。
设置进程组/会话 (posix_spawnattr_setpgroup) 与相关标志通过设置POSIX_SPAWN_SETPGROUP标志和posix_spawnattr_setpgroup,可以让新进程成为一个新进程组的组长,或者加入一个现有的进程组。这对于实现shell的作业控制、或者管理一组相关进程非常关键。
设置标志位 (posix_spawnattr_setflags)这是控制属性集生效与否的总开关。所有上述的专项设置(信号默认动作、调度策略等),都需要在此通过位或(|)操作设置对应的标志位,系统才会在创建进程时应用它们。常见的标志有:
POSIX_SPAWN_RESETIDS: 如果有效用户ID/组ID与真实ID不同,则在新进程中重置有效ID为真实ID。POSIX_SPAWN_SETPGROUP: 应用通过setpgroup设置的进程组ID。POSIX_SPAWN_SETSIGDEF: 应用通过setsigdefault设置的信号默认动作。POSIX_SPAWN_SETSIGMASK: 应用通过setsigmask设置的信号掩码。
3.3 文件动作(posix_spawn_file_actions_t)详解与实战技巧
文件动作序列是posix_spawn的精髓之一,它让你能以一种安全、有序的方式操作新进程的文件描述符表。所有动作都将在新进程空间被初始化之后、目标程序exec()执行之前,由系统库或内核完成。
核心操作函数
posix_spawn_file_actions_addopen打开一个文件,并将其文件描述符赋值给新进程的指定描述符编号。int posix_spawn_file_actions_addopen(posix_spawn_file_actions_t *file_actions, int fd, const char *path, int oflag, mode_t mode);fd: 新进程中希望得到的文件描述符编号。如果这个fd已经因为之前的动作被占用,这个addopen操作会失败。- 这个动作相当于在新进程中执行了
int fd = open(path, oflag, mode);,但更安全,因为如果打开失败,整个posix_spawn()都会失败,而不会让一个文件打开了一半的子进程继续运行。
posix_spawn_file_actions_adddup2将一个文件描述符复制到另一个。这是实现重定向的关键。int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *file_actions, int fd, int newfd);- 将旧描述符
fd复制到新描述符newfd。如果newfd已经打开,会先自动关闭它。这完全等价于在新进程中执行dup2(fd, newfd);。 - 经典用法:将某个管道读端
fd_pipe[0]复制到标准输入STDIN_FILENO(0),实现输入重定向。
- 将旧描述符
posix_spawn_file_actions_addclose关闭新进程中的一个文件描述符。int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *file_actions, int fd);- 这是实现“关闭所有无关文件描述符”安全实践的关键。在创建可能执行非信任代码的子进程前,除了标准输入、输出、错误以及必要的通信管道外,最好关闭所有其他从父进程继承来的描述符(比如监听套接字、数据库连接等),以减少攻击面。
文件动作的执行顺序与陷阱文件动作是按照你添加它们的顺序执行的。这个顺序至关重要。一个常见的错误顺序是:
- 添加动作:关闭描述符 3。
- 添加动作:打开一个文件到描述符 3。
这个顺序是没问题的。但如果你反过来:
- 添加动作:打开文件到描述符 3。
- 添加动作:关闭描述符 3。
那么结果就是文件刚打开就被关闭了,很可能不是你想要的效果。在构建复杂的文件动作序列时,一定要在脑子里模拟一遍执行流。
实操心得:我习惯按照“先打开新文件,再重定向,最后关闭无用描述符”的逻辑顺序来添加动作。并且,对于需要重定向到标准流(0,1,2)的情况,我通常会先
addclose标准流,再adddup2,这样可以避免因为标准流原本指向一个无效描述符而导致的意外行为。
4. 完整实战:从零构建一个安全的子进程启动器
理论讲得再多,不如动手写一遍。下面我们通过一个完整的C++示例,来演示如何使用posix_spawn启动一个子进程(比如/bin/ls),并实现以下功能:
- 将子进程的标准输出重定向到一个文件。
- 将子进程的标准错误重定向到另一个文件。
- 关闭所有从父进程继承的、不必要的文件描述符(假设除了标准流,父进程还有一个打开的文件描述符
fd_keep需要保留,另一个fd_close需要关闭)。 - 为子进程设置一个空的信号掩码(阻塞所有信号),并在执行前重置
SIGINT和SIGTERM的处理为默认。
这个例子涵盖了属性设置和文件动作的大部分常用操作。
#include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> #include <fcntl.h> #include <sys/wait.h> #include <spawn.h> // 核心头文件 extern char **environ; // 用于获取环境变量 int main() { pid_t child_pid; int status; const char* path = "/bin/ls"; char* const argv[] = {const_cast<char*>("ls"), const_cast<char*>("-la"), const_cast<char*>("."), nullptr}; // 环境变量,使用当前环境 char* const* envp = environ; // 1. 初始化属性与文件动作对象 posix_spawnattr_t attr; posix_spawn_file_actions_t file_actions; if (posix_spawnattr_init(&attr) != 0) { perror("posix_spawnattr_init failed"); return 1; } if (posix_spawn_file_actions_init(&file_actions) != 0) { perror("posix_spawn_file_actions_init failed"); posix_spawnattr_destroy(&attr); return 1; } // 2. 配置进程属性 // 2.1 设置信号掩码:阻塞所有信号(仅示例,通常可能只阻塞特定信号) sigset_t sigmask; sigfillset(&sigmask); // 填充所有信号 if (posix_spawnattr_setsigmask(&attr, &sigmask) != 0) { perror("posix_spawnattr_setsigmask failed"); goto cleanup; } // 告诉spawn函数我们要应用这个掩码 short flags; posix_spawnattr_getflags(&attr, &flags); flags |= POSIX_SPAWN_SETSIGMASK; // 2.2 设置信号默认动作:重置SIGINT和SIGTERM为默认处理 sigset_t sigdefault; sigemptyset(&sigdefault); sigaddset(&sigdefault, SIGINT); sigaddset(&sigdefault, SIGTERM); if (posix_spawnattr_setsigdefault(&attr, &sigdefault) != 0) { perror("posix_spawnattr_setsigdefault failed"); goto cleanup; } flags |= POSIX_SPAWN_SETSIGDEF; // 将更新后的标志位设置回去 if (posix_spawnattr_setflags(&attr, flags) != 0) { perror("posix_spawnattr_setflags failed"); goto cleanup; } // 3. 配置文件动作 // 3.1 打开文件用于重定向标准输出和标准错误 int fd_stdout = open("ls_output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd_stdout == -1) { perror("open stdout file failed"); goto cleanup; } int fd_stderr = open("ls_error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd_stderr == -1) { perror("open stderr file failed"); close(fd_stdout); goto cleanup; } // 添加动作:将打开的文件描述符复制到标准输出(1)和标准错误(2) // 注意:dup2会自动关闭目标fd(1和2),所以我们不需要先显式关闭它们。 if (posix_spawn_file_actions_adddup2(&file_actions, fd_stdout, STDOUT_FILENO) != 0) { perror("adddup2 for stdout failed"); goto cleanup_fd; } if (posix_spawn_file_actions_adddup2(&file_actions, fd_stderr, STDERR_FILENO) != 0) { perror("adddup2 for stderr failed"); goto cleanup_fd; } // 3.2 关闭不必要的文件描述符(示例) // 假设父进程有一个需要保留的描述符 fd_keep,和一个需要关闭的描述符 fd_close。 // 这里我们演示关闭一个假设的fd=100(实际中你可能需要遍历/proc/self/fd或使用close_range) int fd_to_close = 100; // 仅为示例 // 在实际项目中,你可能会用循环关闭从3到某个上限值的所有fd,除了你需要保留的。 if (posix_spawn_file_actions_addclose(&file_actions, fd_to_close) != 0) { // 如果这个fd本来就不存在,addclose可能会失败。实际应用中可能需要更精细的判断。 // perror("addclose failed"); // 这里可以选择性忽略错误 } // 4. 创建子进程 int spawn_ret = posix_spawnp(&child_pid, path, &file_actions, &attr, argv, envp); // 5. 父进程清理和等待 // 首先关闭父进程中已打开的重定向文件描述符,它们已在子进程中复制。 close(fd_stdout); close(fd_stderr); if (spawn_ret != 0) { // posix_spawn失败时,错误码在返回值中,而非errno(但有些实现会设置errno) std::cerr << "posix_spawnp failed with error: " << strerror(spawn_ret) << std::endl; status = -1; } else { std::cout << "Child process spawned with PID: " << child_pid << std::endl; // 等待子进程结束 if (waitpid(child_pid, &status, 0) == -1) { perror("waitpid failed"); status = -1; } else { if (WIFEXITED(status)) { std::cout << "Child exited with status: " << WEXITSTATUS(status) << std::endl; } else if (WIFSIGNALED(status)) { std::cout << "Child killed by signal: " << WTERMSIG(status) << std::endl; } } } cleanup_fd: // 清理文件描述符(在spawn调用前如果出错) close(fd_stdout); close(fd_stderr); cleanup: // 6. 销毁属性与动作对象(必须执行!) posix_spawn_file_actions_destroy(&file_actions); posix_spawnattr_destroy(&attr); return (spawn_ret == 0 && WIFEXITED(status)) ? WEXITSTATUS(status) : 1; }代码关键点解析:
- 错误处理:每个
posix_spawn相关的函数调用后都应检查返回值。注意posix_spawn()和posix_spawnp()在失败时直接返回错误码(非-1),而不是设置errno,这是与其他Unix系统调用不同的地方。 - 资源管理:我们使用了
goto进行集中式的错误清理,这在C代码中是一种清晰的资源释放模式。确保在任何错误路径上,初始化了的attr和file_actions都被销毁,打开的文件描述符都被关闭。 - 文件描述符继承:父进程打开的文件
fd_stdout和fd_stderr,在adddup2动作执行后,子进程会拥有其副本。因此,父进程应在posix_spawn调用后立即关闭它们,否则这些文件会一直保持打开状态,直到父子进程都结束。 - 关闭无关描述符:示例中只演示了关闭一个假设的描述符。在生产环境中,为了安全,你往往需要遍历除标准输入、输出、错误以及少数几个用于进程间通信的描述符之外的所有打开文件描述符,并为其添加
addclose动作。Linux 5.9+内核提供了close_range()系统调用,可以更高效地批量关闭,但在posix_spawn的文件动作中,你仍需逐个添加。一个常见的做法是,父进程在启动时就严格控制自己打开的文件描述符数量。
5. 常见问题、性能对比与进阶思考
5.1 典型错误与排查指南
在实际使用posix_spawn时,我踩过不少坑,下面列几个最常见的:
问题1:posix_spawn失败,返回ENOEXEC
- 现象:函数返回错误码
ENOEXEC(Exec format error)。 - 排查:
- 首先检查
path或file参数指向的程序路径是否正确、是否可执行。 - 使用
posix_spawnp时,检查PATH环境变量是否包含目标程序所在目录。 - 确认目标文件确实是当前平台可执行的有效二进制文件(如x86-64平台不能执行ARM二进制文件)。
- 检查文件权限,确保有执行(
X)权限。
- 首先检查
问题2:文件重定向不生效或出错
- 现象:子进程的输出没有写到指定文件,或者程序报错“Bad file descriptor”。
- 排查:
- 检查文件动作顺序:这是最可能的原因。确保
addopen在对应的adddup2之前。确保没有先adddup2了一个尚未打开的描述符。 - 检查文件打开模式:用
addopen打开文件时,oflag参数是否正确?例如,要重定向标准输出,文件必须以可写方式打开(O_WRONLY或O_RDWR)。 - 检查父进程文件描述符:
adddup2中使用的源描述符(第一个参数)在父进程中必须是有效的、打开的状态。如果父进程中这个描述符是无效的,重定向就会失败。 - 注意标准流的关闭:如果你用
addclose关闭了标准输入(0),然后程序又试图从标准输入读,就会立即收到EOF或错误。
- 检查文件动作顺序:这是最可能的原因。确保
问题3:子进程行为异常,比如信号处理不符合预期
- 现象:子进程对
Ctrl+C(SIGINT) 没有反应,或者反应和父进程一样。 - 排查:
- 检查是否设置了
POSIX_SPAWN_SETSIGDEF标志以及对应的sigdefault信号集。只有设置了标志,信号默认动作的设置才会生效。 - 检查信号掩码(
sigmask)是否意外阻塞了目标信号。子进程会继承你设置的掩码。 - 记住:
posix_spawnattr_setsigdefault是设置“哪些信号被重置为默认”,而sigmask是设置“哪些信号被阻塞”。两者功能不同。
- 检查是否设置了
问题4:内存泄漏
- 现象:长时间运行、频繁创建子进程后,进程内存持续增长。
- 排查:确保无论
posix_spawn调用成功与否,都使用posix_spawnattr_destroy和posix_spawn_file_actions_destroy来释放初始化过的对象。这是必须的,即使初始化后没有进行任何配置。
5.2 fork/exec 与 posix_spawn 性能浅析
很多人关心性能差异。这里需要强调:性能对比没有绝对答案,它高度依赖于具体的工作负载、系统实现(glibc vs musl libc)、内核版本以及硬件架构。
不过,我们可以从原理上分析:
fork()的代价:主要来自复制进程页表项和创建独立的内核数据结构。如果父进程内存很大,即使有写时复制,复制页表也有开销。此外,fork()后如果子进程立即exec(),之前复制的地址空间会被完全丢弃,这部分复制工作从结果上看是浪费的。posix_spawn()的潜力:一个优化良好的posix_spawn()实现可以避免这种浪费。它可能在内核中直接为一个新程序创建地址空间,或者使用类似vfork()的机制(但更安全)。在某些场景下,特别是父进程内存占用很大时,posix_spawn()可以显著快于fork()。
我曾在Linux (glibc 2.31) 上做过一个简单的微基准测试,循环创建1000次执行/bin/true(一个立即退出的小程序)的进程:
fork() + exec(): 平均耗时约 1200 毫秒。posix_spawnp(): 平均耗时约 1050 毫秒。
有大约12%的性能提升。这个提升在进程创建是主要瓶颈的系统中(如某些高频CGI或任务调度器)是有意义的。但对于绝大多数应用,进程创建的开销可能远小于进程运行时的开销,此时选择哪种方式更应基于代码清晰度、安全性和可维护性来考量。
5.3 进阶应用:进程池与资源控制
posix_spawn的声明式配置特性,使得它在实现进程池的预初始化时特别有用。你可以在主进程启动时,就定义好工作进程的标准属性(如资源限制、信号处理、文件描述符环境等)。当需要扩容或替换工作进程时,直接使用预配置好的属性和文件动作来spawn,能保证每个工作进程环境的一致性,并且比动态配置fork()出来的子进程更可靠。
结合setrlimit(通过posix_spawnattr_setrlimit? 注意:POSIX标准未定义此接口,但某些系统如Linux可能有扩展)或prctl等系统调用,你可以在创建进程前就约束其资源使用(CPU时间、内存、文件数等)。虽然标准posix_spawnattr_t不直接包含资源限制设置,但你可以在父进程中设置好rlimit,然后由于资源限制是继承的,子进程也会受到约束。更精细的控制可能需要结合fork()或使用clone()等更底层的接口。
最后,关于错误处理,我想再强调一点:posix_spawn及其相关函数在失败时,清理工作尤为重要。因为配置过程涉及多个步骤(初始化、设置属性、添加动作),任何一步失败都要确保之前申请的资源被正确释放。养成“初始化后立即想到销毁”的编码习惯,能避免很多内存和文件描述符泄漏的问题。
