什么会导致exec失败? 接下来发生什么?

exec(execl,execlp等)可能会失败的原因是什么? 如果你打电话给exec,它会返回,除了恐慌和调用退出之外,还有什么最佳实践吗?

exec(3)手册页 :

execl()execle()execlp()execvp()execvP()函数可能会失败,并为库函数execve(2)malloc(3)指定的任何错误设置errno。

execv()函数可能会失败,并为库函数execve(2)指定的任何错误设置errno。

然后从execve(2)手册页 :

错误

如果出现以下情况, Execve()将失败并返回到调用进程:

  • [E2BIG] – 新进程参数列表中的字节数大于系统限制。 此限制由sysctl(3) MIB变量KERN_ARGMAX
  • [EACCES] – 对于路径前缀的组件,搜索权限被拒绝。
  • [EACCES] – 新的进程文件不是一个普通的文件。
  • [EACCES] – 新的进程文件模式拒绝执行权限。
  • [EACCES] – 新的进程文件位于禁用执行的文件系统上( MNT_NOEXEC位于<sys/mount.h> )。
  • [EFAULT] – 新的进程文件没有其标题中的大小值所指示的那么长。
  • [EFAULT] – 路径,argv或envp指向非法地址。
  • [EIO] – 从文件系统读取时发生I / O错误。
  • [ELOOP] – 在翻译路径名时遇到了太多的符号链接。 这被认为是循环符号链接的指示。
  • [ENAMETOOLONG] – 路径名的组件超过了{NAME_MAX}字符,或者整个路径名超过了{PATH_MAX}字符。
  • [ENOENT] – 新的进程文件不存在。
  • [ENOEXEC] – 新的进程文件具有适当的访问权限,但具有无法识别的格式(例如,标题中的无效幻数)。
  • [ENOMEM] – 新进程需要比强加的最大值( getrlimit(2) )允许的更多的虚拟内存。
  • [ENOTDIR] – 路径前缀的一个组件不是一个目录。
  • [ETXTBSY] – 新的进程文件是一个纯粹的过程(共享文本)文件,目前正在打开一些进程的写或读。

malloc()复杂得多,只使用ENOMEM 。 从malloc(3) man page

如果成功, calloc()malloc()realloc()reallocf()valloc()函数将返回一个指向已分配内存的指针。 如果有错误,则返回一个NULL指针并将errnoENOMEM

处理exec失败的问题是,通常exec是在子进程中执行的,而你想在父进程中执行错误处理。 但是,你不能只是exit(errno)因为(1)你不知道错误代码是否适合退出代码,(2),你不能区分从新程序到失败exec和失败退出代码你exec

我知道的最好的解决方案是使用管道来传达exec的成功或失败:

  1. 分叉之前,在父进程中打开一个管道。
  2. 分叉后,父母关闭管道的写入结束并从读取结束读取。
  3. 孩子关闭阅读结束,并为写作结束设置关闭执行标志。
  4. 小孩叫exec。
  5. 如果exec失败,孩子使用管道将错误代码写回父母,然后退出。
  6. 如果子成功执行exec ,则父母读取eof(零长度读取),因为执行close-on-exec使exec成功关闭了管道的写入结束。 或者,如果exec失败,父节点读取错误代码并可以相应地继续。 无论哪种方式,父母阻止,直到孩子调用exec
  7. 父母关闭管道的阅读结束。

exec()调用返回后的操作取决于上下文 – 程序应该执行什么操作,错误是什么,以及可以怎样解决这个问题。

麻烦的一个来源可能是你指定了一个简单的程序名而不是路径名。 也许你可以用execvp()重试,或者把命令转换成sh -c 'what you originally specified'的调用。 这些是否合理取决于应用。 如果涉及重大的安全问题,可能你不要再试。

如果你指定了一个路径名,并且有一个问题(ENOTDIR,ENOENT,EPERM),那么你可能没有任何明智的回退,但你可以有意义地报告错误。

在过去的十多年前,有些系统不支持“#!” shebang符号,如果您不确定是否执行可执行文件或shell脚本,则将其作为可执行文件尝试,然后将其作为shell脚本重试。 如果你正在运行一个Perl脚本,这可能会也可能不会工作,但在那些日子里,你编写了你的​​Perl脚本来检测它们是否被shell运行,并用Perl重新执行自己。 幸运的是,那些日子已经过去了。

在可能的范围内,确保进程报告问题是很重要的,以便能够跟踪 – 将其消息写入日志文件或写入stderr(甚至可能是syslog() ),以便那些必须工作的人出了什么问题有更多的信息来帮助他们,而不是倒霉的最终用户的报告“我试过X,它不工作”。 至关重要的是,如果什么都不起作用,那么退出状态不是0就表示成功。 即使这可能被忽略 – 但你做了你能做的。

无论是恐慌,你可以根据errno的价值做出决定。

执行应该总是成功。 (除了shell,即如果用户输入了伪命令)

如果exec失败,则表示:

  • 程序出现“故障”(组件丢失或损坏,路径名错误,内存不足,或…)
  • 严重的系统错误(内存不足,进程太多,磁盘故障…)

对于任何严重的错误,正常的做法是在stderr上写入错误消息,然后用失败代码退出。 几乎所有的标准工具都是这样做的。 对于exec:

 execl("bork", "bork", NULL); perror("failed: exec"); exit(127); 

shell也这样做(或多或少)。

通常如果一个子进程失败,父进程也失败了,应该退出。 孩子在执行中失败还是在运行程序时都没有关系。 如果exec失败,为什么exec失败并不重要。 如果因为任何原因儿童程序失败,呼叫过程就麻烦了,需要停止。

不要浪费大量时间来预测所有可能的错误状况。 不要编写试图以最好的方式处理每个错误代码的代码。 你只会膨胀代码,并引入许多新的错误。 如果你的程序被破坏,或者被滥用,它应该只是失败。 如果迫使它继续下去,那么更糟糕的麻烦就会出现。

例如,如果系统内存不足和交换颠倒,我们不想一遍又一遍地尝试运行一个进程; 这只会让情况变得更糟。 如果我们遇到文件系统错误,我们不想继续在该文件系统上运行; 这可能会使腐败变得更糟。 如果程序安装错误,或者有错误,或者内存损坏,我们希望尽快停止,然后破坏的程序会造成一些真正的破坏(比如向客户端发送损坏的报告,摧毁数据库等)。 ..)。

一种可能的选择:一个失败的进程可能需要帮助,暂停(SIGSTOP),然后重试该操作,如果被告知继续。 当系统内存不足,或者磁盘已满,或者即使程序出现故障时,这也可能有所帮助。 很少的操作如此昂贵和重要,这是值得的。

如果您正在制作交互式GUI程序,请尝试将其作为可重复使用的命令行工具(如果出现错误则退出)的薄包装。 程序中的每个函数都应该可以通过GUI,命令行和函数调用来访问。 写你的功能。 编写几个工具来为任何函数制作命令行和GUI包装器。 也使用子流程。

如果你正在制造一个真正的关键系统,比如一个核电站的控制器,或者预测海啸的计划,那么你在阅读我的愚蠢建议是什么? 关键系统不应完全取决于计算机或软件。 需要有一个“手动控制”,有人来驱动它。 特别是,不要试图在MS Windows上构建一个关键系统,就像在水下建造沙堡一样。