APUE - 进程控制

进程标识符

每个进程都有一个非负整型表示的唯一进程 ID。

虽然是唯一的,但是进程 ID 可以重用。当一个进程终止后,其进程 ID 就可以再次使用了。大多数 UNIX 系统实现延迟重用算法,使得赋予新建进程的 ID 不同于最近终止进程所使用的 ID。这防止了将新进程误认为是使用同一 ID 的某个已终止的先前进程。

除了进程 ID,每个进程还有一些其他的标识符。下列函数返回这些标识符。

#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);

fork 函数

一个现有进程可以调用 fork 函数创建一个新进程。

#include <unistd.h>
pid_t fork(void);

由 fork 创建的新进程被称为子进程。fork 函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是 0,父进程的返回值则是新子进程的进程 ID。

一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的。

文件共享

在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork 的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父、子进程的每个相同的打开描述符共享一个文件表项。

在 fork 之后处理文件描述符有两种常见的情况:

  • 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应更新。
  • 父、子进程各自执行不同的程序段。在这种情况下,在 fork 之后,父、子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。

vfork 函数

vfork 用于创建一个新进程,而该新进程的目的是 exec 一个新程序。vfork 与 fork 一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec 或 exit,于是也就不会存访该地址空间。相反,在子进程调用 exec 或 exit 之前,它在父进程的空间中运行。

vfork 和 fork 之间的另一个区别是:vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行。

wait 和 waitpid 函数

如果进程由于接收到 SIGCHLD 信号而调用 wait,则可期望 wait 会立即返回。但是如果在任意时刻调用 wait,则进程可能会阻塞。

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

这两个函数的区别如下:

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一个选项,可使调用者不阻塞。
  • waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

waitid 函数

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

wait3 和 wait4 函数

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

exec 函数

当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。exec 只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv[]);

在执行 exec 后,进程 ID 没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:

  • 进程 ID 和父进程 ID。
  • 实际用户 ID 和实际组 ID。
  • 附加组 ID。
  • 进程组 ID。
  • 会话 ID。
  • 控制终端。
  • 闹钟尚余留的时间。
  • 当前工作目录。
  • 根目录。
  • 文件模式创建屏蔽字。
  • 文件锁。
  • 进程信号屏蔽。
  • 未处理信号。
  • 资源限制。
  • tms_utime、tms_stime、tms_cutime 以及 tms_cstime 值。

对打开文件的处理与每个描述符的执行时关闭标志值有关。若此标志设置,则在执行 exec 时关闭该描述符,否则该描述符仍打开。除非特地用 fcntl 设置了该标志,否则系统的默认操作是在执行 exec 后仍保持这种描述符打开。

注意,在执行 exec 前后实际用户 ID 和实际组 ID 保持不变,而有效 ID 是否改变则取决于所执行程序文件的设置用户 ID 位和设置组 ID 位是否设置。如果新程序的设置用户 ID 位已设置,则有效用户 ID 变成程序文件所有者的 ID,否则有效用户 ID 不变。对组 ID 的处理方式与此相同。

更改用户 ID和组 ID

可以用 setuid 函数设置实际用户 ID 和有效用户 ID。与此类似,可以用 setgid 函数设置实际组 ID 和有效组 ID。

#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);

关于谁能更改 ID 有若干规则。

  1. 若进程具有超级用户特权,则 setuid 函数将实际用户 ID、有效用户 ID,以及保存的设置用户 ID 设置为 uid。
  2. 若进程没有超级用户特权,但是 uid 等于实际用户 ID 或保存的设置用户 ID,则 setuid 只将有效用户 ID 设置为 uid。不改变实际用户 ID 和保存的设置用户 ID。
  3. 如果上面两个条件都不满足,则将 errno 设置为 EPERM,并返回 -1。

在这里假定 _POSIX_SAVED_IDS 为真。如果没有提供这种功能,则上面所说的关于保存的设置用户 ID 部分都无效。

关于内核所维护的三个用户 ID,还要注意下列几点:

  1. 只有超级用户进程可以更改实际用户 ID。
  2. 仅当对程序文件设置了设置用户 ID 位时,exec 函数才会设置有效用户 ID。如果设置用户 ID 位没有设置,则 exec 函数不会改变有效用户 ID,而将其维持为原先值。
  3. 保存的设置用户 ID 是由 exec 复制有效用户 ID 而得来的。如果设置了文件的设置用户 ID 位,则在 exec 根据文件的用户 ID 设置了进程的有效用户 ID 以后,就将这个副本保存起来。

setreuid 和 setregid 函数

#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);

seteuid 和 setegid 函数

#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);

system 函数

#include <stdlib.h>
int system(const char *cmdstring);

使用 system 而不是直接使用 fork 和 exec 的优点是:system 进行了所需的各种出错处理,以及各种信号处理。

用户标识

系统通常记录用户登录时使用的名字,用 getlogin 函数可以获取登录名。

#include <unistd.h>
char *getlogin(void);

如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。

进程时间

我们可以测量的三种时间:墙上时钟时间、用户 CPU 时间和系统 CPU 时间。任一进程都可以调用 times 函数以获取它自己及已终止子进程的上述值。

#include <sys/times.h>
clock_t times(struct tms *buf);

struct tms {
    clock_t tms_utime;  /* user CPU time */
    clock_t tms_stime;  /* system CPU time */
    clock_t tms_cutime; /* user CPU time, terminated children */
    clock_t tms_cstime; /* system CPU time, terminated children */
};