apue:进程控制

来源:互联网 发布:性玩具知乎 编辑:程序博客网 时间:2024/05/01 23:48

    UNIX的进程控制,包括创建新进程、执行程序和进程终止;进程的各种ID—实际、有效和保存的用户和组ID,以及它们如何受到进程控制原语的影响;解释器文件和system函数。

进程标示

    每个进程都有一个非负整型的唯一进程ID。因为进程ID标识符总是唯一的,常将其用做其他标识符的一部分以保证其唯一性。有某些专用的进程:进程ID0是调度进程,常常被称为交换进程(swapper)。该进程并不执行任何磁盘上的程序—它是内核的一部分,因此也被称为系统进程。进程ID1通常是init进程,在自举过程结束时由内核调用。此进程负责在内核自举后起动一个UNIX系统。init通常读与系统有关的初始化文件(/etc/rc*文件),并将系统引导到一个状态。init进程决不会终止。它是一个普通的用户进程,但是它以超级用户特权运行。在某些UNIX的虚存实现中,进程ID2是页精灵进程(page daemon)。此进程负责支持虚存系统的请页操作。与交换进程一样,页精灵进程也是内核进程。
    下列函数返回这些标识符。

#include <sys/types.h>#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函数

    一个现存进程调用fork函数是UNIX内核创建一个新进程的唯一方法

#include <sys/types.h>#include <unistd.h>pid_t fork(void);/*返回:子进程中为0,父进程中为子进程ID,出错为-1*/

    由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。
    一个进程打开了三个不同文件,它们是:标准输入、标准输出和标准出错。在从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的进程总数超过了系统限制。
    fork有两种用法:
- 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec。

vfork函数

    vfork函数的调用序列和返回值与fork相同,但两者的语义不同。vfork用于创建一个新进程,而该新进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会存访该地址空间。不过在子进程调用exec或exit之前,它在父进程的空间中运行。vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。

wait和waitpid函数

    当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数。对于这种信号的系统默认动作是忽略它。
    调用wait或waitpid的进程可能会:
- 阻塞(如果其所有子进程都还在运行)。
- 待子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)。出错立即返回(如果它没有任何子进程)。
- 如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在一个任一时刻调用wait,则进程可能会阻塞。

#include <sys/types.h>#include <sys/wait.h>pid_t wait(int* statloc);pid_t waitpid(pid_t pid, int* statloc, int options);/*两个函数返回:若成功则为进程ID,若出错则为-1*/

    这两个函数的区别是:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选择项,可使调用者不阻塞。
- waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的进程。
    如果一个子进程已经终止,是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。
    对于waitpid的pid参数的解释与其值有关:
- pid==-1等待任一子进程。于是在这一功能方面waitpid与wait等效。
- pid>0等待其进程ID与pid相等的子进程。
- pid==0等待其组ID等于调用进程的组ID的任一子进程。
- pid<-1等待其组ID等于pid的绝对值的任一子进程。
    waitpid函数提供了wait函数没有提供的三个功能:
- waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。
- waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
- waitpid支持作业控制。

wait3和wait4函数

    这两个函数提供的功能比函数wait和waitpid所提供的分别要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源摘要。

#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时间总量、缺页次数、接收到信号的次数等。这些资源信息只包括终止子进程,并不包括处于停止状态的子进程。

竞态条件

    当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,认为这发生了竞态条件(race condition)。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞态条件活跃的孳生地。

exec函数

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

#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[]);/*六个函数返回:若出错则为-1,若成功则不返回*/

    这些函数之间的第一个区别是前四个取路径名作为参数,后两个则取文件名作为参数。当指定filename作为参数时:
- 如果filename中包含/,则就将其视为路径名。
- 否则就按PATH环境变量,在有关目录中搜寻可执行文件。
    PATH变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。
    如果execlp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
    第二个区别与参数表的传递有关(l表示表(list),v表示矢量(vector))。函数execl、execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外三个函数(execv,execvp和execve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数。
    最后一个区别与向新程序传递环境表相关。以e结尾的两个函数(execle和execve)可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。
    在执行exec后,进程ID没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:
- 进程ID和父进程ID。
- 实际用户ID和实际组ID。
- 添加组ID。
- 进程组ID。
- 对话期ID。
- 控制终端。
- 闹钟尚余留的时间。
- 当前工作目录。
- 根目录。
- 文件方式创建屏蔽字。
- 文件锁。
- 进程信号屏蔽。
- 未决信号。
- 资源限制。
- tms_utime,tms_stime,tms_cutime以及tms_ustime值。
    在很多UNIX实现中,这六个函数中只有一个execve是内核的系统调用。另外五个只是库函数,它们最终都要调用系统调用。这六个函数之间的关系如下图:
exec函数关系示意

更改用户ID和组ID

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

#include <sys/types.h>#include <unistd.h>int setuid(uid_t uid);int setgid(gid_t gid);/*两个函数返回:若成功则为0,若出错则为-1*/

    有关改变用户ID的规则:
- 若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置-用户-ID设置为uid。
- 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置-用户-ID,则setuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置-用户-ID。
- 如果上面两个条件都不满足,则errno设置为EPERM,并返回出错。
    关于内核所维护的三个用户ID,还要注意下列几点:
- 只有超级用户进程可以更改实际用户ID。通常,实际用户ID是在用户登录时,由login程序设置的,而且决不会改变它。因为login是一个超级用户进程,当它调用setuid时,设置所有三个用户ID。
- 仅当对程序文件设置了设置-用户-ID位时,exec函数设置有效用户ID。如果设置-用户-ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置-用户-ID。自然,不能将有效用户ID设置为任一随机值。
- 保存的设置-用户-ID是由exec从有效用户ID复制的。在exec按文件用户ID设置了有效用户ID后,即进行这种复制,并将此副本保存起来。

setreuid和setregid函数

    setregid函数功能是交换实际用户ID和有效用户ID的值。

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

    一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个设置-用户-ID程序转换成只具有用户的普通许可权,以后又可再次转换回设置-用户-ID所得到的额外许可权。

seteuid和setegid函数

    两个函数seteuid和setegid,它们只更改有效用户ID和有效组ID。

#include <sys/types.h>#include <unistd.h>int seteuid(uid_t uid);int setegid(gid_t gid);/*两个函数返回:若成功则为0,若出错则为-1*/

    一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置-用户-ID。对于一个特权用户则可将有效用户ID设置为uid。

组ID

    添加组ID不受setgid函数的影响。设置不同用户ID的函数如下:
设置ID函数关系示意

解释器文件

    这种文件是文本文件,其起始行的形式是:

#!pathname[optional-argument]

    在惊叹号和pathname之间的空格是可任选的。
    pathname通常是个绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索)。对这种文件的识别是由内核作为exec系统调用处理的一部分来完成的。内核使调用exec函数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中pathname所指定的文件。

system函数

    ANSIC定义了system函数,但是其操作对系统的依赖性很强。

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

    如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以决定在一个给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值:
- 如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,而且errno中设置了错误类型。
- 如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。
- 否则所有三个函数(fork,exec和waitpid)都成功,并且system的返回值是shell的终止状态,其格式已在waitpid中说明。

用户标示

    任一进程都可以得到其实际和有效用户ID及组ID。但是有时希望找到运行该程序的用户的登录名。如果一个用户有多个登录名,这些登录名又对应着同一个用户ID,用getlogin函数可以存取此登录名。

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

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

进程时间

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

#include <sys/times.h>clock_t times(struct tms* buf);/*返回:若成功则为经过的墙上时钟时间(单位:滴答),若出错则为-1*/

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

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

    此结构没有包含墙上时钟时间。作为代替,times函数返回墙上时钟时间作为函数值。

0 0
原创粉丝点击