Linux/UNIX系统编程手册::基础

第 1 章:历史与标准

UNIX 和 C 语言简史

  • 1969 年,在 AT&T 电话公司下辖的 bell 实验室中, Ken Thompson 开发出了首个 UNIX 实现。
  • 1970 年, AT&T 的工程师们又在刚购进的 Digital PDP-11 小型机上,以汇编语言重写了 UNIX。
  • 1972 年,Dennis Ritchie(Thompson 在 bell 实验室的同事, UNIX 开发的早期合作者) 设计并实现出了 C 编程语言。
  • 1973 年, C 语言步入了成熟期,人们能够使用这一新语言重写几乎整个 UNIX 内核。
  • 1974 年的 UNIX 第五版开始, AT&T 准许高校在支付象征性的发布费用后使用 UNIX 系统,UNIX 开始在高校流行。
  • 1979 年 1 月的 UNIX 第七版改善了系统的可靠性,配备了增强型的文件系统。从该版本起, UNIX 分裂为了两 大分支:BSD 和 System V。
  • 1979 年 12 月,诞生了首个完整的 UNIX 发布版 3BSD。
  • 1983 年,加州大学伯克利分校的计算机系统研究组(Computer Systems Research Group)发布 了 4.2BSD。 该版本的发布意义深远,因为其包含了完整的 TCP/IP 实现,其中包括套接字应用编程 接口(API)以及各种网络工具。
  • 到 20 世纪 80 年代末,商业性质的 UNIX 实现在各种硬件架构上都有了广泛应用。

Linux 简史

1991 年,Linus Torvalds,一位芬兰赫尔辛基大学的学生,在外界的激励下为自己的 Intel 80386 PC 开发了一个操作系统。1991 年 10 月 5 日,为求得其他程序员的帮助,Torvalds 在 Usenet 新闻组 comp.os.minix 上发布了内核。Torvalds 做到了一呼百应。其他程序员与 Torvalds 一起加入到 Linux 的开发行列,添加了很多新特性,诸如:改进型的文件系统、对网络的支持、设备驱动程序以及对多处理器的支持等。到了 1994 年 3 月,开发者们发布了 Linux 1.0 版本。随之,Linux 1.2 发布于 1995 年 3 月,Linux 2.0 发布于 1996 年 6 月,Linux 2.2 发布于 1999 年 1 月,Linux 2.4 发布于 2001 年 1 月。对内核 2.5 版本的开发始于 2001 年 11 月,并最终于 2003 年 12 月发布了 Linux 内核 2.6。

标准化

  • C 语言标准
    • 1989 年,ANSI(美国国家标准委员会)C 语言标准(X3.159-1989)获批。这份标准在定义 C 语言语法和语义的同时,还对标准 C 语言库操作进行了描述, 这包括 stdio 函数、字符串处理函数、数学函数、各种头文件等等。通称为 C89。
    • 1999 年,ISO 又正式批准了对 C 语言标准的修订版。通常将这一标准称为 C99,其中包括了对 C 语言及其标准库的一系列修改,诸如,增加了 long long 和布尔数据类型、C++风格的注释(//)、受限指针以及可变长数组等。
  • POSIX 标准
    • 术语“POSIX(可移植操作系统 Portable Operating System Interface 的缩写)”是指在 IEEE(电 器及电子工程师协会),确切地说是其下属的可移植应用标准委员会(PASC) 赞助下所开发的一系列标准。PASC 标准的目标是提升应用程序在源码级别的可移植性。
    • FIPS 是 Federal Information Processing Standard(联邦信息处理标准)的缩写, 这套标准由美 国政府为规范其对计算机系统的采购而制定。基于 POSIX 标准和 ANSI C 语言标准。
  • X/Open 公司和 The Open Group
    • X/Open 公司是由多家国际计算机厂商所组成的联盟,致力于采纳和改进现有标准,以制定出一套全面而又一致的开放系统标准。该公司编纂的《X/Open 可移植性指南》是一套基于 POSIX 标准的可移植性指导丛书。
  • SUSv3 和 POSIX.1-2001
    • 始于 1999 年,出于修订并加强 POSIX 标准和 SUS 规范的目的,IEEE、Open 集团以及 ISO/ IEC 联合技术委员会共同成立了奥斯丁公共标准修订工作组。
    • 2001 年 12 月,该工作组正式批准了 POSIX 1003.1-2001,有时简称为 POSIX.1-2001。POSIX 1003.1-2001 取代了 SUSv2、POSIX.1、POSIX.2 以及大批的早期 POSIX 标准。有时,人们也将该标准称为 Single Unix Specification 版本 3,本书在后续内容中将称其为 SUSv3。
    • SUS(和 XPG)标准顺应了相应 POSIX 标准,并被组织为 POSIX 的功能超集。除了对许多额外接口作出规范外,SUS 标准还将诸多被 POSIX 视为可选的接口和行为规范作为必备项。
  • SUSv4 和 POSIX.1-2008
    • 2008 年,奥斯丁工作组完成了对已合并的 POSIX 和 SUS 规范的修订工作。较之于之先前版本,该标准包含了基本规范以及 XSI 扩展。人们将这一修订版本称为 SUSv4。

第 2 章:基本概念

内核

  • 职责
    • 进程调度
    • 内存管理
    • 提供文件系统
    • 创建和中止进程
    • 对设备的访问
    • 联网
    • 提供系统调用应用程序接口
  • 内核态和用户态
    • 执行硬件指令可使 CPU 在两种状态间来回切换。与之对应,可将虚拟内存区域划分为用户空间部分或内核空间部分。在用户态下运行时,CPU 只能访问被标记为用户空间的内存,试图访问属于内核空间的内存会引发硬件异常。当运行于核心态时,CPU 既能访问用户空间内存,也能访问内核空间内存。仅当处理器在核心态运行时,才能执行某些特定操作,确保了用户进程既不能访问内核指令和数据结构,也无法执行不利于系统运行的操作。

shell

shell 是一种具有特殊用途的程序,主要用于读取用户输入的命令,并执行相应的程序以响应命令。有时,人们也称之为命令解释器。
尽管某些操作系统将命令解释器集成于内核中,而对 UNIX 系统而言, shell 只是一个用户进程。

  • 几种重要的 shell
    • Bourne shell
    • C shell
    • Korn shell
    • Bourne again shell

用户和组

系统会对每个用户的身份做唯一标识,用户可隶属于多个组。

  • 用户
    • 系统的每个用户都拥有唯一的登录名(用户名)和与之相对应的整数型用户 ID(UID)。
    • 出于管理目的,尤其是为了控制对文件和其他资源的访问,将多个用户分组是非常实用的做法。
  • 超级用户
    • 超级用户在系统中享有特权。超级用户账号的用户 ID 为 0,通常登录名为 root。 在一般 的 UNIX 系统上,超级用户凌驾于系统的权限检查之上。系统管理员可以使用超级用户账号来执行各种系统管理任务。

单根目录层级、目录、链接及文件

内核维护着一套单根目录结构,以放置系统的所有文件。

  • 文件类型
    • 在文件系统内,会对文件类型进行标记,以表明其种类。其中一种用来表示普通数据文件,人们常称之为“普通文件”或“纯文本文件”,以示与其他种类的文件有所区别。其他文件类型包括设备、管道、套接字、目录以及符号链接。
  • 路径和链接
    • 目录是一种特殊类型的文件,内容采用表格形式,数据项包括文件名以及对相应文件的引用。这一“文件名+引用”的组合被称为链接。每个文件都可以有多条链接,因而也可以有多个名称,在相同或不同的目录中出现。
  • 符号链接
    • 类似于普通链接,符号链接给文件起了一个“别号(alternative name)”。在目录列表中,普通链接是内容为“文件名+指针”的一条记录,而符号链接则是经过特殊标记的文件,内容包含了另一文件的名称。
  • 文件名
    • 在大多数 Linux 文件系统上,文件名最长可达 255 个字符。文件名可以包含除“/”和空 字符(\0)外的所有字符。
  • 路径名
    • 路径名是由一系列文件名组成的字符串,彼此以“/”分隔,首字符可以为“/”
    • 绝对路径名以“/”开始,指明文件相对于根目录的位置。
    • 相对路径名定义了相对于进程当前工作目录的文件位置,与绝对路径名相比,相对路径名缺少了起始的“/”。
  • 当前工作目录
    • 每个进程都有一个当前工作目录(有时简称为进程工作目录或当前目录)。这就是单根目录层级下进程的“当前位置”,也是进程解释相对路径名的参照点。
  • 文件的所有权和权限
    • 每个文件都有一个与之相关的用户 ID 和组 ID,分别定义文件的属主和属组。系统根据文件的所有权来判定用户对文件的访问权限。

文件 I/O 模型

UNIX 系统 I/O 模型最为显著的特性之一是其 I/O 通用性概念。也就是说,同一套系统调 用(open()、read()、write()、close()等)所执行的 I/O 操作,可施之于所有文件类型,包括设备文件在内。因此,采用这些系统调用的程序能够处理任何类型的文件。就本质而言,内核只提供一种文件类型:字节流序列,在处理磁盘文件、磁盘或磁带设备时,可通过 lseek()系统调用来随机访问。

  • 文件描述符
    • I/O 系统调用使用文件描述符–非负整数—-来指代打开的文件。
    • 由 shell 启动的进程会继承 3 个已打开的文件描述符:
      • 描述符 0 为标准输入,指代为进程提供输入的文件;
      • 描述符 1 为标准输出,指代供进程写入输出的文件;
      • 描述符 2 为标准错误,指代供进程写入错误消息或异常通告的文件。
    • 在交互式 shell 或程序中,上述三者一般都指向终端。在 stdio 函数库中,这几种描述符分别与文件流 stdin、stdout 和 stderr 相对应。
  • stdio 函数库
    • C 编程语言在执行文件 I/O 操作时,往往会调用 C 语言标准库的 I/O 函数。也将这样一组 I/O 函数称为 stdio 函数库,其中包括 fopen()、fclose()、scanf()、printf()、fgets()、fputs()等。 stdio 函数位于 I/O 系统调用层(open()、close()、read()、write()等)之上。

程序

程序通常以两种面目示人。其一为源码形式,由使用编程语言(比如, C 语言)写成的一系 列语句组成,是人类可以阅读的文本文件。要想执行程序,则需将源码转换为第二种形式—计 算机可以理解的二进制机器语言指令。

一般认为,术语“程序”的上述两种含 义几近相同,因为经过编译和链接处理,会将源码转换为语义相同的二进制机器码。

  • 过滤器
    • 从 stdin 读取输入,加以转换,再将转换后的数据输出到 stdout, 常常将拥有上述行为的 程序称为过滤器, cat、grep、tr、sort、wc、sed、awk 均在其列。

进程

简而言之,进程是正在执行的程序实例。执行程序时,内核会将程序代码载入虚拟内存,为程序变量分配空间,建立内核记账(bookkeeping)数据结构,以记录与进程有关的各种信息(比如,进程 ID、用户 ID、组 ID 以及终止状态等)。

  • 进程的内存布局
    • 文本:程序的指令。
    • 数据:程序使用的静态变量。
    • 堆:程序可从该区域动态分配额外内存。
    • 栈:随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间。
  • 创建进程和执行程序
    • 进程可使用系统调用 fork()来创建一个新进程。
    • 子进程要么去执行与父进程共享代码段中的另一组不同函数,或者,更为常见的 情况是使用系统调用 execve()去加载并执行一个全新程序。execve()会销毁现有的文本段、数 据段、栈段及堆段,并根据新程序的代码,创建新段来替换它们。
  • 进程 ID 和父进程 ID
    • 每一进程都有一个唯一的整数型进程标识符(PID)。此外,每一进程还具有一个父进程标识符(PPID)属性,用以标识请求内核创建自己的进程。
  • 进程终止和终止状态
    • 进程可使用_exit()系统调用请求退出
    • 其二,向进程传递信号,将其“杀死”
    • 根据惯例,终止状态为 0 表示进程“功成身退”,非 0 则表示有错误发生。
  • 进程的用户和组标识符(凭证)
    • 真实用户 ID 和组 ID:用来标识进程所属的用户和组。 新进程从其父进程处继承这些 ID。登录 shell 则会从系统密码文件的相应字段中获取其真实用户 ID 和组 ID。
    • 有效用户 ID 和组 ID:进程在访问受保护资源(比如,文件和进程间通信对象)时,会使用这两个 ID(并结合下述的补充组 ID)来确定访问权限。一般情况下,进程的有效 ID 与相应的真实 ID 值相同。正如即将讨论的那样,改变进程的有效 ID 实为一种机制,可使进程具有其他用户或组的权限。
    • 补充组 ID:用来标识进程所属的额外组。新进程从其父进程处继承补充组 ID。登录 shell 则从系统组文件中获取其补充组 ID。
  • 特权进程
    • 特权进程是指有效用户 ID 为 0(超级用户)的进程
    • 由某一特权进程创建的进程,也可以是特权进程
    • 成为特权进程的另一方法是利用 set-user-ID 机制,该机制允许某进程的有效用户 ID 等同于该进程所执行程序文件的用户 ID。
  • 能力(Capabilities)
    • 赋予某进程部分能力,使得其既能够执行某些特权级操作,又防止其执行其他特权级操作。
  • init 进程
    • 系统引导时,内核会创建一个名为 init 的特殊进程,即“所有进程之父”,该进程的相应 程序文件为/sbin/init。
    • init 的主要任务是创建并监控系统运行所需的一系列进程。
  • 守护进程
    • 守护进程指的是具有特殊用途的进程,系统创建和处理此类进程的方式与其他进程相同
    • 守护进程通常在系统引导时启动,直至系统关闭前,会一直存在
    • 守护进程在后台运行,且无控制终端供其读取或写入数据
  • 环境列表
    • 每个进程都有一份环境列表,即在进程用户空间内存中维护的一组环境变量。这份列表的每一元素都由一个名称及其相关值组成。由 fork() 创建的新进程,会继承父进程的环境副本。这也为父子进程间通信提供了一种机制。
  • 资源限制
    • 每个进程都会消耗诸如打开文件、内存以及 CPU 时间之类的资源。使用系统调用 setrlimit(),进程可为自己消耗的各类资源设定一个上限。此类资源限制的每一项均有两个相关值:软限制 (soft limit)限制了进程可以消耗的资源总量, 硬限制(hard limit)软限制的调整上限。

内存映射

调用系统函数 mmap()的进程,会在其虚拟地址空间中创建一个新的内存映射。

  • 文件映射:将文件的部分区域映射入调用进程的虚拟内存。映射一旦完成,对文件映射内容的访问则转化为对相应内存区域的字节操作。映射页面会按需自动从文件中加载。
  • 相映成趣的是并无文件与之相对应的匿名映射,其映射页面的内容会被初始化为 0。
  • 由某一进程所映射的内存可以与其他进程的映射共享。达成共享的方式有二:其一是两个进程都针对某一文件的相同部分加以映射,其二是由 fork() 创建的子进程自父进程处继承映射。
  • 内存映射用途很多,其中 包括:以可执行文件的相应段来初始化进程的文本段、内存(内容填充为 0)分配、文件 I/O(即映射内存 I/O)以及进程间通信(通过共享映射)。

静态库和共享库

静态库(有时,也称之为档案文件[archives])是早期 UNIX 系统中唯一的一种目标库。本质上说来,静态库是对已编译目标模块的一种结构化整合。要使用静态库中的函数,需要在创建程序的链接命令中指定相应的库。主程序会对静态库中隶属于各目标模块的不同函数加以引用。链接器在解析了引用情况后,会从库中抽取所需目标模块的副本,将其复制到最终的可执行文件中,这就是所谓静态链接。

如果将程序链接到共享库,那么链接器就不会把库中的目标模块复制到可执行文件中,而是在可执行文件中写入一条记录,以表明可执行文件在运行时需要使用该共享库。一旦在运行时将可执行文件载入内存,一款名为“动态链接器”的程序会确保将可执行文件所需的动态库找到,并载入内存,随后实施运行时链接,解析可执行文件中的函数调用,将其与共享库中相应的函数定义关联起来。在运行时,共享库代码在内存中只需保留一份,且可供所有运行中的程序使用。

进程间通信及同步

信号(signal),用来表示事件的发生。

管道(亦即 shell 用户所熟悉的“|”操作符)和 FIFO,用于在进程间传递数据。

套接字,供同一台主机或是联网的不同主机上所运行的进程之间传递数据。

文件锁定,为防止其他进程读取或更新文件内容,允许某进程对文件的部分区域加以锁定。

消息队列,用于在进程间交换消息(数据包)。

信号量(semaphore),用来同步进程动作。

共享内存,允许两个及两个以上进程共享一块内存。当某进程改变了共享内存的内容时,其他所有进程会立即了解到这一变化。

信号

进程收到信号,就意味着某一事件或异常情况的发生。信号的类型很多,每一种分别标识不同的事件或情况。采用不同的整数来标识各种信号类型,并以 SIGxxxx 形式的符号名加以定义。

程序可选择不采取默认的信号动作,而是忽略信号或者建立自己的信号处理器。信号处理器是由程序员定义的函数,会在进程收到信号时自动调用,根据信号的产生条件执行相应动作。

线程

在现代 UNIX 实现中,每个进程都可执行多个线程。可将线程想象为共享同一虚拟内存及一干其他属性的进程。每个线程都会执行相同的程序代码,共享同一数据区域和堆。可是,每个线程都拥有属于自己的栈,用来装载本地变量和函数调用链接信息。

进程组和 shell 任务控制

shell 执行的每个程序都会在一个新进程内发起。

几乎所有的主流 shell 都提供了一种交互式特性,名为任务控制。该特性允许用户同时执行并操纵多条命令或管道。在支持任务控制的 shell 中,会将管道内的所有进程置于一个新进程组或任务中。进程组中的每个进程都具有相同的进程组标识符,其实就是进程组中某个进程的进程 ID。

会话、控制终端和控制进程

会话指的是一组进程组(任务)。会话中的所有进程都具有相同的会话标识符。会话首进程(session leader)是指创建会话的进程,其进程 ID 会成为会话 ID。

使用会话最多的是支持任务控制的 shell,由 shell 创建的所有进程组与 shell 自身隶属于同一会话,shell 是此会话的会话首进程。

伪终端

伪终端是一对相互连接的虚拟设备,也称为主从设备。在这对设备之间,设有一条 IPC 信道,可供数据进行双向传递。

伪终端广泛应用于各种应用领域,最知名的要数 telnet 和 ssh 之类提供网络登录服务的应用,以及 X Window 系统所提供的终端窗口实现。

日期和时间

真实时间:指的是在进程的生命期内(所经历的时间或时钟时间),以某个标准时间 点(日历时间)或固定时间点(通常是进程的启动时间)为起点测量得出的时间。

进程时间:亦称为 CPU 时间,指的是进程自启动起来,所占用的 CPU 时间总量。可进一步将 CPU 时间划分为系统 CPU 时间和用户 CPU 时间。前者是指在内核模式中,执行代码所花费的时间。后者是指在用户模式中,执行代码所花费的时间。

客户端/服务器架构

客户端:向服务器发送请求消息,请求服务器执行某些服务。

服务器:分析客户端的请求,执行相应的动作,然后,向客户端回发响应消息。

实时性

实时性应用程序是指那些需要对输入做出及时响应的程序。此类输入往往来自于外接的 传感器或某些专门的输入设备,而输出则会去控制外接硬件。

为支持实时性应用,POSIX.1b 定义了多个 POSIX.1 扩展,其中包括异步 I/O、共享内存、内存映射文件、内存锁定、实时性时钟和定时器、备选调度策略、实时性信号、消息队列,以及信号量等。

/proc 文件系统

/proc 文件系统是一种虚拟文件系统,以文件系统目录和文件形式,提供一个指向内核数据结构的接口。这为查看和改变各种系统属性开启了方便之门。此外,还能通过一组以 /proc/PID 形式命名的目录(PID 即进程 ID)查看系统中运行各进程的相关信息。

第 3 章:系统编程概念

系统调用

系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名义去执行某些动作。以应用程序编程接口(API)的形式,内核提供有一系列服务供程序访问。这包括创建新进程、执行 I/O,以及为进程间通信创建管道等。

  • 注意点
    • 系统调用将处理器从用户态切换到核心态,以便 CPU 访问受到保护的内核内存。
    • 系统调用的组成是固定的,每个系统调用都由一个唯一的数字来标识。(程序通过名称来标识系统调用,对这一编号方案往往一无所知。)
    • 每个系统调用可辅之以一套参数,对用户空间(亦即进程的虚拟地址空间)与内核空间之间(相互)传递的信息加以规范。
  • 系统调用发起过程
    • 应用程序通过调用 C 语言函数库中的外壳(wrapper)函数,来发起系统调用。
    • 对系统调用中断处理例程(稍后介绍)来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器。
    • 由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器(%eax)中。
    • 外壳函数执行一条中断机器指令(int 0x80),引发处理器从用户态切换到核心态,并执行系统中断 0x80 (十进制数 128)的中断矢量所指向的代码。
    • 为响应中断 0x80,内核会调用 system_call()例程来处理这次中断
      • 在内核栈中保存寄存器值
      • 审核系统调用编号的有效性
      • 以系统调用编号对存放所有调用服务例程的列表(内核变量 sys_call_table)进行索引,发现并调用相应的系统调用服务例程。若系统调用服务例程带有参数,那么将首先检查参数的有效性。随后,该服 务例程会执行必要的任务,这可能涉及对特定参数中指定地址处的值进行修改,以及在用户内存和内核内存间传递数据(比如,在 I/O 操作中)。最后,该服务例程会将结 果状态返回给 system_call() 例程。
      • 从内核栈中恢复各寄存器值,并将系统调用返回值置于栈中。
      • 返回至外壳函数,同时将处理器切换回用户态。
    • 若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 errno。然后,外壳函数会返回到调用程序,并同时返回一个整型值,以表明系统调用是否成功。

库函数

一个库函数是构成标准 C 语言函数库的众多库函数之一。库函数的用途多种多样,可用来执行以下任务:打开文件、将时间转换为可读格式,以及进行字符串比较等。

许多库函数(比如,字符串操作函数)不会使用任何系统调用。另一方面,还有些库函数构建于系统调用层之上。

标准 C 语言函数库

标准 C 语言函数库的实现随 UNIX 的实现而异。 GNU C 语言函数库(glibc)是 Linux 上最常用的实现。

处理来自系统调用和库函数的错误

几乎每个系统调用和库函数都会返回某类状态值,用以表明调用成功与否。要了解调用是否成功,必须坚持对状态值进行检查。若调用失败,那么必须采取相应行动。至少,程序应该显示错误消息,警示有意想不到的事件发生。

每个系统调用的手册页记录有调用可能的返回值,并指出了哪些值表示错误。通常,返回值为 -1 表示出错。

系统调用失败时,会将全局整形变量 errno 设置为一个正值,以标识具体的错误。在每个手册页内标题为 ERRORS 的章节内,都刊载有一份相应系统调用可能返回的 errno 值列表。系统调用失败后,常见的做法之一是根据 errno 值打印错误消息。提供库函数 perror()和 strerror(), 就是出于这一目的。函数 perror() 会打印出其 msg 参数所指向的字符串,紧跟一条与当前 errno 值相对应的消息。函数 strerror()会针对其 errnum 参数中所给定的错误号,返回相应的错误字符串。

  • 不同的库函数在调用发生错误时,返回的数据类型和值也各不相同。从错误处理的角度来说,可将库函数划分为以下几类。
    • 某些库函数返回错误信息的方式与系统调用完全相同—返回值为 −1,伴之以 errno 号来表示具体错误。remove()便是其中一例,可使用该函数来删除文件或目录。对此类函数所发生的错误进行诊断,其方式与系统调用完全相同。
    • 某些库函数在出错时会返回 −1 之外的其他值,但仍会设置 errno 来表明具体的出错情况。
    • 还有些函数根本不使用 errno。 对此类函数来说,确定错误存在与否及其起因的方法各不 相同,可见诸于相应函数的手册页中,不应使用 errno、perror()或 strerror()来诊断错误。

可移植性问题

编写可移植性应用程序时,有时会希望各个头文件只显露遵循特定标准的定义(常量、函 数原型等)。要达到这一目的,在编译程序时需要定义下列一个或多个特性测试宏。

不同的数据类型,随着 UNIX 实现的不同,有时甚至是同一实现中编译环境的不同,这些基本类型的大小各不相同。更有甚者,不同实现可能会使用不同类型来表示相同信息。即便是针对同一款 UNIX 实现,用以表征信息的类型在不同版本之间也会有所不同。为避免此类可移植性问题, SUSv3 规范了各种标准系统数据类型,并要求各个实现适当 加以定义和使用。每种类型的定义均使用 C 语言的 typedef 特性。例如在 32 位系统上,pid_t 数据类型表示进程 id,typedef int pid_t

其他可移植性问题,初始化操作和使用结构,对于同一个结构体,字段顺序不一致。使用未见诸于所有实现的宏,并非所有的 UNIX 实现都对一个宏做了定义,可以使用 c 语言的 #ifdef 来检测。不同实现所需头文件的变化,有些情况下,包含各种系统调用和库函数原型的头文件,在不同 UNIX 实现之间会有所不同。