UNIX环境高级编程(第2版)- 第1~10章

UNIX环境高级编程(第2版)- 第1~10章

http://blog.csdn.net/yourtommy/article/details/7244660

这篇博客是我看英文版原书时,翻译成中文,并测试了书中的代码。纯粹是为了加深理解和记忆。真正想学习的,还是阅读原书(资源包含中、英文版以及相关原码):

UNIX环境高级编程第二版


第一章 UNIX系统总览

1.1 引言(Introduction)

所有的操作系统都为应用程序的运行提供服务,典型的服务包括执行一个程序、打开一个文件、读取一个文件、分配一块内存区域、得到当前时间等等。

1.2 UNIX架构(Architecture)

一个操作系统的严格定义为:一个控制计算机硬件资源(hardware resource)以及为应用程序(programs)提供运行环境(environment)的软件。通常我们称这个软件(操作系统)为内核(kernel),因为它相对较小而且处于运行环境的核心(core)。

内核(kernel)的接口是一层被称为系统调用(system calls)的软件,系统调用的上层为公共函数库。应用程序可以调用库函数,也可以直接使用系统调用。shell是一个特殊的应用程序,它提供接口来运行其它的应用程序。Shell与库函数并级处于同一层(下层为kernel)。最上层为应用程序。

操作系统的广义定义为:内核(kernel)以及所有其它使得计算机有用(useful)和个性化(personality)的软件。这些软件包括系统工具(system utilities)、应用程序(applications)、各种shell、公共函数库(libraries of common functions)等等。举个例子,Linux是GNU操作系统的内核,一些人称其为GNU/Linux操作系统,但其更常用的名称就是Linux。尽管简单地称其为Linux并不是严格定义,但其是可理解的。

1.3 登录(Logging In)

当登录UNIX系统时,我们需要输入用户名和密码。用户名和密码信息存储在/etc/passwd文件里。该文本的每一条记录包含一个用户信息,记录用冒号分隔的多个域组成,包括用户名、加密后的密码、UID、GID、注释、主目录路径和登录后启动的shell程序。如今所有系统都把密码存储在另一个文件里。

Shells

当我们登录系统后,通常能看到一些系统信息的消息,接着就可以在shell程序里输入命令了。一些系统在你登录后启动一个窗口管理程序(window management program),但通常有一个shell窗口。shell是一个命令行解释器,它读取用户输入并执行命令。用户可以通过终端(terminal)或者脚本文件(shell script)来输入命令。通常使用的shell有Bourne Shell、Bourne-again Shell、C shell、Korn shell和TENEX C shell。

系统通过读取password文件(/etc/passwd)可以知道应该运行哪个shell。

1.4 文件和路径(Files and Directories)

文件系统(File System)是目录和文件的层次化排列(hierarchical arrangement)。所有的文件或目录都起始于一个被称为根(root)的目录,路径名是一个斜杠(/)。

目录(directory)是一个特殊的文件,它包含目录入口(directory entries)。逻辑上,我们可以认为目录入口包含了一个文件名(filename)和一个描述文件属性(attributes)的信息结构体(structure of information)。文件属性包括文件类型(type)-普通文件还是目录、文件大小(size)、文件的属主(owner)、文件的权限(permissions)-其他用户是否访问这个文件、以及文件的最后修改时间(last modified)。函数stat和fstat可以返加一个包含一个文件所有属性的信息结构体。

文件名(filename):目录下的名字都被称为文件名。只有两上字符不能出现在文件名里:斜杠(“/”)和空字符(null)。斜杠用于分隔路径名(pathname)中的各个文件名,而空字符表示一个路径名的结束。尽管如此,最好还是使用普通可打印(normal printing characters)的字符作为文件名,否则我们只能使用引用机制(quoting mechanism)来表示文件名,这样会比较复杂。

每当一个新的目录被创建时,两个默认的文件名会被自动创建:“.”(dot)和“..”(dot-dot),分别表示当前目录和父目录。根目录下的..与.一样,都表示根目录。

Research UNIX System和一些老的UNIX System V的文件系统只允许文件名包含14个字符。BSD扩展这个限制到255个字符。当今几乎所有的商业UNIX文件系统都支持至少255个字符的文件名。

路径名(pathname)是由斜杠分隔的一个或多个文件名。以斜杠开头的路径名称为绝对路径(absolute pathname),否则称为相对路径(relative pathname)。根目录的路径名(/)是一个特殊情况:它不含任何文件名。

工作目录(working directory):每个进程都有一个工作目录,它有时也被称为当前工作目录(current working directory)。这个目录用于解释所有的相对路径名。进程可以使用chdir函数来改变工作目录。

主目录(home directory):登录后的默认工作目录,这个目录可以在password文件里找到。

1.5 输入和输出(Input and Output)

文件描述符(file descriptors)通常是一个小的非负整型,内核用它来识别被一个特定进程访问的各个文件。无论何时打开来创建一个文件,内核都会返回一个文件描述符供我们读写文件使用。

标准输入、标准输出和标准错误(standard input, standard output and standard error):按协定所有shell都会在一个程序运行时打开这三个文件描述符,它们都默认绑定到终端。大多数shell都支持它们的重定向。

无缓冲I/O(Unbuffered I/O)由函数open、read、write、lseek和close提供。这些函数都使用文件描述符来工作。

标准I/O(Standard I/O)函数为无缓冲I/O提供缓冲接口。使用标准I/O可以不用担心选择最优缓冲区大小的问题,而且可以简化行输入的处理。比如read函数,读取指定数目的字节,而标准I/O库提供函数允许我们控制缓冲的风格(style of buffering)。最普遍的标准I/O函数是printf,它声明在<stdio.h>里,这个头文件包含了所有的标准I/O函数的声明。

1.6 程序与进程(program and process)

程序(program)是一个在硬盘(disk)上某个目录里的一个可执行(executable)文件。内核把程序读入内存然后执行,通过使用6个exec函数中的一个。

进程和进程号(process and process ID):程序的一个正在执行的实例(instance)被称为一个进程,有些操作系统用术语任务(task)来描述一个正被执行的程序。UNIX系统保证所有的进程都有一个独一无二的数字ID:进程号,它总是一个非负整型。

进程控制(process control):有三个主要的进程控制函数:fork、exec和waitpid。exec函数有6个变种,但我们通常简单统称它们为exec函数。

线程与线程号(Threads and Thread IDs):一个进程可以有多个线程,在多核系统上,多线程可以发挥并行的可能性。同一进程内的所有线程都共享同一个地址空间、文件描述符、栈(stacks)、进程相关的属性。因为它们可以访问相同的内存,所以线程在访问共享数据时需要同步控制。每个线程都有一个线程号,这个线程号在所在进程内是唯一的。其它进程内的线程号对于当前进程而言是毫无意义的。

控制线程的函数与控制进程的函数不同,因为线程在进程模型建立很久以后才加入到UNIX系统。尽管如此,线程模型和进程模型有一些复杂的交集。

1.7 错误处理(Error Handling)

当UNIX系统函数出错时,经常会返回一个负数,同时我们一般能根据一个整型数errno的值得到更多的信息。比如,函数open成功后会返回一个非负数,否则返回-1。open可能的错误大约有15种,比如文件不存在、权限问题等等。有些函数使用一些协定,而不是返回负值。比如,多数函数成功时返回一个有效指针,否则返回一个null指针。

文件<errno.h>定义了errno,以及errno各种可能的常数值。所有的常数都以字母E开头,比如EACCES表示权限问题。

POSIX和ISO C都把errno定义成一个可修改的整型左值。它既可以是一个错误号(error number),也可以是一个指向错误号的指针。早期的定义为extern int errno;,在多线程环境,errno被定义为:
extern int *__errno_location(void);
#define errno (*__errno_location())

关于errno有两条规则:一、如果没有错误发生,errno的数值不会被清零,所以我们应该只有在函数发生错误的时候才去检查这个值;二、errno的值永远都不可能是0,而且没有一个值为0的错误常数。

有两个打印错误信息的函数:
char *strerror(int errnum)(<string.h>):把错误号翻译成可读信息;
void perror(const char *msg)(<stdio.h>):根据当前errno的信打印如下格式的错误信息:msg: <错误信息>。

错误恢复(Error Recovery)

定义在<errno.h>里的错误可以分成两类:致命的(fatal)和非致命的。致命错误无法恢复,能做的最多只有在屏幕或日志里输出错误信息,然后退出。非致命错误有时可以被得到更健壮的处理。多数非致命错误一般是暂时性的,比如资源紧缺,当系统任务更少时可能就不再发生。资源相关(resource-related)的非致命错误包括EAGAIN、ENFILE、ENOBUFS、ENLCK、ENOSPC、ENOSR、EWOULDBLOCK,有时也包括ENOMEM。有时候当EBUSY表示一个共享资源正在被使用的时候,也可以被视为非致命错误。有时候当EINTR中断一个慢的系统调用时也可以视为非致命错误。

典型的资源相关的非致命错误的恢复操作是等待一段时间再进行尝试。这种技术还可以应用在别的场景,比如网络连接错误时,等待后再尝试可能可以重新建立网络连接。一些应用程序使用指数退避算法(exponential backoff algorithm),每次尝试都会等待更长一段时间。

一个错误是否能恢复,最终还是取决于应用程序的开发人员,如果有一个合理的恢复方法,我们可以避免退出程序来提高程序的健壮性。

1.8 用户身份证明(User Identification)

用户ID(User ID)作为一个数值定义在password文件里,用来为我们向系统提供身份证明。这些用户ID由系统管理员在为我们分配用户名的时候分配,而且我们不能修改这个ID。用户ID一般对每个用户都是独一无二的。

我们称用户ID为0的用户为根用户(root)或超级用户(superuser)。我们可以在password文件里看到登录名为root的一行记录。一些操作系统函数只允许超级用户访问。超级用户可以掌管整个系统。

组ID(Group ID)同样作为一个数值定义在password文件里,同样也是在系统管理员为我们分配用户名的时候分配。一般在password文件里会有多行记录来描述同一个组ID。组(Groups)通常用来把多个用户集中到一个项目或一个部门,这样就可以在同一个组里共享资源,比如文件。我们可以设置一个文件的权限,使得它只能被一个组的成员访问,而组外的用户则不访问该文件。

还有一个组文件用来把组名(group names)映射到数值化的组ID,这个文件通常为/etc/group。

用户ID和组ID之所以用数值表示,是有历史原因的。对于每个文件,文件系统都要为它存储它的属主(owner)的用户ID和组ID,如果每个ID存储为2个字节的整型,则只要4个字节就够了,但完整的ASCII码登录名和组名需要更多的磁盘空间。此外,当检查权限时,比较字符串相对于比较整型数而言花费更大。

尽管如此,用户更喜欢名字而不是数字,所以password文件维护了登录名到用户ID的映射,而组文件提供了组名到组ID的映射。函数getuid和getgid可以用来得到用户ID和组ID。

补充组ID(Supplementary Group IDs)

自从4.2BSD开始,多数的UNIX系统版本都允许用户除了在password文件里指定的组ID外,还可以同时属于其它的组。4.2BSD允许用户同时有至多16个补充组ID。这些补充组ID在登录时通过文件/etc/group得到。POSIX要求系统至少支持8个补充组ID,而多数系统支持至少16个。

1.9 信号(Signals)

信号是用来在一些条件(conditions)发生的情况下通知进程的一种技术。比如当一个进程被0除时,一个被为SIGFPE(floating-point-exception)的信号会被发送给这个进程。这个进程有三种处理该信号的方式:
1、忽略该信号。当信号表示硬件异常,比如被零除或者进程引用地址空间外部的内存时,这种做法并不推荐,因为会导致未定义的结果。
2、让默认行为发生。比如被零除的条件,默认行为是终止进程。
3、当信号发生时,提供一个可被调用的函数(称为捕获信号,catching the signal)。通过提供一个函数,我们可以知道信号何时发生,同时可以根据我们自己的意愿来处理该信号。

许多条件会产生信号。两个终端按键--中断(interrupt)键(通常是DELETE键或Control-C)和退出(quit)键(通常是Control-backslash),常被用来中断正在运行的进程。另一种产生信号的方式是调用kill函数。我们可以在某个进程里调用kill函数来给另一个进程发送信号。这样做是有限制的:我们必须是另一个进程的属主或者超级用户才能给它发送信号。

1.10 时间值(Time Values)

很早之前开始,UNIX系统维护了两个不同的时间值:
日历时间(Calender time):这个值记录了从Epoch(1970/1/1-00:00:00,UTC)开始到现在的秒数。这个时间可以用来记录文件修改的时间等等。早期系统的数据类型time_t维护这个时间值。
进程时间(Process time):也叫CPU时间,用来测量一个进程使用的中央处理器资源。进程时间用时钟周期(clock ticks)来测量,历史上一秒钟的时钟周期为50、60或者100。早期系统的数据类型clock_t维护了这个时间值。

当我们测量一个进程的执行时间时,我们可以发现UNIX系统维护了三个值:时钟时间(Clock time)、用户时钟时间(User CPU time)和系统时钟时间(System CPU time)。

时钟时间,有时也称为挂钟时间(wall clock time)。是进程用来运行的时间量,这个值取决于其它正在运行的进程的数量。当在报告时钟时间的时候,我们都是指在没有其它活动进程的时候测量的结果。

用户CPU时间是用户指令(user instructions)所花去的CPU时间,而系统CPU时间是内核执行所花支的CPU时间。比如,当一个进程执行系统服务比如read或write时,该系统服务在内核里花费的时间也属于进程时间的一部分。用户CPU时间和系统CPU时间的总和经常被统称为CPU时间。

命令time可以简单地测量任何一个进程的时钟时间、用户时间和系统时间。它以一个命令作为参数:time -p grep _POSIX_SOURCE */*.h > /dev/null。可以看到输出:
real 0m0.81s
user 0m0.11s
sys 0m0.07s
输出取决于当前使用的shell。有的shell不运行/usr/bin/time,而是用别的内建函数一测量命令运行的时间。

1.11 系统调用和库函数(System Calls and Library Functions)

所有的操作系统都提供了应用程序可以调用内核服务的服务接口(service points)。UNIX系统的所有实现版本都提供一个良好定义的、数量有限的进入内核的入口点,称为系统调用(system calls)。Research UNIX系统的第7个版本提供了大给50个系统调用,4.4BSD提供了大约110个,SVR4有大约120个。Linux根据不同的版本有240到260个。FreeBSD有大约320个。

系统调用接口一直被定义在UNIX Programmre's Manual的Section 2。它们以C语言定义,但不规定它们的具体实现。这点与许多更老的操作系统不同,它们把内核入口定义为汇编语言。

每个UNIX系统调用在标准C库里都有同名函数的定义,用户进程用C语言的调用方式调用这些函数,而后这些函数会调用正确的内核服务,不管内部使用何种技术来实现这些函数。例如:函数可以接受一个或多个参数并把它们放到通用寄存器(general registers)里,然后执行一些机器指令在内核中产生一个软件中断(software interrupt)。我们可以简单地把系统调用看作是C函数。

UNIX Programmre's Manual的Section 3里定义了一个可用的通用(general-purpose)函数。虽然这些函数可能会调用一个或多个内核系统调用,但它们不是内核入口。比如:printf函数可能使用write系统调用来输出字符串,但strcpy和atoi函数不会使用系统调用。

从实现者的角度看,系统调用和库函数的区别是很重要的,但对于用户而言,这并没有那么重要--系统调用和库函数看起来都是普通的C函数。两者都为应用程序提供服务。尽管如此,我们必须意识到,我们可以根据需要替换库函数,但通常不能替换系统函数。

例如malloc函数。有很多方法可以实现内存分配和垃圾回收,没有一种技术对所有应用程序都是最优化的。分配内存的系统调用sbrk并不提供一个通用的内存管理:它让进程增加或减少指定数量字节的地址空间,这些地址空间如果管理则取决于进程。内存分配函数malloc实现了一种特殊的分配方式。如果我们不喜欢这个操作,我们可以定义我们自己的malloc函数,在实现时很可能会调用sbrk系统调用。事实上有不少软件包都使用sbrk系统调用实现他们自己的内存分配算法。

这里我们有一个很清晰的责任区分:内核里的系统调用为进程分配一大块内存,而库函数malloc在用户态(user level)管理这些内存。

另一个区分系统调用和库函数的例子是UNIX系统提供的获得当前时间和日期的接口。一些操作系统提供一个系统调用来返回时间,和另一个系统调用来返回日期。任何特殊操作,比如进行夏令时的转换,可以由内核处理,也可以是人为处理。UNIX系统提供了仅有的一个系统调用来返回从Epoch至今的秒数。这个值的任何解释,比如用本地时区把它转换成可读的时间和日期,都交给用户进程。标准C函数库提供了一些函数来处理大多数的转换,比如各种夏令时转换的算法。

应用程序既可以调用系统函数也可以调用库函数,而且许多库函数也都调用了系统函数。两类函数的另一个区别是系统调用通常提供一个最小化的接口,而库函数提供更智能的功能。比如sbrk和malloc,还有unbuffered I/O和standard I/O。

进程控制系统调用(fork、exec和wait)通常会被用户程序直接调用,然而一些函数库提供了简化普遍情况的方法,比如system库和popen库。

1.12 总结(summary)

本章对UNIX进行的简短的介绍。我们介绍了我们将会一遍又一遍看到一些基本的术语,以及一些小的编程例子。


第二章 UNIX标准与实现

2.1 引言(Introduction)

人们做了大量工作来标准化UNIX编程环境和C语言。尽管应用程序可以很好的移植到不同版本的UNIX系统,但自从上世纪80年代开始各种版本的出现和差异导致许多用户,包括美国政府,要求一个统一化的标准。

在这章我们先看下过去二十年为标准化的作出的各种努力。然后我们讨论下这些UNIX编程标准对操作系统实现的影响。在所有标准化的工作里,一个重要的部分是所有UNIX实现都必须定义的各种限制(limits),所以我们要看看这些限制以及决定这些限制的值的各种方法。

2.2 UNIX标准(UNIX Standardization)

ISO C:在1989年末,C语言的ANSI标准X3.159-1989就被通过了。这个标准同时被作为国际化标准ISO/IEC 9899:1990。ANSI全称为American National Standards Institute,ISO的美国成员。ISO的全称为Internetational Organization for Standardization。IEC全称为International Electrotechnical Commission。

C标准现在由ISO/IEC国际标准工作组维护和改进。这个工作组为ISO/IEC JTC1/SC22/WG14,简称为WG14。ISO C标准的目的是提供使C程序可以移植到各种不同的操作系统上,而不仅仅是UNIX。这个标准不仅定义了编程语言的语法和语义,同时还定义了一个标准库(standard library)[Chapter 7 of ISO1999; Plauger 1992; Appendix B of Kernighan and Ritchie 1988]。这个库非常重要,因为所有当代UNIX系统都提供了在C标准中定义的库函数。

在1999年,ISO C标准更新并作为ISO/IEC 9899:1999被采纳,大幅提升了对多进程应用程序的支持。除了在一些函数声明里加上了restrict关键字,这次更新没有影响到POSIX标准。restrict关健字通过指出一个指针所指的对象仅被该指针引用,来告诉编译器这个指针引用是可以被优化的。

和多数标准一样,从标准出台到软件修改以符合标准之间会有一个延迟。随着开发商的编译系统的升级,他们为最新版本的ISO标准加了更多的支持。

根据标准定义的头文件,ISO C库可以分为24个领域:
<assert.h>:核查程序断言。
<complex.h>:复杂算法支持。
<ctype.h>:字符类型。
<errno.h>:错误代码。
<fenv.h>:浮点环境。
<float.h>:浮点常量。
<inttypes.h>:整型格式转换。
<iso646.h>:关系运算符的代替者的宏。比如"and"、"or"等。
<limits.h>:系统实现相关的一些常量。比如ARG_MAX、PATH_MAX等。
<locale.h>:地点的类目。比如__LC_ADDRESS、__LC_TELEPHONE等。
<math.h>:数学常量。
<setjmp.h>:非局部的goto。
<signal.h>:信号。
<stdarg.h>:变量参数列表。
<stdbool.h>:布尔类型和值。
<stddef.h>:标准定义。
<stdint.h>:整数类型。
<stdio.h>:标准I/O库。
<stdlib.h>:工具函数。
<string.h>:字符串操作。
<tgmath.h>:范型数学宏。
<time.h>:时间与日期。
<wchar.h>:双字节和宽字符支持。
<wctype.h>:宽字符分类与映射支持。

POSIX.1标准除了这些头文件,也包含了其它一些头文件。稍后会列出这部分文件。

IEEE POSIX是由IEEE(Institute of Electrical can Electronic Engineers)发展的一组标准集。POSIX全称为Portable Operating System Interface。最开始它只特指IEEE标准1003.1-1988--操作系统接口--但后来扩展后从而包含了称号1003(1003 designation)的许多标准和标准草稿,包括shell和实用工具(1003.2)。

本文专门讨论1003.1操作系统接口标准,它的宗旨是促进应用程序在各种UNIX系统环境上的可移植性。这个标准规定符合POSIX标准的操作系统都必须提供的服务。多数计算机厂商都采纳了这个标准。尽管这个标准是基于UNIX操作系统的,但它并不局限于UNIX和类UNIX系统。事实上,一些厂商在供应专利操作系统的时候,在提供专利特性的同时,仍声明他们的操作系统是符合POSIX标准的。

因为1003.1标准规定了接口而非实现,所以系统调用和库函数并没有区分开来。所有在标准里定义的指令(routines)都没称为函数(functions)。

标准总是持续发展,1003.1标准也不例外。这个标准的1988版本,IEEE标准1003.1-1998,被修改并提交给了IOS。没有添加新的接口或特性,但文字被修订了。修订好的文档作为IEEE标准1003.1-1990[IEEE 1990]发布,同时也作为国际化标准ISO/IEC 9945-1990。这个标准被普遍称为POSIX.1。

IEEE1003.1工作组仍在持续修改这个标准。在1993年,IEEE1003.1标准的一个修订版本发布,包含了1003.1-1990标准和1003.1b-1993实时扩展(real-time extensions)标准。在1996年,这个标准再次更新为ISO/IEC 9945-1:1996。它包含了多线程编程接口,被称为pthread,表示POSIX线程。随着IEEE标准1003.1d-1999的发布,更多的实时(real-time)接口在1999年被加入进来。一年后,IEEE标准1003.1j-2000发布了,包含更为丰富的real-time接口。同时IEEE标准1003.1q-2000也发布了,加入了事件跟踪扩展(event-tracing extensions)。

1003.1的2001版本与之前的版本有些不同,它集成了1003.1的一些修改、1003.2标准以及部分的Single UNIX Specification(SUS)的版本2(之后还有更新的版本)。这种集合后的标准,便是IEEE标准1003.1-2001,包含了以下其它的标准:
1、ISO/IEC 9945-1(IEEE标准1003.1-1996),包含IEEE标准1003.1-1990、IEEE标准1003.1b-1993(real-time的扩展)、IEEE标准1003.1c-1995(pthreads)和IEEE标准1003.1i-1995(real-time技术勘误);
2、IEEE P1003.1a标准草案(系统接口修订);
3、IEEE标准1003.1d-1999(高级real-time扩展);
4、IEEE标准1003.1q-2000(跟踪-tracing);
5、IEEE标准1003.2d-1994(批处理扩展);
6、IEEE P1003.2标准草案(补充工具);
7、部分IEEE标准1003.1g-2000(协议无关接口--protocol-independent interfaces);
8、ISO/IEC 9945-2(IEEE标准1003.2-1993);
9、Single UNIX Specification版本2的基本规范,包括系统接口定义(Issue 5)、命令和工具(Issue 5)、系统接口和头文件(Issue5);
10、开放组技术标准(Open Group Technical Standard),网络服务(Issue 5.2);
11、ISO/IEC 9899:1999,编程语言-C;

POSIX标准规定的必须包含的头文件有:
<dirent.h>:目录入口;
<fcntl.h>:文件控制;
<fnmatch.h>:文件名匹配;
<glob.h>:路径名模式匹配;
<grp.h>:组文件;
<netdb.h>:网络数据库操作;
<pwd.h>:密码文件;
<regex.h>:正则表达式;
<tar.h>:tar文件;
<termios.h>:终端I/O;
<unistd.h>:符号(symbolic)常量;
<utime.h>:文件时间;
<wordexp.h>:字扩展(word-expansion);
<arpa/inet.h>:Internet定义;
<net/if.h>:套接字接口;
<netinet/in.h>:Internet地址族;
<netinet/tcp.h>:TCP(Transmission Control Protocol)定义;
<sys/mman.h>:内存管理声明;
<sys/select.h>:select函数;
<sys/socket.h>:套接字接口;
<sys/stat.h>:文件状态;
<sys/times.h>:进程时间;
<sys/types.h>:原始系统数据类型;
<sys/un.h>:UNIX域套接字定义;
<sys/utsname.h>:系统名称;
<sys/wait.h>:进程控制。

POSIX标准中XSI扩展的头文件有:
<cpio.h>:cpio文件;
<dlfcn.h>:动态链接;
<fmtmsg.h>:消息显示结构;
<ftw.h>:文件树遍历;
<iconv.h>:代码组(code set)转换工具;
<langinfo.h>:语言信息常量;
<libgen.h>:模式匹配函数的定义;
<monetary.h>:货币类型;
<ndbm.h>:数据库操作;
<nl_types.h>:消息类目;
<poll.h>:投票函数;
<search.h>:查找表;
<strings.h>:字符串操作;
<syslog.h>:系统错误日志;
<ucontext.h>:用户上下文;
<ulimit.h>:用户限制;
<utmpx.h>:用户帐号数据库;
<sys/ipc.h>:IPC;
<sys/msg.h>:消息队列;
<sys/resource.h>:资源操作;
<sys/sem.h>:信号量;
<sys/shm.h>:共享内存;
<sys/statvfs.h>:文件系统信息;
<sys/time.h>:时间类型;
<sys/timeb.h>:补充日期与时间定义;
<sys/uio.h>:矢量I/O操作。

POSIX标准定义的可选头文件有:
<aio.h>:异步I/O;
<mqueue.h>:消息队列;
<pthread.h>:线程;
<sched.h>:执行调度;
<semaphore.h>:信号量;
<spawn.h>:实时量产接口;
<stropts.h>:XSI STREAMS接口;
<trace.h>:事件追踪。

因为POSIX.1包含了ISO C标准,所以之前列出的ISO C中的头文件一样也包含在POSIX.1中。POSIX.1的接口分为必需的和可选两部分,可选接口根据功能能进一步分成50个小项,这些项包括一些以下编程接口(每个接口都有一个选项代码--option code。选项代码是二个或三个字母组成的缩写,表示一个功能领域--functional area):
ADV、_POSIX_ADVISORY_INFO:咨询信息(real-time);
AIO、_POSIX_ASYNCHRONOUS_IO:异步输入输出(real-time);
BAR、_POSIX_BARRIERS:栅栏(real-time);
CPT、_POSIX_CPUTIME:进程CPU时间周期(real-time);
CS、_POSIX_CLOCK_SELECTION:时间选项(real-time);
CX、无:ISO C标准的扩展;
FSC、_POSIX_FSYNC:文件同步;
IP6、_POSIX_IPV6:IPv6接口;
MF、_POSIX_MAPPED_FILES:内存映射文件;
ML、_POSIX_MEMLOCK:进程内存锁(real-time);
MLR、_POSIX_MEMLOCK_RANGE:内存范围锁(real-time);
MON、_POSIX_MONOTONIC_CLOCK:单调锁(real-time);
MPR、_POSIX_MEMORY_PROTECTION:内存保护;
MSG、_POSIX_MESSAGE_PASSING:消息传递(real-time);
MX、无:IEC 60559浮点选项;
PIO、_POSIX_PRIORITIZED_IO:优先化输入输出;
PS、_POSIX_PRIORITIZED_SCHEDULING:进程调度(real-time);
RS、_POSIX_RAW_SOCKETS:裸套接字;
RTS、_POSIX_REALTIME_SIGNALS:real-time信号扩展;
SEM、_POSIX_SEMAPHORES:信号量(real-time);
SHM、_POSIX_SHARED_MEMORY_OBJECTS:共享内存对象(real-time);
SIO、_POSIX_SYNCHRONIZED_IO:同步输入输出(real-time);
SPI、_POSIX_SPIN_LOCKS:旋转锁(real-time);
SPN、_POSIX_SPAWN:量产(real-time);
SS、_POSIX_SPORADIC_SERVER:进程间断服务器(real-time);
TCT、_POSIX_THREAD_CPUTIME:线程CPU时间周期(real-time);
TEF、_POSIX_TRACE_EVENT_FILTER:追踪事件过滤器;
THR、_POSIX_THREADS:线程;
TMO、_POSIX_TIMEOUTS:timeout(real-time);
TMR、_POSIX_TIMERS:计时器(real-time);
TPI、_POSIX_THREAD_PRIO_INHERIT:线程优先级继承(real-time);
TPP、_POSIX_THREAD_PRIO_PROTECT:线程优先级保护(real-time);
TPS、_POSIX_THREAD_PRIORITY_SCHEDULING:线程执行调度(real-time);
TRC、_POSIX_TRACE:跟踪;
TRI、_POSIX_TRACE_INHERIT:跟踪继承;
TRL、_POSIX_TRACE_LOG:跟踪日志;
TSA、_POSIX_THREAD_ATTR_STACKADDR:线程栈地址属性;
TSF、_POSIX_THREAD_SAFE_FUNCTIONS:线程安全函数;
TSH、_POSIX_THREAD_PROCESS_SHARED:进程共享的线程同步;
TSP、_POSIX_THREAD_SPORADIC_SERVER:线程间断服务器(real-time);
TSS、_POSIX_THREAD_ATTR_STACKSIZE:线程栈地址大小;
TYM、_POSIX_TYPED_MEMORY_OBJECTS:分类内存对象(real-time);
XSI、_XOPEN_UNIX:X/Open扩展接口;
XSR、_XOPEN_STREAMS:XSI STREAMS。

POSIX.1并没有包含“超级用户”的概念。取而代之的是:有些操作需要“恰当的权限(appropriate privileges)”,POSIX把这个术语的定义交给具体的实现。遵守"Department of Defense”准则的UNIX系统有很多层安全等级。本文使用传统的术语以及谈论那些需要超级用户权限的操作。

经过近二十年的努力,标准已经变得成熟和稳定。POSIX.1标准现在被一个称为Austim Group的开放工作组维护。为了保证这些标准仍然有价值,它们需要不时地更新或重申。

单一UNIX规范(The Single UNIX Specification、SUS)是POSIX.1标准的超集,定义了一些额外的接口来扩展基本POSIX.1规范提供的功能。这个完整的系统接口集称为X/Open系统接口(X/Open System Interface,XSI)。_XOPEN_UNIX符号常量用来表示在XSI中而不在POSIX.1里定义的接口。

XSI同时来定义了遵守XSI标准的系统实现必须支持、而在POSIX.1描述为可选接口的那部分内容。这些内容包括文件同步、内存映射文件、内存保护、线程接口以及上面提到的标记为“强制SUS”的那些。只有遵守XSI标准的实现才能被称为UNIX系统。

The Open Group拥有UNIX的商标并且使用单一UNIX规范定义UNIX系统实现必须支持的接口。实现必须列出遵守的规范、通过测试来验证确实遵守了这些规范,然后获得许可来使用UNIX商标。

在XSI中定义的额外接口,有些是必须支持的,而另一些是可选的。这些接口根据功能可划分为几组:
加密(Encryption):由常量_XOPEN_CRYPT表示;
实时(real-time):由常量_XOPEN_REALTIME表示;
高级的实时(Advanced real-time);
实时线程(real-time threads):由常量_XOPEN_REALTIME_THREADS表示;
高级实时线程(Advanced real-time threads);
追踪(Tracing);
XSI STREAMS:由常量_XOPEN_STREAMS表示;
合法性:由常量_XOPEN_LEGACY表示;

单一UNIX规范(SUS)由The Open Group发布,该组织在1996年成立,由X/OPEN和Open Software Foundation(OSF)合并而成,两者同属工会(industry consortia)。X/Open常发布X/Open移植指导,该指导采纳专门的标准并填补缺失功能的空白。指导的目标是通过公布的标准,增加应用程序的可移植的可能性。

SUS的第一个版本由X/OPEN在1994年颁布。它也被熟知为“Spec 1170”,因为它有大约1170个接口。它是在Common Open Software Enviroment(COSE)的倡导下成长起来的,COSE的宗旨是进一步地提高应用程序在所有UNIX操作系统实现上的可移植性。COSE成员有:SUN、IBM、HP、Novell/USL和OSF。它不仅仅认可标准,还研究那些可能被普通商业应用使用的接口。这1170个接口便是从这些程序中选出的,而且还包含了X/Open Common Application Environment(CAE), Issue 4(根据它历史上的前身--X/Open Portability Guide,被熟知为“XPG4”)、系统V接口定义(System V Interface Definition,SVID),Edition 3, Level 1接口,还有OSF应用环境规范(Application Environment Specification,AES)Full Use接口。

SUS的第二个版本由The Open Group在1997年颁布。这个新版本加入了对线程、实时接口、64位处理、超大文件和增强双字节字符处理的支持。

SUS的第三个版本(简称SUSv3)由The Open Group在2001年颁布,SUSv3的基本标准与IEEE标准1003.1-2001一样而且被分为4个部分:基本定义、系统接口、Shell和Utilities、还有基本原理(rationale)。SUSv3还包含了X/Open Curses Issue 4, Version 2,但这个规范不是POSIX.1的一部分。

在2002年,ISO通过了这个版本作为国际标准ISO/IEC 9945:2002。The Open Group在2003年再次更新了1003.1标准,包含了技术修正,并由ISO通过作为国际标准ISO/IEC 9945:2003。在2004年4月,The Open Group颁布了单一UNIX规范的第三个版本,2004版。它主要是包含了更多的技术修正。

FIPS全称为Federal Information Processing Standard,联邦信息处理标准。它由美国政府公布,他们用它来进行电脑系统采购。FIPS151-1(1989年4月)基于IEEE标准1003.1-1998和ANSI C标准草案。接着FIPS 151-2(1993年5月)出台,基于IEEE标准1003.1-1990。FIPS 152-2要求一些POSIX.1中描述为可选的特性。所有这些可选部分作为必须支持的特性加入到POSIX.1-2001。

POSIX.1 FIPS导致任何想要卖遵守POSIX.1的计算机系统给美国政府的厂商都必须支持一些POSIX.1中的可选特性。POSIX.1 FIPS已经被撤回,所以本文不再讨论。

2.3 UNIX系统实现(UNIX System Implementations)

ISO C、IEEE POSIX和单一UNIX规范这三个标准由独立的机构创建。标准仅仅一个接口定义,那怎么把这些标准带到真实的世界呢?厂商接受这些标准并把它们转换成真实的实现。本文我们讨论标准也会讨论它们的实现。

McKusick et al. [1996]的1.1部分给出了UNIX系统族谱的详细历史和漂亮的图。所有的一切始于在PDP-11上的UNIX Time-Sharing系统的第六版(1976)和第七版(1979),通常被称为Version 6和Version 7。这些是在贝尔实验室之外广泛使用的第一个发行版。共有三个发展的分支:
1、AT&T的System III和System IV,被称为UNIX系统的商业版本;
2、伯克利的加利福尼亚大学的4.xBSD;
3、UNIX系统的实验版本,在AT&T贝尔实验室的计算机科学研究中心开发,发展成为之后的UNIX Time-Sharing系统第8版、第9版,终于1990年的第10版。

UNIX System V Release 4 (SVR4)是AT&T的UNIX系统实验室(UNIX System Laboratories,USL,前身为AT&T's UNIX Software Operation)。SVR4整合了AT&A UNIX System V Release 3.2(SVR3.2)、Sun Microsystems的SunOS操作系统、加利福尼亚大学的4.3BSD和Microsoft的Xenix系统的功能。(Xneix由Version 7发展而来,有许多功能都被System V整合)。SVR4的源代码在1989年末被公开,在1990年有第一个用户拷贝。SVR4同时遵守了PSOIX.1标准和X/Open Portability Guid, Issue 3 (XPG3)。

AT&T也发布了系统V接口定义(SVID) [AT&T 1989]。SVID的Issue 3定义了UNIX系统V Release 4的实现必须具有的功能。和POSIX.1一样,SVID描述了一个接口,而不是实现。SVID没有把系统调用和库函数区分开。必须查看真正实现的引用手册才能区分两者。

4.4BSD

伯克利软件产品(Berkeley Software Distribution,BSD)由伯克利的加利福尼亚大学的计算机系统研究组(Computer System Research Group,CSRG)生产与发行。4.2BSD在1983年发行而4.3BSD在1986年发行。两者都在VAX迷你计算机上运行。下一个版本4.3BSD Tahoe在1988年发行,同样也在一台名为Tahoe的特殊迷你计算机上运行。(Leffler等人1989年箸的书详述了4.3BSD Tahoe发行版)。接下来是1990年的4.3BSD Reno,支持了很多POSIX.1的特性。

最原始的BSD系统包含了AT&T的专利源代码,受到AT&T专利保护。你必须从AT&T拿到UNIX源码许可才可以得到BSD系统的源码。之后多年来事情发生了变化,由于很多加入Berkeley系统的新特性都是由非AT&T源继承,越来越多的AT&T源码由非AT&T源码替代。

在1989年,伯克利标识出在4.3BSD中的许多非AT&T源码,并且把它们作为BSD网络软件第一个发行版(BSD Networking Software, Release 1.0)公开。接着有1991年的第2版,继承自4.3BSD Reno版本。这样做的目的是基本上让4.4BSD系统不受AT&T许可的约束,使得任何人都能得到源码。

4.4BSD-Lite是CSRG的最终版本。由于与USL的合法战,它的出现延期了。在合法性的分歧被解决后,4.4BSD-Lite马上在1994年发行,完全没有阻碍,不需要任何UNIX源码许可就可以得到它。CSRG在1995年发行一个bug修正版。这个版本为4.4BSD-Lite, Release 2,是CSRG的最终的BSD版本。(McKusick等在1996年箸的书讲述了这个版本的BSD)。

这个由Berkeley开发的UNIX系统最开始运行在PDP-11上,接着移到VAX迷你计算机上,然后移到所谓的工作站上。在上世纪90年代,Berkeley为流行的基于80386的个人电脑提供支持,从而产生了386BSD。这是由Bill Jolitz完成的并且在1991年由Dobb博士的月刊上发表文档。它的许多代码都出现在BSD网络软件第2版中。

FreeBSD是基于4.4BSD-Lite的操作系统。在CSRG决定终止BSD的工作,而386BSD似乎被忽略太久的情况下,为了保持BSD的生产线,FreeBSD项目成立。

所有由FreeBSD生产的软件都免费公开binary和源码。FreeBSD5.2.1操作系统是本文要测试的四个系统中的一个。

Linux是一个提供丰富的UNIX编程环境的操作系统,而且在GNU公开许可下是免费的。Linux的流行是计算机工业的奇迹。Linux因为经常成为第一个支持新的硬件的操作系统而扬名。

Linux在1991年由Linux Torvalds创建,作为MINIX的替代品。草根努力迅速加入,世界各地的许多开发者都自愿奉献他们的时间来使用和增强它。

Linux的Mandrake 9.2版本是本文用来测试例子的系统之一。这个版本使用Linux操作系统内核的2.4.22版本。

Mac OS X基于与之前版本完全不同的技术。它的核心的操作系统被称为“Darwin”,基于Mach内核和FreeBSD的结合体。Darwin作为一个开源项目被管理,和FreeBSD与Linux一样。

Mac OS X 10.3版本(Darwin 7.4.0)是本文用于测试的另一个系统。

Solaris是由Sun Microsystems开发的UNIX系统版本。它基于系统V版本4,并经过了Sun Microsystems的工程师十多年的改善。它是唯一一个成功的商业化的SVR4的后裔,并且正式认证为UNIX系统。

Solaris 9 UNIX系统也是本文测试用的系统之一。

其它已经被认证的UNIX系统的版本有:
AIX:IBM的UNIX系统版本;
HP-UX:惠普(Hewlett-Packard)版本;
IRIX:由Silicon Graphics发行的UNIX系统版本;
UnixWare:SVR4的后裔,目前由SCO发售。

2.4 标准与实现的关系(Relationship of Standards and Implementations)

我们之前提过的标准定义了任何真实系统的子集。本文的重点在4个真实的系统:FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3和Solaris 9。尽管只有Solaris能真正称为UNIX系统,但四者都提供了UNIX编程环境。因为这四个系统在不同程度上都遵守POSIX,我们也只讨论POSIX.1标准规定的必需的特性,而不讨论那些POSIX与真正实现之间的不同之处。那些仅在某一特定实现上才有的特性和程序都会被指出。由于SUSv3是POSIX.1的超集,我们也会标出那些属于SUSv3而不属于POSIX.1的特性。

注意这些实现提供了对早期版本比如SVR3.2和4.3BSD的向后兼容性。比如Solaris同时支持POSIX.1的非阻塞I/O(O_NONBLOCK)和传统的系统V方法(O_NDELAY)。在本文中,我们只使用POSIX.1的特性,尽管我们会谈及它代替的非标准特性。类似地,SVR3.2和4.3BSD提供了与POSIX.1不同的可靠信号,我们只讨论POSIX.1的信号机制。

2.5 限量(Limits)

各种实现定义了很多魔数和常量。其中有许多使用临时特定的技术(ad hoc technique)被硬编码在程序里。随着各种标准化的努力,有更多的可移植的方法可以用来决定这些魔数和由实现定义的限量,极大地提高了我们软件的可移植性。

总共需要两种类型的限量:
1、编译期限量(比如short型的最大值);
2、运行期限量(比如文件名的最大字符数);

编译期限量可以被定义在头文件里,任何程序都可以在编译期引入,但是运行期限量需要进程调用函数来获得限量的值。

此外,有一些限量在一些实现上是固定的,它们能静态地定义在一个头文件里,然而在其它实现上是变化的,从而需要一个运行期的函数调用。这种类型的限量的例子有文件名的最大字符数。在SVR4前,历史上系统V只允许一个文件名里有14个字符,而BSD的后裔把这个数值增大到了255。大多数UHIX系统实现最近都支持多个文件系统类型,而且每种类型都有它自己的限量。这种情况下,一个文件名的运行期限量取决于它的文件系统。比如在root文件系统可以有14个字符的限量,而在另一个系统可以有255个字符的限量。

为了解决这样的问题,有三种类型的限量被提供:
1、编译期限量(头文件);
2、与文件和目录无关的运行期限量(sysconf函数);
3、与文件和目录有关的运行期限量(pathconf和fpathconf函数)。

如果一个特定的运行期限量在一个系统实现上是不变的,则它可以在头文件里静态定义。如果不定义在头文件里,则应用程序必须调用三个conf函数中的一个来得到运行期的值。

ISO C限量

所有在ISO C里定义的限量都是编译期限量。下表给出定义在<limits.h>的C标准里的限量。这些常量一直都定义在这个头文件里,而不会在系统实现上有变化。第三列显示了ISO C标准最小的可接受的值,考虑到一个使用反码计算(one's-complement arithmetic)的16位整型的系统。第四列显示了使用补码计算(two's-complement arithmetic)的32位Linux系统上的值。注意没有一个无符号数据类型有最小值,因为对于无符号数据类型来说这些值必须为0。在64位系统上,long型值的最大值与long long型的最大值相同。

在<limits.h>里定义的整型值的大小
名称
描述
可接受的最小值
典型值
CHAR_BITchar里的位数88
CHAR_MAXchar的最大值稍后讨论127
CHAR_MINchar的最小值稍后讨论-128
SCHAR_MAXsigned char的最大值127127
SCHAR_MINsigned char的最小值-127-128
UCHAR_MAXunsigned char的最大值255255
INT_MAXint的最大值32,7672,147,483,647
INT_MINint的最小值-32,767-2,147,483,648
UINT_MAXunsigned int的最大值65,5354,294,967,295
SHRT_MINshort的最小值-32,767-32,768
SHRT_MAXshort的最大值32,76732,767
USHRT_MAXunsigned short的最大值65,53565,535
LONG_MAXlong的最大值2,147,438,6472,147,483,647
LOGN_MINlong的最小值-2,147,483,647-2,147,483,648
ULONG_MAXunsigned long的最大值4,294,967,2954,294,967,295
LLONG_MAXlong long的最大值9,223,372,036,854,775,8079,223,372,036,854,775,807
LLONG_MINlong long的最小值-9,223,372,036,854,775,807-9,223,372,036,854,775,808
ULLONG_MAXunsigned long long的最大值18,446,744,073,709,551,61518,446,744,073,709,551,615
MB_LEN_MAX多字节字符常量的最大字节数116
有一个我们将会碰到的不同之处,是系统是提供有符号字符还是无符号字符。从第4列可以看到这个特定的系统使用有符号字符。我们看到CHAR_MIN与SCHAR_MIN相等,而CHAR_MAX与SHAR_MAX相等。如果系统使用无符号字符,我们就会有CHAR_MIN等于0而CHAR_MAX与UCHAR_MAX相同。

浮点数据类型定义在<float.h>,它也有着一组类似的定义。任何人要执行重要的浮点工作时都要检查这个文件。

另一个我们将碰到的ISO C常量是FOPEN_MAX,它表示一个系统实现可以保证的同时打开的标准I/O流的最小数量。这个值定义在<stdio.h>头文件里,而且它的值为8。POSIX.1的STREAM_MAX值,如果它定义了的话,必须与FOPEN_MAX的值一样。

ISO C在<stdio.h>里还定义了一个常量TMP_MAX。它是tmpnam函数可以产生的独一无二的文件名的最大数量。

下表我们给出本文讨论的四个系统上FOPEN_MAX和TMP_MAX的值:

限量FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
FOPEN_MAX20162020
TMP_MAX308,915,776238,328308,915,77617,576

ISO C还定义了常量FILENAME_MAX,但是我们应避免使用它,因为由于历史原因有些操作系统把它设得太小而不适合使用。

POSIX限量

POSIX.1定义了许多处理操作系统实现的限量的常量。不幸的是,这是一个更令人困惑特性。尽量POSIX.1定义了许多限量和常量,但我们仅仅关心影响基本POSIX.1接口的那些。这些限量和常量被分为五个各类:
1、不变的最小值:下表中的19个常量;
2、不变的值:SSIZE_MAX;
3、运行期可增长的值:CHARCLASS_NAME_MAX、COLL_WEIGHTS_MAX、LINE_MAX、NGROUPS_MAX、RE_DUP_MAX;
4、运行期可变化的值,可能不定:ARG_MAX、CHILD_MAX、HOST_NAME_MAX、LOGIN_NAME_MAX、OPEN_MAX、PAGESIZE、RE_DUP_MAX、STREAM_MAX、SYMLOOP_MAX、TTY_NAME_MAX和TZNAME_MAX;
5、路径名变量的值,可能不定:FILESIZEBITS、LINK_MAX、MAX_CANON、MAX_INPUT、NAME_MAX、PATH_MAX、PIPE_BUF和SYMLINK_MAX。

在<limits.h>里定义的POSIX.1的不变最小值
名称描述:...的可接受的最小值
_POSIX_ARG_MAXexec函数参数的长度4,096
_POSIX_CHILD_MAX每个真实用户ID的子进程数25
_POSIX_HOST_NAME_MAXgethostname返回的主机名的最大长度255
_POSIX_LINK_MAX一个文件的链接数8
_POSIX_LOGIN_NAME_MAX登录名的最大长度9
_POSIX_MAX_CANON终端最简洁的(canonical)输入队列的字节数255
_POSIX_MAX_INPUT终端输入队列的可用空格255
_POSIX_NAME_MAX文件名的字节数,不包括终止字符null14
_POSIX_NGROUPS_MAX一个进程的同步的补充组ID的数量8
_POSIX_OPEN_MAX一个进程打开的文件数20
_POSIX_PATH_MAX路径名的字节数,包括终止符null256
_POSIX_PIP_BUF可被原子写入管道的字节数512
_POSIX_RE_DUP_MAX当使用间隔标记“\{m,n\}”时,被regexec和regcomp函数认可的基本正则表达式的重复出现次数255
_POSIX_SSIZE_MAX可以存入ssize_t对象的值32,767
_POSIX_STREAM_MAX一个进程能同时打开的标准I/O流的数量8
_POSIX_SYMLINK_MAX符号链接的字节数255
_POSIX_SYMLOOP_MAX路径名解析时可以转换的符号链接数8
_POSIX_TTY_NAME_MAX终端设备名的长度,包括终止符null9
_POSIX_TZNAME_MAX时区名的字节数6

在这44个限量和常量中,有些可能定义在<limits.h>里,而其它的可能有也可能没有定义,取决于特定的条件。当我们讨论sysconf、pathconf和fpathconf函数时会再讨论这些可能有也可能没有定义的限量和常量。

19个不变最小值定义在上表中。这些值不会因为系统不同而有所区别。它们为那些特性定义了最具有约束性的值。遵守POSIX.1的系统实现必须提供至少那么大的值,这也是为什么它们被称为最小值的原因,尽管它们名字里都含有“MAX”。还有,为了确保可移植性,一个严格遵守标准的应用程序一定不能要求一个更大的值。我们在本文会讨论所有这些常量。

一个严格遵守POSIX标准(srictly-conforming POSIX)的应用程序与一个仅遵守POSIX标准(merely POSIX conforming)的应用程序不同。一个严格遵守的应用程序只使用IEEE标准1003.1-2001的接口。一个严格遵守的应用程序是一个POSIX程序,而且不信赖于任何无定义的行为、不使用任何逐步废弃的接口、以及不使用比上表中的最小值更大的值。

不幸的是,不变最小值里有一些太小了,而不适合实际使用。比如,多数UNIX系统为每个进程打开远大于20个的文件。同样,_POSIX_PATH_MAX的255的限制也太小,路径名会超过这个限制。这意味着我们不能使用_POSIX_OPEN_MAX和_POSIX_PATH_MAX作为编译期的数组大小。

上表中的19个不变最小值中,每一个都有一个相应实现,名字为去掉_POSIX_前缀的常量名。没有_POSIX前绷的名字说明这是个真实系统实现上支持的值。这19个实现值是早先我们列出的第2~5项:不变值、运行期可增长的值、运行期不变值和路径名变量值。问题是这19个实现值并不保证一定定义在<limits.h>头文件里。

比如,一个特定的值可能没有包含在这个头文件里,如果它的进程的真实值依赖于系统里的内存总量。如果值没有在头文件里定义,我们不能在编译期里把它作为数组边界。如以, POSIX.1决定为我们提供三个运行期函数:sysconf、pathconf和fpathconf,用来得到在运行时的真实实现值。尽管如此,仍然有个问题,POSIX.1定义的值里面有些被定义为“可能不确定的”,即理论上是“无限”的。这意味着这个值没有实际的上限。比如,在Linux上,可以被readv和writev使用的iovec结构的数量只限于系统的内存总量。为此,IOV_MAX在Linux被认为是“不确定”的,我们在讲述后面的运行期限量时再来讨论这个问题。

XSI 限量

XSI同样定义了处理实现限量的常数,包括:
1、不变最小量:下表中的10个常量;
2、数字限量:LONG_BIT和WORD_BIT;
3、运行期不变值,可能不确定:ATEXIT_MAX、IOV_MAX和PAGE_SIZE。

<limits.h>里定义的XSI不变最小值
名称
描述
最小可接受值
典型值
NL_ARGMAXprintf和scanf调用数字的最大值99
NL_LANGMAXLANG环境变量的最大字节数1414
NL_MSGMAX最大消息数量32,76732,767
NL_NMAX多对一映射字符的最大字节数未定义1
NL_SETMAX最大集合数量255255
NL_TEXTMAX消息字符串的最大字节数_POSIX2_LINE_MAX2,048
NZERO默认的进程优先级2020
_XOPEN_IOV_MAXreadv和writev使用的iovec结构的最大数量1616
_XOPEN_NAME_MAX文件名的最大字节数255255
_XOPEN_PATH_MAX路径名的最大字节数1,0241,024
上表中有许多值是用来处理消息类别。最后两个值证明了POSIX.1的最小值太小了,很可能只允许嵌入式的POSIX.1系统,所以单一UNIX规范在XSI标准里加入更大的最小值。

函数synconf、pathconf和fpathconf

我们已经列出一个系统实现必须支持的各种最小值,但是我们怎么知道一个特定系统真正支持了那些限量呢?如我们早先所述,这些限量中有些可以在编译期可用,而一些只有在运行期得到。我们也提到过有些限量在某个系统上是固定的,而其它可能是变化的,因为它们与文件或目录相关。运行期限量可以通过调用以下三个函数来得到:
#include <unistd.h>
long sysconf(int name);
long pathconf(const char* pathname, int name);
long fpathconf(int filedes, int name);

这三个函数成功都返回一个相应的值,而失败则返回-1。

最后两个函数的区别在于一个接受路径名作为参数,而另一个接受文件描述符作为参数。

下表列出了sysconf得到系统限量所用的名字参数,都以_SC_开头:

sysconf的名字参数以及相应的限量
限量名
描述
名字参数
AGR_MAXexec函数参数的最大长度,以字节为单位_SC_ARG_MAX
ATEXIT_MAX可以被atexit函数注册的函数的最大数量_SC_ATEXIT_MAX
CHILD_MAX一个真实用户ID可以拥有的最大进程数_SC_CHILD_MAX
clock ticks/second每秒钟的时钟周期_SC_CLK_TCK
COLL_WEIGHTS_MAX在本地定义文件里可以赋值给LC_COLLATE命令关键字的重量的最大数量_SC_SOLL_WEIGHTS_MAX
HOST_NAME_MAXgethostname返回的主机名的最大长度_SC_HOST_NAME_MAX
IOV_MAXreadv和writev使用的iovec结构的最大数量_SC_IOV_MAX
LINE_MAX实用工具的输入行的最大长度_SC_LINE_MAX
LOGIN_NAME_MAX登录名的最大长度_SC_LOGIN_NAME_MAX
NGROUPS_MAX每个进程的同步补充组ID数_SC_NGROUPS_MAX
OPEN_MAX一个进程打开文件的最大数量_SC_OPEN_MAX
PAGESIZE系统内存页的字节数_SC_PAGESIZE
PAGE_SIZE系统内存页的字节数_SC_PAGE_SIZE
RE_DUP_MAXregexec和regcomp函数使用间隔符\{m,n\}可以识别的基本表达式的重复次复_SC_RE_DUP_MAX
STREAM_MAX在任何时候一个进程的标准流的最大数量;如果有定义,这个值必须与FOPEN_MAX一样_SC_STREAM_MAX
SYMLOOP_MAX路径名解析时可以处理的符号链接数_SC_SYMLOOP_MAX
TTY_NAME_MAX终端设备名的长度,包括终止符null_SC_TTY_NAME_MAX
TZNAME_MAX时区名的最大字节数_SC_TZNAME_MAX

下表列出了pathconf和fpathconf函数使用的名字参数,都以_PC_开头:

pathconf和fpathconf的名字参数以及相应限量
限量名描述名字参数
FILESIZEBITS为了表示在指定目录下允许的普通文件大小的最大值,所使用的有符号整型值所需的最小比特位数_PC_FILESIZEBITS
LINK_MAX一个文件链接数的最大值_PC_LINK_MAX
MAX_CONON一个终端最简洁的输入队列的最大字节数_PC_MAX_CONON
MAX_INPUT一个终端的输入队列可用空间的字节数_PC_MAX_INPUT
NAME_MAX文件名的最大字节数,不包括终止符null_PC_NAME_MAX
PATH_MAX相对路径名的最大字节数,包括终止符null_PC_PATH_MAX
PIPE_BUF可以原子写入管道的最大字节数_PC_PIP_BUF
SYMLINK_MAX一个符号链接的字节数_PC_SYMLINK_MAX

下面再深入讨论下这三个函数的不同的返回值:
1、如果名字参数不正确,这三个函数都会返回-1并设置errno值为EINVAL。
2、一些名字参数可以返回一个变量的值,或者表明这个值是不确定的。通过返回-1但不改变erron的值来表示一个不确定的值;
3、_SC_CLK_TC的返回值是每秒钟的时钟周期,用来与times函数的返回值共同使用。

在传递路径名给pathconf和域参数给fpathconf时有些约束。如果这些约束中任意一个没有满足,则结果无定义:
1、_PC_MAX_CONON和_PC_MAX_INPUT的对应的文件并须是一个终端文件;
2、_PC_LINK_MAX的对应的文件既可以是文件也可以是目录。如果相应文件是一个目录,则返回值对应于目录本身,而不是目录下的文件;
3、_PC_FILESIZEBITS和_PC_NAME_MAX的相应文件必须是目录。返回值对应于这个目录下的文件;
4、_PC_PATH_MAX相应的文件必须是目录。当这个目录是工作目录时,返回值是相对路径名的最大长度。(不幸的是,这不是我们想知道的绝对路径的真实长度,稍后再来讨论这个问题。)
5、_PC_PIPE_BUF的相应文件必须是个管道,FIFO或者目录。前两种情况下返回值为相应管道或FIFO的限量。最后一种情况的返回值为指定目录下创建的任一FIFO的限量。
6、_PC_SYMLINK_MAX的相应文件必须是一个目录。返回值为目录里的符号链接可以包含的字符串的最大长度。

不确定的运行期限量(Indeterminate Runtime Limits)

我们之前提到了有运行期变量可以是不确定的。问题在于如果这些限量没有在<limits.h>里定义,那我们就不能在编译期使用它们。而且如果它们的值是不确定的,那么可能在运行期也没有定义!我们来看下两个特殊的情况:为路径名分配内存,以及决定文件描述符的数量。

路径名(Pathname)

许多程序需要为路径名分配内存。典型地,内存在编译期分配,而一些魔数(没有一个是正确的值)被用来作为数组尺寸:256、512、1024或都标准I/O常量BUFSIZ。定义在<sys/parm.h>里的4.3BSD常量MAXPATHLEN才是正确的值,但许多程序都不用它。

POSIX.1试图用PATH_MAX来解决这个问题,但如果这个值是不确定的,那我们仍然不走运。本文使用下面这个函数来动态地为路径名分配内存:

#include <errno.h>
#include <limits.h>
#include <unistd.h>

#ifdef PATH_MAX
static int pathmax = PATH_MAX;
#else
static int pathmax = 0;
#endif

#define SUSV3 200112L

static long posix_version = 0;

/* If MATH_MAX is indeterminate, no guarantee this is adquate */
#define PATH_MAX_GUESS 1024

void err_sys(const char* msg);

char *
path_alloc(int *sizep) /* also return allocated size, if nonnull */
{
    char *ptr;
    int size;
    
    if (posix_version == 0)
        posix_version = sysconf(_SC_VERSION);

    if (pathmax == 0) { /*first time through */
        errno = 0;
        if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) {
            if (errno == 0)
                pathmax = PATH_MAX_GUESS; /* it's indeterminate */
            else
                err_sys("pathconf error for _PC_PATH_MAX");
        } else {
            pathmax++; /* add one since it's relative to root */
        }
    }
    if (posix_version < SUSV3)
        size = pathmax + 1;
    else
        size = pathmax;

    if ((ptr = malloc(size)) == NULL)
        printf("malloc error for pathname");

    if (sizep != NULL)
        *sizep = size;
    return(ptr);
}

void err_sys(const char* msg) {
    printf("%s\n", msg);
    exit(1);
}
如果PATH_MAX在<limits.h>里定义了,那就使用它,不然就调用pathconf。如果第一个参数是工作路径,那么pathconf的返回值是一个相对路径名的最大尺寸,所以我们把根目录作为第一个参数,而后在结果上加1。如果pathconf指出PATH_MAX是不确定的,那我们只有猜测一个值了。

SUSv3之前的标准并没有清楚说明PATH_MAX最末尾是否包括一个null的字节。如果操作系统实现信赖于这些早期的标准,则我们需要加一个字节的内存,这样会更安全些。

处理不确定结果情况的正确方法取决于分配的空间怎样被使用。如果分配的内存是为了函数调用getcwd--比如:返回当前工作目录的绝对路径名--而且如果分配的空间太小的话,那errno会返回一个错误ERANGE。我们可以接着调用realloc来增加这块分配的空间并再次尝试。我们可以持续这样做,直到getcwd成功为止。

打开文件的最大数量(Maximum Number of Open Files)

一个后台进程(daemon process)--运行在后台,不与任何终端关联的进程--的一个普遍代码流程是关闭所有打开的文件。一些程序用以下代码,设想常量NOFILE定义在<sys/param.h>头文件里:
#include <sys/param.h>
for (i = 0; i < NOFILE; i++)
    close(i);

其它一些程序使用一些版本的<stdio.h>提供的常量_NFILE作为上限。有些把它硬编码为20.

我们本希望用POSIX.1的值OPEN_MAX来决定这个值,但如果这个值是不确定的,我们仍然会有问题。如果我们把代码写成下面的模样,而同时OPEN_MAX是不确定的,那会造成死循环,因为sysconf会返回-1:
#include <unistd.h>
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
    close(i);

我们最好的解决方案是仅仅关闭所有不超过一个任意上限(比如256)的描述符。和我们的pathname例子一样,这个并不保证可以在所有情况下都工作,但这是我们能做的最好的事情。看如下代码:

#include <errno.h>
#include <limits.h>
#include <unistd.h>

#ifdef OPEN_MAX
static long openmax = OPEN_MAX;
#else
static long openmax = 0;
#endif

/*
 * If OPEN_MAX is indeterminate, we're not
 * guaranteed that this is adequate.
 */
#define OPEN_MAX_GUESS 256

void err_sys(const char* msg);

long
open_max(void)
{
    if (openmax == 0) { /* first time through */
        errno = 0;
        if ((openmax = sysconf(_SC_OPEN_MAX)) < 0) {
            if (errno == 0)
                openmax = OPEN_MAX_GUESS; /* it's indeterminate */
            else
                err_sys("sysconf error for _SC_OPEN_MAX");
        }
    }
    return(openmax);
}
我们可能想尝试调用close函数,直到得到一个返回的错误,但是这个从close(EBADF)返回的错误并不能区分它是一个无效的文件描述符,还是这个文件描述符并没有打开。如果我们尝试使用这种技术,但描述符9并没有打开而描述符10被打开,我们可以终止在9而不能关闭10。如果OPEN_MAX溢出的话,dup函数确实返回一个特定的错误,但只能重复这个描述符两百多次才能得到这个值。

一些系统实现会返回LONG_MAX当作限值,来有效的表示没有限制。Linux的ATEXIT_MAX就是其一。这不是个好方法,因为它可能会导致程序很坏的行为。

比如,我们可以用Bourne-again shell的内置ulimit命令来改变我们进程可以同时打开的文件数的最大值。如果这个限制要设置成无限制的话,通常需要一个特殊(超级用户)的权限。但一旦它被设置为无限的话,sysconf会返回LONG_MAX作为OPEN_MAX的值。程序信赖于这个值作为关闭的文件描述符的上限,而尝试关闭2,147,483,647个文件描述符是非常浪费时间的,而它们之中大多数都没有被使用。

支持UNIX单一规范里的XSI扩展的系统会提供getrlimit函数。它能用来返回一个进程能打开的描述符的最大数量。利用这个函数,我们可以确保我们的进程没有打开超过预设的上限数的文件,从而避免上述问题。

OPEN_MAX被POSIX称为运行期不变量,意味着它的值不能在一个进程的生命周期内改变。但在支持XSI扩展的系统上我们可以调用setrlimit函数在为一个运行着的进程改变这个值。(这个值也可以在C shell上用limit命令改变,或在Bourne、Bourne-again和Knon shell上用ulimit函数。)如果我们的系统支持这个功能,我们可以改变上述代码:每次调用open_max函数时,都调用sysconf,而不仅仅在第一次调用时。

2.6 可选项(Options)

我们之前看过了一个POSIX.1的可选项列表,而且讨论过XSI的可选项组。如果我们要写需要选项被支持的特性的可移植的程序,我们需要一个可移植的方法来决定某个系统实现是否支持该选项。

和前面提到的限量一样,单一UNIX规范定义了三种方法来实现:
1、编译期选项,定义在<unistd.h>;
2、运行期选项,由sysconf函数得到,与特定文件或目录无关;
3、运行期选项,通过pathconf或fpathconf函数得到,与特定文件或目录有关。

可选项包含了前面在“POSIX可选接口列表”的选项,还包含了下面两个表列出的符号:

可选项以及对应sysconf的参数名
可选项名描述参数名
_POSIX_JOB_CONTROL指出实现是否支持job控制_SC_JOB_CONTROL
_POSIX_READER_WRITER_LOCKS指出实现是否支持读写锁_SC_READER_WRITER_LOCKS
_POSIX_SAVED_IDS指出实现是否支持保存的set-user-ID和保存的set-gropu-ID_SC_SAVED_IDS
_POSIX_SHELL指出实现是否支持POSIX shell_SC_SHELL
_POSIX_VERSION指出POSIX.1的版本_SC_VERSION
_XOPEN_CRYPT指出实现是否支持XSI加密可选项组_SC_XOPEN_CRYPT
_XOPEN_LEGACY指出实现是否支持XSI遗留可选项组_SC_XOPEN_LEGACY
_XOPEN_REALTIME指出实现是否支持XSI实时可选项组_SC_XOPEN_REALTIME
_XOPEN_REALTIME_THREADS指出实现是否支持XSI实时线程可选项组_SC_XOPEN_REALTIME_THREADS
_XOPEN_VERSION指出XSI的版本_SC_XOPN_VERSION

可选项以及对应的pathconf和fpathconf参数
可选项名
描述
参数名
_POSIX_CHOWN_RESTRICTED指出chown的使用是否有限制_PC_CHOWN_RESTRICTED
_POSIX_NO_TRUNC指定路径名比NAME_MAX长时是否报错_PC_NO_TRUNC
_POSIX_VDISABLE如果定义了,终端特殊字符可能用这个值禁掉_PC_VDISABLE
_POSIX_ASYNC_IO指出异步I/O是否可以与相关文件使用_PC_ASYNC_IO
_POSIX_PRIO_IO指出优先化I/O是否可以与相关文件使用_PC_PRIO_IO
_POSIX_SYNC_IO指出同步I/O是否可以与相关文件使用_PC_SYNC_IO

如果符号常量没有定义,我们必须使用syncconf、pathconf或fpathconf来确定这个可选项是否被支持。在这种情况下,函数的命名参数由替换符号名前的_POSIX为_SC或_PC而成。对于那些以_XOPEN开头的符号,命名参数必须在其之前添加_SC或_PC前缀,而非替换它。比如,如果常量_POSIX_THREADS没有定义,那我们以_SC_THREADS为参数调用sysconf来知道系统是否支持POSIX线程选项。如果常量_XOPEN_UNIX没有定义,我们可以用命名参数_SC_XOPEN_UNIX命名参数调用sysconf来知道系统是否支持XSI扩展。

如果符号常量有定义,则有三种可能性:
1、定义的值为-1,表示系统不支持该选项;
2、定义的值大于0,表示该选项被支持;
3、定义的值等于0,则我们必须调用sysconf、pathconf或fpathconf来决定这个值是否被支持。

和系统限量一样,根据sysconf、pathconf和fpathconf如何对待可选项,有几点需要注意的地方:
1、_SC_VERSION的返回值表明标准的4个数字的年份和2个数字的月份。这个值可以是198808L、199009L、199506L或一些其它可新版本标准的值。与SUS好v3相关的值为201112L。
2、_SC_XOPEN_VERSION的返回值表明系统编译的XSI版本。对应的SUSv3的值为600;
3、_SC_JOB_CONTROL、SC_SAVED_IDS和_PC_VDISABLE不再是可选的特性。从SUSv3开始,这些特性是必需的,尽管为了向后兼容这些符号仍被保留;
4、_PC_CHOWN_RESTRICTED和_PC_NO_TRUNC返回-1,但不改变errno的值,如果指定的路径名或域不支持该选项;
5、_PC_CHOWN_RESTRICTED相关的文件必须是文件或目录。如果是目录,返回值表明选项是否应用在该目录下的所有文件;
6、_PC_NO_TRUNC的相关文件必须是一个目录。返回值应用于该目录下的所有文件;
7、_PC_VDISABLE相关文件必须是一个终端文件。

在下表我们展示几个配置可选项以及它们在样例系统上的对应值。注意有些系统没有跟上SUS的最新版本。比如Mac OS X10.3支持POSIX线程,但把_POSIX_THREADS定义为:
#define _POSIX_THREADS
而没有任何的值。根据SUSv3,符号如果定义的话,其值必须为-1、0或200112。
 

配置可选项的例子
限量FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
UFS文件系统PCFS文件系统
_POSIX_CHOWN_RESTRICTED11111
_POSIX_JOB_CONTROL11111
_POSIX_NO_TRUNC1111不支持
_POSIX_SAVED_IDS不支持1不支持11
_POSIX_THREADS200112200112有定义11
_POSIX_VDISABLE255025500
_POSIX_VERSION200112200112198808199506199506
_XOPEN_UNIX不支持1无定义11
_XOPEN_VERSION不支持500无定义33

标记为“无定义”的项表示这个特性没有定义,也就是说,系统没有定义这个符号常量,也没有相应的_PC或_SC参数名。相反,“有定义”的项表示符号常量被定义,但没有值,比如之前提到的_POSIX_THREADS。一个“不支持”的项表明系统定义了这个符号常量,但它的值为-1,或者它的值为0但对应的sysconf和pathconf调用的返回值为-1。

注意在Solaris上的PCFS文件系统上,_PC_NO_TRUNC的pathconf调用返回-1。PCFS文件系统支持(软盘的)DOS格式,而根据DOS文件系统的需求,DOS文件名都静默地转换为8.3的格式。

2.7 特性测试宏(Feature Test Macros)

正如我们之前所述,在头文件里定义了许多POSIX.1和XSI的符号。但多数系统实现在POSIX.1和XSI定义之外,还可加入它们自己的定义。如果我们想编译一个程序,使它只依赖于POSIX定义而不使用任何系统实现定义的限量,我们需要定义常量_POSIX_C_SOURCE。所有的POSIX.1的头文件都使用这个常量来排除所有实现定义。

POSIX.1标准的前一个版本定义了_POSIX_SOURCE常量。它在POSIX.1的2001版本被_POSIX_C_SOURCE取代而被废弃。

常量_POSIX_C_SOURCE和_XOPEN_SOURCE被称作特性测试宏。所有的特性测试宏都以下划线开头。当被使用时,它们被典型地定义在cc命令里,比如:
cc -D_POSIX_C_SOURCE=200112 file.c。

这样会在任何头文件被导入C程序之前定义特性测试宏。如果我们想要只使用POSIX.1的定义,我们可以在源文件的第一行写上:
#define _POSIX_C_SOURCE 200112

为了在程序里使用SUSv3的功能,我们必须定义常量_XOPEN_SOURCE为600。这与定义_POSIX_C_SOURCE为200112L引进POSIX.1的功能是一样的效果。

SUS定义了c99工具作为C编译环境的接口。使用它我们可以这样编译一个文件:
c99 -D_X_OPEN_SOURCE=600 file.c -o file

为了在gcc编译器启用1999 ISO C扩展,我们使用-std=99选项,比如:
gcc -D_XOPEN_SOURCE=600 -std=c99 file.c -o file

另一个特性测试宏是__STDC__,它被C编译器自动定义如果它遵守ISO C标准。这样允许我们编写在ISO C编译器和非ISO C都能编译的程序。例如,为了在编译器支持的情况下使用ISO C的原型特性,我们可以这样定义头文件:
#ifdef __STDC__
void *myfunc(const char *, int);
#else
void *myfunc();
#endif

尽管今天大多数C编译器都支持ISO标准,但__STDC__特性测试宏可以在许多头文件里找到。

2.8 原始系统数据类型(Primitive System Data Types)

历史上,特定的C数据类型与选定的UNIX系统变量相关联。比如,主设备号和从设备号被存储在一个16位的整型值里,其中8位表示主设备号,而另8位表示从设备号。然而更大的系统需要比256更大的值来表示设备号,所以需要不同的技术。(事实上,Solaris使用32位表示设备号:14位表示主设备而18位表示从设备。)

头文件<sys/types.h>定义了一些实现相关的数据类型,被称为原始系统数据类型。更多的数据类型也定义在了其它头文件里。这些数据类型使用C的typedef定义在头文件里。大多数以_t结尾。下表列出了许多本文本将会碰到的原始数据类型:

一些通用的原始系统数据类型
类型
描述
caddr_t核心地止(core address)
clock_t时钟周期计数
comp_t压缩的时候周期
dev_t设备号(主设备和从设备)
fd_set文件描述符集合
fpos_t文件位置
gid_t数字的组ID
ino_ti-node数
mode_t文件类型,文件创建模式
nlink_t目录项的链接数
off_t文件大小与偏移量(有符号的)
pid_t进程ID和进程组ID(有符号的)
ptrdiff_t两个指针的差值(有符号的)
rlim_t资源限制
sig_atomic_t可以原子访问的数据类型
sigset_t信号集
size_t对象(比如字符串)的大小(无符号的)
ssize_t函数的返回字节数(有符号的)
time_t日历时间的秒数
uid_t数字的用户ID
wchar_t表示所有不同的字符编码

通过用这种方式定义数据类型, 我们不在程序中包含根据系统不同而有所区别的实现细节。我们在本文后面会描述所有这些数据类型的用途。

2.9 标准之间的冲突(Conflicts Between Standards)

总的来说,这些不同的标准彼此都很好的相处。既然SUSv3是POSIX.1的超集,我们主要关心的ISO C和POSIX.1之间是任何差异。它们确实有些差异。

ISO C定义了函数clock来返回进程使用的CPU时间的量。这个返回值是clock_t值。为了把它转换成秒,我们用它除以CLOCKS_PER_SEC,它定义在<time.h>头文件里。POSIX.1定义了函数times来返回CPU时间(给调用者和它的terminated children)和时钟时间。所有这些时间值都是clock_t值。sysconf函数用来得到每秒的时钟tick数,与times函数的返回值一起使用。我们有相同的术语:每秒钟的tick数,但在ISO C和POSIX.1有不同的定义。两个标准都使用相同的数据类型(clock_t)来表示这些不同的值。这个差异可以在Solaris里看到,clock返回微秒(导致CLOCK_PER_SEC值为一百万),而sysconf返回值100作为每秒的时钟tick数。

当ISO C标准规定一个函数但不像POSIX.1那样强硬地规定时,会有潜在的冲突。这种情况函数会要求在POSIX环境(在多进程环境下)和ISO C环境下(基本不设想当前的操作系统)有不同的实现。signal函数就是一个例子。如果我们不明就里地使用由Solaris提供的signal函数(期望写出可以运行在ISO C环境和更老的UNIX系统上的代码),它将提供与POSIX.1的sigaction函数不同的语义。后面在谈到signal时我们会更深入地讨论。

2.10 总结(summary)

近二十年来UNIX编程环境的标准化发生了很多事情。我们已经描述了占统治地位的标准--ISO C、POSIX和单一UNIX规范--和它们在4个实现上的影响:FreeBSD、Linux、Mac OS X和Solaris。这些标准试图定义一些可以在各个实现上改变的参数,但是我们已经看到这些限制并不完美。在本文我们会碰到很多这些限量和魔数。


第三章 文件I/O(File I/O)

3.1引言(Introduction)

我们从用来进行文件 I/O操作--打开文件、读文件、写文件等等--来开始我们关于UNIX系统的讨论。在UNIX系统上的大多数文件I/O都可以用仅仅5个函数实现:open、read、write、lseek和close。我们接着会检查下各种不同的缓冲区大小对read和write函数的影响。

这章描述的函数经常被视为未缓冲的I/O(Unbuffered I/O),与标准I/O函数相对。术语“未缓冲的”意思是每个read和write函数都会在内核进行系统调用。这些未缓冲的I/O函数不是ISO C的一部分,而是POSIX.1和SUS的一部分。

不管什么时候我们描述多个进程间的资源共享,原子操作的概念都变得很重要。我们根据文件I/O和传给open函数的参数来检查这个概念。这引发了关于文件如何在多个进程以及相关内核数据结构之间共享的讨论。在介绍完那么特性后,我们再讨论下dup、fcntl、sync、fsync和ioctl函数。

3.2 文件描述符(File Descriptor)

对内核来说,所有打开的文件都能用文件描述符来引用。一个文件描述符是一个非负整数。当我们打开一个已经存在的文件或创建一个新的文件,内核会返回一个文件描述符给进程。当我们想读或者写一个文件时,我们用open或create返回的文件描述符来标识这个文件,作为read或write的参数。

根据协定,UNIX系统外壳把文件描述符0与一个进程的标准输入关联,文件描述符1与标准输出关联,而文件描述符2与标准错误关联。这个协定被各个外壳以及很多应用程序使用。它不是UNIX内核的特性。尽管如此,如果这些相联没有的话,许多程序都会崩溃。

在POSIX程序里,魔数0、1和2应该用符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO代替。这些常量在<unistd.h>时定义。

文件描述符的取值范围从0到OPEN_MAX。早期的UNIX系统实现有19的上限,允许每个进程最多打开20个文件,但是许多系统把这个限量增加为63。

在FreeBSD 5.2.1、Mac OS X 10.3和Solaris 9,这个限量本质上是无限的,由系统内存量、整型大小和任何由系统管理员配置的软硬限量来界定。Linux 2.4.22对每个进程 的文件描述符数量给了一个硬性限量1,048,576。

3.3 open函数(open Function)

通过调用open函数可以打开或创建一个文件:
#include <fcntl.h>
int open(const char *pathname, int oflga, .../* mode_t mode */ );
如果成功返回文件描述符,错误返回-1。

第三个参数为...,是ISO C表示剩余参数的数量和类型可变的方法。对于这个函数来说,第三个参数仅当创建一个文件时使用,我们稍后讨论。我们在函数原型中把这个参数作为一个注释。

pathname是打开或创建的文件的名字,这个函数有一个选项群,由参数oflag描述。这个参数由一个或多个以下定义在<fcntl.h>里的常量的通过位或组合而成:
O_RDONLY:只读方式打开;
O_WRONLY:只写方式打开;
O_RDWR:读写方式打开。

为了与早期的程序兼容,多数实现把O_RDONLY定义为0、O_WRONLY定义为1、而O_RDWR定义为2。

这三个常量必须有且只能有一个被指定。以下的常量是可选的:
O_APPEND:每次写时添加到文件末尾;
O_CREAT:创建这个文件如果它不存在。这个选项需要为open函数提供第三个参数--打开模式--来指定这个新建文件的访问权限位
O_EXCL:如果O_CREAT也同时被指定而文件已存在的话会产生一个错误 。这个文件是否存在的测试以及当文件不存在时文件的创建是一个原子操作。
O_TRUNC:如果文件存在而且它以只写或读写方式被成功打开的话,把它的长度截为0;
O_NOCTTY:如果路径名指向一个终端设备,则不要为这个进程把这个设备作为控制终端分配;
O_NONBLOCK:如果文件名指向一个FIFO,一个块特殊文件或一个字符特殊文件,这个选项为打开文件及后续I/O设置非阻塞模式。

在系统V的早期版本,有个O_NDELAY(no delay)标志。这个选项和O_NONBLOCK(nonblocking)选项类似,但是会为一个读操作的返回值产生歧义。如果没有数据可从一个管道、FIFO或设备读入时,no-delay选项导致一个读操作返回0,这与碰到文件结束时返回0产生冲突。基于SVR4的系统仍然支持带有早期语义的no-delay选项,但是新的程序应该使用nonblocking选项。

下面三个标志同样是可选的。它们是SUS(也是POSIX.1)的同步输入输出选项:
O_DSYNC:让每个write都等待物理I/O的完成,但如果不影响读取已写入的数据,则不等待文件属性的更新;
O_RSYNC:让每个在文件描述符上的read操作都等待,直到任何正在执行的在文件的相同部分上的写操作都结束为止;
O_SYNC:让每个write都等待物理I/O的完成,包括write引起的文件属性更新的修改。

O_DSYNC和O_SYNC标志相似,但稍微有些不同。O_DSYNC标志仅当文件属性的更新反应了文件数据的改变时才会影响文件的属性(例如,更新文件大小来反映更多的数据。)对于O_SYNC标志,数据和属性一直都同步地更新。当使用O_DSYNC选项覆写一个文件已经存在的部分时,文件的时间不会同步更新。相反,如果我们以O_SYNC标志打开这个文件,每个write都会在write返回前更新这个文件的时间,而不管我们在覆写存在的字节还是添加到这个文件。

Solaris 9支持这三个标志。FreeBSD 5.2.1和Mac OS X 10.3有另一个与O_SYNC做相同事情的标志(O_FSYNC)。因为这两个标志是一样的,所以FreeBSD 5.2.1把它们定义成相同的值(但是很奇怪的是,Mac OS 10.3没有定义O_SYNC)。FreeBSD 5.2.1和Mac OS X 10.3没有支持O_DSYNC和O_RSYNC标志。Linux 2.4.22把这两个标志等同视为O_SYNC。

open返回的文件描述符被确保为可用描述符的最小值。一些应用程序利用这个事实在标准输入、标准输出或标准错误上打开一个新的文件。例如,一个程序可能关闭标准输出--通常情况下是文件描述符1--然后打开另一个文件,并知道它将作为描述符1被打开。我们将会看到让文件在给定描述符上打开一个文件的更好方法:dup2函数。

文件名和路径名截断(Filename and Pathname Truncation)

如果NAME_MAX是14而我们尝试在当前目录创建一个文件名包含15个字符的新文件会怎么样?System V的早期版本,比如SVR2,允许这种事发生,同时静默地把14位后的字符截断。基于BSD的系统返回一个错误状态,把errno设为ENAMETOOLONG。静默截断文件名会引起不只简单地影响文件创建的问题。如果NAME_MAX是14而有一个存在的名字为刚好14个字符的文件,任何接受路径参数的函数比如open或stat都无法知道文件原始的名字,因为原始名可能已经被截断了。

POSIX.1规定,常量_POSIX_NO_TRUNC决定是裁断长文件名和长路径名还是返回一个错误。是否返回错误是一个存在很久的问题。比如,基于SVR4的系统不会为传统的System V文件系统(S5)产生错误。然而,基于SVR4的系统却不会为BSD风格文件系统(UFS)产生错误。

另一个例子,Solaris为UFS返回错误,但不为PCFS(DOS兼容文件系统)返回错误,因为DOS静默地截断不符合8.3格式的文件名。

基于BSD的系统和Linux总是会返回错误。

如果_POSIX_NO_TRUNC起效,当整个文件名超过PATH_MAX或路径名中的任何文件名超过NAME_MAX,errno会被设为ENAMETOOLONG,而且一个错误状态会被返回。

3.4 creat函数(creat function)

一个新文件也可以通过调用creat函数来创建:
#include <fcntl.h>
int creat(cont char *pathname, mode_t mode);
成功则返回打开的只写文件描述符,错误返回-1

注意这个函数等同于:
open (pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);

历史上早期的UNIX版本,open的第二个参数只能是0、1或2。当时不可能用open函数来打开一个不存在的文件。因此,一个独立的系统调用--creat,被用来创建新的文件。现在O_CREAT和O_TRUNC选项可以在open里使用,独立的creat已经不再需要了。

在4.5节当我们深入讨论文件访问权限时再来说明如何指定mode。

creat的一个缺点是打开的文件只能用来写。在open的新版本出现之前,如果我们想打开一个临时文件写入然后再读回,我们必须调用creat,close,然后再是open。一个更好的方式是使用open函数:
open (pathname, O_RDWR | O_CREAT | O_TRUNC, mode);

3.5 close函数(close function)

一个打开的文件可以通过调用close函数来关闭:
#include <unistd.h>
int close(int filedes);
成功返回0,错误返回-1。

关闭一个文件会同时释放进程在这个文件上可能会有的任何记录锁(record locks)。我们在14.3节来讨论它。

当一个进程终止时,它所有的打开文件都被内核自动关闭。许多程序利用这个便利而不显式地关闭打开的文件。

3.6 lseek函数

每个打开的文件都有一个相应的“当前文件偏移量(current file offset)”,一般是一个表示从文件开头开始的字节数量的非负整数。(我们待会来讨论下“非负”的一个例外。)读和写的操作一般从当前文件偏移量开始,而且会根据读出或写入的字节数导致偏移量的增加。当文件打开时,这个偏移量默认初始化为0,除非O_APPEND选项被指定。

一个打开的文件的偏移量可以通过调用lseek显示地设置:
#include <unistd.h>
off_t lseek(int fileds, off_t offset, int whence);
成功返回新的文件偏移,错误返回-1。

offset的解释取决于whence参数:
如果whence为SEEK_SET,那文件偏移量会设置为从文件开头之后的offset字节数的位置;
如果whence为SEEK_CUR,那文件偏移量会设置为当前的偏移量加上offset的值。offset可以是正也可以是负的;
如果whence为SEEK_END,那文件偏移量为文件的尺寸加上offset的值。offset可以是正也可以是负的。

因为一个成功的lseek调用会返回新的文件偏移量,我们可以通过从当前位置seek 0字节来决定当前的偏移量:
off_t currpos;
durrpos = lseek(fd, 0, SEEK_CUR);

这个技术同样也可以用来确定一个文件是否有seek的能力。如果文件描述符指向一个管道、FIFO或套接字,lseek把errno设置为ESPIPE并返回1。

这三个符号常量SEEK_SET、SEEK_CUR、SEEK_END在System V中被引入。在此之前,whence被指定为0(绝对的)、1(相对于当前偏移量)或2(相对于文件结尾)。许多软件仍然使用这些数字的硬编码。

lseek的字符“l”意思是“long integer”。在引入off_t数据类型前,offset参数和返回值是长整型。lseek在版本7当长整型被加入到C时引入。(版本6通过函数seek和tell提供类似的功能。)

下面的代码用来测试标准输入是否可以seek:

#include <unistd.h>

int
main(void)
{
    if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
        printf("cannot seek\n");
    else
        printf("seek OK\n");
    exit(0);
}
编译后可以用./a.out < somefile来测试标准输入(可以seek);cat < somefile | ./a.out来测试管道(不能seek);./a.cout < /var/spool/cron/FIFO来测试FIFO(不能seek)。

通常情况下,一个文件的当前偏移量必须是非负整数。尽管如此,有些设备可能允许负的偏移量。但对于普通文件,偏移量必须是非负的。因为负数偏移量是可能的,我们应该小心比较lseek的返回值,应测试它是否等于-1,而不能测试它是否小于0。

在Intel x86处理器上的FreeBSD的/dev/kmem设备运行负的偏移量。

因为偏移量(off_t)是一个有符号的数据类型,我们在最大文件尺寸里丢失了一个2的因子。如果off_t是32位整型,文件最大尺寸为2^32-1字节。

lseek只在内核里记录当前文件偏移量--它没有引起任何的I/O操作。这个偏移量在下次读或写操作时被使用。

文件的偏移量可以比文件当前尺寸更大,在这种情况下下次文件的write会扩展这个文件。这表示会在文件里创建一个空洞,而这是允许的。在文件里读任何没有写过的字节都会得到0。

文件的空洞不需要在磁盘上战用存储空间。取决于文件系统实现,当你找到超过文件末尾的位置然后写时,新的磁盘块可能会被分配用来存储数据,但没有必要为旧的文件末尾与你开始写的位置之间的数据分配磁盘空间。

下面的代码在创建了一个带空洞的文件:

#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

#define FILE_MODE   (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define err_sys(msg) {printf("%s\n", msg); exit(1);}

int
main(void)
{
    int fd;
    if ((fd = creat("file.hole", FILE_MODE)) < 0)
        err_sys("creat error");

    if (write(fd, buf1, 10) != 10)
        err_sys("buf1 write error");
    /* offset now = 10 */

    if (lseek(fd, 16384, SEEK_SET) == -1)
        err_sys("lseek error");
    /* offset now = 16384 */

    if (write(fd, buf2, 10) != 10)
        err_sys("buf2 write error");
    /* offset now = 16394 */

    exit(0);
}
首先用ls -l file.hole来查看下这个文件的属性:
-rw-r--r-- 1 tommy tommy 16394 2012-02-17 12:13 file.hole

它的大小是16394字节。再后od -c file.hole命令来查看文件的内容:
0000000   a   b   c   d   e   f   g   h   i   j  \0  \0  \0  \0  \0  \0
0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
*
0040000   A   B   C   D   E   F   G   H   I   J
0040012
可以看到没有写过的字节都被读回0。每行开头的7位数字,是表示文件偏移量的八进制数。

为了证明这个文件确实有个空洞,把它和另一个相同尺寸,没有空洞的文件进行比较:
ls -ls file file.hole file.nohole
16 -rw-r--r-- 1 tommy tommy 16394 2012-02-17 12:26 file.hole
20 -rw-r--r-- 1 tommy tommy 16394 2012-02-17 12:28 file.nohole

可以看到无洞的文件使用了20个磁盘块,而有洞的文件使用了16个块。3.8节会讨论write函数,而4.12节会深入讨论带有空洞的文件。

因为lseek使用的偏移量地址由off_t表示,所以系统实现被允许支持任何适用于它们平台的尺寸。今天多数平台提供两个操作文件偏移量的接口集:一个集合使用32位文件偏移量而另一个使用64位文件偏移量。

单一UNIX规范通过sysconf函数为应用程序提供了一种知道哪种环境被支持的方法。下表总结了定义的sysconf常量:

数据大小选项与对应在sysconf的命名参数
选项名描述命名参数
_POSIX_V6_ILP32_OFF32int、long、指针和off_t类型是32位的。_SV_V6_ILP32_OFF32
_POSIX_V6_ILP32_OFFBIGint、long和指针类型是32位的,off_t是至少64位的。_SV_V6_ILP32_OFFBIG
_POSIX_V6_LP64_OFF64int类型是32位的,long、指针和off_t类型是64位的。_SC_V6_LP64_OFF64
_POSIX_V6_LP64_OFFBIGint类型是32位的,long、指针和off_t类型是至少64位的。_SC_V6_LP64_OFFBIG

c99编译器需要我们使用getconf命令来把需要的数据大小模型映射到编译和链接我们程序时所需的标志。根据每个平台所支持的环境,可能需要不同的标志和库。

不幸的是,这是系统实现没有跟上标准的一个领域。更令人疑惑的事情是在SUS第2版和第3版之间的名字改变。

为了解决这个问题,应用程序可以通过设置_FILE_OFFSET_BITS常量的值为64来启用64位偏移。这样会把off_t的定义改变为64位有符号整型。把_FILE_OFFSET_BITS设置为32会启用32位偏移。尽管如此,要注意虽然本文讨论的四个平台都可以设置_FILE_OFFSET_BITS来同时支持32位和64位的偏移量,但这并不保证可以移植。

注意尽管你可能启用了64位偏移,你是否能创建大于2TB(2^31-1字节)的文件仍取决于底下的文件系统类型。

3.7 read函数(read Function)

可以使用read函数从打开的文件里读取数据:
#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
返回读取到的字节数,文件结尾则返回0,错误返回-1

以下几种情况会造成实际读取的字节数比请求的数量少:
1、在读一个普通文件时,如果在读入请求的字节数之前碰到了文件尾。比如,在请求读取100字节时,只剩30字节就到文件末尾了,read返回30。下次我们调用read时,它会返回0(文件末尾。)
2、当从一个终端设备读数据时。通常,一次最多只能读一行。(我们将在18章展示如何改变它。)
3、当从网络上读数据时。网络的缓冲区可能会导致读入的比请求的少。
4、当从一个管道或FIFO中读数据时。如果管道包含比请求量少的字节,read会只返回可用的字节数。
5、当从一个面向记录(record-oriented)设备读入数据时。一些面向记录的设备,比如磁带(magnetic type),一次可以最多返回一个记录。
6、当被一个信号中断,而只读了部分数据时。我们在10.5节深入讨论。

read操作从文件当前偏移量开始。在成功返回前,偏移量会增加实际读取的字节数。

POSIX.1改过几次这个函数的原型。经典的定义是:
int read(int filedes, char *buf, unsigned nbytes);

首先,第二个参数从char *变为void *来与ISO C保持一致:void *用来表示范型。
其次,返回值必须是一个有符号整型(ssize_t)来返回一个正的字节数、0(表示文件结尾)或-1(表示错误)。
最后,第三个参数在历史上曾经是一个无符号整型,这是为了允许16位系统实现可以一次读写最多65,534字节。1990 Posix.1标准把原始数据类型ssize_t引来来提供有符号的返回值,而无符号的size_t用作第三个参数。

3.8 write函数(write Function)

可能用write函数向一个打开的文件里写数据:
#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
成功返回写入的字节数,错误返回-1。

返回值一般与nbytes参数相等,不然就说明出错了。write错误的一个常见的原因可能是磁盘写满或超过了一个进程的文件大小限制。

对于一个普通文件,写操作从文件当前偏移量开始。如果当文件被打开时O_APPEND选项被指定,每次写操作前,文件的当前偏移量会设在在文件末尾。在成功写入后,文件偏移量会增加真正写入的字节数。

3.9 I/O效率(I/O Efficiency)

下面的代码只使用read和write函数复制一个文件。

#include <unistd.h>

#define BUFFSIZE 4096

void err_sys(const char* msg);

int
main(void)
{
    int n;
    char buf[BUFFSIZE];

    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
        if (write(STDOUT_FILENO, buf, n) != n)
            err_sys("write error");

    if (n < 0)
        err_sys("read error");

    exit(0);
}
1、这段代码假设程序启动前标准输出和标准输入都已经被shell设置好,并从标准输入读入,而后写出到标准输出。确实,所有一般的UNIX系统shell提供打开一个文件以从标准输入中读取和为标准输出创建或覆写一个文件的方法。这样程序就不必打开输入和输出的文件。
2、许多程序都假设标准输入的文件描述符是0而标准输出的文件描述符为1。在这个例子里,我们使用了两个在<unistd.h>定义的名字:STDIN_FILENO和STDOUT_FILENO。
3、程序必没有关闭输入或输出文件,相反,程序使用了特性:当进程终止时,UNIX内核会关闭所有打开的文件描述符。

4、这个例子对于文本文件和字节文件都可以工作,因为对于UNIX内核两者没有任何区别。

我们仍有一个没有回答的问题:我们如何来选取BUFFSIZE的值?在回答这个问题之前,让我们用不同的BUFFSIZE值来运行下这个程序。我们使用上面的程序,把标准输出重定向到/dev/null。测试使用使用4,096字节的块的Linux ext2文件系统。下表显示了用20个不同的buffer值读取一个103,316,352字节的文件的结果:

在Linux上使用不同的buffer大小读取文件的结果
BUFFSIZE
用户CUP(秒数)
系统CPU(秒数)
时钟时间(秒数)
循环数
1124.89161.65288.64103,316,352
263.1080.96145.8151,658,176
431.8440.0072.7525,829,088
815.1721.0136.8512,914,544
167.8610.2718.766,457,272
324.135.019.763,228,636
642.112.486.761,614,318
1281.011.276.82807,159
2560.560.626.80403,579
5120.270.417.03201,789
1,0240.180.237.84100,894
2,0480.050.196.8250,447
4,9060.030.166.8625,223
8,1920.010.186.6712,611
16,3840.020.186.876,305
32,7680.000.166.703,152
65,5360.020.196.921,576
131,0720.000.166.84788
262,1440.010.257.30394
524,2880.000.227.35198

可以看到当BUFFSIZE为4,096时,有一个最小的系统时间,超过这个大小的buffer没有更好的效果。

多数文件系统支持某种超前读入的方法来提高性能。当发现有一连串的读操作时,系统尝试读入比系统请求更多的数据,假设程序会在不久后读到它们。从上表中最后几行可以看到,在超过128K后ext2的超前读入就不再有效果了。

我们待会再回来讨论这个时间例子。在3.14节我们会展示同步写的效果;在5.8节我们会比较这些未缓冲I/O时间和标准I/O库。

当尝试测量读写文件的程序的性能时要注意,操作系统会尝试把文件缓存到主内存(incore,意思为main memory,过去系统主内存是用铁核--ferrite core做成的,这也是术语“core dump”的由来:一个程序的主内存映象存储到磁盘上的一个文件用来诊断),所以当你重复地测量这个程序的性能时,后面的时间会比第一次要少。这时因为第一次运行会导致文件被缓存到系统缓存中,而后续运行会从系统缓存而不是硬盘里访问文件。

在上表中的测试里,每次不同buffer尺寸的运行都是使用文件的不同拷贝,这样当前运行就不会找到上次运行的缓存数据。这些文件都已经有足够大,没有全部留在缓存中。(测试系统的配置是512M的RAM)。

3.10 文件共享(File Sharing)

UNIX系统支持多个进程共享打开的文件。在讨论dup函数前,我们需要讨论下共享。为此,我们将检查内核为所有I/O使用的数据结构。

接下来的描述是概念化的。它可能也可能不匹配于一个特定的实现。参考Bach[1986]对于System V里的这些结构的讨论。McKusick等[1996]描述了4.4BSD里的这些结构。McKusick和Neville-Neil[2005]讨论了FreeBSD 5.2。要看Solaris的类似讨论,参考Mauro和McDougall[2001]。

内核使用了三种数据结构来表示一个打开的文件,而它们之间的关系决定了一个进程在文件共享上对另一个进程的效果。

1、每个进程在进程表里都有一个项。这个项是一个打开文件描述符的表,我们可以把它看成一个向量,每个描述符对应一个项。与每个文件描述符相关的有:
    a、文件描述符标志(close-on-exec;参考3.6节和3.14节。)
    b、文件表项的一个指针。
2、内核为所有打开的文件维护了一个文件表。每个文件表项包含:
    a、文件的文件状态标志,比如读、写、添加、同步和非阻塞;3.14节会讨论更多这方面内容;
    b、当前文件偏移量;
    c、文件的v-node表项的指针。
3、每个打开文件(或设备)有一个v-node结构,它包含关于文件类型和在文件上的操作的函数指针的信息。对于大多数文件,v-node同样包含了这个文件的i-node。当文件被打开时这些信息从硬盘读入,以便所有与该文件相关的信息都可读。比如,i-node包含了文件的属主,文件的大小,文件在硬盘上的真实数据块的指针,等等。(我们在4.14节深入描述典型UNIX文件系统时讨论更多关于i-nodes。)Linux没有v-node。相反,它使用一个通用i-node结构(generic i-node structure)。尽管实现不同,v-node与通用i-node在概念上是相同的。两者都指向一个特定文件系统的i-node结构。

我们会忽略一些不影响我们讨论的实现细节。比如,打开文件描述符的表可以存储在用户区域而不是进程表里。这些表可以用许多方式实现--它们不必是数组,也可以实现为结构的链表。这些实现细节都不会影响我们关于文件共享的讨论。

这三个表的排列方式自从UNIX系统的早期版本就已经存在了,而这种排列是文件在多个进程内共享的关键。当我们在本章末讨论文件共享的更多方式时,我们再回过来讨论这个排列。

v-node被发明用来支持在一个单一的计算机系统上支持多个文件系统类型。它由Peter Weinberger(贝尔实验室)和Bill Joy(Sun Microsystems)独立发明。Sun称之为虚拟文件系统(Virtual File System),而称i-node中文件系统无关的部分为v-node[Kleiman 1986]。v-node在各种厂商实现在传播开来,比如Sun的网络文件系统(Network File System,NFS)。当NFS被加入时,Berkeley的第一个提供v-node的版本是4.3BSD Reno版本。

在SVR4,v-node代替了SVR3的文件系统无关i-node。Solaris从SVR4继承而来,因此也使用v-node。

与把数据结构分为v-node和i-node不同,Linux使用一个文件系统无关的i-node和一个文件系统有关的i-node。

如果两个独立的进程有相同的打开的文件,我们可以看到以下排列:两个进程有两个不同的进程表项,每个进程表项指向不同的文件表项,但这两个不同的文件表项指向相同的一个v-node表项。每个进程都有它自己的文件表项的原因是它们都有自己的文件当前偏移量。

有了这些数据结构,我们现在需要更深入地探讨执行我们已经讨论过的特定操作时会发生什么:
1、在每个写操作完成后,文件表项里的当前文件偏移量都根据写入的字节数增加。如果这导致当前文件偏移量超过当前文件尺寸,在i-node表里的当前文件尺寸会被设置成当前文件偏移量(比如,这个文件被扩展)。
2、如果文件用O_APPEND标志打开,一个对应的标志会设置给文件表项的文件状态标志中。每次一个写操作使用这个append标志应用到一个文件上时,在文件表项的当前文件偏移量首先设置为从i-node表项中得到的当前文件尺寸。这强迫每个write都会添加到文件的当前结束位置。
3、如果文件使用lseek定位到它当前的文件末尾,所有发生的事情只有在文件表项时的当前文件偏移量被设置为从i-node表项中得到的当前文件尺寸。(注意这与以O_APPEND标志打开文件不同,我们会在3.11节看到。)
4、lseek函数仅修改在文件表项的当前文件偏移量。没有I/O发生。

多个描述符项指向同一个文件表项是可能的,正如我们将在3.12节讨论的dup函数。这同样在一个fork操作之后发生,这时父进程和子进程为每个打开的描述符共享同一个文件表项。

注意文件描述符标志和文件状态标志的范围的区别。前者仅应用于一个进程里的一个描述符,而后者应用于任何进程里指向给定文件表项的所有描述符。当我们在3.14节讨论fcntl函数时,将会看到如何获取和修改文件描述符标志和文件状态标志。

我们在本节至今讨论过的所有的东西在多进程读取相同的文件时都能良好工作。每个进程都有自己的文件表项以及自己的当前文件偏移量。尽管如此,当多个进程写入相同的文件时,可能会得到意想不到的结果。要知道怎样避免这种意外,我们需要了解原子操作的概念。

3.11 原子操作(Atomic Operations)

添加到文件

考虑一个想添加到文件末尾的单独进程。UNIX的早期版本没有为open支持O_APPEND选项,所以程序可能会以如下方式编码:
if (lseek(fd, 0L, 2) < 0)
    err_sys("lseek error");
if (write(fd, buf, 100) != 100)
    err_sys("write error");

这在单进程上可以工作,但如果多进程使用这种技术来添加到同一文件时会产生问题。(比如,如果一个程序的多个实例在一个日志文件上添加信息时,这种情形会发生。)

假设两个独立的进程,A和B,添加到同一个文件。每个进程打开文件但不使用O_APPEND标志。每个进程都有自己的文件表项,但它们共享一个单一的v-node表项。假设A进程用lseek设置当前偏移量为1500字节(当前的文件末尾)。然后内核交换进程,B进程继续运行。B进程然后使用lseek设置当前偏移量为1500字节(当前文件末尾)。然后B调用write,使得B的当前文件偏移量增加到1600。然后内核交换进程而A继续运行。当A调用write时,数据从A的当前文件偏移量开始写,它是1500字节。这样会覆盖掉B写入文件的数据。

这里的问题是我们的逻辑操作“定位到文件末尾并写入”需要两个函数调用。解决方法是让定位到文件末尾和写入成为一个对于其它进程而言的原子操作。任何需要两个函数调用的操作都不可能是原子的,因为内核总是可能在两个函数调用之间临时挂起一个进程。

UNIS系统提供了一个原子方式来实现这种操作:当打开文件时设置O_APPEND标志。正如我们在前一节描述的一样,这会导致内核在每次write之前定位到当前文件末尾。我们不再需要在每次write前调用lseek。

pread和pwrite函数

SUS包含了允许应用程序原子化定位并执行I/O的XSI扩展。这个扩展为pread和pwrite:
#include <unistd.h>
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
返回读取的字节数,文件末尾则返回0,错误返回-1。

ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
成功返回写入的字节数,错误返回-1。

调用pread等价于调用lseek后再调用read,除了以下区别:
1、使用pread时不能中断这两个操作。
2、文件指针没有更新。

调用pwrite等价于调用lseek后再调用write,和上面有类似的区别。

创建一个文件

当描述open函数的O_CREAT和O_EXCL选项时,我们看过另一个原子操作的例子。当两个选项都被指定时,如果文件已经存在open会失败。我们也说过检查文件的存在和文件的创建是作为一个原子操作来执行的。如果我们没有这个原子操作,我们可能会尝试:
if ((fd = open(pathname, O_WRONLY)) <0) {
    if (errno == ENOENT) {
        if ((fd = creat(pathname, mode)) < 0)
            err_sys("creat error");
    }
}

如果在open和creat之间有另一个进程创建这个文件时会发生问题。如果在两个函数调用之间另一个进程创建了这个文件并写入了些数据,那这些数据会在creat执行时被擦除。把测试文件存在与创建合并成一个原子操作避免了这个问题。

一般来说,术语原子操作是指一个可能由多个步骤组成的操作。如果操作被原子执行,那要么多久的步骤都被执行,要么没有一个步骤被执行。不可能只有这些步骤的一个子集被执行。我们在4.15节讨论link函数时和在14.3节讨论记录锁时再回到这个原子操作的话题。

3.12 dup和dup2函数

一个存在的文件描述符可以由以下函数的其中一个复制:
#includde <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
两者成功都返回新的文件描述符,错误返回-1。

由dup返回的新的文件描述符被保证为可用文件描述符里的最小数字。在dup2,我们用filedes2参数指定新的描述符的值。如果filedes2已经被打开,它会首先被关掉。如果filedes与filedes2相等,那么dup2返回filedes2,但不关闭它。

函数返回的新的文件描述符与filedes参数共享同一个文件表项。

我们假设一个进程启动时,执行了newfd = dup(1); 并假设下一个可用的文件描述符为3(因为0、1、2都被shell打开,所以很有可能)。因为两个描述符指向同一个文件表项,它们共享同一个文件状态标志--read、write、append等等--还有相同的当前文件偏移量。

每个描述符有自己的文件描述符标志。正如我们将在下节描述的那样,为新描述符为设的close-on-exec文件描述符标志总是被dup函数清除掉。

另一个复制描述符的方法是使用fcntl函数,我们会在3.14节讨论。事实上,函数调用dup(filedes);与fcntl(filedes, F_DUPFD, 0);等价。类似地,函数调用dup2(filedes, filedes2);与close(filedes2); fcntl(filedes, F_DUPFD, filedes2);等价。

在最后一种情况,dup2并不与close之后的fcntl完全相同。区别在于:
1、dup2是一个原子操作,而另一个调用了两个函数。后者可能会出现在close和fcntl操作之间,有一个信号捕获(signal catcher)被调用的情况。我们在第10章讨论信号。
2、dup2和fcntl有些不同的errno。dup2系统调用起源于第7版本并传播至所有的BSD版本。复制文件描述符的fcntl方法从System III开始出现,并在System V里延续。SVR3.2挑捡了dup2函数,而4.2BSD选择了fcntl函数和F_DUPFD功能。POSIX.1同时要求dup2和fcntl的F_DUPFD功能。

3.13 sync、fsync和fdatasync函数

传统的UNIX系统实现在内核有一个多数硬盘I/O操作通过的缓冲区。当我们向一个文件写入数据时,数据通常被内核拷贝到它的其中一个缓冲区,并排队以便在之后的某个时刻写入到磁盘中。这被称为延迟写入。(Bach[1986]的第3章深入讨论了缓冲缓存。)

内核最终把所有的延迟写入块写到磁盘中,一般在它需要为其它磁盘块重用这个缓冲区时。为了确保磁盘内容和缓冲区缓存的一致性,sync、fsync和fdatasync函数被提供:
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
成功返回0,错误返回-1。
void sync(void);

sync函数简单地把所有修改过的块缓冲区为写入而排队,之后便返回。它不等待磁盘写操作的发生。

sync函数通常被一个系统daemon进程周期性地调用(通常为每30秒),这经常被称为update。这保证了内核块缓冲的一般强制输出。命令sync同样调用了sync函数。

函数fsync仅引用一个文件,由文件描述符指定,并且在返回前等待磁盘写的完成。fsync的使用是为了一个需要保证修改的块已经写入到磁盘的程序,比如数据库。

fdatasync函数与fsync相似,除了它只影响文件的数据部分。fsync会同样更新文件的属性。

本文讨论的四个平台都支持sync和fsync。但是,FreeBSD 5.2.1和Mac OS X 10.3并不支持fdatasync。

3.14 fcntl函数

fcntl函数可以改变一个已打开文件的属性:
#include <fcntl.h>
int fcntl(int fildes, int cmd, .../* int arg */);
成功返回相应的值(如下),错误返回-1。

在本节的例子里,第三个参数总是一个整型值,对应于前面函数原型的注释。但但我们在14.3节讨论记录锁时,第三个参数变为一个结构体指针。

使用fcntl函数有5种不同的目的:
1、复制一个存在的描述符(cmd=F_DUPFD)
2、得到/设置文件描述符标志(cmd=F_GETFD或F_SETFD)
3、得到/设置文件状态标志(cmd=F_GETFL或F_SETFL)
4、得到/设置异步I/O所有权(cmd=F_GEOWN或F_SETOWN)
5、得到/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)

我们将讨论这十个cmd值的前7个。(我们要等到14.3节再讨论最后三个,来处理记录锁)。我们已经说明文件描述符标志与一个进程表项的各个文件描述符相关,而文件状态标志与各个文件表项相关。

F_DUPFD:复制文件描述符filedes。返回值为新的描述符。它是可用的未打开的文件描述符里的最小值,它大于或等于传入的第三个(整型)参数。新的描述符与filedes共享同一个文件表项。但新的描述符有它自己的文件描述符标志集,而且它的FD_CLOEXEC文件符标准被清除。(这意味着文件描述符会在经历一个exec的过程后仍然保持打开,我们会在第8章讨论)
F_GETFD:返回filedes的文件描述符标志。目前,只有一个文件描述符标志被定义:FD_CLOEXEC标志。
F_SETFD:为filedes设置文件描述符标志。新的标志值通过第三个(整型)参数设置。注意一些存在的处理文件描述符标志的程序没有使用常数FD_CLOSEXEC。相反,程序设置这个标志为或者0(退出时不关闭,默认值)或者1(退出时关闭)。
F_GETFL:为filedes返回文件状态标志。我们在讨论open函数时讨论过文件状态标志。下表列出各个标志:

fcntl的文件状态标志
文件状态标志描述
O_RDONLY打开为只读
O_WRONLY打开为只写
O_RDWR打开为读写
O_APPEND每次写时添加
O_NONBLOCK非阻塞模式
O_SYNC等待写完成(数据和属性)
O_DSYNC等待写完成(仅数据)
O_RSYNC同步读和写
O_FSYNC等待写完成(仅FreeBSD和Mac OS X)
O_ASYNC异步I/O(仅FreeBSD和Mac OS X)
不幸的是,这三个访问模式标志--O_RDONLY、O_WRONLY和O_RDWR--不是可以测试的独立的位。(正如我们之前提到的,由于历史原因,这三个的值通常为0、1和2。而且,这三个值是互斥的,一个文件仅能有其中之一被启用。)因此,我们必须首先使用O_ACCMODE掩码来得到访问模式的位来然把结果跟这三个值进行比较。
F_SETFL:把文件状态标志设置为第三个(整型)参数。标志只能能为O_APPEND、O_NOBLOCK、O_SYNC、O_DSYNC、ORSYNC、O_FSYNC和O_ASYNC。
F_GETOUN:得到当前接收SIGIO和SIGURG信号的进程ID和进程组ID。我们在14.6.2节讨论这些异步I/O信号。
F_SETOWN:设置当前接收SIGIO和SIGURG信号的进程ID和进程组ID。一个正值表示一个进程ID。一个负值隐含一个值为参数绝对值的进程组ID。

下面代码:

#include <fcntl.h>
#include <stdarg.h>

void err_quit(const char* msg) { printf("%s\n", msg); exit(0); }

void err_sys(const char* fmt, ...) {
    va_list         ap;
    va_start(ap, fmt);
    printf(fmt, ap);
    va_end(ap);
    exit(1);
}

void err_dump(const char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    printf(fmt, ap);
    va_end(ap);
}

int
main(int argc, char *argv[])
{
    int val;

    if (argc != 2)
        err_quit("usage: a.out <descriptor#>");

    if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
        err_sys("fcntl error for fd %d", atoi(argv[1]));

    switch (val & O_ACCMODE) {
    case O_RDONLY:
        printf("read only!");
        break;
    case O_WRONLY:
        printf("wirte only!");
        break;
    case O_RDWR:
        printf("read write");
        break;
    default:
        err_dump("unknown access mode");
    }

    if (val & O_APPEND)
        printf(", appended");
    if (val & O_NONBLOCK)
        printf(", nonblocking");
#if defined(O_SYNC)
    if (val & O_SYNC)
        printf(", synchronous writes");
#endif
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)
    if (val & O_FDSYNC)
        printf(", synchronous writes");
#endif
    putchar('\n');
    exit(0);
}
注意我们使用特性测试宏_POSIX_C_SOURCE来条件编译不属于POSIX.1的文件访问标志。下面展示在bash下执行上面程序的结果:
$ ./a.out 0 < /dev/tty
read only!
$ ./a.out 1 > tmp.foo
$ cat tmp.foo
write only!
$ ./a.out 2 2>> tmp.foo
wirte only!, appended
$ ./a.out 5 5<>tmp.foo
read write

当我们修改文件描述符标志或文件状态标志时,我们必须小心取存在的标志值,根据需要修改它,然后设置新的标志值。我们不能简单地执行一个F_SETFD或F_SETFL,因为这可能会关掉之前设置的标志位。

下面代码展示为文件描述符设置一个或多个文件状态标志的函数:

void
set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
    int val;
    if ((val = fcntl(fd, F_GETFL, 0)) < 0)
        exit(1);
    val |= flags; /* turn on flags */
    if (fcntl(fd, F_SETFL, val) < 0)
        exit(1);
}
如果我们把中间的语句变为:val &= ~flags; /* turn flags off */,我们便有了一个名为clr_fl的函数。我们在之后的例子里会用到它。这个语句让flags的反码逻辑与当前的val。

如果在前面测试缓冲区大小与I/O效率关系的代码里把下面这行:
set_fl(STDOUT_FILENO, O_SYNC);
加到代码的开头,我们便开启了同步写标志。这会导致每次写都会等待数据写入磁盘之后才会返回。一般来说在UNIX系统,一个write只把数据排队,而真正的磁盘写操作发生在之后的某刻。一个数据库系统很可能使用O_SYNC,以便在从wirte返回时它能知道数据已经真实写到磁盘上了,以防异常系统错误。

我们预测当程序运行时O_SYNC会增加时钟时间。为了测试它,我们可以运行之前测试I/O效率的代码,并从磁盘上的一个文件里拷贝98.5M的数据到另一个文件,同时与设置O_SYNC标志的版本相比较。比较结果如下表所示:

各种同步机制下Linux ext2时间测量结果
操作用户CPU(秒)系统CPU(秒)时钟时间(秒)
之前测试BUFFSIZE=4,096的结果0.030.166.86
一般的写入磁盘文件0.020.306.87
设置O_SYNC,写入磁盘文件0.030.306.83
在写入磁盘文件之后使用fdatasync0.030.4218.28
在写入磁盘文件之后使用fsync0.030.3717.95
设置O_SYNC,在写入磁盘文件后使用fsync0.050.4417.95
上表的6行都是用大小为4,096的BUFFSIZE测量的。在早先测量I/O效率时,我们读取磁盘文件并写入到/dev/null,所以并没有磁盘输出。上表的第2行对应着从读取一个磁盘文件并写入到另一个磁盘文件。这是为什么第一行和第二行的结果不同。当我们写一个磁盘文件时系统时间增加,因为内核从我们的进程复制数据并把它排队以等待磁盘驱动器的写入。当我们写一个磁盘文件时,同样预期时钟时间会增加,但在这个测试中它并没有显著地增加,这表明我们的写进入了系统缓存,而我们没有测量数据写入磁盘的真实花销。

当我们启用同步写时,系统时间和时钟时间本应该显著地增加。但如第三行显示的,同步写的时间与延迟写的时间基本相同。这蕴含着Linux ext2文件系统没有支持O_SYNC标志,这个猜测在第6行(和第5行)得到了证实:写磁盘文件后调用fsync在有无O_SYNC标志的情况下都一样大。在同步写一个文件后,我们预期fsync的调用将会没有效果。

下表展示了在Mac OS X 10.3上执行相同测试的时间结果:

各种同步机制下Mac OS X时间测量结果
操作用户CPU(秒)系统CPU(秒)时钟时间(秒)
写入/dev/null0.060.794.33
一般的写入磁盘文件0.053.5614.40
设置O_FSYNC,写入磁盘文件0.139.5322.48
在写入磁盘文件之后使用fsync0.113.3114.12
设置O_FSYNC,在写入磁盘文件后使用fsync0.179.1422.12

注意时间与我们预期的一样:同步写比延迟写的耗费大了许多,而在同步写时使用fsync并没有得到不同的测量结果。同样注意到在延迟写后加入一个fsync调用同样没有测量结果上的不同。很有可能是当我们把新数据写到文件时,操作系统将先前的数据强制输出到磁盘中,所以在我们调用fsync时,已经没有多少剩余工作可以做了。

对比下更新文件内容的fsync和fdatasync,和每次写都更新文件内容的O_SYC标志。

在这个例子里,我们看到fcntl的作用。我们程序操作一个描述符(标准输出),而不知道shell通过该描述符打开的文件的名字。既然shell打开了文件,我们不能在文件被打开时设置O_SYNC标志。使用fcntl,我们可以在仅知道描述符的情况下,修改一个描述符的属性。我们可以在15.2节讨论非阻塞管道时看到fcntl的另一个用处,因为我们知道管道的所有信息就是一个描述符。

3.15 ioctl函数

ioctl函数总是包含了所有的I/O操作。任何不能用本章其它函数来表达的事情通常都需要ioctl来指定。终端I/O是这个函数的最大用户。(当我们到第18章时,我们将看到POSIX.1已经把终端I/O操作替换为单独的几个函数。)

#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and LINUX */
#include <stropts.h> /* XSI STREAMS */

int ioctl(int filedes, int request, ...);
错误返回-1,成功返回其它值。

ioctl函数被作为一个处理STREAMS设备[Rago 1993]的扩展包含在单一UNIX规范里。然而UNIX系统实现却使用它执行许多混杂的设备操作。一些实现还把它扩展为可以操作普通文件。

我们展示的函数原型对应着POSIX.1。FreeBSD 5.2.1和Mac OS X 10.3把第二个参数声明为unsigned long。这种细节无关紧要,因为第二个参数总是一个头文件里的一个#defined名。

根据ISO C原型,省略号被用来表示剩余的参数。尽管如此,通常只有一个多余的参数,而它一般是指向一个变量或结构体的指针。

在这个原型里,我们只展示包含函数本身的头文件。通常需要额外的设备相关的头文件。比如超越了POSIX.1规定的基本操作的处理终端I/O的ioctl命令,都需要头文件<termios.h>。

每个设备驱动器可以定义它自己的ioctl命令集。尽管如此,系统为不用类型的设备提供了一个通用的ioctl命令。下表给出一FreeBSD支持的这些通用iotcl的一些类目的例子:

通用FreeBSD ioctl操作
类目常量名头文件ioctl的数量
磁盘标签DIOXXX<sys/disklabel.h>6
文件I/OFIOXXX<sys/filio.h>9
磁带I/OMTIOXXX<sys/mtio.h>11
套接字I/OSIOXXX<sys/sockio.h>60
终端I/OTIOXXX<sys/ttycom.h>444
磁带操作允许我们在磁带上写上文件结束标记,倒回磁带,前进越过特定数量的文件或记录,等等。这些操作中没有一个是能用本章其它函数(read、write、lseek等等)简单表达的,所以处理这些设备的最简单的方法一直是使用ioctl访问它们的操作。

我们在14.4节讨论STREAMS系统时、在18.12节得到和设置终端窗口的尺寸时、以及在19.7节访问伪终端的高级特性时会使用ioctl函数。

3.16 /dev/fd

更新的系统提供了一个名为/dev/fd的目录,它下面的文件名为0、1、2等。打开文件/dev/fd/n等价于假设描述符n已经打开并复制描述符n。

/dev/fd特性由Tom Duff开发,并在Research UNIX系统的第8版出现。本文提及的4个系统都支持这个特性:FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3和Solaris 9。但它不是POSIX.1的一部分。

在函数调用fd = open("dev/fd/0", mode);里,多数系统都忽略指定的mode,而其它系统要求mode为引用文件(本例为标准输入)在打开时的模式的子集。因为前面的open等价于fd = dup(0);,所以描述符0和fd共享同一个文件表项。比如,如果描述符0以只读方式打开,我们也只能通过fd来读。即使系统忽略了open模式,而且函数调用fd = open("/dev/fd/0", O_RDWR);成功了,我们也不在向fd中写入。

我们也可以用/dev/fd路径名作为creat函数和指定O_CREAT的open函数的参数。举个例子,这允许调用creat函数的程序仍然可以工作,如果路径名参数为/dev/fd/1。

一些系统提供了路径名/dev/stdin、/dev/stdout和/dev/stderr。这些路径名等价于/dev/fd/0、/dev/fd/1和/dev/fd/2。

/dev/fd文件的主要在shell里使用。它允许使用路径名参数的程序可以和处理其它路径名一样的方式,来处理标准输出和标准输入。比如,cat程序指定寻找一个名为“-”的文件名表示标准输入。命令filter file2 | cat file1 - file3 | lpr就是一个例子。首先,cat读取file1,接着是标准输入(从filter程序的file2的输出),然后是file3.如果/dev/fd被支持,那“-”的特殊处理可以从cat中移除,我们可以输入
filter file2 | cat file 1 /dev/fd/0 file3 | lpr。

“-”作为一个命令行参数表示标准输入或标准输出的特殊意思已经蔓延到很多程序。如果我们指定“-”为第一个文件同样会有问题,因为它看起来像另一个命令行选项的开头。使用/dev/fd会更加规范和清晰。

3.17 总结

这样讨论了UNIX系统提供的基本I/O函数。因为每个read和write引发一个内核的系统调用,这些通常被称为未缓冲的I/O函数。在只使用read和write的情况下,我们看到了各种I/O尺寸对于读取文件所需时间的影响。我们同样看到了强制输出已写入的数据到磁盘的几种方法,以及它们对程序性能的影响。

当多个进程添加到同一文件时,和多个进程创建一个新的文件时,原子操作被介绍了。我们也看到了内核用来共享打开文件信息所使用的数据结构。我们将在本文后面回来讨论这些数据结构。

我们也讨论了ioctl和fcntl函数。我们会在14章重回讨论这些函数,在那里我们在STREAMS I/O系统里使用ioctl,以及用fcntl来控制记录锁。


第四章 文件与目录

4.1 引言

在前一章我们讲述了基本的I/O函数。讨论主要集中在普通文件的I/O--打开文件、读文件或写文件。我们将看到一些文件系统和文件属性的更多特性。我们将从stat函数开始中并讲解stat结构体的每个成员,看看一个文件的所有属性。在这个过程里,我们也会讲述每个修改这些属性的函数:改变属主、改变权限等等。我们还会看到更多关于UNIX文件系统结构和符号链接的细节。我们会以操作目录的函数结束本章,同时会写一个遍历一个目录结构的函数。

4.2 stat、fstat和lstat函数

这章的讨论主要集中在这三个stat函数,以及它们返回的信息。

#include <sys/stat.h>

int stat(const char *restrict pathname, struct stat *restrict buf);

int fstat(int filedes, struct stat *buf);

int lstat(const char *restrict pathname, struct stat *restrict buf);

三个函数成功都返回0,错误都返回-1。

给定一个路径名,stat函数返回一个关于名字对应的文件的信息。fstat函数得到关于已经在描述符filedes上打开的文件的信息。lstat函数与stat相似,但当文件是一个符号链接时,lstat函数返回关于符号链接的信息,而不是其所指向的文件。(我们在4.21节遍历目录结构时会用到lstat。在4.16节会更深入讲述符号链接。)

第二个参数是一个指向一个我们必须提供的结构体的指针。函数填充buf指向的结构体。结构体的定义在不同实现间会有不同,但它看起来大概是:

struct stat {
  mode_t       st_mode;    /* 文件类型和模式(权限) */
  ino_t        st_ino;     /* i-node号(序列化号) */
  dev_t        st_dev;     /* 设备号(文件系统) */
  dev_t        st_rdev;    /* 特殊文件的设备号 */
  nlink_t      st_nlink;   /* 链接的数量 */
  uid_t        st_uid;     /* 属主的用户ID */
  git_t        st_gid;     /* 属主的组ID */
  off_t        st_size;    /* 普通文件的以字节为单位的尺寸 */
  time_t       st_atime;   /* 最后访问时间 */
  time_t       st_mtime;   /* 最后修改时间 */
  time_t       st_ctime;   /* 最后文件状态改变时间 */
  blksize_t    st_blksize; /* 最好的I/O块尺寸 */
  blkcnt_t     st_blocks;  /* 分配的磁盘块的数量 */
};
st_rdev、st_blksize和st_blocks域并不是POSIX.1所要求的。它们定义在SUS的XSI扩展里。

注意每个成员都由原始系统数据类型指定。我们将会遍历这个结构体的每个成员来检查一个文件的属性。

stat函数的最大的用户很可能是ls -l命令,用来知道一个文件的所有信息。

4.3 文件类型 (File Types)

迄今我们已经说过两个不同的文件类型:普通文件和目录。UNIX系统的多数文件都是普通文件或者目录,但也有其它的文件类型。文件类型有:
1、普通文件。最普遍的文件类型,以某种格式包含数据。对于UNIX内核而言,数据是文本还是二进制没有区别。一个普通文件内容的任何任何解释都留给处理这个文件的程序。一个显著的例外是二进制可执行文件。为了执行一个程序,内核必须了解它的格式。所有的二进制可执行文件都遵守一个允许内核来找到在哪里载入一个程序的代码和数据的格式。

2、目录文件。一个包含其它文件名和这些文件的信息指针的文件。任何从一个有目录文件读权限的进程都可以读取这个目录的内容,但只有内核才能直接向一个目录文件写入。进程必须使用这章介绍的来对一个目录进行修改。

3、块特殊文件。一种提供以固定尺寸单位的缓冲I/O方式访问设备(比如硬盘)的文件类型。

4、字符特殊文件。一种提供以可变尺寸单位的未缓冲I/O方式访问设备的文件类型。在一个系统上的所有设备不是块特殊文件就是字符特殊文件。

5、FIFO。一种用于进程间通信的文件类型。它有时被称为命令管道。我们在15.5节讨论FIFO。

6、套接字。一种用于进程间网络通信的文件类型。一个套接字也可以用来在同一主机上的进程间的非网络通信。我们在16章使用套接字来进程进程间的通信。

7、符号链接。一种指向另一个文件的文件类型。我们在4.16节更深入讨论符号链接。

一个文件的类型编码在stat结构体的st_mode成员里。我们可以使用下表的宏来决定文件类型。这些宏的参数都是stat结构体的st_mode成员:

<sys/stat.h>里的文件类型宏
文件类型
S_ISREG()普通文件
S_ISDIR()目录文件
S_ISCHR()字符特殊文件
S_ISBLK()块特殊文件
S_ISFIFO()管道或FIFO
S_ISLNK()符号链接
S_ISSOCK()套接字

POSIX.1允许系统实现把进程间通信(interprocess communication,IPC)对象,比如消息队列和信号量作为文件。在下表中的宏允许我们根据stat结构体决定IPC对象的类型。这些实验室与上表中的不同,它们的参数是指向stat结构体的指针,并非以st_mode成员:

<sys/stat.h>里的IPC类型宏
对象类型
S_TYPEISMQ()消息队列
S_TYPEISSEM()信号量
S_TYPEISSSHM()共享内存对象

消息队列、信号量和共享内存对象会在第15章讨论。尽管如此,在本文讨论的UNIX实现里没有一个把这些对象表示成文件。

下面的例子打印每个命令行参数的文件类型:

#include <sys/stat.h>

int main(int argc, char *argv[])
{
    int i;
    struct stat buf;
    char *ptr;

    for (i = 1; i < argc; i++) {
        printf("%s: ", argv[i]);
        if (lstat(argv[i], &buf) < 0) {
            printf("lstat error\n");
            continue;
        }
        if (S_ISREG(buf.st_mode))
            ptr = "regular";
        else if (S_ISDIR(buf.st_mode))
            ptr = "directory";
        else if (S_ISCHR(buf.st_mode))
            ptr = "character special";
        else if (S_ISBLK(buf.st_mode))
            ptr = "block special";
        else if (S_ISFIFO(buf.st_mode))
            ptr = "fifo";
        else if (S_ISSOCK(buf.st_mode))
            ptr = "socket";
        else if (S_ISLNK(buf.st_mode))
            ptr = "symbolic link";
        else
            ptr = "** unknown mode **";
        printf("%s\n", ptr);
    }
    exit (0);
}
我们特别使用了lstat函数而不是stat函数,来查明符号链接。如果我们使用stat函数,我们不能看到符号链接。

为了在Linux系统上编译这个程序,我们必须定义_GNU_SOURCE来包含S_ISSOCK宏的定义。

历史上,UNIX系统的早期版本并没有提供S_ISXXX宏。相反,我们必须对st_mode值和S_IFMT掩码进行逻辑与操作,然后比较与操作后的结果和名为S_IFxxx的常量。多数系统在<sys/stat.h>里定义了这个掩码和相关常量。如果我们检查这个文件,我们会发现S_ISDIR宏的定义类似于:
#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR)

我们已经说过普通文件是占主导地位的,但看看在给定系统上每个文件类型的文件百分比是很有趣的。下表展示了使用Linux系统作为一个单一用户的工作站的数量和百分比。这个数据是通过4.21节的程序获得的:

不同文件类型的计数和百分比
文件数量百分比
普通文件226,85688.22%
目录23,0178.95
符号链接6,4422.51
字符特殊4470.17
块特殊3120.12
套接字690.03
FIFO10.00

4.4 设置用户ID和设置组ID(Set-User-ID and Set-Group-ID)

每个进程都有相关的6个或更多的ID,如下表所示:

每个进程相关的用户ID和组ID
真实用户ID(real user ID)
真实组ID(real group ID)
我们真实的身份
有效用户ID(effective user ID)
有效组ID(effective group ID)
补充组ID(supplementary group ID)
用于文件权限核查
保存的 set-user-ID(saved set-user-ID)
保存的 set-group-ID(saved set-group-ID)
由exec函数保存
1、真实用户ID和真实组ID标识了我们真实的身份。这两个域在我们登录时从password文件里的我们的记录里得到。通常,这些值在一个登录会话期里不会改变;
2、有效用户ID、有效组ID和补充组ID决定了我们文件的访问权限,正如我们在下一节会讲述的一样。(我们在1.8节定义过补充组ID。)
3、保存的set-user-ID和保存的set-group-ID在程序执行时,包含了有效用户ID和有效组ID的复制。我们在8.11节讨论setuid函数时讨论这两个保存的值的函数。这些保存的ID被POSIX.1的2001版所需要。它们在POSIX的早期版本上经常是可选的。一个程序可以在编译期测试_POSIX_SAVED_IDS常量或者在运行期用_SC_SAVED_IDS参数调用sysconf函数,来看实现是否支持这个特性。

通常,用效用户ID和真实用户ID相等,而有效组ID和真实组ID相等。

每个文件都有一个属主和一个所属组。属主由stat结构的st_uid成员指定,而所属组由st_gid成员指定。

当我们执行一个程序文件,进程的有效用户ID常常是真实用户ID,而有效组ID常是真实组ID。然而我们可以为文件模式(st_mode)设置一个特殊的标志,表示“这个文件执行时,把进程的有效用户ID设置为文件的属主”。类似的,文件模式的另一位可以设置以便有效组ID成为文件的所属组(st_gid)。在文件模式字的这两个位称为set-user-ID位和set-group-ID位。

举个例子,如果文件的属组是超级用户,而如果文件的set-user-ID被设置,则当程序文件当作一个进程运行时,它便有了超级用户的权限,而不管执行这个文件的进程的真实用户ID。例如,允许任何人改变他或她的密码的UNIX系统程序passwd,就是一个set-user-ID程序。这是必需的,以便程序可以把新的密码写入密码文件,通常是/etc/passwd或/etc/shadow,而这些文件只应该对超级用户可写。因为一个运行set-user-ID的进程通常得到额外的权限,所以它必须小心地写。我们会在第8章更深入地讨论这种类型的程序。

回到stat函数,set-user-ID位和set-group-ID位包含在文件的st_mode值里。我们可以用常量S_ISUID和S_ISGID来测试这两个位。

4.5 文件访问权限(File Access Permission)

st_mode值也包含了文件的访问权限位。当我们说一个文件时,意思是任何我们早先提到的文件类型。所有文件类型--目录、字符特殊文件等--都有权限。许多人认为只有普通文件有访问权限。

每个文件有9个权限位,分为三个类别。如下表所示:

<sys/stat.h>的9个文件访问权限位
st_mode 掩码含义
S_ISUSR用户可读
S_IWUSR用户可写
S_IXUSR用户可执行
S_IRGRP组可读
S_IWGRP组可写
S_IXGRP组可执行
S_IROTH其他人可读
S_IWOTH其他人可写
S_IXOTH其他人可执行

前三行的术语“用户”表示文件的属主。chmod命令,通常用来修改这9个权限位,允许我们指定u(用户,属组)、g(组)或o(其他人)。一些把这三个描述为owner、group和world,这是令人困惑的,因为chmod命令使用o表示other,而不是owner。我们将使用术语user、group和other,来与chomd命令保持一致。

上表中的三个类别--读、写和执行--被不同的函数用各种方法使用。我们在这里概括总结一下,然后在我们讨论真实的函数时再回过来讨论它们:
1、第一条规则是,无论何时我们想要通过名字打开任何类型的文件,我们必须有这个名字提及的目录的执行权限,包括当前路径,如果它是隐含的。这也是为什么目录的执行权限经常被称为查找位(search bit)。
比如,为了打开文件/usr/include/stdio.h,我们需要目录“/”、/usr、/usr/include的执行权限。根据我们如何打开这个文件(只读、读写等),我们然后需要文件本身恰当的权限。

如果当前目录是/usr/include,我们则需要当前目录的执行权限来打开文件stdio。这是个当前路径被隐含而非显示提及的例子。它等同于我们打开文件./stdio.h。

注意目录的读权限和执行权限是两码事。读权限让我们读这个目录,等到这个目录的所有文件名列表。当目录是一个我们要访问的路径名的一部分时,执行权限让我们解析这个目录。(我们需要查找这个目录来得到指定的文件名。)

另一个隐含目录的例子是:PATH环境变量(8.10节)指定了一个没有执行权限的目录。这种情况下,shell不会找到那个目录下的可执行文件。

2、文件的读权限决定了我们是否能打开一个存在的文件来读:open函数里指定O_RDONLY和O_RDWR标志。

3、文件的写权限决定了我们是否能打开一个存在的文件来写:open函数里指定O_WRONLY和O_RDWR标志。

4、我们必须有一个文件的写权限才能在open函数里指定O_TRUNC标志。

5、除非我们有一个目录的写权限和执行权限,否则我们不能在它下面创建一个新的文件。

6、为了删除一个存在的文件,我们需要包含这个文件的目录的写权限和执行权限。我们不需要文件本身的读权限或写权限。

7、如果我们想用6个exec函数(8.10节)中的任一个执行某文件,该文件的执行权限必须开启。


每当一个进程打开,创建或删除一个文件时,内核执行的文件的访问测试依赖于文件的属主和所属组(st_uid和st_gid)、进程的有效ID(用效用户ID和有效组ID)以及进程的补充组ID(如果支持的话)。属主ID和所属组ID是文件的属性,而两个有效ID和补充组ID是进程的属性。这些测试由内核以如下方式执行:

1、如果进程的有效组ID为0(超级用户),访问被允许。这给予了超级用户整个文件系统的自由。

2、如果进程的有效组ID与文件的属主ID一样(即进程拥有这个文件),且恰当的用户访问权限位被设置的话,访问被允许。否则,访问被拒绝。恰当的访问权限位,意思是进程打开文件来读时,用户读的位必须开启;如果进程打开文件来写,用户写的位必须开启;如果进程打开文件来执行,用户执行的位必须开启;等等。

3、如果进程的有效组ID或它其中一个补充组ID与文件的组ID相同,且恰当的组访问权限位被设置,访问被允许。否则访问被拒绝。

4、如果恰当的其它访问权限位被设置,访问被允许。否则访问被拒绝。

这四个步骤是依次被尝试。注意如果进程拥有这个文件(第2步),访问的接受或拒绝取决于用户访问权限,而组权限不会被查看。类似的,如果进程没有拥有这个文件,但属于一个恰当的组,访问的接受或拒绝取决于组访问权限,其它权限不会被查看。

4.6 新文件和目录的所属(Ownership of New File and Directories)

当我们在第3章描述仍用open或creat进行一个新文件的创建时,我们没有说过这个新文件的用户ID和组ID的值是多少。我们将在4.20节讨论mkdir时看到如何创建一个新的目录。一个新目录的所属规则与本节的新文件所属规则相同。

一个新文件的用户ID被设置为进程的有效用户ID。POSIX.1允许一个实现来选择以下选项来决定新文件的组ID:
1、新文件的组ID可以是进程的有效组ID;

2、新文件的组ID可以是被创建文件所在目录的组ID。FreeBSD 5.2.1和Mac OS X 10.3总是使用目录的组ID作为新文件的组ID。Linux ext2和ext3文件系统允许在POSIX.1的两个选项中选择,通过mount命令的一个特殊标志。在Linux 2.4.22(使用合适的mount选项)和Solaris 9,新文件的组ID取决于创建的文件所在的目录的set-group-ID是否被设置。如果这个位被设置,则新文件的用户ID设置为目录的用户ID,否则新文件的用户ID设置为进程的有效组ID。

使用每二个选项--从目录的组ID继承--向我们保证了在目录下创建的所有的文件和目录都会有属于该目录的组ID。文件和目录的所属组关系会随着该目录沿着目录结构往下蔓延。例如,这应用在Linuxr的/var/spool/mail目录。

如我们提到的,在FreeBSD 5.2.1和Mac OS X 10.3上,这个所属组关系是默认的,然而在Linux和Solaris上是可选的。在Linux 2.4.22和Solaris 9下,我们必须开启set-group-ID位,同时mkdir函数必须自动传播一个目录的set-group-ID位,才能达到这种所属组关系。(在4.20里会讨论。)

4.7 access函数

正如我们早先描述的那样,当我们打开 一个文件,内核会基于有效用户和组ID来进行访问测试。有时进程会想基于真实用户和组ID来执行它的访问测试。当进程使用set-user-ID或set-group-ID特性以其他人的身份运行时这非常有用。即使一个进程可能通过set-user-ID成为根用户,它可能仍想检查真实用户是否能访问给定的文件。access函数基于真实用户和组ID来测试。(把4.5节末尾的4个步骤里的“真实”替换为“有效”。)

#include <unistd.h>

int access(const char *pathname, int mode);
成功返回0,错误返回-1。

mode为下表常量中的位或值:

mode描述
R_OK测试读权限
W_OK测试写权限
X_OK测试执行权限
F_OK测试文件存在

看下面的代码:

#include <unistd.h>
#include <fcntl.h>

int
main(int argc, char *argv[])
{
    if (argc != 2)
        exit(1);
    if (access(argv[1], R_OK) < 0)
        printf("access error for %s\n", argv[1]);
    else
        printf("read access OK\n");
    if (open(argv[1], O_RDONLY) < 0)
        printf("open error for %s\n", argv[1]);
    else
        printf("open for reading OK\n");
    exit(0);
}
使用该程序来操作/etc/shadow文件。首先看该文件以及程序的权限:
$ ls -l /etc/shadow
-r--r----- 1 root shadow 1461 2012-02-03 11:14 /etc/shadow
$ ls -l a.out
-rwxr-xr-x 1 tommy tommy 7289 2012-02-22 10:26 a.out

再运行a.out:
$ ./a.out /etc/shadow
access error for /etc/shadow
open error for /etc/shadow

把自己变为超级用户后,为a.out的设置set-userID权限:
$ su
密码:
# chown root a.out
# chmod u+s a.out
# ls -l a.out
-rwsr-xr-x 1 root tommy 7289 2012-02-22 10:26 a.out

退出root身份
# exit
exit

再执行程序(以普通用户的身份)
$ ./a.out /etc/shadow
access error for /etc/shadow
open for reading OK

在这个例子里,我们把a.out设置为set-user-ID程序,它可以查觉到真实用户(tommy)不能访问这个文件,虽然它可以使用open函数来访问该文件。

在前面的例子以及在第8章里,我们有时会变为超级用户,来证明事情是怎么工作的。如果你在一个多用户系统而没有超级用户权限的话,你无法完全重现这些例子。

4.8 umask函数

既然我们已经描述了每个文件相关的9个权限位,我们可以描述下与每个进程相关取的文件模式创建掩码(file mode creation mask)。

umask函数为进程设置文件模式创建掩码,并返回其之前的值。(这是很少的几个不返回错误值的函数之一。)

#include <sys/stat.h>

mode_t umask(mode_t cmask);

返回前一个文件模式创建掩码

cmask参数由4.5节里的表中的9个常量的任意位或的值:S_IRUSR、S_IWUSR等。

每当进程创建一个新文件或一个新目录时,文件模式创建掩码就会被使用。(回想下3.3节和3.4节里我们对open和creat函数的描述。两者都接受一个mode参数来指定新文件的访问权限位。)我们在4.20节讲述如何创建一个新的目录。文件模式创建掩码的任何位都会在文件模式里关闭。

看下面的例子:

#include <sys/stat.h>
#include <fcntl.h>

#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)

int
main(void)
{
        umask(0);
        if (creat("foo", RWRWRW) < 0)
                exit(1);
        umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
        if (creat("bar", RWRWRW) < 0)
                exit(1);
        exit(0);
}
首先查看下当前的umask值:
$ umask
0022
运行a.out,并查看新文件的模式:
$ ./a.out
$ ls -l foo bar
-rw------- 1 tommy tommy 0 2012-02-22 10:55 bar
-rw-rw-rw- 1 tommy tommy 0 2012-02-22 10:55 foo
再查看下现在的umask值:
$ umask
0022

UNIX系统的多数用户从不处理他们的umask值。它通常在登录时被shell的start-up文件设置一次,然后不再改变。尽管如此,当写创建新文件的程序时,如果我们想保证特定的访问权限位被启用,我们则必须在进程运行时修改umask的值。比如,如果我们想要保证每个人都能读取一个文件,我们应该设置umask为0。不然,当我们进程运行时,起效的umask值会导致权限位被关闭。

在前一个例子里,我们使用shell的umask命令在我们运行程序前后打印文件模式创建掩码。这给我们展示了改变一人进程的文件模式创建掩码不会影响它父进程(通常是shell)的掩码。所有的shell都有一个内建的umask命令,我们可以用它来设置或打印当前的文件模式创建掩码。

用户可以设置umask值来控制他们创建的文件的默认权限。这个值以八进程表示,其中每个位表示一个屏蔽的权限,如下表所示:

umask的文件模式访问权限位
掩码位含义
0400用户读
0200用户写
0100用户执行
0040组读
0020组写
0010组执行
0004其他人读
0002其他人写
0001其他人执行
设置相应的位会否定权限。一些普遍的umask值为002,阻止其他人写你的文件,022阻止组成员和其他人写你的文件,027阻止组成员写你的文件以及阻止其他们读、写或执行你的文件。

单一UNIX规范要求shell支持umask命令的符号格式。不像八进制,符号格式指定哪些权限是被允许的(也就是说,清除文件创建掩码)而不是拒绝它(也就是设置文件创建掩码)。比较下面命令的两种格式:
$ umask
0022
$ umask -S
u=rwx,g=rx,o=rx
$ umask 027
$ umask -S
u=rwx,g=rx,o=

4.9 chmod和fchmod函数

这两个函数允许我们改变一个已存在的文件的访问权限。

#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);

int fchmode(int filedes, mode_t mode);

两者成功都返回0,失败返回-1。

chmod操作一个指定的文件,而fchmod操作一个已打开的文件。

为了改变一个文件的权限位,进程的有效用户ID必须与文件的属主ID相同,或者进程必须有超级用户权限。

mode有下表的常量的位或值指定:

<sys/stat.h>是chmod函数的模式常量
模式描述
S_ISUID执行时的set-user-ID
S_ISGID执行时的set-group-ID
S_ISVTXsaved-text(粘滞位)
S_IRWXU用户读、写、执行
S_IRUSR用户读
S_IWUSR用户写
S_IXUSR用户执行
S_IRWXG组读、写、执行
S_IRGRP组读
S_IWGRP组写
S_IXGRP组执行
S_IRWXO其他人读、写、执行
S_IROTH其他人读
S_IWOTH其他人写
S_IXOTH其他人执行
注意上表中有9个访问权限位与4.5节的表一样。我们加入了两个设置ID常量(S_ISUID和S_ISGID)、saved-text常量(S_ISVTX)、以及三个联合常量(S_IRWXU、S_IRWXG和S_IRWXO)。

saved-text位(S_ISVTX)不是POSIX.1的一部分。这作为SUS的一个XSI扩展被定义。我们会在下节讲述它的用途。

看下面的代码:

#include <sys/stat.h>

int
main(void)
{
        struct stat statbuf;

        /* turn on set-group-ID and turn off group-execute */

        if (stat("foo", &statbuf) < 0)
                exit(1);
        if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
                exit(1);

        /* set absolute mode to "rw-r--r--" */

        if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
                exit(1);
        exit(0);
}
仍然用前面umask所使用的两个文件:
$ ls -l foo bar
-rw------- 1 tommy tommy 0 2012-02-22 11:37 bar
-rw-rw-rw- 1 tommy tommy 0 2012-02-22 11:37 foo
$ ./a.out
$ ls -l foo bar
-rw-r--r-- 1 tommy tommy 0 2012-02-22 11:37 bar
-rw-rwSrw- 1 tommy tommy 0 2012-02-22 11:37 foo

在这个例子里,我们把文件bar的权限设置为一个绝对值,而不管它之前的权限位是什么。对于文件foo,我们设置它的权限位为一个相对值。为了完成这件事,我们首先调用stat来得到当前的权限然后修改它们。我们显示地打开设置组ID位并关闭组执行位。注意ls命令把组执行位设置为“S”来表示设置组ID位被设置,而组执行位没有被设置。(“s”表示两者都被设置。)

在Solaris,ls命令显示一个“1”而非“S”来表示受委托的文件和记录锁在这个文件上开启了。这只能应用在普通文件上,但我们会在14.3节讨论更多细节。

最后,注意ls命令列出的时间和日期并有在程序运行后改变。我们将在4.18节看到chmod函数只更新i-node上次改变的时间。默认情况下,ls -l会列出文件内容的最后修改时间。

chmod函数在下面的条件下会自动清除两个权限位:

1、一些系统,比如Solaris,当普通文件使用粘滞位时会赋予其特殊的意义。如果在这种系统上,我们尝试在普通文件上设置粘滞位(S_ISVTX)而没有超级用户的权限的话,模式里的粘滞位会被自动关闭。(我们在下节讨论粘滞位。)这意味着只有超级用户可以为普通文件设置粘滞位。原因是为了避免恶意用户设置粘滞位从而影响系统的性能。

在FreeBSD 5.2.1、Mac OS X 10.3和Solaris 9上,只有超级用户可以在普通文件上设置粘滞位。Linux 2.4.22没有这样的限制,因为在Linux上普通文件上的粘滞位没有任何意义。尽管FreeBSD和Mac OS X上当应用到普通文件时这个位也没有意义,但这些系统阻止除了用户外的任何人为普通文件设置这个位。

2、一个新建文件的组ID可能并不包含创建该文件的进程。回想下4.6节,有可能新建文件的组ID是父目录的组ID。特别地,如果新建文件的组ID不等于进程的有效组ID或进程的一个补充组ID,且进程没有超级用户权限,那么设置组ID位会被自动关闭。这避免了用户创建一个属于一个该用户不属于的组的设置组ID文件,

FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3和Solaris 9加入另一个安全特性,试图避免一些保护位的误用。如果一个没有超级用户权限的进程写一个文件时,设置用户ID和设置组ID位会自动关闭。如果恶意用户找到一个他们能写的设置组ID或设置用户ID文件,尽管他们能修改这个文件,但他们也失去了这个文件的特殊权限。

4.10 粘滞位(Sticky Bit)

S_ISVTX位有一个有趣的历史。在UNIX系统的提前所需页面高度的版本上,这个位被熟知为粘滞位。如果它被设置到一个可执行程序文件,那么程序第一次执行时,程序的代码(text)的一个复制会在程序终止时保留在交换区。(程序的代码部分是机器指令。)这会使程序下次运行时更快地载入到内存,因为相对于在一个普通UNIX文件系统里的数据块的可能的随机位置,交换区作为一个相接的(contiguous)文件被处理。粘滞位过支常为普通应用程序设置,比如文本编辑器和C编译器的各个阶段。自然地,在用光交换区空间前有一个可以包含在交换区里的粘滞文件数的限制,但它是个非常有用的技术。名字“粘滞”的由来是因为系统的代码段直到系统重启一直粘在交换区里。UNIX的吏后期的版本把这个指为“保存代码”位,从而常量为S_ISVTX。在今天UNIX系统的更新的版本上,多数都有虚拟内存系统和更快的文件系统,这种技术就不再需要了。

在当代系统上,粘滞位的使用被扩展。SUS允许为目录设置粘滞位。如果目录被设置粘滞位,用户只有在拥有目录的写权限并同时满足以下条件,才能删除或重命名目录里的文件:
1、拥有这个文件;
2、拥有这个目录;
3、是超级用户。

目录/tmp和/var/spool/uucppublic是粘滞位的典型侯选--它们是任何用户都能创建文件的目录。这两个目录的权限通常对所有人(用户、组、其他人)可读、可写和可执行。然而用户不该删除或重命名其他人拥有的文件。

代码保留位不是POSIX.1的一部分。它作为基本POSIX.1功能的XSI扩展定义在SUS,并被FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3和Solaris 9支持。

Soloaris9为普通文件的粘滞位赋予特殊的含义。这种情况下,如果没有执行位被设置,操作系统会缓存文件的内容。

4.11 chown、fchown和lchown函数

各chown函数允许我们改变一个文件的用户ID和组ID。

#include <unistd.h>

int chown(const char *pathname, uid_t owner, gid_t group);

int fchown(int filedes, uid_t owner, gid_t group);

int lchown(const char *pathname, uid_t owner, gid_t group);

三者成功都返回0,失败返回-1。

这三个函数基本类似,除非引用的文件是一个符号链接。在这种情况下,lchown改变符号链接本身的属主,而不是符号链接指向的文件。

lchown函数是POSIX.1功能的XSI扩展,定义在SUS里。作为这样的身份,所有的UNIX系统实现都被期望来支持它。

如果owner或group参数的为-1,则对应的ID保持不变。

历史上,基于BSD的系统强制限制只有超级用户能改变文件的属主关系。这是为了避免用户放弃他们自己的文件给别人,从而打破硬盘空间限额的限制。尽管如此,系统V已允许任何用户改变他们拥有的任何文件的属主。

POSIX.1允许两种操作中的任何一个,取决于_POSIX_CHOWN_RESTRICTED的值。

Solaris 9上,这个功能是一个配置的选项,它的默认值是实行这个限制。FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3一直实行这个chown限制。

回想下2.6节_POSIX_CHOWN_RESTRICTED常量可以在头文件<unistd.h>里可选择地被定义,而且总是可以使用pathconf或fpathconf函数查询。同样回想下这个可先项可以依赖于引用的文件,它可以根据不同的文件系统基准启用或禁用。我们将使用短语“如果_POSIX_CHOWN_RESTRICTED有效”来表示它应用于我们正讨论的特定文件,而不管真正的常量是否定义在头文件里。

如果_POSIX_CHOWN_RESTRICTED对指定的文件有效,那么:

1、只有超级用户进程可以改变文件的用户ID;

2、如果非超级用户进程拥有这个文件(有效用户ID和文件用户ID相等),则它可以改变文件的组ID。owner被指定为-1或与文件用户ID相同,而group与进程的有效组ID或进程某个补充组ID相同。

这意味着如果_POSIX_CHOWN_RESTRICTED有效时,你不能改变其他用户文件的用户ID。你可以改变你拥有的文件的组ID,而且你属于该组。

如果一个非超级用户进程调用了这三个函数,在成功返回后,设置用户ID和设置组ID位都会被清除。

4.12 文件尺寸(File Size)

stat结构体的st_size成员包含文件以字节为单位的尺寸。这个域只有普通文件、目录和符号链接有意义。

Solaris同样定义了管道的尺寸,表示管道可读的字节数。我们将在15.2节讨论管道。

对于一个普通文件,文件尺寸为0是允许的。我们将在第一次读取文件时得到一个文件结束标记。

对于一个目录,文件尺寸通常是一个数的倍数,比如16或512.我们在4.21讨论读目录。

对于符号链接,文件尺寸是文件名的字节数。比如,下面的情况下,文件尺寸为7,表示路径名/usr/lib的长度:
lrwxrwxrwx 1 tommy tommy 7 2012-02-22 13:54 lib -> usr/lib

(注意符号链接没有包含在名字末尾的通常C终止符,长度一直由st_size指定。)

多数当代UNIX系统提供了域st_blksize和st_blocks。前者是文件I/O最喜欢的块尺寸,而后者是所分配的512字节块的真实数量。回想下3.9节当我们使用st_blksize进行读操作时,我们碰到了读取文件所需的最小时间量。我们将在第5章讲述的标准I/O库,,同样为了效率一次读或写st_blksize字节。

注意UNIX不同的版本为st_blocks使用不是512字节块的其它值。使用这个值是不可移植的。

文件里的空洞

在3.6节,我们提到一个普通文件可以包含“空洞”。我们也展示了一个例子。空洞可以通过seek超过当前文件结束位置然后写一些数据来创建。举个例子,考虑下面:
$ ls -l file.hole
-rw-r--r-- 1 tommy tommy 8483258 2012-02-22 14:05 file.hole
$ du -s file.hole
8    file.hole
文件fle.hole的尺寸刚超过了8M,然而du命令显示文件使用的磁盘空间是8个1024字节块。(基于BSD的系统报告一个1024字节块的数量,Solaris报告512字节块的数据。)显然,这个文件有许多空洞。

正如我们在3.6节提到的那样,read函数为任何没有写过的字节部分返回字节0。如果我们执行下面的命令,我们能看到普通I/O操作通过文件尺寸来获得信息:
$ wc -c file.hole
8483258 file.hole
使用-c选项的wc命令返回文件的字符(字节)数。

如果我们使用一个比如cat的工具来复制这个文件,所有的这些空洞都会被真实的数据字节0写入:
$ cat file.hole > file.hole.copy
$ ls -l file.hole file.hole.copy
-rw-r--r-- 1 tommy tommy 8483258 2012-02-22 14:05 file.hole
-rw-r----- 1 tommy tommy 8483258 2012-02-22 14:14 file.hole.copy
$ du -s file.hole*
8    file.hole
8288    file.hole.copy

这里,新文件真正使用的字节数为8486912(8288 x 1024)。这个值和ls报告的值不同,因为文件系统使用一些块来保存指向真实数据块的指针。

4.13 文件裁切(File Truncation)

有时我们想要通过在文件末尾删除数据来裁切一个文件。我们可以在open时使用用O_TRUNC标志来清空一个文件,这是裁切的一个特殊例子。

#include <unistd.h>

int truncate(const char *pathname, off_t length);

int ftruncate(int filedes, off_t length);

两者成功都返回0,失败返回-1。

这两个函数把已存在的文件裁切成length字节。如果文件之前的尺寸比length大,超过length后的部分便不再能被访问了。如果之前尺寸比length小,效果依系统而定,但遵守XSI的系统将会增加文件尺寸。如果实现没有扩展一个文件,文件旧末尾与新末尾之间会被读为0(也就是说,文件里很可能创建了一个空洞。)

ftruncate函数是POSIX.1的一部分,truncate函数是POSIX.1功能定义在SUS的XSI扩展。

4.4BSD之前的BSD版本通过truncate只能使文件变得更小。

Solaris还包含了一个fcntl的扩展(F_FREESP),允许我们释放文件的任何部分,而不仅仅是文件末的部分。

我们在13章里的一个程序里,需要在得到文件锁之后清空一个文件时,使用ftruncate。

4.14 文件系统

为了理解一个文件的链接的概念,我们需要UNIX文件系统结构的概念上的理解。理解i-node和指向一个i-node的目录项(directory entry)也是很有用的。

当今有各种UNIX文件系统的实现。比如,Solaris,支持几咱不同的磁盘文件系统类型:传统的基于BSD的UNIX文件系统(称为UFS)、一个(称为PCFS的)读写DOS格式磁盘的文件系统、一个(称为HSFS的)读取CD文件系统的文件系统。我们在第2章已经看过一个文件系统类型之间的区别。UFS是基于Berkely快速文件系统,我们将在本节介绍它。

我们可以认为一个磁盘驱动器分为一个或多个分区。每个分区可以包含一个文件系统,如下图所示:

i-nodes是一堆固定长度的项,包含一个文件的大多数信息。

如果我们更深入的检查磁道组(cylinder group)的i-node和数据块部分,我们可以得到下图:

我们研究下上图:
1、我们展示了两个目录项,它们指向同一个i-node项。每个i-node有一个链接数来包含指向该i-node的目录项的数量。只有当链接数减为0时文件才会被删除(也就是说,文件关联的数据块可以被释放。)这是为什么操作“解链接一个文件”并不总是意味着“删除文件相关的块”。这是为什么删除目录项的函数被称为“unlink”,而不是delete。在stat结构体里,链接数包含在st_nlink成员里。它的原始系统数据类型是nlink_t。这些链接的类型被称为硬链接。回想下2.5.2节的POSIX.1常量LINK_MAX表示一个文件链接数的最大值。

2、另一种类型的链接被称为符号链接。通过符号链接,文件的真实内容--数据块--存储着符号链接指向的文件的名字。在下面的例子里,在目录项里的文件名是3个字符的lib而在文件的数据里的7个字节是/usr/lib:
lrwxrwxrwx 1 tommy tommy 7 2012-02-22 13:54 lib -> usr/lib
i-node里的文件类型应该是S_IFLNK,以使系统知道这是个符号链接。

3、i-node包含文件的所有信息:文件类型、文件访问权限位、文件尺寸、指向文件数据块的指针、等等。stat结构体的多数信息都是从i-node里得到。只有两个有关的项是存储在目录项里:文件名和i-node号。其它项--文件名的长度和目录记录的长度--不在这里讨论。i-node数的数据类型为ino_t。

4、因为目录项指向的i-node号指向在同一文件系统的一个i-node,我们不能有一个指向另一个文件系统的i-node的目录项。这也是为什么ln命令(创建一个指向已存在文件的新的目录项)不能跨越文件系统。我们在下节讲述link函数。

5、当重命名一个文件而不改变文件系统时,文件的真实内容不需要被移动--所有需要做的就是加上一个指向已存在的i-node的新的目录项,然后解链接旧的目录项。链接数会保持不变。比如,为了重命名文件/usr/lib/foo为/usr/foo,如果目录/usr/lib和/usr在同一个文件系统上,文件foo的内容不必移动。这时mv命令通常如何执行的。

我们已经讨论过一个普通文件的链接数,但一个目录的链接数域是怎么样的呢?假设我们在工作目录创建一个新的目录,如:
$ mkdir testdir

下图显示了结果。注意这张图里我们显式地展示了.和..的项:

号为2549的i-node有一个“direcotry”的类型域,和等于2的链接数。任何叶目录(一个不包含任何其它目录的目录)总是有值为2的链接数。这个2值是从以这个目录为名(testdir)的目录项和该目录的.项而来。号为1267的i-node有一个“directory”类型域和一个大于等于3的链接数。我们知道链接数大于等于3的原因是,最低程度下,它被以目录为名的目录(图中没有展示)、点目录、和在testdir的点点目录。注意父目录里的每个子目录都导致父目录的链接数增一。

这个格式与UNIX文件系统的经典格式相似。

4.15 link、unlink、remove和rename函数

正如我们在前一节看到的,任何文件可以有多个目录项指向它的i-node。我们创建一个已存在的文件的链接的方法是使用link函数。

#include <unistd.h>

int link(const char *existingpath, const char *newpath);

成功返回0,失败返回-1。

这个函数创建一个新的目录项newpath,指向已存在文件exsitingpath。如果newpath已存在,则返回一人错误。只有newpath的最后一个部分被创建。路径的其余部分必须已经存在。

新目录项的创建和链接接的增加必须是一个原子操作。(回想下3.11节关于原子操作的讨论。)

多数实现需要两个路径名在同一个文件系统,尽管POSIX.1允许一个实现支持跨越文件系统的链接。如果一个实现支持目录硬链接的创建,它只能由超级用户完成。原因是这样做可能会引起文件系统的回路,多数处理文件系统的工具没有能力处理。(我们在4.16节展示由一个符号链接引起回路的一个例子。)许多文件系统实现因为这个原因不允许目录的硬链接。

为了删除一个已存在的目录项,我们调用unlink函数。

#include <unistd.h>

int unlink(const char *pathname);

成功返回0,错误返回-1。

函数删除目录项并减少pathname引用的文件的链接数。如果文件有其它链接,文件的数据仍可以通过别的链接访问。如果错误发生,文件不会改变。

我们已经提过在解链接文件之前,我们必须有包含该目录项的目录的写权限和执行权限,因为这是我们正删除的目录项。同样,我们在4.10节也提过如果目录设置了粘滞位,我们必须有这个目录的写权限和以下三者之一:拥有该文件、拥有该目录、有超级用户权限。

只有当链接数减为0时文件的内容才能删除。另一个阻止文件内容被删除的条件是:只要有一些进程正打开该文件,它的内容不能被删除。

当一个文件被关闭后,内核首先检查打开该文件的进程数量。如果进程数为0,内核检查链接数,如果链接数为0,文件内容被删除。

看下面的代码:

#include <fcntl.h>

int 
main(void)
{
	if (open("tempfile", O_RDWR) < 0)
		exit(1);
	if (unlink("tempfile") < 0)
		exit(1);
	printf("file unlinked\n");
	sleep(15);
	printf("done\n");
	exit(0);
}
首先看看文件的大小
$ ls -l tempfile
-rw-r----- 1 tommy tommy 8483258 2012-02-22 16:21 tempfile

再看看可用的剩余空间
tommy@tommy-zheng-ThinkPad-T61:~/code/c/unix$ df /home
文件系统           1K-块      已用      可用 已用% 挂载点
/dev/sda1             73843936   9101884  60990936  13% /

后台方式执行程序

$ ./a.out &
[1] 4991

$ file unlinked

查看文件被unlinked后文件是否存在
ls -l tempfile
ls: 无法访问tempfile: 没有那个文件或目录

查看unlink后可用的空间(没有变化)

$ df /home
文件系统           1K-块      已用      可用 已用% 挂载点
/dev/sda1             73843936   9101884  60990936  13% /

程序运行完毕,查看文件关闭后可用的空间(已释放文件内容)

$ done
df /home
文件系统           1K-块      已用      可用 已用% 挂载点
/dev/sda1             73843936   9101816  60991004  13% /

unlink的属性通常被程序用来确保当崩溃的,它创建的临时文件不会留在那里。进程用open或creat创建一个文件然后立即调用unlink。尽管如此,文件并没有被删除,因为它仍被打开。只有当进程关闭这个文件或终止时(导致内核关闭它的所有的打开文件),文件才会被删除。

如果路径名是一个符号链接,unlink删除这个符号链接,而不是链接指向的文件。没有给定链接名删除其指向的文件的函数。

超级用户可以用目录的路径名调用unlink,但是函数rmdir函数应该被用来解链接一个目录,而不是unlink。我们在4.20节讲述rmdir函数。

我们还可以用remove函数解链接一个文件或目录。对于一个文件,remove与unlink相同。对于一个目录,remove和rmidir相同。

#include <stdio.h>

int remove(const char *pathname);

成功返回0,失败返回-1。

ISO C规定了remove函数来删除一个文件。名字从历史上的UNIX名字unlink改变是因为那时实现C标准的多数非UNIX系统都不支持文件链接的概念。

一个文件或目录可以用rename函数来重命名。

#include <stdio.h>

int rename(const char *oldname, const char *newname);

成功返回0,失败返回-1。

这个函数被ISO C为文件定义。(C标准没有处理目录。)POSIX.1扩展了这个定义,包含了目录和符号链接。

根据oldname是指向一个文件、目录还是符号链接,我们有几种条件来讲述下。我们必须说下如果newname已经存在时会发生什么:

1、如果oldname指定一个不是目录的文件,那我们就是重命名一个文件或符号链接。在这种情况下,如果newname已经存在,它不能是一个目录。如果newname已存在且不是一个目录,那么它会被删除,而且oldname会重命名为newname。我们必须有包含oldname的目录和包含newname的目录的写权限,因为我们改变了两个目录。

2、如果oldname指向一个目录,那么我们正在重命名一个目录。如果newname存在的话,它必须是一个目录,而且它必须为空。(当我们说目录为空时,我们指这个目录里的项只有点和点点。)如果newname存在且为空时,它被删除,oldname被重命名为newname。额外地,当我们重命名一个目录时,newname不能包含以oldname为前缀的路径。比如,我们不能重命名/usr/foo为/usr/foo/testdir,因为旧名字(/usr/foo)是新名字的路径前缀,且不能被删除。

3、如果oldname或newname指向一个符号链接,链接本身被处理,而不是它解析的文件。

4、作为一个特殊例子,如果oldname和newname是同一个文件,那么函数成功返回但不改变任何东西。

如果newname已经存在,我们需要有删除它的权限。同样,因为我们正在删除oldname的目录项而且可能创建newname的目录项,我们需要包含旧文件所在目录和新文件所在目录的写权限和执行权限。

4.16 符号链接

一个符号链接是一个文件的间接指针,不像上节的硬链接--直接指向文件的i-node。符号链接用来解决硬链接的限制。

1、硬链接通常需要链接和文件处在同一个文件系统上。

2、只有超级用户才能创建目录的硬链接。

符号链接和它指向的文件没有文件系统的限制,任何人都可以创建一个目录的符号链接。符号链接被典型用来把一个文件或整个目录结构移到系统的另一个位置。

符号链接在4.2BSD上引入,之后被SVR4支持。

当使用通过名字表示文件的函数时,我们问题需要知道函数是否解析一个符号链接。如果函数解析一个符号链接,函数的路径名参数就表示符号链接指向的文件。否则,路径名参数表示符号链接本身,而不是链接指向的文件。下表总结了本章函数是否解析一个符号链接:

 
函数不解析符号链接解析符号链接
access *
chdir *
chmod *
chown**
creat *
exec *
lchown* 
link *
函数mkdir、mkfifo、mknod和rmdir不在这张表里,因为当路径名是一个符号链接时它们会返回一个错误。还有,接受文件描述符参数的函数,比如fstat和fchmod,也没有列出来,因为符号链接的处理已经被返回文件描述符的函数(通常是open)完成了。chown是否解析一个符号链接取决于实现。

在Linux的早期版本(在2.1.81之前),chown没有解析符号链接。从2.1.81之后,chown开始解析符号链接。在FreeBSD 5.2.1和Mac OS X 10.3上,chown解析符号链接。(4.4BSD之前,chown没有解析符号链接,但这在4.4BSD上得以改变。)在Solaris 9,chown同样解析符号链接。所有的这些平台都提供一个lchown的实现来改变符号链接本身的属主。

上表中的一个例外是当open函数在O_CREAT和O_EXCL同时被设置时被调用。在这种情况下,如果路径名是一个符号链接,open会失败,errno被设为EEXIST。这种行为是为了关闭安全漏洞以便有权限的进程不会被愚弄导致写入一个错误文件。

使用符号链接有可能把回路引进文件系统。当它发生时,多数查询路径名的函数返回一个ELOOP的errno。考虑以下命令:
$ mkdir foo
$ touch foo/a
$ ln -s ../foo foo/testdir
$ ls -l foo
总用量 0
-rw-r--r-- 1 tommy tommy 0 2012-02-22 17:21 a
lrwxrwxrwx 1 tommy tommy 6 2012-02-22 17:22 testdir -> ../foo
这些命令创建了一个目录foo,它包含文件a,还有一个指向foo的符号链接。这样便产生了一个回路。如果使用Solaris的ftw标准函数来遍历文件结构,打印所有碰到的路径名,输出为:
foo
foo/a
foo/testdir
foo/testdir/a
foo/testdir/testdir/a
foo/testdir/testdir
foo/testdir/testdir/testdir/a
(更多的行直到我们遇到ELOOP错误。)

在4.21节,我们会提供我们自己的ftw函数的版本,使用lstat而不是stat,来避免解析符号链接。

同样使用Linux上的tree命令可以看到:
$ tree -l foo
foo
├── a
└── testdir -> ../foo  [recursive, not followed]
在Linux上的ftw函数使用lstat,所以不会有上述行为发生。

这种格式的回路很容易删除。我们可以unlink文件foo/testdir,因为unlink不会解析一个符号链接。然后如果我们创建一个这种类型的回路的硬链接,它的删除将会困难的多。这是为什么除非进程有超级用户权限,否则link函数不会硬链接一个目录。

事实上,Rich Stevens(第一版的作者)在原版中写这节时在他自己的系统上作过实验。文件系统崩溃了,而且普通的fsck工具也不能修好。需要不被赞成使用的工具clri和dcheck来修复文件系统。

目录硬链接的需求很早之前就有了。有了符号链接和mkdir函数,任何不再需要创建目录的硬链接了。

当我们打开一个文件时,如果传入open的路径名是一个符号链接,open解析这个链接到指定的文件。如果指向的文件不存在,open返回错误表明它不能打开这个文件。这会使不熟悉符号链接的用户困惑。比如:
$ ln -s /no/such/file/ myfile
$ ls myfile
myfile
$ cat myfile
cat: myfile: 没有那个文件或目录
$ ls -l myfile
lrwxrwxrwx 1 tommy tommy 14 2012-02-22 17:45 myfile -> /no/such/file/

文件myfle确实存在,然而cat却说没有这个文件,因为myfile是一个符号链接而它指向的文件不存在。ls的-l选项给了我们两个提示:第一个字符是l,意思是符号链接,而符号->同样表示它是个符号链接。ls命令有另一个选项(-F),可以在符号链接的文件名后加上一个符号@,可以在不使用-l选项的时候帮助我们标记一个符号链接:
ls -F myfile
myfile@

4.17 symlink和readlink函数

symlink函数可以用来创建符号链接。

#include <unistd.h>

int symlink(const char *actualpath, const char *sympath);

成功返回0,错误返回-1。

一个新的目录项sympath会被创建指向actualpath。当符号链接被创建时,并不需要actualpath存在。(在前一节的末尾的例子里我们已经看到这种情况了。)还有,actualpath和sympath不需要在同一个文件系统上。

因为open函数解析一个符号链接,我们需要一个打开链接本身以及读取链接里的名字的方法。readlink函数做这这件事情。

#include <unistd.h>

ssize_t readlink(const char* restrict pathname, char *restrict buf, size_t bufsize);

成功返回字节数,失败返回-1。

这个函数合并了open、read和close的操作。如果函数成功,它返回buf里的字节数。返回到buf的符号链接的内容不以null终止。

4.18 文件时间(File Times)

每个文件维护了三个时间域。下表总结了它们的目的:

与每个文件相关的三个时间域
描述例子ls 选项
st_atime文件数据的最后访问时间read-u
st_mtime文件数据的最后修改时间write默认
st_ctimei-node状态的最后改变时间chmod, chown-c

注意修改时间(st_mtime)和改变状态时间(st_ctime)的区别。修改时间是文件内容最后被修改的时间。状态改变时间是文件的i-node最后被修改的时间。在本章,我们已经描述过许多影响i-node而不改变文件真实内容的操作:改变文件的访问权限、改变用户ID、改变链接数、等等。因为i-node的信息与文件真实内容分开存放,所以我们在修改时间之外,需要状态改变时间。

注意系统没有为一个i-node维护最后访问时间。这是为什么函数access以及stat没有改变这三个时间的任一个的原因。

访问时间通常被系统管理员使用来删除有一定时间没有被访问过的文件。典型的例子是删除上星期没被访问过的名为a.out和core的文件。find命令经常被作为这种类型的操作。

修改时间和状态改变时间可以用来存档那些内容或i-node被修改过的文件。

ls命令显示或排序这三个时间值中的一个。当使用-l或-t选项时,它默认使用文件的修改时间。-u选项使它使用访问时间,而-c选项令它使用状态改变时间。

下表总结了我们描述过的操作这三个时间的各种函数。回想在4.14节,一个目录只是一个简单地包含目录项(文件名和相关的i-node号)的文件。增加、删除或修改这些目录项会影响目录相关的三个时间。这也是为什么下表包含了一行来展示文件或目录的三个时间,和另一行展示父目录相关的三个时间。例如,创建一个新文件影响包含该新建文件的目录,也会影响这个新文件的i-node。然而,读或写一个文件仅影响文件的i-node而不影响目录。(mkdir和rmdir函数在4.20节介绍。utime函数在下节介绍。6个exec函数在8.10节介绍。mkfifo和pipe函数在第15章介绍。)

各种函数对访问时间、修改时间和状态改变时间的影响
函数引用的文件或目录引用文件或目录的父目录章节注释
amcamc
chmod, fchmod  *   4.9 
chown, fchown  *   4.11 
creat*** **3.4O_CREAT新文件
creat **   3.4O_TRUNC已有文件
exec*     8.10 
lchown  *   4.11 
link  * **4.15第二个参数的父目录
mkdir*** **4.20 
mkfifo*** **15.5 
open*** **3.3O_CREAT新文件
open **   3.3O_TRUNC已有文件
pipe***   15.2 
read*     3.7 
remove  * **4.15删除文件=unlink
remove    **4.15删除目录=rmdir
rename  * **4.15两个参数的时间
rmdir    **4.20 
truncate, ftruncate **   4.13 
unlink  * **4.15 
utime***   4.19 
write **   3.8 

4.19 utime函数

文件的访问时间和修改时间可以用utime函数改变。

#include <utime.h>

int utime(const char *pathname, const struct utimebuf *times);

成功返回0,失败返回-1。

这个函数使用的结构体为:
struct utimbuf {
  time_t actime; /* 访问时间 */
  time_t modtime; /* 修改时间 */

};

在这个结构体里的两个时间值是日期时间,正如1.10节描述的那样,是从Epoch开始的秒数。

这个函数的操作和执行它所需的权限,取决于时间参数时否为NULL。

1、如果times为null指针,访问时间和修改时间都被设为当前时间。要这样做的话,进程的有效有用ID必须和文件的属主ID相同,或者进程必须有这个文件的写权限。2

2、如果times是一个非空指针,访问时间和修改时间设为times指向的结构体的值。在这种情况下,进程的有效用户ID必须和文件的属主ID相同,或者进程必须是一个超级用户进程。仅有文件的写权限是不够的。

注意我们不能为状态改变时间指定一个值,st_ctime--i-node最近修改的值--因为这个域utime函数被调用时会被自动更新。

UNIX系统的一些实现,touch命令使用了这个函数。还有,标准存档程序,tar和cpio,选择性地调用utime来把文件的时间设为文件被存档的时间。

下面的程序使用O_TRUNC选项的open函数把文件裁切成0长度,但不改变它们的访问或修改时间。要这样做,程序首先通过stat函数得到时间,接着裁切文件,然后用utime函数重设时间:

#include <fcntl.h>
#include <utime.h>

int
main(int argc, char *argv[])
{
    int i, fd;
    struct stat statbuf;
    struct utimbuf timebuf;

    for (i = 1; i < argc; i++) {
        if (stat(argv[i], &statbuf) < 0) {
            printf("%s: stat error\n", argv[i]);
            continue;
        }
        if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) {
            printf("%s: open error\n", argv[i]);
            continue;
        }
        close(fd);
        timebuf.actime = statbuf.st_atime;
        timebuf.modtime = statbuf.st_mtime;
        if (utime(argv[i], &timebuf) < 0) {
            printf("%s: utime error\n", argv[i]);
            continue;
        }
    }
    exit(0);
}
$ ls -l tmp
-rw-rw-r-- 1 tommy tommy 896467 2012-02-22 20:35 tmp
$ ls -lu tmp
-rw-rw-r-- 1 tommy tommy 896467 2012-02-22 20:29 tmp
$ ls -lc tmp
-rw-rw-r-- 1 tommy tommy 896467 2012-02-22 20:35 tmp
$ date
2012年 02月 22日 星期三 20:37:29 CST
$ ./a.out tmp
$ ls -l tmp
-rw-rw-r-- 1 tommy tommy 0 2012-02-22 20:35 tmp
$ ls -lu tmp
-rw-rw-r-- 1 tommy tommy 0 2012-02-22 20:29 tmp
$ ls -lc tmp
-rw-rw-r-- 1 tommy tommy 0 2012-02-22 20:37 tmp
正如我们预期的一样,最后修改时间和最后访问时间都没有改变。然而,状态改变时间改成了程序运行的时间。

4.20 mkdir和rmdir函数

目录通过mkdir函数来创建,通过rmdir函数删除。

#include <sys/stat.h>

int mkdir(const char *pathname, mode_t mode);

成功返回0,失败返回-1。

这个函数创建一个新的,空的目录。点和点点的目录项被自动创建。指定的文件访问权限,mode,被进程的文件模式创建掩码所修改。

一个常见的错误是指定一个和文件一样的mode:只是读和写权限。然而对于目录而言,我们通常想要至少一个执行位被启用,以便访问目录的文件名。

新目录的用户ID和组ID根据我们在4.6节描述的规则建立。

Solaris 9和Linux 2.4.22也让新目录继承父目录的set-group-ID位,以便新目录里创建的文件会继承那个目录的组ID。在Linux上,文件系统实现决定这是否被支持。例如,ext2和ext3文件系统允许这个行为被mount命令的一个选项控制。然而,在UFS文件系统的Linux实现上,这个行为是不可先的,它继承设置组ID位来效仿历史的BSD实现,在那目录的组ID从父目录继承而来。

基于BSD的实现不传播设置组ID位,它们简单地继承组ID作为一种策略。因为FreeBSD和Mac OS X 10.3是基于4.4BSD的,它们并不要求继承设置组ID位。在这些平台上,新创建的文件和目录总是从父目录继承组ID,而不管设置组ID位。

UNIX系统的早期版本没有mkdir函数。它在4.2BSD和SVR3中被引入。在早期版本,一个进程必须调用mknod函数来创建一个新的目录。然而mknod函数仅限于超级用户进程使用。为了绕过这个限制,创建目录的通用命令,mkdir,必须被root拥有,且要设置设置用户ID位。为了从一个进程里创建一个目录,mkdir命令必须通过system函数调用。

一个空的目录用rmdir函数删除。回想下一个空的目录是只包含点和点点目录项的目录。

#include <unistd.h>

int rmdir(const char *pathname);

成功返回0,失败返回-1。

如果调用这个函数后目录的链接数变为0,而且没有其它进程打开这个目录,那么目录开辟的空间会被释放掉。如果当链接数达到0时有一个或多个进程打开了这个目录,则函数返回前最后的链接会被删除,而且点和点点项也会被删除。另外,这个目录里不能再创建新的文件。然而,目录没有被释放,最后一个进程关闭它。(即使有些其它进程打开了这个目录,它也不可能在这个目录里做很多事,因为目录必须为空rmdir函数才能成功。

4.21 读目录(Reading Directories)

目录可以被任何有这个目录读权限的人读。然而只有内核才能写一个目录,来保证文件系统的健全。回想4.5节目录的写权限位和执行权限位决定我们是否可以在该目录创建和删除文件--他们并没有指定我们是否可以写这个目录本身。

目录的真实格式取决于UNIS系统实现和文件系统的设计。早期系统,比如Version 7,有一个简单的结构:每个目录项有16字节,其中14字节为文件名,而2个字节为i-node号。当更长的文件名被加入到4.2BSD时,每个项变为可变长度,这意味着任何读目录的程序都依赖于系统。为了简化这个,一堆目录指令被发展起来,并成为POSIX.1的部分。许多系统避免程序使用read函数来访问目录的内容,因此更加隔离了程序和目录格式的实现相关的细节。

#include <dirent.h>

DIR *opendir(const char *pathname);
成功返回指针,失败返回null。

struct dirent *readdir(DIR *dp);
成功返回指针,目录尾或失败返回null。

void rewinddir(DIR *dp);
int closedir(DIR *dp);
成功返回0,失败返回-1。

long telldir(DIR *dp);
返回dp相关的目录的当前位置。

void seekdir(DIR *dp, long loc);

telldir和seekdir函数不是基本POSIX.1标准的一部分,它们是SUS的XSI扩展,所以所有遵守协议的UNIX系统实现都被期望来提供它们。

看下面的代码,ls的简单实现:

#include <dirent.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    DIR *dp;
    struct dirent *dirp;

    if (argc != 2)
        exit(1);

    if ((dp = opendir(argv[1])) == NULL)
        exit(1);
    while ((dirp = readdir(dp)) != NULL)
        printf("%s\n", dirp->d_name);
    closedir(dp);
    exit(0);
}
在文件<dirent.h>里定义的dirent结构体依赖于系统实现。实现定义这个结构体至少要包括以下两个成员:
struct dirent {
  ino_t d_ino;    /* i-node号 */
  char d_name[NAME_MAX + 1]; /* null结尾的文件名 */
};

d_ino项不在POSIX.1中定义,因为它是一个实现特性,但它定义在POSIX.1的XSI扩展中。POSIX.1只在这个结构体里定义了d_name项。

注意NAME_MAX在Solaris上不是一个定义好的常量--它的值取决于目录所在的文件系统,而且它的值通常是从fpathconf函数得到。NAME_MAX的一个普遍的值是255。(回想下第2节关于限量的表。)尽管如此,既然文件名是null终止的,数组d_name在头文件里怎么定义并不重要,因为数据尺寸并不表明文件名的长度。

DIR结构体是一个内部结构,被那六个函数用来维护关于被读目录的信息。DIR结构体的作用与标准I/O库维护的FILE结构体类似,我们会在第5章讲述。

从opendir返回的DIR结构体的指针被其它5个函数使用。opendir函数初始化所有事情,以便第一个readdir操作读到目录的第一项。在目录里的项的顺序是实现相关的,而且通常不是字母序。

我们将使用这些目录函数来写一个遍历文件层次的程序。目标是产生各种类型的文件的计数,就如4.3节末的表一样。程序接受一个参数--开始的路径名--并从那一点开始递归地遍历层次结构。Solaris提供了一个函数,ftw,来进行层次结构的真正遍历,并为每个文件调用用户定义的函数。这个函数的问题是它为每个文件调用stat函数,这会导致程序去解析符号链接。例如,如果我们从根目录开始,并有一个指向/usr/lib的名为/lib的符号链接,/usr/lib目录里的所有文件都会被计数两次。为了更正它,Solaris提供了一个额外的函数,nftw,带有一个阻止解析符号链接的可选项。尽管我们可以使用ntfw,但我们仍将写我们自己的简单的文件遍历器来展示目录函数的用法。

在SUS,ftw和nftw都包含在基本POSIX.1规范的XSI扩展中。Solaris 9和Linux 2.4.22包含了实现。基于BSD的系统有一个不同的函数,fts,提供了相似的功能。它在FreeBSD 5.2.1、Mac OS X 10.3和Linux 2.4.22中可用。

看下面的代码:

#include <dirent.h>
#include <limits.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

/* function type that is called for each filename */
typedef int Myfunc(const char *, const struct stat *, int);

static Myfunc myfunc;
static int myftw(char *, Myfunc *);
static int dopath(Myfunc *);
char *path_alloc(int *sizep); /* from section 2.5.5 */

static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot;

int 
main(int argc, char *argv[])
{
    int ret;

    if (argc != 2)
        exit(1);

    ret = myftw(argv[1], myfunc); /* does it all */

    ntot = nreg + ndir + nblk + nchr + nfifo + nslink + nsock;
    if (ntot == 0)
        ntot = 1; /* avoid divide by 0; print 0 for all counts */
    printf("regular files  = %71d, %5.2f %%\n", nreg, nreg*100.0/ntot);
    printf("directories    = %71d, %5.2f %%\n", ndir, ndir*100.0/ntot);
    printf("block special  = %71d, %5.2f %%\n", nblk, nblk*100.0/ntot);
    printf("char special   = %71d, %5.2f %%\n", nchr, nchr*100.0/ntot);
    printf("FIFOs          = %71d, %5.2f %%\n", nfifo, nfifo*100.0/ntot);
    printf("symbolic links = %71d, %5.2f %%\n", nslink, nslink*100.0/ntot);
    printf("sockets        = %71d, %5.2f %%\n", nsock, nsock*100.0/ntot);

    exit(ret);
}

/*
 * Descend through the hierarchy, starting at "pathname".
 * The caller's func() is called for every file.
 */
#define FTW_F 1     /* file other than directory */
#define FTW_D 2     /* directory */
#define FTW_DNR 3   /* directory that can't be read */
#define FTW_NS 4    /* file that we can't stat */

static char *fullpath;  /*contains full pathname for every file */

static int      /* we return whatever func() returns */
myftw(char *pathname, Myfunc *func)
{
    int len;
    fullpath = path_alloc(&len);        /* malloc's for PATH_MAX+1 bytes */
    strncpy(fullpath, pathname, len);   /* protect against */
    fullpath[len -1] = 0;               /* buffer overrun */

    return(dopath(func));
}

/*
 * Descend through the hierarchy, starting at "fullpath".
 * If "fullpath" is anything other than a directory, we lstat() it,
 * call func(), and return. For a directory, we call ourself
 * recursively for each name in the directory.
 */
static int  /* we return whatever func() returns */
dopath(Myfunc* func)
{
    struct stat     statbuf;
    struct dirent   *dirp;
    DIR             *dp;
    int             ret;
    char            *ptr;

    if (lstat(fullpath, &statbuf) < 0)  /* stat error */
        return(func(fullpath, &statbuf, FTW_NS));
    if (S_ISDIR(statbuf.st_mode) == 0)  /* not a directory */
        return(func(fullpath, &statbuf, FTW_F));

    /*
     * It's a directory. First call func() for the directory,
     * then process each filename in the directory.
     */
    if ((ret = func(fullpath, &statbuf, FTW_D)) != 0)
        return(ret);

    ptr = fullpath + strlen(fullpath); /*point to end of fullpath */
    *ptr++ = '/';
    *ptr = 0;

    if ((dp = opendir(fullpath)) == NULL) /* can't read directory */
        return(func(fullpath, &statbuf, FTW_DNR));

    while ((dirp = readdir(dp)) != NULL) {
        if (strcmp(dirp->d_name, ".") == 0 ||
            strcmp(dirp->d_name, "..") == 0)
                continue;    /*ignore dot and dot-dot */
    
        strcpy(ptr, dirp->d_name); /* append name after slash */
    
        if ((ret = dopath(func)) != 0)
            break; /* time to leave */
    }
    ptr[-1] = 0; /* erase everything from slash onwards */
    
    if (closedir(dp) < 0) {
        printf("can't close directory %s", fullpath);
        return -1;
    }

    return ret;
}

static int
myfunc(const char *pathname, const struct stat *statptr, int type)
{
    switch (type) {
    case FTW_F:
        switch (statptr->st_mode & S_IFMT) {
        case S_IFREG:   nreg++; break;
        case S_IFBLK:   nblk++; break;
        case S_IFCHR:   nchr++; break;
        case S_IFIFO:   nfifo++;break;
        case S_IFLNK:   nslink++;   break;
        case S_IFSOCK:  nsock++;    break;
        case S_IFDIR:
            printf("for S_IFDIR for %s", pathname);
                /* directories should have type = FTW_D */
        }
    break;

    case FTW_D:
        ndir++;
        break;

    case FTW_DNR:
        printf("can't read directory %s", pathname);
        break;
    
    case FTW_NS:
        printf("stat error for %s", pathname);
        break;
    
    default:
        printf("unknown type %d for pathname %s", type, pathname);
    }
    return(0);
}

#ifdef PATH_MAX
static int pathmax = PATH_MAX;
#else
static int pathmax = 0;
#endif

#define SUSV3 200112L

static long posix_version = 0;

/* If MATH_MAX is indeterminate, no guarantee this is adquate */
#define PATH_MAX_GUESS 1024

char *
path_alloc(int *sizep) /* also return allocated size, if nonnull */
{
    char *ptr;
    int size;

    if (posix_version == 0)
        posix_version = sysconf(_SC_VERSION);

    if (pathmax == 0) { /*first time through */
        errno = 0;
        if ((pathmax = pathconf("/", _PC_PATH_MAX)) < 0) {
            if (errno == 0)
                pathmax = PATH_MAX_GUESS; /* it's indeterminate */
            else
                printf("pathconf error for _PC_PATH_MAX\n");
        } else {
            pathmax++; /* add one since it's relative to root */
        }
    }
    if (posix_version < SUSV3)
        size = pathmax + 1;
    else
        size = pathmax;

    if ((ptr = malloc(size)) == NULL)
        printf("malloc error for pathname\n");

    if (sizep != NULL)
        *sizep = size;
    return(ptr);
}
运行./a.out /dev,结果为:
regular files  =   283, 32.49 %
directories    =   75,  8.61 %
block special  = 29,  3.33 %
char special   =  196, 22.50 %
FIFOs          =      1,  0.11 %
symbolic links = 285, 32.72 %
sockets        =    2,  0.23 %

更多关于遍历文件系统和许多标准UNIX系统命令(find、ls、tar等)里这种技术的使用,参考Fowler, Korn, and Vo[1989]。

4.22 chdir、fchdir和getcwd函数

每个进程都有一个当前工作目录。这个目录是所有相对路径(不以斜杠开头的路径)开始查找的地方。当一个用户登录一个UNIX系统时,当前工作目录一般从文件/etc/passwd里的第6个域指定的目录--用户的主目录--开始。当前工作路径是进程的一个属性,主目录是登录名的一个属性。

我们可以用chdir和fchdir函数来改变进程的当前路径。

#include <unistd.h>

int chdir(const char *pathname);

int fchdir(int filedes);

成功返回0,失败返回-1。

我们可以用路径名或文件描述符来指定一个新的当前工作目录。

fchdir函数不是基本POSIX.1规范的一部分,它是SUS的一个XSI扩展。本文讨论的4个平台都支持fchdir。

下面的代码把当前工作目录改为“/tmp":

#include <unistd.h>

int
main(void)
{
    if (chdir("/tmp") < 0) {
        printf("chdir failed\n");
        exit(1);
    }
    printf("chdir to /tmp succeeded\n");
    exit(0);
}
运行结果为:
$ pwd
/home/tommy/code/c/unix
$ ./a.out
chdir to /tmp succeeded
$ pwd
/home/tommy/code/c/unix
shell的当前工作目录之所以没有改变,是因为当前工作目录是进程的一个属性,进程改变当前工作目录不能影响到其它进程,即使是其父进程。(我们会在第8章更深入讨论进程间的关系。)要改变shell的的当前工作目录,可以使用其内置的cd命令。

因为内核必须维护当前工作目录的信息,我们应该可以得到它的当前值。不幸的是,内核并不维护这个目录的完全路径,而是关于目录的信息,比如指向目录v-node的指针。

我们需要的是一个从当前工作目录(点)开始,并使用点点往上遍历其层次的函数。每进入上一层目录时,函数读取它的目录项,直到找到对应于进入该目录前的目录的i-node的名字。重复这个过程,直到到达要目录,从而得到当前工作目录的整个绝对路径。幸运的是,已经有一个函数来为我们完成这个任务了。

#include <unistd.h>

char *getcw(char *buf, size_t size);

成功返回buf,错误返回NULL。

我们必须给这个函数传递一个缓冲区,buf,的地址,以及它的(字节)大小。这个缓冲区必须足够大以容下绝对路径名加一个终止null字节,不然会返回错误。(回想下2.5.5节关于为最大尺寸的路径名分配空间的讨论。)

一些getcwd的早期实现允许第一个参数为NULL。这种情况下,函数调用malloc来动态分配size大小的字节。这不是POSIX.1的部分,也不是SUS的部分,所以应该避免使用。

看下面的代码:

#include <unistd.h>

char *path_calloc(int *);

int
main(void)
{
    char *ptr;
    int size;

    if (chdir("/var/spool/mail") < 0) {
        printf("chdir failed\n");
        exit(1);
    }

    ptr = path_alloc(&size); /* our own funciton */
    if (getcwd(ptr, size) == NULL) {
        printf("getcwd failed\n");
        exit(1);
    }

    printf("cwd = %s\n", ptr);
    exit(0);
}
$ ./a.out
cwd = /var/mail
$ ls -l /var/spool/mail
lrwxrwxrwx 1 root root 7 2011-10-12 16:16 /var/spool/mail -> ../mail
可以看到chdir解析了符号链接,正如我们期望的。getcwd在往上查找的过程中,并不知道/var/mail是一个符号链接指向的目录。这是符号链接的特性。

当我们有一个需要返回到原来的文件系统的位置的程序时,getcwd函数有很有用的。我们可以在改变我们的工作目录之前调用getcwd来保存开始位置。在完成我们的操作后,我们可以把从getcwd得到的路径名传给chdir来返回到我们在文件系统的开始的位置。

fchdir函数提供我们完成这个任务的一个简单方法。不需要使用getcwd,我们可以在进入文件系统别的位置前用open当前目录并保存文件描述符。当我们想要返回我们开始的位置时,我们可以简单地把描述符会给fchdir。

4.23 设备特殊文件(Device Special Files)

st_dev和st_rdev这两个域经常令人困惑。我们将在18.9节写ttyname函数时需要这些域。规则很简单:

1、每个文件系统都有它的主要的和次要的设备号,它们有原始系统数据类型dev_t编码。主设备号标识了设备驱动,有时也编码为与其交流的外设主板,从设备号标识了指定的从设备。回想下4.14节关于文件系统的几个图,硬盘经常包含几个文件系统。在同一个硬盘上的每个文件系统通常有相同的主设备号,但是有不同的从设备号。

2、我们通常可以使用多数实现定义的两个宏:major和minor,来访问主从设备号。这意味着我们不必知道在dev_t对象的两个号是如何存储的。早期系统把设备号存储为16位整型,8位为主设备号,24位为从设备号。FreeBSD 5.2.1和Mac OS X 10.3用一个32位整型表示dev_t,14位为主设备号而18位为从设备号。在64位系统上,Solaris 9用64位整型表示dev_t,两者各32位。在Linux 2.4.22,尽管dev_t是一个64位整型,目前主从设备号都只有8位。

POSIX.1指出dev_t类型的存在,但并没有定义它包含的内容,以及如何获取它的内容。宏major和minor被多数实现所定义。它们定义的头文件也取决于实现。它们可以在基于BSD的系统的<sys/types.h>里找到。Solaris把它们定义在<sys/mkdev.h>。Linux把这些宏定义在<sys/sysmacros.h>,它被<sys/types.h>所引用。

3、系统上的每个文件名的st_dev的值是包含这个文件和它对应的i-node的文件系统的设备号。

4、只有字符特殊文件和块特殊文件才有一个st_rdev值。这个值包含了真实设备的设备号。

看下面的代码:

#include <sys/types.h>
#include <sys/stat.h>

int
main(int argc, char *argv[])
{
    int     i;
    struct stat buf;

    for (i = 1; i < argc; i++) {
        printf("%s: ", argv[i]);
        if (stat(argv[i], &buf) < 0) {
            printf("stat error");
            continue;
        }

        printf("dev = %d/%d", major(buf.st_dev), minor(buf.st_dev));
        if (S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode)) {
            printf(" (%s) rdev = %d%d",
                (S_ISCHR(buf.st_mode)) ? "character" : "block",
                major(buf.st_rdev), minor(buf.st_rdev));
        }
        printf("\n");
    }
    exit(0);
}
$ ./a.out / /home/tommy /dev/tty[01]
/: dev = 8/1
/home/tommy: dev = 8/1
/dev/tty0: dev = 0/5 (character) rdev = 40
/dev/tty1: dev = 0/5 (character) rdev = 41

查看当前mount的设备,以及对应的目录
$ mount
/dev/sda1 on / type ext4 (rw,errors=remount-ro,user_xattr,commit=0)

通过ls来查看它们的设备号
$ ls -lL /dev/tty[01] /dev/sda[01]
brw-rw---- 1 root disk 8, 1 2012-02-23 14:36 /dev/sda1
crw--w---- 1 root tty  4, 0 2012-02-23 14:36 /dev/tty0
crw------- 1 root root 4, 1 2012-02-23 14:36 /dev/tty1
硬盘设备是块特殊文件,而两个终端设备是字符特殊文件。(通常,块特殊文件的设备唯一的类型是那么可以包含随机访问(random-access)文件系统:硬盘、软盘、CD-ROM等等。UNIX系统的一些早期版本支持磁带的文件系统,但这已经不再广泛使用了。)

注意两个终端设备(st_dev)的文件名和i-node是在设备0/5--实现/dev的devfs伪文件系统--然而它们真实的设备号是4/0和4/1。

4.24 文件访问权限位汇总(Summary of File Access Permission Bits)

我们已经完全介绍了所有的文件访问权限位,其中一些有多种功能。下表对它们进行了汇总,并给出当它们应用于普通文件或目录时的作用。

文件访问权限位汇总
常量描述对普通文件的影响对目录的影响
S_ISUID设置用户ID在执行时设置有效用户ID(没有使用)
S_ISGID设置组ID如果组执行被设置的话那么在执行时设置有效组ID;
否则启用强制记录锁(如果支持的话)
设置目录的新文件的组ID为目录的组ID
S_ISVTX粘滞位控制目录内容的缓存(如果支持的话)限制目录里文件的删除与重命名
S_IRUSR用户读用户读文件的权限用户读目录项的权限
S_IWUSR用户写用户写文件的权限用户在目录里删除和创建文件的权限
S_IXUSR用户执行用户执行文件的权限用户在目录中查找给定路径名的权限
S_IRGRP组读组读文件的权限组读目录项的权限
S_IWGRP组写组写文件的权限组在目录里删除和创建文件的权限
S_IXGRP组执行组执行文件的权限组在目录中查找给定路径名的权限
S_IROTH其他人读其他人读文件的权限其他人读目录项的权限
S_IWOTH其他人写其他人写文件的权限其他人在目录里删除和创建文件的权限
S_IXOTH其他人执行其他人执行文件的权限其他人在目录中查找给定路径名的权限

这最后的9个常量也可以分为三组,如:
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR
S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP
S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH


第五章 标准I/O库(Standard I/O Library)

5.1 引言

本单说明标准I/O库。因数不仅在UNIX上,还有很多操作系统都实现了此库,所以它ISO C标准说明。SUS对ISO C标准进行了扩展,定义了另外一些接口。

标准I/O库处理很多细节,比如缓冲区分配,以优化的长度执行I/O等。这些处理使用户不必担心如何选择使用正确的块长度(如3.9节所述)。这使得它便于用户使用,然而,如果不较深入地了角I/O库函数的操作,也会带一些问题。

标准I/O库是由Dennis Ritchie在1975年左右编写,是由Mike Lesk编写的可移植I/O库的主要修改版本。令人惊讶的是,经过30年后,标准I/O库只做过极小的修改。

5.2 流和FILE对象(Streams and FILE Objects)

在第三章,所有的I/O函数都集中在文件描述符。当一个文件被打开时,会返回一个文件描述符,然后这个描述符会被所有的后续I/O操作使用。对于标准I/O库,讨论主要集中在流(stream)。(不要把标准I/O的术语“流”和作为System V的一部分的STREAMS I/O系统相混淆,后者在SUS的XSI STREAMS选项里被标准化。)当我们用标准I/O库打开或创建一个文件时,我们说我们有一这个文件的关联的流。

根据ASCII字符集,一个单一的字符由一个字节表示。根据国际字符集,一个字符可以由一个以上的字节来表示。标准I/O文件流可以用来操作单字节和多字节(宽,wide)字符集。一个流的方向(orientation)决定了字符是以单字节还是多字节的方式读取。初始地,当一个流被创建时,它没有方向。如果一个多字节I/O函数(参考<wchar.h>)用在了没有方向的流上,那么流的方向会设为面向宽字符的。如果一个字节I/O函数用在一个没有方向的流上,那么流的方向会设为面向字节的。只有两个函数可以在设置后改变这个方向。freopen函数(简单说明)将会清除一个流的方向,而fwide函数用来设置一个流的方向。

#include <stdio.h>

#include <wchar.h>

int fwide(FILE *fp, int mode);

流面向宽字符返回正值、面向字节返回负值、没有方向则返回0。

fwdie函数根据mode参数的值,执行不同的任务:

1、如果mode参数为负值,fwide将会尝试让流面向字节;

2、如果mode参数为正值,fwide将会尝试让流面向宽字符;

3、如果mode参数为0,fwid将不会设置方向,而是返回一个表示流的方向的值。

注意fwide不会改变一个已经有方向的流的方向。同样注意它不会返回错误。考虑如果流无效时会发生什么。我们仅有的资源是在调用fwide之前清除errno,然后在它返回时查看errno的值。在本文的剩余部分,我们将只处理面向字节的流。

当我们打开一个流时,标准I/O函数fopen返回一个指向一个FILE对象的指针。这个对象通常是一个包含所有标准I/O库管理流所需的所有信息的结构体。它包含:真实I/O使用的文件描述符、指向流的缓冲区的指针、缓冲区的大小、缓冲区当前字符数的计数器、一个错误标志,等等。

应用软件不应该需要检查FILE对象。为了引用流,我们把FILE指针作为一个参数传给每个标准I/O函数。在本文,我们将用一个FILE对象的指针(FILE *)作为文件指针。

在本章,我们在UNIX系统的上下文讲述标准I/O库。正如我们提到的那样,这个库已经广泛移植到其它操作系统上。然而为了展示这个库是怎么实现的,我们将讨论它在UNIX系统上的典型实现。

5.3 标准输入、标准输出、以及标准错误(Standard Input, Standard Output, and Standard Error)

预定义并对一个进程可用的三个流是:标准输入、标准输出和标准错误。这些流和文件描述符:STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO指向同样的文件,我们在3.2节已经提过。

这三个标准I/O流被预定义的文件指针stdin、stdout和stderr引用。这些文件指针定义在<stdio.h>头文件里。

5.4 缓冲(Buffering)

标准I/O库提供的缓冲的目的是使用最少的read和write调用。(回想第3章我们展示了使用各种缓冲的尺寸执行I/O所需要的CPU时间。)此外,它尝试为每个I/O流进行缓冲,消除程序担心它的需要。不幸的是,标准I/O库产生最多混淆的就是缓冲。

有三种缓冲类型被提供:

1、完全缓冲(Fully buffered)在这种情况下,当标准I/O的缓冲被填满后,真实的I/O才会发生。在磁盘上的文件通常都被标准I/O库完全缓冲。这个被使用的缓冲通常被标准I/O库在第一次操作一个流时通过调用malloc得到(7.8节)。

术语“冲洗(flush)”描述了标准I/O缓冲的写操作。一个缓冲可以被标准I/O函数自动冲洗,比如在缓冲满时,或者我们可以调用函数fflush来冲洗一个流。不幸的是,在UNIX环境里,冲洗有两个意思。在标准I/O库里,它表示缓冲的内容写出,它可能是部分填充的。在终端设备的术语里,比如18章的tcflush函数,它表示丢弃缓冲里存储的数据。

2、行缓冲(Line buffered)。在这种情况下,标准I/O库在输入输出时碰到一个换行符时会执行I/O操作。这让我们可以一次输出单个字符(使用标准I/O的fputc函数),因为我们知道只当我们完成了每行的写操作时真实I/O才会发生。行缓冲被典型用在终端的流上:比如标准输入、标准输出。

行缓冲伴随两个警告。第一,标准I/O库用来收集各行的缓冲区大小是固定的,所以当我们在写一个换行符之前填满这个缓冲I/O可能会发生。第二、不管何时通过标准I/O库从a、一个未缓冲流或b、(需要从内核被请求数据的)一个行缓冲流请求一个输入时,所有行缓冲输出流都会被冲洗。b情况括号内的补充条件的原因是,请求的数据可能已经在缓冲里了,这时不再需要从内核中读取数据。显然,a情况下任何从未缓冲流的输入,都会要求从内核获取数据。

3、无缓冲(Unbuffered)。标准I/O库不缓冲字符。例如,如果我们用标准I/O函数fputs写15个字符,我们会期望这15个字符尽快地输出,它很可能使用了3.8节的write函数。

例如,标准错误流通常是无缓冲的。这是为了使任何错误消息都能尽快地显示,而不管它们是否包含一个换行符。

ISO C定义了以下缓冲性质:
1、当且仅当它们没有指向交互式的设备,标准输入和标准输出为完全缓冲的。

2、标准错误绝不是完全缓冲的。

然而,这里没有告诉我们如果标准输入和标准输出表示交互式的设备时,它们是无缓冲的还是行缓冲的;又或者标准错误是无缓冲的还是行缓冲的。多数实现默认使用以下的缓冲类型:
1、标准错误总是无缓冲的。

2、所有其它流,如果它们指向一个终端设备,都是行缓冲的;否则,它们是完全缓冲的。

本文讨论的四个平台遵守了这些协定:标准错误是无缓冲的,为终端设备打开的流是行缓冲的,而所有其它流是完全缓冲的。

我们会在5.12节更详细地介绍标准I/O的缓冲。

如果我们不喜欢任何给定流的这些默认行为,我们可以通过调用下面两个函数的其中一个来改变缓冲:

#include <stdio.h>

void setbuf(FILE *restrict fp, char *restrict buf);

void setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);

成功返回0,失败返回非0值。

这些函数必须在流打开后(显然,因为它们第一个参数都是一个合法文件指针),但在这个流上执行任何操作之前调用。

通过setbuf,我们可以打开或关闭缓冲。要开启缓冲,buf必须是指向长度为BUFSIZ的缓冲的指针,这个常量定义在<stdio.h>里。通常,这个流是完全缓冲的,但是如果这个流与一个终端设备关联的话,一些系统可能会设置成行缓冲。要禁用缓冲,我们把buf设置为NULL。

通过setvbuf,我们精确地指定我们想要的缓冲类型。这通过mode参数来完成:
_IOFBF:完全缓冲的;
_IOLBF:行缓冲的;
_IONBF:无缓冲的。

如果我们指定一个无缓冲的流,buf和size参数会被忽略。如果我们指定完全缓冲或行缓冲,buf和size可以选择性地指定一个缓冲和它的尺寸。如果流是缓冲的而buf是NULL,标准I/O库会自动使用恰当的尺寸为这个流开辟它自己的缓冲。恰当的尺寸表示由BUFSIZ常量指定的值。

一些C库的实现使用stat结构体的st_blksize成员的值(参考4.2节)来决定优化的标准I/O缓冲大小。我们将在本章的后面部分看到,GNU C库使用了这种方法。

下表总结了这两个函数的行为以及它们的各种选项:

setbuf和setvbuf函数汇总
函数modebuf缓冲和长度缓冲类型
setbuf 非null长度为BUFSIZ的用户buf完全缓冲或行缓冲
NULL(没有缓冲)无缓冲的
setvbuf_IOFBF非null长度为size的用户buf完全缓冲
NULL合适长度的系统缓冲
_IOLBF非null长度为size的用户buf行缓冲
NULL合适长度的系统缓冲
_IONBF(忽略)(没有缓冲)无缓冲的

注意如果我们在一个函数里用一个自动变量(automatic variable)开辟一个标准I/O的缓冲作,我们必须在从函数返回前关闭这个流。(我们将在7.8节更多讨论。)还有,一些实现为内部记账(internal bookkeeping)而使用这个缓冲的一部分,所以可以存储在这个缓冲的数据的真实字节数要比它的尺寸小。一般来说,我们应该让系统选择缓冲尺寸并自动地开避这个缓冲。当我们这样做时,在我们关闭这个流时,标准I/O库自动地释放这个缓冲。

任何时候,我们可以强制一个流的冲洗。

#include <stdio.h>

int fflush(FILE *fp);

成功返回0,失败返回EOF。

这个函数导致流中任何未写的数据都被传递级内核。作为一个特殊情况,如果fp为NULL,函数会冲洗所有的输出流。

5.5 打开一个流(Opening a Stream)

下面三个函数打开一个标准I/O流。

#include <stdio.h>

FILE *fopen(const char *restrict pathname, const char *restrict type);

FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);

FILE *fdopen(int filedes, const char *type);

成功返回文件指针,错误返回NULL。

这三个函数的区别在于:
1、fopen函数打开一个指定的文件。

2、freopen函数在一个指定的流上打开一个指定的文件,并关闭之前打开的流。如果之前的流有方向,freopen会清除它。这个函数被典型用来打开一个指定的文件,作为其中一个预定义的流:标准输入、标准输出或标准错误。

3、fdopen函数接受一个已有的文件描述符,它可以通过open、dup、dup2、fcntl、pipe、socket、socketpair或accept函数得到。这个文件描述符会和标准I/O流关联起来。这个函数经常和创建管道和网络通信渠道的函数的返回值一起使用。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须调用设备相关的函数来得到一个文件描述符,再通过fdopen函数把这个描述符与标准I/O流相联起来。

fopen和freopen两个都是ISO C的一部分。fdopen是POSIX.1的一部分,因为ISO C没有处理文件描述符。

下表显示了ISO C为type参数指定的15个值:

打开一个标准I/O流的type参数
type描述
r或rb打开来读
w或wb打开来写
a或ab添加;打开写入文件末尾,或创建来写
r+或r+b或rb+打开来读写
w+或w+b或wb+裁切成0长度或创建来读写
a+或a+b或ab+打开或创建来在文件尾读写

使用字符b作为类型的一部分允许标准I/O系统来区分一个文本文件和二进制文件。因为UN不X内核不区分这两种文件类型,指定这个字符b没有效果。

在fdopen里,type参数的含义有一些小区别。因为描述符已经被打开了,所以打开来写不会裁切这个文件。(例如,如果描述符由open函数创建,而且这个文件已经存在,那么O_TRUNC标志会控制这个文件是否被裁切。fdopen函数不能简单的裁切任何它打开来写的文件。)还有,标准I/O添加模式不能创建这个文件(因为由一个描述符指向的这个文件必须存在)。

当一个文件用添加类型打开时,每次写都会发生在文件的当前末尾。如果多个进程使用标准I/O添加模式打开同一个文件,每个进程的数据都会被正确写入到这个文件。

在4.4BSD之前的Bdrkeley的fopen的各种版本,以及在Kernighan and Ritchie[1988]第177页上展示的简单版本,没有正确地处理添加模式。这些版本在流打开时用lseek到文件末尾。为了当多进程被引入时正确支持添加模式,文件必须用O_APPEND标志打开,我们已经在3.3节讨论过了。每次写前调用lseek也不能工作,正如我们在3.11节讨论的。

当一个文件打开来读写(类型中的+号)时,会有以下限制:

1、输出后,没有fflush、fseek、fsetpos或rewind的操作的话,不能直接进行输入。

2、输入后,没有fseek、fsetpos、rewind或一个碰到文件尾的读操作的话,不能直接进行输出。

下表总结了打开一个流的6种方式:

打开一个标准I/O流的6种方式
限制rwar+w+a+
文件必须存在*  *  
文件之前的内容被丢弃 *  * 
流可以读*  ***
流可以写 *****
流只能在末尾写  *  *

注意如果一个新文件通过指定一个w或a的类型创建,我们不能指定这个文件的访问权限位,因为我们本可以在open函数或creat函数来做这件事(第3章)。

默认情况下,流以完全缓冲方式打开,除非它指向一个终端设备,在这种情况下它是行缓冲的。一旦流被打开,在我们对其做任何操作之前,如果愿意的话,我们可以改变这个缓冲,通过使用上节提到的setbuf和setvbuf函数。

一个打开的流可以调用fclose来关闭。

#include <stdio.h>

int fclose(FILE *fp);

成功返回0,错误返回EOF。

任何缓冲的输出数据都在文件关闭前被冲洗。任何可能被缓冲的输入数据都会被舍弃。如果标准I/O库之前自动开辟了一个缓冲,这个缓冲会被释放。

当一个进程通过直接调用exit函数或从main函数返回而正常终止时,所有包含未写的缓冲数据的标准I/O流都会被冲洗,而且所有的标准I/O流都会关闭。

5.6 读写流(Reading and Writing a Stream)

一旦我们打开一个流,我们可以从以下三种无格式的I/O中选择:
1、一次一字符(Character-at-a-time)I/O。我们可以一次读写一个字符,而让标准I/O函数处理所有的缓冲事宜,如果流有缓冲的话。

2、一次一行(Line-at-a-time)I/O。如果我们读一次读写一行,我们使用fgets和fputs。每行都以一个换行符终止,而且当我们调用fgets时必须指定我们能处理的最大行长度。我们在5.7节讲述这两个函数。

3、直接(Direct)I/O。这种类型的I/O通过fread和fwrite函数支持。对于每个I/O操作,我们读写一些对象,而每个对象都有一个指定的尺寸。这两个函数经常用来处理二进制文件,那里我们每次操作读写一个结构。我们在5.9节讲述这两个函数。

术语“直接I/O”,从ISO C标准而来,有很多名字:二进制(binary)I/O、一次一对象(object-at-a-time)I/O、面向记录(record-oriented)I/O,或面向结构(structure-oriented)I/O。

(我们在5.11节讲述格式化I/O函数,比如printf和scanf。)

输入函数:

有三个允许我们一次读一个字符的函数。

#include <stdio.h>

int getc(FILE *fp);

int fgetc(FILE *fp);

int getchar(void);

三者成功都返回下一个字符,失败都返回EOF。

函数getchar被定义为与getc(stdin)等价。前两个函数的区别是getc可以被作为一个宏而实现,而fgetc不能作为宏实现。这表明了三件事:

1、getc的参数不应该是有副作用的表达式。

2、因为fgetc被保证为一个函数,所以我们可以拿到它的地址。这允许我们把fgetc的地址作为另一个函数的参数。

3、fgetc的调用很可能比getc的调用时间长,因为调用一个函数的时间通常要更长。

这三个函数返回下一个字符,作为一个unsigned char并转换为一个int。指定无符号的原因是为了高位位如果设置的话,不会返回一个负值。返回一个int值的原因是为了可以返回所有可能的字符值,以及一个错误或碰到文件尾的标识。在<stdio.h>定义的常量EOF被要求必须是一个负值,它通常为-1。这种表示方式同样也意味着我们不能把这三个函数的返回值存储在一个字符变量里并稍后用这个值与常量EOF比较。

注意这些函数当碰到一个错误或到达文件尾时返回同一个值。为了区别这两者,我们必须调用ferror或feof。

#include <stdio.h>

int ferror(FILE *fp);

int feof(FILE *fp);

两者如果条件成立的话返回非0(真),否则返回0(假)。

void clearerr(FILE *fp);

在多数实现上,在FILE对象里为每个流维护了两个标志:
1、一个错误标志;
2、一个文件结束标志。

两个标志都可以调用clearerr来清除。

在从一个流读入后,我们可以调用ungetc来把字符放回去。

#include <stdio.h>

int ungetc(int c, FILE *fp);

成功返回c,否则返回EOF。

放回的字符会被后续在流上的读操作以放回的相反顺序返回。尽管如此,注意,虽然ISO C允许一个实现支持任意数量的放回,但一个实现只被要求提供单一字符的放回。我们不应该指望会多于一个字符。

我们放回的字符不必和我们之前读取的字符一样。我们可以放回EOF。但是当我们到达文件尾时,我们可以放回一个字符。下次读时会返回这个字符,再下次读会返回EOF。这得以工作是因为成功的ungetc操作会清除流的文件结束标识。

放回通常用在我们读一个输入流并把输入分为多个单词或一些格式的语素。有时我们需要预先查看下下一个字符来决定如何处理当前字符。我们可以很容易把预查的字符放回,以便下次的getc返回它。如果标准I/O库没有提供这种放回的能力,那么我们必须把这个字符存储到我们自己的变量里,以及一个告诉我们下次需要一个字符时使用这个字符而不是调用getc来获取字符的标志。

当我们用ungetc放回字符时,它们不会写入下层的文件或设备。它们被保存在标准I/O库的缓冲里。

输出函数

我们将找到对应于每个我们之前讨论过的输入函数的输出输出。

#include <stdio.h>

int putc(int c, FILE *fp);

int fputc(int c, FILE *fp);

int putchar(int c);

三者成功都返回c,否则返回EOF。

和输入函数相似,putchar(c)和putc(c, stdout)等价,而putc可以作为一个宏实现,但fputc不能作为一个宏实现。

5.7 一次一行I/O(Line-at-a-Time I/O)

一次一行输入由以下两个函数提供。

#include <stdio.h>

char *fgets(char *restrict buf, int n, FILE* restrict fp);

char *gets(char *buf);

两者成功返回buf,文件结束或错误返回NULL。

两都都指明一个把行读入的缓冲区的地址。gets函数从标准输入中读,而fgets从指定的流中论坛版主。

使用fgets,我们必须指定缓冲的尺寸,n。这个函数一直读到并包括下一个换行符,然不会多于n-1个字符,读入缓冲区里。这个缓冲由一个null字节终止。如果这行,包括终止的换行符比n-1长,那么只有行的一部分被返回,但缓冲总是null终止的。另一个fgets的调用会读取该行的剩余部分。

gets函数不应该被使用。问题是它不允许调用者指定缓冲大小。如果行比缓冲长的话,这会允许缓冲溢出,并覆写掉跟随在缓冲之后的内存。这个缺陷用来作为1988年的因特网蠕虫的一部分。gets的另一个区别是它不和fgets一样在buffer里存储一个换行符。

两个函数的这个处理换行符的区别可以追朔到UNIX系统的起源。Version 7的手册(1979)甚至指出“gets删除一个换行符,fgets保留它,都是以身后兼容的名义。”

尽管ISO C要求一个实现来提供gets,但还是使用fgets。

一次一行输出由fputs和puts提供:

#include <stdio.h>

int fputs(const char *restrict str, FILE *restrict fp);

int puts(const char *str);

成功返回非负值,否则返回EOF。

函数fputs把一个null终止的字符串写入到指定的流中。末尾的null字节没有被写。注意这不必是一个一次一行输出,因为字符串最后的非null字符不必是一个换行符。虽然通常都是这种情况--最后一个非空字符为换行符--但这并不被要求。

puts函数在标准输出写一个空字符终止的字符串,但不会把这个空字节写出。不同的时puts接着会把一个换行符写入到标准输出。

puts函数并不会不安全,就像与它相当的gets一样。尽管如此,我们将避免使用它,以免记住它是否添加一个换行符。如果我们总是合适fgets和fputs,我们知道我们必须处理每行最后的换行符。

5.8 标准I/O效率(Standard I/O Efficiency)

使用上节讨论的函数,我们会知道标准I/O系统的效率。下面的程序和3.9节的程序相似:它使用getc和putc简单地把标准输入拷贝到标准输出。这两个函数可以作为宏实现:

#include <stdio.h>

int main(void)
{
    int c;
    while ((c = getc(stdin)) != EOF)
        if (putc(c, stdout) == EOF)
            exit(1);
    if (ferror(stdin))
        exit(1);
    exit(0);
}
我们可以使用fgetc和fputc实现这个程序的另一个版本,它们应该是函数,而不是宏。(这里我们不展示这种小的改变。)

最后,我们有一个读写行的版本:

#include <stdio.h>

#define MAXLINE 4096

int
main(void)
{
    char buf[MAXLINE];

    while (fgets(buf, MAXLINE, stdin) != NULL)
        if (fputs(buf, stdout) == EOF)
            exit(1);

    if (ferror(stdin))
        exit(1);

    exit(0);
}
注意上面两个程序里,我们没有显式地关闭标准I/O流。相反,我们知道exit函数会冲洗任何没有写的数据然后关闭所有打开的流。(我们将在8.5节讨论这个。)有趣的是比较这三个程序的耗时与3.9节的耗时。我们在下表展示操作同一个文件(98.5M,300万行)的数据:
使用标准I/O函数的耗时结果
函数用户CPU(秒)系统CPU(秒)时钟时间(秒)程序代码的字节
3.9节里最好的时间0.010.186.67 
fgets, fputs2.590.197.15139
getc, putc10.840.2712.07120
fgetc, fputc10.440.2711.42120
3.9节里的单一字节时间124.89161.65288.64 

对这三个标准I/O版本的每个,用户CPU时间都比3.9节最好的读版本要长,因为一次一字符标准I/O版本有一个执行了1亿次的循环,而一次一行版本的循环执行了3,14,984次。在read版本里,它循环只执行了12,611次(当缓冲尺寸为8,192时)。在时钟时间里的区别在于用户时间的区别,以及等待I/O完成所花时间的区别,而系统系统是可比的。

系统时间和之前的大致相同,这是因为内核请求的数量大致相同。注意使用标准I/O函数的优点是我们不用担心缓冲以及选择优化的I/O尺寸。虽然我们必须决定使用fgets的版本的最大行尺寸,但是这比选择优化的I/O尺寸简单。

使用一次一行I/O的版本比使用一次一字符的版本快了近一倍。如果fgets和fputs函数用getc和putc来实现的话,那我们会期望耗时与getc版本相似。事实上,我们可能会预期一次一行版本花的时间更长,因为我们在已有的600万次的基础上,加上了额外的200万次函数调用。这个例子里发生的是一次一行函数使用memccpy函数实现。通常,memccpy函数由汇编而非C实现,为了效率。

最后一个关于这些耗时的有趣的事是fgtc版本比BUFFSIZE=1的3.9节版本快了如此之多。两者都调用了相同数量的函数--大约2亿--然而fgetc版本在CUP时间上快了近12倍而在时钟时间上稍超出25倍。区别在于使用read的这个版本执行2亿次函数调用,从而造成2亿次系统调用。fgetc版本里,我们执行了2亿次函数调用,但只有25,222次系统调用。系统调用通常比普通函数调用耗时得多。

作为一个反对者,你应该已经注意到这些耗时结果只在它们运行的单一系统上有效。这个结果取决于在每个UNIX系统上都不一样的许多实现的特性。尽管如此,有这些类似的数字,并解释这些版本为什么不同,帮助我们更好地了解这个系统。从这节和3.9节,我们已经知道了标准I/O库并不比直接调用read和write慢很多。我们看到的大概的花销是大约0.11秒的时钟时间用getc和putc来拷贝一M的数据。对于多数的实际的程序,用户CPU时间最多被程序花费掉,而不是标准I/O函数。

5.9 二进制I/O(Binary I/O)

5.6节的函数一次操作一个字符,而5.7节的函数一次操作一行。如果我们执行二进制I/O,我们经常是想一次读写整个结构。为了使用getc或putc,我们必须在整个结构里循环,一次一个字节,读写各个字节。我们不能使用一次一行函数,因为fputs当碰到空字节时结束写,而在结构里可能会有空字节。类似的,fgets不能用来输入如果任何数据字节包含空字节或的换行符。因此,以下两个函数被用来执行二进制I/O。

#include <stdio.h>

size_t fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

size_t fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict fp);

两者都返回读或写的对象数。

这些函数有两种普遍的使用方法:

1、读写一个二进制数组。例如,写一个浮点数组的第2到第5人元素,我们可以写:
float data[10];
if (fwrite(&data[2], sizeof(float), 4, fp) != 4)
    err_sys("fwrite error");

这里,我们指定size作为数组每个元素的尺寸而nobj作为元素的数量。

2、读写一个结构体。例如,我们可能这样写:
struct {
  short count;
  long total;
  char name[NAMESIZE];
} item;
if (fwrite(&item, sizeof(item), 1, fp) != 1)
  err_sys("fwrite error");

这里,我们指定size作为结构体的尺寸而nobj指定为1(写的对象的数量)。

这两种情况的明显的扩展是读写一个结构体数组。要这样做,size应该是结构体的sizeof,而nboj应该是数组的元素数量。

fread和fwrite都返回读入或写出的对象的数据。对于read,这个数量可以比nboj小,如果有错误发生或碰到文件结束。这些种情况下ferror和feof必须调用。对于write,如果返回值比所需的nobj少,表示发生错误。

二进制I/O的一个基本问题,是它只能用来读在同一系统上写的数据。这在很多年前,当所有UNIX系统都在PDP-11上时,是没问题的。但如今多种多样的系统用网络连接在一起。想要在一个系统上写数据而在另一个上处理它是很普遍的。这两个函数将不能工作,基于两个原因:
1、一个结构体里的成员的偏移量在编译器间和系统间可以不同,因为不同的对齐需要。事实上,一些编译器有一个选项允许结构体被紧凑打包,以运行时的性能为代价节省空间,或者精确地排列,来优化每个成员的运行访问时间。这意味着即使在同一个系统上,一个结构体的字节排列也可以不同,取决于编译器选项。
2、用来存储多字节整型和浮点型的值在各种机器架构上不同。

我们将在16章讨论套接字时碰到一些这样的问题。在不同系统上交换二进制数据的真实解决方案,是使用更高层的协议。我们将在8.14节回到fread函数,当我们用它来讯一个二进制结构体,UNIX进程记账记录的时候。

5.10 定位流(Positioning a Stream)

有三种方式来定位一个标准I/O流:
1、两个函数ftell和fseek。它们在Version 7出现,但它们假设一个文件位置可以用一个长整型存储;
2、两个函数ftello和fseeko。它们在SUS引入,来允许文件可能不适合长整型的文件偏移量。它们用off_t数据类型代码长整型;
3、两个函数fgetpos和fsetpos。它们被ISO C引入。它们用一个抽象数据类型fpos_t,来记录一个文件的位置。这个数据类型可以是记录一个文件位置所需而随意大。

想移到非UNIX系统的可移植的程序应该使用fgetpos和fsetpos。

#include <stdio.h>

long ftell(FILE *fp);

如果成功返回当前文件位置指示器,错误返回-1L。

int fseek(FILE *fp, long offset, int whence);

成功返回0,否则返回非0值。

void rewind(FILE *fp);

对于一个二进制文件,一个文件的位置指示器是从文件开始的字节来衡量的。ftell为二进制文件的返回值是这个字节位置。为了用fseek定位一个二进制文件,我们必须指定一个字节偏移以及这个偏移如何被解释。whence的值和3.9节的lseek函数里的一样:SEEK_SET表示从文件开头开始;SEEK_CUR表示从当前文件位置开始;SEEK_END表示从文件末尾开始。ISO C没有要求一个实现支持二进制文件的SEEK_END,因为一些系统要求一个二进制文件在末尾添加一些0以及文件大小为一些魔数的整数。然而,在UNIX系统,SEEK_END用来支持二进制文件。

对于文本文件,文件的当前位置可能不能简单用字节偏移来测量。一些非UNIX系统下可能把文本文件存储成另一种格式。为了定位一个文本文件,whence必须是SEEK_SET,而且只有两个值被允许:0-表示回到开始,或一个由ftell返回的值。一个流也可以用rewind函数回到文件开头。

ftello函数和ftell函数相同,而fseeko函数和fseek函数相同,除了偏移量的类型是off_t而不是long。

#include <stdio.h>

off_t ftello(FILE* fp);

成功返回当前文件位置,否则返回(off_t)-1。

int fseeko(File *fp, off_t offset, int whence);

成功返回0,失败返回非0.

回想下3.6节关于off_t数据类型的讨论。实现可以把off_t类型定义的比32位更大。

正如我们提到的,fgetpos和fsetpos函数由ISO C标准引入。

#inlcude <stdio.h>

int fgetpos(FILE *restrict fp, fpos_t *restrict pos);

int fsetpos(FILE *fp, const fpos_t *pos);

两者成功都返回0,失败返回非0值。

fgetpos返回把文件位置指示器的值存储在由pos指向的对象里。这个值可以被之后的fsetpos调用使用,来重定位流的位置。

5.11 格式化I/O

格式化输出

格式化输出由4个printf函数处理

#include <stdio.h>

int printf(const char *restrict format, ...);

int fprintf(FILE *restrict fp, const char *restrict format, ...);

两者成功都返回字符数,输出错误则返回负值。

int sprintf(char *restrict buf, const char *restrict format, ...);

int snprintf(char *restrict buf, size_t n, const char *restrict format, ...);

两者成功都返回在字节里存储的字符数量,编码错误返回负值。

printf函数向标准输出写,fprintf向一个指定的流写,而sprintf把格式化的字符串放入到数组buf里。sprintf函数在数组末自动将上一个空字节,但这个空字节没有包含在返回值里。

注意sprintf可能为溢出buf指向的缓冲区。这是调用者的执行来保证这个缓冲区足够大。因为这会导致缓冲区溢出的问题,于是snprintf被引入。用它可以把缓冲的尺寸作为一个显式的参数;任何超过缓冲区末尾的字符都会被舍弃掉。snprintf函数返回假如缓冲足够大时会写入的字符数。而sprintf一样,返回值不包括终止的空字节。如果snprintf返回一个比缓冲大小n更小的正值,那么输出则被裁切过了。如果编码出错,snprintf返回一个负值。

格式规范控制了剩下的参数如何被编码和最终显示。每个参数根据一个以百分号(%)开头的转换规格(conversion specification)来编码。除了转换规格,其它在格式里的字符都被无修改地复制。一个转换规格有4个可先部分,如下方括号里所示:
%[flags] [fldwidth] [precision] [lenmodifier] convtype

flags由下表汇总:

转换规格的flag部分
Flag描述
-域里面左输出
+总是显示一个正负号
(空格)如果没有正负号则以一个空格开头
#使用代替的格式转换(例如16进制格式中包含0x前缀)
0以0填充,而不是空格

fldwidth部分指定了最小的域宽度。如果转换结果有更少的字符,则以空格填充。域完是一个非负十进制整数或一个星号。

precision部分在整数转换时指定了整数中数字的最小数量,在浮点数转换时指定了小数点右侧的数字的最小数量,或者在字符串转换中指定字节的最大数量。precision是一个“.”接着一个可选的非负十进制整数或星号。

域宽和精度都可以是一个星号。这种情况下,一个指定这个值的参数被使用,它直接在出现在被转换的参数前。

lenmodifier部分指定了参数的尺寸。下表总结了可能的值:

转换格式的长度修改符
Length modifier描述
hh有符号或无符号char
h有符号或无符号short
l有符号或无符号long或宽字符
ll有符号或无符号long long
jintmax_t或uintmax_t
zsize_t
tptrdiff_t
Llong double

convtype部分不是可选的。它控制了参数如何被解释。下表总结了各种转换类型:

转换规格的转换类型部分
Conversion type描述
d, i有符号十进制
o无符号八进制
u无符号十进制
x, X无符号十六进制
f, Fdouble浮点数
e, E指数格式的double浮点数
g, G根据要转换的值,以f,F,e或E来解释
a, A十六进制指数格式的double浮点数
c字符(长度修改符l表示宽字符)
s字符串(长度修改符l表示宽字符串)
pvoid指针
n指向表示目前为止已写字符数量的有符号整型的指针
%一个%字符
C宽字符(XSI扩展,等价于lc)
S宽字符串(XIS扩展,等价于ls)

下面4个printf家族的变体与前4个函数相似,只是参数列表(...)被arg代替。

#include <stdarg.h>
#include <stdio.h>

int vprintf(const char *restrict format, va_list arg);

int vfprintf(FILE *restrict fp, const char *restrict format, va_list arg);

两者成功都返回输出的字符数,失败返回负值

int vsprintf(char *restrict buf, const char *restrict format, va_list arg);

int vsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list arg);

两都成功都返回数组存储的字符数,编码错误返回负数。

注意可变长度的参数列表由ISO C提供,定义在<stdarg.h>头文件以及相关的程序,与老的UNIX系统提供在<varargs.h>程序的不同。

格式化输入

格式化输入由三个scanf函数处理

#include <stdio.h>

int scanf(const char *restrict format, ...);

int fscanf(FILE *restrict fp, const char *restrict format, ...);

int sscanf(const char *restrict buf, const char *restrict format, ...);

三者都返回被赋值的输入项的数目,输入出错或碰到文件结尾时返回EOF。

scanf家族用来解析一个输入的字符串并转换成指定类型的变量。在格式后的参数包含需要用转换结果初绐化的变量的地址。

格式规格(format specification)控制了参数如何为赋值而被转换。百分号(%)表示格式规格的开始。除了转换规格和空格,其它在格式里的字符都用来匹配输入。如果有一个字符不匹配,操作停止,不再读剩下的输入。

转换规格中有三个可选部分,如下面方括号里所示:
%[*] [fldwidth] [lenmodifier] convtype

可选的开头的星号用来抑制转换。输入根据转换规格的其它部分转换,但结果不会存储在一个参数里。

fldwidth部分指定域的最大字符数。lenmodifier部分指定要以转换结果初始化的参数的尺寸。被printf家族支持的长度修改符同样也被scanf家族支持。

convtype域与printf家族所用的转换类型域类似,但有一些不同。一个区别是,存在一个无符号类型的结果在输入里可以是有符号的。下表总结了scanf家族支持的转换类型:

转换规格的转换类型部分
Conversion type描述
d有符号十进制
i有符号数,根据输入格式决定进制
o无符号八进制(输入可以是有符号的)
u无符号十进制(输入可以是有符号的)
x无符号十六进制(输入可以是有符号的)
a, A, e, E, f, F, g, G浮点数
c字符(长度修改符l表示宽字符)
s字符串(长度修改符l表示宽字符串)
[匹配一个字符列表,以]结束
[^匹配除列出的之外的所有字符,以]结束
pvoid指针
n指向表示目前为止已写字符数量的有符号整型的指针
%一个%字符
C宽字符(XSI扩展,等价于lc)
S宽字符串(XIS扩展,等价于ls)

和printf家族一样,scanf家族也支持使用可变参数列表的函数,它们由<stdarg.h>指定。

#include <stdarg.h>
#include <stdio.h>

int vscanf(const char *restrict format, va_list arg);

int vfscanf(FILE *restrict fp, const char *restrict format, va_list arg);

int vsscanf(const char *restrict buf, const char *restrict format, va_list arg);

三者都返回赋好值的输入项的数量,错误或转换前遇到文件结尾时返回EOF。

参考你的UNIX系统手册来获得更多关于scanf家族函数的细节。

5.12 实现细节(Implementation Details)

正如我们提到过的,在UNIX系统下,标准I/O库最后会调用第3章描述的I/O程序。每个标准I/O流有一个相关的文件描述符,而我们可以调用fileno来得到流的文件描述符。

#include <stdio.h>

int fileno(FILE *fp);

返回相关联的文件描述符。

注意fileno不是ISO C标准的一部分,而是POSIX.1支持的一个扩展。

例如,如果想调用dup或fcntl函数,我们需要这个函数。

要看你系统上标准I/O库的实现,从头文件<stdio.h>开始。这会展示FILE对象是如何定义的,每个流标志的定义,还有任何被定义为宏的I/O函数,比如getc。GNU标准I/O库的实现是公开的。

下面的代码打印三个标准流以及与一个普通文件关联的流的缓冲:

#include <stdio.h>

void pr_stdio(const char *, FILE *);

int
main(void)
{
    FILE *fp;

    fputs("enter any character\n", stdout);
    if (getchar() == EOF)
        exit(1);
    fputs("one line to standard error\n", stderr);

    pr_stdio("stdin", stdin);
    pr_stdio("stdout", stdout);
    pr_stdio("stderr", stderr);

    if ((fp = fopen("/etc/motd", "r")) == NULL)
        exit(1);
    if (getc(fp) == EOF)
        exit(1);
    pr_stdio("/tec/motd", fp);
    exit(0);
}

void
pr_stdio(const char *name, FILE *fp)
{
    printf("stream = %s, ", name);

    /*
     * The following is nonportable.
     */
    if (fp->_IO_file_flags & _IO_UNBUFFERED)
        printf("unbuffered");
    else if (fp->_IO_file_flags & _IO_LINE_BUF)
        printf("line buffered");
    else /* if neither of above */
        printf("fully buffered");
    printf(", buffer size = %d\n", fp->_IO_buf_end - fp->_IO_buf_base);
}

注意我们在打印每个流的缓冲状态前,都为它执行I/O,因为第一个I/O操作通常会导致流的缓冲被开辟。结构体成员_IO_file_flags、_IO_buf_base、和_IO_buf_end以及常量_IO_UNBUFFERED和_IO_LINE_BUFFERED由在Linux上使用的GNU标准I/O库定义。注意其它UNIX系统可能有不同的标准I/O库的实现。

看运行结果:
$ ./a.out
enter any character
a
one line to standard error
stream = stdin, line buffered, buffer size = 1024
stream = stdout, line buffered, buffer size = 1024
stream = stderr, unbuffered, buffer size = 1
stream = /tec/motd, fully buffered, buffer size = 4096


$ ./a.out <tmp >std.out 2>std.err
$ cat std.out
enter any character
stream = stdin, fully buffered, buffer size = 4096
stream = stdout, fully buffered, buffer size = 4096
stream = stderr, unbuffered, buffer size = 1
stream = /tec/motd, fully buffered, buffer size = 4096
$ cat std.err
one line to standard error

我们可以看到默认情况下,系统的标准输入和标准输出连接到终端时是行缓冲的。行缓冲有1024字节。注意这并不限制我们输出或输入1024字节的行,那只是缓冲的大小。向标准输出写一个2048字节的行将会请求两个系统调用。当我们把这两个流重定向到普通文件时,它们变为完全缓冲的了,而缓冲大小与文件系统的首选I/O尺寸一样--stat结构体里的st_blksize的值。我们也看到标准错误总是无缓冲的,正如它应该的那样。一个普通文件默认为完全缓冲的。

5.13 临时文件(Temporary Files)

ISO C标准定义了两个由标准I/O库提供的函数,用来协助创建临时文件。

#include <stdio.h>

char *tmpnam(char *ptr);

返回指向唯一的路径名的指针。

FILE *tmpfile(void);

成功返回文件指针,失败返回NULL。

tmpname函数返回一个不与任何已有文件相同的合法路径名的字符串。这个函数每次调用会产生一个不同的路径名,最多TMP_MAX次。TMP_MAX定义在<stdio.h>里。

尽管ISO C定义了TMP_MAX,但是C标准只要求它的值最少为25。然而SUS要求XSI系统支持一个最少10000的值。尽管这个最小值允许一个实现使用四位数(0000~9999),但多数UNIX系统的实现使用小写或大写的字符。

如果ptr为NULL,那么产生的pathname存储在一个静态 区域里,而这个区域的指针被作为这个函数的返回值。随后的tmpnam调用可以覆盖这个区域。(这意味着如果我们不只一次调用这个函数而又想保存这个路径名,我们必须存储这个路径名的拷贝,而这是指针的拷贝。)如果ptr不为NULL,它被假设为一个指向至少L_tmpnam字符的数组的指针。(常量L_tmpnam定义在<stdio.h>里)。生成的路径名被存储在这个数组里,而ptr也作为这个函数的返回值。

tmpfile创建一个临时二进制文件(wb+类型),当它被关闭时或程序终止时,这个文件会被自动删除。在UNIX系统,这个文件作为二进制文件并没有什么不同。

看下面的例子:

#include <stdio.h>
#define MAXLINE 4096

int
main(void)
{
    char name[L_tmpnam], line[MAXLINE];
    FILE *fp;

    printf("%s\n", tmpnam(NULL));   /* first temp name  */

    tmpnam(name);  /* second temp name */
    printf("%s\n", name);

    if ((fp = tmpfile()) == NULL)   /* create temp file */
        exit(1);
    fputs("one line of output\n", fp);  /* write to temp file */
    rewind(fp);
    if (fgets(line, sizeof(line), fp) == NULL)
        exit(1);
    fputs(line, stdout);        /* print the lien we wrote */

    exit(0);
}
/tmp/fileV8XZkg
/tmp/filelq0E40
one line of output

tmpfile函数经常使用的标准技术是调用tmpnam创建一个唯一的路径名,然后创建这个文件,之后立即unlink它。回想下4.15节,解链接一个文件在文件关闭前都不会删除它的内容。这种方法,进程显式关闭文件或进程终止时,文件的内容会被删除。

SUS定义了两个额外的函数,作为XSI扩展,来处理临时文件。第一个是tempnam函数。

#include <stdio.h>

char *tmpnam(const char *directory, const char *prefix);

返回指向唯一路径名的指针。

tempnam函数是tmpnam的一个变体,允许调用者指定目录和生成路径名的前缀。directory有四种可能的选择,第一个满足的情况将作为这个目录:
1、如果环境变量TMPDIR定义了,它被作为这个目录。(我们将在7.9节描述环境变量。)
2、如果directory不为NULL,它被作为目录。
3、<stdio.h>里的字符串P_tmpdir被作为这个目录。
4、一个本地目录,通常是/tmp,被作为这个目录。

如果prefix参数不为NULL,它应该是一个最多5字节的字符串,作为文件名的开头。

这个函数调用malloc函数来为生成的路径名开辟一个动态空间。我们可以在使用完这个路径名后释放这个空间。(我们将在7.8节讨论malloc和free函数。)

看下面的例子:

#include <stdio.h>

int
main(int argc, char *argv[])
{
    if (argc != 3)
        exit(1); /* usage: a.out <directory> <prefix> */

    printf("%s\n", tempnam(argv[1][0] != ' ' ? argv[1] : NULL,
        argv[2][0] != ' ' ? argv[2] : NULL));

    exit(0);
}
$ ./a.out " " pre-
/tmp/pre-QeucSV
$ ./a.out ~ pre-
/home/tommy/pre-XDvhdK
$ ./a.out ~ " "
/home/tommy/fileh5eC4M
$ TMPDIR=/var/tmp ./a.out ~ " "
/var/tmp/file3ql3ps
$ TMPDIR=/no/such/dir ./a.out ~ " "
/home/tommy/file0uZmGD
正如我们之前列出的四个步骤,目录名按照顺序尝试,而函数也检查对应的目录名是否有效。如果文件名不存在(比如/no/such/dir),这种情况会被忽略,而下个目录名的尝试会被尝试。从这个例子,我们可以看到在这个实现上,P_tmpdir目录是/tmp。我们用来设置环境变量技术(在程序名前指定TMPDIR=),被Bourne shell、Korn shell和bash使用。

XSI定义的第二个函数是mkstemp。它也tmpfile相似,但返回这个临时文件打开后的文件描述符,而不是文件指针。

#include <stdlib.h>

int mkstemp(char *template);

成功返回文件描述符,失败返回-1。

返回的文件描述符能用来写和读。这个临时文件的名字通过template字符串来选择。这个字符串是最后6个字符被设置为XXXXXX的一个路径名。如果mkstemp返回成功,它会把template字符串修改成反映这个临时文件的名字。

和tmpfile不同,这个由mkstemp创建的临时文件不会自动删除。如果我们想从系统名字空间里删除它,我们需要自己解链接它。

使用tmpnam和tempnam有一个缺陷:在唯一路径名返回和程序用这个名字创建文件之间有一个间隙。在这个时间间隙里,另一个进程可以用同样的名字创建一个文件。应该使用tmpfile和mkstemp函数,因为它们没有这个问题。

mktemp函数和mkstemp相似,除了它只创建一个适用于一个临时文件的名字。mktemp函数不创建一个文件,所以它和tmpnam和tempnam有相同的问题。mktemp函数在SUS里被标记为一个遗留的接口。遗留接口在将来的SUS版本里可能会被删除,所以应该避免使用。

5.14 标准I/O的替代品(Alternative to Standard I/O)

标准I/O库并不完美。Korn and Vo[1991]列出了许多缺陷:一些在基本设计上,而多数在各种实现上。

一个在标准I/O库里的低效的是数据拷贝的量。当我们使用一次一行函数,fgets和fputs,数据通常被拷贝两次:一次在内核和标准I/O缓冲之间(当对应的read和write被使用时),另一次在标准I/O缓冲和我们的行缓冲之间。快速I/O库[fio in At&T 1990a]通过下面方式解决这个问题:让这个读取一行的函数返回一个行的指针,而不是把这行拷贝到另一个缓冲里。Hume[1988]指出,通过这个改变,grep工具的一个版本速度大幅提升。

Korn and Vo[1991]描述另一个标准I/O库的替代:sfio。这个包和fio库的速度相当,通常比标准I/O库快。sfio包还提供了一些在其它库里没有的新特性:生成的用于同时表示文件和内存区域的I/O流、可以写并可以在I/O流上压栈以改变流的操作的处理模块、和更好的异常处理。

Krieger、Stumm、和Unrau[1992]描述了另一个替代品,它使用映射文件--我们将在14.9节讲述的mmap函数。这个新包被称为ASI,Alloc Stream Interface。这个编程接口重汇集了UNIX系统内存分配函数(malloc、realloc和free,在7.8节讨论。)和sfio包一样,ASI试图通过使用指针最小化数据拷贝的量。

在为小内存系统设计的,比如嵌入式系统,的C库里有种标准I/O库的实现。这些实现适度内存请求,超过可移植性,速度,或者功能。两个这种实现是uClibs C库和newlibs C库。

5.15 总结

标准I/O库被许多UNIX程序使用。我们已经看过这个库提供的所有函数,以及一些实现细节的效率考虑。注意库使用的缓冲,这是产生最多问题和困惑的领域。


第六章 系统数据文件和信息(System Data Files and Information)

6.1 引言

UNIX系统需要许多通常操作所用的数据文件:密码文件/etc/passwd和组文件/etc/group是被各种程序经常使用的两个文件。例如,密码文件在每次用户登录UNIX系统和每次某人执行一个ls -l命令里被使用。

历史上,这些数据文件一直是ASCII文本文件,并由标准I/O库读取。但是对于更大的系统,密码文件的线性浏览变得很耗时。我们想能够把这些数据文件存储成ASCII文本之外的其它格式,但仍然提供一个接口,使工作在任何文件格式上的程序仍然可以工作。这个针对这些数据文件的可移植的接口就是本章的内容。我们还要包含系统辨认函数(system idendification functions),以及时间日期函数。

6.2 密码文件(Password File)

UNIX系统的密码文件,被POSIX.1称为用户数据库,包含了下表所示的域。这些域包含在定义于<pwd.h>里的一个passwd结构体里:

/etc/passwd文件的域
描述结构体passwd的成员POSIX.1FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
用户名char *pw_name*****
加密密码char *pw_passwd ****
数值化用户IDuid_t pw_uid*****
数值化组IDgid_t pw_gid*****
注解域char *pw_gecos ****
初始工作目录char *pw_dir*****
初始外壳(用户程序)char *pw_shell*****
用户访问类char *pw_class * * 
下次修改密码的时间time_t pw_change * * 
帐户到时时间time_t pw_expire * * 

注意POSIX.1只指定了passwd结构里十个域里的五个。多数平台支持至少七个域。基于BSD的平台支持所有的域。

历史上,密码文件存储在/etc/passwd里,并且是个ASCII文件。每行包含着上表描述的域,以冒号分隔。例如,Linux上的/etc/passwd文件的四行可能是:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
rwhod:x:119:65534::/var/spool/rwho:/bin/null

注意关于这些项的以下几点:
1、通常有一个名为root的项。这个项有个值为0的用户ID(超级用户)。
2、加密密码域包含单个字符,作为占位符。早期UNIX系统版本使用该域存储加密密码。因为在一个对所有人可读的文件里存储加密密码是一个安全漏洞,所以加密密码被存储在其它地方。我们将在下节讨论密码时更深入地讨论这个问题。
3、密码文件项的一些域可以为空。如果加密密码域为空,它通常表示这个用户没有密码。(这不被提倡。)项rwhod有一个空白域:注解域。一个空注解域没有效果。
4、shell域包含了可执行程序的路径,它作为用户的登录shell。一个空的shell域的默认值通常为/bin/sh。虽然这样,注意,项rwhod有一个/dev/null的登录shell。显然,这是一个设备,而且不能执行,所以它这里的使用是阻止任何人作为用户rwhod登录我们的系统。许多服务都为后台进程(第13章)有独立的用户ID,来帮助实现这个服务。
5、/dev/null有几个替代品,都可以阻止特定用户登录一个系统。常见的有/bin/false,它简单地退出并返回一个不成功(非0)的状态;shell视这个状态为false。另一个常见的是/bin/true,它做的只是返回一个成功(0)状态。一些系统提供nologin命令,它打印一个可定制的错误信息,并返回一个非0状态并退出。
6、nobody用户名可以用来登录一个系统,但它的用户ID(65534)和组ID(65534)没有提供任何权限。这个用户ID和组ID能访问的文件只有对所有人可读可写的文件。(它假设没有任何属于用户ID65534和组ID65534的文件,这也应该是这样。)
7、一些提供finger命令的系统在注解域支持额外的信息。这些域的由冒号分隔:用户名、办公地点、办公电话以及家庭电话。此外,在注释域里的一个与号(ampersand,&)被一个(转换的)用户名通过一些工具代替。例如,我们可能有:
someone:x:988:1004:Mr. Someone, Shang hai NanJing Rd., 021-12345678, 021-87654321:/home/here:/bin/null
通过finger命令来打印someone的信息:
$ finger -p someone
Login: someone                    Name: Mr. Someone
Directory: /home/here                   Shell: /bin/null
Office:  Shang hai NanJing Rd.        Office Phone:  021-12345678
Home Phone:  021-87654321
Never logged in.
No mail.
即使你的系统不支持finger命令,这些域仍然可以在注释域中,只是这些域只是简单一个注解,而不被系统工具解释。

一些系统提供了vipw命令,允许管理员修改密码文件。vipw命令把改变序列化到密码文件,并保证额外的文件与做过的改变一致。这在通过GUI提供相似功能的系统上也很普遍。

POSIX.1只定义了两个函数来从密码文件中得到项。这些函数允许我们通过用户名或用户ID查找一个项。

#include <pwd.h>

struct passwd *getpwuid(uid_t uid);

struct passwd *getpwnam(const char *name);

两者成功都返回指针,失败返回NULL。

getpwuid函数被ls程序使用,来把包含在一个i-node里的用户ID数值映射到一个用户登录名。getpwnam函数被login函数使用,当我们输入我们的登录名的时候。

两个函数都返回一个指向passwd结构体的指针,并填满它。这个结构体通常是这个函数的一个静态变量,所以它的内容在每次我们调用这些函数的时候会被覆写。

这两个POSIX.1函数在我们要查找登录名或用户ID时都很好,但一些程序想遍历整个密码文件。有以下三个函数可以用:

#include <pwd.h>

struct passwd *getpwent(void);

成功返回指针,失败或文件结尾返回NULL。

void setpwent(void);

void endpwent(void);

这三个函数不是基本POSIX.1的一部分,而是定义在SUS的XSI扩展里。如此,所有UNIX系统都需要支持它们。

我们调用getpwent函数来返回密码文件的下个项。和那两个POSIX.1函数一样,getpwent返回一个由它填满的结构体的指针。这个结构体通常在我们每次调用这个函数的时候被覆写。如果这是第一次调用这个函数,它会打开任何它使用的文件。当我们使用这个函数时没有隐含的顺序:这些项可以是任何顺序,因为一些系统使用文件/etc/passwd的哈希版本。

函数setpwent回退任何它使用的文件,而endpwent关闭这些文件。当使用getpwent时,我们必须总是保证在完成后调用endpwent来关闭它们。尽管getpwent足够智能,知道它何时必须打开它的文件(我们每一次调用它的时候),但它从不知道我们什么时候完成。

下面的代码是getpwnam的一个实现:

#include <pwd.h>
#include <stddef.h>
#include <string.h>

struct passwd *
getpwnam(const char *name)
{
    struct passwd *ptr;

    setpwent();
    while((ptr = getpwent()) != NULL)
        if (strcmp(name, ptr->pw_name) == 0)
            break;      /* founc a match */
    endpwent();
    return(ptr);        /* a ptr is NULL if no match found */
}
开头的setpwent调用是一个自我防御机制:我们保证文件被回退,以防调用者已经通过调用getpwent函数打开了这些文件。完成时的endpwent的调用是因为getpwnam或getpwuid应该关闭所有打开的文件。

6.3 影子文件(Shadow Passwords)

加密密码是通过一个单向加密算法的用户密码的一个拷贝。因为这个算法是单向的,我们不能从加密的版本猜测原始密码。

历史上,这个使用的算法一直生成13个可打印的64字符集[a-zA-Z0-9./]中的字符。一些更新的系统使用一个MD5算法来加密密码,为每个密码生成31个字符。(使用越多字符存储加密密码,会有越多的组合方式,这样通过尝试所有可能的变体来猜测密码会更难。)当我们在加密密码域里放入单一字符时,我们保证一个加密密码绝不会匹配这个值。

给定一个加密密码,我们不能应用一个把它回转并返回普通文本密码的一算法。(普通文本密码是我们在Password:提示里输入的东西。)然而我们可以猜一个密码,通过一个单向算法加密它,然后把结果与加密密码比较。如果用户密码被随机选择,这种暴力方式不会太成功。然而,用户倾向于选择一些非随机密码,比如配偶的名字,街道名或宠物名。一些人得到密码文件拷贝的一个普遍经历就是尝试猜测这些密码。

为了更难获取裸材料(加密好的密码),系统现在把加密密码存在另一个文件里,通常被称为影子密码文件。这个文件最少必须包含用户名和加密密码。其它密码相关的信息同样也存在这里。看下表:

文件/etc/shadow的域
描述结构体spwd成员
用户登录名char *sp_namp
加密密码char *sp_pwdp
最后修改密码的自Epoch的天数int sp_lstchg
直到改变允许的天数int sp_min
需要改变之前的天数int sp_max
警告密码到期的天数int sp_warn
帐号失效前的天数int sp_inact
帐号过期的自Epoch距今的天数int sp_expire
保留unsigned int sp_flag
唯一两个强制的域是用户登录名和加密密码。其它的域控制了密码多久改变一次--被熟知为“密码年龄(password aging)”--以及一个帐号被允许存在多久。

影子密码文件不应该被所有人读。只有很少的程序需要访问加密密码--例如login和passwd--而这些程序通常是设置用户ID的根用户。有了影子密码、普通密码文件/etc/passwd可以被所有人读。

在Linux2.4.22和Solaris 9,一个独立的函数休可能用来访问影子密码文件,与访问密码文件的函数集相似。

#include <shadow.h>

struct spwd *getspnam(const char *name);

struct spwd *getspent(void);

两者成功时都返回指针,否则返回NULL。

void setspent(void);

void endspent(void);

在FreeBSD 5.2.1和Mac OS X 10.3,没有影子密码结构体。额外的帐号信息被存储在密码文件里。

6.4 组文件

UNIX系统的组文件,被POSIX.1称为组数据库,包含了下表展示的域。这些域被一个group结构体包含,定义在<grp.h>里。

/etc/group文件的域
描述结构体group的成员POSIX.1FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
组名char *gr_name*****
加密密码char *gr_passwd ****
数值化组IDint gr_gid*****
指向各个用户名的指针数组char **gr_mem*****

域gr_mem是一个指向属于该组的用户名的指针数组。这个数组由空指针结束。

我们可以用下面两个函数查找组名或组ID,它们定义在POSIX.1里:
#include <grp.h>

struct group *getgrgid(gid_t gid);

struct group *getgrnam(const char *name);

两者成功都返回指针,否则返回NULL。

和密码文件函数一样,这两个函数通常都返回静态变量的指针,每次调用时都被覆写。

如果我们要查找整个组文件,我们需要一些额外函数。以下三个函数与它们相对的密码文件函数相似:

#include <grp.h>

struct group *getgrent(void);

成功返回指针,失败或文件尾返回NULL。

void setgrent(void);

void endgrent(void);

这三个函数不是基本POSIX.1标准的一部分,而是SUS的XSI扩展里定义的。所有UNIX系统都提供了它们。

setgrent函数打开组文件,哪果它还没有被打开的话。getgrent函数从组文件中读取下一个项,首先打开这个文件,如果它还没打开的话。endgrent函数关闭这个组文件。

6.5 补充组ID(Supplementary Group IDs)

UNIX系统里组的使用随着时间发生了变化。在版本7,每个用户在任何时刻只属于某一个组。当我们登录的时候,我们被分配给真实组ID,对应于我们密码文件项的数值组ID。我们可以在任何时候执行newgrp来修改它。如果newgrp命令成功(参考权限规则的手册页),我们的真实组ID被改为新组的ID,而这会被后续的所有文件访问权限检查所使用。我们总是可以回到我们原始的组,通过执行无任何参数的newgrp。

这个组成员关系直到4.2BSD(1983年前后)才发生变化。在4.2BSD里,引入了补充组ID的概念。我们不仅属于对应于密码文件项里组ID表示的组,同时还属于最多16个补充的组。文件访问权限检查被修改,以致不仅有效组ID会与文件组ID比较,同时所有的补充组ID也会和文件组ID比较。

补充组ID是POSIX.1所要求的一个特性。(在POSIX.1的早期版本,它们是可选的。)常量NGROUPS_MAX(第2章)指定了补充组ID的数量。一个普遍的值是16。

使用补充组ID的好处是我们不再需要显式地改变组。在同一时间同时属于多个组(也就是说,参与了多个项目)并不是不常见。

有三个函数用来得到和设置补充组ID。

#include <unistd.h>

int getgroups(int gidsetsize, gid_t grouplist[]);

成功返回补充组ID的数量,错误返回-1。

#inlcude <grp.h>  /* on Linux */

#inlcude <unistd.h> /* on FreeBSD, Mac OS X, and Solaris */

int setgroups(int ngroups, const gid_t grouplist[]);

#inlcude <grp.h> /* on Linux and Solaris */

#inlcude <unistd.h> /* on FreeBSD and Mac OS X */

int initgroups(const char *username, gid_t basegid);

两者成功都返回0,否则返回-1。

在这三个函数中,只有getgroups是POSIX.1指定的。因为setgroups和initgroups是需要权限的操作,它们不是POSIX.1的一部分。然而,本文提到的四个平台都支持了这三个函数。

在Mac OS X 10.3,basegid被定义为int类型。

getgroups函数用补充组ID填充了数组grouplist。最多gidsetsize个元素被存储到这个数组里。函数返回被存储到数组的补充组ID的数量。

作为一个特殊情况,如果gidsetsize为0,函数只返回补充组ID的数量。数组grouplist不会被修改。(这允许调用都决定要分配的grouplist数组的大小。)

setgroups函数可以被超级用户调用来为调用进程设置补充组ID列表:grouplist包含了组ID的数组,而ngroups指定了数组元素的数量。ngroups的值不能比NGROUPS_MAX大。

setgroups唯一的使用经常是通过initgroups函数,它读取整个组文件--我们早先描述的通过函数getgrent、setgrent和endgrent--并决定username的组成员关系。它接着调用setgroups来为这个用户初始化补充组ID。必须是超级用户才能调用initgroups,因为它调用了setgroups。除了组文件里找到所有username所属的组,initgroups还把basegid包含在补充组ID列表里,basegid是密码文件的username的组ID。

initgroups只被很少的程序调用:例如loing程序当我们登录时调用它。

6.6 实现区别(Implementation Differences)

我们已经讨论过Linux和Solaris支持的影子密码文件。FreeBSD和Mac OS X用另一种方式存储加密密码。下表总结了这本文包括的四个平台如何存储用户和组信息:

帐号实现的区别
信息
FreeBSD 5.2.1
Linux 2.4.22
Mac OS X 10.3
Solaris 9
帐号信息/etc/passwd/etc/passwdnetinfo/etc/passwd
加密密码/etc/master.passwd/etc/shadownetinfo/etc/shadow
是否哈希密码文件
组信息/etc/group/etc/groupnetinfo/etc/group

在FreeBSD上,影子密码文件是/etc/master.passwd。特殊的命令被用来编辑它,从影子密码文件产生一个/etc/passwd的拷贝。此外,文件的哈希版本也被产生:/etc/pad.db是/etc/passwd的哈希版本,而/etc/spwd.db是/etc/master.passwd的哈希版本。这些为大的安装提供了很好的性能。

在Mac OS X上,/etc/passwd和/etc/master.passwd却只用在单一用户模式(当系统用于内部维护;单用记模式通常意味着没有系统服务被启用。)在多用户模式--在普通操作期间--netinfo目录服务提供了用户和组的帐号信息的访问。

尽管Linux和Solairs支持类似的影子密码接口,但仍然有些小区别。例如,/etc/shadow文件的域里的整型域在solaris里定义为int类型,而在Linux里定义为long int。另一个区别是帐号失效域。Solaris把它定义为自从用户上次登录至今的天数,而Linux把它定义为最大密码年龄到达之后的天数。

在许多系统上,用户和组数据库使用网络信息服务(Network Information Service,NIS)实现。这允许管理员编辑这些数据库的母拷贝(master copy)并把它们自动分发到一个组织里的所有服务器上。客户端系统与服务器联系来查询用户和组的信息。NIS+和轻量目录访问协议(Lightweight Directory Access Protocol, LDAP)提供了相似的功能。许多系统控制着用来管理配置文件/etc/nsswitch.conf的信息的所有类型的方法。

6.7 其它数据文件

我们迄今已经讨论过两个系统数据文件:密码文件和组文件。UNIX系统在日常操作中使用了许多其它文件。例如,BSD网络软件有一个由各种网络服务器提供为服务的一个数据文件(/etc/services),一个为协议提供的文件(/etc/protocols),和一个为网络提供的文件(/etc/networks)。幸运的是,这些各类文件的接口和我们已经讨论过的密码和组文件的接口相似。

基本原则是每个数据文件有至少三个函数:
1、get函数,读取下一个记录,必要时打开这个文件。这些函数通常返回一个结构体的指针。当到达文件尾时一个空指针被返回。多数get函数返回一个静态结构体的指针,所以我们如果想保存它则总是需要拷贝它。
2、set函数,如果文件没打开的话打开这个文件,并回退这个文件。这个函数在我们知道我们想从文件开头重新开始时被使用。
3、end项,关闭数据文件。正如我们早先提到的,我们总是需要在完成工作时调用它,来关闭所有的文件。

此外,如果数据文件支持一些关键字查找的格式,有些提供的函数可以根据关键字查找一个记录。例如,两个关键字查找函数为密码文件而提供:getpwnam根据用户名查找一个记录,而getpwuid根据用户ID查找一个记录。

下表展示了一些这样的函数,它们在UNIX上很普遍。在这个表时,我们展示密码文件和组文件的函数,我们在本章的更早部分讨论过,还有一些网络函数。这张表里有所有数据文件的get、set和end函数:

访问系统数据文件的类似函数
描述数据文件头文件结构体补充的关键字查找函数
密码/etc/passwd<pwd.h>passwdgetpwnam, getpwuid
/etc/group<grp.h>groupgetgrnam, getgrgid
影子/etc/shadow<shadow.h>spwdgetspanam
主机/etc/hosts/<netdb.h>hostentgethostbyname, gethostbyaddr
网络/etc/networks<netdb.h>netentgetnetbyname, getnetbyaddr
协议/etc/protocols<netdb.h>protoentgetprotobyname, getprotobynumber
服务/etc/services<netdb.h>serventgetservbyname, getservbyport

在Solaris下,上表的最后四个文件是在目录/etc/inet下的同名文件的符号链接。多数UNIX系统实现有类似的补充函数,但这些补充函数倾向于处理系统管理文件并针对于每个实现。

6.8 登录帐号(Login Accounting)

由多数UNIX系统提供的两个数据文件是utmp文件,它跟踪所有当前登录的用户,和wtmp文件,它记录所有的登入与登出。在版本7,一个记录类型被这两个文件写,一个与下面结构体一致的二进制记录:
struct utmp {
  char ut_line[8]; /* tty line: "tyh0", "ttyd0", "ttyp0", ... */
  char ut_name[8]; /* login name */
  long ut_time; /* seconds since Epoch */
};

当登录时,一个这样的结构体被填充,并由login程序写入到utmp文件里,而同样的结构体被添加到wtmp文件里。在登出时,utmp文件的项被删除--用空字节填充--由init进程,而一个新的项被添加到wtmp文件里。这个在wtmp文件的登出项的ut_name域被清零。一些特殊的项被添加到wtmp文件来指明系统何时重启,和系统时间和日期改变前后的时间。who程序读取utmp文件并以可读格式打印它的内容。UNIX系统的更新版本提供了last命令,来从wtmp文件里读取并打开选择的项。

UNIX系统的多数版本仍提供tmp和wtmp文件,但正如预期的,这些文件的信息量已经增长了。版本7的20字节的结构体在SVR2增长到了36字节,而SVR4的扩展的utmp结构体超过了350字节!

这些记录在Solaris的细节格式在utmpx手册页里给出。在Solaris 9,两个文件都在/var/adm目录下。Solaris在getutx里提供了许多函数来读取这两个文件。

在FreeBSD 5.2.1、Linux 2.4.22、Mac OS X 10.3上,utmp手册页给了这些登入记录的它们版本的格式。这两个文件的路径名为/var/run/utmp和/var/log/wtmp。

6.9 系统识别(System Identification)

POSIX.1定义了uname函数来返回当前主机和操作系统的信息。

#include <sys/utsname.h>

int uname(struct utsname *name);

成功返回非负值,失败返回-1。

我们传递一个utsname结构体的地址,而这个函数把它填满。POSIX.1只定义了这个结构体里的最少的域,它们都是字符数组,根据每个实现来设置各数组的尺寸。一些实现在这个结构体里提供了额外的域。

struct utsname {
  char sysname[];  /* name of the operating system */
  char nodename[]; /* name of hits node */
  char release[]; /* current release of operating system */
  char version[]; /* current version of this release */
  char machine[]; /* name of hardware type */
};

每个字符串都以null终止。本文的四个平台的最大名字长度在下表中列出:

系统识别名的限制
接口最大名字长度
FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
uname25665256257
gethostname25664256256

utsname结构体的信息通常可能用uname命令打印出来。

POSIX.1警告,nodename可能不足以用来引用一个联系网络的主机。这个函数从System V而来,而在过去,nodename元素被用来在一个UUCP网络标识主机。

还要意识到这个结构体里的信息没有给出关于POSIX.1级别的任何作息。它应该使用_POSIX_VERSION得到,如2.6节。

最后,这个函数只给了我们一种得到这个结构体里的信息的方法,而POSIX.1没有指定任何关于初始化这个信息的方法。

历史上,基于BSD的系统提供了gethostname函数来只返回主机的名字。这个名字通常是在TCP/IP网络上的主机名。

#include <unistd.h>

int gethostname(char *name, int namelen);

成功返回0,失败返回-1。

namelen参数指定了name缓冲的尺寸。如果提供了足够的空间,返回的字符串以null结尾。如果没有提供足够的空间,没有指定这个字符串是否以null结尾。

gethostname函数现是POSIX.1的一部分,指定了最大主机名的长度为HOST_NAME_MAX。由本文四个平台支持的最大名字长度在前面的表里给出了。

如果主机连接到一个TCP/IP网络,那么主机名通常是这个主机的完整域名。

有一个hostname命令可以得到或设置主机名。(主机名由超级用户使用一个相似的函数sethostname来设置。)主机名通常在启动时,由/etc/rc或init引入的start-up文件设置。

6.10 时间日期函数(Time and Date routines)

由UNIX内核提供的基本时间服务计算自从Epoch:统一全球时间(Coordinated Universal Time, UTC)1970-1-1 00:00:00至今经过的秒数。在1.10节,我们说过这些秒数用time_t数据类型表示,而我们称它们为日历时间。这些日历时间同时表示时间和日期。UNIX系统总是和其它操作系统有所区别:a、保留UTC时间而不是本地时间;b、自动完成转换,比如夏令时时间;c、把时间和日期作为单一量存储。

time函数返回当前的时间和日期。

#include <time.h>

time_t time(time_t *calptr);

成功返回时间值,错误返回-1。

这个时间值总是作为函数的值返回。如果参数是一个非空值,时间值也被存储在calptr指向的地点里。

我们没有说过内核如何初始化当前时间。历史上,在从System V继承下来的实现上,stime函数被调用,而基于BSD的系统使用settimeofday。

SUS没有指定系统如何设置它的当前时间。

gettimeofday函数提供了比time函数更好的精度(精确到微秒)。这对于一些应用程序很重要。

#include <sys/time.h>

int gettimeofday(struct timeval *restrict tp, void *restrict tzp);

总是返回0。

这个函数定义在SUS的XSI扩展里。tzp的唯一合法值是NULL。其它值会导致未指定的行为。一些平台通过使用tzp支持一个时区的指定,然而这是系统指定的而不是SUS定义的。

gettimeofday函数把以Epoch衡量的当前时间存储在由tp指向的内存里。这个时间被表示为一个timeval结构里,它存储秒数和微秒数:
struct timeval {
  time_t tv_sec;  /* seconds */
  long tv_usec; /* microseconds */
}:

一但我们有了这个计算从Epoch至今的秒数的整型值,我们可以调用其它函数来把它转换成人可读的时间和日期。

localtime和gmtime这两个函数把日历时间转换了一个被称为分解时间(broken-down time)的一个结构体tm。

struct tm {  /* a broken-down time */
  int tm_sec;  /* seconds after the minute: [0 - 60] */
  int tm_min;  /*  minutes after the hour: [0-59] */
  int tm_hour;  /* hours after midnight:[0-23] */
  int tm_mday;  /* day of the month: [1-31] */
  int tm_mon;  /* months since January: [0-11] */
  int tm_year;  /* years since 1900 */
  int tm_wday;  /* days since sunday: [0-6] */
  int tm_yday;  /* days since January 1: [0-365] */
  int tm_isdst;  /* daylight saving time flag: <0, 0, >0 */
};

秒数能大于59的原因是为了允许闰秒(leap second)。注意除了月份日之外的所有域都是基于0的。夏令时标志为正表示夏令时有效,0表示无效,负值表示些信息不可用。

在SUS的前一个版本,双闰秒(double leap seconds)被允许。这样,tm_sec成员的合法范围是0-61。UTC的格式定义不允许双闰秒,所以秒的合法值现在被定义为0-60。

#include <time.h>
struct tm *gmtime(const time_t *calptr);

struct tm *localtime(const time_t *calptr);

两者都返回分解时间的指针。

localtime和gmtime的区别在于第一个把日历时间转换为本地时间,根据时区和夏令时标志,而后者把日历时间转换成一个表示为UTC的分解时间。

函数mktime接受一个表示为本地时间的分解时间,并把它转换成一个time_t值。

#include <time.h>

time_t mktime(struct tm *tmptr);

成功返回日历时间,错误返回-1

asctime和ctime函数生产熟悉的26字节字符串,它与date命令的默认输出类似:
2012年 02月 27日 星期一 17:15:51 CST

#include <time.h>

char *asctime(const struct tm *tmptr);

char *ctime(const time_t *calptr);

两者都返回以null结尾的字符串。

asctime的参数是一个指向分解时间字符串的指针,而ctime的参数是一个指向日历时间的指针。

最后一个时间函数,strftime,是最复杂的。它是一个对于时间值类似于printf的函数。

#include <time.h>

size_t strftime(char *restrict buf, size_t maxsize, const char *restrict format, const struct tm *restrict tmptr);

如果空间足够返回存储在数组里的字符数,否则返回0。

最后的参数是格式所需的时间值,由一个分解时间值的指针指定。格式化的结构存储在尺寸为maxsize的buf数组里。如果包括终止null的结果的尺寸,可以放入这个缓冲,那么函数返回在buf里存储的字符数,不包括终止null。否则,该函数返回0。

参数format控制这个时间值的格式化。和printf函数相似,转换指示符由一个百分号接着一个特殊的字符给定。格式化字符串的所有其它字符被拷贝到输出中。一行内的两个百分号在输出中产生单个百分号。不像printf函数,每个指定的转换产生一个不同的固定大小的输出字符串--在格式化字符串里没有域长度。下表描述了37个ISO C的转换指示符。该表的第三列是Linux下的strftime输出,对应时间日期为Tue Feb 10 18:27:38 EST 2004。

   
格式描述例子
%a缩写的星期名Tue
%A完全星期名Tuesday
%b缩写的月名Feb
%B完全月名February
%c日期与时间Tue Feb 10 18:27:38 2004
%C年份除以100: [00-99]20
%d月内日期:[01-31]10
%D日期[MM/DD/YY]02/10/04
%e月内日期(单个数字用空格开头)[1-31]10
%FISO 8601日期格式[YYYY-MM-DD]2004-02-10
%gISO 8601基于星期的年份的后两个数字[00-99]04
%GISO 8601基于星期的年份2004
%h与%b相同Feb
%H一天内的小时(24小时格式):[00-23]18
%I一天内的小时(12小时格式):[01-12]06
%j一年内的天数[001-366]041
%m月份:[01-12]02
%M分:[00-59]27
%n换行符 
%pAM/PMPM
%r本地时间(12小时格式)06:27:38 PM
%R和“%H:%M”相同18:27
%S秒:[00-60]38
%t水平制表符 
%T和“%H:%M:%S”相同18:27:38
%uISO 8601星期[周一=1,1-7]2
%U周日星期数:[00-53]06
%VISO 8601星期数:[01-53]07
%w星期:[0=周天,0-6]2
%W周一星期数:[00-53]06
%x日期02/10/04
%X时间18:27:38
%y年份后两位:[00-99]04
%Y年份2004
%zUTC以ISO 8601格式的偏移量-500
%Z时间区EST
%%翻译为一个百分号%

不明朗的指示符只有%U、%V和%W。%U指示符表示年内的星期数,包含第一个星期天的星期被作为第一个星期。%W指示符表示年内的星期数,包含第一个周一的是第一个星期。%V指示符不同。如果包含新年一月的第一天的星期有四天或更多天,这个星期被作为第一个星期。不然,它作为去年的最后一个星期。在两种情况下,周一都作为星期的第一天。

和printf一样,strftime支持了一些转换指示符的修改符。E和O修改符可以用来产生替代格式,如果本地化被支持的话。

一些系统在strftime的格式化字符串时支持额外的,非标准的扩展。

localtime、mktime、ctime和strftime被TZ环境变量影响。如果定义的话,这个环境变量的值被这些函数使用代替默认的时区。如果变量被定义为空字符串,比如TZ=,那么UTC通常被使用。这个环境变量的值通常是像TZ=EST5EDT的东西,但POSIX.1允许细节地多的指定。参考SUS的环境变量那一样来得到TZ变量的所有细节。

本节描述的所有的时间和日期函数,除了gettimeofday,都定义在ISO C标准里。然而,POSIX.1加入了TZ环境变量。在FreeBSD 5.2.1、Linux 2.4.2和Mac OS X 10.3上,TZ变量的更多信息可以在tzset手册页上找到。在Solaris 9,这个信息在environ手册页里。

6.11 总结

密码文件和组文件在所有UNIX系统上被使用。我们已经看过读这些文件的各种函数。我们也说过影子密码,它能帮助系统安全。补充组ID提供了一个在同一时刻参与多个组的方法。我们也看到由多数系统提供的访问其它系统相关的数据文件的函数是如何相似。我们讨论了程序可以用来标识他们正在运行的系统的POSIX.1函数。我们在本章末看了由ISO C和SUS提供的时间和日期函数。

第七章 进程环境(Process Environment)

7.1 引言

在下章讨论进程控制前,我们需要检查下单一进程的环境。在本章,我们将看到当程序执行时main函数是如何被调用的;命令行参数是如何传递给新的程序;典型的内存布局看起来如何,怎么开辟额外的内存;进程如何使用环境变量;进程终止的各种方法。此外,我们将看到longjmp和setjmp函数和它们与栈的交互。我们在本章末检查一个进程的资源限制。

7.2 main 函数

一个C函数从执行一个名为main的函数开始。main函数的原型为:
int main(int argc, char *argv[]);

argc是命令行参数的数量而argv是参数指针的数组。我们在7.4节讨论这些参数。

当一个C程序被内核执行时--通过一个exec函数,我们在8.10节讨论--一个特殊的启动程序在main函数调用前被调用。这个可执行程序文件指定了这个程序,作为程序的开始地址;这是由C编译器调用的链接编辑器设置的。这个启动程序从内核得到几个值--命令行参数和环境--然后把事情设置好以便main函数像之前所述的那样被调用。

7.3 进程终止

有8种方法终止一个进程。普通终止有5种:

1、从main函数中返回;

2、调用exit;

3、调用_exit或_Exit;

4、最后线程从start函数返回;(11.5节)

5、从最后线程里调用pthread_exit(11.5节)

异常终止有3种:

6、调用abort(10.17节)

7、收到一个信号(10.2节)

8、最后线程回应一个取消请求(11.5节和12.7节)

目前,我们将忽略这三个与线程相关的终止方法,直到我们在11章和12章讨论线程。

我们在前一节提到的启动程序通常被写为:当main函数返回时,exit函数被调用。如果启动程序用C编码(它通常用汇编来编码),main函数的调用可能看起来像:
exit(main(argc, argv));

Exit 函数

三个普通终止程序的函数:_exit和_Exit从内核立即返回;eixt执行特定清理处理然后从内核返回。

#include <stdlib.h>

void exit(int status);

void _Exit(int status);

#include <unistd.h>

void _exit(int status);

我们将在8.5节讨论这三个函数在其它进程,比如终止进程的子进程和父进程,上的效果。

不同头文件的原因是exit和_Exit由ISO C规定而_exit由POSIX.1规定。

历史上,exit函数问题执行一个标准I/O库的清理:fclose函数被调用来关闭所有打开的流。回想下5.5节,这会使所有缓冲的输出数据被冲洗(写入文件)。

这三个exit函数都期望一个整型参数,我们称它为退出状态。多数UNIX系统的外壳提供一种检查进程退出状态的方法。如果a、这些函数中任何一个被调用而没有退出状态,b、main返回而没有返回值,或者c、main函数没有被声明为返回一个整型值,那么进程的退出状态是无定义的。然而,如果main的返回类型是一个整型数而main从函数掉出来(一个隐含的return),那么进程的退出状态为0。

这是在ISO C标准的1999版本的新行为。历史上,如果main函数到达结束位置,而没有显式调用一个return语句或exit函数,退出状态是无定义的。

从main函数里返回一个整型值等价于用相同值调用exit。因而exit(0);和main函数里的return(0);相同。

看下经典的hello world程序:

#include <stdio.h>

main()
{
    printf("hello, world\n");
}
当我们编译并运行这个程序时, 我们可以看到返回代码是一个随机值。如果我们在不同的系统上编译这个相同的程序,很可能会得到不同的返回代码,这取决于main函数返回时栈和寄存器的内容。

$ cc hello_world.c
$ ./a.out
hello, world
$ echo $?
13

启用1999 ISO C编译扩展:
$ cc -std=c99 hello_world.c
hello_world.c:3:1: warning: return type defaults to ‘int’
$ ./a.out
hello, world
$ echo $?
0

注意当雇用1999 ISO C扩展时编译器警告出现。这是因为main函数的返回值没有显式声明为一个整型。如果我们加上这个声明,那这个消息就不会出现。然而,如果我们启用所有推荐的错误(用-Wall标志),那么我们将会看到类似于“control reaches end of nonvoid function.”的消息。(我在gcc version 4.5.2 (Ubuntu/Linaro 4.5.2-8ubuntu4)上没有看到这个消息。)

main的作为整型返回的声明和使用exit而不是return从一些编译器和lint程序产生不需要的警告。问题是编译器不知道从main函数的exit和一个return是相同的。一种处理这些警告的方式,在一段时间后会变得很烦,是在main里用return而不是exit。但这样做会阻止我们使用UNIX系统的grep工具来定位一个程序里的所有的exit调用。另一个解决办法是把main声明为void,而不是int,然后继续调用exit。这处理了编译器警告,但看起来不对(特别是在编程文本里),而且能产生其它的编译错误,因为main的返回值应该是一个有符号的整型。在本文,我们让main返回一个整型,因为这是ISO C和POSIX.1共同定义的。

不同的编译器的警告有不同的信息。注意GNU C编译器通常不产生这些不重要的警告信息,除非额外的警告选项被指定。

atexit函数

在ISO C,一个进程可以最多注册32个自动被exit函数调用的函数。这些被称为exit处理器,并通过调用atexit来注册。

#include <stdlib.h>

int atexit(void (*func)(void));

成功返回0,错误返回非0值。

声明说我们传递一个函数地址作为atexit的参数。当这个函数被调用时,不传入任何参数也不返回任何值。exit函数以它们注册的顺序的相反顺序调用这些函数。每个函数都被调用和它被注册的一样多的次数。

这些退出处理器第一次出现在1989年的ANSI C标准里。在ANSI C之前的系统,比如SVR3和4.3BSD,没有提供这些退出处理器。

ISO C要求系统支持至少32个退出处理器。sysconf函数可能用来决定一个给定系统支持的退出处理器的最大数量。

在ISO C和POSIX.1,exit首先调用退出处理器,然后(通过fclose)关闭所有打开的文件,最后调用_exit或_Exit回到内核。POSIX.1扩展了ISO C标准,指出如果程序调用任何一个exec家族的函数,那么任何安装的退出处理器将被清理掉。

注意内核执行一个程序的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是调用_exit或_Eixt,不管是显式地还是隐式地(通过调用exit函数)。一个进程也可以被一个信号非自愿地终止。

下面的代码使用了atexit函数:

#include <stdio.h>

static void my_exit1(void);
static void my_exit2(void);

int
main(void)
{
    if (atexit(my_exit2) != 0) {
        printf("can't register my_exit2");
        exit(1);
    }

    if (atexit(my_exit1) != 0) {
        printf("can't register my_exit1");
        exit(1);
    }
    if (atexit(my_exit1) != 0) {
        printf("can't register my_exit1");
        exit(1);
    }

    printf("main is done\n");
    return(0);
}

static void
my_exit1(void)
{
    printf("first exit handler\n");
}

static void
my_exit2(void)
{
    printf("second exit handler\n");
}
$ ./a.out
main is done
first exit handler
first exit handler
second exit handler
一个退出处理器对于每次注册都被调用一次。上面的代码里,第一个退出处理器被注册了两次,所以它也被调用了两次。注意我们没有调用exit,相反,我们从main里返回。

7.4 命令行参数

当一个程序被执行时,使用exec的进程可以传递命令行参数给这个新的程序。这是UNIX系统外壳的普通操作的一部分。我们在前面章节中的很多例子里已经看到过。

看下面的例子:

#include <stdio.h>

int
main(int argc, char *argv[])
{
    int i;

    for (i = 0; i < argc; i++)  /* echo all command-line args */
        printf("argv[%d]: %s\n", i, argv[i]);
    exit(0);
}
$ ./a.out 123 hello good "bye bye"
argv[0]: ./a.out
argv[1]: 123
argv[2]: hello
argv[3]: good
argv[4]: bye bye

我们被ISO C和POSIX.1两者保证argv[argc]是一个空指针。这允许我们用另一种方式编码参数处理循环:
for (i = 0; argv[i] != NULL; i++)

7.5 环境列表(Environment List)

每个程序还被传入一个环境列表。就像参数列表那样,环境列表是一个字符指针的数组,每个指针包含一个以null终止的C字符串的地址。这个指针数组的地址包含在全局变量environ里:
extern char **environ;

例如,如果环境由5个字符串组成,那environ就指向一个长度为5的指针数组。数组里的每个地址都指向一个如“HOME=/home/tommy\0”形式的字符串。这里显式地给出最后的终止符。

我们称environ为环境指针,称指针数组为环境列表 。称它们指向的字符串为环境字符串。

根据协议,环境由字符串name=value组成。多数预定义的名字是完全大写的,但这只是一个协议而已。

历史上,多数UNIX系统 ,为main函数提供第三个参数,它是环境列表的地址:
int main (int argc, char *argv[], char *envp[]);

因为ISO C指出,main 函数有两个参数,而又因为第三个参数没有提供比全局变量environ更好的功能,所以POSIX.1 指出environ应该用来代替那个(可能的)第三个参数。访问特定的环境变量通常是通过getenv和putenv函数,在7.9节讨论,而不是通过environ变量。然而要遍历整个环境变量,environ指针必须被使用。

7.6 C程序的内存布局

历史上,一个C程序可以由以下部分组成:

1、代码段(text segment),CPU执行的机器指令。通过,代码段是可共享的,以便经常执行的程序只需在内存里单个拷贝,比如文本编辑器,C编译器,外壳,等等。还有代码段通常是只读的,为了阻止一个程序偶然修改了它的指令。
2、初始化的数据段(Initialized data segment),通常简称为数据段,包括在程序里特别初始化的变量。例如,C出现在任何函数外的声明int maxcount = 99;会导致这个变量以其初始值存储在初始数据段里。
3、未初始化的数据段(Uninitialized data segment),经常被称为“bss”段,在代表“block started by symbol”的古老的汇编操作之后命令。在这个段的数据被内核在程序开始执行前初始化为数字0或null指针。出现在任何函数外的C声明long sum[1000];导致这个变量被存储在未初始化的数据段里。
4、栈,存储自动变量和每次一个函数调用时保存信息的地方。每次一个函数被调用时,它要返回到的地址和关于调用者环境的特定信息,比如一些机器寄存器,被保存在栈里。新调用的函数然后在栈上为自动和临时变量开辟空间。这是在C里的递归函数如何工作的。每次一个递归函数调用它自身时,一个新的栈框架被使用,所以一堆变量不会和这个函数的其它实例的变量冲突。
5、堆,动态内存分配通常发生的地方。历史上,堆一直放在未初始化数据和栈之间。

这些段的典型布局是:最低地址是代码段,其上是初始化数据,再上是未初始化数据,最高地址是命令行参数和环境变量,其下是栈,在栈和bss段之间是堆。这是一个程序看起来的逻辑图,没有要求说一个给定的实现必须以这种风格排列它的内存。尽管如此,这给了我们一个可以描述的典型的排列。在Intel x86处理器上的Linux上,代码段从地址0x8048000开始中,而栈的底部从0xC0000000开始。(在这个特定的架构上,栈从高位地址向地位地址增长。)在堆的顶部和栈的顶部之间的空间是巨大的。

一些存在于一个a.out里的更多的段类型,包括符号表,调试信息,为动态共享库的链接表,等等。这些额外的段没有被载入作为被进程执行的程序映像的一部分。

注意未初始化数据段的内容没有存储在磁盘上的程序文件里。这是因为内核在程序开始运行时设置它为0。需要保存在程序文件部份只有代码段和初始化数据。

size命令报告代码、数据和bss段的(字节)尺寸。例如:
$ size /usr/bin/cc /bin/sh
   text       data        bss        dec        hex    filename
 296400       2000       5736     304136      4a408    /usr/bin/cc
  93358        900      10188     104446      197fe    /bin/sh

第三和第四列是前三个尺寸的总数。分别以十进制和十六进制表示。

7.7 共享库(Shared Libraries)

当今多数UNIX系统支持共享库。Arnold[1986]描述了一个在System V下的早期实现,而Gingell等[1987]描述了在SunOS下的一个不同的实现。共享库从可执行文件中删除了通用的库程序,取而代之的是维护一个所有进程引用的内存里某处的库程序的单一拷贝。这减少了每个可执行程序文件的尺寸,但增加了运行时的开销,当程序第一次被执行或每个共享库函数被调用时。共享库的另一个好处是库函数可以用新的版本代替,而不用重链接每个使用这个库的程序。(这假设参数的数量和类型没有改变。)

不同的系统为一个程序提供了不同的方法来表明它是否想使用共享库。cc和ld命令的选项是典型的。作为一个尺寸区别的例子,下面的可执行文件--经典的hello world程序--不使用共享库而被创建:
$ cc -static hello_world.c
$ ls -l a.out
-rwxrwxr-x 1 tommy tommy 644429 2012-02-27 21:49 a.out
$ size a.out
   text       data        bss        dec        hex    filename
 575523       2056       7048     584627      8ebb3    a.out

如果我们使用共享库来编译这个程序,这个可执行程序的代码和数据尺寸会大幅减少:
$ cc hello_world.c
$ ls -l a.out
-rwxrwxr-x 1 tommy tommy 7165 2012-02-27 21:51 a.out
$ size a.out
   text       data        bss        dec        hex    filename
   1132        256          8       1396        574    a.out

7.8 内存分配(Memory Allocation)

ISO C为内存分配规定了三个函数:
1、malloc,它开辟指定字节数量的内存。内存的初始值是不确定的。
2、calloc、为指定数量的指定尺寸的对象开辟空间。这个空间被初始化为0。
3、realloc、增加或减少之前开辟的区域。当尺寸增加时,它可能会导致把之前开辟的空间移到其它地方,来在尾部提供额外的空间。还有,当尺寸增加时,在旧对空和新区域尾部之间的空间的初始值是不确定的。

#include <stdlib.h>

void *malloc(size_t size);

void *calloc(size_t nobj, size_t size);

void *realloc(void *ptr, size_t newsize);

三者成功都返回非空指针,错误返回NULL。

void free(void *ptr);

这三个分配内存的函数的返回值被保证为适当对齐的,以便它能用于任何数据对象。比如,如果在一个特定系统上的最限制的对齐请求要求double必须从8的倍数的内存地址开始,那么这三个函数返回的所有指针都会如此对齐。

因为这三个alloc函数返回一个通用的void *指针,所以如果我们#include <stdilb.h>(来得到函数原型),那么当我们把这些函数返回的指针赋给不同类型的指针时,不用显式地转换。

函数free导致ptr指向的空间被释放。这被释放的空间通常放入可用内存的池时,在下次这三个alloc函数的调用时可以被再次分配。

realloc函数让我们增加或减少前一个被分配区域的尺寸。(最普遍的使用是增加一个区域。)例如,如果我们在我们运行时填满的一个数组里为512个元素开辟空间,但是发现我们需要多于512个元素的空间,那么我们可调用realloc。如果已有区域末尾之后有可以符合请求的空间,那么realloc不必移动任何东西,它只简单地在末尾开辟额外的空间然后返回我们传递给它的同样的指针。但是如果在已有区域尾部没有足够的空间,realloc会开辟另一块足够大的空间,并把已有的512个元素的数组拷贝到这块新区域,并释放旧的数据,然后返回新区域的指针。因为区域可能会移动,所以我们不应该拥有这个区域的任何指针。第4章的练习展示了realloc和getcwd一起使用来处理任何长度的路径名。17章展示了一个使用realloc来避免一个固定的编译器尺寸的数组的例子。

注意realloc的最后一个参数是区域的新尺寸,而不是旧尺寸和新尺寸的差别。作为一个特殊的例子,如果ptr是一个空指针,realloc和malloc行为相似,开辟一个指定尺寸的区域。

这些函数的早期版本允许我们来realloc一个我们曾用malloc、realloc或calloc开辟过的但已经释放了的块。这个把戏在版本7上就有,使用malloc的查找策略来执行存储压缩。Solaris仍然支持这个特性,但许多其它平台已经不支持了。这个特性不赞成被使用,而且也不应该被使用。

内存分配函数通常用sbrk系统调用实现。这个系统调用扩展(或缩小)进程的堆。malloc和free的一个样本实现在Kernighan and Ritchie[1988]的8.7节给出。

尽管sbrk可以扩展和缩小进程的内存,malloc和free的多数版本都不减少它们的内存尺寸。我们释放的空间可以用作下次的分配,但释放的空间不会返回给内核,而是被malloc池保存。

多数实现开辟比所请求的稍多一点的空间并使用额外的空间来保存记录--分配了的块的尺寸、指向下一个分配了的块的指针等等。意识到这点是重要的。这意味着在一个分配了的区域末写可能会覆写之更后面的块的记录保存信息。这些错误类型经常是惨重的,却很难发现,因为这个错误可能会等到很晚才会出现。还有,在分配的区域开头之前写数据同样可能会覆写记录保存信息。

在一个动态分配的缓存的末尾后面或开头之前写可以破坏比内部记录保存信息更多。在一个动态分配的缓存之前或之后的内存可能潜在地用作其它的动态分配对象。这些对象可以和破坏它们的代码没有关系,这会导致更难找到破坏的源头。

其它可能的致命的错误是释放一个已经被释放的块,还有释放一个不是通过那三个alloc函数得到的一个指针。如果一个进程调用malloc,但忘记了调用free,它的内存使用会持续增长。这被称为泄露。不调用free来返回不使用的空间的话,进程地址空间的尺寸会缓慢增长,直到没有空闲内存剩余。在这段时间,性能会因为过量的换页开销而降低。

因为内存分配错误很难被追踪,所以一些系统在这些函数的版本上在每次调用这三个alloc函数或free函数时提供额外的检查。函数的这些版本经常通过为链接器引入一个特殊的库来指定。同样还有公开可用的源码,你可以用特殊的标志编译它们以启用额外的运行期检查。

FreeBSD,Mac OS X和Linux通过环境变量的设置来支持额外的调用。此外,选项可以通过符号链接/etc/malloc.conf传递给FreeBSD库。

替代的内存分配器

malloc和free有很多可用的替代品。一些系统已经包含了提供替代的内存分配实现的库。其它系统只提供了标准分配器,让软件开发员在需要时下载替代品。我们在这里讨论一些替代品。

libmalloc

基于SVR-4的系统,比如Solaris,包含了libmalloc库。它提供了一堆匹配ISO C内存分配函数的接口。libmalloc库包括mallopt,一个允许进程设置特定的控制存储分配器的变量的函数。一个称为mallinfo的函数同样可以用来提供内存分配器的统计资料。

vmalloc

Vo[1996]描述了一个允许进程未不同内存区域使用不同的技术的来分配内存的内存分配器。除了vmalloc相关的函数,这个库还提供了ISO C内存分配函数的模拟。

quick-fit

历史上,标准的malloc算法使用最佳适配(best-fit)或最先适配(first-fit)的内存分配策略。快速适配(Quick-fit)比它们都快,但会使用更多内存。Weinstock and Wulf[1988]描述了这个算法,它基于把内存分割为不同尺寸的缓冲,并根据缓冲的尺寸在不同的释放列表上维护未使用的缓冲。基于快速适配的malloc和free的免费实现在几个FTP站点上可读。

alloca函数

还有一个函数值提及。函数alloca有和malloc一样的调用顺序,然而,它不在堆上分配内存,而是在当前函数的栈框架上。好处是我们不必释放内存,它在函数返回时会自动释放。alloca函数增加了栈框架的尺寸。缺点是一些系统如果在函数调用后无法增加栈框架的尺寸,则不支持alloca。尽管如此,许多软件包使用它,而实现存在于各个系统上。

本文讨论的四个平台都支持alloca函数。

7.9 环境变量(Environment Variables)

正如我们早先提到的,环境字符串通常是这样的格式:
name=value

UNIX内核从不看这些字符串,它们的解释交给各个应用程序。例如,shell使用许多环境变量。一些比如HOME和USER的变量在登录时自动设置,而其它的是我们自己设置。我们通常在shell启动文件时设置环境亦是来控制shell的行为。例如,如果我们设置环境变量MAILPATH,它告诉Bourne shell,GNU Bourne-again shell和Korn shell哪里去查找邮件。

ISO C定义了一个我们可以用来从环境获取值的函数,但是标准说环境的内容是由实现定义的。

#include <stdlib.h>

char *getenv(const char *name);

返回和name相关的值的指针,没有找到则返回NULL。

注意这个函数返回一个name=value的字符串的指针。我们应该使用getenv来从环境得到指定的值,而不是直接访问environ。

一些环境变量在SUS里由POSIX.1定义,而其它的只当XSI扩展被支持时才定义。下表列出了由SUS定义的环境变量并标出哪些实现支持这些变量。任何定义在POSIX.1里的环境变量用*标记,否则它是一个XSI扩展。许多补充的实现相关的环境变量在文本讨论的四个平台上使用。注意ISO C没有定义任何环境变量。

SUS定义的环境变量
变量POSIX.1FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9描述
COLUMNS*****终端宽度
DATEMSKXSI * *getdate模板文件路径名
HOME*****主目录
LANG*****本地名
LC_ALL*****本地名
LC_COLLATE*****校验本地名
LC_CTYPE*****字符分类本地名
LC_MESSAGES*****消息本地名
LC_MONETARY*****货币编辑本地名
LC_NUMERIC*****数值编辑本地名
LC_TIME*****时间日期格式本地名
LINES*****终端高度
LOGNAME*****登录名
MSGVERBXSI*  *进程的fmtmsg消息组件
NLSPATHXSI****消息种类的模板序列
PATH*****寻找可执行程序的路径前缀列表
PWD*****当前工作目录的绝对路径名
SHELL*****用户最喜欢的shell
TERM*****终端类型
TMPDIR*****创建临时文件的目录的路径名
TZ*****时区信息

除了从一个环境变量获取值,有时我们还可能想设置一个环境变量。我们可能想改变一个已存在的变量或在环境里加上一个新的变量。(在下一章,我们将看到我们可以影响当前进程和任意我们能调用的子进程的环境。我们不能影响父进程的环境,它通常是shell。尽管如此,能够修改环境列表仍然是有用的。)不幸的是,不是所有系统都支持这种能力。下表展示了由各种标准和实现支持的函数:

各种环境列表函数的支持
函数ISO CPOSIX.1FreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
getenv******
putenv XSI****
setenv **** 
unsetenv **** 
clearenv   *  

clearenv不是SUS的一部分。它用来移除环境列表的所有的项。

上表中间的三个函数的原型是:

#include <stdlib.h>

int putenv(char *str);

int setenv(const char *name, const char *value, int rewrite);

int unsetenv(const char *name);

三者成功返回0,错误返回非0.

这三个函数的操作如下:
1、putenv函数接受一个如name=value格式的字符串,并把它放在环境列表里。如果name已经存在,它的旧的定义会首先被移除。

2、setenv函数给name设置一个value。如果name存在于环境中,那么a、如果rewrite为非0,则存在的name的定义首先被移除;b、如果rewrite为0,name的已存在的定义不被删除,name不会被设置为新的value,也没有错误发生。

3、unsetenv函数删除任何name的定义。如果没有定义存在,也不是一个错误。注意putenv和setenv的区别。setenv必须开辟内存来创建它参数的name=value的字符串,putenv可以把字符串直接传给环境。事实上,Linux和Solaris上,putenv的实现把我们传递给它的字符串的地址直接放入环境列表里。在这种情况下,传递一个在栈内分配的字符串将会是个错误,因为内存在当前函数返回后会被重用。

有意思的事是检查这些函数当修改环境列表时必须怎样操作。回想下,环境列表--指向真实name=value的字符串的指针的数组--和环境字符串通常存储在进程内存空间的顶部,在栈之上。删除一个字符串很简单,我们只需在环境列表内找到这个指针并把所有随后的指针都往下移一格,然而加上一个字符串或修改一个已经存在的字符串要更困难。栈顶部的空间是不能被扩展的,因为它经常在进程的地址空间的顶部,所以不能往上扩展;它也不能往下扩展,因为其下所有的栈框架都不能被移动。

1、如果我们修改一个已存在的名字:
a、如果新的值的尺寸小于或等于已存在的值的尺寸,那么我们可以仅把新字符串拷贝到旧字符串之上。
b、如果新值的尺寸大于旧的尺寸,我们并发调用malloc来为这个新字符串获得空间,把新值拷贝到这个区域,然后把环境列表里的旧指针替换为指向这个开辟的区域的指针。

2、如果我们添加一个新的名字,那这将更复杂。首先,我们并须调用malloc来为name=vlaue的字符串分配空间,然后复制这个字符串到这个区域。
a、然后,如果这是我们第一次加入一个新的名字,我们必须调用malloc来为新的指针列表分配空间。我们把旧的环境列表拷贝到这个新的区域,然后把name=value字符串保存到这个指针列表的末尾。当然,我们同样在这个列表的末尾存储一个空指针。最后,我们把environ设置为指向这个新的指针列表。注意如果原始环境列表存储在栈的顶部,就像普遍的那样,那么我们把这个指针列表移到了堆。但是这个列表的多数指针仍然指向在栈顶部的name=value字符串。
b、如果这不是我们第一次在环境列表里加入新的字符串,那么我们知道我们已经为这个列表在堆里分配了空间,所以我们只调用realloc来为一个更多的指针分配空间。指向这个新的name=value字符串的指针被存储在这个列表的末尾(之前放空指针的位置),随后接一个空的指针。

7.10 setjmp和longjmp函数

在C里,我们不能用goto进入另一个函数的标签。相反,我们必须使用setjmp和longjmp函数来执行这种类型的跳转。正如我们将看到的那样,这两个函数对于处理发生在嵌套地很深的函数调用里的错误情况很有用。

考虑下面代码的seketon。它由一个从标准输入读取行的主循环和处理各行的函数do_line调用组成。这个函数随后调用get_token来从输入行里获取下一个语素。一行的第一个语素被视为某种格式的一个命令,而switch语句选择每个命令。对显示的单个命令,cmd_add函数被调用。

#include <stdio.h>

#define TOK_ADD 5
#define MAXLINE 4096

void do_line(char *);
void cmd_add(void);
int get_token(void);

int
main(void)
{
    char line[MAXLINE];

    while (fgets(line, MAXLINE, stdin) != NULL)
        do_line(line);
    exit(0);
}

char *tok_ptr;  /* global pointer for get_token() */

void
do_line(char *ptr)  /* process one line of input */
{
    int cmd;

    tok_ptr = ptr;
    while ((cmd = get_token()) > 0) {
        switch (cmd) { /* one case for each command */
        case TOK_ADD:
            cmd_add();
            break;
        }
    }
}

void
cmd_add(void)
{
    int token;

    token = get_token();
    /*  rest of processing for this command */
}

int
get_token(void)
{
    /* fetch next token from line pointed to by tok_ptr */
}
上面的代码是典型的程序:读取命令,决定命令类型,然后调用函数来处理每个命令。栈的底部往上分别是main、do_line和cmd_add的栈框架。自动变量存储在每个函数的栈框架里。数据line在main的栈框架里,整型cmd在do_line的栈框架里,而整型token在cmd_add的栈框架里。

正如我们已经说过的,这种栈的排列类型是典型的,但不是必需的。栈不必向更低内存地址增长。在没有支持栈的内置硬件的系统上,C实现可能用一个链表来作为它的栈框架。

就像上述代码一样的程序进程碰到的编码问题是如何处理非致命错误。例如,如果cmd_add函数碰到一个错误--比如一个无效数字--它可以想打印一个错误,忽略输入行的剩余部分,并返回到main函数来读取下个输入行。但是当我们从main函数很深地嵌套了很多层时,在C里面很难做到。(在这个例子里,在cmd_add函数,我们只从main里往下两层,但从我们从想要回到的地方到当前位置有五层或更多并不是不普遍的事。)如果我们必须在每个函数里返回一个特殊的值来返回一层会变得很凌乱。

这个问题的解决方案是使用一个非本地的的goto:setjmp和longjmp函数。形容词“非本地的”是因为我们不能在一个函数里用普通的C goto语句,相反,我们要通过调用框架来跳转到一个当前函数的调用路径里的一个函数。

#include <setjmp.h>

int setjmp(jmp_buf env);

如果直接调用返回0,如果从longjmp调用返回则返回非0。

void longjmp(jmp_buf env, int val);

我们从我

们想回到的地点里调用setjmp,在这个例子里是main函数。这种情况下, setjmp返回0因为我们直接调用它。在这个setjmp的调用里,env参数是一个特殊的类型jmp_buf。这个数据类型是某种格式的数组,能够存储所有所需的信息,当我们调用longjmp时用来恢复栈的状态。通常,env变量是一个全局变量,因为我们将需要从另一个函数里引用它。

当我们碰到一个错误--比如在cmd_add函数里--我们用两个参数调用longjmp。第一个和我们在setjmp调用里一样的env,而第二个,是个作为setjmp的返回值的一个非0值。例如,我们可以从cmd_add用一个值为1的val调用longjmp,也可以从get_token以值为2的val调用longjmp。在main函数里,从setjmp返回的值是1或2,而如果我们愿意的话,可以测试这个值,来决定longjmp是从cmd_add还是get_token出来的。

让我们回到这个例子。下面的代码展示了main和cmd_add函数。(另两个函数,do_line和get_token,没有改变。)

#include <setjmp.h>

jmp_buf jmpbuffer;

int
main(void)
{
    char line[MAXLINE];

    if (setjmp(jmpbuffer) != 0)
        printf("error");
    while (fgets(line, MAXLINE, stdin) != NULL)
        do_line(line);
    exit(0);
}

void
cmd_add(void)
{
    int token;

    token = get_token();
    if (token < 0)
        longjmp(jmpbuffer, 1);
    /*  rest of processing for this command */
}
当main被执行时,我们调用setjmp,它在变量jmpbuffer里记录任何它需要信息并返回0。我们然后调用do_line,它会用cmd_add,并假定察觉到某种形式的一个错误。在cmd_add里调用longjmp之前,栈里有main、do_line和cmd_add函数的框架。但是longjmp导致栈直接回到main函数,把cmd_add和do_line的栈框架给丢弃了。调用longjmp导致main里的setjmp返回,但这次它返回的值为1(longjmp的第二个参数。)

自动、寄存器、和易变变量(Automatic, Register, and Volatile Variables)

我们已经看到调用longjmp之后的栈是怎么样的。下一个问题是:“在main函数里的自动变量和寄存器变量是什么状态?”当通过longjmp回到main,这些变量是当setjmp上次调用时对应的值(也就是说,它们的值被回滚),还是没有被干涉,以致它们的值是当do_line被调用时的任何一个值(do_line调用了cmd_add,而cmd_add调用了longjmp)?不幸的是,答案是“看情况”。多数实现都不尝试回滚这些自动变量和寄存器变量,但是标准只说它们的值是不确定的。如果你有一个不想回滚的自动变量,把它定义成易变变量。被声明为全局或静态的变量当longjmp被执行时不会被干涉。

看下面的例子:

#include <stdio.h>
#include <setjmp.h>

static void f1(int, int, int, int);
static void f2(void);

static jmp_buf jmpbuffer;
static int globval;

int
main(void)
{
    int             autoval;
    register int    regival;
    volatile int    volaval;
    static int      statval;

    globval = 1; autoval = 2; regival = 3; volaval = 4; statval = 5;

    if (setjmp(jmpbuffer) != 0) {
        printf("after longjmp:\n");
        printf("globval = %d, autoval = %d, regival = %d,"
            " volaval = %d, statval = %d\n",
            globval, autoval, regival, volaval, statval);
        exit(0);
    }

    /*
     * change variables after setjmp, but before longjmp.
     */
    globval = 95; autoval = 96; regival = 97; volaval = 98; statval = 99;

    f1(autoval, regival, volaval, statval); /* never returns */
    exit(0);
}

static void
f1(int i, int j, int k, int l)
{
    printf("in f1():\n");
    printf("globval = %d, autoval = %d, regival = %d,"
        " volaval = %d, statval = %d\n",
        globval, i, j, k, l);
    f2();
}

static void
f2(void)
{
    longjmp(jmpbuffer, 1);
}

不使用编译优化的结果:

$ cc longjmp_on_variables.c

$ ./a.out
in f1():
globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99
after longjmp:

globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99

使用编译优化的结果:

$ cc longjmp_on_variables.c -O
$ ./a.out
in f1():
globval = 95, autoval = 96, regival = 97, volaval = 98, statval = 99
after longjmp:
globval = 95, autoval = 2, regival = 3, volaval = 98, statval = 99

注意优化不会影响全局、静态和易变变量。它们的值在longjmp之后和我们假设的最后的值一样。在一个系统上的setjmp手册页表明存储在内存里的变量将会有longjmp调用时的值,而CPU和浮点寄存器里的变量在setjmp调用时会回复它们的值。这些确实是我们上面代码所看到的。在没有优化的情况下,这五个变量都存储在内存里(register提示被忽略)。当我们启用优化时,autoval和regival都放入寄存器里,即使前者没有显式声明为register,而volatile变量保留在内存里。这个例子里要知道的事是你如果在写一个使用非本地跳转的可移植的程序时,你必须使用volatile属性。其它任何东西在各个系统上都会改变。

在上面代码里的一些printf格式字符串太长,不能在程序文本里很好地显示。我们依赖ISO C的字符串连接特性,而不是调用多次printf。代码"string1" "string2"等价于"string1string2"。

我们将在第10章讨论信号处理和它们的信号版本:sigsetjmp和siglongjmp时再回到这两个函数:setjmp和longjmp。

自动变量的潜在问题

看过栈框架通常被处理的方式,我们应该看下处理自动变量的潜在问题。基本原则是一个自动变量绝不能在声明它的函数返回后被引用。贯穿整个UNIX系统手册,有许多关于它的警告。

下面的代码展示了一个名为open_data的函数,它打开一个标准I/O流,并为这个流设置缓冲:
#include <stdio.h>

#define DATAFILE "datafile"

FILE *
open_data(void)
{
    FILE *fp;
    char databuf[BUFSIZ];   /* setvbuf makes this the stdio buffer */

    if ((fp = fopen(DATAFILE, "r")) == NULL)
        return(NULL);
    return(fp); /* error */
}
问题是当open_data返回时,它在栈上使用的空间将会被下一个被调用的函数使用。然而标准I/O仍在使用那块作为它的流缓冲的内存部分。这肯定会引起混乱。为了更正这个问题,数组databuf应该从全局内存里分配,或者静态的(static或extern)或者动态的(某个alloc函数)。

7.11 getrlimit和setrlimit函数

每个进程都有一堆资源限制,其中一些可以用getrlimit和setrlimit函数查询和改变。

#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlptr);

int setrlimit(int resource, const struct rlimit *rlptr);

两者成功都返回0,错误都返回非0。

这两个函数作为SUS的XSI扩展被定义。一个进程的资源限制通常在系统被初始化的时候被进程0建立,然后被每个后继进程继承。每个实现都有它自己调整各种限制的方法。

这两个函数的每次调用都指单个资源和一个指向以下结构体的指针:

struct rlimit {
  rlim_t rlim_cur;  /* soft limit: current limit */
  rlim_t rlim_max;  /* hard limit: maximum value for rlim_cur */
};

管理资源限制改变的三个准则:
1、一个进程可以改变它的软限制为一个小于或等于它的硬限制的值;
2、一个进程可以把它的硬限制降低为一个大于或等于它的软限制的值,这个硬限制的降低对普通用户来说是不可逆的。
3、只有超级用户才能提升一个硬限制。

一个无限的限制由常量RLIM_INFINITY指定。

resource参数为以下的某个值。下表展示了哪些限制由SUS定义,和被各实现支持。

资源限制的支持
限制XSIFreeBSD 5.2.1Linux 2.4.22Mac OS X 10.3Solaris 9
RLIMIT_AS* * *
RLIMIT_CORE*****
RLIMIT_CPU*****
RLIMIT_DATA*****
RLIMIT_FSIZE*****
RLIMIT_LOCKS  *  
RLIMIT_MEMLOCK *** 
RLIMIT_NOFILE*****
RLIMIT_NPROC *** 
RLIMIT_RSS *** 
RLIMIT_SBSIZE *   
RLIMIT_STACK*****
RLIMIT_VMEM *  *
RLIMIT_AS:进程总的可用内存的最大字节尺寸。这会影响sbrk函数(1.11节)和mmap函数(14.9节)。
RLIMIT_CORE:一个核心文件的最大字节尺寸。一个值为0的限制会阻止创建一个核心文件。
RLIMIT_CPU:最大的CPU时间的秒数。当软限制被超过时,SIGXCPU信号会被发送给进程。
RLIMIT_DATA:数据段的最大字节尺寸:初始化数据,未初绐化数据和堆的总和。
RLIMIT_FSIZE:一个文件可以创建的最大字节尺寸。当软限制被超过时,进程会收到SIGXFSZ信号。
RLIMIT_LOCKS:一个进程能拥有的最大文件锁的数量。(这个数量也包括文件租约,一个Linux的特性。参考Linux的fcntl手册页。)
RLIMIT_MEMLOCK:一个进程可以用mlock锁住的最大内存字节数。
RLIMIT_NOFILE:每个进程打开文件的最大数量。改变这个限制影响参数为_SC_OPEN_MAX的sysconf函数的返回值。
RLIMIT_NPROC:每个真实用户ID的子进程的最大数量。改变这个限制影响参数为_SC_CHILD_MAX的sysconf函数的返回值。
RLIMIT_RSS:最大常驻集尺寸(resident set size, RSS)的字节数。如果可用物理内存没有了,内存从进程超过它们RSS的部分取得内存。
RLIMIT_SBSIZE:一个用户在任何给定的时间内可以消息的套接字缓冲的最大尺寸的字节数。
RLIMIT_STACK:栈的最大字节尺寸。
RLIMIT_VMEM:RLIMIT_AS的同义词。

资源限制影响了调用它的进程并由它的子进程继承。这意味着资源限制的设定需要在shell里完成以影响我们所有的将来的进程。确实,在Bourne shell、GNU Bourne-again shell和Korn shell有内置的ulimit命令,而C shell有内置的limit命令。(umask和chdir函数同样必须作为shell内置的。)

下面的代码打印了所有在系统上支持的资源限制的软限制和硬限制。为了在所有不同的实现上编译这个程序,我们必须选择性的包含不同的资源名。注意我们在把rlim_t定义为一个unsigned long long而不是unsigned long的平台上必须使用一个不同的printf格式。

#include <stdio.h>
#if defined(BSD) || defined(MACOS)
#include <sys/time.h>
#define FMT "%21d "
#else
#define FMT "%10d "
#endif
#include <sys/resource.h>

#define doit(name) pr_limits(#name, name)

static void pr_limits(char *, int);

int
main(void)
{
#ifdef RLIMIT_AS
    doit(RLIMIT_AS);
#endif
    doit(RLIMIT_CORE);
    doit(RLIMIT_CPU);
    doit(RLIMIT_DATA);
    doit(RLIMIT_FSIZE);
#ifdef RLIMIT_LOCKS
    doit(RLIMIT_LOCKS);
#endif
#ifdef RLIMIT_MEMLOCK
    doit(RLIMIT_MEMLOCK);
#endif
    doit(RLIMIT_NOFILE);
#ifdef RLIMIT_NPROC
    doit(RLIMIT_NPROC);
#endif
#ifdef RLIMIT_RSS
    doit(RLIMIT_RSS);
#endif
#ifdef RLIMIT_SBSIZE
    doit(RLIMIT_SBSIZE);
#endif
    doit(RLIMIT_STACK);
#ifdef RLIMIT_VMEM
    doit(RLIMIT_VMEM);
#endif
    exit(0);
}

static void
pr_limits(char *name, int resource)
{
    struct rlimit limit;

    if (getrlimit(resource, &limit) < 0) {
        printf("getrlimit error for %s\n", name);
        exit(1);
    }
    printf("%-14s ", name);
    if (limit.rlim_cur == RLIM_INFINITY)
        printf("(infinite) ");
    else
        printf(FMT, limit.rlim_cur);
    if (limit.rlim_max == RLIM_INFINITY)
        printf("(infinite)");
    else
        printf(FMT, limit.rlim_max);
    putchar((int)'\n');
}
注意我们使用了ISO C的字符创建操作符(#)在doit宏里,来产生每个资源名的字符串值。当我们说doit(RLIMIT_CORE);时,C预处理器把它展开为pr_limits("RLIMIT_CORE", RLIMIT_CORE);

程序输出(Linux 2.6.38-13-generic):
RLIMIT_AS      (infinite) (infinite)
RLIMIT_CORE             0 (infinite)
RLIMIT_CPU     (infinite) (infinite)
RLIMIT_DATA    (infinite) (infinite)
RLIMIT_FSIZE   (infinite) (infinite)
RLIMIT_LOCKS   (infinite) (infinite)
RLIMIT_MEMLOCK      65536      65536
RLIMIT_NOFILE        1024       4096
RLIMIT_NPROC   (infinite) (infinite)
RLIMIT_RSS     (infinite) (infinite)
RLIMIT_STACK      8388608 (infinite)

我们在讲完信号后,会在第10章的练习里继续讨论资源限制。

7.12 总结

了解UNIX系统环境下的C程序的环境是了解UNIX系统的进程控制特性的先决条件。在这章,我们已经看到进程如何被启动,它能如何终止,和它怎么样被传递一个参数列表和一个环境。尽管两者者不被内核解释,但确实是内核把这两才从exec的调用者那传递给新的进程。

我们也已经检查过C程序的典型布局,和进程怎样动态分配和释放内存。它值得我们看下处理环境的可用的函数的细节,因为它们调用内存分配。函数setjmp和longjmp也被讨论了,提供了一种在进程内执行非本地话跳转。本章末描述了各种实现支持的资源限制。


第八章 进程控制

8.1 引言

我们现在进入UNIX系统提供的进程控制。这包含了新的进程的创建,程序的执行,和程序的终止。我们也看到进程的各种ID属性--真实的,有效的,和保存的;用户和组ID--以及它们怎么样被进程控制概念影响。解释文件和system函数同样也被介绍。我们通过看下多数UNIX系统提供的进程报告来结束本章。这让我们从另一个角度看进程控制函数。

8.2 进程识别(Process Identifiers)

每个进程都有一个独一无二的进程ID,一个非负整型。因为进程ID是一个总是独一无二的进程的仅有的被熟知的标识,所以它经常用来作为其它标识符的一部分,来保证唯一性。例如,应用程序有时包含进程ID作为文件名的一部分,来产生独一无二的文件名。

尽管唯一,进程ID是被重用的。当进程终止后,它们的ID变为可重用的侯选者。然而,多数UNIX系统实现算法来延迟重用,以便新创建的进程会分配一个与最近终止的进程不一样的ID。这避免了一个新的进程因为相同的ID被错当成之前的进程。

虽然有一些特殊的进程,但是各个实现上的细节都不同。进程ID 0通常是调用进程,并经常被熟知为对换程序(swapper)。在硬盘上没有对应于这个进程的程序,它是内核的一部分而被熟知为一个系统进程。进程ID 1通常是init进程,在启动过程结束后被内核调用。这个进程的程序文件在UNIX系统的早期版本是/etc/init,而在更新的版本在/sbin/init。这个进程负责在内核已经被启动后引入一个UNIX系统。init通常读取系统相关初始化文件--/etc/rc*文件或/etc/inittab和在/etc/init.d里的文件--和把系统引入一个特殊的状态,比如多用户。init进程绝对不死。这是普通的用户进程,不是内核的系统进程,不像swapper,尽管它需要超级用户权限来运行。本章稍后,我们将看到init是如何变为任何孤立的子进程的父进程。

每个UNIX系统实现有它自己的内核进程集,来提供操作系统服务。例如,在UNIX系统上的一些虚拟内存实现,进程ID 2是换页后台。这个进程负责支持虚拟内存系统的换页。

除了进程ID,每个进程还有其它的标识符。以下的函数返回这些标识符。

#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。

注意这些函数中没有一个有错误返回。我们将在下节讨论fork函数时回到父进程ID。真实和有效有户和组ID在4.4节讨论过了。

8.3 fork函数

一个存在的进程可以调用fork函数来创建一个新的进程

#include <unistd.h>

pid_t fork(void);

在子进程里返回0,在父进程里返回子进程ID,错误返回-1。

由fork函数创建的新进程被称为子进程。函数被调用一次却被返回两次。返回的唯一的区别是子进程的返回值是0,而父进程的返回值是新的子进程的ID。子进程ID被返回给父进程的原因是一个进程可以有不只一个子进程,而没有函数允许一个进程获得它的子进程的ID。fork在子进程里返回0的原因是一个进程只能有一个父进程,而子进程总是可以调用getppid来得到它的父进程ID。(进程ID 0被内核预留使用,所以它不可能是一个子进程的进程ID。)

子进程和父进程都持续执行fork调用之后的指令。子进程是父进程的一个复制品。例如,子进程得到父进程数据空间,堆和栈的拷贝。注意这对一个子进程来说是一个拷贝。父进程和子进程没有共享内存。父进程和子进程共享代码段。

当前实现不执行一个父进程数据、栈和堆的完全拷贝,因为fork后通常接着一个exec。相反,一个称为拷贝时写(copy-on-write, COW)被使用。这些区域被父进程和子进程共享,而且它们的保护机制由内核改变为只读。如果任何一个进程尝试修改这些区域,内核就只拷贝这块内存,一般是虚拟内存系统的一个页。

fork函数的变种由一些平台提供。本文讨论的4个平台都支持下节讨论的vfork的变体。

Linux2.4.22还通过clone系统调用支持新进程的创建。这上fork的衍生形式,允许调用者控制在父进程和子进程之间什么是共享的。

FreeBSD 5.2.1提供了rfork系统调用,它和Linux的clone系统调用相似。rfork调用由Plan 9操作系统继承。

Solaris 9提供了两个线程库:一个为了POSIX线程(pthreads),一个为了Solaris线程。fork的行为在这个两个库之间有所不同。对于POSIX线程,fork创建一个只包含调用线程的进程,而对于Solaris线程,fork创建一个包含调用线程所在的进程所里所有线程的拷贝的进程。为了提供和POSIX线程相似的语义,Solaris提供了fork1函数,它可以用来创建一个只复制调用线程的进程,而无视使用的线程库。线程在第11章和第12章深入讨论。

下面的代码演示了fork函数的使用,展示了一个子进程里的变量的改变是如何不影响父进程里的变量的值的:

#include <unistd.h>

int glob = 6;   /* external variable in initialized data */
char buf[] = "a write to stdout\n";

int
main(void)
{
    int var;    /* automatic variable on the stack */
    pid_t pid;

    var = 88;
    if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1) {
        printf("write error\n");
        exit(1);
    }
    printf("before fork\n");    /* we don't flush stdout */

    if ((pid = fork()) < 0) {
        printf("fork error\n");
        exit(1);
    } else if (pid == 0) {   /* child */
        glob++;             /* modify variables */
        var++;
    } else {                /* parent */
        sleep(2);
    }

    printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
    exit(0);
}
执行结果为:
$ ./a.out
a write to stdout
before fork
pid = 2185, glob = 7, var = 89
pid = 2184, glob = 6, var = 88
$ ./a.out > tmp
$ cat tmp
a write to stdout
before fork
pid = 2187, glob = 7, var = 89
before fork
pid = 2186, glob = 6, var = 88

一般来说,我们从不知道子进程在父进之前还是之后执行。这取决于内核使用的调度算法。如果要求子进程和父进程同步,需要一些形式的进程间信息。在上面的代码里,我们简单让父进程睡眠2秒,来让子进程执行。这并不保证可行,而我们在8.9节讨论竞争条件时会谈论它和其它类型的同步。在10.1节,我们展示如何在一个fork后使用信号来同步一个父进程和子进程。

当我们向标准输出写入里,我们把buf的尺寸减一来避免写入终止的空字节。尽管strlen在计算字符串的长度时不包括这个终止的空字节,然而sizeof计算缓冲的尺寸时会包括这个终止的空字节。另一个区别是使用strlen需要一个函数调用,而sizeof在编译期计算缓冲的长度,因为缓冲由一个已知的字符串初始化,而它的尺寸是固定的。

注意上面代码里fork和I/O函数的交互。回想下第三章,write函数是没有缓冲的。因为write在fork之前调用,所以它的数据只写入标准输出一次。然而,标准I/O函数是缓冲的。回想下5.12节,如果标准输出与一个终端设备连接,那么它是行缓冲的,不然,它是完全缓冲的。当我们交互式地运行这个程序时,我们只得到prinf行的一份拷贝。在第二种情况,fork之前的printf被调用了一次,但是当fork被调用时这行仍保留在缓冲里。然后这个缓冲在父进程的数据空间拷贝到子进程时,被拷贝到子进程里。父进程和子进程现在都有一个包含这行的缓冲。在exit之前的第二个printf,把它的数据添加到已有的缓冲里。当进程终止时,它的缓冲的拷贝最终被冲洗了。

文件共享(File Sharing)

的前面的代码里,当我们重定向父进程的标准输出的时候,子进程的标准输出也被重定向了。事实上,fork的一个特性是所有被父进程打开的文件描述符都被复制到子进程。我们说“复制”因为每个描述上都好像调用了dup函数。父进程和子进程为每个打开的描述符都共享一个文件表项。

考虑一个进程有三个不同的打开的文件,对应标准输入、标准输出和标准错误。当从fork返回时,两个进程的三个描述符会指向同样的文件表项。表项里有文件状态标志,当前文件偏移量和v-node指针。

父进程和子进程共享同样的文件偏移量是很重要的。考虑一个fork一个子进程的进程,它然后wait子进程的结束。假定两个进程都向标准输出写入,作为它们的普通处理。如果父进程把它的标准输出重定向(通过外壳,也许),则当子进程写入到标准输出时,更新父进程的文件偏移量是必要的。在这种情况下,当父进程在用wait等待它时,子进程可以向标准输出写入;当子进程完成时,父进程可以继续向标准输出写入,并知道它的输出会添加到子进程写的任何东西的后面。如果父进程和子进程没有共享同样的文件偏移,这类交互将会更难完成而且需要父进程显式的行动。

如果父进程和子进程都向同一个描述符写入,在没有任何形式的同步下,比如让父进程wait子进程,它们的输出会混在一起(假设描述符在fork之前被打开。)尽管这是可能的,但它不是操作的普通模式。

在fork之后有两种处理描述符的普通情况:

1、父进程等待子进程完成。在这种情况下,父进程不用对它的描述符做任何事情。当子进程终止时,子进程


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值