进程控制
进程的创建
每个进程都有一个非负整数表示的唯一进程
ID
。- 所谓的唯一,即当前正在系统中运行的所有进程的
ID
各不相同 - 当一个进程
A
终止后,它的进程ID
可以复用
- 大多数UNIX系统实现的是延迟复用算法,使得新进程
B
的ID
不同于最近终止的进程A
的ID
- 大多数UNIX系统实现的是延迟复用算法,使得新进程
- 系统中有一些专用的进程
ID
为0的进程通常是调度进程,也称作交换进程。该进程是操作系统内核的一部分,并不执行任何磁盘上的程序,因此也称作是系统进程ID
为1的进程通常是init
进程,在自举过程结束时由内核调用。
- 该进程对应的程序文件为
/etc/init
,在较新的版本中是/sbin/init
文件 - 该进程负责在自举内核后启动一个UNIX系统
- 该进程通常读取与系统有关的初始化文件(
/etc/rc*
文件,/etc/inittab
文件以及/etc/init.d
中的文件),并经系统引导到一个状态 - 该进程永远不会终止
- 该进程是一个普通的用户进程(不是内核中的系统进程),但是它以超级用户特权运行
- 该进程对应的程序文件为
- 所谓的唯一,即当前正在系统中运行的所有进程的
获取进程的标识符:
#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
函数:创建一个新进程#include<unistd.h> pid_t fork(void);
- 返回值:
- 成功:
- 子进程返回 0
- 父进程返回子进程
ID
- 失败:返回 -1
- 成功:
注意:
- 如果
fork
调用成功,则它被调用一次,但是返回两次。 两次返回的区别是:子进程的返回值是0,父进程的返回值是新建子进程的进程ID
- 子进程返回值是 0 的理由:一个进程总可以通过
getpid
知道它的进程ID
,通过getppid
知道它的父进程的ID
- 父进程返回值是子进程的进程
ID
的理由:一个进程的子进程可以有多个,但是并没有函数可以获取它的子进程的ID
- 子进程返回值是 0 的理由:一个进程总可以通过
- 子进程是父进程的一份一模一样的拷贝,如子进程获取了父进程数据空间、堆、栈的副本。
- 父子进程共享正文段(因为正文段是只读的)
- 父子进程并不共享这些数据空间、堆、栈
- 子进程和父进程都从
fork
调用之后的指令开始执行。也就是子进程从出生开始,就跟父进程处于同样的状态 - 由于创建子进程的目的通常是为了完成某个任务,因此
fork
之后经常跟随exec
,所以很多操作系统的实现并不执行一个父进程数据段、堆和栈的完全拷贝,而是使用写时赋值技术(copy-on-write:COW
)
- 这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读
- 如果父子进程中有一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本
- 通常
fork
之后,是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的进程调度算法 - 注意标准
IO
库的跨fork
行为。由于标准IO
库是带缓冲的,因此在fork
调用之后,这些缓冲的数据也被拷贝到子进程中 - 父进程的所有打开的文件描述符都被复制到子进程中。父进程和子进程每个相同的打开描述符共享同一个文件表项
- 更重要的是:父进程和子进程共享同一个文件偏移量
- 如果父进程和子进程写同一个描述符指向的文件,但是又没有任何形式的同步,则它们的输出会相互混合
- 如果父进程
fork
之后的任务就是等待子进程完成,而不作任何其他的事情,则父进程和子进程无需对打开的文件描述符做任何处理。因为此时只有子进程处理文件 - 如果父进程
fork
之后,父进程与子进程都有自己的任务要处理,则此时父进程和子进程需要各自关闭它们不需要使用的文件描述符,从而避免干扰对方的文件操作
- 如果父进程
- 除了打开的文件描述符之外,子进程还继承了父进程的下列属性:实际用户
ID
、实际组ID
、有效用户ID
、有效组ID
、附属组ID
、进程组ID
、会话ID
、控制终端、设置用户ID
标志和设置组ID
标志、当前工作目录、根目录、文件模式创建屏蔽字、信号屏蔽和信号处理、对任一打开文件描述符的执行时关闭标志、环境、连接的共享存储段、存储映像、资源限制 - 父进程和子进程的区别为:
fork
返回值不同- 进程
ID
不同 - 进程父进程
ID
不同 - 子进程的
tms_utime,tms_stime,tms_cutime,tms_ustime
的值设置为0 - 子进程不继承父进程设置的文件锁
- 子进程的未处理闹钟被清除
- 子进程的未处理信号集设置为空集
fork
失败的零个主要原因:
- 系统已经有了太多的进程
- 实际用户
ID
的进程总数超过了系统的限制(CHILD_MAX
规定了每个实际用户ID
在任何时刻拥有的最大进程数)
- 返回值:
fork
有两种用法:- 父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务中很常见:父进程等待请求,然后调用
fork
并使子进程处理请求 - 父进程要执行一个不同的程序。在
shell
是很常见。此时子进程从fork
返回之后立即调用exec
- 父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务中很常见:父进程等待请求,然后调用
vfork
函数的调用序列和返回值与fork
相同,但是二者语义不同:
vfork
用于创建一个新进程,该新进程的目的是exec
一个新程序,所以vfork
并不将父进程的地址空间拷贝到子进程中。
vfork
的做法是:在调用exec
或者exit
之前,子进程在父进程的空间中运行
所以在
exec
或者exit
之前,子进程可以篡改父进程的数据空间vfork
保证子进程优先运行,在子进程调用exec
或者exit
之后父进程才可能被调度运行
当子进程调用exec
或者exit
中的任何一个时,父进程会恢复运行,在此之前内核会使父进程处于休眠状态示例:
进程的终止
进程有 8 种方式使得进程终止,其中 5 种为正常终止,3 种异常终止:
- 正常终止方式:
- 从
main
函数返回,等效于exit
- 调用
exit
函数。exit
会调用各终止处理程序,然后关闭所有标准IO流 - 调用
_exit
函数或者_Exit
函数。它们不运行终止处理程序,也不冲洗标志IO流 - 多线程的程序中,最后一个线程从其启动例程返回。但是该线程的返回值并不用做进程的返回值,进程是以终止状态 0 返回的
- 多线程的程序中,从最后一个线程调用
pthread_exit
函数。进程也是以终止状态 0 返回的
- 从
- 异常终止方式:
- 调用
abort
函数。它产生SIGABRT
信号 - 接收到一个信号
- 多线程的程序中,最后一个线程对取消请求作出响应
- 调用
更进一步的:
- 不管进程如何终止,最后都会执行内核中的同一段代码:这段代码为相应进程关闭所有打开的描述符(不仅仅是文件描述符),释放它所使用的内存。
不管进程如何终止,我们需要有一种方法来通知父进程,本进程是如何终止的。
- 对于
exit,_exit,_Exit
这三种情况:将本进程的退出状态作为参数传给函数,并且在最后调用_exit
时,内核将退出状态转换成终止状态
exit
函数和_Exit
函数最终调用的是_exit
函数- 对于
- 对于异常终止情况,内核产生一个指示异常终止原因的终止状态
在任意一种情况下,终止进程的父进程都能够用wait
或者waitpid
函数取得终止状态。然后父进程能够检测终止状态。如果发现子进程是正常终止,则可以从终止状态中提取出退出状态- 正常终止方式:
如果父进程在子进程之前终止,那么内核会将该子进程的父进程改变为
init
进程,称作由init
进程收养。其原理为:- 在一个进程终止时,内核逐个检查所有活动进程,以判断这些活动进程是否是正要终止的进程的子进程
- 如果是,则该活动进程的父进程
ID
就改为 1
这种方式确保了每个进程都有一个父进程
内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用
wait
函数或者waitpid
函数时,可以得到这些信息。- 这些信息至少包括:终止进程的进程
ID
、终止进程的终止状态、终止进程的使用的CPU时间总量 - 内核此时可以释放终止进程使用的所有内存,关闭它所有的打开文件。但是该终止进程还残留了上述信息等待父进程处理
- 我们称一个已经终止、但是等待父进程对它进行善后处理的进程称作僵死进程,在
ps
命令中显示为Z
- 所谓善后处理,就是父进程调用
wait
函数或者waitpid
函数读取终止进程的残留信息 - 一旦父进程进行了善后处理,则终止进程的所有占用资源(包括残留信息)都得到释放,该进程被彻底销毁
- 所谓善后处理,就是父进程调用
- 对于
init
超级进程,它被设计成:任何时候只要有一个子进程终止,就立即调用wait
函数取得其终止状态。这种做法防止系统中塞满了僵死进程
- 这些信息至少包括:终止进程的进程
当一个进程终止时,内核就向其父进程发送
SIGCHLD
信号。这种信号是一个异步信号,因为该信号可能在任何时间发出- 父进程可以选择忽略此信号。这是系统的默认行为
- 父进程也可以针对此信号注册一个信号处理程序,从而当接收到该信号时调用相应的信号处理程序
wait/waitpid
函数:#include<sys/wait.h> pid_t wait(int *staloc); pid_t waitpid(pid_t pid,int *staloc,int options);
参数:
staloc
:存放子进程终止状态的缓冲区的地址。如果你不关心子进程的终止状态,则可以设它为空指针NULL
对于
waitpid
函数:
pid
:
- 如果
pid==-1
:则等待任意一个子进程终止 - 如果
pid>0
:则等待进程ID
等于pid
的那个子进程终止 - 如果
pid==0
:则等待组ID
等于调用进程组ID
的任一子进程终止 - 如果
pid<0
:等待组ID
等于pid
绝对值的任一子进程终止
- 如果
options
:或者是0,或者是下列常量按位或的结果:
WNOHANG
:没有指定的子进程终止时,并不阻塞程序的执行WUNTRACED
:执行作业控制。若操作系统支持作业控制,则由pid
指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态WCONTINUED
:执行作业控制。若操作系统支持作业控制,则由pid
指定的任一子进程已处于停止状态,并且其状态自停止以来尚未报告过,则返回其状态
进程的停止状态:类似于暂停。它不同于终止状态
- 返回值:
- 成功:返回终止子进程的进程
ID
- 失败:返回 0 或者 -1
- 成功:返回终止子进程的进程
注意:
wait
的语义是等待任何一个子进程终止:- 如果当前进程的所有子进程都还在运行,则阻塞
- 如果有一个子进程已终止,正在等待父进程获取其终止状态,则当前进程取得该子进程的终止状态并立即返回
- 如果当前进程没有任何子进程,则立即出错返回
waitpid
的语义是等待指定的子进程终止:- 如果当前进程的所有子进程都在运行:
- 如果
options
指定为WNOHANG
,则waitpid
并不阻塞,而是立即返回 0 - 如果
options
未指定为WNOHANG
,则waitpid
阻塞
- 如果
- 如果指定
pid
的子进程已终止,正在等待父进程获取其终止状态,则当前进程取得该子进程的终止状态并立即返回 - 如果指定的
pid
有问题(如不存在,或者不是当前进程的子进程),则立即出错返回
- 如果当前进程的所有子进程都在运行:
对于出错的情况:
wait
出错的原因是:
- 调用进程没有子进程
- 函数调用(正在阻塞中)被一个信号中断
waitpid
出错的原因是:
- 指定的进程或者进程组不存在
pid
指定的进程不是调用进程的子进程- 函数调用(正在阻塞中)被一个信号中断
- 可以通过宏从终止状态中取得退出状态以及终止原因等:
WIFEXITED(status)
:如果子进程正常终止,则为真。此时可以执行WEXITSTATUS(status)
获取子进程的退出状态的低 8 位WIFSIGNALED(status)
:如果子进程异常终止,则为真。此时可以执行WTERMSIG(status)
获取使得子进程终止的信号编号WIFSTOPPED(status)
:如果子进程的当前状态为暂停,则为真。此时可执行WSTOPSIG(status)
获取使得子进程暂停的信号编号WIFCONTINUED(status)
:如果子进程在暂停后已经继续执行了,则为真。
waitid
函数:它类似waitpid
,但是提供了更灵活的参数#include<sys/wait.h> int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);
- 参数:
idtype
:指定了id
类型,可以为下列常量
P_PID
:等待特定进程。此时id
表示要等待的子进程的进程ID
P_GID
:等待属于特定进程组的任一子进程。此时id
表示要等待的进程组ID
P_ALL
:等待任一子进程。此时忽略id
id
:指定的进程id
或者进程组id
infop
:一个缓冲区的地址。该缓冲区由waitid
填写,存放了造成子进程状态改变的有关信号的详细信息options
:指示调用者关心哪些状态变化。可以是下列常量的按位或:
WCONTINUED
:等待这样的子进程:它以前曾被停止过,此后又继续执行,但是其状态尚未报告WEXITED
:等待已经终止的子进程WNOHANG
:如无可用的子进程终止状态,立即返回而不是阻塞WNOWAIT
:不破坏子进程的终止状态,该子进程的终止状态可以由后续的wait,waitid,waitpid
调用取得WSTOPPED
:等待这样的子进程:它已经停止,但是其状态尚未报告
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1
- 参数:
wait3/wait4
函数:可以返回终止子进程及其子子进程的资源使用情况#include<sys/types.h> #include<sys/wait.h> #include<sys/time.h> #include<sys/resource.h> pid_t wait3(int *staloc,int options,struct rusage *rusage); pid_t wait4(pid_t pid,int *staloc,int options,struct rusage *rusage);
参数:
staloc
:存放子进程终止状态的缓冲区的地址。如果你不关心子进程的终止状态,则可以设它为空指针NULL
rusage
:一个缓冲区的地址,该缓冲区存放由wait3,wait4
返回的终止子进程的资源统计信息,包括:用户CPU时间总量、系统CPU时间总量、缺页次数、接收到的信号的次数等
pid
和options
参数与waitpid
相同- 返回值:
- 成功:返回终止子进程的进程
ID
- 失败:返回 -1
- 成功:返回终止子进程的进程
如果
fork
之后的逻辑依赖于父进程还是子进程先执行,则产生了竞争条件。- 可以使用进程间通信机制解决这类竞争问题
exec
当进程调用一种
exec
函数时,该进程执行的程序完全替换成新程序,而新程序则从main
函数开始执行- 调用
exec
前后,进程ID
并未改变。因为exec
并不创建新进程 exec
只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
- 调用
有7种不同的
exec
函数可以供使用,它们被统称称作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[]); int fexecve(int fd,char *const argv[],char *const evnp[]);
返回值:
- 若成功:不返回
- 若失败:返回 -1
这几个函数的区别:
- 前四个函数取路径名作为参数;后两个函数取文件名作为参数;最后一个取文件描述符做参数
- 若
filename
中包含/
,则视为路径名 - 若
filename
不包含/
,则按照PATH
环境变量指定的各个目录中搜寻可执行文件
- 若
- 函数
execl,execlp,execle
要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾;函数execv,execvp,execve,fexecve
应先构造一个指向各参数的指针数组,然后将该指针数组的地址作为参数
l
表示列表list
v
表示矢量vector
l
形式中,必须以空指针结尾,否则新程序根本不知道要读取多少个参数。空指针就是命令行参数序列终止的标记v
形式中,数组的最后一个元素必须是空指针,否则报错。
- 以
e
结尾的execle,execve,fexecve
可以传递一个指向环境字符串指针数组的指针。注意这个数组的最后一个元素必须是空指针,否则报错。其他四个函数则使用调用进程的environ
变量为新程序复制现有的环境
注意:
- 操作系统对参数表和环境表的总长度有一个限制。在
POSIX
中,这个值至少是 4096 字节 - 执行
exec
之后,新程序的进程ID不变,进程的大多数属性不变。但是对打开文件的处理要注意:
- 进程中每个打开的文件描述符都有一个执行时关闭标志。若设置了此标志,则执行
exec
时会关闭该文件描述符;否则该文件描述符仍然保持打开。系统默认行为是不设置执行时关闭标志
- 进程中每个打开的文件描述符都有一个执行时关闭标志。若设置了此标志,则执行
- 执行
exec
之后,进程的实际用户 ID 和实际组 ID不变,但是进程的有效用户 ID 要注意:
- 进程的有效用户 ID 和有效组 ID 是否改变取决于所执行程序文件的设置用户 ID 和设置组 ID 位是否设置。
- 若程序文件的设置用户 ID 位已设置,则进程的有效用户 ID 变成程序文件所有者的 ID;否则有效用户 ID 不变
- 若程序文件的设置组 ID 位已设置,则进程的有效组 ID 变成程序文件所有组的 ID;否则有效组 ID 不变
- 进程的有效用户 ID 和有效组 ID 是否改变取决于所执行程序文件的设置用户 ID 和设置组 ID 位是否设置。
- 在很多UNIX操作系统中,这7个函数只有
execve
是内核的系统调用。另外 6 个只是库函数。它们最终都要调用该系统
system
函数:在程序中执行一个命令字符串#include<stdlib.h> int system(const char *cmdstring);
- 参数:
cmdstring
:命令字符串(在shell
中执行),如"ps -aux"
- 返回值:
- 有三种返回值。见下面描述
system
用于将一个字符作为命令来执行。它等同于同时调用了fork、exec、waitpid
。有三种返回值:fork
失败或者waitpid
返回除了EINTR
之外的错误,则system
返回 -1,并且设置errno
以指示错误类型- 如果
exec
失败(表示不能执行shell
),则其返回值如同shell
执行了exit(127)
一样 - 如果三个函数都执行成功,则
system
返回值是shell
的终止状态,其格式在waitpid
中说明
system
对操作系统依赖性很强。目前在UNIX
操作系统上,system
总是可用的。如果cmdstring
为空指针,则如果system
返回 0 表示该操作系统不支持system
函数;否则支持。system
相较于fork+exec
的优点是:system
进行了所需的各种出错处理以及各种信号处理。缺点是:一旦调用system
的进程具有超级用户权限,则system
执行的命令也具有超级用户权限。因为
system
的实现过程中并没有更改有效用户ID和实际用户ID的操作。- 因此如果一个进程以特殊的权限运行,而它又想生成另一个进程执行另外一个程序,则它应该直接使用
fork_exec
并且在fork
之后,exec
之前改回普通权限。 - 设置用户
ID
和设置组ID
程序绝不应该调用system
函数
- 参数:
更改用户ID和更改组ID
在设计应用程序时,应该使用最小特权模型:程序应当只具有为完成给定认为所需的最小的特权
- 当进程需要增加特权或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或者组ID,使得新ID具有合适的特权或者访问权限
- 当前进程需要降低其特权或者阻止对某些资源的访问时,也需要更换用户ID或者组ID,新ID不具有相应的特权
- 进程在大部分时候都是最低特权运行。只有到必要的时候提升特权访问资源,一旦资源访问完毕立即降低特权
setuid/setgid
函数:设置实际用户ID和有效用户ID/ 实际组ID和有效组ID#include<unistd.h> int setuid(uid_t uid); int setgid(gid_t gid);
- 参数:
uid
:待设置的用户ID
gid
:待设置的组ID
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1
设置的规则为:
- 如果进程具有超级用户特权,则
setuid
函数将实际用户ID
,有效用户ID
以及保存的设置用户ID
(saved set-user-ID
) 全部设置为uid
(此时uid
没有限制) - 如果进程没有超级用户特权,但是
uid
等于实际用户ID
或者保存的设置用户ID
,则setuid
只会将有效用户ID
设置为uid
,不改变实际用户ID
和保存的设置用户ID
- 如果上面两个条件都不满足,则
errno
设置为EPERM
并返回 -1 - 上述讨论中,假设
_POSIX_SAVED_IDS
为真。如果为提供此功能,则对于保存的设置用户ID
部分都无效 - 针对
setgid
的讨论类似setuid
- 参数:
操作系统内核为每个进程维护3个用户
ID
:实际用户ID
、有效用户ID
、保存的设置用户ID
:- 只有超级用户进程可以更改实际用户
ID
- 通常是基用户
ID
是在用户登录时,由login
程序设置的,而且绝不会改变它。login
是一个超级用户进程,当它调用setuid
时,设置所有的3个用户ID
- 通常是基用户
- 仅当对程序文件设置了设置用户
ID
时,exec
函数才设置有效用户ID
。如果程序文件的设置用户ID
位没有设置,则exec
函数不会改变有效用户ID
,而是维持其现有值
- 任何时候都可以调用
setuid
将有效用户ID
设置为实际用户ID
或者保存的设置用户ID
- 调用
setuid
时,有效用户ID
不能随意取值,只能从实际用户ID
或者保存的设置用户ID
中取得
- 任何时候都可以调用
- 保存的设置用户
ID
是由exec
复制有效用户ID
而得到。如果设置了程序文件的设置用户ID
位,则exec
根据文件的用户ID
设置了进程的有效用户ID
之后,这个副本就保存起来 - 目前可以通过
getuid
获取进程的当前实际用户ID
,可以通过geteuid
获取进程的当前有效用户ID
,但是没有函数获取进程当前的保存的设置用户ID
- 只有超级用户进程可以更改实际用户
POSIX
提供了两个函数:#include<unistd.h> int seteuid(uid_t uid); int setegid(gid_t gid);
- 参数:
uid
:待设置的有效用户ID
gid
:待设置的有效组ID
- 返回值:
- 成功: 返回 0
- 失败: 返回 -1
seteuid
只修改进程的有效用户ID
;setegid
只修改进程的有效组ID
。- 如果进程具有超级用户权限,则
seteuid
将设置进程的有效用户ID
为uid
(此时uid
没有限制) - 如果进程没有超级用户权限,则
seteuid
只能将进程的有效用户ID
设置为它的实际用户ID
或者保存的设置用户ID
- 针对
setegid
的讨论类似seteuid
- 参数:
getlogin
:获取运行该程序的用户的登录名#include<unistd.h> char *getlogin(void)
- 返回值:
- 成功:返回指向登录名字符串的指针
- 失败:返回
NULL
通常失败的原因是:进程的用户并没有登录到系统。比如守护进程。
- 返回值:
进程会计
- 大多数
UNIX
系统提供了一个选项以进行进程会计处理
- 启用该选项后,每当进程结束时内核就会写一个会计记录
- 超级用户执行命令
accton pathname
则会启用会计处理,会计记录会写到pathname
指定的文件中
- 如果不带文件名参数,会停止会计处理
- 会计记录文件是个二进制文件,包含的会计记录是二进制数据
会计记录结构定义在
<sys/acct.h>
头文件中。虽然各个操作系统的实现可能有差别,但是基本数据如下:typedef u_short comp_t; struct acct { char ac_flag; //标记 char ac_stat; //终止状态 uid_t ac_uid; //真实用户ID gid_t ac_gid; //真实组ID dev_t ac_tty; // 控制终端 time_t ac_btime;// 起始的日历时间 comp_t ac_utime;// 用户 CPU 时间 comp_t ac_stime;// 系统 CPU 时间 comp_t ac_etime;// 流逝时间 comp_t ac_mem; // 平均内存使用 comp_t ac_io; // `read`和`write`字节数量 comp_t ac_rw; // `read`和`write`的块数 char ac_comm[8];//命令名。对于LINUX ,则是 ac_comm[17] };
ac_flag
记录了进程执行期间的某些事件:
AFORK
:进程是由fork
产生的,但从未调用exec
ASU
:进程使用超级用户特区ACORE
:进程转储core
(转储core
的字节并不计算在会计记录内)AXSIG
:进程由一个信号杀死
- 在大多数平台上,时间是以时钟滴答数来记录的
- 会计记录所需的所有数据都由内核保存在进程表中,并在一个新进程被创建时初始化
- 进程终止时,会写一个会计记录。这产生两个后果:
- 我们不能获取永远不终止的进程的会计记录。因此
init
进程以及内核守护进程不会产生会计记录 - 在会计文件中记录的顺序对应的是进程终止的顺序,而不是他们启动的顺序
- 我们不能获取永远不终止的进程的会计记录。因此
- 会计记录对应的是进程而不是程序。因此如果一个进程顺序的执行了3个程序 :
A exec B, B exec C
,则只会写一个会计记录。在该记录中的命令名对应于程序C
,但是CPU
时间是程序A,B,C
之和
times
函数:任何进程都可以用该函数获取它自己以及已经终止子进程的运行时间#include<sys/times.h> clock_t times(struct tms *buf);
- 参数:
buf
:执行tms
结构的指针。该结构由times
填写并返回
- 返回值:
- 成功:返回流逝的墙上始终时间(以始终滴答数为单位)
- 失败:返回 -1
一个进程可以度量的有3个时间:
- 墙上时钟流逝的时间。从进程从开始运行到结束时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间
- 用户
CPU
时间:用户进程获得了CPU资源以后,在用户态执行的时间
与用户进程对应的是内核进程
系统
CPU
时间:用户进程获得了CPU资源以后,在内核态的执行时间进程的三种状态为阻塞、就绪、运行
- 墙上时钟流逝的时间 = 阻塞时间 + 就绪时间 +运行时间
- 用户CPU时间 = 运行状态下用户空间的时间
- 系统CPU时间 = 运行状态下系统空间的时间
- 用户CPU时间+系统CPU时间=运行时间
times
函数就是获取进程的这几个时间的。这里的tms
结构定义为:struct tms{ clock_t tms_utime; //用户 CPU 时间 clock_t tms_stime; //系统 CPU 时间 clock_t tms_cutime; //终止的子进程的用户 CPU 时间的累加值 clock_t tms_cstime; //终止的子进程的系统 CPU 时间的累加值
注意:
- 墙上时钟是相对于过去某个时刻度量的,所以不能用其绝对值而必须用相对值。通常的用法是:调用两次
times
,然后取两次墙上时钟的差值 tms_cutime
和tms_cstime
包含了wait
函数族已经等待到的各个子进程的值clock_t
可以使用_SC_CLK_TCK
(用sysconf
函数)转换成秒数
- 参数: