第27章 程序的执行

执行新程序:execve
  1. #include <unistd.h>
  2. int execve(const char* pathname, char* const argv[], char *const envp[]);
  3. // 返回值:若成功不返回,若出错,返回-1
  4. // pathname既可以是绝对路径,也可以是相对路径,通常应该等于basename
  5. // argv指定了传递的参数
  6. // envp指定了新程序的环境列表
  7. // Linux特有的/proc/PID/exe文件是一个符号链接,包含PID对应进程可执行文件的绝对路径名
  • 如果pathname指定的文件设置了set-user-ID或set-group-ID权限位,系统会在执行此文件时将进程的有效用户ID或有效组ID设置为程序文件的属主或组ID,利用这个机制,可另用户在运行程序时获得特权
  • 无论是否更改了有效用户ID,都会以进程的有效用户ID覆盖已保存的set-user-ID,以有效组ID覆盖已保存的set-group-ID

函数一旦返回,说明出现了错误:

  • EACCES:pathname没有指向一个常规文件,未对该文件赋予可执行权限,或某一级目录不可搜索,还有一种可能:以MS_NOEXEC标志挂载文件系统
  • ENOENT:pathname指向的文件不存在
  • ENOEXEC:系统无法识别其文件格式
  • ETXTBSY:一个或多个进程已经以写方式打开pathname指向的文件
  • E2BIG:参数列表和环境列表所需空间总和超过了允许的最大值
    exec库函数
    ```

    include

int execl(const char path, const char arg, …); int execlp(const char file, const char arg, …); int execle(const char path, const char arg,…, char const envp[]); int execv(const char path, char const argv[]); int execvp(const char file, char const argv[]); int fexecve(int fd, char const argv[],char *const envp[]);

int execve(const char file, char const argv[],char *const envp[]);

  1. - 第一个参数是文件名、文件描述符或文件路径,当path作为参数时,如果包含/,视为路径名,否则按照PATH环境变量指定的目录搜索可执行文件
  2. - 后缀l表示列表listv表示矢量vector,前三个函数的每个命令行参数都是一个独立的参数,而后四个函数应先构造一个指向各参数的指针数组,再将其作为参数传递
  3. - e结尾的三个函数可以传递一个指向环境字符串指针数组的指针,其他四个函数则是使用调用进程中的environ变量为新程序复制现有的环境
  4. - 字母p表示该函数取path作为参数,并且用PATH环境变量寻找可执行文件
  5. - lv互斥,pe互斥,pf互斥
  6. - 进程中每个打开描述符都有一个执行时关闭标志FD_CLOEXEC,若设置了此标志,执行exec时关闭该描述符
  7. - exec执行前后实际用户ID和实际组ID保持不变,而有效ID是否改变取决于所执行程序文件的设置用户ID位和设置组ID位是否设置,如果设置了,则有效用户ID变成程序文件所有者ID,组ID处理方式一样
  8. - 在大多UNIX实现中,只有execve是内核的系统调用,其余六个只是库函数
  9. ##### 解释器脚本
  10. 解释器:能够读取并执行文本格式命令的程序,如UNIX shellawksedperlpythonruby的程序,除了能够交互式读取和执行命令外,还可以从脚本文件中读取和执行命令
  11. - 必须赋予脚本文件可执行权限
  12. - 文件的起始行必须指定脚本解释器的路径名,一般是绝对路径

! interpreter-path [optional-arg]

  1. ##### exec与文件描述符
  2. 默认由exec的调用程序打开的所有文件描述符在exec执行时会保持打开状态,且在新程序中依然有效,shell利用这一特性为其所执行的程序处理IO重定向

ls /tmp > dir.txt

  1. 以上shell命令执行了如下步骤:
  2. 1. 调用fork创建子进程
  3. 1. shell以描述符1(标准输出)打开文件用于输出
  4. 1. shell执行程序lsls将其结果输出到标准输出,亦即文件中
  5. 从安全角度考虑,执行新程序应该关闭不必要的文件描述符,使用close会有局限:
  6. - 某些描述符是库函数打开的,但库函数无法使主程序在执行exec之前关闭文件描述符,所以库函数应该总是为打开的文件设置close-on-exec标志
  7. - 如果exec调用失败可能还需要使描述符保持打开状态,如果这些描述符已经关闭,将它们重新打开几乎不可能
  8. 如果设置close-on-exec标志,执行exec时,会自动关闭该文件描述符,如果调用exec失败,则继续保持打开状态;当调用dupdup2fcntl为文件描述符创建副本时,总是会清除副本描述符的close-on-exec标志
  9. ##### exec与信号
  10. exec会将现有进程的文本段丢掉,也包含调用进程创建的信号处理程序,内核会将对所有已设信号的处置重置为SIG_DFL,而其他信号的处置则保持不变,在调用exec期间,进程信号掩码以及挂起信号的设置都得以保存
  11. ##### 执行shell命令:system

include

int system(const char *cmdstring);

``` UNIX中,system总是可用的,在其实现中调用了fork、waitpid和exec,有四种返回值:

  • 如果cmdstring为空,仅当system命令可用时,返回非0值,否则返回0,这可以确定系统是否支持system函数
  • fork失败或waitpid返回除了EINTR之外的出错,则system返回-1,且设置errno以指示错误类型
  • 如果exec失败(表示不能执行shell),返回值如同shell执行了exit(127)一样
  • 否则三个函数都成功,那么system返回shell的终止状态,格式在waitpid中已说明

使用system而不是使用fork和exec的优点:

  • 进行了所需的各种出错处理以及各种信号处理
  • 无需处理fork、waitpid和exec的调用细节

设置了用户ID和组ID的程序在特权模式下运行时,绝对不能调用system,以为你shell对操作的控制依赖于各种环境变量,因此这样会不可避免的给系统带来安全隐患

system的实现

简化版


system内部正确处理信号:
如果调用system的进程还创建了其他子进程,对SIGCHLD的信号处理函数也执行了wait,在shell创建的子进程退出并产生SIGCHLD信号,有机会调用waitpid之前,主程序的信号处理函数可能就会执行,即产生了竞争条件,两个不良后果:

  • 调用程序误以为为其所创建的某个子进程终止了
  • system函数无法获取其子进程的终止状态

system("sleep 20")在输入中断或退出字符,会发送信号给3个进程:调用system程序的进程、system锁创建的一个shell进程,sleep进程,shell在等待子进程期间会忽略SIGINT和SIGQUIT信号,默认会杀死其他两个进程,SUSv3规定:

  • 调用进程在执行命令期间应该忽略SIGINT和SIGQUIT信号
  • 子进程对这两个信号的处理,如同调用fork和exec一样,即对所有已设信号的处置重置为SIG_DFL,而其他信号的处置则保持不变

改进版


注意事项:

  • 针对”如果cmdstring为空,仅当system命令可用时,返回非0值,否则返回0“,唯一可靠的办法是递归调用system去运行shell命令,并检查返回状态
  • 只有system的调用者才需要阻塞SIGCHLD,同时忽略SIGINT和SIGQUIT,必须在fork之前执行
  • system的调用者必须使用waitpid,如果使用wait,可能捕获到其他子进程的状态