APUECh02

 5th July 2017 at 10:07am

这章主要讲了 Unix 相关标准,以及一些限制。

2.2 Unix Standardization

这里涉及的几个重要规范是:

  1. ISO C
  2. IEEE POSIX
  3. The Single UNIX Specification

2.2.1 ISO C

C 语言的规范是 ANSI 在 1989 年发布的,所以 C89 也被称作 ANSI C。一年后 ISO 收纳了这份 C 标准,有了 C90 跟 ISO C 的说法。C89 跟 C90 指的是同一份规范。这之后还有 C95, C99, C11,Wikipedia 上的 ANSI C 有简要描述。

现阶段最新的 gcc 6.1 默认使用的 C 语言规范是 gnu11,即 GNU dialect of ISO C11。默认的 C++ 规范是 gnu++14,即 GNU dialect of -std=c++14。(来源

2.2.2 IEEE POSIX

Portable Operating System Interface (POSIX) 定义了操作系统的接口需要实现什么功能。由 IEEE 维护,一开始制订时基于 Unix 系统,但是不限制在 Unix 上。

POSIX 一开始是指 IEEE Standard 1003.1-1988。这个规范又被称作 POSIX.1。这个规范也是这本书内容所涉及的规范。同时这个规范包含了 C 语言的规范。

满足这个规范的操作系统实现,被称作 POSIX compliant。貌似大部分主流操作系统是 POSIX compliant 的(但是不重要,反正你在 Unix/Linux 平台编程时,程序是相对容易在各个操作系统间移植的;而为 Windows 写的程序往往用途不一样,没有必要与 Linux 程序在代码上兼容)。

POSIX 的后续发展可以参考 Wikipedia 页面

POSIX 只定义了接口规范,但是没定义实现规范,所以不同的 POSIX compliant 系统(如 Linux 和 Windows)可能有不同的系统 API。

2.2.3 The Single UNIX Specification

The Single Unix Specification (SUS) 是 POSIX 的超集,定义了一些额外的功能(如文件同步、进程线程间同步等),由 The Open Group 维护。

SUS 定义了 X/Open System Interfaces (XSI),实现了 XSI 的操作系统叫作 XSI conforming。The Open Group 持有 UNIX 的商标,只有遵守 SUS 规范(即也实现了 XSI)的操作系统,才可以被称作 UNIX 操作系统。它的发展历史,参考 Wikipedia 页面

2.3 Unix System Implementations

这部分很复杂,而且不重要,在 Wikipedia 上找了张图:

2.5 Limits

规范定义了一些限制。主要分为编译期知道的,比如一个 short 整形的最小取值;和运行期知道的,如文件名最长可以是多少字节(跟文件系统 (ext2, ext4, etc) 相关)。为了解决这个问题,规范定义了三种类型的限制:

  1. 编译期知道的限制(通过头文件)
  2. 跟文件/目录无关的,运行期知道的限制(通过 sysconf 函数)
  3. 跟文件/目录相关的,运行期知道的限制(通过 pathconf 函数)

简单的说:ISO C 定义了数值类型相关的限制,POSIX.1 定义了操作系统相关的限制。XSI 的限制,不是太受关心。

2.5.1 ISO C Limits

ISO C 定义的编译期限制都放在 <limits.h> 头文件中。定义这些限制的目的,应该是让不同的系统(主要跟 CPU 相关,有不同的整型长度)可以用上补码运算(one's complement)。

留意其中比较奇怪的,是 CHAR_MINCHAR_MAX 的值。对于一个提供 signed char 的系统(如 Linux),CHAR_MINSCHAR_MIN 一样,CHAR_MAXSCHAR_MAX 一样;而对于一个提供 unsigned char 的系统(如 IBM AIX 6.1),CHAR_MIN 则跟 UCHAR_MIN 一样,CHAR_MAX 同理。

浮点数的限制定义在 <float.h>

POSIX.1 对 C 规范做了扩展,要求:

  • INT_MAX >= 2,147,483,647
  • INT_MIN <= -2,147,483,647
  • UINT_MAX >= 4,294,967,295
  • CHAR_BIT == 8,SCHAR_MIN == -128,SCHAR_MAX == 127,UCHAR_MAX == 255

ISO 还定义了几个限制。FOPEN_MAX 定义了一个进程同时可以打开的文件数,TMP_MAX 定义了 tmpnam 创建的临时文件的文件名长度限制,FILENAME_MAX 定义了文件名的长度限制,但是没什么用,因为 POSIX.1 定义了更好的 NAME_MAXPATH_MAX。一般不需要关注他们,因为你不会遇到这些问题。

2.5.2 POSIX Limits

POSIX.1 定义了一些限制。

但是这些限制存在的问题是,它有些值太小了不适合使用(比如 _POSIX_OPEN_MAX 才 20 个),而且大多数的 UNIX 系统都提供了更大的限制值。因此这表中 25 个值中,都存在一个跟本地系统相关的限制值。其中大部分(并不保证全部都有本地限制)定义在 <limits.h> 中,名称是其 POSIX 对应名去掉 _POSIX_ 前缀,如 _POSIX_OPEN_MAX 的 local limit 定义为了 OPEN_MAX

可以通过 /usr/include/limits.h 文件为入口,一层层看到上面提到的变量。

2.5.3 XSI Limits

We don't care.

2.5.4 sysconf, pathconf, and fpathconf Functions

#include <unistd.h>
long sysconf(int name);
long pathconf(const char *pathname, int name);
long fpathconf(int fd, int name);

All three return: corresponding value if OK, −1 on error (see later)

sysconf 的参数基本上是相关的 POSIX.1 限制名(2.5.2)加上 _SC_ 前缀,如 _SC_ARG_MAX, _SC_OPEN_MAXpathconffpathconf 是相应限制名加上 _PC_ 前缀。

关于返回值:

  1. 如果 name 不是合适的值,这些函数返回 -1 并设 errnoEINVAL
  2. 有一些 name 会返回该值不可知(indeterminate),体现在返回 -1 但是不改变 errno 的值
  3. _SC_CLK_TCK 的返回值被用于与 times 系列函数一起使用(8.17 节详述)

编程实现上(参考书中代码例子),应该在调用这类函数前,先将 errno 设为 0,再判断调用后的 errno

对于 pathconffpathconf 有一些限制如果不被满足,那么返回结果是未定义的;比如 _PC_MAX_CANON _PC_MAX_INPUT 的文件对象必需是个 terminal。这些使用时查阅即可。

这里得到的有些限制值被发现有不正确的。比如在 Linux 上,SYMLOOP_MAX 返回值为无限制,但是代码文件里面写死了软链接在不为循环的情况下遍历的最高次数为 40。同时在 Linux 下,如果 glibc 库不认识你的文件系统,那么 pathconffpathconf 得到的值只是 “educated guess”。

总而言之,获取一个限制值的办法是

  1. 定义在 limits.h 中,去掉 POSIX_ 前缀的常量,比如 OPEN_MAX
  2. 如果 limits.h 中无定义相应的常量或者其值为 -1,使用 sysconf 或者 pathconf 拿到的值,如 sysconf(_SC_OPEN_MAX);如果 limits.h 中的常量的值为 0,必须用 sysconf pathconf 判断这个常量是否受到支持
  3. 如果上一步的函数调用拿不到值,给一个猜测的值
  4. POSIX_ 前缀的相应常量值,是对这个限制的最高要求,实际场景一般不使用

2.5.5 Indeterminate Runtime Limits

有一些限制可能在运行时仍不可知。书里举了两个例子,PATH_MAXOPEN_MAX

如果你需要为 getpwd 这类函数分配内存空间时,你需要关注路径的最大长度是多少,即 PATH_MAX 变量。PATH_MAX 存在的问题是:

  1. PATH_MAX 有可能不存在在 <limits.h> 中,这时我们需要调用 pathconf
  2. pathconf 获取 PATH_MAX 时返回的是相对路径的最大长度,因此我们需要传入根路径,再对返回结果加 1
  3. 如果 pathconf 获取不到值,我们只能给一个猜想的值
  4. POSIX.1 2001 版本前,并没有规定 pathconf 返回的值中是否包含最后的空字符;Single UNIX Specification 有指明需要

作者写了一段程序用来获取绝对路径的最长长度,值得参考。

OPEN_MAX 的使用场景,出现在一个后台进程需要关闭全部文件时。有些人假设 <sys/param.h> 中存在 NOFILE(not portable),写了这样的程序:

#include <sys/param.h>
for (i = 0; i < NOFILE; i++)
    close(i);

还有人用 <stdio.h> 中定义的 _NFILE。这些都是不可移植的。正确的做法类似上一例子,优先用定义了的宏(OPEN_MAX),如果不存在用 pathconf,如果 pathconf 无法确定,那就用我们猜的值。

OPEN_MAX 不受限制的情况下(比如你用 ulimit 修改了它),Linux 会返回 LONG_MAX。作者觉得这是个不好的实践,因为会导致上面的程序尝试关闭 2,147,483,647 个文件描述符,浪费一堆 CPU。

支持 getrlimit 的系统(XSI conforming),可以使用这个函数获取一个进程可打开的最大文件数,效果同作者 figure 2.17 里写的程序类似。

2.6 Options

这节主要讲了一些选项(Option),他们的值用来表示系统是否支持这种功能。比如 _PC_NO_TRUNC 为 -1 时表示系统不支持这个功能,大于 0 时表示文件名大于 NAME_MAX 时会产生错误。有一些选项(如 _POSIX_READER_WRITER_LOCKS)的值是 200809L,表示是 POSIX1.2008 规范加进来的功能。

_SC_VERSION _SC_XOPEN_VERSION 分别可以看系统支持的 POSIX 规范版本、XSI 功能对应的 SUS 规范的版本。

2.7 Feature Test Macros

一些系统实现可能会定义除了 POSIX、XSI 以外,自己的常量定义。如果想让程序不依赖这些常量定义,可以在编译时指定变量 _POSIX_C_SOURCE ,比如 cc -D_POSIX_C_SOURCE=200809L file.c 表示 file.c 遵守 POSIX.1 规范。同样的 XSI 规范对应的变量是 _XOPEN_SOURCE

2.8 Primitive System Data Types

POSIX 定义了一些数据类型,用来表示某种特殊的含义。比如 size_t 可以用来表示字符串长度。这样做的目的是为了让写程序的不需要去关心具体的数据大小,比如你要存一个字符串长度,你不需要去考虑用 unsigned short 还是 unsigned int,你只需要直接用 size_t 就好了。你也不需要去知道 size_t 表示多大的数据范围。同时 size_t 可能在不同的系统上有不同的数据范围。

2.9 Differences Between Standards

ISO C 与 POSIX 规范有冲突时,POSIX 规范会尊重 ISO C。绝大多数是没有冲突的,但是也有一些小情况有冲突。比如对于 clock_t,ISO C 函数 clock 返回一个进程所使用的 CPU 时间,你需要拿他除以 CLOCKS_PER_SEC 来表示使用了多少秒;但是对于 POSIX 的 times 系列函数,返回的值需要除以 sysconf 获取的 clock ticks per second 值来表示使用了多少秒。这里不同的 per second 值可能导致 clock_t 的单位不一样。