《UNIX环境高级编程》--8进程控制

进程控制

进程的创建

  1. 每个进程都有一个非负整数表示的唯一进程 ID

    • 所谓的唯一,即当前正在系统中运行的所有进程的ID各不相同
    • 当一个进程A终止后,它的进程 ID 可以复用
      • 大多数UNIX系统实现的是延迟复用算法,使得新进程BID不同于最近终止的进程AID
    • 系统中有一些专用的进程
      • ID为0的进程通常是调度进程,也称作交换进程。该进程是操作系统内核的一部分,并不执行任何磁盘上的程序,因此也称作是系统进程
      • ID为1的进程通常是init进程,在自举过程结束时由内核调用。
        • 该进程对应的程序文件为/etc/init,在较新的版本中是/sbin/init文件
        • 该进程负责在自举内核后启动一个UNIX系统
        • 该进程通常读取与系统有关的初始化文件(/etc/rc*文件,/etc/inittab文件以及/etc/init.d中的文件),并经系统引导到一个状态
        • 该进程永远不会终止
        • 该进程是一个普通的用户进程(不是内核中的系统进程),但是它以超级用户特权运行
  2. 获取进程的标识符:

    
    #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
    • 这些函数都没有出错返回
  3. fork函数:创建一个新进程

    
    #include<unistd.h>
    
    pid_t fork(void);
    • 返回值:
      • 成功:
        • 子进程返回 0
        • 父进程返回子进程ID
      • 失败:返回 -1

    注意:

    • 如果fork调用成功,则它被调用一次,但是返回两次。 两次返回的区别是:子进程的返回值是0,父进程的返回值是新建子进程的进程ID
      • 子进程返回值是 0 的理由:一个进程总可以通过getpid知道它的进程ID,通过getppid知道它的父进程的ID
      • 父进程返回值是子进程的进程ID的理由:一个进程的子进程可以有多个,但是并没有函数可以获取它的子进程的ID
    • 子进程是父进程的一份一模一样的拷贝,如子进程获取了父进程数据空间、堆、栈的副本。
      • 父子进程共享正文段(因为正文段是只读的)
      • 父子进程并不共享这些数据空间、堆、栈
    • 子进程和父进程都从fork调用之后的指令开始执行。也就是子进程从出生开始,就跟父进程处于同样的状态
    • 由于创建子进程的目的通常是为了完成某个任务,因此fork之后经常跟随exec,所以很多操作系统的实现并不执行一个父进程数据段、堆和栈的完全拷贝,而是使用写时赋值技术(copy-on-write:COW
      • 这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读
      • 如果父子进程中有一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本
    • 通常fork之后,是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的进程调度算法
    • 注意标准IO库的跨fork行为。由于标准IO库是带缓冲的,因此在fork调用之后,这些缓冲的数据也被拷贝到子进程中
    • 父进程的所有打开的文件描述符都被复制到子进程中。父进程和子进程每个相同的打开描述符共享同一个文件表项
      • 更重要的是:父进程和子进程共享同一个文件偏移量
      • 如果父进程和子进程写同一个描述符指向的文件,但是又没有任何形式的同步,则它们的输出会相互混合
        • 如果父进程fork之后的任务就是等待子进程完成,而不作任何其他的事情,则父进程和子进程无需对打开的文件描述符做任何处理。因为此时只有子进程处理文件
        • 如果父进程fork之后,父进程与子进程都有自己的任务要处理,则此时父进程和子进程需要各自关闭它们不需要使用的文件描述符,从而避免干扰对方的文件操作
          child_process_file
    • 除了打开的文件描述符之外,子进程还继承了父进程的下列属性:实际用户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在任何时刻拥有的最大进程数)
  4. fork有两种用法:

    • 父进程希望复制自己,使父进程和子进程同时执行不同的代码段。在网络服务中很常见:父进程等待请求,然后调用fork并使子进程处理请求
    • 父进程要执行一个不同的程序。在shell是很常见。此时子进程从fork返回之后立即调用exec
  5. vfork函数的调用序列和返回值与fork相同,但是二者语义不同:

    • vfork用于创建一个新进程,该新进程的目的是exec一个新程序,所以vfork并不将父进程的地址空间拷贝到子进程中。
      • vfork的做法是:在调用exec或者exit之前,子进程在父进程的空间中运行

    所以在exec或者exit之前,子进程可以篡改父进程的数据空间

  6. vfork保证子进程优先运行,在子进程调用exec或者exit之后父进程才可能被调度运行
    当子进程调用exec或者exit中的任何一个时,父进程会恢复运行,在此之前内核会使父进程处于休眠状态
  7. 示例:

进程的终止

  1. 进程有 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函数取得终止状态。然后父进程能够检测终止状态。如果发现子进程是正常终止,则可以从终止状态中提取出退出状态
  2. 如果父进程在子进程之前终止,那么内核会将该子进程的父进程改变为init进程,称作由init进程收养。其原理为:

    • 在一个进程终止时,内核逐个检查所有活动进程,以判断这些活动进程是否是正要终止的进程的子进程
    • 如果是,则该活动进程的父进程ID就改为 1

    这种方式确保了每个进程都有一个父进程

  3. 内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait函数或者waitpid函数时,可以得到这些信息。

    • 这些信息至少包括:终止进程的进程ID、终止进程的终止状态、终止进程的使用的CPU时间总量
    • 内核此时可以释放终止进程使用的所有内存,关闭它所有的打开文件。但是该终止进程还残留了上述信息等待父进程处理
    • 我们称一个已经终止、但是等待父进程对它进行善后处理的进程称作僵死进程,在ps命令中显示为Z
      • 所谓善后处理,就是父进程调用wait函数或者waitpid函数读取终止进程的残留信息
      • 一旦父进程进行了善后处理,则终止进程的所有占用资源(包括残留信息)都得到释放,该进程被彻底销毁
    • 对于init超级进程,它被设计成:任何时候只要有一个子进程终止,就立即调用wait函数取得其终止状态。这种做法防止系统中塞满了僵死进程
  4. 当一个进程终止时,内核就向其父进程发送SIGCHLD信号。这种信号是一个异步信号,因为该信号可能在任何时间发出

    • 父进程可以选择忽略此信号。这是系统的默认行为
    • 父进程也可以针对此信号注册一个信号处理程序,从而当接收到该信号时调用相应的信号处理程序
  5. 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指定的任一子进程已处于停止状态,并且其状态自停止以来尚未报告过,则返回其状态

      进程的停止状态:类似于暂停。它不同于终止状态

  6. 返回值:
    • 成功:返回终止子进程的进程ID
    • 失败:返回 0 或者 -1
  7. 注意:

    • 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):如果子进程在暂停后已经继续执行了,则为真。
  8. 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
  9. 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时间总量、缺页次数、接收到的信号的次数等

      pidoptions参数与waitpid相同

    • 返回值:
      • 成功:返回终止子进程的进程ID
      • 失败:返回 -1
  10. 如果fork之后的逻辑依赖于父进程还是子进程先执行,则产生了竞争条件。

    • 可以使用进程间通信机制解决这类竞争问题

exec

  1. 当进程调用一种exec函数时,该进程执行的程序完全替换成新程序,而新程序则从main函数开始执行

    • 调用exec前后,进程ID并未改变。因为exec并不创建新进程
    • exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段
  2. 有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 不变
    • 在很多UNIX操作系统中,这7个函数只有execve是内核的系统调用。另外 6 个只是库函数。它们最终都要调用该系统
  3. 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

  1. 在设计应用程序时,应该使用最小特权模型:程序应当只具有为完成给定认为所需的最小的特权

    • 当进程需要增加特权或需要访问当前并不允许访问的资源时,我们需要更换自己的用户ID或者组ID,使得新ID具有合适的特权或者访问权限
    • 当前进程需要降低其特权或者阻止对某些资源的访问时,也需要更换用户ID或者组ID,新ID不具有相应的特权
    • 进程在大部分时候都是最低特权运行。只有到必要的时候提升特权访问资源,一旦资源访问完毕立即降低特权
  2. 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. 操作系统内核为每个进程维护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
  4. POSIX提供了两个函数:

    
    #include<unistd.h>
    
    int seteuid(uid_t uid);
    int setegid(gid_t gid);
    • 参数:
      • uid:待设置的有效用户ID
      • gid:待设置的有效组ID
    • 返回值:
      • 成功: 返回 0
      • 失败: 返回 -1

    seteuid只修改进程的有效用户IDsetegid只修改进程的有效组ID

    • 如果进程具有超级用户权限,则seteuid将设置进程的有效用户IDuid(此时uid没有限制)
    • 如果进程没有超级用户权限,则seteuid只能将进程的有效用户ID设置为它的实际用户ID或者保存的设置用户ID
    • 针对setegid的讨论类似seteuid
  5. getlogin:获取运行该程序的用户的登录名

    
    #include<unistd.h>
    
    char *getlogin(void)
    • 返回值:
      • 成功:返回指向登录名字符串的指针
      • 失败:返回NULL

    通常失败的原因是:进程的用户并没有登录到系统。比如守护进程。

进程会计

  1. 大多数UNIX系统提供了一个选项以进行进程会计处理
    • 启用该选项后,每当进程结束时内核就会写一个会计记录
    • 超级用户执行命令accton pathname则会启用会计处理,会计记录会写到pathname指定的文件中
      • 如果不带文件名参数,会停止会计处理
    • 会计记录文件是个二进制文件,包含的会计记录是二进制数据
  2. 会计记录结构定义在<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之和
  3. 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_cutimetms_cstime包含了wait函数族已经等待到的各个子进程的值
    • clock_t可以使用_SC_CLK_TCK(用sysconf函数)转换成秒数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值