第6章 进程

在本章中,我们看下进程的结构,重点关注进程虚拟内存的布局和结构。还会介绍下进程的某些属性。在后续的章节中,会进一步介绍进程的属性 (例如,在第9章中介绍进程凭证,在第35章介绍进程优先级和调度) 。从第24章到27章,我们会介绍进程是如何创建的,如何终止的,如何执行新的程序。

6.1 Processes and Programs

一个 进程(process) 是一个正在运行程序(program)的实例。在本节中,我们会详细说明程序和进程的区别。
一个程序是一个文件,它包含了一系列信息,用于描述如何在运行时构建一个进程。这些信息包括:

  • 二进制格式ID (Binary format identification): 每个程序文件包含了用于描述可执行文件格式的元信息(metainformation)。这些信息可以让内核知道如何解析文件中剩下的信息。历史上,UNIX可执行文件有两种被广泛使用的格式:起初的 a.out (“assembler output”) 格式和后来的更加成熟的 COFF(Common Object File Format)。而当今,大部分UNIX系统(包括Linux)采用可执行与可链接格式(Executable and Linking Format,ELF ),它与老的格式相比,具有更多优势。
  • 机器语言指令(Machine-language instructions):它们将程序的算法进行编码。
  • 程序入口点地址(Program entry-point address) : 用于识别从哪个指令位置开始执行程序。
  • 数据(data) : 包含初始化变量的值和程序中使用到的字母常量的程序文件。
  • 符号和重定位表(Symbol and relocation table): 它们描述了程序中函数和变量的位置和名称。这些表用于各种目的,包括调试(debugging)和运行时符号解析(symbol resoltuin, dynamic linking)。
  • 共享库和动态链接信息(Shared-library and dynamic-linking information):程序文件中包含了运行时程序需要的共享库的列表以及动态链接器(dynamic linker)加载这些库所需的路径名(pathname)。
  • 其他信息(Other information): 含有各种其他信息的程序文件,这些信息用于描述如何构建一个进程。

一个程序可能会用于构建很多进程。或者反着说,很多进程可能以相同的程序运行。
我们可以将 进程 重新定义成:进程是一个由内核定义的,为了让程序运行,需为它分配系统资源的抽象的实体。
从内核的角度看,一个进程包含 用户空间内存(user-space memory) 和一系列 内核数据结构 。用户空间内存中存储了程序代码和代码中用到的变量。内核数据结构中包含进程状态的信息。内核数据结构中记录的信息包含各种与进程、虚拟内存表、打开文件描述符列表、信号传递的相关信息、进程资源使用和限制、当前工作目录、主机和其他信息相关的 标识符数字(IDs)

6.2 Process ID and Parent Process ID

每个进程都有一个进程ID(Process ID, PID),它是一个正整数,在系统中对进程进行唯一标识。进程ID会被很多系统调用使用和返回。例如,kill()系统调用(章节20.5)可以让调用者将信号(signal)发送给指定进程ID的进程。如果我们需要为进程构建一个具有唯一性的标识符,那么进程ID就很有用。一个常用例子是使用进程ID作为进程文件名的一部分。
getpid() 系统调用返回调用进程的进程ID。

#include <unistd.h>
//总是可以成功地返回调用进程的进程ID
pid_t getpid(void);

pid_t 数据类型用于getpid()的返回值,它是由SUSv3中规定的一个整型(integer),用于存储进程ID。
除了很少的一些系统进程,例如 init进程ID是1),进程的进程ID与程序之间没有固定的关系。
Linux内核将进程ID限制为小于等于32767。当新的进程被创建时,它会被赋予一个按序的可用的进程ID。每次到达了32767,内核会重置 进程ID计数器(process ID Counter),这样被赋予的进程ID会重新从小的整数值开始。

一旦进程ID计数器到达了32767,计数器会被重新设置为300,而不是1。因为很多小的进程ID会被系统进程和守护进程永久地使用。
在Linux 2.4和更早的版本中,进程ID 32767的数量限制是在内核常量PID_MAX中定义的。在Linux 2.6中,做了变动,进程ID 默认 的上限仍然是32767,但上限值可以通过Linux特有的 /proc/sys/kernel/pid_max 这个文件调整(比最大进程ID大1,例如pid_max的文件内容是32768,那么最大进程ID是32767)。在32位系统中,这个文件的最大值是32768,但是在64位系统中,可以调整到222(大概4百万)间的任意值,使得可以满足很大数量的进程。

每个进程都有一个父进程,父进程是创建该进程的那个进程。进程可以通过使用 getppid() 系统调用找到父进程ID。

#include <unistd.h>
//总能成功地返回调用进程的父进程的ID
pid_t getppid(void);

事实上,每个进程的父进程ID的属性表示成系统中所有进程的树状关系。每个进程的父进程都有它们的父进程,以此类推,一直可以追朔到 init进程(进程ID 1),它是所有进程的祖先。(可以通过使用pstree(1) 命令查看家族树(family tree))

在这里插入图片描述
如果一个子进程成为了孤儿进程,因为创建(birth)它的父进程终止了。那么这个子进程就会被init进程收养,随后在子进程中调用getppid()会返回1(章节26.2)。
任何进程的父进程都可以通过Linux特有的 /proc/PID/status 文件中的PPid字段找到。
在这里插入图片描述

6.3 Memory Layout of a Process

分配给每个进程的内存可分成很多部分,这些部分被称为 segment 。这些segment如下:

  • 文本片段(text segment) 包含进程运行的程序的机器语言指令。text segment是 只读 的,这样进程不会因为意外导致坏的指针值修改这些指令。因为很多进程可能运行在相同的程序上,text segment是 可共享 的,所以程序代码的单份复制可以映射到所有进程的虚拟地址空间上。
  • 初始化数据片段(initialized data segment,也称user-initialized data segment) 包含明确(explicitly)初始化的全局和静态变量。这些变量的值是在程序被加载到内存中时,从可执行文件中读取。
  • 未初始化数据片段(uninitialized data segment,也称zero-initialized data segment) 包含未初始化的全局和静态变量。在启动程序之前,系统将这个segment上的所有内存初始化为0。由于历史原因,这个segment经常被称为 bss segment,该名称来源于“block started by symbol”。将初始化和未初始化的全局和静态变量分开放入不同的segments的主要原因是当程序存储在磁盘上时,不需要为未初始化的数据分配空间。可执行程序仅仅只需要为uninitialized data segment记录需要的位置和大小,这块空间会由程序加载器(program loader)在运行时分配。
  • 栈(stack) 是一块动态增长和收缩的segment,包含 栈帧 (stack frames) 。栈帧是为每个当前调用的函数分配的。栈帧存储了函数的本地变量(local variable)、参数和返回值。栈帧会在章节6.5进行更详细的讨论。
  • 堆(heap) 是一块内存区域,可以在运行时动态分配。堆的顶端被称为 program break

size(1) 这个命令显示二进制可执行文件的text segment、initialized data segment和uninitialized data segment的大小(size)。

文中用到的术语segment不要跟一些硬件架构中用到的硬件segmentation搞混。segments是UNIX系统中进程虚拟内存的逻辑划分。有时,使用术语section替代segment,因为术语section与ELF(Executable and Linking Format)规范中用到的术语是一致的。
在本书的很多地方,我们可以注意到一个库函数返回的指针指向静态分配内存。这意味着这块分配的内存是在initialized data segment或uninitialized data segment中。

Listing 6-1 中展示了各种C变量类型,对应的注释说明了每个变量所处的segment。这些注释是基于非优化的编译器的以及应用二进制接口(ABIs)中的所有参数都会传入到栈中。事实上,一个优化过的编译器可能会将频繁用到的变量放入到寄存器中。此外,一些ABIs要求函数的参数和结果通过寄存器传递,而不是栈。尽管如此,下面的例子用于展示C变量和进程segments之间的映射关系。
Listing 6-1:Locations of program variables in process memory segments

// proc/mem_segments.c
#include <stdio.h>
#include <stdlib.h>
char globBuf[65536]; /* Uninitialized data segment */
int primes[] = {2, 3, 5, 7}; /* Initialized data segment */
static int square(int x) /* Allocated in frame for square() */
{
	int result; /* Allocalted in frame for square() */
	result = x * x;
	return result; /* Return value passed via register */
}

static void doCalc(int val) /* Allocated in frame for doCalc() */
{
	printf("The square of %d is %d\n", val, square(val));
	if (val < 1000)
	{
		int t;  /* Allocated in frame for doCalc() */
		t = val * val * val;
		printf("The cube of %d is %d\n", val, t);
	}
}

int main(int argc, char *argv[])
{
	static int key = 9973; /* Initialized data segment*/
	static char mbuf[10240000]; /* Uninitialized data segment*/
	char *p; /* Allocated in frame for main() */
	p = malloc(1024); /* Points to memory in heap segment */
	doCalc(key);
	exit(EXIT_SUCCESS);
}

应用二进制接口(application binary interface , ABI) 是一组规则,规定了二进制可执行文件在运行时如何通过使用一些服务(例如内核或者函数库)交换数据。ABI还规定了交换这些信息需要用到寄存器和栈的哪些位置。一旦某个二进制可执行文件是为特定ABI而编译的,那么这个二进制可执行文件就可以在任何提供了相同ABI的系统上运行。这与标准API(例如SUSv3)一样,保证了源码编译后应用的可移植性。

尽管没有在SISv3中规定,大部分UNIX系统(包括Linux)中的C程序环境提供了三种全局符号:etextedataend 。分别用于在程序中获取程序文本(program text,也就是text segment)的末尾(end,也就是最后一个字节)地址、initialized data segment的末尾地址和uninitialized data segment的末尾地址。想要使用这些符号,我们需要显式地声明它们,如下:

/*For example, &etext gives the address of the end of the program text*/
extern char etext, edata, end;

Figure 6-1 展示了在x86-32中各种内存segments的布局。图表上方由argv、environ标记的空间用于存储命令行参数(在C中是main()函数的argv参数)和进程环境列表(environment list)。图表中显示的十六进制(hexadecimal)地址会有很多,取决于内核配置和程序链接选项。图表中灰色的区域表示进程虚拟地址空间中的无效范围(invalid ranges)。也就是说,还没创建page tables的区域(请看下面虚拟内存管理的讨论)。
在章节48.5,我们会介绍共享内存和共享库在进程虚拟内存中的位置,到时我们会更详细地回顾进程内存布局这个主题。

在这里插入图片描述

6.4 Virtual Memory Management

因为对虚拟内存(virtual memory)的理解有助于我们后续内容的学习,例如:fork()系统调用、共享内存、映射文件。所以我们现在来看下细节。
像大部分现代内核一样,Linux采用了一种称为 虚拟内存管理(virtual memory management) 的技术。这种技术的目的是提高CPU和RAM(物理内存)的效率,它是通过使用一种大部分程序都有的属性:locality of reference(引用局部性) 来实现的。大部分程序有两类局部性(locality):

  • 空间局部性(spatial locality) 是一种趋势:程序中引用了某个内存地址,那么附近的内存地址最近也可能会被访问 (因为指令的顺序处理和有时数据结构的顺序处理)。
  • 时间局部性(temporal locality) 是一种趋势:程序中访问了某个内存地址,那么在不久的将来,可能会被再次访问(因为循环)。

引用局部性的要点是:在RAM中,放入运行程序需要的部分地址空间。
虚拟内存的模式是将每个程序用到的内存切分成小的、固定大小的单元,称之为 page。相应地,RAM(物理内存)被划分成一系列相同大小的 page frames。在任何时刻,只有程序的一部分pages需要驻扎在物理内存的page frames中。这些pages组成了所谓的 驻留集合(resident set) 。程序的未使用的pages的拷贝保存在交换区(swap area)–
它是磁盘空间中的一块保留区域,用于补充计算机的RAM–只有需要的时候才会加载到物理内存中。当进程引用的page当前没有驻扎在物理内存时,就会发生一个page fault,同时内存暂停执行该进程,并将page从磁盘中加载到内存中。

在x86-32中,page的大小(size)是4096字节。一些其他的Linux系统使用更大的page。例如Alpha使用8192字节的page,IA-64使用一个可变的page大小,默认是16384字节。程序可以使用sysconf(_SC_PAGESIZE)系统调用决定系统虚拟内存的page大小。

在这里插入图片描述

Figure 6-2: Overview of virtual memory
为了支持这种组织,内核为每个进程维护了一个 page table (Figure 6-2)。page table描述了虚拟地址空间(virtual address space) 中每个page的位置。page table中每个条目不是表示RAM中的虚拟页就是表示当前驻扎在磁盘中的虚拟页。
进程的虚拟地址空间中不是所有的地址范围都要在page-table中有条目。一般来说,虚拟地址空间中较大的那部分虚拟地址未被使用,所以也不需要维护对应的page-teble条目。如果进程尝试访问page-table中没有相应条目的(虚拟地址空间的)地址,就会收到SIGSEGV信号。
进程的有效虚拟地址的范围是可以改变的,因为内核为会进程分配和回收pages(和page-table条目)。这会发生在以下情形中:

  • 随着栈的增大或减小,会超过之前到达过的界限(limit)。
  • 当在堆中分配或回收内存,通过使用brk()、sbrk()或者malloc函数家族(第7章)提升堆的program break(堆的最顶端)。
  • 当System V(UNIX操作系统的一个分支)中的共享内存区域使用shmat()被attched以及使用shmdt()被detached。(第48章)。
  • 当使用mmap()创建内存映射以及使用munmap()解除内存映射时(第49章)。

虚拟内存的实现需要硬件支持,需要paged memory management unit (PMMU)。PMMU将每个虚拟内存地址引用转换成对应的物理内存地址,当某个虚拟内存地址对应的page没有驻扎在RAM时,会向内核报告一个page fault。

虚拟内存管理(virtual memory management)将进程的虚拟地址空间和RAM的物理地址空间分开来。这样做有几个优势:

  • 进程之间以及进程与内核之间相互隔离,所以进程不能读取或修改另一个进程或内核的内存。这是通过每个进程的page-table条目指向RAM(或swap area)中不同的pages集合来完成的。
  • 当合适的时候,两个或多个进程可以共享内存。内核使不同进程的page-table条目指向RAM中相同的pages成为可能。内存共享发生在以下两种通常情形:
    • 多个进程执行相同程序可以共享程序代码的一份拷贝(只读)。当多个程序执行相同程序文件(或加载相同的共享库),这种类型的共享会隐式执行。
    • 进程可以使用 shmget()mmap() 系统调用显示地请求与其他进程共享内存区域。这样做的目的主要是为了进程间通信。
  • 内存保护模式的实现是容易的。也就是page-table条目可以标记对应page的内容是可读的、可写的、可执行的或者这三种权限的组合。当多个进程共享RAM中的pages时,可以为每个进程指定不同的访问(pages的)权限。例如,一个进程对某个page具有只读权限,而另一个进程对这个page具有读写权限。
  • 程序员以及例如编译器和链接器这样的工具不需要关心程序在RAM中的物理布局。
  • 因为只要程序的一部分才需要驻扎在内存,程序加载和运行会更快。此外,一个进程的内存占用可以超过RAM的容量。

6.5 The Stack and Stack Frames

Stack会随着函数的调用和返回而线性地增长和减短。在x86-32架构上的Linux(以及其他Linux和UNIX实现),stack驻扎在内存的高端,且向下增长(朝着heap)。stack pointer(栈指针) 是一个用于特殊目的的寄存器,用于跟踪当前 栈顶(top of the stack) 。每次一个函数被调用,就会在stack中分配一个frame,当函数返回时这个frame就会被删除。

即使stack向下增长,我们仍然将stack增长的那端叫做 top ,因为在抽象的(stack)术语中,就是这么叫的。stack增长的实际方向是一种(硬件)实现细节,HP PA-RISC这种Linux系统,就使用了向上增长的栈。
在虚拟内存术语中,stack segment会随着stack frame的分配(入栈)而增长,但是在大部分系统实现中,当这些frame删除(出栈)后,栈不会减小(这些内存会供给新的stack frame使用)。当我们提到stack segment的增长和减短时,我们会从逻辑的角度思考frame的入栈和出栈。

有时,我们使用术语 用户栈(user stack) 来区分 内核栈 (kernel stack)。kernel stack是内核内存中为每个进程维护的一块内存区域,在系统调用(system call)执行时就会用到kernel stack。
每个(user)stack frame包含以下信息:

  • 函数参数和局部变量 :在C中,这些被称为 自动变量(automatic variable) ,因为当函数被调用时它们会自动地创建。当函数返回时,这些变量也会自动消失(因为stack frame消失了),这是自动变量和静态(和全局)变量的主要区别:后者会永久地存在,与函数执行无关。
  • 调用链接信息(call linkage information) :每个函数使用特定的CPU寄存器,例如程序计数器(program counter)指向下一条需要执行的机器语言指令。每当一个函数调用了另一个函数,这些寄存器的一份拷贝会保存到调用函数的stack frame中,这样当函数返回时,可以为调用函数还原合适的寄存器值。

因为函数可以调用另一函数,所以在stack中可能会有多个frames(如果一个函数递归调用自己,stack中会有这个函数的多个frames)。参考Listing 6-1,在函数square()执行的过程中,stack会包含Figure 6-3所示的frames:
在这里插入图片描述

6.6 Command-Line Argument( argc, argv)

每个C程序必须有一个main()函数,这是程序执行的入口点。当程序执行时,可以通过main()函数的两个参数来传入命令行参数。第一个参数 int argc 表示有几个命令行参数。第二个参数 char * argv[] 是一个指向命令行参数的指针数组。第一个数组元素argv[0]是程序本身的名字。argv中的指针列表以NULL指针结束(也就是argv[argc]是NULL)。
Figure 6-4 展示了执行Listing 6-2程序时,argc和argv相关的数据结构:
在这里插入图片描述

#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
 int j;
 for (j = 0; j < argc; j++)
 printf("argv[%d] = %s\n", j, argv[j]);
 exit(EXIT_SUCCESS);
}

因为argv列表是以NULL值结尾的,我们也可以将Listing 6-2写成如下形式:

char **p;
for (p = argv; *p != NULL; p++)
	puts(*p);

argc/argv机制的一个限制是这些变量只能作为main()的参数。想要使这些命令行参数在其他函数中可用,我们必须将argv作为参数传入这些函数,或者设置一个全局变量指向argv。
有一些方法可以在程序中的 任何地方 访问部分或全部的这些信息:

  • 任何进程的命令行参数都可以从Linux特有的 /proc/PID/cmdline 文件中读取。每个参数之间由 null字节(’\0’字符) 分隔。程序可以通过 /proc/self/cmdline 访问自己的命令行参数。
  • GNU C库提供了两个全局变量,用于在程序的任何地方获取调用程序的名字(也就是命令行的第一个参数)。第一个变量是program_invocation_name,可以获取调用程序的完整的路径名。第二个变量是program_invocation_short_name,可以获取调用程序的文件名(去除了路径)。这两个变量的声明可以通过定义宏_GNU_SOURCE从<errno.h>获得。

正如Figure 6-1 所示,argv和environ数组,以及它们所指向的字符串,是驻扎在进程栈(process stack)上方的一块连续的内存区域中。这块区域可以存储的总字节数具有上限值。SUSv3中规定使用ARG_MAX常量(定义在<limits.h>)或者调用sysconf(_SC_ARG_MAX)来决定这个限制。SUSv3要求ARG_MAX至少是_POSIX_ARG_MAX(4096)字节。

在Linux中,ARG_MAX由于历史原因,固定在32页(也就是,在Linux/x86-32是131072字节)。从kernel 2.6.23开始,argv和environ使用到的总的空间限制可以通过RLIMIT_STACK这个资源限制(resources limit)控制,是RLIMIT_STACK的四分之一。

很多程序使用getopt()库函数来解析命令行选项。我们会在附录B中描述getopt()

6.7 Environment list

每个进程都有一个称为 环境列表(environment list)(或者简称为 environment)的字符串数组。每个字符串都以 name=value 的形式定义。因此,环境列表 表示一组name-value对,可用于持有任何信息。列表中的名称(name)被称为 环境变量(environment variables)
当一个新的进程被创建时,它会继承父进程的环境变量的一份拷贝。环境列表 提供了将父进程的信息传递给子进程的一种方法。因为子进程只有在被创建时才会获取父进程的环境列表,这种信息的传递是单向的且只有一次。在子进程被创建后,每个进程都可能改变自己的环境列表,这些变动对于其他进程不可见。
环境变量常见的用法是在shell中。通过给环境变量赋值,shell可以确保这些值被传入它所创建的进程中。
一些库函数允许通过设置环境变量来修改它们的行为。这使得用户可以通过函数来控制一个应用的行为,而不需要改变应用的代码或者重链接相应的库。这种技术的一个例子是getopt()函数(附录B),它的行为可以通过设置POSIXLY_CORRECT环境变量来修改。
大部分shell中,可以使用 export 命令为环境列表增加值:

$ SHELL=/bin/bash                                         // Create a shell variable
$ export SHELL                                            // Put variable into shell process's environment

在bash和Korn shell中,可以简写成:

$ export SHELL=/bin/bash

在C shell中,使用 setenv 命令替代:

% setenv SHELL /bin/bash

上面的命令会将值永久地添加到shell的环境列表中,然后这个环境列表会被这个shell创建的所有子进程继承。
任何时候,环境变量可以通过 unset 命令进程删除(在C shell中是 unsetenv)。
在Bourne shell和它的后代中(例如bash和Korn shell),以下语法可用于为执行单个程序添加环境列表值,而不影响(parent)shell(以及随后的命令):

$NAME=value program

如果需要,可以在程序名称(上面的program)之前添加多个值,使用空格隔开。

env 命令使用修改过的shell环境列表的拷贝来运行一个程序。可以对这份拷贝中的环境变量进行添加和移除。更多详情请看 evn(1) 手册页。

printenv 用于显示当前环境列表,以下是一个例子:
在这里插入图片描述

任何进程的环境列表都可以通过Linux特有的 /proc/PID/environ 文件来查看,每个NAME=value对都以null字节结尾。

Accessing the environment from a program

在C程序中,环境列表可以通过使用全局变量 char **environ 来访问(C运行时的启动代码定义了这个变量,并且将环境列表的地址赋给它)。像argv一样,environ指向了以NULL为结尾的指针列表,每个指针指向以NULL结尾的字符串。Figure 6-5展示了环境列表的数据结构:
在这里插入图片描述

Listing 6-3中的程序通过访问 environ 来列出程序环境列表中的所有的值。这个程序产生与printenv命令相同的输出。

// Listing 6-3:Displaying the process environment
// proc/display_env.c
#include "tlpi_hdr.h"
extern char **environ;
int main(int argc, char *argv[])
{
	char **ep;
	for (ep = environ; *ep != NULL; ep++)
		puts(*ep);
	exit(EXIT_SUCCESS);
}

访问环境列表的另一种方法是在main()函数中声明第三个参数:

int main(int argc, char *argv[], char *envp[])

这个参数可以被当作environ使用,不过它的作用域(scope)只限于main()函数。尽管这个功能在UNIX系统实现中被广泛使用,但我们应该避免使用它。因为除了作用域的限制外,它并没有在SUSv3中指定。

getenv() 函数用于获取进程的单个环境变量。

#include <stdlib.h>
//Retruns poniter to (value) string, or NULL if no such variable
char *getenv(const char *name);

给定一个环境变量的名称,getenv()返回指向相应值得指针。因此在我们之前的例子中,如果传入的name参数是SHELL,那么就会返回/bin/bash。如果指定name的环境变量不存在,那么getenv()返回NULL。
注意,在使用getenv()时,需要考虑到可移植性的问题:

  • SUSv3规定应用中不能修改getenv()返回的字符串。这是因为这个字符串是真实的环境列表的一部分。如果我们需要改变环境变量的值,那么我们可以使用setenv()或putenv()函数。
  • SUSv3允许getenv()的实现使用静态的分配过的buffer来返回结果。这个结果可以被随后的getenv()、setenv()、putenv()或unsetenv()调用覆盖。尽管getenv()的glibc实现没有按这种方式使用静态buffer,但是一个可移植的程序应该保护getenv()返回的值,在调用上面的函数之前,将这个值复制到另一个位置。

Modifying the environment

有时,为进程修改它的环境列表是有用的。其中一个原因是环境列表的改变对随后这个进程创建的子进程可见。另一种可能性是我们想为该进程将要加载到内存中的新程序设置一个环境变量。从这种意义上来说,环境列表不仅仅是 进程间(interprocess)通信 的一种形式,还是 程序间(interprogram) 通信的一种方法。(在第27章,对于这点我们会有更深的认识,我们会解释exec()函数是如何允许在同个进程中,使用新的程序来替代自己)
putenv() 函数为调用进程的环境列表添加一个新的环境变量或者修改已经存在环境变量的值。

#include <stdlib.h>
//成功时返回0,失败时返回非0
int putenv(char *string);

string参数是一个指针,指向形为name=value的字符串。在putevn()调用后,string成为环境列表的一部分。换句话说,environ中的一个元素会被设置成指向string的位置。因此,如果我们随后修改了string指向的字节,这个变化会影响到进程的环境列表。因为这个原因,string不应该是一个自动变量(也就是栈中分配的字符数组),因为一旦定义该变量的函数返回(return)后,这块内存区域可能会被覆盖。
注意,putenv()遇到错误时返回非0,而不是-1。
setenv() 函数是putenv()的一种替代方案,用于向环境列表中添加变量:

#include <stdlib.h>
//成功时返回0,失败时-1
int setenv(const char *name, const char *value, int overwrite);

setenv()函数通过为形如name=value的字符串分配一个内存缓冲,并将name和value指向的字符串 复制 到缓冲中,从而创建一个新的环境变量。请注意,我们不需要(事实上是不可以)在name的前面或者value的后面加上等号(=)。因为setenv()会为我们自动加上。
如果name已经存在,并且overwrite的值是0时,那么setenv()不会改变环境列表。如果overwrite是非0的,环境列表就会改变。
事实上,setenv() 复制 它的参数意味着:与putevn()不同,我们随后可以修改name和value指向的字符串的内容,而不会影响环境列表。也就说说,使用自动变量作为setenv()的参数不会引起任何问题。
unsetenv()函数从环境列表中删除name指定的变量。

#include <stdlib.h>
//成功时返回0,遇到错误时返回-1
int unsetenv(const char *name);

跟setenv()一样,name中不应该包含等号(=)。
setenv()和unsetenv()来源于BSD,比putevn()使用得要少。尽管没有在POSIX1或SUSv2中指定,但是在SUSv3中指定了。
有时,需要删除整个环境列表,然后重新构建它。我们可以通过给environ赋NULL值来删除环境列表:

environ = NULL;

clearenv() 库函数就是这么实现的:

#define _BSD_SOURCE    /*Or: #define _SVID_SOURCE*/
#include <stdlib.h>
//成功时返回0,错误时返回非0
int clearenv(void)

在某些环境中,使用setenv()和clearenv()会导致程序的内存泄漏。我们注意到setenv()分配了一块内存缓冲,然后成为环境列表的一部分。当我们调用clearenv()时,它没有释放这块缓冲(它不能释放是因为它不知道这个缓冲的存在)。程序如果不断调用这两个函数,会使内存泄漏稳步增加。事实上,这不大可能是一个问题,因为一般只在程序启动时会调用clearenv()一次,以便删除从父进程继承而来的环境列表(也就是程序调用exec()来启动程序)。

Example program

Listing 6-4展示了本节中所有函数的用法。首先清除环境列表,然后将所有命令行参数添加到环境列表中;接着,如果不存在GREET这个环境变量,就进行添加,否则忽略;删除名为BYE的环境变量;最后,打印当前的环境列表。

// proc/modify_env.c
#define _GNU_SOURCE /* 从<stdlib.h>中获取各种声明 */
#include <stdlib.h>
#include "tlpi_hdr.h"

extern char **environ;
int main(int argc, char *argv[])
{
	int j;
	char **ep;
	clearen(); /* 删除整个环境变量列表 */
	
	for (j=1; j < argc; j++) 
		if (putenv(argv[j] != 0))
			errExit("putenv: %s", argv[j]);
	
	if (setenv("GREET", "Hello world", 0) == -1)
		errExit("setenv");
	
	unsetenv("BYE");
	for (ep = environ; *ep != NULL; ep++)
		puts(*ep);
	exit(EXIT_SUCESS);
}

如果我们将environ赋值为NULL(正如Listing 6-4 中调用clearenv()),然后执行以下的循环就会报错,因为 *environ是无效的:

for (ep = environ; *ep != NULL; ep++) {
	puts(*ep);
}

但是,如果setenv()和putenv()发现environ是NULL,它们会创建一个新的环境列表,将environ指向它,这样上面的循环就会正确执行。

6.8 Performing a Nonlocal Goto: setjmp() and longjmp()

setjmp()longjmp() 库函数用于执行 nonlocal goto。术语 nonlocal 指代goto的位置是在当前函数 之外 的某个的位置。
像很多编程语言一样,C语言中有goto语句,goto语句有时会使程序变得难以阅读和维护,但有时会使程序简单或更快。
C语言中的goto的其中一个限制就是不能从当前函数跳到另一个函数。然而这种功能有时是很有用的。

本节接下的内容:略,请去原文查看

6.9 Summary

每个进程都有唯一的进程ID以及记录了它父进程的ID。
进程的虚拟内存逻辑上可以分成多种段(segments):文本段、初始化数据段、未初始化数据段、栈段和堆段。
栈是由一系列的帧组成,当函数被调用时就有一个新的帧添加到栈中,当函数返回时相应的帧就会被删除。每个帧都由本地变量、函数参数和单个函数调用的调用链接信息组成的。
当程序被调用时,命令行参数可以通过main()函数的argc和argv参数获得。按照惯例,argv[0]是调用程序的名称。
每个进程都从父进程继承一份环境列表,环境列表是一组name-value对。可以通过全局变量environ和各种库函数访问和修改进程的环境列表。
setjmp()和longjmp()函数提供了一种非局部(nonlocal)goto方式,可以从一个函数跳到另一个函数。为了避免编译器优化的问题,当使用这些函数时,可能需要使用volatile修饰符。非局部goto会让程序难以阅读和维护,应当尽量避免使用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值