2015-10-03 10:42:15 SYP35 阅读数 3213
在UNIX系统中进程由以下三部分组成:①进程控制块PCB;②数据段;③正文段。
        UNIX系统为了节省进程控制块所占的内存空间,把每个进程控制块分成两部分。一部分常驻内存,不管进程是否正占有处理器运行,系统经常会对这部分内容进行查询和处理,常驻部分内容包括:进程状态、优先数、过程特征、数据段始址、等待原因和队列指针等,这是进行处理器调度时必须使用的一些主要信息。另一部分非常驻内存,当进程不占有处理器时,系统不会对这部分内容进行查询和处理,因此这部分内容可以存放在磁盘的对换区中,它随用户的程序和数据部分换进或换出内存。
        UNIX系统把进程的数据段又划分成三部分:用户栈区(供用户程序使用的信息区);用户数据区(包括用户工作数据和非可重入的程序段);系统数据区(包括系统变量和对换信息)。
        正文段是可重入的程序,能被若干进程共享。为了管理可共享的正文段,UNIX设置了一张正文表,每个正文段都占用一个表目,用来指出该正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。
2016-08-24 18:57:44 taoyanqi8932 阅读数 690

进程控制

进程的创建

  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函数)转换成秒数
2013-09-28 09:32:36 hanzy0823 阅读数 694

每个进程运行在它的私有地址空间。在用户态下运行的进程涉及到私有栈、数据区和代码区。在内核态运行时,进程访问内核的数据区和代码区,单使用另外的私有栈。

因为内核是可重入的,因此几个内核控制路径交叉的执行,在这种情况下,每个内核控制路径都有自己的内核栈。

尽管看起来每个进程访问一个私有地址空间,但有时进程之间也共享部分地址空间。在一些情况下,这种共享由进程显示的提出;在另一种情况下,有内核自动完成共享以节约内存。

如一个共同一个应用程序由几个用户共同使用,则这个程序只被装入内存一次,其指令由需要它的用户共享,单每个用户都有独立的数据。这种共享的地址空间有内核自动完成以节省内存。

进程间也可以共享部分地址空间,以实现进程间通信,这就是有system V引入并且已经被linux支持的“共享内存”技术。

最后linux支持mmap()系统调用,该系统调用允许存放在块设备上的文件或信息一部分映射到进程的部分地址空间。如果一个文件由几个进程共享,那么共享他的每个进程空间都拥有它的内存映射。

2016-11-11 08:22:21 qq_17308321 阅读数 571
8– 进程控制
本章介绍了UNIX系统的进程控制 : 创建新进程 ,执行程序和进程终止 ,还说明了进程属性的各种ID .


8.2进程标识 :
每个进程都有一个非负整形标识唯一进程ID .
常用其作为其他标识符的一部分以保证其唯一性 .
例如 :应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件 .


一些专用进程 :ID为0的进程通常是调度进程 .
              ID为1的通常是init进程 ,此进程负责在自举内核后启动一个UNIX系统 ,init通常读取与系统有关的初始化文件 .Init进程绝不会终止 .


8.3fork :
由fork创建的新进程被称为子进程 :被调用一次返回两次 :子进程 :0 父进程 :子进程ID;
一个进程只会有一个父进程 ,所以子进程总是可以调用getppid以获得其父进程的进程ID .


Fork之后,子进程是父进程的副本,例如 :子进程获得父进程的数据空间 ,堆和栈的副本 .这是子进程所拥有的副本 ,父子进程并不共享这些存储空间部分 . 父进程和子进程共享正文段 .

例程 :

/*****************************************************
*fork函数   pid_t fork(void)  用一个现有进程创建新进程
*新进程 被称为 子进程 
*子进程是父 进程 的副本  子进程 会获得 父进程的 程序数
*据空间  堆和栈的 副本    但 父进程和 子进程 共享正文段
*
*
*******************************************************/

#include<apue.h>
#include<myerr.h>

int glob_var =66;

char buf [] =  "a write to stdout\n";    


int main()
{
	int var = 6;
	
	pid_t pid;
	
	if(write(STDOUT_FILENO,buf,sizeof(buf)-1) != sizeof(buf)-1) err_sys("write error");
					       /*sizeof(宏定义) 计算包括NULL在内的字符串长度
						*父进程和子进程 共享一个 文件偏移量
						*此处 父进程等待子进程时 子进程 写到标准输出
						*而在 子进程终止后 父进程也写到标准输出 
						*/	
	
	printf("before fork\n");		
						/*我们不冲洗缓冲区
						*如果标准输出连接到终端设备 则它是行缓冲 出现一次(父进程调用后马上冲了)
						*如果重定向到文件  输出 两次  因为当调用fork 时
						*该 缓冲数据人 也被复制进 子进程 exit 之前 第二个 pri
						*ntf  将数据追加至已有的缓冲区中。
						*/
	if((pid = fork())<0) err_sys("fork error");
	
	if(pid == 0)				//子进程
	{
		glob_var++;
		var++;		
	}
	else sleep(2);		//让子进程先 运行 。  父子进程运行顺序不确定  取决于内核调度算法
	
	//printf("after fork\n");		//若放在fork  后出现 两次 。
	
	printf("pid is %d     var is %d     glob_var is %d\n",getpid(),var,glob_var);
	
	exit(0);	
}



8.4vfork
Vfork函数用于创建一个新进程 ,而该新进程的目的是exec一个新程序 .它并不将父进程的地址空间完全复制到子进程中 ,因为子进程会立即调用exec .
Vfork和fork的一个区别是 :vfork保证子进程先运行(这段时间内核会使父进程休眠) ,它在调用exec或exit之后父进程才可能被调度运行 .

/*************************************************************
*vfork  创建一个新进程 ,而该新进程 的目的是 exec 一个新程序
*它不将  父进程 空间完全 复制到子进程 因为 自己成会立刻调用 exec 或exit 
*
*vfork  保证 子进程 先运行 ,它调用exec 或exit 之后 父进程 才可能被调度运行
*************************************************************/

#include <apue.h>
#include<myerr.h>

int glob_var =66;

int main()
{
	int var = 6;
	
	pid_t pid;
	
	printf("before fork\n");/*
							 *此处若重定向普通文件也不会 出现两次了
							 */
					
	if((pid = vfork())<0) err_sys("vfork error");
	
	if(pid == 0)				//子进程
	{
		glob_var++;
		var++;	
		
		_exit(0);			//不冲洗缓冲区	
	}

	printf("pid is %d     var is %d     glob_var is %d\n",getpid(),var,glob_var);
							/*
							 *会发现 子进程 改变了父进程的 变量值 
							 *因为 子进程在 父进程 地址空间运行。
							 */
	exit(0);	
}




8.5exit


8.6 wait 和 waitpid
一个进程正常或异常终止时 ,内核就向其父进程发送SIGCHILD信号 .因为子进程终止是个异步事件 ,所以这种信号也是内核向父进程发的异步通知 .
调用wait和waitpid会发生什么 ;
1. 如果所有子进程都还在运行 ,则阻塞 .
2. 若一个子进程已经终止 ,正等待父进程获取其终止状态 ,则取得该子进程的终止状态立即返回 .

3. 如果没有任何子进程 ,则立即出错返回 .


/*******************************************************************************
*wait/waitpid  
*1.若所有进程还在运行 ,在阻塞 
*2.若一个子进程终止,正等待父进程获取其终止状态,则取得该子进程终止状态 立即返回
*3.若没任何子进程,则立即出错返回
*
*两者区别 :
*1.(waitepid 有一选项可使调用者不阻塞)
*2.waitpid 不等其调用之后的第一个终止子进程  它有若干选项可以控制它所有等待进程、
********************************************************************************/

#include<apue.h>
#include<sys/wait.h>
#include<myerr.h>

#define WCOREDUMP

void pre_exit(int status)		//打印exit状态说明
{
	
	if(WIFEXITED(status))	printf("nomal termination ,exit staus = %d\n",WEXITSTATUS(status));
	else if(WIFSIGNALED(status)) printf("abnormal termination , signal number = %d%s\n",WTERMSIG(status),
	#ifdef WCOREDUMP 
		WCOREDUMP(status) ? "(core file generated)" : "");
	#else
		"");
	#endif
		else if(WIFSTOPPED(status)) printf("child stopped, signal number = %d\n,",WSTOPSIG(status));
	
}

int 
main(void)
{
	pid_t pid;
	int status;
	
	if((pid = fork())<0) err_sys("fork error");	
		else if(pid==0) exit(7);	//WEXITSTATUS 获取子进程传送给exit参数的低8位
			
	if(wait(&status)!=pid)
		err_sys("wait error");
	else pre_exit(status);	
	
	
	
	
	
	if((pid = fork())<0) err_sys("fork error");	
		else if(pid==0) abort();	//产生异常信号
			
	if(wait(&status)!=pid)
		err_sys("wait error");
	else pre_exit(status);	
	
	
	
	
	
	if((pid = fork())<0) err_sys("fork error");	
	else if(pid==0) status/=0;	//除0产生 SIGFPE
			
	if(wait(&status)!=pid)
		err_sys("wait error");
	else pre_exit(status);	
	
	exit(0);
}


/****************************************************************
*waitpid(pid_t pid ,int *statloc, int option)
*pid ==-1 等待任一子进程 等效于 wait 
*pid >0   等待进程id 与 pid 相等的子进程
*pid ==0  等待组 ID 等于调用进程组ID的任一进程
*pid <-1  等待组 ID 等于pid 绝对值的任一进程
*
*waitpid 返回终止子进程ID , 并将其终止状态 存放statloc 所指存储单元
*
*options: 进一步的控制操作 。详 见书193
****************************************************************/


#include <myerr.h>
#include <sys/wait.h>


int
main(void)
{
	
	pid_t pid;
	
	printf("1 pid = %ld \n",(long)getpid());
	
	if((pid = fork())<0)	err_sys("fork error");
		
	else if(pid==0)	
	{
		printf("2 pid = %ld \n",(long)getpid());
		
		if((pid=fork())<0)  err_sys("fork_2 error");
		
		if(pid>0)  	//子进程fork 后 pid  为其子进程 ID 故>0  终止它 。 
		exit(0); 
		else 
		{
		
		sleep(2);	//休眠2秒确保 其 父进程 终止 否则 可能返回 其父进程 ID  而不是 init 的ID 1	
		
		printf("second child, parent pid = %ld \n",(long)getppid());
		}
		
	}
	
	if(waitpid(pid,NULL,0)!=pid) err_sys("waitpid error");
	
	exit(0);	//注意 : 子进程的 子进程  会休眠两秒 故本程序进程会先终止 返回到 shell
}




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


8.10exec

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


#include <myerr.h>
#include <sys/wait.h>

char *env_init[] = {"USER=unknown","PATH=/tmp",NULL};

int main(void)
{
	pid_t pid;
	
	if((pid=fork())<0) err_sys("fork error");
	else if(pid == 0)  
	{
		
	if(execle("./echoall","echoall","myarg1","myarg2",(char *)0,env_init )<0) err_sys("execle error");	
		
	}
	
	
	
	if((waitpid(pid,NULL,0))<0) err_sys("waitpid error");
	
	
	
	if((pid=fork())<0) err_sys("fork error");	
	else if(pid == 0)  
	{
		
	if(execlp("echoall","echoall","only 1 arg",(char *)0)<0) err_sys("execle error");	
		
	}
	
	
	exit(0);
}

static void chartime(char *str) 
{
	char *ptr;
	
	int c;
	
	setbuf(stdout,NULL);	     //NULL 关闭缓冲   使内核竟可能多次的在两个进程间进行切换。
	
	for(ptr = str; (c= *ptr)!=0;ptr++)
	{
		putc(c,stdout);
	}
	
}


8.11更改用户ID和组ID .
2013-09-27 23:17:23 hanzy0823 阅读数 639

cpu既可以运行在用户态下,也可以运行在内核态。当一个程序在用户态执行时,它不能直接访问内核数据结构或内核的程序。每种CPU模型都提供了从用户态到内核态的转换的特殊指令,反之亦然。一个程序运行时,大部分时间都处于用户态,只有需要内核所提供的服务时才切换到内核态。当内核满足了程序的请求后,它让程序切换到用户态。

内核本身并不是一个进程,而是进程管理者。进程/内核模式假定:请求内核服务的进程使用所谓系统调用的特殊编程机制,每个系统调用都设置了一组识别进程请求的参数,然后执行与硬件相关的cpu指令完成从用户态到内核态的切换。除了用户进程之外,unix系统还包括几个所谓内核线程的特权进程,它们具有以下特点:

1、它们以内核态运行在内核地址空间

2、它们不与用户直接交互,因此不需要终端设备。

3、它们通常在系统启动时创建,然后一直处于活跃状态,直到系统关闭

激活内核例程的几种方式:

1、进程调用系统调用

2、正在执行进程的cpu发出一个异常信号,内核捕捉到信号,并进入异常处理

3、外围设备向cpu发出一个终端信号以通知一个事件的发生,如一个要求注意的请求、一个状态的变化或一个I/O的完成等。每个中断信号都由内核的中断处理程序来处理。外围设备和cpu异步操作,因此,中断在不可预知的时间发生。

4、内核线程被执行。因为内核线程运行在内核态,因此必须认为其相应程序是内核的一部分。

unix进程状态转换

阅读数 3385

UNIX进程管理

阅读数 772

没有更多推荐了,返回首页