Unix之进程控制(八)

本文主体内容来自《UNIX环境高级编程第三版》。

本章介绍UNIX系统的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID——实际、有效和保存的用户ID和组ID,以及它们如何受到进程控制原语的影响。本章还包括了解释器文件和system函数。本章最后讲述大多数UNIX系统所提供的进程会计机制,这种机制使我们能够从另一个角度了解进程的控制功能。


进程标识

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

系统中有一些专用进程,但具体细节随实现而不同。ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是 init 进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX较新版本中是 /sbin/init。此进程负责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init 进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。

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

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
pid_t getpid(void); // 返回值:调用进程的进程ID

pid_t getppid(void); // 返回值:调用进程的父进程ID

uid_t getuid(void); // 返回值:调用进程的实际用户ID

uid_t geteuid(void); // 返回值:调用进程的有效用户ID

gid_t getgid(void); //返回值:调用进程的实际组ID

gid_t getegid(void); //返回值:调用进程的有效组ID

注意,这些函数都没有出错返回,在下一节讨论 fork 函数时,将进一步讨论父进程ID。


函数fork

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

1
2
3
#include <unistd.h>
pid_t fork(void);
// 返回值:子进程返回0,父进程返回子进程ID;若出错,返回−1

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

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全副本。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

文件共享

在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。

父进程和子进程共享同一个文件偏移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已重定向(很可能是由 shell 实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。

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

(1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。

(2)父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

  • 实际用户ID、实际组ID、有效用户ID、有效组ID

  • 附属组ID

  • 进程组ID

  • 会话ID

  • 控制终端

  • 设置用户ID标志和设置组ID标志

  • 当前工作目录

  • 根目录

  • 文件模式创建屏蔽字

  • 信号屏蔽和安排

  • 对任一打开文件描述符的执行时关闭(close-on-exec)标志

  • 环境

  • 连接的共享存储段

  • 存储映像

  • 资源限制

父进程和子进程之间的区别具体如下。

  • fork的返回值不同。

  • 进程ID不同。

  • 这两个进程的父进程ID不同。

  • 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0。

  • 子进程不继承父进程设置的文件锁。

  • 子进程的未处理闹钟被清除。

  • 子进程的未处理信号集设置为空集。

使fork失败的两个主要原因是:(a)系统中已经有了太多的进程(通常意味着某个方面出了问题),(b)该实际用户ID的进程总数超过了系统限制。CHILD_MAX 规定了每个实际用户ID在任一时刻可拥有的最大进程数。

fork有以下两种用法。

(1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。

(2)一个进程要执行一个不同的程序。这对 shell 是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

函数vfork

vfork函数的调用序列和返回值与fork相同,但两者的语义不同。可移植的应用程序不应该使用这个函数。

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序(如上一节末尾的(2)中一样)。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。这种优化工作方式在某些UNIX系统的实现中提高了效率,但如果子进程修改数据(除了用于存放vfork返回值的变量)、进行函数调用、或者没有调用 exec 或 exit 就返回都可能会带来未知的结果。

vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。)


函数exit

进程有5种正常终止及3种异常终止方式。5种正常终止方式具体如下。

(1)在main函数内执行return语句。等效于调用exit。

(2)调用exit函数。此函数由ISO C定义,其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。

(3)调用 _exit_Exit函数。ISOC定义 _Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准 I/O 流是否进行冲洗,这取决于实现。在 UNIX系统中,_Exit_exit 是同义的,并不冲洗标准 I/O 流。_exit 函数由 exit 调用,它处理UNIX系统特定的细节。_exit 是由POSIX.1说明的。

在大多数UNIX系统实现中, exit(3)是标准C库中的一个函数,而_exit(2)则是一个系统调用。

(4)进程的最后一个线程在其启动例程中执行return语句。但是,该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。

(5)进程的最后一个线程调用 pthread_exit 函数。如同前面一样,在这种情况中,进程终止状态总是0,这与传送给pthread_exit的参数无关。

3种异常终止具体如下。

(1)调用abort。它产生 SIGABRT 信号,这是下一种异常终止的一种特例。

(2)当进程接收到某些信号时。信号可由进程自身(如调用abort函数)、其他进程或内核产生。例如,若进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号。

(3)最后一个线程对“取消”(cancellation)请求作出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于 3个终止函数(exit_exit_Exit),将其退出状态作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid取得其终止状态。

对于父进程已经终止的所有进程,它们的父进程都改变为 init 进程。我们称这些进程由 init 进程收养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这种处理方法保证了每个进程有一个父进程。

内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(zombie)。ps 命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。

一个由init进程收养的进程终止时不会变成一个僵死进程,因为init被编写成无论何时只要有一个子进程终止, init 就会调用一个 wait 函数取得其终止状态。这样也就防止了在系统中塞满僵死进程。当提及“一个init的子进程”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init收养的进程。


函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略它。调用 waitwaitpid 的进程可能会发生:

  • 如果其所有子进程都还在运行,则阻塞。

  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。

  • 如果它没有任何子进程,则立即出错返回。

如果进程由于接收到 SIGCHLD 信号而调用 wait,我们期望wait会立即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

1
2
3
4
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
// 两个函数返回值:若成功,返回进程ID;若出错,返回0(见后面的说明)或−1

这两个函数的区别如下。

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一选项,可使调用者不阻塞。

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

这两个函数的参数 statloc 是一个整型指针。如果 statloc 不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

POSIX.1规定,终止状态用定义在 <sys/wait.h> 中的各个宏来查看。有4个互斥的宏可用来取得进程终止的原因,它们的名字都以 WIF开始。基于这4个宏中哪一个值为真,就可选用其他宏来取得退出状态、信号编号等。

img

对于waitpid函数中pid参数的作用解释如下。

pid ==−1 等待任一子进程。此种情况下,waitpid与wait等效。
pid > 0 等待进程ID与pid相等的子进程。
pid == 0 等待组ID等于调用进程组ID的任一子进程。
pid <−1 等待组ID等于pid绝对值的任一子进程。

waitpid 函数返回终止子进程的进程ID,并将该子进程的终止状态存放在由statloc指向的存储单元中。对于 waitpid,如果指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错。

options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是下图中常量按位或运算的结果。

img

waitpid函数提供了wait函数没有提供的3个功能。

(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。

(2)waitpid提供了一个 wait 的非阻塞版本。有时希望获取一个子进程的状态,但不想阻塞。

(3)waitpid通过 WUNTRACEDWCONTINUED 选项支持作业控制。


函数waitid、wait3、wait4

Single UNIX Specification包括了另一个取得进程终止状态的函数—waitid,此函数类似于waitpid,但提供了更多的灵活性。

1
2
3
#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
// 返回值:若成功,返回0;若出错,返回−1

与 waitpid 相似,waitid 允许一个进程指定要等待的子进程。但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程ID或进程组ID组合成一个参数。id参数的作用与idtype的值相关。该函数支持的 idtype 类型列在下图中。

img

WCONTINUEDWEXITEDWSTOPPED 这3个常量之一必须在 options 参数中指定。

infop 参数是指向 siginfo 结构的指针。该结构包含了造成子进程状态改变有关信号的详细信息。

大多数UNIX系统实现提供了另外两个函数wait3和wait4。它们提供的功能比POSIX.1函数wait、waitpid和waitid所提供功能的要多一个,这与附加参数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。

1
2
3
4
5
6
7
#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);
// 两个函数返回值:若成功,返回进程ID;若出错,返回−1

资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。下图列出了各个wait函数所支持的参数。

img


竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件(race condition)。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个。如果一个进程要等待其父进程终止,则可使用下列形式的循环:

1
2
while(getppid() != 1)
sleep(1);

这种形式的循环称为轮询(polling),它的问题是浪费了CPU时间,因为调用者每隔1 s都被唤醒,然后进行条件测试。

为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号发送和接收的方法。在UNIX 中可以使用信号机制。各种形式的进程间通信也可使用。


函数exec

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

有7种不同的exec函数可供使用,它们常常被统称为exec函数。用fork可以创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。这些是基本的进程控制原语。

1
2
3
4
5
6
7
8
9
#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[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
// 7个函数返回值:若出错,返回−1;若成功,不返回

这些函数之间的第一个区别是前4个函数取路径名作为参数,后两个函数取文件名作为参数,最后一个取文件描述符作为参数。指定 filename 作为参数时:

  • 如果filename中包含 /,则就将其视为路径名;

  • 否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。

PATH 变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。例如,下列 name=value 环境字符串指定在4个目录中进行搜索。

PATH=/bin:/usr/bin:/usr/local/bin:.

最后的路径前缀 . 表示当前目录。(零长前缀也表示当前目录。在value的开始处可用:表示,在行中间则要用::表示,在行尾以:表示。)

如果 execlpexecvp 使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个shell脚本,于是试着调用 /bin/sh,并以该filename作为shell的输入。

第二个区别与参数表的传递有关(l表示列表list,v表示矢量vector)。函数 execlexeclpexecle 要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外4个函数(execvexecvpexecvefexecve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这4个函数的参数。

最后一个区别与向新程序传递环境表相关。以e结尾的3个函数(execleexecvefexecve)可以传递一个指向环境字符串指针数组的指针。其他4个函数则使用调用进程中的environ变量为新程序复制现有的环境。通常,一个进程允许将其环境传播给其子进程,但有时也有这种情况,进程想要为子进程指定某一个确定的环境。例如,在初始化一个新登录的shell时,login程序通常创建一个只定义少数几个变量的特殊环境,而在我们登录时,可以通过shell启动文件,将其他变量加到环境中。

这7个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个参数表,它与字母v互斥。v表示该函数取一个argv[ ]矢量。最后,字母e表示该函数取envp[ ]数组,而不使用当前环境。下图显示了这7个函数之间的区别。

img

在很多UNIX实现中,这7个函数中只有 execve 是内核的系统调用。另外6个只是库函数,它们最终都要调用该系统调用。这7个函数之间的关系在下图中。

img


更改用户ID和更改组ID

在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户ID和组ID的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户ID或组ID,新ID不具有相应特权或访问这些资源的能力。

一般而言,在设计应用时,我们总是试图使用最小特权(least privilege)模型。依照此模型,我们的程序应当只具有为完成给定任务所需的最小特权。这降低了由恶意用户试图哄骗我们的程序以未预料的方式使用特权造成的安全性风险。

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

1
2
3
4
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
// 两个函数返回值:若成功,返回0;若出错,返回−1

关于谁能更改ID有若干规则。现在先考虑更改用户ID的规则(关于用户ID所说明的一切都适用于组ID)。

(1)若进程具有超级用户特权,则 setuid 函数将实际用户ID、有效用户ID以及保存的设置用户ID(saved set-user-ID)设置为 uid

(2)若进程没有超级用户特权,但是 uid 等于实际用户ID或保存的设置用户ID,则 setuid 只将有效用户ID设置为uid。不更改实际用户ID和保存的设置用户ID。

(3)如果上面两个条件都不满足,则 errno 设置为 EPERM,并返回−1。

下图总结了更改这3个用户ID的不同方法。

img

函数setreuid和setregid

1
2
3
4
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
// 两个函数返回值:若成功,返回0;若出错,返回-1

如若其中任一参数的值为−1,则表示相应的ID应当保持不变。

规则很简单:一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个设置用户ID程序交换成用户的普通权限,以后又可再次交换回设置用户ID权限。POSIX.1引进了保存的设置用户ID特性后,其规则也相应加强,它允许一个非特权用户将其有效用户ID设置为保存的设置用户ID。

函数seteuid和setegid

POIX.1包含了两个函数seteuid和setegid。它们类似于setuid和setgid,但只更改有效用户ID和有效组ID。

1
2
3
4
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
// 两个函数返回值:若成功,返回0;若出错,返回−1

一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置用户ID。对于一个特权用户则可将有效用户ID设置为uid。(这区别于setuid函数,它更改所有3个用户ID。)

下图给出了本节所述的更改3个不同用户ID的各个函数。

img


解释器文件

所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文件是文本文件,其起始行的形式是:

#! pathname [ optional-argument ]

在感叹号和pathname之间的空格是可选的。最常见的解释器文件以下列行开始:

#! /bin/sh

pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)。对这种文件的识别是由内核作为 exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行有长度限制。这包括#!、pathname、可选参数、终止换行符以及空格数。


函数system

ISO C定义了system函数,但是其操作对系统的依赖性很强。POSIX.1包括了system接口,它扩展了ISO C定义,描述了system在POSIX.1环境中的运行行为。

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

如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定在一个给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。

因为system在其实现中调用了 forkexecwaitpid,因此有3种返回值。

(1)fork失败或者waitpid返回除EINTR之外的出错,则system返回−1,并且设置 errno 以指示错误类型。

(2)如果 exec失败(表示不能执行 shell),则其返回值如同 shell执行了 exit 一样。

(3)否则所有3个函数(fork、exec和waitpid)都成功,那么system的返回值是shell的终止状态,其格式已在waitpid中说明。

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


进程会计

大多数UNIX系统提供了一个选项以进行进程会计(process accounting)处理。启用该选项后,每当进程结束时内核就写一个会计记录。典型的会计记录包含总量较小的二进制数据,一般包括命令名、所使用的CPU时间总量、用户ID和组ID、启动时间等。

一个至今没有说明的函数(acct)启用和禁用进程会计。唯一使用这一函数的是accton(8)命令(这是在几种平台上都类似的少数几条命令中的一条)。超级用户执行一个带路径名参数的accton命令启用会计处理。会计记录写到指定的文件中,在FreeBSD和Mac OS X中,该文件通常是/var/account/acct;在Linux中,该文件是/var/account/pacct;在Solaris中,该文件是/var/adm/pacct。执行不带任何参数的accton命令则停止会计处理。

会计记录结构定义在头文件<sys/acct.h>中,虽然每种系统的实现各不相同,但会计记录样式基本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef u_short comp_t;    /* 3-bit base 8 exponent; 13-bit fraction*/
struct acct
{
char  ac_flag;        /* flag (see Figure 8.26)*/
char  ac_stat;        /* termination status(signal & core flag only)*/
/* (Solaris only)*/
uid_t ac_uid;        /* real user ID*/
gid_t ac_gid;        /* real group ID*/
dev_t ac_tty;        /* controlling terminal*/
time_t ac_btime;       /* starting calendar time*/
comp_t ac_utime;       /* user CPU time*/
comp_t ac_stime;       /* system CPU time*/
comp_t ac_etime;       /* elapsed time*/
comp_t ac_mem;        /* average memory usage*/
comp_t ac_io;         /* bytes transferred (by read and write)*/
comp_t ac_rw;         /* blocks read or written*/
char  ac_comm[8];      /* command name: [8] for Solaris,*/
/* "blocks" on BSD systems*/
/* (not present on BSD systems)*/
/* [10] for Mac OS X, [16] for FreeBSD, and*/
/* [17] for Linux*/
};

用户标识

任一进程都可以得到其实际用户ID和有效用户ID及组ID。但是,我们有时希望找到运行该程序用户的登录名。我们可以调用 getpwuid(getuid()),但是如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,又将如何呢?(一个人在口令文件中可以有多个登录项,它们的用户 ID 相同,但登录 shell 不同。)系统通常记录用户登录时使用的名字,用 getlogin 函数可以获取此登录名。

1
2
3
#include <unistd.h>
char *getlogin(void);
// 返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL

如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。通常称这些进程为守护进程(daemon)。


进程调度

调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优先级运行(通过调整nice值降低它对CPU的占有,因此该进程是“友好的”)。只有特权进程允许提高调度权限。

POSIX实时扩展增加了在多个调度类别中选择的接口以进一步细调行为。我们这里只讨论用于调整nice值的接口,这些包括在POSIX.1的XSI扩展选项中。

Single UNIX Specification 中 nice 值的范围在 0~(2NZERO)-1 之间,有些实现支持 0~2NZERO。nice值越小,优先级越高。虽然这看起来有点倒退,但实际上是有道理的:你越友好,你的调度优先级就越低。 NZERO是系统默认的nice值。

注意,定义 NZERO 的头文件因系统而异。进程可以通过nice函数获取或更改它的nice值。使用这个函数,进程只能影响自己的nice值,不能影响任何其他进程的nice值。

1
2
3
#include <unistd.h>
int nice(int incr);
// 返回值:若成功,返回新的nice值NZERO;若出错,返回−1

incr 参数被增加到调用进程的 nice 值上。如果 incr 太大,系统直接把它降到最大合法值,不给出提示。类似地,如果 incr 太小,系统也会无声息地把它提高到最小合法值。由于−1是合法的成功返回值,在调用nice函数之前需要清楚 errno,在nice函数返回−1时,需要检查它的值。如果 nice 调用成功,并且返回值为−1,那么 errno 仍然为0。如果 errno 不为0,说明 nice 调用失败。

getpriority 函数可以像 nice 函数那样用于获取进程的nice值,但是 getpriority 还可以获取一组相关进程的nice值。

1
2
3
#include <sys/resource.h>
int getpriority(int which, id_t who);
// 返回值:若成功,返回-NZERO~NZERO-1之间的nice值;若出错,返回−1

which参数可以取以下三个值之一:PRIO_PROCESS 表示进程,PRIO_PGRP 表示进程组, PRIO_USER表示用户ID。which参数控制who参数是如何解释的,who参数选择感兴趣的一个或多个进程。如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值)。当which设为PRIO_USER并且who为0时,使用调用进程的实际用户ID。如果which参数作用于多个进程,则返回所有作用进程中优先级最高的(最小的nice值)。

setpriority 函数可用于为进程、进程组和属于特定用户ID的所有进程设置优先级。

1
2
3
#include <sys/resource.h>
int setpriority(int which, id_t who, int value);
// 返回值:若成功,返回0;若出错,返回−1

参数 whichwhogetpriority 函数中相同。value增加到NZERO上,然后变为新的nice值。


进程时间

可以度量的3个时间:墙上时钟时间、用户CPU时间和系统CPU时间。任一进程都可调用 times 函数获得它自己以及已终止子进程的上述值。

1
2
3
#include <sys/times.h>
clock_t times(struct tms *buf));
// 返回值:若成功,返回流逝的墙上时钟时间(以时钟滴答数为单位);若出错,返回-1

此函数填写由 buf 指向的 tms 结构,该结构定义如下:

1
2
3
4
5
6
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 */
};

注意,此结构没有包含墙上时钟时间。 times 函数返回墙上时钟时间作为其函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。例如,调用times,保存其返回值。在以后某个时间再次调用times,从新返回的值中减去以前返回的值,此差值就是墙上时钟时间。

该结构中两个针对子进程的字段包含了此进程用wait函数族已等待到的各子进程的值。所有由此函数返回的 clock_t 值都可用 _SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数sysconf(_SC_CLK_TCK))转换成秒数。