APUE第8章 进程控制

8.1 引言

在理解线程之前,首先需要了解UNIX/Linux进程。 进程是由操作系统创建的,需要相当数量的“开销”。 进程包含有关程序资源和程序执行状态的信息,包括:它是一个在随机访问内存(RAM)中,正在执行的程序,它是资源分配的最小单位。

image.png

pcb内容:
1)进程ID,进程组ID,用户ID和组ID
2)环境
3)工作目录
4)程序说明
5)寄存器
6)栈
7)堆
8)文件描述符
9)信号动作
10)共享库
11)进程间通信工具(例如消息队列,管道,信号量或共享内存)

然后就是代码段数据段进程空间啥的了

本章介绍UNIX系统的进程控制,包括创建新进程、执行程序和进 程终止。还将说明进程属性的各种ID—实际、有效和保存的用户ID和 组ID,以及它们如何受到进程控制原语的影响。本章还包括了解释器文 件和system函数。本章最后讲述大多数UNIX系统所提供的进程会计机 制,这种机制使我们能够从另一个角度了解进程的控制功能。

8.2 进程标识

每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识 符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。例 如,应用程序有时就把进程 ID 作为名字的一部分来创建一个唯一的文 件名。

虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进 程ID就成为复用的候选者。大多数UNIX 系统实现延迟复用算法,使得 赋予新建进程的 ID 不同于最近终止进程所使用的ID。这防止了将新进 程误认为是使用同一ID的某个已终止的先前进程。
系统中有一些专用进程,但具体细节随实现而不同。ID为 0的进程 通常是调度进程,常常被称为交换进程(swapper)。

该进程是内核的 一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程 ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件 在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负 责在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化 文件(/etc/rc*文件或/etc/inittab文件,以及在/etc/init.d中的文件),并 将系统引导到一个状态(如多用户)。init 进程决不会终止。它是一个 普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是 它以超级用户特权运行。本章稍后部分会说明init如何成为所有孤儿进 程的父进程。

每个UNIX系统实现都有它自己的一套提供操作系统服务的内核进 程,例如,在某些UNIX的虚拟存储器实现中,进程ID 2是页守护进程 (page daemon),此进程负责支持虚拟存储器系统的分页操作。
除了进程ID,每个进程还有一些其他标识符。

下列函数返回这些标 识符。

#include  
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
#include 
#include 
#include 
#include 
#include 
#include 
int main() {
    /*----------------------------------- test getpid. ----------------------------------------*/
    printf("pid: %d\nparentid: %d\nuid: %d\n", getpid(), getppid(), getuid());

    /*----------------------------------- test get pwd ----------------------------------------*/
    passwd my_name;
    passwd *ptr = &my_name;

    ptr = getpwuid(getuid());
    printf("username: %s\nuserpwd: %s\n", ptr->pw_name, ptr->pw_dir);

    return 0;
}

注意,这些函数都没有出错返回,在下一节讨论fork函数时,将进
一步讨论父进程ID。在4.4节中已讨论了实际和有效用户ID及组ID。

8.4 fork

进程创建

1. fork

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

#include  
pid_t fork(void);

返回值:子进程返回0,父进程返回子进程ID;若出错,返回−1

由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID

父进程fork后为子进程生成一个PCB

pcb内容:
1)进程ID,进程组ID,用户ID和组ID
2)环境
3)工作目录
4)程序说明(指令寄存器)
5)寄存器
6)栈
7)堆
8)打开的文件描述符表
9)信号动作
10)共享库
11)进程间通信工具(例如消息队列,管道,信号量或共享内存)

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

fork 使子进程得到返回值 0 的理 由是:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID(进程ID 0总是由内核交换进程使用,所以 一个子进程的进程ID不可能为0)。

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段(见下图)。

image.png

由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一 个父进程数据段、栈和堆的完全副本。

作为替代,使用了写时复制 (Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

某些平台提供 fork 函数的几种变体, 几乎所有平台都支持将要讨论vfork

Linux 3.2.0 提供了另一种新进程创建函数—clone系统调用。 这是一种fork的推广形式,它允许调用者控制哪些部分由父进程和子进程共享

FreeBSD 8.0提供了rfork系统调用,它类似于Linux的clone系 统调用。rfork调用是从Plan 9操作系统(Pike等[1995])派生出来 的。

Solaris 10提供了两个线程库:一个用于POSIX线程 (pthreads),另一个用于Solaris线程。在这两个线程库中,fork 的 行为有所不同。对于 POSIX 线程,fork 创建一个进程,它仅包含调用该fork的线程,但对于Solaris线程,fork创建的进程包含了调用线程 所在进程的所有线程的副本。在Solaris 10中,这种行为改变了。不管 使用哪种线程库,fork创建的子进程只保留调用线程的副本。Solaris 也提供了fork1函数,它创建的进程只复制调用线程。还有forkall函 数,它创建的进程复制了进程中所有的线程。第11章和第12章将详细讨 论线程。

程序演示了fork函数,从中可以看到子进程对变量所做的改变 并不影响父进程中该变量的值。

我们写一个代码演示一下

#include 
#include 
#include 
#include 
#include 
#include 

/* Intager in global segment. */
int globalnum = 666;

int main() {
    /*----------------------------------- test fork() ----------------------------------------*/
    
    /* Display str. */
    std::string str = "hello world\n";

    /* Intager in Stack (automatic variable on the stack). */
    int num = 233;
    /* Pid queue. */
    std::vector pid_queue;

    /* Pid. */
    pid_t new_pid;

    std::cout << "before fork()" ;//这里故意不刷新缓冲区

    if ((new_pid = fork()) < 0) {
        std::cout << "fork() error" << std::endl;
    }

    if (new_pid  == 0) {
        /* child. */
        ++globalnum;
        ++num;
    } else {
        /* parent. */
        sleep(2);
    }

    std::cout << "pid = " << getpid() << ", globalnum = " << globalnum << ", num = " << num << std::endl;

    return 0;
}

输出结果:

image.png

可以看到fork()之后的代码开始分支,并且子进程的改动并没有修改父进程的数据。

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

代码中父进程使自己休眠2 s,以此使子进程先执行。但并不保证2 s已经足够。讲述竟争条件时还将谈及这一问题及其他类型的同步方法。我们将说明在fork之后如何使用信号使父进程和子进程同步。

回忆一下,如果标准输出连到终端设备,则它是行缓冲的;否则它是全缓冲的。当以交互方式运行该程序时,只得到该cout输出的行"before fork()"一次,其原因 是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到cout输出的行"before fork()"输出行两次。其原因是,在fork之前调用了cout一次, 但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。在exit之前的第二个cout将其数据 追加到已有的缓冲区中。当每个进程终止时,其缓冲区中的内容都被写到相应文件中。

我们可以重定向看看:


image.png

重定向到文件结果果然输出了两次
image.png

文件共享

要注意到的一点是,虽然子进程复制了父进程的数据段、堆和栈,生成了uid,但是PCB的其它部分却和父进程一致
我们再看一眼PCB的内容

pcb内容:
1)进程ID,进程组ID,用户ID和组ID
2)环境
3)工作目录
4)程序说明(指令寄存器)
5)寄存器
6)栈
7)堆
8)打开的文件描述符表
9)信号动作
10)共享库
11)进程间通信工具(例如消息队列,管道,信号量或共享内存)

实际上,fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项

考虑下述情况,一个进程具有3个不同的打开文件,它们是标准输 入、标准输出和标准错误。在从fork返回时,我们有了如图8-2中所示的 结构。


image.png

重要的一点是,父进程和子进程共享同一个文件偏移量。考虑下述 情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已重定向(很可能是由 shell 实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。在这个例 子中,当父进程等待子进程时,子进程写到标准输出;而在子进程终止 后,父进程也写到标准输出上,并且知道其输出会追加在子进程所写数据之后。如果父进程和子进程不共享同一文件偏移量,要实现这种形式 的交互就要困难得多,可能需要父进程显式地动作。

如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假 定所用的描述符是在fork之前打开的)。虽然这种情况是可能发生的 (见图8-2),但这并不是常用的操作模式。

在fork之后处理文件描述符有以下两种常见的情况。

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

除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:

•实际用户ID、实际组ID、有效用户ID、有效组ID •附属组ID
•进程组ID
•会话ID
•控制终端
•设置用户ID标志和设置组ID标志
•当前工作目录
•根目录
•文件模式创建屏蔽字
•信号屏蔽和安排 •对任一打开文件描述符的执行时关闭(close-on-exec)标志
•环境
•连接的共享存储段
•存储映像
•资源限制
父进程和子进程之间的区别具体如下。
•fork的返回值不同。
•进程ID不同。 
•这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变。 
•子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0。
•子进程不继承父进程设置的文件锁。
•子进程的未处理闹钟被清除。
•子进程的未处理信号集设置为空集。

使fork失败的两个主要原因是:
(a)系统中已经有了太多的进程(通常意味着某个方面出了问题),
(b)该实际用户ID的进程总数超 过了系统限制。其中CHILD_MAX规定了每个实际用户ID 在任一时刻可拥有的最大进程数。

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

某些操作系统将第 2 种用法中的两个操作(fork 之后执行 exec)组 合成一个操作,称为spawn

UNIX系统将这两个操作分开,因为在很多 场合需要单独使用fork,其后并不跟随exec。另外,将这两个操作分 开,使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向、 用户ID、信号安排等。

8.4 vfork()

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

vfork 起源于较早的 2.9BSD。有些人认为,该函数是有瑕疵的。 但是本书讨论的 4 种平台都支持它。事实上,BSD 的开发者在 4.4BSD 中删除了该函数,但 4.4BSD 派生的所有开放源码BSD版本又将其收 回。在SUSv3中,vfork被标记为弃用的接口,在SUSv4中被完全删除。 我们只是由于历史的原因还是把它包含进来。可移植的应用程序不应该 使用这个函数。

vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序.

shell基本部分就是这类程序的一个例子。vforkfork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec 或exit之前,它在父进程的空间中运行

这种优化工作方式在某些UNIX 系统的实现中提高了效率,但如果子进程修改数据(除了用于存放vfork 返回值的变量)、进行函数调用、或者没有调用 exec 或 exit 就返回都可 能会带来未知的结果。

就像上一节中提及的,实现采用写时复制技术 以提高fork之后跟随exec操作的效率,但是不复制比部分复制还是要快 一些。)

vforkfork之间的另一个区别是: vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。

如果在调用这两个函数之前子进 程依赖于父进程的进一步动作,则会导致死锁

我们尝试来用vfork代替fork

#include 
#include 
#include 
#include 

/* Global Num. */
int global_num = 666;

int main() {
    /* Pid. */
    pid_t pid;

    /* Nun in Stack. */
    int num = 233;

    std::cout << "brefore vfork()\n";

    if ((pid = vfork()) < 0 ) {
        std::cout << "vfork() error" << std::endl;
    }

    if (pid == 0) {
        /* child. */
        ++global_num;
        ++num;

        /* must exit without change parent space. */
        _exit(0);
    }

    /* parent. */
    std::cout << "pid = " << getpid() << ", globalnum = " << global_num << ", num = " << num << std::endl;

    return 0;
}
执行结果

从结果可以看出,子进程不仅先于父进程执行,并且改变了父进程的栈区和数据段内容,可以验证vfork子进程共享父进程空间不进行copy。子进程对变量做增1的操作,结果改变了父进程中的变量值,因为子进程在父进程的地址空间中运行,所以这并不令人惊讶。但是其作用 的确与fork不同。

调用了_exit而不是exit。_exit并不执行标准I/O缓冲区的冲洗操作。如果调用的是exit而不是 _exit,则该程序的输出是不确定的。它依赖于标准I/O库的实现,我们 可能会看到输出没有发生变化,或者发现没有出现父进程的printf输出。

如果子进程调用 exit,实现冲洗标准 I/O 流。如果这是函数库采取 的唯一动作,那么我们会见到这样操作的输出与子进程调用_exit所产生 的输出完全相同,没有任何区别。如果该实现也关闭标准I/O 流,那么 表示标准输出FILE 对象的相关存储区将被清 0。

大多数exit的现代实现不再在流的关闭方面自找麻烦。因为进程即 将终止,那时内核将关闭在进程中已打开的所有文件描述符。在库中关 闭这些,只是增加了开销而不会带来任何益处。

vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
其次,子进程vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
但此处有一点要注意的是用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()`则不存在这个情况。

3.clone

系统调用fork()vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。
fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法.而在vfork调用中,子进程先运行,父进程挂起,直到子进程调用了execexit之后,父子进程的执行次序才不再有限制;clone中由标志CLONE_VFORK来决定子进程在执行时父进程阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止。

8.4.函数exit

image.png

进程有5种正常终止及3种异常终止方式。5种正常终 止方式具体如下。

  • (1)在main函数内执行return语句。如在7.3节中所述,这等效于调 用exit。

  • (2)调用exit函数。此函数由ISO C定义,其操作包括调用各终止 处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准 I/O流等。因为ISO C并不处理文件描述符、多进程(父进程和子进程) 以及作业控制,所以这一定义对UNIX系统而言是不完整的。

  • (3)调用_exit或_Exit函数。ISOC定义_Exit,其目的是为进程提供 一种无需运行终止处理程序或信号处理程序而终止的方法。对标准 I/O 流是否进行冲洗,这取决于实现。在 UNIX系统中,_Exit 和_exit 是同 义的,并不冲洗标准 I/O 流。_exit 函数由 exit 调用,它处理UNIX系统 特定的细节。_exit是由POSIX.1说明的。

在大多数UNIX系统实现中,exit(3)是标准C库中的一个函数,而 _exit(2)则是一个系统调用。

  • (4)进程的最后一个线程在其启动例程中执行return语句。但是, 该线程的返回值不用作进程的返回值。当最后一个线程从其启动例程返 回时,该进程以终止状态0返回。

  • (5)进程的最后一个线程调用 pthread_exit 函数。如同前面一样, 在这种情况中,进程终止状态总是0,这与传送给pthread_exit的参数无 关。

3种异常终止具体如下:

  • (1)调用abort。它产生SIGABRT信号,这是下一种异常终止的一 种特例。

  • (2)当进程接收到某些信号时。信号可由进程自身(如调用abort函数)、其他进程或内核产生。 例如,若进程引用地址空间之外的存储单元、或者除以0,内核就会为 该进程产生相应的信号。

  • (3)最后一个线程对“取消”(cancellation)请求作出响应。默认 情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若 干时间之后,目标线程终止。

不管进程如何终止,最后都会执行内核中的同一段代码。这段代码 为相应进程关闭所有打开描述符,释放它所使用的存储器等。

对上述任意一种终止情形,我们都希望终止进程能够通知其父进程 它是如何终止的。对于 3个终止函数(exit、_exit和_Exit),实现这一点的方是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况,内核(不是进程本身)产生一个指示其异常终止原因的终止状 态(termination status)。在任意一种情况下,该终止进程的父进程都能用waitwaitpid函数取得其终止状态。

注意,这里使用了“退出状态”(它是传递给向3个终止函数的参数,或main的返回值)和“终止状态”两个术语,以表示有所区别。在最后调用_exit时,内核将退出状态转换成终止状态. 图8- 4说明父进程检查子进程终止状态的不同方法。如果子进程正常终止, 则父进程可以获得子进程的退出状态。

孤儿进程

在说明fork函数时,显而易见,子进程是在父进程调用fork后生成 的。上面又说明了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,又将如何呢?其回答是:对于父进程已经终止的所 有进程,它们的父进程都改变为 init 进程。

父进程先于子进程终止的,其子进程的父进程将会变为init进程,由init进程托管,这种进程被称为孤儿进程

我们称这些进程由init进程收养。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID)。这种处理方法保证了每个进程有一个父进程。 最终管理回收孤儿进程的资源。

僵尸进程

另一个我们关心的情况是,如果子进程在父进程之前终止,那么父 进程又如何能在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获取它的终止状态的。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用waitwaitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量

内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在 UNIX 术语中,一个已经终止、但是其父进程尚未对其进行善后处理 (获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵死进程(僵尸进程)(zombie)

ps命令将僵死进程的状态打印为Z。如果编写一 个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程

某些系统提供了一种避免产生僵死进程的方法,我们之后再介绍

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

最后一个要考虑的问题是:一个由init进程收养的进程终止时会发 生什么?它会不会变成一个僵死进程?对此问题的回答是“否”,因为 init被编写成无论何时只要有一个子进程终止, init 就会调用一个 wait 函 数取得其终止状态。这样也就防止了在系统中塞满僵死进程。当提 及“一个init的子进程”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init收养的进
程。

8.5.函数waitid() 和waitpid()

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

父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号 处理程序)。对于这种信号的系统默认动作是忽略它

现在需要知道的是调用waitwaitpid的进程可能会发生什么。
•如果其所有子进程都还在运行,则阻塞。
•如果一个子进程已终止,正等待父进程获取其终止状态,则取得 该子进程的终止状态立即返回。

•如果它没有任何子进程,则立即出错返回 。

如果进程由于接收到SIGCHLD信号而调用wait,我们期望wait会立 即返回。但是如果在随机时间点调用wait,则进程可能会阻塞。

#include 
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);

两个函数返回值:若成功,返回进程ID;若出错,返回0(见后面的说 明)或−1

这两个函数的区别如下。
•在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项, 可使调用者不阻塞。
•waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选 项,可以控制它所等待的进程。

如果子进程已经终止,并且是一个僵死进程,则wait立即返回并取 得该子进程的状态;否则wait使其调用者阻塞,直到一个子进程终止。 如调用者阻塞而且它有多个子进程,则在其某一子进程终止时,wait就 立即返回。因为wait返回终止子进程的进程ID,所以它总能了解是哪一 个子进程终止了。

这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

依据传统,这两个函数返回的整型状态字是由实现定义的。其中某 些位表示退出状态(正常返回),其他位则指示信号编号(异常返 回),有一位指示是否产生了core文件等。POSIX.1规定,终止状态用 定义在中的各个宏来查看。有4个互斥的宏可用来取得进程 终止的原因,它们的名字都以WIF开始。基于这4个宏中哪一个值为 真,就可选用其他宏来取得退出状态、信号编号等。

写个代码测试一下

#include 
#include 
#include 
#include 
#include 

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end) {
    int mid = (end - start) >> 1;
    int i = start, j = end-1;
}
int main() {
    /*----------------------------------- test wait ----------------------------------------*/
    
    /* Pid. */
    pid_t pid;
    
    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
    }

    if ( pid == 0 ) {
        /* child. */
        printf("pid: %d, I am child\n", getpid());
        
        /* sleep for 10s. */
        sleep(10);
        exit(0);
    } else {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = wait(&child_status);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}
image.png

可以看到,子进程休眠10秒未终止前,父进程被阻塞。

等待指定的子进程结束

正如前面所述,如果一个进程有几个子进程,那么只要有一个子进 程终止,wait 就返回。如果要等待一个指定的进程终止(如果知道要等待进程的ID),那么该如何做呢?在早期的UNIX版本中,必须调用 wait,然后将其返回的进程ID和所期望的进程ID相比较。如果终止进程 不是所期望的,则将该进程ID和终止状态保存起来,然后再次调用 wait。反复这样做,直到所期望的进程终止。下一次又想等待一个特定 进程时,先查看已终止的进程列表,若其中已有要等待的进程,则获取 相关信息;否则调用wait。其实,我们需要的是等待一个特定进程的函 数。POSIX.定义了waitpid函数以提供这种功能(以及其他一些功能)。

pid ==−1 等待任一子进程。此种情况下,waitpid与wait等效。
pid > 0 等待进程ID与pid相等的子进程。
pid == 0 等待组ID等于调用进程组ID的任一子进程。(9.4节将说明
进程组。)

waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存
放在由statloc指向的存储单元中。对于 wait,其唯一的出错是调用进程 没有子进程(函数调用被一个信号中断时,也可能返回另一种出错。第 10章将对此进行讨论)。但是对于waitpid,如果指定的进程或进程组不 存在,或者参数pid指定的进程不是调用进程的子进程,都可能出错。

options参数使我们能进一步控制waitpid的操作。此参数或者是0, 或者是图8-7中常量按位或运算的结果。

image.png

我们让waitpid的第三个参数为0,表示子进程终止前父进程阻塞
WNOHANG表示不阻塞

#include 
#include 
#include 
#include 
#include 

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end) {
    int mid = (end - start) >> 1;
    int i = start, j = end-1;
}
int main() {
    /*----------------------------------- test wait ----------------------------------------*/
    
    /* Pid. */
    pid_t pid;
    
    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0) {
        printf("fork error\n");
    }

    if ( pid == 0 ) {
        /* child. */
        printf("pid: %d, I am child\n", getpid());
        
        /* sleep for 10s. */
        sleep(10);
        exit(0);
    } else {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = waitpid(child_pid, &child_status, WNOHANG);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}


NOHANG没有阻塞

waitpid函数提供了wait函数没有提供的3个功能。
(1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程 的状态。在讨论popen函数时会再说明这一功能。
(2)waitpid提供了一个 wait 的非阻塞版本。有时希望获取一个子 进程的状态,但不想阻塞。
(3)waitpid通过WUNTRACED和WCONTINUED选项支持作业控 制。

  • 如何避免僵尸进程
    回忆有关僵死进程的讨论。如果一个进程fork一个子进程, 但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终 止,实现这一要求的诀窍是调用fork两次

8.6.waitid

Single UNIX Specification包括了另一个取得进程终止状态的函数— waitid,此函数类似于waitpid,但提供了更多的灵活性。

#include 
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

返回值:若成功,返回0;若出错,返回−1 与 waitpid 相似,waitid 允许一个进程指定要等待的子进程。但它使用两个单独的参数表示要等待的子进程所属的类型,而不是将此与进程 ID或进程组ID组合成一个参数。id参数的作用与idtype的值相关。该函 数支持的idtype类型列在图8-9中。

image.png
image.png
#include 
#include 
#include 
#include 
#include 

/* Global Intager. */
int golbal_num = 666;
void mergesort(int *p, int start, int end)
{
    int mid = (end - start) >> 1;
    int i = start, j = end - 1;
}
int main()
{
    /*----------------------------------- test wait ----------------------------------------*/

    /* Pid. */
    pid_t pid;

    /* Stack Intager. */
    int num = 0;

    if ((pid = fork()) < 0)
    {
        printf("fork error\n");
    }

    if (pid == 0)
    {
        /* child. */
        printf("pid: %d, I am child\n", getpid());

        /* sleep for 10s. */
        sleep(10);
        exit(0);
    }
    else
    {
        /* sava child pid. */
        pid_t child_pid = pid;

        /* parent. */
        printf("pid: %d, I am parent\n", getpid());

        printf("slove child\n");
        int child_status;
        pid_t cpid = waitpid(child_pid, &child_status, WNOHANG);
        printf("after wait, a child process terminated\n");
        printf("child pid: %d, status: %d\n", cpid, child_status);
    }

    return 0;
}

WCONTINUED、WEXITED或WSTOPPED这3个常量之一必须在 options参数中指定。
infop参数是指向siginfo结构的指针。该结构包含了造成子进程状态 改变有关信号的详细信息。

8.7.wait3和wait4

大多数UNIX系统实现提供了另外两个函数wait3和wait4。历史上, 这两个函数是从UNIX系统的BSD分支延袭下来的。它们提供的功能比 POSIX.1函数wait、waitpid和waitid所提供功能的要多一个,这与附加参 数有关。该参数允许内核返回由终止进程及其所有子进程使用的资源概况。

#include 
#include 
#include 
#include 
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时间总量、缺页次
数、接收到信号的次数等。

image.png

8.8 竞争条件

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决 于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如 果在 fork 之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运 行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。通 常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运 行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法

当多个进程都企图对共享数据进行某种处理,而最后的结果又取决 于进程运行的顺序时,我们认为发生了竞争条件(race condition)。如 果在 fork 之后的某种逻辑显式或隐式地依赖于在fork 之后是父进程先运 行还是子进程先运行,那么 fork 函数就会是竞争条件活跃的滋生地。通 常,我们不能预料哪一个进程先运行。即使我们知道哪一个进程先运 行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算法。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数中 的一个。如果一个进程要等待其父进程终止(如图8-8程序中一样), 则可使用下列形式的循环:

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

这种形式的循环称为轮询(polling),它的问题是浪费了CPU时 间,因为调用者每隔1 s都被唤醒,然后进行条件测试。
为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号 发送和接收的方法。在UNIX 中可以使用信号机制,各种形式的进程间通信(IPC)也可使用。

在父进程和子进程的关系中,常常出现下述情况。在fork之后,父 进程和子进程都有一些事情要做。例如,父进程可能要用子进程 ID 更 新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。 在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方, 并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以 用代码描述如下:

#include 
#include "../all.h"
TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx*/
if ((pid = fork()) < 0)
{
    err_sys("fork error");
}
else if (pid == 0)
{ /* child*/
    /* child does whatever is necessary ...*/
    TELL_PARENT(getppid());                 /* tell parent we're done*/
    WAIT_PARENT(); /* and wait for parent*/ /* and the child continues on its way ...*/
    exit(0);
}
/* parent does whatever is necessary ...*/ 
TELL_CHILD(pid); /* tell child we're done*/
WAIT_CHILD();                                               /* and wait for child*/
/* and the parent continues on its way ...*/
exit(0);

假定在头文件 apue.h 中定义了需要使用的各个变量。5 个例程 TELLWAIT、TELL PARENT、TELL_CHILD、WAIT_PARENT以及 WAIT_CHILD可以是宏,也可以是函数。
在后面几章中会说明实现这些TELL和WAIT例程的不同方法

下面的代码父进程和子进程的输出顺序可能随机

#include 
#include "../all.h"

/* No Buffer display str. */
static void display(char *str) {
    char *ptr;
    int c;
    setbuf(stdout, NULL);
    for (ptr = str; (c = *ptr++) != 0; ) {
        putc(c, stdout);
    }
}


int main() {
    pid_t pid;

    if((pid = fork()) < 0) {
        printf("fork error\n");
    } else if(pid == 0) {
        /* child. */
        display("output from child\n");
    } else {
        display("output from parent\n");
    }

    exit(0);
}
image.png

下面的代码可以避免竞争,实现同步

#include 
#include "../all.h"

/* No Buffer display str. */
static void display(char *str) {
    char *ptr;
    int c;
    setbuf(stdout, NULL);
    for (ptr = str; (c = *ptr++) != 0; ) {
        putc(c, stdout);
    }
}


int main() {
    pid_t pid;

    TELL_WAIT();
    if ((pid = fork()) < 0)
    {
        printf("fork error\n");
    } else if(pid == 0) {
        /* child. */
        WAIT_PARENT();
        display("output from child\n");
    } else {
        display("output from parent\n");
        TELL_CHILD(pid);
    }

    exit(0);
}

8.10 exec

exec

image.png

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

有7种不同的exec函数可供使用,它们常常被统称为exec函数,我们 可以使用这7个函数中的任一个。这些exec函数使得UNIX系统进程控制 原语更加完善。用fork可以创建新进程,用exec可以初始执行新的程 序。exit函数和wait函数处理终止和等待终止。这些是我们需要的基本的 进程控制原语。

#include 
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环境变量,在它所指定的各目录中搜寻可执行文 件。
PATH 变量包含了一张目录表(称为路径前缀),目录之间用冒号(:)分隔。例如,下列name=value环境字符串指定在4个目录中进行搜 索。

PATH=/bin:/usr/bin:/usr/local/bin:.

最后的路径前缀.表示当前目录。(零长前缀也表示当前目录。在 value的开始处可用:表示,在行中间则要用::表示,在行尾以:表示。)

如果execlp或execvp使用路径前缀中的一个找到了一个可执行文件, 但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件 是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输 入。

fexecve函数避免了寻找正确的可执行文件,而是依赖调用进程来完 成这项工作。调用进程可以使用文件描述符验证所需要的文件并且无竞 争地执行该文件。否则,拥有特权的恶意用户就可以在找到文件位置并 且验证之后,但在调用进程执行该文件之前替换可执行文件(或可执行 文件的部分路径)

函数 execl、execlp和execle要求将新程序的每个命令行参数都 说明为一个单独的参数。这种参数表以空指针结尾。对于另外4个函数 (execv、execvp、execve和fexecve),则应先构造一个指向各参数的指 针数组,然后将该数组地址作为这4个函数的参数。

在使用ISO C原型之前,对execl、execle和execlp三个函数表示命令 行参数的一般方法是:

char *arg0, char *arg1, ..., char *argn, (char *)0
这种语法显式地说明了最后一个命令行参数之后跟了一个空指针。 如果用常量0来表示一个空指针,则必须将它强制转换为一个指针;否 则它将被解释为整型参数。如果一个整型数的长度与char *的长度不 同,那么exec函数的实际参数将出错。

最后一个区别与向新程序传递环境表相关。以e结尾的3个函数 (execle、execve和fexecve)可以传递一个指向环境字符串指针数组的指 针。其他4个函数则使用调用进程中的environ变量为新程序复制现有的 环境(回忆7.9节及图7-8中对环境字符串的讨论。其中曾提及如果系统 支持setenv和putenv这样的函数,则可更改当前环境和后面生成的子进程 的环境,但不能影响父进程的环境)。通常,一个进程允许将其环境传 播给其子进程,但有时也有这种情况,进程想要为子进程指定某一个确 定的环境。例如,在初始化一个新登录的shell时,login程序通常创建一 个只定义少数几个变量的特殊环境,而在我们登录时,可以通过shell启 动文件,将其他变量加到环境中。
在使用ISO C原型之前,execle的参数是:

char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]

从中可见,最后一个参数是指向环境字符串的各字符指针构成的数 组的指针。而在ISO C原型中,所有命令行参数、空指针和envp指针都 用省略号(...)表示。
这7个exec函数的参数很难记忆。函数名中的字符会给我们一些帮 助。字母p表示该函数取filename作为参数,并且用PATH环境变量寻找 可执行文件。字母l表示该函数取一个参数表,它与字母v互斥。v表示该 函数取一个argv[ ]矢量。最后,字母e表示该函数取envp[ ]数组,而不使 用当前环境。

image.png

每个系统对参数表和环境表的总长度都有一个限制。
这种限制是由ARG_MAX给出的。在POSIX.1系统中,此值至 少是4 096字节。当使用shell的文件名扩充功能产生一个文件名列表时, 可能会受到此值的限制。

为了摆脱对参数表长度的限制,我们可以使用xargs(1)命令,将长参 数表断开成几部分。

前面曾提及,在执行exec 后,进程ID没有改变。但新程序从调用进 程继承了的下列属性:

• 进程ID和父进程ID •实际用户ID和实际组ID •附属组ID
•进程组ID
•会话ID
•控制终端
•闹钟尚余留的时间
•当前工作目录
•根目录
•文件模式创建屏蔽字
•文件锁
•进程信号屏蔽
•未处理信号
•资源限制
•nice值
•tms_utime、tms_stime、tms_cutime以及tms_cstime值

注意文件描述符

对打开文件的处理与每个描述符的执行时关闭(close-on-exec)标
志值有关

FD_CLOEXEC标志的说明,进程 中每个打开描述符都有一个执行时关闭标志。若设置了此标志,则在执行exec 时关闭该描述符;否则该描述符仍打开。除非特地用fcntl设置了 该执行时关闭标志,否则系统的默认操作是在exec后仍保持这种描述符 打开。

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

在很多UNIX实现中,这7个函数中只有execve是内核的系统调用。 另外6个只是库函数,它们最终都要调用该系统调用。

image.png
#include 
#include 
#include 
#include 

int main() {
    execlp("echo", "echo","only 1 args", (char*) 0);
    return 0;
}

8.11 更改用户id和组ID

在UNIX系统中,特权(如能改变当前日期的表示法)以及访问控 制(如能否读、写一个特定文件),是基于用户ID和组ID的。当程序 需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换 自己的用户ID或组ID,使得新ID具有合适的特权或访问权限。与此类 似,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用 户ID或组ID,新ID不具有相应特权或访问这些资源的能力。
一般而言,在设计应用时,我们总是试图使用最小特权(least privilege)模型。依照此模型,我们的程序应当只具有为完成给定任务 所需的最小特权。这降低了由恶意用户试图哄骗我们的程序以未预料的 方式使用特权造成的安全性风险。

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

#include  
int setuid(uid_t uid);  
int setgid(gid_t gid);

两个函数返回值:若成功,返回0;若出错,返回−1 关于谁能更改ID有若干规则。现在先考虑更改用户ID的规则(关于用户ID我们所说明的一切都适用于组ID)。
(1)若进程具有超级用户特权,则setuid函数将实际用户ID、有效
用户ID以及保存的设置用户ID(saved set-user-ID)设置为uid。

2)若进程没有超级用户特权,但是uid等于实际用户ID或保存的 设置用户ID,则setuid只将有效用户ID设置为uid。不更改实际用户ID和
保存的设置用户ID。

(3)如果上面两个条件都不满足,则errno设置为EPERM,并返回 −1。

关于内核所维护的3个用户ID,还要注意以下几点。
(1)只有超级用户进程可以更改实际用户ID。通常,实际用户ID 是在用户登录时,由login(1)程序设置的,而且决不会改变它。因为login 是一个超级用户进程,当它调用setuid时,设置所有3个用户ID。
(2)仅当对程序文件设置了设置用户ID位时,exec函数才设置有 效用户ID。如果设置用户ID位没有设置,exec函数不会改变有效用户 ID,而将维持其现有值。任何时候都可以调用setuid,将有效用户ID设 置为实际用户ID或保存的设置用户ID。自然地,不能将有效用户ID设 置为任一随机值。
(3)保存的设置用户ID是由exec复制有效用户ID而得到的。如果 设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的 有效用户ID以后,这个副本就被保存起来了。

image.png
1.函数setreuid和setregid
历史上,BSD支持setreuid函数,其功能是交换实际用户ID和有效用 户ID的值。
#include 
int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid);
两个函数返回值:若成功,返回0;若出错,返回-1 如若其中任一参数的值为−1,则表示相应的ID应当保持不变。
规则很简单:一个非特权用户总能交换实际用户ID和有效用户 ID。这就允许一个设置用户ID程序交换成用户的普通权限,以后又可 再次交换回设置用户ID权限。POSIX.1引进了保存的设置用户ID特性 后,其规则也相应加强,它允许一个非特权用户将其有效用户ID设置为 保存的设置用户ID。


2.函数seteuid和setegid
POIX.1包含了两个函数seteuid和setegid。它们类似于setuid和setgid, 但只更改有效用户ID和有效组ID。
#include 
int seteuid(uid_t uid); int setegid(gid_t gid);
两个函数返回值:若成功,返回0;若出错,返回−1 一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存
的设置用户ID。对于一个特权用户则可将有效用户ID设置为uid。(这 区别于setuid函数,它更改所有3个用户ID。)
image.png

8.12 解释器文件

所有现今的UNIX系统都支持解释器文件(interpreter file)。这种文 件是文本文件,其起始行的形式是:

! pathname [ optional-argument ]

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

! /bin/sh

pathname通常是绝对路径名,对它不进行什么特殊的处理(不使用 PATH进行路径搜索)。对这种文件的识别是由内核作为 exec系统调用 处理的一部分来完成的。内核使调用 exec函数的进程实际执行的并不是 该解释器文件,而是在该解释器文件第一行中pathname所指定的文件。 一定要将解释器文件(文本文件,它以#!开头)和解释器(由该解释器 文件第一行中的pathname指定)区分开来。

很多系统对解释器文件第一行有长度限制。这包括#!、pathname、 可选参数、终止换行符以及空格数。

在FreeBSD 8.0中,该限制是4 097字节。Linux 3.2.0中,该限制 为128字节。Mac OS X 10.6.8中,该限制为513字节,而Solaris 10的 限制是1 024字节。

#include 
#include 
#include 
#include 

int main() {
    pid_t pid;

    if((pid = fork()) < 0) {
        printf("fork error\n");
    }

    if(pid == 0) {
        /* child. */
        execl("path", "program", "args1", "args2", (char *)0);
    } else {
        if(waitpid(pid, nullptr,0) < 0) {
            printf("waitpid error\n");
        }
        exit(0);
    }
}

program内容是

!# python 

实际上exec将会加载python, 并执行 python progra args1 args2

8.13函数system

在程序中执行一个命令字符串很方便。例如,假定要将时间和日期 放到某一个文件中,则可使用6.10节中的函数实现这一点。调用time得 到当前日历时间,接着调用localtime将日历时间变换为年、月、日、 时、分、秒、周日的分解形式,然后调用strftime对上面的结果进行格式 化处理,最后将结果写到文件中。但是用下面的system函数则更容易做 到这一点:

system("date > file");
ISO C定义了system函数,但是其操作对系统的依赖性很强。 POSIX.1包括了system接口,它扩展了ISO C定义,描述了system在 POSIX.1环境中的运行行为。

#include 
int system(const char *cmdstring);

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

(3)否则所有3个函数(fork、exec和waitpid)都成功,那么system 的返回值是shell的终止状态,其格式已在waitpid中说明。

image.png

8.14 会计进程

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

任一标准都没有对进程会计进行过说明。于是,所有实现都有令人 厌烦的差别。例如,关于I/O的数量,Solaris 10使用的单位是字节, FreeBSD 8.0和Mac OS X 10.6.8使用的单位是块,但又不考虑不同的块 长,这使得该计数值并无实际效用。Linux 3.2.0则完全没有保持I/O统 计数。
每种实现也都有自己的一套管理命令去处理这种原始的会计数据。 例如,Solaris 提供了runacct(1m)和acctcom(1),FreeBSD则提供 sa(8)命令处理并总结原始会计数据。

一个至今没有说明的函数(acct)启用和禁用进程会计。唯一使用 这一函数的是accton(8)命令(这是在几种平台上都类似的少数几条命令 中的一条)。超级用户执行一个带路径名参数的accton命令启用会计处 理。会计记录写到指定的文件中,在FreeBSD和Mac OS X中,该文件通 常是/var/account/acct;在Linux中,该文件是/var/account/pacct;在 Solaris中,该文件是/var/adm/pacct。执行不带任何参数的accton命令则 停止会计处理。
会计记录结构定义在头文件中,虽然每种系统的实现

各不相同,但会计记录样式基本如下:

typedef u_short comp_t;    /* 3-bit base 8 exponent; 13-bit
fraction*/ struct acct
{
char  ac_flag;        /* flag (see Figure 8.26)*/
char  ac_stat;        /* termination status(signal & core
flag only)*/
/* (Solaris only)*/
uid_t ac_uid;        /* real user ID*/ gid_t ac_gid;        /* real group ID*/ dev_t ac_tty;        /* controlling terminal*/
time_t ac_btime;       /* starting calendar time*/
comp_t ac_utime;       /* user CPU time*/
comp_t ac_stime;       /* system CPU time*/ comp_t ac_etime;       /* elapsed time*/
comp_t ac_mem;        /* average memory usage*/ comp_t ac_io;         /* bytes transferred (by
read and write)*/
comp_t ac_rw;         /* blocks read or
written*/
char  ac_comm[8];      /* command name: [8] for
Solaris,*/
/* "blocks" on BSD systems*/
/* (not present on BSD systems)*/
/* [10] for Mac OS X, [16] for FreeBSD, and*/ /* [17] for Linux*/
};
在大多数的平台上,时间是以时钟滴答数记录的,但FreeBSD以微 秒进行记录的。ac_flag成员记录了进程执行期间的某些事件。这些事件 见图8-26。

image.png

会计记录所需的各个数据(各CPU时间、传输的字符数等)都由内 核保存在进程表中,并在一个新进程被创建时初始化(如fork之后在子 进程中)。进程终止时写一个会计记录。这产生两个后果。
第一,我们不能获取永远不终止的进程的会计记录。像init这样的 进程在系统生命周期中一直在运行,并不产生会计记录。这也同样适合 于内核守护进程,它们通常不会终止。
第二,在会计文件中记录的顺序对应于进程终止的顺序,而不是它 们启动的顺序。为了确定启动顺序,需要读全部会计文件,并按启动日 历时间进行排序。这不是一种很完善的方法,因为日历时间的单位是秒 (见 1.10 节),在一个给定的秒中可能启动了多个进程。而墙上时钟时 间的单位是时钟滴答(通常,每秒滴答数在60~128)。但是我们并不 知道进程的终止时间,所知道的只是启动时间和终止顺序。这就意味 着,即使墙上时钟时间比启动时间要精确得多,仍不能按照会计文件中 的数据重构各进程的精确启动顺序。
会计记录对应于进程而不是程序。在fork之后,内核为子进程初始 化一个记录,而不是在一个新程序被执行时初始化。虽然exec并不创建

一个新的会计记录,但相应记录中的命令名改变了,AFORK标志则被 清除。这意味着,如果一个进程顺序执行了3个程序(A exec B、B exec C,最后是C exit),只会写一个会计记录。在该记录中的命令名对应于 程序C,但CPU时间是程序A、B和C之和。

8.15用户标识

任一进程都可以得到其实际用户ID和有效用户ID及组ID。但是, 我们有时希望找到运行该程序用户的登录名。我们可以调用 getpwuid(getuid()),但是如果一个用户有多个登录名,这些登录名又对应 着同一个用户ID,又将如何呢?(一个人在口令文件中可以有多个登录 项,它们的用户 ID 相同,但登录 shell 不同。)系统通常记录用户登录 时使用的名字(见 6.8 节),用getlogin函数可以获取此登录名。

#include 
    char *getlogin(void);

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

#include 
#include 

int main() {
    printf("username: %s\n", getlogin());
    return 0;
}
image.png

返回值:若成功,返回指向登录名字符串的指针;若出错,返回NULL 如果调用此函数的进程没有连接到用户登录时所用的终端,则函数会失败。通常称这些进程为守护进程(daemon).

给出了登录名,就可用getpwnam在口令文件中查找用户的相应记 录,从而确定其登录shell等。

你可能感兴趣的:(APUE第8章 进程控制)