UNIX环境高级编程——进程控制

8.1 引言

本章介绍UNIX系统的进程控制,包括:

  • 创建新进程、执行程序、进程终止
  • 进程属性ID——实际有效保存用户ID组ID
  • 解释器文件
  • system函数
  • 进程会计机制

8.2 进程标识

进程ID:一个非负整数,进程的唯一标识

  • 进程ID可复用:当某个进程终止后,它的进程ID可被之后创建的进程复用;
  • 几个专用进程
    (1)进程ID为0的是调度进程,也称交换进程,它是内核中的系统进程;
    (2)进程ID为1的是init进程,此进程负责在自举内核后启动一个UNIX系统,init进程绝不会终止,它是一个以超级用户特权运行的普通用户进程;
    (3)进程ID为2的是页守护进程,负责支持虚拟存储器系统的分页操作。

UNIX系统提供了返回进程某些标识符的函数:

#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

8.3 函数fork

fork函数用于创建一个新进程:

#include <unistd.h>

pid_t fork(void);
										// 子进程返回0,父进程返回子进程ID;若出错,返回-1
  • fork调用一次,返回两次,子进程返回0,父进程返回子进程ID;
  • 一个进程的子进程可以有多个,但父进程只有一个;
  • 子进程是父进程的副本,子进程得到父进程的数据空间、堆和栈的副本,子进程和父进程共享正文段;
  • 由于在fork之后经常跟随着exec,所以很多实现并不执行一个父进程数据段、栈和堆的完全副本,而是采用写时复制(Copy-On-Write,COW)技术,这些区域由父进程和子进程共享,且内核将它们的访问权限改为只读,如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟内存系统中的“一页”;
  • fork之后是父进程还是子进程先执行是不确定的;
  • 父进程的所有打开文件描述符都被复制到子进程,每个相同的打开描述符共享一个文件表项;
    在这里插入图片描述

8.4 函数vfork

vfork函数的调用序列和返回值与fork相同,但二者语义不同:

  • vfork用于创建一个新进程,该新进程的目的是执行一个新程序,vfork不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),所以也就不会引用该地址空间,不过子进程在调用execexit前,它在父进程的空间中执行;
  • vfork保证子进程先运行,在它execexit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。

8.5 函数exit

进程有5种正常终止及3种异常终止方式。

5种正常终止方式:

  • main函数内执行return语句,等效于调用exit
  • 调用exit函数;
  • 调用_exit_Exit函数;
  • 进程的最后一个线程在其启动例程中执行return语句,但该线程的返回值不用作进程的返回值,进程以终止状态0返回;
  • 进程的最后一个线程调用pthread_exit函数,进程以终止状态0返回。

3种异常终止方式:

  • 调用abort,产生SIGABRT信号;
  • 当进程接收到某些信号,信号可由进程自身(如调用abort函数)、其他进程或内核产生;
  • 最后一个线程对“取消”请求做出响应,默认情况下,“取消”以延迟方式产生:一个线程要求取消另一个线程,若干时间后,目标线程终止。

关于父进程、子进程先后终止的两种情况:

  • 对于父进程已经终止的所有进程,它们的父进程都改变为init进程,称这些进程由init进程收养;
  • 内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用waitwaitpid时,可以得到这些信息;
  • 一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程
  • 只要有一个由init收养的子进程终止,init就会调用一个wait函数取得其终止状态,所以init的子进程永远不会成为僵死进程。

8.6 函数wait和waitpid

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号,因为子进程终止是个异步事件(这可能在父进程运行的任何时候发生),所以这种信号也是内核向父进程发送的异步通知。

调用waitwaitpid的进程可能会:

  • 如果其所有子进程都还在运行,则阻塞;
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
  • 如果它没有任何子进程,则立即出错返回。
#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
										// 两个函数返回值:若成功,返回进程ID;若出错,返回0或-1

wait函数等待任一终止子进程:

  • 如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。如果调用者阻塞而且它有多个子进程,则在其某一个子进程终止时,wait就立即返回其终止子进程的进程ID;
  • 参数statloc是一个整型指针,若不为空,则终止进程的终止状态存放其中,若不关心终止状态,可将该指针指定为空指针;终止状态用定义在<sys/wait.h>中的各个宏来查看,有4个互斥的宏可用来取得进程终止的原因,基于这4个宏中哪一个值为真,就可选用其他宏来取得退出状态、信号编号等:
    在这里插入图片描述

waitpid函数等待一个特定的进程,其pid参数的作用解释如下:

  • pid == -1:等待任一子进程,等效于wait
  • pid > 0:等待进程ID与pid相等的子进程;
  • pid == 0:等待组ID等于调用进程组ID的任一子进程;
  • pid < -1:等待组ID等于pid绝对值的任一子进程。

options参数可进一步控制waitpid的操作,此参数或者是0,或者是下面常量按位或运算的结果:
在这里插入图片描述
waitpid函数提供了wait函数没有提供的3个功能:

  • waitpid函数可等待一个特定进程,而wait则返回任一终止子进程的状态;
  • waitpid提供了一个wait的非阻塞版本;
  • waitpid通过WUNTRACEDWCONTINUED选项支持作业控制。

8.7 函数waitid

waitid函数也用于获取进程终止状态,它比waitpid更灵活:

#include <sys/wait.h>

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
										// 返回值:若成功,返回0;若出错,返回-1
  • id参数指定要等待的子进程,它的作用与idtype的值有关:
    在这里插入图片描述
  • options参数是下图中个标志的按位或运算,这些标志指示调用者关注哪些状态变化,且WCONTINUEDWEXITEDWSTOPPED这3个常量之一必须在options参数中指定;
    在这里插入图片描述
  • infop参数是指向siginfo结构的指针,该结构包含了造成子进程状态改变有关信号的详细信息。

8.8 函数wait3和wait4

wait3wait4函数提供了获取终止进程及其所有子进程使用资源概况的功能,资源统计信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。

#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

8.9 竞争条件

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

8.10 函数exec

fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序,有7种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 envp[]);
										// 7个函数返回值:若出错,返回-1;若成功,不返回

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

  • 如果filename种包含/,则就将其视为路径名;
  • 否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。

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

最后一个区别与向新程序传递环境表相关,以e结尾的3个函数(execleexecvefexecve)可以传递一个指向环境字符串指针数组的指针,其他4个函数则使用调用进程中的environ变量为新程序复制现有的环境。

7个exec函数的参数记忆方法:

  • 字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件;
  • 字母l表示该函数取一个参数表,它与字母v互斥;
  • 字母v表示该函数取一个argv[]矢量;
  • 字母e表示该函数取envp[]数组,而不使用当前环境。

exec新程序对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标志值有关,进程中每个打开描述符都有一个执行时关闭标志(FD_CLOEXEC)标志,若设置了此标志,则在执行exec时关闭该描述符。

在很多UNIX实现中,这7个函数中只有execve是内核的系统调用,另外6个只是库函数,它们最终都要调用execve,这7个函数之间的关系如下:
在这里插入图片描述

8.11 更改用户ID和更改组ID

setuid函数设置实际用户ID和有效用户ID,setgid函数设置实际组ID和有效组ID:

#include <unistd.h>

int setuid(uid_t uid);
int setgid(gid_t gid);
										// 两个函数返回值:若成功,返回0;若出错,返回-1

更改用户ID的规则(关于用户ID所说明的一切适用于组ID):

  • 若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID以及保存的设置用户ID设置为uid
  • 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid,不更改实际用户ID和保存的设置用户ID;
  • 如果上述两个条件都不满足,则errno设置为EPERM,并返回-1。

关于内核所维护的3个用户ID的注意事项:

  • 只有超级用户进程可以更改实际用户ID;
  • 仅当程序文件设置了设置用户ID位时,exec函数才设置有效用户ID;
  • 保存的设置用户ID是由exec复制有效用户ID而得到的。

下图总结了更改这3个用户ID的不同方法:
在这里插入图片描述
setreuid函数功能是交换实际用户ID和有效用户ID的值:

#include <unistd.h>

int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
										// 两个函数返回值:若成功,返回0;若出错,返回-1
  • 如若两个参数中任一个的值为-1,则表示相应的ID保持不变。

seteuidsetegid函数用于更改有效用户ID和有效组ID:

#include <unistd.h>

int seteuid(uid_t uid);
int setegid(gid_t gid);
										// 两个函数返回值:若成功,返回0;若出错,返回-1
  • 一个非特权用户可将其有效用户ID设置为实际用户ID或保存的设置用户ID;
  • 一个特权用户可将有效用户ID设置为uid

更改3个不同用户ID函数之间的关系:
在这里插入图片描述

8.12 解释器文件

解释器文件是一个文本文件,其起始行的形式是:

#! pathname [optional-argument]

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

! /bin/sh
  • pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用PATH进行路径搜索);
  • 内核使调用exec函数的进程实际执行的并不是该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。

8.13 函数system

system函数用于在程序中执行一个命令字符串:

#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(127)
    (3)否则所有3个函数(forkexecwaitpid)都成功,那么system的返回值是shell的终止状态。

8.14 进程会计

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

8.15 用户标识

系统通常记录用户登陆时使用的名字,用getlogin函数可以获取此登录名:

#include <unistd.h>

char *getlogin(void);
										// 返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL						

8.16 进程调度

  • 进程可以通过调整友好值选择以更低优先级运行,只有特权进程允许提高调度权限;
  • Single UNIX Specification 中友好值的范围:0~(2*NZERO-1),NZERO是系统默认的友好值,默认20;
  • 友好值越小,优先级越高。

进行可以通过nice函数获取或更改它的友好值:

#include <unistd.h>

int nice(int incr);
										// 返回值:若成功,返回新的友好值;若出错,返回-1
  • incr参数被增加到调用进程的友好值上;
  • 如果incr太大,系统直接把它降到最大合法值,不给出提示;如果太小,也会把它调整到最小合法值;
  • 由于-1是合法的成功返回值,在调用nice函数之前需要清除errno,在nice函数返回-1时,需要检查它的值;
  • 进程只能影响自己的友好值,不能影响任何其他进程的友好值。

getpriority函数可以用于获取进程、一组相关进程的友好值:

#include <sys/resource.h>

int getpriority(int which, id_t who);
										// 返回值:若成功,返回-NZERO~NZERO-1之间的友好值;若出错,返回-1
  • which参数可取值:PRIO_PROCESS表示进程,PRIO_PGRP表示进程组,PRIO_USER表示用户ID;
  • who参数含义由which参数决定:
    (1)如果who参数为0,表示调用进程、进程组或者用户(取决于which参数的值);
    (2)当which设为PRIO_USER并且who0时,使用调用进程的实际用户ID;
    (3)如果which参数作用于多个进程,则返回所有作用进程中优先级最高的(最小的友好值)。

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

#include <sys/resource.h>

int setpriority(int which, id_t who, int value);
										// 返回值:若成功,返回0;若出错,返回-1
  • 参数whichwhogetpriority函数中相同;
  • value增加到NZERO上,然后变为新的友好值。

8.17 进程时间

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

#include <sys/times.h>

clock_t times(struct tms *buf);
									// 返回值:若成功,返回流逝的墙上时钟时间(以时钟滴答数为单位);若出错,返回-1
  • 此函数填写由buf指向的tm结构:

    struct tms {
    	clock_t		tms_utime;	// 用户CPU时间
    	clock_t 	tms_stime;		// 系统CPU时间
    	clock_t		tms_cutime;	// 终止子进程的用户CPU时间
    	clock_t		tms_cstime;	// 终止子进程的系统CPU时间
    };
    
  • 函数返回的墙上时钟是相对于过去的某一时刻度量的,不能使用其绝对值而必须使用其相对值。

8.18 实例代码

chapter8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MinBadGuy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值