《UNIX环境高级编程》(APUE) 笔记第八章 - 进程控制

8 - 进程控制

Github 地址


1. 进程标识

每个进程都有一个非负整型表示的 唯一进程 ID 。进程 ID 是可复用的(延迟复用算法)。

ID 为 \(0\) 的进程通常是调度进程,常常被称为 交换进程 (swapper) 。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为 系统进程

ID 为 \(1\) 通常是 init 进程,负责在自举内核后启动一个 UNIX 系统。init 进程决不会终止,它是一个普通的用户进程(不是内核中的系统进程),但它以超级用户特权运行。

获取各种标识符 的函数:

#include <unistd.h>
pit_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

这些函数都没有出错返回。

2. 函数 fork

一个现有的进程可以调用 fork 函数 创建一个新进程:

#include <unistd.h>
pid_t fork(void);

返回值:子进程返回 \(0\) ,父进程返回子进程ID;若出错,返回 \(-1\)

fork 创建的新进程被称为 子进程 (child process)fork 函数被调用一次,但是返回两次。

父进程返回值是子进程ID 的原因是:一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。

子进程返回值是 0 的原因是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得父进程的进程 ID 。

子进程是父进程的 副本 。父进程和子进程 共享正文段

写时复制 (COW) 技术使得子进程可与父进程共享 数据段 ,且内核将它们的访问权限改变为 只读 。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一

clone 系统调用允许调用者控制哪些部分由父进程和子进程共享。

fork 之后是父进程先执行还是子进程先执行是不确定的,取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的 进程间通信

3. 文件共享

父进程所有打开 文件描述符 都被复制到子进程中。父进程和子进程每个相同的打开描述符 共享一个文件表项 (所以父子进程共享一个 文件偏移量)。

描述符在 fork 前打开,之后 处理文件描述符 有以下两种常见情况:

  • 父进程等待子进程完成:在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
  • 父进程和子进程各自执行不同的程序段:在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

父进程与子进程具体 区别

  • fork 的返回值不同
  • 进程 ID 不同
  • 这两个进程的父进程 ID 不同:子进程的父进程 ID 是创建它的进程 ID,而父进程的父进程 ID 不变
  • 子进程的 tms_utimetms_stimetms_cutimetms_ustime 的值设置为 \(0\)
  • 子进程不继承父进程设置的文件锁
  • 子进程的未处理闹钟被清除
  • 子进程的未处理信号集设置为空集

使用 fork 失败 的两个主要原因:

  • 系统中已经有了太多的进程(通常意味着某个方面出了问题)
  • 该实际用户 ID 的进程总数超过了系统限制

fork 的两种用法:

  • 一个父进程希望复制自己,使父进程和子进程执行不同的代码段 。这在网络服务进程中是最常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用 fork,使子进程处理此请求,父进程则继续等待下一个服务请求。
  • 一个进程要执行一个不同的程序 。这对 shell 是常见的情况。这种情况下,子进程从 fork 返回后立即调用 exec。

4. 函数 vfork

vfork 函数的调用序列和返回值与 fork 相同,但两者的语义不同。vfork 函数用于创建一个新进程,而该新进程的目的是 exec 一个新程序。

vforkfork 区别:
bubian

  • vfork 不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 execexit ,不过子进程在调用这两个函数之前,会在父进程的空间中运行。
  • vfork 保证子进程先运行,在它调用 execexit 后父进程才可能被调度运行。

5. 函数 exit

进程的五种正常终止方式

  1. 在 main 函数内执行 return 语句,等效于调用 exit 。
  2. 调用 exit 函数。其操作包括调用各终止处理程序(由 atexit 函数登记的),然后关闭所有标准 I/O 流等。
  3. 调用 _exit_Exit 函数。这两个函数无需运行终止处理程序或信号处理程序,不对标准 I/O 流进行冲洗。
  4. 进程最后一个线程在其启动例程中执行 return 语句。此时进程以终止状态 \(0\) 返回。
  5. 进程的最后一个线程调用 pthread_exit 函数。

进程的三种异常终止方式

  1. 调用 abort ,产生 SIGABRT 信号。
  2. 当进程接收到某些信号。信号可由进程自身(如调用abort)、其他进程或内核产生。
  3. 最后一个线程对 取消 (cancellation) 请求做出响应。

不管进程如何终止,都会关闭所有打开描述符,释放使用的存储器等。

终止进程如何通知其父进程是如何终止的?

  • 对于三个终止函数(exit_exit_Exit),将其退出状态作为参数传递给函数。
  • 对于异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状态。
  • 任一情况下,父进程都通过 waitwaitpid 函数取得其终止状态。

若父进程在子进程前终止,如何处理?

通过 init 进程收养:对于父进程已经终止的子进程( 孤儿进程 ),它们的父进程改变为 init 进程。操作过程为:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程 ID 就更改为 \(1\) ( init 进程的 ID)。孤儿进程 不会有什么危害。

若子进程在父进程之前终止,父进程如何得到子进程终止状态?

当终止进程的父进程调用 waitwaitpid 时,可以得到关于终止进程的信息,包括:进程 ID、该进程的终止状态以及该进程使用的 CPU时间总量。

僵死进程 (zombie)

一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程。僵死进程的资源不会被释放,进程 ID 也一直被占用,但是系统所能使用的进程 ID 是有限的,如果大量地产生僵死进程,将因为没有可用的进程 ID 而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免

一个由 init 进程收养的进程终止时会变成僵死进程吗?

不会。因为 init 被编写成无论何时只要有一个子进程终止,init 就会调用一个 wait 函数取得其终止状体。这样也就防止了在系统中塞满僵死进程。

6. 函数 wait 和 waitpid

当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。子进程终止和内核向父进程发送信号都是异步事件。

调用 waitwaitpid 的函数会:

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

waitwaitpid 函数:

#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

返回值:若成功,返回进程 ID;若出错,返回 \(0\)\(-1\)

两个函数区别:

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一选项,可使调用者不阻塞。
  • waitpid 可等待一个特定进程,而 wait 则返回任一终止子进程的状态(某一子进程终止,就立即返回)。
  • waitpid 通过 WUNTRACEDWCONTINUD 选项支持作业控制。
  • wait 唯一出错情况是没有子进程,对于 waitpid ,如果指定的进程或进程组不存在,或者参数 \(pid\) 指定的进程不是调用进程的子进程,都可能出错。

\(statloc\) 参数是一个整型指针,终止进程的终止状态存放在它所指向的单元内。若不关心终止状态,可设为空指针。

终止状态的宏 定义在 <sys/wait.h> 中:

waitpid\(pid\) 参数bubian作用解释如下:

  • \(pid==-1\) :等待任一子进程。此种情况下,waitpidpid 等效。
  • \(pid > 0\) :等待进程 ID 与 \(pid\) 相等的子进程。
  • \(pid==0\) :等待组 ID 等于调用进程组 ID 的任一子进程。
  • \(pid < -1\) :等待组 ID 等于 \(pid\) 绝对值的任一子进程。

waitpid\(options\) 参数或是 \(0\) ,或是以下常量按位或运算的结果:

7. 函数 waitid

waitid 是另一个可以取得进程终止状态的函数,提供了更多灵活性。

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

返回值:若成功,返回 \(0\) ;若出错。返回 \(-1\)

waitid 允许一个进程指定要等待的子进程,使用两个单独的参数表示要等待的子进程所属的类型。

\(id\) 参数的作用与 \(idtype\) 的值相关:

\(options\) 参数是下图各标志的按位或运算,这些标志指示调用者关注哪些状态变化:

WCONTINUEDWEXITEDWSTOPPED 这三个常量之一必须指定。

\(infop\) 参数是指向 siginfo 结构的指针。该结构包含了造成子进程状态改变有关信号的详细信息。

8. 函数 wait3 和 wait4

这两个函数多了 \(rusage\) 参数,允许内核返回由终止进程及其所有子进程使用的资源概况:

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>

pid_t wait3(int *stbubianatloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);

资源统计信息包括 用户CPU时间总量系统CPU时间总量缺页次数接收到信号的次数 等。

9. 竞争条件

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

如果一个进程要 等待其父进程终止 ,则可使用下列形式的循环:

while (getppid() != 1)
    sleep(1);

这种形式的循环称为 轮询 (polling) ,它的问题是浪费了 CPU 时间,因为调用者每隔 1s 都被唤醒,然后进行条件测试。

避免竞争条件和轮询 ,在多个进程之间需要有某种形式的信号发送和接收的方法。可使用 信号机制 或多种形式的 进程间通信 (IPC)

10. 函数 exec

当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。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 envp[]);

bubian
返回值:若出错,返回 \(-1\) ;若成功,不返回 。

7个函数的区别

  1. 前四个函数以路径名作为参数,之后两个以文件名作为参数(函数名中带 p ),最后一个函数以文件描述符为参数。当指定 \(filename\) 作为参数时,如果 \(filename\) 中含有 \(/\) ,将其视为路径名;否则就按 PATH 环境变量,在它所指定的各目录中搜寻可执行文件。
  2. 参数表传递有区别,l 表示列表 listv 表示矢量 vector 。若使用 列表 ,参数表以空指针结尾。若使用 矢量 ,要先构造一个指向各参数的指针数组。
  3. 与向新进程传递 环境表 有关。以 e 结尾的 \(3\) 个函数( execleexecvefexecve )可以传递一个指向环境字符串指针数组的指针,其他 \(4\) 个函数则使用调用进程中的 environ 变量为新程序复制现有的环境。

对打开文件的处理 与每个描述符的 执行时关闭 (close-on-exec) 标志值有关。进程中每个打开描述符都有一个执行时关闭标志。若设置了此标志,则在执行 exec 时关闭该描述符;否则该描述符仍打开。除非特定使用 fcntl 设置了该执行时关闭标志,否则系统的默认操作是在 exec 后仍保持这种描述符打开。

在 exec 时要关闭打开目录流 ,这通常是由 opendir 函数实现的,它调用 fcntl 函数为对应于打开目录流的描述符设置执行时关闭标志。

在 exec 前后实际用户 ID 和 实际组 ID 保持不变 ,而有效 ID 是否改变取决于所执行程序文件的设置用户 ID 位和设置组 ID 位是否设置 。如果新程序的设置用户 ID 位已设置,则有效用户 ID 变成程序文件所有者的 ID;否则有效用户 ID 不变。对组 ID 的处理方式与此相同。

只有 execve系统调用 ,别的函数都是库函数,调用此系统调用,示意图为:

11. 更改用户 ID 和 更改组 ID

特权 (如能改变当前日期的表示法)和 访问控制 (如能否读、写一个特定文件),是基于 用户 ID组 ID 的。在设计应用时,一般使用 最小特权模型

可使用 setuid 函数设置 实际用户 ID有效用户 ID

#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\)
  • 若两个条件都不满足,则 errno 设置为 EPERM ,并返回 \(-1\)

关于内核所维护的 \(3\) 个用户ID,还要 注意

  • 只有超级用户可以更改实际用户ID 。
  • 仅当对程序文件设置了用户ID位时,exec 函数才设置有效用户 ID 。
  • 保存的设置用户 ID 是由 exec 复制有效用户 ID 而得到的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID之后,这个副本就被保存起来了。

getuidgeteuid 函数获得实际用户ID和有效用户ID的当前值。getresuidgetresgid 函数分别用于获取保存的设置用户ID和保存的设置组ID。

函数 setreuidsetregid

用于设置实际用户ID为 \(ruid\) ,设置有效用户ID为 \(euid\)

#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);

若其中任一参数的值为 \(-1\) ,则表示相应的ID应当保持不变。

函数 seteuidsetegid 只更改有效用户ID和有效组ID。

下图总结了更改 3 个不同用户ID的各个函数

组 ID 相关函数用法同用户ID函数。

保存的设置用户ID的作用:在需要提升特权的时候,通过设置程序文件的设置用户ID而得到额外的权限。

12. 解释器文件

解释器文件 (interpreter file) 是文本文件,其起始行的形式是:

#!pathname [optional-argument]
//如: #!/bin/sh

\(pathname\) 通常是绝对路径名。内核调用 exec 函数的进程执行解释器文件第一行中 \(pathname\) 所指定的文件。

优点

  • 有些程序是用某种语言写的脚本,解释器文件可将这一事实隐藏起来。
  • 解释器在脚本效率方面也提供了好处。
  • 解释器脚本使我们可以使用除 /bin/sh 以外的其他shell来编写 shell 脚本 。

13. 函数 system

system 用于执行一个命令字符串:

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

system 在其实现中调用了 forkexecwaitpid ,因此有 \(3\) 种返回值:

  1. fork 失败或者 waitpid 返回除 EINTR 之外的出错,则 system 返回 \(-1\) ,并且设置 errno 以指示错误类型。
  2. 如果 exec 失败(表示不能执行shell),则其返回值如同 shell 执行了 exit(127)
  3. 否则所有 \(3\) 个函数( forkexecwaitpid) 都成功,那么 system 返回的是 shell 的终止状态,其格式已在 waitpid 中说明。

使用 system 而不是直接使用 forkexec 的优点是:system 进行了所需的各种出错处理以及各种信号处理。

14. 用户标识

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

#include <unistd.h>
char *getlogin(void);

返回值:若成功,返回指向登录名字字符串的指针;若出错,返回 NULL

如果调用此函数的进程( 守护进程 )没有连接到用户登录时所用的终端,则函数会失败。

15. 进程调度

UNIX 系统对进程提供的只是 基于调度优先级的粗粒度的控制调度策略调度优先级 是由内核确定的。

进程可以通过调整 nice 值降低它对CPU的占有,从而降低运行优先级。只有 特权进程 允许提高调度权限。友好值越小,优先级越高。NZERO 是系统默认的友好值。

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

#include <unistd.h>
int nice(int incr);

返回值:若成功,返回新的 niceNZERO ;若出错,返回 \(-1\)

\(incr\) 参数被增加到进程的 nice 值上。若 \(incr\) 太大,系统把它降到最大合法值;若太小,提高到最小合法值。如果 nice 返回 \(-1\)errno 不为 \(0\) ,说明 nice 调用失败。

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

#include <sys/resource.h>
int getpriority(int which, id_t who);

返回值:若成功,返回 -NZERO~NZERO-1 之间的 nice 值;若出错,返回 \(-1\)

\(which\) 参数控制 \(who\) 参数是如何解释的,PRIO_PROCESS 表示进程,PRIO_PGRP 表示进程组,PRIO_USER 表示用户 ID。\(who\) 参数选择感兴趣的一个或多个进程,如果 \(who\) 参数为 \(0\) ,表示调用进程、进程组或者用户(取决于 \(which\) 参数的值 );当 \(which\) 设为 PRIO_USER 并且 \(who\)\(0\) 时,使用调用进程的实际用户ID;如果 \(which\) 参数作用于多个进程,则返回所有作用进程中优先级最高的(最小的 nice 值 )。

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

#include <sys/resources.h>
int setpriority(int which, id_t who, int value);

参数 \(which\)\(who\)getpriority 函数中相同。\(value\) 增加到 NZERO 上,然后变为新的友好值。

子进程从父进程中继承友好值

16. 进程时间

可度量的时间包括 \(3\) 种:墙上时钟时间用户 CPU 时间系统 CPU 时间 ,任一进程可以调用 times 函数获得它自己以及终止子进程的上述值:

#include <sys/time.h>
clock_t times(struct tms *buf);

返回值:若成功,返回流逝的 墙上时钟时间(以时钟滴答数为单位);若出错,返回 \(-1\)

\(tms\) 结构为:

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

times 函数返回墙上时钟时间作为其函数值,此值是相对于过去某一时刻度量的,所以需要使用相对值,所有此函数返回的 clock_t 值都用 _SC_CLK_TCK 转换成秒数。

posted @ 2020-06-29 17:55  BrianLeeLXT  阅读(350)  评论(0编辑  收藏  举报