GNU make 经常作为更大的工具体系中的一个部件来使用,例如集成开发环境(IDE)或编译器工具链等。make 的职责是启动命令,并判定其是否成功。仅仅完成这一职责,并不需要任何特殊的协作机制。然而,有时把 make 与系统的其他部分更紧密地结合起来会更方便。这既包括与上层工具(调用 make 的一方)的协作,也包括与下层工具(被 make 调用的一方)的协作。
GNU make 具有并行执行多条命令 (recipe) 的能力(参见并行执行),并且能够跨越 make 的递归调用为并行作业 (job) 的总数设置上限(参见向子 make 传递选项)。被 make 调用的某些工具,自身也能够使用多个线程或多个进程并行地执行多项操作。可以对这类工具加以扩展,使其参与到 GNU make 的作业管理机制中,从而确保系统上正在运行的线程/进程总数,不会超过分配给 GNU make 的最大槽数。
GNU make 使用一种称为「jobserver」的方法,跨越递归调用来控制活动作业的数量。jobserver 的具体实现因操作系统而异,但有一些基本要点始终是一致的。
首先,make 会通过环境变量 MAKEFLAGS,把访问 jobserver 所需的信息传递给它的子进程。想要参与 jobserver 协议的工具,需要解析这个环境变量,并找到以 --jobserver-auth= 开头的那个词。该选项的值描述了应如何与 jobserver 通信。这个值的解释方式将在下面各节中说明。
请注意,MAKEFLAGS 变量中可能包含多个 --jobserver-auth= 选项。只有最后一个实例才有意义。
其次,make 启动的每一条命令,在其启动之前都已为其预留了一个隐式的作业槽。任何想要参与 jobserver 协议的工具,都可以认为自己无需向 jobserver 发出任何请求,就始终能够运行一个作业。
最后,极为重要的一点是:参与 jobserver 协议的工具,在退出之前——即使发生了错误——也必须把从 jobserver 取得的槽数准确无误地归还给 jobserver。请记住,前述的那个隐式作业槽不应归还给 jobserver!归还的槽数过少,意味着这些槽会在整个构建过程的余下时间里一直丢失;归还的槽数过多,则意味着会多出可用的槽。顶层的 make 命令一旦检测到 jobserver 中可用的槽数不正确,就会在构建结束时显示一条错误消息。
举例来说,假设你正在实现一个支持多线程操作的链接器。你希望对这个链接器加以扩展,使其在被 GNU make 调用时能够参与 jobserver 协议,从而控制链接过程中所使用的线程数。首先,你需要改造链接器,以判定是否设置了 MAKEFLAGS 环境变量。接着,你需要解析该变量的值,以判定 jobserver 是否可用,以及应如何访问它。如果可用,你就可以访问它来取得作业槽,从而控制你的工具能够使用多大的并行度。完成之后,你的工具必须把这些作业槽归还给 jobserver。
在 POSIX 系统上,jobserver 以两种方式之一实现。在支持命名管道(named pipe)的系统上,GNU make 会创建一个命名管道并将其用作 jobserver。在这种情况下,auth 选项的形式为 --jobserver-auth=fifo:PATH,其中「PATH」是命名管道的路径名。要访问 jobserver,你应当打开该命名管道的路径,并按下文所述对其进行读写。
如果系统不支持命名管道,或者用户给出了 --jobserver-style 选项并指定了「pipe」,那么 jobserver 将以一个简单的 UNIX 管道来实现。在这种情况下,auth 选项的形式为 --jobserver-auth=R,W,其中「R」和「W」是表示文件描述符的非负整数:「R」是读文件描述符,「W」是写文件描述符。如果这两个文件描述符中的任一个或两个均为负值,则意味着对本进程而言 jobserver 已被禁用。
使用简单管道时,只有那些被 make 识别为 make 自身递归调用的命令行,才能访问 jobserver(参见MAKE 变量的工作原理)。编写 makefile 时,你必须确保把该命令标记为递归的(最常见的做法是在命令行开头加上 + 标记。参见make 的递归用法)。请注意,jobserver 管道的读取端被设置为「阻塞(blocking)」模式。这一点不应更改。
在 jobserver 的两种实现中,管道里都会预先放入若干个单字符令牌,每个可用作业对应一个令牌。要额外取得一个槽,你必须从 jobserver 读取一个字符;要释放一个槽,你必须向 jobserver 写回一个字符。
释放作业槽时,写回你所读取的同一个字符是很重要的。不要假定所有令牌都是同一个字符;不同的字符对 GNU make 可能具有不同的含义。顺序并不重要,因为 make 本来就完全不知道作业会以何种顺序完成。
为使你的实现足够健壮,你必须考虑各种各样的错误情形:
--jobserver-auth 字符串的格式,就应当认为 jobserver 正在使用另一种样式,因而无法连接。
--jobserver-auth 选项所指向的是一个简单管道,但所指定的文件描述符是关闭的,这意味着发起调用的 make 进程并未把你的工具视为一次递归的 make 调用(例如,命令行开头未加 + 字符)。你应当把这一情形告知用户。
SIGINT)之类来自外部的影响。你或许会想要安装信号处理函数来管理这一写回过程。
MAKEFLAGS 变量的第一个词,看其中是否含有字符 n。如果存在该字符,则说明 make 是带着「-n」选项被调用的,因此你的工具或许应当不执行任何操作便停止。
在 Windows 系统上,jobserver 以一个命名信号量(named semaphore)来实现。该信号量被设置为一个等于可用槽数的初始计数;要取得一个槽,你必须等待(wait)该信号量(可带超时,也可不带)。要释放一个槽,则释放(release)该信号量。
要访问该信号量,你必须解析 MAKEFLAGS 变量,并查找参数字符串 --jobserver-auth=NAME,其中「NAME」是该命名信号量的名称。把这个名称传给 OpenSemaphore,以创建指向该信号量的句柄。
--jobserver-style 唯一有效的样式是「sem」。
为使你的实现足够健壮,你必须考虑各种各样的错误情形:
SIGINT)之类来自外部的影响。你或许会想要安装信号处理函数来管理这一写回过程。
通常,GNU make 会让它启动的所有命令都能访问与 make 自身启动时相同的标准输出和标准错误输出。许多工具会检测输出目标是终端(terminal)还是非终端,并据此改变输出样式。例如,如果输出指向终端,工具可能会加入设置颜色的控制字符,甚至改变光标的位置。如果输出不指向终端,则不会发出这些特殊的控制字符,以免破坏日志文件等。
--output-sync 选项(参见并行执行时的输出)会使这种终端检测失效。当启用输出同步时,GNU make 会安排把所有命令的输出先写入一个文件,以便其输出能够作为一整块写出,而不受其他命令输出的干扰。这样一来,被 make 调用的所有工具都会以为自己的输出不会显示在终端上,即使实际上会显示(因为 make 会在命令完成后把它显示到终端上)。
为了帮助那些想要判定其输出是否会显示在终端上的工具,GNU make 会在调用任何命令之前设置环境变量 MAKE_TERMOUT 和 MAKE_TERMERR。想要判定标准输出(或标准错误输出)是否会显示在终端上的工具,可以检查这些环境变量是否存在且具有非空的值。如果是,则该工具可以认为输出(最终)会显示在终端上。如果这些变量未被设置或值为空,则该工具应当退回到其判定输出是否指向终端的常规方法。
解析这些变量的内容,还可以判别用于显示输出的终端类型。
同样,那些调用 make 并希望捕获其输出、最终将其显示在终端(或某种能够解释终端控制字符的显示设备)上的环境,可以在调用 make 之前先设置这些变量。如果 GNU make 在启动时这些环境变量已经存在,它便不会修改它们。