书名:UNIX环境高级编程(第3版)
ISBN:978-7-115-51675-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]W. 理查德•史蒂文斯(W. Richard Stevens)
[美]史蒂芬•A. 拉戈( Stephen A. Rago)
译 戚正伟 张亚英 尤晋元
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书是被誉为UNIX编程“圣经”的Advanced Programming in the UNIX Environment一书的第3版。在本书第2版出版后的8年中,UNIX发生了巨大的变化,特别是影响UNIX编程接口的有关标准变化很大。本书在保持前一版风格的基础上,根据新的标准对内容进行了修订和增补,反映了新的技术发展。书中除了介绍UNIX文件和目录、标准I/O库、系统数据文件和信息、进程环境、进程控制、进程关系、信号、线程、线程控制、守护进程、各种I/O、进程间通信、网络IPC、伪终端等方面的内容,还在此基础上介绍了众多应用实例,包括如何创建数据库函数库以及如何与网络打印机通信等。此外,还在附录中给出了函数原型和部分习题的答案。 本书内容权威,概念清晰,阐述精辟,对于所有层次UNIX/Linux程序员都是一本不可或缺的参考书。
作为UNIX环境编程方面的经典著作,由著名技术专家W. Richard Stevens撰写的Advanced Programming in the UNIX ® Environment自1992年出版以来,受到专家和读者的普遍欢迎。由Stephen A. Rago 作为共同作者,根据新的系统和规范进行了更新,2005年出版了第2版。2013年由Rago更新到了第3版,涵盖了70多个最新版POSIX.1标准的新增接口,删除了STREAMS相关接口的内容,并将使用的典型平台更新为Solaris 10、Darwin 10.8.0、FreeBSD 8.0和Ubuntu 12.04。
目前UNIX版本不断涌现,例如广为使用的苹果Mac OS X和iOS使用开源类UNIX操作系统Darwin,谷歌的Android采用Linux作为操作系统内核。尽管在UNIX编程环境和C程序设计语言的标准化方面已经做了不少工作,但系统接口不断增加,例如Single UNIX Specification第1版(SUSv1)1994年出版时大约包含了1170个接口(也被称为Spec 1170),到2010年发布第4版时(SUSv4),已经包括1833个接口。虽然系统调用接口和库函数可参见《UNIX程序员手册》第2、3部分,但“手册中没有给出实例及基本原理,而这些正是本书所要讲述的内容”(第1版前言)。本书精选了常用的400多个系统调用和库函数,这些接口基本是UNIX系统软件的核心功能,涵盖了UNIX/Linux系统编程的方方面面。本书通过简明完整的例子来说明其用途,不仅仅说明了其基本用法,还反映了不同平台之间细微差异,有助于读者对整个编程环境有全面深入的了解。在翻译本书的过程中,译者也是收益良多,同时,一些经典的案例已经用于大学课堂教学和编程实践中。
本书的第2章至第12章由同济大学张亚英翻译和校对,其余由上海交通大学软件学院戚正伟翻译和校对,上海交通大学计算机系尤晋元教授对全书统稿。本书第1版和第2版中译本自出版以来,很多读者对其提出了宝贵意见,在本版本中尽量采纳了这些意见。同时,我们的工作还得到上海交通大学软件学院许多研究生(葛馨霓、王佳骏、李垚、王润泽、朱新宇、孙海洋、张子卓、许欣昊、马军、梁丹)的帮助,在此一并表示感谢。
还要特别感谢人民邮电出版社编辑杨海玲在本书的编辑、出版方面所付出的辛勤劳动。
我们希望本书的出版对相关科技人员和读者有所帮助,同时也期望广大专家和读者提出宝贵意见。
我差不多每次在接受专访当中,或是做技术讲座后的提问时间里,总会被问及这样一个问题:“你想到过UNIX会生存这么长时间吗?”自然,每次的回答都是:“没有,我们没想到会是这样。”从某种角度说,UNIX系统已经伴随了商用计算行业历史的大半,而这也早就不是什么新闻了。
发展的历程错综复杂,充满变数。自20世纪70年代初以来,计算机技术经历了沧海桑田般的变化,尤其体现在网络技术的普遍应用、图形化的无所不在、个人计算的触手可及,然而UNIX系统却奇迹般地容纳和适应了所有这些变化。虽然商业应用环境在桌面领域目前仍然为微软和英特尔两家公司所统治,但是在某些方面已经从单一供应商向多种来源转变,特别是近年来对公共标准和免费可用来源的信赖与日俱增。
UNIX作为一种现象而不单是商标品牌,有幸能与时俱进,乃至领导潮流。在20世纪70~80年代,AT&T虽对UNIX的实际源代码进行了版权保护,但却鼓励在系统的接口和语言基础上进行标准化的工作。例如,AT&T发布了SVID(System V Interface Definition,系统V接口定义),这成为POSIX及其后续工作的基础。后来,UNIX可以说相当优雅地适应了网络环境,虽不那么轻巧却也充分地适应了图形环境。再往后,开源运动的技术基础中集成了UNIX的基本内核接口和许多它独特的用户级工具。
即使在UNIX软件系统本身还是专有的时候,鼓励出版UNIX系统方面的论文和书籍也是至关重要的,著名的例子就是Maurice Bach的《UNIX操作系统设计》一书。其实我要说明的是,UNIX长寿的主要原因是,它吸引了极具天分的技术作者,为大众解读它的优美和神秘所在。Brian Kernighan是其中之一,Richard Stevens自然也是。本书第1版连同Stevens所著的系列网络技术书,被公认为优秀的、匠心独具的名著,成为极其畅销的作品。
然而,本书第1版毕竟出版时间太早了,那时还没有出现Linux,源自伯克利 CSRG的UNIX接口的开源版本还没有广为流行,很多人的网络还在用串行调制解调器。Stephen Rago认真仔细地更新了本书,以反映所有这些技术进展,同时还考虑到各种ISO标准和IEEE标准这些年来的变化。因此,他的例子是最新的,也是最新测试过的。
总之,这是一本弥足珍贵的经典著作的更新版。
Dennis Ritchie
2005年3月于新泽西州默里山市
从我第一次修订《UNIX环境高级编程》一书以来已经快有8年了,其间发生了很多的变化。
在出版第2版之前,Open Group完成了2004版的Single UNIX Specification,它涵盖了两套勘误表的修改。2008年,Open Group完成了新版的Single UNIX Specification,它更新了基本定义,添加了新的接口,并且去除了弃用的接口。这套规范被称为2008年版的POSIX.1,其中包含第7版的基本规范,并在2009年发行。2010年,它与更新后的curses接口捆绑,一起作为Single UNIX Specification第4版(SUSv4)进行再版。
运行在Intel处理器上的Mac OS X操作系统的10.5、10.6和10.8版,被Open Group认证为UNIX系统。
苹果公司停止了PowerPC平台上Mac OS X的开发。在10.6发行版(Snow Leopard)之后只针对x86平台发布了新的操作系统版本。
Solaris操作系统以开源的形式发布,试图与FreeBSD、Linux和Mac OS X遵循的开源模式在声望上一争高下。在2010年,Oracle收购了Sun Microsystems之后,OpenSolaris的开发被终止。作为替代,Solaris社区组建了Illumos项目来继续基于OpenSolaris的开源开发。
2011年,C语言标准被更新,但是因为系统并未能跟上其变化,本书中依然参照1999版。
最重要的是,在第2版中使用的平台已经过时了。本书这一版中涉及以下平台。
(1)FreeBSD 8.0,前身是加州大学伯克利分校计算机系统研究组发布的4.4BSD系统,运行在32位Intel Pentium处理器上。
(2)Linux 3.2.0(Ubuntu 12.04发布版),这是一个免费的类UNIX操作系统,运行在64位的Intel Core i5 处理器上。
(3)Apple Mac OS X 10.6.8版(Darwin 10.8.0),运行在64位Intel Core2 Duo处理器上(Darwin基于FreeBSD和Mach)。我选择从PowerPC平台转向Intel平台,是因为最新版的Mac OS X不再支持PowerPC平台。这次选择带来的缺点是涉及的处理器倾向了Intel,而当讨论到异构性问题时,让处理器具有不同的特性(如字节序和整数大小等)将是很有帮助的。
(4)Solaris 10,Sun Microsystems(现在的Oracle)的System V Release 4的派生系统,运行在64位UltraSPARC IIi处理器上。
最大的变化之一是POSIX.1-2008中的Single UNIX Specification弃用了一些STREAMS相关接口。这是准备在该标准的未来版本中去掉全部这些接口过程的第一步。因此,我已经不情愿地在这一版中删除了STREAMS的内容。这是一个不幸的变化,因为STREAMS接口为socket接口提供了一个很好的对照,并且在很多方面更为灵活。不可否认,当谈论到STREAMS时我并非绝对公正,但是毫无疑问的是,在现有系统中它的分量已经减轻。
Linux基础系统中未包含STREAMS,虽然添加该功能的包(LiS和OpenSS7)是可用的。
虽然Solaris 10中包含了STREAMS,但是Solaris 11的socket实现并没有构建在STREAMS之上。
Mac OS X不包含STREAMS支持。
FreeBSD不包含STREAMS支持(也从未包含过)。
随着STREAMS相关内容的去除,新的主题变得有机会替代它,例如POSIX异步I/O。
在本书第2版中,Linux版本是基于2.4版的。在这次的版本中,我们已经更新到了3.2版。两个版本的最大不同之一是线程系统。在Linux 2.4和Linux 2.6之间,线程的实现变为Native POSIX Thread Library(NPTL)。NPTL使得Linux线程的行为与其他系统的线程更加相似。
总的来说,这次的版本涵盖了超过70个新的接口,包括处理异步I/O、自旋锁、屏障和POSIX信号量等接口。除了一些普遍使用的接口被保留,大多数弃用的接口均被删除。
许多读者为第2版寄来了评论和错误报告。我很感谢他们提高了第2版的准确性。下面提及的各位是最早提出建议或者指出错误的:Seth Arnold、Luke Bakken、Rick Ballard、Johannes Bittner、David Bronder、Vlad Buslov、Peter Butler、Yuching Chen、Mike Cheng、Jim Collins、Bob Cousins、Will Dennis、Thomas Dickey、Loïc Domaigné、Igor Fuksman、Alex Gezerlis、M. Scott Gordon、Timothy Goya、Tony Graham、Michael Hobgood、Michael Kerrisk、Youngho Kwon、Richard Li、Xueke Liu、Yun Long、Dan McGregor、Dylan McNamee、Greg Miller、Simon Morgan、Harry Newton、Jim Oldfield、Scott Parish、Zvezdan Petkovic、David Reiss、Konstantinos Sakoutis、David Smoot、David Somers、Andriy Tkachuk、Nathan Weeks、Florian Weimer、Qingyang Xu和 Michael Zalokar。
技术审校者也提高了内容的准确性,感谢Steve Albert、Bogdan Barbu和Robert Day。特别感谢Geoff Clare和Andrew Josey为Single UNIX Specification的升华和第2章的准确性提供了帮助。另外,感谢Ken Thompson对历史问题做出了解答。
我得再一次说,与Addison-Wesley的工作人员的合作非常愉快。感谢Kim Boedigheimer、Romny French、John Fuller、Jessica Goldstein、Julie Nahil和Debra Williams-Cauley,此外,感谢Jill Hobbs在这段时间提供了她的专业审稿能力。
最后,感谢我的家人对我在这次再版上花费了如此多时间给予的理解。
我非常欢迎读者发来邮件,发表评论,提出建议,订正错误。
Stephen A. Rago
sar@apuebook.com
2013年1月于新泽西州沃伦市
我与Rich Stevens最早是通过电子邮件开始交往的,当时我发邮件报告他的第一本书《UNIX网络编程》的一个排版错误。他回信开玩笑说我是第一个给他发这本书勘误的人。到他1999年故去之前,我们会时不时地通一些邮件,一般都是在有了问题认为对方能解答的时候。我们在USENIX会议期间多次相见,并共进晚餐,Rich在会议中给大家做技术 培训。
Rich Stevens真是个益友,行为举止很有绅士风度。我在1993年写《UNIX系统V网络编程》时,试图把书写成他的《UNIX网络编程》的系统V版。Rich高兴地为我审阅了好几章,并不把我当成竞争对手,而是当作一起写书的同事。我们曾多次谈到要合作给他的《TCP/IP详解》写个STREAMS版。天若有情,我们或许已经完成了这个心愿。然而,Rich已经驾鹤西去,修订《UNIX环境高级编程》就成为我跟他一起写书的最易实现的方式。
当Addison-Wesley公司的编辑找到我说想修订Rich的这本书时,我第一反应是这本书没有多少要改的。尽管13年过去了,Rich的书还是巍然屹立。但是,与当初本书出版的时候相比,今日的UNIX行业已经有了巨大的变化。
系统V的各个变种渐渐被Linux所取代。原来生产硬件配以各自的UNIX版本的几个主要厂商,要么提供了Linux的移植版本,要么宣布支持Linux。Solaris可能算是硕果仅存的占有一定市场份额的UNIX系统V版本4的后裔了。
加州大学伯克利分校的CSRG(计算机科学研究组)在发布了4.4BSD之后,已经决定不再开发UNIX操作系统,只有几个志愿者小组还维护着一些可公开获得的版本。
Linux得到数以千计的志愿者的支持,它的引入使任何一个拥有计算机的人都能运行类似于UNIX系统的操作系统,并且可以免费获得源代码支持哪怕最新的硬件设备。在已经存在几种免费BSD版本的情况下,Linux的成功确实是个奇迹。
苹果公司作为一个富有创新精神的公司,已经放弃了老的Mac操作系统,取而代之的是一个在Mach和FreeBSD基础上开发的新系统。
因此,我努力更新本书中的内容,以反映这4种平台。
在Rich 1992年出版了《UNIX环境高级编程》之后,我扔掉了手头几乎所有的UNIX程序员手册。这些年来,我桌上最常摆放的就是两本书:一本是字典,另一本就是《UNIX环境高级编程》。我希望读者也能认为本修订版一样有用。
Rich的书依然屹立,我试图不去改动他这本书原来的风格。但是13年间世事兴衰,尤其是影响UNIX编程接口的有关标准变化很大。
我依据标准化组织的标准,更新了全书相关的接口方面的内容。第2章改动较大,因为它主要是讨论标准的。本书第1版是根据POSIX.1标准的1990年版写的,本修订版依据2001年版的新标准,内容要丰富很多。1990年ISO的C标准在1999年也更新了,有些改动影响到POSIX.1标准中的接口。
目前的POSIX.1规范涵盖了更多的接口。Open Group(原称X/Open)发布的“Single UNIX Specification”的基本规范现在已经并入POSIX.1,后者包含了几个1003.1标准和另外几个标准草案,原来这些标准是分开出版的。
我也相应地增加了些章节,讨论新主题。线程和多线程编程是相当重要的概念,因为它们为程序员处理并发和异步提供了更清楚的方式。
套接字接口现在也是POSIX.1的一部分了。它为进程间通信(IPC)提供了单一的接口,而不考虑进程的位置。它成为IPC章节的自然扩展。
我省略了POSIX.1中的大部分实时接口。这些内容最好是在一本专门讲述实时编程的书中介绍。参考文献里有一本这方面的书。
我把最后面几章的案例研究也更新了,用了更接近现实的例子。例如,现在很少有系统通过串口或并口连接PostScript打印机了,多数PostScript打印机是通过网络连接的,所以我对PostScript打印机通信的例子做了修改。
有关调制解调器通信的那一章如今已经不太适用了。
书中多数实例已经在下述4种平台上运行过了。
(1)FreeBSD 5.2.1,是加州大学伯克利分校CSRG的4.4BSD的一个变种,在英特尔奔腾处理器上运行。
(2)Linux 2.4.22(Mandrake 9.2发布),是一个免费的类UNIX操作系统,运行于英特尔奔腾处理器上。
(3)Solaris 9,是Sun公司系统V版本4的变种,运行于64位的UltraSPARC IIi处理器上。
(4)Darwin 7.4.0,是基于FreeBSD和Mach的操作系统环境,也是Apple Mac OS X 10.3版本的核心,运行于PowerPC处理器上。
首先要感谢Rich Stevens独立创作了本书第1版,它立即成为一本经典著作。
没有家人的支持,我不可能修订此书。他们容忍我满屋子散落稿纸(比平常更甚),霸占了家里的好几台机器,成天埋头于电脑屏幕前。我的妻子Jeanne甚至亲自动手帮我在一台测试的机器上安装了Linux。
多名技术审校者提出了很多改进意见,以确保内容准确。我非常感谢David Bausum、David Boreham、Keith Bostic、Mark Ellis、Phil Howard、Andrew Josey、Mukesh Kacker、Brian Kernighan、Bengt Kleberg、Ben Kuperman、Eric Raymond和Andy Rudoff。
我还要谢谢Andy Rudoff给我解答有关Solaris的问题,谢谢Dennis Ritchie不惜花时间从故纸堆中为我寻找有关历史方面问题的答案。再次谢谢Addison-Wesley公司的员工,与他们合作令人愉快,谢谢Tyrrell Albaugh、Mary Franz、John Fuller、Karen Gettman、Jessica Goldstein、Noreen Regina和John Wait。特别感谢Evelyn Pyle细致地编辑了本书。
就像Rich曾经做到的那样,我非常欢迎读者发来邮件,发表评论,提出建议,订正错误。
Stephen A. Rago
sar@apuebook.com
2005年4月于新泽西州沃伦市
本书描述了UNIX系统的程序设计接口——系统调用接口和标准C库提供的很多函数。本书针对的是所有的程序员。
与大多数操作系统一样,UNIX为程序运行提供了大量的服务——打开文件、读文件、启动一个新程序、分配存储区以及获得当前时间等。这些服务被称为系统调用接口(system call interface)。另外,标准C库提供了大量广泛用于C程序中的函数(格式化输出变量的值、比较两个字符串等)。
系统调用接口和库函数可参见《UNIX程序员手册》第2、3部分。本书不是这些内容的重复。手册中没有给出实例及基本原理,而这些则正是本书所要讲述的内容。
20世纪80年代出现了各种版本的UNIX,20世纪80年代后期,人们在此基础上制定了数个国际标准,包括C程序设计语言的ANSI标准、IEEE POSIX标准系列(还在制定中)、X/Open可移植性指南。
本书也介绍了这些标准,但是并不只是说明标准本身,而是着重说明它们与应用广泛的一些实现(主要指SVR4以及即将发布的4.4BSD)之间的关系。这是一种贴近现实世界的描述,而这正是标准本身以及仅描述标准的文献所缺少的。
本书分为以下6个部分。
(1)对UNIX程序设计基本概念和术语的简要描述(第1章),以及对各种UNIX标准化工作和不同UNIX实现的讨论(第2章)。
(2)I/O—不带缓存的I/O(第3章)、文件和目录(第4章)、标准I/O库(第5章)和标准系统数据文件(第6章)。
(3)进程—UNIX进程的环境(第7章)、进程控制(第8章)、进程之间的关系(第9章)和信号(第10章)。
(4)更多的I/O—终端I/O(第11章)、高级I/O(第12章)和守护进程(第13章)。
(5)IPC—进程间通信(第14章和第15章)。
(6)实例—一个数据库的函数库(第16章)、与PostScript 打印机的通信(第17章)、调制解调器拨号程序(第18章)和使用伪终端(第19章)。
如果对C语言较熟悉并具有某些应用UNIX的经验,对学习本书将非常有益,但是并不要求读者必须具有UNIX编程经验。本书面向的读者主要是:熟悉UNIX的程序员,以及熟悉其他某个操作系统且希望了解大多数UNIX系统提供的各种服务细节的程序员。
本书包含了大量实例—大约10 000行源代码。所有实例都用ANSI C语言编写。在阅读本书时,建议准备一本你所使用的UNIX系统的《UNIX程序员手册》,在细节方面有时需要参考该手册。
几乎对于每一个函数和系统调用,本书都用一个小的完整的程序进行了演示。这可以让读者清楚地了解它们的用法,包括参数和返回值等。有些小程序还不足以说明库函数和系统调用的复杂功能和应用技巧,所以书中还包含了一些较大的实例(见第16章至第19章)。
所有实例的源代码文件都可在因特网上用匿名ftp从因特网主机ftp.uu.net的published/ books/stevens. advprog.tar.Z文件下载。读者可以在自己的机器上修改并运行这些源代码。
遗憾的是,所有的操作系统都在不断变更,UNIX也不例外。下图给出了系统V和4.xBSD最近的进展情况。
4.xBSD是由加州大学伯克利分校CSRG开发的。该小组还发布了BSD Net1和BSD Net2版,其公开的源代码源自4.xBSD系统。SVRx表示AT&T的系统V第x版。XPG3指X/Open可移植性指南的第3个发行版。ANSI C是C语言的ANSI标准。POSIX.1是IEEE和ISO的类UNIX系统接口标准。2.2节和2.3节将对这些标准和不同版本之间的差别做更多的说明。
本书中用4.3+BSD表示源自伯克利的介于BSD Net2和4.4BSD之间的UNIX系统。
在本书写作时,4.4BSD尚未发布,所以还不能称之为4.4BSD。为了用一个简单的名字来引用该系统,故使用4.3+BSD。
本书中的大多数实例曾在下面4种UNIX系统上运行过。
(1)U.H公司(UHC)的UNIX系统V/386 R4.0.2(vanilla SVR4),运行于Intel 80386处理器上。
(2)加州大学伯克利分校CSRG的4.3+BSD,运行于惠普工作站上。
(3)伯克利软件设计公司的BSD/386(是BSD Net2的变种),运行于Intel 80386处理器上。该系统与4.3+BSD几乎相同。
(4)Sun公司的SunOS 4.1.1和4.1.2(该系统与伯克利系统有很深的渊源,但也包含了许多系统V的特性),运行于SPARCstation SLC上。
本书还提供了许多对系统进行的时间测试,并注明了用于测试的实际系统。
在过去的一年半中,家人给予了我大力支持和爱,因为写书我们失去了很多快乐的周末,我深感歉疚。写书从许多方面影响了整个家庭。谢谢Sally、Bill、Ellen和David。
我要特别感谢Brian Kernighan对我写作此书的帮助。他审阅了全部书稿,不但提出了大量深入细致的审稿意见,还对更好的行文风格给出了恰当的建议,但愿我能够在最终成稿中已经加以体现。Stephen Rago也成为了我的创作源泉,不但审阅了全部书稿,还为我解答了有关系统V的许多技术细节和历史问题。还要感谢Addison-Wesley公司邀请的其他技术审校者,他们对书稿的各个部分提出了很有价值的意见,他们是Maury Bach、Mark Ellis、Jeff Gitlin、Peter Honeyman、John Linderman、Doug McIlroy、Evi Nemeth、Craig Patridge、Dave Presotto、Gary Wilson、Gary Wright。
感谢加州大学伯克利分校CSRG的Keith Bostic和Kirk McKusick给了我一个账号,可在最新的BSD系统上测试书中实例(还要感谢Peter Salus)。UHC的Sam Nataros和Joachim Sacksen给我提供了一份SVR4,用来测试书中例子。Trent Hein则帮助我获得BSD/386的alpha和beta版。
其他朋友在过去这些年以各种方式提供了帮助,这些帮助看似不大,却非常重要。他们是Paul Lucchina、Joe Godsil、Jim Hogue、Ed Tankus和Gary Wright。本书的编辑是Addison-Wesley公司的John Wait,他自始至终是我的忠实朋友。我不断地延期交稿,写作篇幅也一再超过计划,他从不抱怨。特别还要感谢美国国家光学天文台(NOAO),尤其是Sidney Wolff、Richard Wolff和Steve Grandi,为我提供准确的计算机时间。
真正的UNIX图书应该用troff写成,本书也遵循了这一优秀传统。最终清样是作者用James Clark写的groff软件包做出来的。非常感谢James Clark提供了这个优异的写作软件,并迅速地修正其中所发现的bug。也许有一天我会最终弄清楚troff软件做脚注的技巧。
我十分欢迎读者发来电子邮件,发表评论,提出建议,订正错误。
W. Richard Stevens
rstevens@kohala.com
1992年4月于亚利桑那州塔克森市
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
本书提供配套源代码,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ selfpublish/submission即可)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
所有操作系统都为它们所运行的程序提供服务。典型的服务包括:执行新程序、打开文件、读文件、分配存储区以及获得当前时间等,本书集中阐述不同版本的UNIX操作系统所提供的服务。
想要按严格的先后顺序介绍UNIX,而不超前引用尚未介绍过的术语,这几乎是不可能的(可能也会令人厌烦)。本章从程序员的角度快速浏览UNIX,对书中引用的一些术语和概念进行简要的说明并给出实例。在以后各章中,将对这些概念做更详细的说明。对于初涉UNIX环境的程序员,本章还简要介绍了UNIX提供的各种服务。
从严格意义上说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。我们通常将这种软件称为内核(kernel),因为它相对较小,而且位于环境的核心。图1-1显示了UNIX系统的体系结构。
图1-1 UNIX操作系统的体系结构
内核的接口被称为系统调用(system call,图1-1中的阴影部分)。公用函数库构建在系统调用接口之上,应用程序既可使用公用函数库,也可使用系统调用。(我们将在1.11节对系统调用和库函数做更多说明。)shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
从广义上说,操作系统包括了内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特性。这里所说的其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等。
例如,Linux是GNU操作系统使用的内核。一些人将这种操作系统称为GNU/Linux操作系统,但是,更常见的是简单地称其为Linux。虽然这种表达方法在严格意义上讲并不正确,但鉴于“操作系统”这个词的双重含义,这种叫法还是可以理解的(这样的叫法更简洁)。
用户在登录UNIX系统时,先键入登录名,然后键入口令。系统在其口令文件(通常是/etc/ passwd
文件)中查看登录名。口令文件中的登录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字用户ID(205)、数字组ID(105)、注释字段、起始目录(/home/sar
)以及shell程序(/bin/ksh
)。
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
目前,所有的系统已将加密口令移到另一个文件中。第6章将说明这种文件以及访问它们的函数。
用户登录后,系统通常先显示一些系统信息,然后用户就可以向shell程序键入命令。(当用户登录时,某些系统启动一个视窗管理程序,但最终总会有一个shell程序运行在一个视窗中)。shell是一个命令行解释器,它读取用户输入,然后执行命令。shell的用户输入通常来自于终端(交互式shell),有时则来自于文件(称为shell脚本)。图1-2总结了UNIX系统中常见的shell。
名称 |
路径 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
---|---|---|---|---|---|
Bourne shell |
|
• |
• |
|
• |
Bourne-again shell |
|
可选的 |
• |
• |
• |
C shell |
|
链接至 |
可选的 |
链接至 |
• |
Korn shell |
|
可选的 |
可选的 |
• |
• |
TENEX C shell |
|
• |
可选的 |
• |
• |
图1-2 UNIX系统中常见的shell
系统从口令文件中相应用户登录项的最后一个字段中了解到应该为该登录用户执行哪一个shell。
自V7以来,由Steve Bourne在贝尔实验室开发的Bourne shell得到了广泛应用,几乎每一个现有的UNIX系统都提供Bourne shell,其控制流结构类似于Algol 68。
C shell是由Bill Joy在伯克利开发的,所有BSD版本都提供这种shell。另外,AT&T的System V/386 R3.2和System V R4(SVR4)也提供C shell(下一章将对这些不同版本的UNIX系统做更多说明)。C shell是在第6版shell而非Bourne shell的基础上构造的,其控制流类似于C语言,它支持Bourne shell没有的一些特色功能,例如作业控制、历史机制以及命令行编辑等。
Korn shell是Bourne shell的后继者,它首先在SVR4中提供。Korn shell是由贝尔实验室的David Korn开发的,在大多数UNIX系统上运行,但在SVR4之前,通常它需要另行购买,所以没有其他两种shell流行。它与Bourne shell向上兼容,并具有使C shell广泛得到应用的一些特色功能,包括作业控制以及命令行编辑等。
Bourne-again shell是GNU shell,所有Linux系统都提供这种shell。它的设计遵循POSIX标准,同时也保留了与Bourne shell的兼容性。它支持C shell和Korn shell两者的特色功能。
TENEX C shell是C shell的加强版本。它从TENEX操作系统(1972年BBN公司开发)借鉴了很多特色,例如命令完备。TENEX C shell在C shell基础上增加了很多特性,常被用来替换C shell。
POSIX 1003.2标准对shell进行了标准化。这项规范基于Korn shell和Bourne shell的特性。
不同的Linux系统使用不同的默认shell。一些Linux默认使用Bourne-again shell。另外一些使用BSD的对Bourne shell的替代品dash(Debian Almquist shell,最早由Kenneth Almquist开发,并在后来移植入Linux)。FreeBSD的默认用户shell衍生于Almquist shell。Mac OS X的默认shell是Bourne-again shell。
Solaries继承了BSD和System V两者,它提供了图1-2中所示的所有shell。在因特网上可以找到shell的自由移植版软件。
本书将使用这种形式的注释来描述历史注释,并对不同的UNIX系统的实现进行比较。当我们了解到历史缘由后,会更好地理解采用某种特定实现技术的原因。
本书将使用很多交互式shell实例来执行所开发的程序,这些实例使用了Bourne shell、Korn shell和Bourne-again shell通用的功能。
UNIX文件系统是目录和文件的一种层次结构,所有东西的起点是称为根(root)的目录,这个目录的名称是一个字符“/
”。
目录(directory)是一个包含目录项的文件。在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性是指文件类型(是普通文件还是目录等)、文件大小、文件所有者、文件权限(其他用户能否访问该文件)以及文件最后的修改时间等。stat
和fstat
函数返回包含所有文件属性的一个信息结构。第4章将详细说明文件的各种属性。
目录项的逻辑视图与实际存放在磁盘上的方式是不同的。UNIX文件系统的大多数实现并不在目录项中存放属性,这是因为当一个文件具有多个硬链接时,很难保持多个属性副本之间的同步。这一点将在第4章讨论硬链接时理解得更明晰。
目录中的各个名字称为文件名(filename)。只有斜线(/
)和空字符这两个字符不能出现在文件名中。斜线用来分隔构成路径名的各文件名,空字符则用来终止一个路径名。尽管如此,好的习惯还是只使用常用印刷字符的一个子集作为文件名字符(如果在文件名中使用了某些shell的特殊字符,则必须使用shell的引号机制来引用文件名,这会带来很多麻烦)。事实上,为了可移植性,POSIX.1推荐将文件名限制在以下字符集之内:字母(a
~z
、A
~Z
)、数字(0
~9
)、句点(.
)、短横线(-
)和下划线(_
)。
创建新目录时会自动创建了两个文件名:.
(称为点)和..
(称为点点)。点指向当前目录,点点指向父目录。在最高层次的根目录中,点点与点相同。
Research UNIX System和某些早期UNIX System V的文件系统限制文件名的最大长度为14个字符,BSD版本则将这种限制扩展为255个字符。现今,几乎所有商业化的UNIX文件系统都支持超过255个字符的文件名。
由斜线分隔的一个或多个文件名组成的序列(也可以斜线开头)构成路径名(pathname),以斜线开头的路径名称为绝对路径名(absolute pathname),否则称为相对路径名(relative pathname)。相对路径名指向相对于当前目录的文件。文件系统根的名字(/
)是一个特殊的绝对路径名,它不包含文件名。
实例
不难列出一个目录中所有文件的名字,图1-3是ls
(1)命令的简要实现。
#include "apue.h"
#include <dirent.h>
int
main(int argc, char *argv[])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
err_quit("usage: ls directory_name");
if ((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
exit(0);
}
图1-3 列出一个目录中的所有文件
ls
(1)这种表示方法是UNIX系统的惯用方法,用以引用UNIX系统手册中的一个特定项。ls
(1)引用第一部分中的ls
项。各部分通常用数字1~8编号,在每个部分中的各项则按字母顺序排列。在本书中始终假定你有自己所使用的UNIX系统的手册。
早期的UNIX系统把8个部分都集中在一本《UNIX程序员手册》(UNIX Programmer’s Manual)中。随着页数的增加,现在的趋势是把这些部分分别安排在不同的手册中,例如用户手册、程序员手册以及系统管理员手册等。
一些UNIX系统用大写字母把某一部分手册进一步分成若干小部分,例如,AT&T[1990e]中的所有标准I/O函数都被指明位于3S部分中,例如
fopen
(3S)。另一些UNIX系统不用数字而是用字母将手册分成若干部分,如用C表示命令部分等。
现今,大多数手册都以电子文档形式提供。如果用的是联机手册,则可用下面的命令查看ls
命令手册页:
man 1 ls
或
man -s1 ls
图1-3只打印一个目录中各个文件的名字,不显示其他信息,如果该源文件名为myls.c
,则可以用下面的命令对其进行编译,编译结果是生成默认名为a.out
的可执行文件中。
cc myls.c
历史上,
cc
(1)是C编译器。在配置了GNU C编译系统的系统中,C编译器是gcc
(1)。其中,cc
通常链接至gcc
。
示例输出如下:
$ ./a.out /dev
.
..
cdrom
stderr
stdout
stdin
fd
sda4
sda3
sda2
sda1
sda
tty2
tty1
console
tty
zero
null
很多行未显示
mem
$ ./a.out /etc/ssl/private
can't open /etc/ssl/private: Permission denied
$ ./a.out /dev/tty
can't open /dev/tty: Not a directory
本书将以以下方式表示输入的命令及其输出:输入的字符以等宽粗体表示,程序输出则以上面所示的等宽字体表示。对输出的注释以中文宋体表示。输入之前的美元符号($
)是shell的提示符,本书总是将shell提示符表示为$
。
注意,myls
程序列出的目录中的文件名不是以字母顺序列出的,而ls
命令一般是按字母顺序打印目录项。
在这个20行的程序中,有很多细节需要考虑。
apue.h
。本书中几乎每一个程序都包含此头文件。它包含了某些标准系统头文件,定义了许多常量及函数原型,这些都将用于本书的各个实例中,附录B列出了这一头文件。dirent.h
,以便使用opendir
和readdir
的函数原型,以及dirent
结构的定义。在其他一些系统里,这些定义被分成多个头文件。比如,在Ubuntu 12.04中,/usr/include/dirent.h
声明了函数原型,并且包含bits/dirent.h
,后者定义了dirent
结构(真正存放在/usr/include/x86_64- linux-gnu/bits
下)。main
函数的声明使用了ISO C标准所使用的风格(下一章将对ISO C标准进行更多说明)。argv[1]
作为要列出其各个目录项的目录名。第7章将说明main
函数如何被调用,程序如何存取命令行参数和环境变量。opendir
、readdir
和closedir
对目录进行处理。opendir
函数返回指向DIR
结构的指针,我们将该指针传送给readdir
函数。我们并不关心DIR
结构中包含了什么。然后,在循环中调用readdir
来读每个目录项。它返回一个指向dirent
结构的指针,而当目录中已无目录项可读时则返回null
指针。在dirent
结构中取出的只是每个目录项的名字(d_name
)。使用该名字,此后就可调用stat
函数(见4.2节)以获得该文件的所有属性。err_sys
和err_quit
。从上面的输出中可以看到,err_sys
函数打印一条消息(“Permission denied”或“Not a directory”),说明遇到了什么类型的错误。这两个出错处理函数在附录B中说明,1.7节将更多地叙述出错处理。exit
。函数exit
终止程序。按惯例,参数0的意思是正常结束,参数值1~255则表示出错。8.5节将说明一个程序(如shell或我们所编写的程序)如何获得它所执行的另一个程序的exit
状态。 每个进程都有一个工作目录(working directory),有时称其为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释。进程可以用chdir
函数更改其工作目录。
例如,相对路径名doc/memo/joe
指的是当前工作目录中的doc
目录中的memo
目录中的文件(或目录)joe
。从该路径名可以看出,doc
和memo
都应当是目录,但是却不能分辨joe
是文件还是目录。路径名/usr/lib/lint
是一个绝对路径名,它指的是根目录中的usr
目录中的lib
目录中的文件(或目录)lint
。
登录时,工作目录设置为起始目录(home directory),该起始目录从口令文件(见1.3节)中相应用户的登录项中取得。
文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这个文件描述符。
按惯例,每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(standard input)、标准输出(standard output)以及标准错误(standard error)。如果不做特殊处理,例如就像简单的命令ls
,则这3个描述符都链接向终端。大多数shell都提供一种方法,使其中任何一个或所有这3个描述符都能重新定向到某个文件,例如:
ls > file.list
执行ls
命令,其标准输出重新定向到名为file.list
的文件。
函数open
、read
、write
、lseek
以及close
提供了不带缓冲的I/O。这些函数都使用文件描述符。
实例
如果愿意从标准输入读,并向标准输出写,则图1-4中所示的程序可用于复制任一UNIX普通文件。
#include "apue.h"
#define BUFFSIZE 4096
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-4 将标准输入复制到标准输出
头文件<unistd.h>
(apue.h
中包含了此头文件)及两个常量STDINFILENO
和STDOUT FILENO
是POSIX
标准的一部分(下一章将对此做更多的说明)。头文件<unistd.h>
包含了很多UNIX
系统服务的函数原型,例如图1-4程序中调用的read
和write
。
两个常量STDIN_FILENO
和STDOUT_FILENO
定义在<unistd.h>
头文件中,它们指定了标准输入和标准输出的文件描述符。在POSIX标准中,它们的值分别是0和1,但是考虑到可读性,我们将使用这些名字来表示这些常量。
3.9节将详细讨论BUFFSIZE
常量,说明它的各种不同值将如何影响程序的效率。但是不管该常量的值如何,此程序总能复制任一UNIX普通文件。
read
函数返回读取的字节数,此值用作要写的字节数。当到达输入文件的尾端时,read
返回0,程序停止执行。如果发生了一个读错误,read
返回−1。出错时大多数系统函数返回−1。
如果将该程序编译成标准名称的a.out
文件,并以下列方式执行它:
./a.out > data
那么标准输入是终端,标准输出则重新定向至文件data
,标准错误也是终端。如果此输出文件并不存在,则shell会创建它。该程序将用户键入的各行复制到标准输出,键入文件结束符(通常是Ctrl+D)时,将终止本次复制。
若以下列方式执行该程序:
./a.out < infile > outfile
会将名为infile
文件的内容复制到名为outfile
的文件中。
第3章将更详细地说明不带缓冲的I/O函数。
标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小,如图1-4中的BUFFSIZE
常量的大小。使用标准I/O函数还简化了对输入行的处理(常常发生在UNIX的应用程序中)。例如,fgets
函数读取一个完整的行,而read
函数读取指定字节数。在5.4节中我们将了解到,标准I/O函数库提供了使我们能够控制该库所使用的缓冲风格的函数。
我们最熟悉的标准I/O函数是printf
。在调用printf
的程序中,总是包含<stdio.h>
(在本书中,该头文件包含在apue.h
中),该头文件包括了所有标准I/O函数的原型。
实例
图1-5程序的功能类似于前一个调用了read
和write
的程序,5.8节将对此程序进行更详细的说明。它将标准输入复制到标准输出,也就能复制任一UNIX普通文件。
#include "apue.h"
int
main(void)
{
int c;
while ((c = getc(stdin)) != EOF)
if (putc(c, stdout) == EOF)
err_sys("output error");
if (ferror(stdin))
err_sys("input error");
exit(0);
}
图1-5 用标准I/O将标准输入复制到标准输出
函数getc
一次读取一个字符,然后函数putc
将此字符写到标准输出。读到输入的最后一个字节时,getc
返回常量EOF
(该常量在<stdio.h>
中定义)。标准I/O常量stdin
和stdout
也在头文件<stdio.h>
中定义,它们分别表示标准输入和标准输出。
程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用exec
函数(7个exec
函数之一),将程序读入内存,并执行程序。8.10节将说明这些exec
函数。
程序的执行实例被称为进程(process)。本书的每一页几乎都会使用这一术语。某些操作系统用任务(task)表示正在被执行的程序。
UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID(process ID)。进程ID总是一个非负整数。
实例
图1-6程序用于打印进程ID。
#include "apue.h"
int
main(void)
{
printf("hello world from process ID %ld\n", (long)getpid());
exit(0);
}
图1-6 打印进程ID
如果将该程序编译成a.out
文件,然后执行它,则有:
$ ./a.out
hello world from process ID 851
$ ./a.out
hello world from process ID 854
此程序运行时,它调用函数getpid
得到其进程ID。我们将会在后面看到,getpid
返回一个pid_t
数据类型。我们不知道它的大小,仅知道的是标准会保证它能保存在一个长整型中。因为我们必须在printf
函数中指定需要打印的每一个变量的大小,所以我们必须把它的值强制转换为它可能会用到的最大的数据类型(这里是长整型)。虽然大多数进程ID可以用整型表示,但用长整型可以提高可移植性。
有3个用于进程控制的主要函数:fork
、exec
和waitpid
。(exec
函数有7种变体,但经常把它们统称为exec
函数。)
实例
UNIX系统的进程控制功能可以用一个简单的程序说明(见图1-7)。该程序从标准输入读取命令,然后执行这些命令。它类似于shell程序的基本实施部分。
#include "apue.h"
#include <sys/wait.h>
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
图1-7 从标准输入读命令并执行
在这个30行的程序中,有很多功能需要考虑。
fgets
从标准输入一次读取一行。当键入文件结束符(通常是Ctrl+D)作为行的第一个字符时,fgets
返回一个null指针,于是循环停止,进程也就终止。第18章将说明所有特殊的终端字符(文件结束、退格字符、整行擦除等),以及如何改变它们。fgets
返回的每一行都以换行符终止,后随一个null字节,因此用标准C函数strlen
计算此字符串的长度,然后用一个null字节替换换行符。这样做是因为execlp
函数要求的参数是以null结束的而不是以换行符结束的。fork
创建一个新进程。新进程是调用进程的一个副本,我们称调用进程为父进程,新创建的进程为子进程。fork
对父进程返回新的子进程的进程ID(一个非负整数),对子进程则返回0。因为fork
创建一个新进程,所以说它被调用一次(由父进程),但返回两次(分别在父进程中和在子进程中)。execlp
以执行从标准输入读入的命令。这就用新的程序文件替换了子进程原先执行的程序文件。fork
和跟随其后的exec
两者的组合就是某些操作系统所称的产生(spawn)一个新进程。在UNIX系统中,这两部分分离成两个独立的函数。第8章将对这些函数进行更多说明。execlp
执行新程序文件,而父进程希望等待子进程终止,这是通过调用waitpid
实现的,其参数指定要等待的进程(即pid
参数是子进程ID)。waitpid
函数返回子进程的终止状态(status
变量)。在我们这个简单的程序中,没有使用该值。如果需要,可以用此值准确地判定子进程是如何终止的。ls
命令。为了传递参数,先要分析输入行,然后用某种约定把参数分开(可能使用空格或制表符),再将分隔后的各个参数传递给execlp
函数。尽管如此,此程序仍可用来说明UNIX系统的进程控制功能。如果运行此程序,将得到下列结果。注意,该程序使用了一个不同的提示符(%
),以区别于shell的提示符。
$ ./a.out
% date
Sat Jan 21 19:42:07 EST 2012
% who
sar console Jan 1 14:59
sar ttys000 Jan 1 14:59
sar ttys001 Jan 15 15:28
% pwd
/home/sar/bk/apue/3e
% ls
Makefile
a.out
shell1.c
% ^D 键入文件结束符
$ 常规的shell提示符
^D
表示一个控制字符。控制字符是特殊字符,其构成方法是:在键盘上按下控制键——通常被标记为Control
或Ctrl
,同时按另一个键。Ctrl+D或^D
是默认的文件结束符。在第18章中讨论终端I/O时,会介绍更多的控制字符。
通常,一个进程只有一个控制线程(thread)——某一时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易得多。另外,多个控制线程也可以充分利用多处理器系统的并行能力。
一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性。
与进程相同,线程也用ID标识。但是,线程ID只在它所属的进程内起作用。一个进程中的线程ID在另一个进程中没有意义。当在一进程中对某个特定线程进行处理时,我们可以使用该线程的ID引用它。
控制线程的函数与控制进程的函数类似,但另有一套。线程模型是在进程模型建立很久之后才被引入到UNIX系统中的,然而这两种模型之间存在复杂的交互,在第12章中,我们会对此进行说明。
当UNIX系统函数出错时,通常会返回一个负值,而且整型变量errno
通常被设置为具有特定信息的值。例如,open
函数如果成功执行则返回一个非负文件描述符,如出错则返回−1。在open
出错时,有大约15种不同的errno
值(文件不存在、权限问题等)。而有些函数对于出错则使用另一种约定而不是返回负值。例如,大多数返回指向对象指针的函数,在出错时会返回一个null指针。
文件<errno.h>
中定义了errno
以及可以赋予它的各种常量。这些常量都以字符E
开头。另外,UNIX系统手册第2部分的第1页,intro
(2)列出了所有这些出错常量。例如,若errno
等于常量EACCES
,表示产生了权限问题(例如,没有足够的权限打开请求文件)。
在Linux中,出错常量在
errno
(3)手册页中列出。
POSIX和ISO C将errno
定义为一个符号,它扩展成为一个可修改的整形左值(lvalue)。它可以是一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。以前使用的定义是:
extern int errno;
但是在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于它自己的局部errno
以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno
,将其定义为:
extern int *__errno_location(void);
#define errno (*__errno_location())
对于errno
应当注意两条规则。第一条规则是:如果没有出错,其值不会被例程清除。因此,仅当函数的返回值指明出错时,才检验其值。第二条规则是:任何函数都不会将errno
值设置为0,而且在<errno.h>
中定义的所有常量都不为0。
C标准定义了两个函数,它们用于打印出错信息。
#include <string.h>
char *strerror(int errnum);
返回值:指向消息字符串的指针
strerror
函数将errnum(通常就是errno
值)映射为一个出错消息字符串,并且返回此字符串的指针。
perror
函数基于errno
的当前值,在标准错误上产生一条出错消息,然后返回。
#include
void perror(const char *msg);
它首先输出由msg指向的字符串,然后是一个冒号,一个空格,接着是对应于errno
值的出错消息,最后是一个换行符。
实例
图1-8程序显示了这两个出错函数的使用方法。
#include "apue.h"
#include <errno.h>
int
main(int argc, char *argv[])
{
fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
图1-8 例示strerror
和perror
如果将此程序编译成文件a.out
,然后执行它,则有
$ ./a.out
EACCES: Permission denied
./a.out: No such file or directory
注意,我们将程序名(argv[0]
,其值是./a.out
)作为参数传递给perror
。这是一个标准的UNIX惯例。使用这种方法,在程序作为管道的一部分执行时,例如:
prog1 < inputfile | prog2 | prog3 > outputfile
我们就能分清3个程序中的哪一个产生了一条特定的出错消息。
本书中的所有实例基本上都不直接调用strerror
或perror
,而是使用附录B中的出错函数。该附录中的出错函数使我们只用一条C语句就可利用ISO C的可变参数表功能处理出错情况。
可将在<errno.h>
中定义的各种出错分成两类:致命性的和非致命性的。对于致命性的错误,无法执行恢复动作。最多能做的是在用户屏幕上打印出一条出错消息或者将一条出错消息写入日志文件中,然后退出。对于非致命性的出错,有时可以较妥善地进行处理。大多数非致命性出错是暂时的(如资源短缺),当系统中的活动较少时,这种出错很可能不会发生。
与资源相关的非致命性出错包括:EAGAIN
、ENFILE
、ENOBUFS
、ENOLCK
、ENOSPC
、EWOULDBLOCK
,有时ENOMEM
也是非致命性出错。当EBUSY
指明共享资源正在使用时,也可将它作为非致命性出错处理。当EINTR
中断一个慢速系统调用时,可将它作为非致命性出错处理(在10.5节对此会进行更多说明)。
对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然后重试。这种技术可应用于其他情况。例如,假设出错表明一个网络连接不再起作用,那么应用程序可以采用这种方法,在短时间延迟后,尝试重建该连接。一些应用使用指数补偿算法,在每次迭代中等待更长时间。
最终,由应用的开发者决定在哪些情况下应用程序可以从出错中恢复。如果能够采用一种合理的恢复策略,那么可以避免应用程序异常终止,进而就能改善应用程序的健壮性。
口令文件登录项中的用户ID(user ID)是一个数值,它向系统标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。下面将介绍内核如何使用用户ID来检验该用户是否有执行某些操作的权限。
用户ID为0的用户为根用户(root)或超级用户(superuser)。在口令文件中,通常有一个登录项,其登录名为root
,我们称这种用户的特权为超级用户特权。我们将在第4章中看到,如果一个进程具有超级用户特权,则大多数文件权限检查都不再进行。某些操作系统功能只向超级用户提供,超级用户对系统有自由的支配权。
Mac OS X客户端版本交由用户使用时,禁用超级用户账户,服务器版本则可使用该账户。在Apple的网站可以找到使用说明,它告知如何才能使用该账户。
口令文件登录项也包括用户的组ID(group ID),它是一个数值。组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件中有多个登录项具有相同的组ID。组被用于将若干用户集合到项目或部门中去。这种机制允许同组的各个成员之间共享资源(如文件)。4.5节将介绍可以通过设置文件的权限使组内所有成员都能访问该文件,而组外用户不能访问。
组文件将组名映射为数值的组ID。组文件通常是/etc/group
。
使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于磁盘上的每个文件,文件系统都存储该文件所有者的用户ID和组ID。存储这两个值只需4字节(假定每个都以双字节的整型值存放)。如果使用完整ASCII登录名和组名,则需更多的磁盘空间。另外,在检验权限期间,比较字符串较之比较整型数更消耗时间。
但是对于用户而言,使用名字比使用数值方便,所以口令文件包含了登录名和用户ID之间的映射关系,而组文件则包含了组名和组ID之间的映射关系。例如,ls -l
命令使用口令文件将数值的用户ID映射为登录名,从而打印出文件所有者的登录名。
早期的UNIX系统使用16位整型数表示用户ID和组ID。现今的UNIX系统使用32位整型数表示用户ID和组ID。
实例
图1-9程序用于打印用户ID和组ID。
#include "apue.h"
int
main(void)
{
printf("uid = %d, gid = %d\n", getuid(), getgid());
exit(0);
}
图1-9 打印用户ID和组ID
程序调用getuid
和getgid
以返回用户ID和组ID。运行该程序的结果如下:
$ ./a.out
uid = 205, gid = 105
除了在口令文件中对一个登录名指定一个组ID外,大多数 UNIX系统版本还允许一个用户属于另外一些组。这一功能是从4.2BSD开始的,它允许一个用户属于多至16个其他的组。登录时,读文件/etc/group
,寻找列有该用户作为其成员的前16个记录项就可以得到该用户的附属组ID(supplementary group ID)。在下一章将说明,POSIX要求系统至少应支持8个附属组,实际上大多数系统至少支持16个附属组。
信号(signal)用于通知进程发生了某种情况。例如,若某一进程执行除法操作,其除数为0,则将名为SIGFPE
(浮点异常)的信号发送给该进程。进程有以下3种处理信号的方式。
(1)忽略信号。有些信号表示硬件异常,例如,除以0或访问进程地址空间以外的存储单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。
(2)按系统默认方式处理。对于除数为0,系统默认方式是终止该进程。
(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信号。通过提供自编的函数,我们就能知道什么时候产生了信号,并按期望的方式处理它。
很多情况都会产生信号。终端键盘上有两种产生信号的方法,分别称为中断键(interrupt key,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+\),它们被用于中断当前运行的进程。另一种产生信号的方法是调用kill
函数。在一个进程中调用此函数就可向另一个进程发送一个信号。当然这样做也有些限制:当向一个进程发送信号时,我们必须是那个进程的所有者或者是超级用户。
实例
回忆一下基本的shell实例(见图1-7程序)。如果调用此程序,然后按下中断键,则执行此程序的进程终止。产生这种后果的原因是:对于此信号(SIGINT
)的系统默认动作是终止进程。该进程没有告诉系统内核应该如何处理此信号,所以系统按默认方式终止该进程。
为了能捕捉到此信号,程序需要调用signal
函数,其中指定了当产生SIGINT
信号时要调用的函数的名字。函数名为sig_int
,当其被调用时,只是打印一条消息,然后打印一个新提示符。在图1-7程序中添加了11行,构成了图1-10程序(添加的11行以行首的+号指示)。
#include "apue.h"
#include <sys/wait.h>
+ static void sig_int(int); /* our signal-catching function */
+
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
+ if (signal(SIGINT, sig_int) == SIG_ERR)
+ err_sys("signal error");
+
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
+
+ void
+ sig_int(int signo)
+ {
+ printf("interrupt\n%% ");
+ }
图1-10 从标准输入读命令并执行
因为大多数重要的应用程序都对信号进行处理,所以第10章将详细介绍信号。
历史上,UNIX系统使用过两种不同的时间值。
(1)日历时间。该值是自协调世界时(Coordinated Universal Time,UTC)1970年1月1日00:00:00这个特定时间以来所经过的秒数累计值(早期的手册称UTC为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等。
系统基本数据类型time_t
用于保存这种时间值。
(2)进程时间。也被称为CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算。每秒钟曾经取为50、60或100个时钟滴答。
系统基本数据类型clock_t
保存这种时间值。2.5.4节将说明如何用sysconf
函数得到每秒的时钟滴答数。
当度量一个进程的执行时间时(见3.9节),UNIX系统为一个进程维护了3个进程时间值:
时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。每当在本书中提到时钟时间时,都是在系统中没有其他活动时进行度量的。
用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行内核程序所经历的时间。例如,每当一个进程执行一个系统服务时,如read
或write
,在内核内执行该服务所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间之和常被称为CPU时间。
要取得任一进程的时钟时间、用户时间和系统时间是很容易的——只要执行命令time
(1),其参数是要度量其执行时间的命令,例如:
$ cd /usr/include
$ time -p grep _POSIX_SOURCE */*.h > /dev/null
real om0.81s
user om0.11s
sys om0.07s
time
命令的输出格式与所使用的shell有关,其原因是某些shell并不运行/usr/bin/time
,而是使用一个内置函数测量命令运行所使用的时间。
8.17节将说明一个运行进程如何取得这3个时间。关于时间和日期的一般说明见6.10节。
所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点被称为系统调用(system call,见图1-1)。Research UNIX系统第7版提供了约50个系统调用,4.4BSD提供了约110个系统调用,而SVR4则提供了约120个系统调用。具体数字在不同操作系统版本中会不同,新近的大多数系统大大增加了支持的系统调用的个数。Linux 3.2.0提供了380个系统调用,FreeBSD 8.0提供的系统调用超过450个。
系统调用接口总是在《UNIX程序员手册》的第2部分中说明,是用C语言定义的,与具体系统如何调用一个系统调用的实现技术无关。这与很多早期的操作系统不同,那些系统按传统方式用机器的汇编语言定义内核入口点。
UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同样名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函数又用系统所要求的技术调用相应的内核服务。例如,函数可将一个或多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器指令。从应用角度考虑,可将系统调用视为C函数。
《UNIX程序员手册》的第3部分定义了程序员可以使用的通用库函数。虽然这些函数可能会调用一个或多个内核的系统调用,但是它们并不是内核的入口点。例如,printf
函数会调用write
系统调用以输出一个字符串,但函数strcpy
(复制一个字符串)和atoi
(将ASCII转换为整数)并不使用任何内核的系统调用。
从实现者的角度来看,系统调用和库函数之间有根本的区别,但从用户角度来看,其区别并不重要。在本书中,系统调用和库函数都以C函数的形式出现,两者都为应用程序提供服务。但是,我们应当理解,如果希望的话,我们可以替换库函数,但是系统调用通常是不能被替换的。
以存储空间分配函数malloc
为例。有多种方法可以进行存储空间分配及与其相关的无用空间回收操作(最佳适应、首次适应等),并不存在对所有程序都最优的一种技术。UNIX系统调用中处理存储空间分配的是sbrk
(2),它不是一个通用的存储器管理器。它按指定字节数增加或减少进程地址空间。如何管理该地址空间却取决于进程。存储空间分配函数malloc
(3)实现一种特定类型的分配。如果我们不喜欢其操作方式,则可以定义自己的malloc
函数,它很可能将使用sbrk
系统调用。事实上,有很多软件包,它们使用sbrk
系统调用实现自己的存储空间分配算法。图1-11显示了应用程序、malloc
函数以及sbrk
系统调用之间的关系。
图1-11 malloc
函数和sbrk
系统调用
从中可见,两者职责不同,内核中的系统调用分配一块空间给进程,而库函数malloc
则在用户层次管理这一空间。
另一个可说明系统调用和库函数之间差别的例子是,UNIX系统提供的判断当前时间和日期的接口。一些操作系统分别提供了一个返回时间的系统调用和另一个返回日期的系统调用。任何特殊的处理,例如正常时制和夏令时之间的转换,由内核处理或要求人为干预。UNIX系统则不同,它只提供一个系统调用,该系统调用返回自协调世界时1970年1月1日零时这个特定时间以来所经过的秒数。对该值的任何解释,例如将其变换成人们可读的、适用于本地时区的时间和日期,都留给用户进程进行处理。在标准C库中,提供了若干例程以处理大多数情况。这些库函数处理各种细节,如各种夏令时算法等。
应用程序既可以调用系统调用也可以调用库函数。很多库函数则会调用系统调用。图1-12显示了这种差别。
图1-12 C库函数和系统调用之间的差别
系统调用和库函数之间的另一个差别是:系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。我们从sbrk
系统调用和malloc
库函数之间的差别中可以看到这一点。当我们比较不带缓冲的I/O函数(见第3章)和标准I/O函数(见第5章)时,还将看到这种差别。
进程控制系统调用(fork
、exec
和wait
)通常由用户应用程序直接调用(请回忆图1-7中的基本shell)。但是为了简化某些常见的情况,UNIX系统也提供了一些库函数,如system
和popen
。8.13节将说明system
函数的一种实现,它使用基本的进程控制系统调用。10.18节还将强化这一实例以正确地处理信号。
为使读者了解大多数程序员应用的UNIX系统接口,我们不得不既说明系统调用,又介绍某些库函数。例如,若只描述sbrk
系统调用,那么就会忽略很多应用程序使用的malloc
库函数。本书除了必须要区分两者时,对系统调用和库函数都使用函数(function)这一术语来表示。
本章快速浏览了UNIX系统。说明了某些以后会多次用到的基本术语,介绍了一些小的UNIX程序实例。读者可以从中大概了解到本书其余部分将要介绍的内容。
下一章是关于UNIX系统的标准,以及这方面的工作对当前系统的影响。标准,特别是ISO C标准和POSIX.1标准,将影响本书的余下部分。
1.1 在系统上验证,除根目录外,目录.
和..
是不同的。
1.2 分析图1-6程序的输出,说明进程ID为852和853的进程发生了什么情况?
1.3 在1.7节中,perror
的参数是用ISO C的属性const
定义的,而strerror
的整型参数没有用此属性定义,为什么?
1.4 若日历时间存放在带符号的32位整型数中,那么到哪一年它将溢出?可以用什么方法扩展溢出浮点数?采用的策略是否与现有的应用相兼容?
1.5 若进程时间存放在带符号的32位整型数中,而且每秒为100时钟滴答,那么经过多少天后该时间值将会溢出?
人们在UNIX编程环境和C程序设计语言的标准化方面已经做了很多工作。虽然UNIX应用程序在不同的UNIX操作系统版本之间进行移植相当容易,但是20世纪80年代UNIX版本种类的剧增以及它们之间差别的扩大,导致很多大用户(如美国政府)呼吁对其进行标准化。
本章首先回顾过去近25年人们在UNIX标准化方面做出的种种努力,然后讨论这些UNIX编程标准对本书所列举的各种UNIX操作系统实现的影响。所有标准化工作的一个重要部分是对每种实现必须定义的各种限制进行说明,所以我们将说明这些限制以及确定它们值的各种方法。
1989年下半年,C程序设计语言的ANSI标准X3.159-1989得到批准。此标准也被采纳为国际标准ISO/IEC 9899:1990。ANSI是美国国家标准学会(American National Standards Institute)的缩写,它是国际标准化组织(International Organization for Standardization,ISO)中代表美国的成员。IEC是国际电子技术委员会(International Electrotechnical Commission)的缩写。
ISO C标准现在由ISO/IEC的C程序设计语言国际标准工作组维护和开发,该工作组称为ISO/ IEC JTC1/SC22/WG14,简称WG14。ISO C标准的意图是提供C程序的可移植性,使其能适合于大量不同的操作系统,而不只是适合UNIX系统。此标准不仅定义了C程序设计语言的语法和语义,还定义了其标准库(参见ISO 1999第7章;Plauger[1992];Kernighan和Ritchie[1988]中的附录B)。因为所有现今的UNIX系统(如本书介绍的几个UNIX系统)都提供C标准中定义的库函数,所以该标准库非常重要。
1999年,ISO C标准被更新,并被批准为ISO/IEC 9899:1999,它显著改善了对进行数值处理的应用软件的支持。除了对某些函数原型增加了关键字restrict
外,这种改变并不影响本书中描述的POSIX接口。restrict
关键字告诉编译器,哪些指针引用是可以优化的,其方法是指出指针引用的对象在函数中只通过该指针进行访问。
1999年以来,已经公布了3个技术勘误来修正ISO C标准中的错误,分别在2001年、2004年和2007年公布。如同大多数标准一样,在批准标准和修改软件使其符合标准两者之间有一段时间延迟。随着供应商编译系统的不断演化,对最新ISO C标准的支持也就越来越多。
虽然C标准已经在2011年更新,但由于其他标准还没有进行相应的更新,因此在本书中我们还是沿用1999年的版本。
按照该标准定义的各个头文件(见图2-1)可将ISO C库分成24个区。POSIX.1标准包括这些头文件以及另外一些头文件。从图2-1中可以看出,所有这些头文件在4种UNIX实现(FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10)中都支持。本章后面将对这4种UNIX实现进行说明。
ISO C头文件依赖于操作系统所配置的C编译器的版本。FreeBSD 8.0配置了
gcc
4.2.1版,Solaris 10配置了gcc
3.4.3版(以及Sun Studio自带的C编译器),Ubuntu 12.04(Linux 3.2.0)配置了gcc
4.6.3版,Mac OS X 10.6.8配置了gcc
4.0.1和4.2.1版。
头文件 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
说明 |
---|---|---|---|---|---|
|
• |
• |
• |
• |
验证程序断言 |
|
• |
• |
• |
• |
复数算术运算支持 |
|
• |
• |
• |
• |
字符分类和映射支持 |
|
• |
• |
• |
• |
出错码(1.7节) |
|
• |
• |
• |
• |
浮点环境 |
|
• |
• |
• |
• |
浮点常量及特性 |
|
• |
• |
• |
• |
整型格式变换 |
|
• |
• |
• |
• |
赋值、关系及一元操作符宏 |
|
• |
• |
• |
• |
实现常量(2.5节) |
|
• |
• |
• |
• |
本地化类别及相关定义 |
|
• |
• |
• |
• |
数学函数、类型声明及常量 |
|
• |
• |
• |
• |
非局部 |
|
• |
• |
• |
• |
信号(第10章) |
|
• |
• |
• |
• |
可变长度参数表 |
|
• |
• |
• |
• |
布尔类型和值 |
|
• |
• |
• |
• |
标准定义 |
|
• |
• |
• |
• |
整型 |
|
• |
• |
• |
• |
标准I/O库(第 5章) |
|
• |
• |
• |
• |
实用函数 |
|
• |
• |
• |
• |
字符串操作 |
|
• |
• |
• |
• |
通用类型数学宏 |
|
• |
• |
• |
• |
时间和日期(6.10节) |
|
• |
• |
• |
• |
扩充的多字节和宽字符支持 |
|
• |
• |
• |
• |
宽字符分类和映射支持 |
图2-1 ISO C标准定义的头文件
POSIX是一个最初由IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师学会)制订的标准族。POSIX指的是可移植操作系统接口(Portable Operating System Interface)。它原来指的只是IEEE标准1003.1-1988(操作系统接口),后来则扩展成包括很多标记为1003的标准及标准草案,如shell和实用程序(1003.2)。
与本书相关的是1003.1操作系统接口标准,该标准的目的是提升应用程序在各种UNIX系统环境之间的可移植性。它定义了“符合POSIX的”(POSIX compliant)操作系统必须提供的各种服务。该标准已被很多计算机制造商采用。虽然1003.1标准是以UNIX操作系统为基础的,但是它并不限于UNIX和UNIX类的系统。确实,有些提供专有操作系统的制造商也声称他们的系统符合POSIX(同时还保留所有专有功能)。
由于1003.1标准说明了一个接口(interface)而不是一种实现(implementation),所以并不区分系统调用和库函数。所有在标准中的例程都被称为函数。
标准是不断演进的,1003.1标准也不例外。该标准的1988版,即IEEE标准1003.1-1988经修改后递交给ISO,它没有增加新的接口或功能,但修订了文本。最终的文档作为IEEE标准1003.1-1990正式出版[IEEE 1990],这也就是国际标准ISO/IEC 9945-1:1990。该标准通常称为POSIX.1,本书将使用此术语来表示不同版本的标准。
IEEE 1003.1工作组此后继续对这一标准做了更多修改。1996年,该标准的修订版发布,它包括了1003.1-1990、1003.1b-1993实时扩展标准以及被称为pthreads的多线程编程接口(POSIX线程),这就是国际标准ISO/IEC 9945-1:1996。1999年出版了IEEE标准1003.1d-1999,其中增加了更多实时接口。一年后,出版了IEEE标准1003.1j-2000和1003.1q-2000,前者包含了更多实时接口,后者增加了标准在事件跟踪方面的扩展。
2001年的1003.1版本与以前各版本有较大的差别,它组合了多个1003.1的修正、1003.2标准以及Single UNIX Specification(SUS)第2版的若干部分(对于SUS,后面将进行更多说明),这形成了IEEE标准1003.1-2001,它包括下列几个标准。
2004年,POSIX.1说明随着技术勘误得到更新,2008年做了更多综合的改动并作为基本说明的第7发行版发布,ISO在2008年底批准了这个版本并在2009年进行发布,即国际标准ISO/IEC9945:2009。该标准基于其他几个标准。
图2-2、图2-3以及图2-4总结了POSIX.1指定的必需的和可选的头文件。因为POSIX.1包含了ISO C标准库函数,所以它还需要图2-1中列出的各个头文件。这4张图中的表也总结了本书所讨论的4种UNIX系统实现所包含的头文件。
头文件 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
说明 |
---|---|---|---|---|---|
|
• |
• |
• |
• |
异步I/O |
|
• |
• |
• |
• |
|
|
• |
• |
• |
• |
目录项(4.22节) |
|
• |
• |
• |
• |
动态链接 |
|
• |
• |
• |
• |
文件控制(3.14节) |
|
• |
• |
• |
• |
文件名匹配类型 |
|
• |
• |
• |
• |
路径名模式匹配与生成 |
|
• |
• |
• |
• |
组文件(6.4节) |
|
• |
• |
• |
• |
代码集变换实用程序 |
|
• |
• |
• |
• |
语言信息常量 |
|
• |
• |
• |
• |
货币类型与函数 |
|
• |
• |
• |
• |
网络数据库操作 |
|
• |
• |
• |
• |
消息类 |
|
• |
• |
• |
• |
投票函数(14.4.2节) |
|
• |
• |
• |
• |
线程(第11章、第12章) |
|
• |
• |
• |
• |
口令文件(6.2节) |
|
• |
• |
• |
• |
正则表达式 |
|
• |
• |
• |
• |
执行调度 |
|
• |
• |
• |
• |
信号量 |
|
• |
• |
• |
• |
字符串操作 |
|
• |
• |
• |
• |
|
|
• |
• |
• |
• |
终端I/O(第18章) |
|
• |
• |
• |
• |
符号常量 |
|
• |
• |
• |
• |
字扩充类型 |
|
• |
• |
• |
• |
因特网定义(第16章) |
|
• |
• |
• |
• |
套接字本地接口(第16章) |
|
• |
• |
• |
• |
因特网地址族(16.3节) |
|
• |
• |
• |
• |
传输控制协议定义 |
|
• |
• |
• |
• |
存储管理声明 |
|
• |
• |
• |
• |
|
|
• |
• |
• |
• |
套接字接口(第16章) |
|
• |
• |
• |
• |
文件状态(第4章) |
|
• |
• |
• |
• |
文件系统信息 |
|
• |
• |
• |
• |
进程时间(8.17节) |
|
• |
• |
• |
• |
基本系统数据类型(2.8节) |
|
• |
• |
• |
• |
UNIX域套接字定义(17.2节) |
|
• |
• |
• |
• |
系统名(6.9节) |
|
• |
• |
• |
• |
进程控制(8.6节) |
图2-2 POSIX标准定义的必需的头文件
本书中描述了POSIX.1 2008年版,其接口分成两部分:必需部分和可选部分。可选接口部分按功能又进一步分成40个功能分区。图2-5按各自的选项码总结了包含未弃用的编程接口。选项码是能够表述标准的2~3个字母的缩写,用以标识属于各个功能分区的接口,其中的接口依赖于特定选项的支持。很多选项处理实时扩展。
头文件 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
说明 |
---|---|---|---|---|---|
|
• |
• |
• |
• |
消息显示结构 |
|
• |
• |
• |
• |
文件树漫游(4.22节) |
|
• |
• |
• |
• |
路径名管理函数 |
|
• |
• |
• |
数据库操作 |
|
|
• |
• |
• |
• |
搜索表 |
|
• |
• |
• |
• |
系统出错日志记录(13.4节) |
|
• |
• |
• |
用户账户数据库 |
|
|
• |
• |
• |
• |
IPC(15.6节) |
|
• |
• |
• |
• |
XSI消息队列(15.7节) |
|
• |
• |
• |
• |
资源操作(7.11节) |
|
• |
• |
• |
• |
XSI信号量(15.8节) |
|
• |
• |
• |
• |
XSI共享存储(15.9节) |
|
• |
• |
• |
• |
时间类型 |
|
• |
• |
• |
• |
矢量I/O操作(14.6节) |
图2-3 POSIX标准定义的XSI可选头文件
头文件 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
说明 |
---|---|---|---|---|---|
|
• |
• |
• |
消息队列 |
|
|
• |
• |
• |
• |
实时spawn接口 |
图2-4 POSIX标准定义的可选头文件
选项码 |
SUS强制的 |
符号常量 |
说明 |
---|---|---|---|
ADV |
|
建议性信息(实时) |
|
CPT |
|
进程CPU时间时钟(实时) |
|
FSC |
• |
|
文件同步 |
IP6 |
|
IPv6接口 |
|
ML |
|
进程存储区加锁(实时) |
|
MLR |
|
存储区域加锁(实时) |
|
MON |
|
单调时钟(实时) |
|
MSG |
|
消息传送(实时) |
|
MX |
|
IEC 60559浮点选项 |
|
PIO |
|
优先输入和输出 |
|
PS |
|
进程调度(实时) |
|
RPI |
|
健壮的互斥量优先权继承(实时) |
|
RPP |
|
健壮的互斥量优先权保护(实时) |
|
RS |
|
原始套接字 |
|
SHM |
|
共享存储对象(实时) |
|
SIO |
|
同步输入和输出(实时) |
|
SPN |
|
产生(实时) |
|
SS |
|
进程阵发性服务器(实时) |
|
TCT |
|
线程CPU时间时钟(实时) |
|
TPI |
|
非键壮的互斥量优先权继承(实时) |
|
TPP |
|
非键壮的互斥量优先权保护(实时) |
|
TPS |
|
线程执行调度(实时) |
|
TSA |
• |
|
线程栈地址属性 |
TSH |
• |
|
线程进程共享同步 |
TSP |
|
线程阵发性服务器(实时) |
|
TSS |
• |
|
线程栈长度属性 |
TYM |
|
类型存储对象(实时) |
|
XSI |
• |
|
X/Open扩充接口 |
图2-5 POSIX.1可选接口组和选项码
POSIX.1没有包括超级用户(superuser)这样的概念,代之以规定某些操作要求“适当的优先权”,POSIX.1将此术语的含义留由具体实现进行解释。某些符合美国国防部安全性指南要求的UNIX系统具有很多不同的安全级。本书仍使用传统的UNIX术语,并指明要求超级用户特权的操作。
经过20多年的工作,相关标准已经成熟稳定。POSIX.1标准现在由Austin Group开放工作组维护。为了保证它们仍然有价值,仍需经常对这些标准进行更新或再确认。
Single UNIX Specification(SUS,单一UNIX规范)是POSIX.1标准的一个超集,它定义了一些附加接口扩展了POSIX.1规范提供的功能。POSIX.1相当于Single UNIX Specification中的基本规范部分。
POSIX.1中的X/Open系统接口(X/Open System Interface,XSI)选项描述了可选的接口,也定义了遵循XSI(XSI conforming)的实现必须支持POSIX.1的哪些可选部分。这些必须支持的部分包括:文件同步、线程栈地址和长度属性、线程进程共享同步以及_XOPEN_UNIX
符号常量(在图2-5中它们被加上“SUS强制的”的标记)。只有遵循XSI的实现才能称为UNIX系统。
Open Group拥有UNIX商标,他们使用Single UNIX Specification定义了一系列接口。一个系统要想称为UNIX系统,其实现必须支持这些接口。UNIX系统供应商必须以文件形式提供符合性声明,并通过验证符合性的测试,才能得到使用UNIX商标的许可证。
有些接口在遵循XSI的系统中是可选的,这些接口根据功能被分成若干选项组(option group),具体如下。
_XOPEN_CRYPE
标记。_XOPEN_REALTIME
标记。_XOPEN _REALTIME_THREADS
标记。Single UNIX Specification是Open Group的出版物,而Open Group是由两个工业社团X/Open和开放系统软件基金会(Open System Software Foundation,OSF)在1996年合并构成的。X/Open过去出版了X/Open Portability Guide(X/Open可移植性指南),它采用了若干特定标准,填补了其他标准缺失功能的空白。这些指南的目的是改善应用的可移植性,使其不仅仅符合已发布的标准。
X/Open在1994年发布了Single UNIX Specification第1版,因为它大约包含了1170个接口,因此也称为“Spec 1170”。它源自通用开放软件环境(Common Open Software Environment,COSE)的倡议,该倡议的目标是进一步改善应用程序在所有UNIX操作系统实现之间的可移植性。COSE的成员包括Sun、IBM、HP、Novell/USL以及OSF等,他们的UNIX都包含了通用商业化应用软件使用的接口,这较之仅仅赞同和支持标准前进了一大步。从这些应用软件的接口中选出的1170个接口被包括在下列标准中:X/Open通用应用环境(Common Application Environment,CAE)第4发行版(也被称为XPG4,以表示它与其前身X/Open Portability Guide的历史关系)、系统V接口定义(System V Interface Definition,SVID)第3版Level 1接口、OSF应用环境规范(Application Environment Specification,AES)Full Use接口。
1997年,Open Group发布了Single UNIX Specification第2版。新版本增加了对线程、实时接口、64位处理、大文件以及增强的多字节字符处理等功能的支持。
Single UNIX Specification第3版(SUSv3)由Open Group在2001年发布。SUSv3的基本规范与IEEE标准1003.1-2001相同,分成4个部分:基本定义、系统接口、shell和实用程序以及基本理论。SUSv3还包括X/Open Curses第4发行版第2版,但该规范并不是POSIX.1的组成部分。
2002年,ISO将IEEE标准1003.1-2001批准为国际标准ISO/IEC 9945:2002。Open Group在2003年再次更新了1003.1标准,包括了技术方面的更正。ISO将其批准为国际标准ISO/IEC 9945:2003。2004年4月,Open Group发布了Single UNIX Specification第3版2004年版,将更多技术上的更正合并到标准的正文中。
2008年,Single UNIX Specification再次更新,包括了更正和新的接口、移除弃用的接口以及将一些未来可能被删除的接口标记为弃用接口等。另外,有一些过去被认为可选的接口变成必选接口,其中包括异步I/O、屏障、时钟选择、存储映像文件、内存保护、读写锁、实时信号、POSIX信号量、旋转锁、线程安全函数、线程、超时机制以及时钟等。最终形成的标准就是基本规范的第7发行版,也即POSIX.1-2008。Open Group把这个版本和X/OPEN Curses规范的更新版打包,并于2010年作为Single UNIX Specification第4版发布。我们把这个规范称为SUSv4。
FIPS代表的是联邦信息处理标准(Federal Information Processing Standard),这一标准是由美国政府发布的,并由美国政府用于计算机系统的采购。FIPS151-1(1989年4月)基于IEEE标准1003.1-1988及ANSI C标准草案。此后是FIPS 151-2(1993年5月),它基于IEEE标准1003.1-1990。在POSIX.1中列为可选的某些功能,在FIPS 151-2中是必需的。所有这些可选功能在POSIX.1-2001中已成为强制性要求。
POSIX.1 FIPS的作用是,它要求任何希望向美国政府销售符合POSIX.1标准的计算机系统的厂商都应支持POSIX.1的某些可选功能。因为POSIX.1 FIPS已经撤回,所以在本书中我们不再进一步考虑它。
上一节说明了3个由各自独立的组织所制定的标准:ISO C、IEEE POSIX以及Single UNIX Specification。但是,标准只是接口的规范。这些标准是如何与现实世界相关联的呢?这些标准由厂商采用,然后转变成具体实现。本书中我们不仅对这些标准感兴趣,还对它们的具体实现感兴趣。
在McKusick等[1996]的1.1节中给出了UNIX系统家族树的详细历史。UNIX的各种版本和变体都起源于在PDP-11系统上运行的UNIX分时系统第6版(1976年)和第7版(1979年)(通常称为V6和V7)。这两个版本是在贝尔实验室以外首先得到广泛应用的UNIX系统。从这棵树上演进出以下3个分支。
(1)AT&T分支,从此引出了系统III和系统V(被称为UNIX的商用版本)。
(2)加州大学伯克利分校分支,从此引出4.xBSD实现。
(3)由AT&T贝尔实验室的计算科学研究中心不断开发的UNIX研究版本,从此引出UNIX分时系统第8版、第9版,终止于1990年的第10版。
SVR4(UNIX System V Release 4)是AT&T的UNIX系统实验室(UNIX System Laboratories,USL,其前身是AT&T的UNIX Software Operation)的产品,它将下列系统的功能合并到了一个一致的操作系统中:AT&T UNIX系统V 3.2版(SVR3.2)、Sun Microsystems公司的SunOS操作系统、加州大学伯克利分校的4.3BSD以及微软的Xenix系统(Xenix是在V7的基础上开发的,后来又采纳了很多系统V的功能)。其源代码于1989年后期发布,在1990年开始向终端用户提供。SVR4符合POSIX 1003.1标准和X/Open XPG3标准。
AT&T也出版了系统V接口定义(SVID)[AT&T 1989]。SVID第3版说明了UNIX系统要达到SVR4质量要求必须提供的功能。如同POSIX.1一样,SVID定义了一个接口,而不是一种实现。SVID并不区分系统调用和库函数。对于一个SVR4的具体实现,应查看其参考手册,以了解系统调用和库函数的不同之处[AT&T 1990e]。
BSD(Berkeley Software Distribution)是由加州大学伯克利分校的计算机系统研究组(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许可证。为了获得BSD系统的源代码,首先需要持有AT&T的UNIX源代码许可证。这种情况正在改变,近几年,越来越多的AT&T源代码被替换成非AT&T源代码,很多添加到BSD系统上的新功能也来自于非AT&T方面。
1989年,伯克利将4.3BSD Tahoe中很多非AT&T源代码包装成BSD网络软件1.0版,并使其成为可公开获得的软件。1991年发布了BSD网络软件2.0版,它是从4.3BSD Reno版派生出来的,其目的是使大部分(如果不是全部的话)4.4BSD系统不再受AT&T许可证的限制,这样,大家都可以得到源代码。
4.4BSD-Lite是CSRG计划开发的最后一个发行版。由于与USL产生的法律纠纷,该版本曾一度延迟推出。在纠纷解决后,4.4BSD-Lite立即于1994年发布,并且不再需要具有UNIX源代码使用许可证就可以使用它。1995年CSRG发布了修复了bug的版本。4.4BSD-Lite第2发行版是CSRG的最后一个BSD版本(McKusick等[1996]描述了该BSD版本)。
在伯克利所进行的UNIX开发工作是从PDP-11开始的,然后转移到VAX小型机上,接着又转移到工作站上。20世纪90年代早期,伯克利得到支持在广泛应用的80386个人计算机上开发BSD版本,结果产生了386BSD。这一工作是由Bill Jolitz完成的,其工作在1991年全年的Dr. Dobb’s期刊上以每月一篇文章连载发表。其中很多代码出现在BSD网络软件2.0版中。
FreeBSD基于4.4BSD-Lite操作系统。在加州大学伯克利分校的CSRG决定终止其在UNIX操作系统的BSD版本的研发工作,而且386BSD项目被忽视很长时间之后,为了继续坚持BSD系列,形成了FreeBSD项目。
由FreeBSD项目产生的所有软件,包括其二进制代码和源代码,都是免费使用的。为了测试书中的实例,本书选取了4个操作系统,FreeBSD 8.0操作系统是其中之一。
有许多基于BSD的免费操作系统。NetBSD项目类似于FreeBSD项目,但是更注重不同硬件平台之间的可移植性。OpenBSD项目也类似于FreeBSD项目,但更注重于安全性。
Linux是一种提供类似于UNIX的丰富编程环境的操作系统,在GNU公用许可证的指导下,Linux是免费使用的。Linux的普及是计算机产业中的一道亮丽风景线。Linux经常是支持较新硬件的第一个操作系统,这一点使其引人注目。
Linux是由Linus Torvalds在1991年为替代MINIX而研发的。一位当时名不见经传人物的努力掀起了澎湃巨浪,吸引了遍布全世界的很多软件开发者,在使用和不断增强Linux方面自愿贡献出了他们大量的时间。
Ubuntu 12.04的Linux分发版本是用以测试本书实例的操作系统之一。该系统使用了Linux操作系统3.2.0版内核。
与其以前的版本相比,Mac OS X使用了完全不同的技术。其核心操作系统称为“Darwin”,它基于Mach内核(Accetta等[1986])、FreeBSD操作系统以及具有面向对象框架的驱动和其他内核扩展的结合。Mac OS X 10.5的Intel部分已经被验证为是一个UNIX系统。
Mac OS X 10.6.8(Darwin 10.8.0)是用以测试本书实例的操作系统之一。
Solaris是由Sun Microsystems(现为Oracle)开发的UNIX系统版本。它基于SVR4,在超过15年的时间里,Sun Microsystems的工程师对其功能不断增强。它是唯一在商业上取得成功的SVR4后裔,并被正式验证为UNIX系统。
2005年,Sun Microsystems把Solaris操作系统的大部分源代码开放给公众,作为OpenSolaris开放源代码操作系统的一部分,试图建立围绕Solaris的外部开发人员社区。
Solaris 10 UNIX操作系统也是用以测试本书实例的操作系统之一。
已经通过验证的其他UNIX版本包括:
前面提到的各个标准定义了任一实际系统的子集。本书主要关注4种实际的UNIX系统:FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10。在这4种系统中,虽然只有Mac OS X 和Solaris 10 能够称自己是一种UNIX系统,但是所有这4种系统都提供UNIX编程环境。因为所有这4种系统都在不同程度上符合POSIX标准,所以我们也将重点关注POSIX.1标准所要求的功能,并指出这4种系统具体实现与POSIX之间的差别。仅仅一个特定实现所具有的功能和例程会被清楚地标记出来。我们还关注那些属于UNIX系统必需的,但却在符合POSIX标准的系统中是可选的功能。
应当看到,这些实现都提供了对它们早期版本(如SVR3.2和4.3BSD)功能的向后兼容性。例如,Solaris对POSIX.1规范中的非阻塞I/O(O_NONBLOCK
)以及传统的系统V中的方法(O_NDELAY
)都提供了支持。本书将只使用POSIX.1的功能,但是也会提及它所替换的是哪一种非标准功能。与此相类似,SVR3.2和4.3BSD以某种方法提供了可靠的信号机制,这种方法也有别于POSIX.1标准。第10章将只说明POSIX.1的信号机制。
UNIX系统实现定义了很多幻数和常量,其中有很多已被硬编码到程序中,或用特定的技术确定。由于大量标准化工作的努力,已有若干种可移植的方法用以确定这些幻数和具体实现定义的限制。这非常有助于改善UNIX环境下软件的可移植性。
以下两种类型的限制是必需的。
(1)编译时限制(例如,短整型的最大值是什么?)
(2)运行时限制(例如,文件名有多少个字符?)
编译时限制可在头文件中定义。程序在编译时可以包含这些头文件。但是,运行时限制则要求进程调用一个函数获得限制值。
另外,某些限制在一个给定的实现中可能是固定的(因此可以静态地在一个头文件中定义),而在另一个实现中则可能是变动的(需要有一个运行时函数调用)。这种类型限制的一个例子是文件名的最大字符数。SVR4之前的系统V由于历史原因只允许文件名最多包含14个字符,而源于BSD的系统则将此增加为255。目前,大多数UNIX系统支持多文件系统类型,而每一种类型有它自己的限制。文件名的最大长度依赖于该文件处于何种文件系统,例如,根文件系统中的文件名长度限制可能是14个字符,而在另一个文件系统中文件名长度限制可能是255个字符,这是运行时限制的一个例子。
为了解决这类问题,提供了以下3种限制。
(1)编译时限制(头文件)。
(2)与文件或目录无关的运行时限制(sysconf
函数)。
(3)与文件或目录有关的运行时限制(pathconf
和fpathconf
函数)。
使事情变得更加复杂的是,如果一个特定的运行时限制在一个给定的系统上并不改变,则可将其静态地定义在一个头文件中,但是,如果没有将其定义在头文件中,应用程序就必须调用3个conf
函数中的一个(我们很快就会对它们进行说明),以确定其运行时的值。
ISO C定义的所有编译时限制都列在头文件<limits.h>
中(见图2-6)。这些限制常量在一个给定系统中并不会改变。表中第3列列出了ISO C标准可接受的最小值。这用于16位整型的系统,用1的补码表示。第4列列出了32位整型Linux系统的值,用2的补码表示。注意,我们没有列出无符号数据类型的最小值,这些值应该都为0。在64位系统中,其long
整型的最大值与表中long long
整型的最大值相匹配。
名称 |
说明 |
可接受的最小值 |
典型值 |
---|---|---|---|
|
|
8 |
8 |
|
|
(见后) |
127 |
|
|
(见后) |
−128 |
|
|
127 |
127 |
|
|
−127 |
−128 |
|
|
255 |
255 |
|
|
32 767 |
2 147 483 647 |
|
|
−32 767 |
−2 147 483 648 |
|
|
65 535 |
4 294 967 295 |
|
|
32 767 |
32 767 |
|
|
−32 767 |
−32 768 |
|
|
65 535 |
65 535 |
|
|
2 147 483 647 |
2 147 483 647 |
|
|
−2 147 483 647 |
−2 147 483 648 |
|
|
4 294 967 295 |
4 294 967 295 |
|
|
9 223 372 036 854 775 807 |
9 223 372 036 854 775 807 |
|
|
−9 223 372 036 854 775 807 |
−9 223 372 036 854 775 808 |
|
|
18 446 744 073 709 551 615 |
18 446 744 073 709 551 615 |
|
在一个多字节字符常量中的最大字节数 |
1 |
6 |
图2-6 <limits.h>
中定义的整型值大小
我们将会遇到的一个区别是系统是否提供带符号或无符号的字符值。从图2-6中的第4列可以看出,该特定系统使用带符号字符。从图中可以看到CHAR_MIN
等于SCHAR_MIN
,CHAR_MAX
等于SCHAR_MAX
。如果系统使用无符号字符,则CHAR_MIN
等于0,CHAR_MAX
等于UCHAR_MAX
。
在头文件<float.h>
中,对浮点数据类型也有类似的一组定义。如若读者在工作中涉及大量浮点数据类型,则应仔细查看该文件。
虽然ISO C标准规定了整型数据类型可接受的最小值,但POSIX.1对C标准进行了扩充。为了符合POSIX.1标准,具体实现必须支持INT_MAX
的最小值为2 147 483 647,INT_MIN
为-2 147 483 647,UINT_MAX
为4 294 967 295。因为POSIX.1要求具体实现支持8位的char
类型,所以CHAR_BIT
必须是8,SCHAR_MIN
必须是−128,SCHAR_MAX
必须是127,UCHAR_MAX
必须是255。
我们会遇到的另一个ISO C常量是FOPEN_MAX
,这是具体实现保证可同时打开的标准I/O流的最小个数,该值在头文件<stdio.h>
中定义,其最小值是8。POSIX.1中的STREAM_MAX
(若定义的话)则应与FOPEN_MAX
具有相同的值。
ISO C还在<stdio.h>
中定义了常量TMP_MAX
,这是由tmpnam
函数产生的唯一文件名的最大个数。关于此常量我们将在5.13节中进行更多说明。
虽然ISO C定义了常量FILENAME_MAX
,但我们应避免使用该常量,因为POSIX.1提供了更好的替代常量(NAME_MAX
和PATH_MAX
),我们很快就会介绍该常量。
在图2-7中,我们列出了本书所讨论4种平台上的FILENAME_MAX
、FOPEN_MAX
和TMP_MAX
值。
限制 |
FreeBSD 8.0 |
Linux 3.2.0 |
Mac OS X 10.6.8 |
Solaris 10 |
---|---|---|---|---|
|
20 |
16 |
20 |
20 |
|
308 915 776 |
238 328 |
308 915 776 |
17 576 |
|
1 024 |
4 096 |
1 024 |
1 024 |
图2-7 在各种平台上ISO的限制
POSIX.1定义了很多涉及操作系统实现限制的常量,遗憾的是,这是POSIX.1中最令人迷惑不解的部分之一。虽然POSIX.1定义了大量限制和常量,我们只关心与基本POSIX.1接口有关的部分。这些限制和常量分成下列7类。
(1)数值限制:LONG_BIT
、SSIZE_MAX
和WORD_BIT
。
(2)最小值:图2-8中的25个常量。
(3)最大值:_POSIX_CLOCKRES_MIN
。
(4)运行时可以增加的值:CHARCLASS_NAME_MAX
、COLL_WEIGHTS_MAX
、LINE_MAX
、NGROUPS_MAX
和RE_DUP_MAX
。
(5)运行时不变值(可能不确定):图2-9中的17个常量(加上12.2节中介绍的4个常量和14.5节中介绍的3个常量)。
(6)其他不变值:NL_ARGMAX
、NL_MSGMAX
、NL_SETMAX
和NL_TEXTMAX
。
(7)路径名可变值:FILESIZEBITS
、LINK_MAX
、MAX_CANON
、MAX_INPUT
、NAME_MAX
、PATH_MAX
、PIPE_BUF
和SYMLINK_MAX
。
在这些限制和常量中,某些可能定义在<limits.h>
中,其余的则按具体条件可定义、可不定义。在2.5.4节中说明sysconf
、pathconf
和fpathconf
函数时,我们将描述可定义或可不定义的限制和常量。在图2-8中,我们列出了25个最小值。
这些最小值是不变的——它们并不随系统而改变。它们指定了这些特征最具约束性的值。一个符合POSIX.1的实现应当提供至少这样大的值。这就是为什么将它们称为最小值,虽然它们的名字都包含了MAX。另外,为了保证可移植性,一个严格符合POSIX标准的应用程序不应要求更大的值。我们将在本书的适当章节说明每一个常量的含义。
一个严格符合(strictly conforming)POSIX的应用区别于一个刚刚符合POSIX(merely POSIX confirming)的应用。符合POSIX的应用只使用在IEEE 1003.1-2001中定义的接口。严格符合POSIX的应用满足更多的限制,例如,不依赖于POSIX未定义的行为、不使用其任何已弃用的接口以及不要求所使用的常量值大于图2-8中所列出的最小值。
名称 |
说明:最小可接受值 |
值 |
---|---|---|
|
|
4 096 |
|
每个实际用户ID的子进程数 |
25 |
|
定时器最大超限运行次数 |
32 |
|
|
255 |
|
至一个文件的链接数 |
8 |
|
登录名的长度 |
9 |
|
终端规范输入队列的字节数 |
255 |
|
终端输入队列的可用空间 |
255 |
|
文件名中的字节数,不包括终止null字节 |
14 |
|
每个进程同时添加的组ID数 |
8 |
|
每个进程的打开文件数 |
20 |
|
路径名中的字节数,包括终止null字节 |
256 |
|
能原子地写到一个管道中的字节数 |
512 |
|
当使用间隔表示法\ |
255 |
|
为应用预留的实时信号编号个数 |
8 |
|
一个进程可以同时使用的信号量个数 |
256 |
|
信号量可持有的值 |
32 767 |
|
一个进程可发送和挂起的排队信号的个数 |
32 |
|
能存在 |
32 767 |
|
一个进程能同时打开的标准I/O流数 |
8 |
|
符号链接中的字节数 |
255 |
|
在解析路径名时,可遍历的符号链接数 |
8 |
|
每个进程的定时器数目 |
32 |
|
终端设备名长度,包括终止null字节 |
9 |
|
时区名字节数 |
6 |
图2-8 <limits.h>
中的POSIX.1最小值
遗憾的是,这些不变最小值中的某一些在实际应用中太小了。例如,目前在大多数UNIX系统中,每个进程可同时打开的文件数远远超过20。另外,_POSIX_PATH_MAX
的最小限制值为255,这太小了,路径名可能会超过这一限制。这意味着在编译时不能使用_POSIX_OPEN_MAX
和_POSIX_PATH_MAX
这两个常量作为数组长度。
图2-8中的25个不变最小值的每一个都有一个相关的实现值,其名字是将图2-8中的名字删除前缀_POSIX_
后构成的。没有_POSIX_
前缀的名字用于给定具体实现支持的该不变最小值的实际值。这25个实现值是本节开始部分所列出的1、4、5、7类:2个是运行时可以增加的值、15个是运行时不变值(见图2-9)、7个是路径名可变值,以及数值SSIZE_MAX
。问题是并不能确保所有这25个实现值都在<limit.h>
头文件中定义。
例如,某个特定值可能不在此头文件中定义,其理由是:一个给定进程的实际值可能依赖于系统的存储总量。如果没有在头文件中定义它们,则不能在编译时使用它们作为数组边界。所以,POSIX.1提供了3个运行时函数以供调用,它们是sysconf
、pathconf
和fpathconf
。使用这3个函数可以在运行时得到实际的实现值。但是,还有一个问题,其中某些值由POSIX.1定义为“可能不确定的”(逻辑上无限的),这就意味着该值没有实际上限。例如,在Solaris中,进程结束时注册可运行atexit
的函数个数仅受系统存储总量的限制。所以在Solaris中,ATEXIT_MAX
被认为是不确定的。2.5.5节还将讨论运行时限制不确定的问题。
名称 |
说明 |
最小可接受值 |
---|---|---|
|
|
|
|
可用 |
32 |
|
每个实际用户ID的子进程最大个数 |
|
|
定时器最大超限运行次数 |
|
|
|
|
|
登录名最大长度 |
|
|
赋予新建文件描述符的最大值+1 |
|
|
系统内存页大小(以字节为单位) |
1 |
|
为应用程序预留的实时信号的最大个数 |
|
|
一个进程可使用的信号量最大个数 |
|
|
信号量的最大值 |
|
|
一个进程可排队信号的最大个数 |
|
|
一个进程一次可打开的标准I/O流的最大个数 |
|
|
路径解析过程中可访问的符号链接数 |
|
|
一个进程的定时器最大个数 |
|
|
终端设备名长度,其中包括终止的null字节 |
|
|
时区名的字节数 |
|
图2-9 <limits.h>
中的POSIX.1运行时不变值
XSI定义了代表实现限制的几个常量。
(1)最小值:图2-10中列出的5个常量。
(2)运行时不变值(可能不确定):IOV_MAX
和PAGE_SIZE
。
图2-10列出了最小值。最后两个常量值说明了POSIX.1最小值太小的情况,根据推测这可能是考虑到了嵌入式POSIX.1实现。为此,Single UNIX Specification为符合XSI的系统增加了具有较大最小值的符号。
名称 |
说明 |
最小可接受值 |
典型值 |
---|---|---|---|
|
在 |
14 |
14 |
|
默认进程优先级 |
20 |
20 |
|
|
16 |
16 |
|
文件名中的字节数 |
255 |
255 |
|
路径名中的字节数 |
1 024 |
1 024 |
图2-10 <limits.h>
中的XSI最小值
sysconf
、pathconf
和fpathconf
我们已列出了实现必须支持的各种最小值,但是怎样才能找到一个特定系统实际支持的限制值呢?正如前面提到的,某些限制值在编译时是可用的,而另外一些则必须在运行时确定。我们也曾提及某些限制值在一个给定的系统中可能是不会更改的,而其他限制值可能会更改,因为它们与文件和目录相关联。运行时限制可调用下面3个函数之一获得。
#include <unistd.h>
long sysconf(int name);
long pathconf(const char *pathname, int name);
log fpathconf(int fd, int name);
所有函数返回值:若成功,返回相应值;若出错,返回-1(见后)
后面两个函数的差别是:一个用路径名作为其参数,另一个则取文件描述符作为参数。
图2-11中列出了sysconf
函数所使用的name参数,它用于标识系统限制。以_SC_
开始的常量用作标识运行时限制的sysconf
参数。图2-12列出了pathconf
和fpathconf
函数为标识系统限制所使用的name参数。以_PC_
开始的常量用作标识运行时限制的pathconf
或fpathconf
参数。
我们需要更详细地讨论一下这3个函数不同的返回值。
(1)如果name参数并不是一个合适的常量,这3个函数都返回−1,并把errno
置为EINVAL
。图2-11和图2-12的第3列给出了我们在整本书中将要涉及的限制常量。
(2)有些name会返回一个变量值(返回值0)或者提示该值是不确定的。不确定的值通过返回−1来体现,而不改变
errno
的值。
限制名 |
说明 |
name参数 |
---|---|---|
|
|
|
|
可用 |
|
|
每个实际用户ID的最大进程数 |
|
时钟滴答 |
每秒时钟滴答数 |
|
|
在本地定义文件中可以赋予 |
|
|
定时器最大超限运行次数 |
|
|
|
|
|
|
|
|
实用程序输入行的最大长度 |
|
|
登录名的最大长度 |
|
|
每个进程同时添加的最大进程组ID数 |
|
|
每个进程最大打开文件数 |
|
|
系统存储页长度(字节数) |
|
|
系统存储页长度(字节数) |
|
|
当使用间隔表示法\ |
|
|
为应用程序预留的实时信号的最大个数 |
|
|
一个进程可使用的信号量最大个数 |
|
|
信号量的最大值 |
|
|
一个进程可排队信号的最大个数 |
|
STREAM_MAX |
一个SC STREAM_MAX进程在任意给定时刻标准I/O流的最大个数。如果定义,必须与FOPEN_MAX有相同值 |
_SC_STREAM_MAX |
|
在解析路径名时,可遍历的符号链接数 |
|
|
每个进程的最大定时器个数 |
|
|
终端设备名长度,包括终止null字节 |
|
|
时区名中的最大字节数 |
|
图2-11 对sysconf
的限制及name参数
限制名 |
说明 |
name参数 |
---|---|---|
|
以带符号整型值表示在指定目录中允许的普通文件最大长度所需的最小位(bit)数 |
|
|
文件链接计数的最大值 |
|
|
终端规范输入队列的最大字节数 |
|
|
终端输入队列可用空间的字节数 |
|
|
文件名的最大字节数(不包括终止null字节) |
|
|
相对路径名的最大字节数,包括终止null字节 |
|
|
能原子地写到管道的最大字节数 |
|
|
文件时间戳的纳秒精度 |
|
|
符号链接的字节数 |
|
图2-12 对pathconf
和fpathconf
的限制及name参数
(3)_SC_CLK_TCK
的返回值是每秒的时钟滴答数,用于times
函数的返回值(8.17节)。
对于pathconf
的参数pathname和fpathconf
的参数fd有很多限制。如果不满足其中任何一个限制,则结果是未定义的。
(1)_PC_MAX_CANON
和_PC_MAX_INPUT
引用的文件必须是终端文件。
(2)_PC_LINK_MAX
和_PC_TIMESTAMP_RESOLUTION
引用的文件可以是文件或目录。如果是目录,则返回值用于目录本身,而不用于目录内的文件名项。
(3)_PC_FILESIZEBITS
和_PC_NAME_MAX
引用的文件必须是目录,返回值用于该目录中的文件名。
(4)_PC_PATH_MAX
引用的文件必须是目录。当所指定的目录是工作目录时,返回值是相对路径名的最大长度(遗憾的是,这不是我们想要知道的一个绝对路径名的实际最大长度,我们将在2.5.5节中再次回到这一问题上来)。
(5)_PC_PIPE_BUF
引用的文件必须是管道、FIFO或目录。在管道或FIFO情况下,返回值是对所引用的管道或FIFO的限制值。对于目录,返回值是对在该目录中创建的任一FIFO的限制值。
(6)_PC_SYMLINK_MAX
引用的文件必须是目录。返回值是该目录中符号链接可包含字符串的最大长度。
实例
图2-13中所示的awk
(1)程序构建了一个C程序,它打印各pathconf
和sysconf
符号的值。
#!/usr/bin/awk -f
BEGIN {
printf("#include \"apue.h\"\n")
printf("#include <errno.h>\n")
printf("#include <limits.h>\n")
printf("\n")
printf("static void pr_sysconf(char *, int);\n")
printf("static void pr_pathconf(char *, char *, int);\n")
printf("\n")
printf("int\n")
printf("main(int argc, char *argv[])\n")
printf("{\n"}
printf("\tif (argc != 2)\n")
printf("\t\terr_quit(\"usage: a.out <dirname>\");\n\n")
FS="\t+"
while (getline <"sysconf.sym" > 0) {
printf("#ifdef %s\n", $1)
printf("\tprintf(\"%s defined to be %%ld\\n\", (long)%s+0);\n", $1, $1)
printf("#else\n")
printf("\tprintf(\"no symbol for %s\\n\");\n", $1)
printf("#endif\n")
printf("#ifdef %s\n", $2)
printf("\tpr_sysconf(\"%s =\", %s);\n", $1, $2)
printf("#else\n")
printf("\tprintf(\"no symbol for %s\\n\");\n", $2)
printf("#endif\n")
}
close("sysconf.sym")
while (getline <"pathconf.sym" > 0) {
printf("#ifdef %s\n", $1)
printf("\tprintf(\"%s defined to be %%ld\\n\", (long)%s+0);\n", $1, $1)
printf("#else\n")
printf("\tprintf(\"no symbol for %s\\n\");\n", $1)
printf("#endif\n")
printf("#ifdef %s\n", $2)
printf("\tpr_pathconf(\"%s =\", argv[1], %s);\n", $1, $2)
printf("#else\n")
printf("\tprintf(\"no symbol for %s\\n\");\n", $2)
printf("#endif\n")
}
close("pathconf.sym")
exit
}
END {
printf("\texit(0);\n")
printf("}\n\n")
printf("static void\n")
printf("pr_sysconf(char *mesg, int name)\n")
printf("{\n"}
printf("\tlong val;\n\n")
printf("\tfputs(mesg, stdout);\n")
printf("\terrno = 0;\n")
printf("\tif ((val = sysconf(name)) < 0) {\n"}
printf("\t\tif (errno != 0) {\n"}
printf("\t\t\tif (errno == EINVAL)\n")
printf("\t\t\t\tfputs(\" (not supported)\\n\", stdout);\n")
printf("\t\t\telse\n")
printf("\t\t\t\terr_sys(\"sysconf error\");\n")
printf("\t\t) else {\n"}
printf("\t\t\tfputs(\" (no limit)\\n\", stdout);\n")
printf("\t\t)\n")
printf("\t} else {\n"}
printf("\t\tprintf(\" %%ld\\n\", val);\n")
printf("\t)\n")
printf("}\n\n")
printf("static void\n")
printf("pr_pathconf(char *mesg, char *path, int name)\n")
printf("{\n"}
printf("\tlong val;\n")
printf("\n")
printf("\tfputs(mesg, stdout);\n")
printf("\terrno = 0;\n")
printf("\tif ((val = pathconf(path, name)) < 0) {\n"}
printf("\t\tif (errno != 0) {\n"}
printf("\t\t\tif (errno == EINVAL)\n")
printf("\t\t\t\tfputs(\" (not supported)\\n\", stdout);\n")
printf("\t\t\telse\n")
printf("\t\t\t\terr_sys(\"pathconf error, path = %%s\", path);\n")
printf("\t\t) else {\n"}
printf("\t\t\tfputs(\" (no limit)\\n\", stdout);\n")
printf("\t\t)\n")
printf("\t) else {\n"}
printf("\t\tprintf(\" %%ld\\n\", val);\n")
printf("\t}\n")
printf("}\n")
}
图2-13 构建C程序以打印所有得到支持的系统配置限制
该awk
程序读两个输入文件——pathconf.sym
和sysconfig.sym
,这两个文件中包含了用制表符分隔的限制名和符号列表。并非每种平台都定义所有符号,所以围绕每个pathconf
和sysconf
调用,awk
程序都使用了必要的#ifdef
语句。
例如,awk
程序将输入文件中类似于下列形式的行:
NAME_MAX _PC_NAME_MAX
转换成下列C代码:
#ifdef NAME_MAX
printf("NAME_MAX is defined to be %d\n", NAME_MAX+0);
#else
printf("no symbol for NAME_MAX\n");
#endif
#ifdef _PC_NAME_MAX
pr_pathconf("NAME_MAX =", argv[1], _PC_NAME_MAX);
#else
printf("no symbol for _PC_NAME_MAX\n");
#endif
由awk
产生的C程序如图2-14所示,它会打印所有这些限制,并处理未定义限制的情况。
#include "apue.h"
#include <errno.h>
#include <limits.h>
static void pr_sysconf(char *, int);
static void pr_pathconf(char *, char *, int);
int
main(int argc, char *argv[])
{
if (argc != 2)
err_quit("usage: a.out <dirname>");
#ifdef ARG_MAX
printf("ARG_MAX defined to be %ld\n", (long)ARG_MAX+0);
#else
printf("no symbol for ARG_MAX\n");
#endif
#ifdef _SC_ARG_MAX
pr_sysconf("ARG_MAX =", _SC_ARG_MAX);
#else
printf("no symbol for _SC_ARG_MAX\n");
#endif
/* similar processing for all the rest of the sysconf symbols... */
#ifdef MAX_CANON
printf("MAX_CANON defined to be %ld\n", (long)MAX_CANON+0);
#else
printf("no symbol for MAX_CANON\n");
#endif
#ifdef _PC_MAX_CANON
pr_pathconf("MAX_CANON =", argv[1], _PC_MAX_CANON);
#else
printf("no symbol for _PC_MAX_CANON\n");
#endif
/* similar processing for all the rest of the pathconf symbols... */
exit(0);
}
static void
pr_sysconf(char *mesg, int name)
{
long val;
fputs(mesg, stdout);
errno = 0;
if ((val = sysconf(name)) < 0) {
if (errno != 0) {
if (errno == EINVAL)
fputs(" (not supported)\n", stdout);
else
err_sys("sysconf error");
} else {
fputs(" (no limit)\n", stdout);
}
} else {
printf(" %ld\n", val);
}
}
static void
pr_pathconf(char *mesg, char *path, int name)
{
long val;
fputs(mesg, stdout);
errno = 0;
if ((val = pathconf(path, name)) < 0) {
if (errno != 0) {
if (errno == EINVAL)
fputs(" (not supported)\n", stdout);
else
err_sys("pathconf error, path = %s", path);
} else {
fputs(" (no limit)\n", stdout);
}
} else {
printf(" %ld\n", val);
}
}
图2-14 打印所有可能的sysconf
和pathconf
值
图2-15总结了在本书讨论的4种系统上图2-14所示程序的输出结果。“无符号”项表示该系统没有提供相应_SC
或_PC
符号以查询相关常量值。因此,该限制是未定义的。与此对比,“不支持”项表示该符号由系统定义,但是未被sysconf
和pathcon
函数识别。“无限制”项表示该系统将相关常量定义为无限制,但并不表示该限制值可以是无限的,它只表示该限制值不确定。
限制 | FreeBSD 8.0 | Linux 3.2.0 | Mac OS X 10.6.8 | Solaris 10 | |
---|---|---|---|---|---|
UFS 文件系统 |
PCFS 文件系统 |
||||
ARG_MAX |
262 144 | 2 097 152 | 262 144 | 2 096 640 | 2 096 640 |
ATEXIT_MAX |
32 | 2 147 483 647 | 2 147 483 647 | 无限制 | 无限制 |
CHARCLASS_NAME_MAX |
无符号 | 2 048 | 14 | 14 | 14 |
CHILD_MAX |
1 760 | 47 211 | 266 | 8 021 | 8 021 |
时钟滴答/秒 | 128 | 100 | 100 | 100 | 100 |
COLL_WEIGHTS_MAX |
0 | 255 | 2 | 10 | 10 |
FILESIZEBITS |
64 | 64 | 64 | 41 | 不支持 |
HOST_NAME_MAX |
255 | 64 | 255 | 255 | 255 |
IOV_MAX |
1 024 | 1 024 | 1 024 | 16 | 16 |
LINE_MAX |
2 048 | 2 048 | 2 048 | 2 048 | 2 048 |
LINK_MAX |
32 767 | 65 000 | 32 767 | 32 767 | 1 |
LOGIN_NAME_MAX |
17 | 256 | 255 | 9 | 9 |
MAX_CANON |
255 | 255 | 1 024 | 256 | 256 |
MAX_INPUT |
255 | 255 | 1 024 | 512 | 512 |
NAME_MAX |
255 | 255 | 255 | 255 | 8 |
NGROUPS_MAX |
1 023 | 65 536 | 16 | 16 | 16 |
OPEN_MAX |
3 520 | 1 024 | 256 | 256 | 256 |
PAGESIZE |
4 096 | 4 096 | 4 096 | 8 192 | 8 192 |
PAGE_SIZE |
4 096 | 4 096 | 4 096 | 8 192 | 8 192 |
PATH_MAX |
1 024 | 4 096 | 1 024 | 1 024 | 1 024 |
PIPE_BUF |
512 | 4 096 | 512 | 5 120 | 5 120 |
RE_DUP_MAX |
255 | 32 767 | 255 | 255 | 255 |
STREAM_MAX |
3 520 | 16 | 20 | 256 | 256 |
SYMLINK_MAX |
1 024 | 无限制 | 255 | 1 024 | 1 024 |
SYMLOOP_MAX |
32 | 无限制 | 32 | 20 | 20 |
TTY_NAME_MAX |
255 | 32 | 255 | 128 | 128 |
TZNAME_MAX |
255 | 6 | 255 | 无限制 | 无限制 |
图2-15 配置限制的实例
注意,有些限制报告地并不正确。例如,在Linux中,
SYMLOOP_MAX
被报告成无限制,但是检查源代码后就会发现,实际上它在硬编码中有限制值,这一限制将循环缺失的情况下遍历连续符号链接的数目限制为40(参阅fs/namei.c
中的follow_link
函数)。Linux中另一个潜在的不精确的来源是
pathconf
和fpathconf
函数都是在C库函数中实现的,这些函数返回的配置限制依赖于底层的文件系统类型,因此如果你的文件系统不被C库熟知的话,函数返回的是一个猜测值。
我们将在4.14节中看到,UFS
是Berkeley快速文件系统的SVR4实现,PCFS
是Solaris的MS-DOS FAT文件系统的实现。
前面已提及某些限制值可能是不确定的。我们遇到的问题是,如果这些限制值没有在头文件<limits.h>
中定义,那么在编译时也就不能使用它们。但是,如果它们的值是不确定的,那么在运行时它们可能也是未定义的。让我们来观察两个特殊的情况,为一个路径名分配存储区,以及确定文件描述符的数目。
很多程序需要为路径名分配存储区,一般来说,在编译时就为其分配了存储区,而且不同的程序使用各种不同的幻数(其中很少是正确的)作为数组长度,如256、512、1 024或标准I/O常量BUFSIZ
。4.3BSD头文件<sys/param.h>
中的常量MAXPATHLEN
才是正确的值,但是很多4.3BSD应用程序并未使用它。
POSIX.1试图用PATH_MAX
值来帮助我们,但是如果此值是不确定的,那么仍是毫无帮助的。图2-16程序是本书用来为路径名动态分配存储区的函数。
#include "apue.h"
#include <errno.h>
#include <limits.h>
#ifdef PATH_MAX
static long pathmax = PATH_MAX;
#else
static long pathmax = 0;
#endif
static long posix_version = 0;
static long xsi_version = 0;
/* If PATH_MAX is indeterminate, no guarantee this is adequate */
#define PATH_MAX_GUESS 1024
char *
path_alloc(size_t *sizep) /* also return allocated size, if nonnull */
{
char *ptr;
size_t size;
if (posix_version == 0)
posix_version = sysconf(_SC_VERSION);
if (xsi_version == 0)
xsi_version = sysconf(_SC_XOPEN_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 */
}
}
/*
* Before POSIX.1-2001, we aren't guaranteed that PATH_MAX includes
* the terminating null byte. Same goes for XPG3.
*/
if ((posix_version < 200112L) && (xsi_version < 4))
size = pathmax + 1;
else
size = pathmax;
if ((ptr = malloc(size)) == NULL)
err_sys("malloc error for pathname");
if (sizep != NULL)
*sizep = size;
return(ptr);
}
图2-16 为路径名动态地分配空间
如果<limits.h>
中定义了常量PATH_MAX
,那么就没有任何问题;如果未定义,则需调用pathconf
。因为pathconf
的返回值是基于工作目录的相对路径名的最大长度,而工作目录是其第一个参数,所以,指定根目录为第一个参数,并将得到的返回值加1作为结果值。如果pathconf
指明PATH_MAX
是不确定的,那么我们就只能猜测某个值。
对于PATH_MAX
是否考虑到在路径名末尾有一个null字节这一点,2001年以前的POSIX.1版本表述得并不清楚。出于安全方面的考虑,如果操作系统的实现符合某个先前版本的标准,但并不符合Single UNIX Specification的任何版本(SUS明确要求在结尾处加一个终止null字节),则需要在为路径名分配的存储量上加1。
处理不确定结果情况的正确方法与如何使用分配的存储空间有关。例如,如果我们为getcwd
调用分配存储空间(返回当前工作目录的绝对路径名,见4.23节),但分配到的空间太小,则会返回一个错误,并将errno
设置为ERANGE
。然后可调用realloc
来增加分配的空间(见7.8节和习题4.16)并重试。不断重复此操作,直到getcwd
调用成功执行。
守护进程(daemon process,在后台运行且不与终端相连接的一种进程)中一个常见的代码序列是关闭所有打开文件。某些程序中有下列形式的代码序列,这段程序假定在<sys/param.h>
头文件中定义了常量NOFILE
。
#include <sys/param.h>;
for (i = 0; i < NOFILE; i++)
close(i);
另外一些程序则使用某些<stdio.h>
版本提供的作为上限的常量_NFILE
。某些程序则直接将其上限值硬编码为20。但是,这些方法都不是可移植的。
我们希望用POSIX.1的OPEN_MAX
确定此值以提高可移植性,但是如果此值是不确定的,则仍然有问题,如果我们编写下列代码:
#include <unistd.h>
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
close(i);
如果OPEN_MAX
是不确定的,那么for
循环根本不会执行,因为sysconf
将返回-1。在这种情况下,最好的选择就是关闭所有描述符直至某个限制值(如256)。如同上面的路径名实例一样,虽然并不能保证在所有情况下都能正确工作,但这却是我们所能选择的最好方法。图2-17的程序中使用了这种技术。
我们可以耐心地调用close
,直至得到一个出错返回,但是从close
(EBADF
)出错返回并不区分无效描述符和没有打开的描述符。如果使用此技术,而且描述符9未打开,描述符10打开了,那么将停止在9上,而不会关闭10。dup
函数(见3.12节)在超过了OPEN_MAX
时确实会返回一个特定的出错值,但是用复制一个描述符两三百次的方法来确定此值是一种非常极端的方法。
#include "apue.h"
#include <errno.h>
#include <limits.h>
#ifdef OPEN_MAX
static long openmax = OPEN_MAX;
#else
static long openmax = 0;
#endif
/*
* If OPEN_MAX is indeterminate, this might be inadequate.
*/
#define OPEN_MAX_GUESS 256
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);
}
图2-17 确定文件描述符个数
某些实现返回LONG_MAX
作为限制值,但这与不限制其值在效果上是相同的。Linux对ATEXIT_MAX
所取的限制值就属于此种情况(见图2-15),这将使程序的运行行为变得非常糟糕,因此并不是一个好方法。
例如,我们可以使用Bourne-again shell的内建命令ulimit
来更改进程可同时打开文件的最多个数。如果要将此限制值设置为在效果上是无限制的,那么通常要求具有特权(超级用户)。但是,一旦将其值设置为无穷大,sysconf
就会将LONG_MAX
作为OPEN_MAX
的限制值报告。程序若将此值作为要关闭的文件描述符数的上限(如图2-17所示),那么为了试图关闭2 147 483 647个文件描述符,就会浪费大量时间,实际上其中绝大多数文件描述符并未得到使用。
支持Single UNIX Specification中XSI扩展的系统提供了getrlimit
(2)函数(见7.11节)。它返回一个进程可以同时打开的描述符的最多个数。使用该函数,我们能够检测出对于进程能够打开的文件数实际上并没有设置上限,于是也就避开了这个问题。
OPEN_MAX
被POSIX称为运行时不变值,这意味着在一个进程的生命周期中其值不应发生变化。但是在支持XSI扩展的系统上,可以调用setrlimit
(2)函数(见7.11节)更改一个运行进程的OPEN_MAX
值(也可用C shell的limit
或Bourne shell、Bourne-again shell、Debian Almquist和Korn shell的ulimit
命令更改这个值)。如果系统支持这种功能,则可以更改图2-17中的函数,使得每次调用此函数时都会调用sysconf
,而不只是在第一次调用此函数时调用sysconf
。
图2-5列出了POSIX.1的选项,并且2.2.3节讨论了XSI的选项组。如果我们要编写可移植的应用程序,而这些程序可能会依赖于这些可选的支持的功能,那么就需要一种可移植的方法来判断实现是否支持一个给定的选项。
如同对限制的处理(见2.5节)一样,POSIX.1定义了3种处理选项的方法。
(1)编译时选项定义在<unistd.h>
中。
(2)与文件或目录无关的运行时选项用sysconf
函数来判断。
(3)与文件或目录有关的运行时选项通过调用pathconf
或fpathconf
函数来判断。
选项包括了图2-5中第3列的符号以及图2-19和图2-18中的符号。如果符号常量未定义,则必须使用sysconf
、pathconf
或fpathconf
来判断是否支持该选项。在这种情况下,这些函数的name参数前缀_POSIX
必须替换为_SC
或_PC
。对于以_XOPEN
为前缀的常量,在构成name参数时必须在其前放置_SC
或_PC
。例如,若常量_POSIX_RAW_THREADS
是未定义的,那么就可以将name参数设置为SC_RAW_THREADS
,并以此调用sysconf
来判断该平台是否支持POSIX线程选项。如若常量_XOPEN_UNIX
是未定义的,那么就可以将name参数设置为_SC_XOPEN_UNIX
,并以此调用sysconf
来判断该平台是否支持XSI扩展。
对于每一个选项,有以下3种可能的平台支持状态。
(1)如果符号常量没有定义或者定义值为−1,那么该平台在编译时并不支持相应选项。但是有一种可能,即在已支持该选项的新系统上运行老的应用时,即使该选项在应用编译时未被支持,但如今新系统运行时检查会显示该选项已被支持。
(2)如果符号常量的定义值大于0,那么该平台支持相应选项。
(3)如果符号常量的定义值为0,则必须调用sysconf
、pathconf
或fpathconf
来判断相应选项是否受到支持。
图2-18总结了pathconf
和fpathconf
使用的符号常量。除了图2-5中列出的选项之外,图2-19总结了其他一些sysconf
使用的未弃用的选项及它们的符号常量。注意,我们省略了与实用命令相关的选项。
选项名 |
说明 |
name参数 |
---|---|---|
|
使用 |
|
|
路径名长于 |
|
|
若定义,可用此值禁用终端特殊字符 |
|
|
对相关联的文件是否可以使用异步I/O |
|
|
对相关联的文件是否可以使用优先的I/O |
|
|
对相关联的文件是否可以使用同步I/O |
|
|
目录中是否支持符号链接 |
|
图2-18 pathconf
和fpathconf
的选项及name参数
选项名 |
说明 |
name参数 |
---|---|---|
|
此实现是否支持POSIX异步I/O |
|
|
此实现是否支持屏障 |
|
|
此实现是否支持时钟选择 |
|
|
此实现是否支持作业控制 |
|
|
此实现是否支持存储映像文件 |
|
|
此实现是否支持存储保护 |
|
|
此实现是否支持读者-写者锁 |
|
|
此实现是否支持实时信号 |
|
|
此实现是否支持保存的设置用户ID和保存的设置组ID |
|
|
此实现是否支持POSIX信号量 |
|
|
此实现是否支持POSIX shell |
|
|
此实现是否支持旋转锁 |
|
|
此实现是否支持线程安全函数 |
|
|
此实现是否支持线程 |
|
|
此实现是否支持基于超时的变量选择函数 |
|
|
此实现是否支持定时器 |
|
|
POSIX.1版本 |
|
|
此实现是否支持XSI加密可选组 |
|
|
此实现是否支持XSI实时选项组 |
|
|
此实现是否支持实时线程选项组 |
|
|
此实现是否支持XSI共享存储选项组 |
|
|
XSI版本 |
|
图2-19 sysconf
的选项及name参数
如同系统限制一样,关于sysconf
、pathconf
和fpathconf
如何处理选项,有如下几点值得注意。
(1)_SC_VERSION
的返回值表示标准发布的年(以4位数表示)、月(以2位数表示)。该值可能是198808L、199009L、199506L或表示该标准后续版本的其他值。与SUSv3(POSIX.1 2001年版)相关联的值是200112L,与SUSv4(POSIX.1 2008年版)相关联的值是200809L。
(2)_SC_XOPEN_VERSION
的返回值表示系统支持的XSI版本。与SUSv3相关联的值是600,与SUSv4相关的值是700。
(3)_SC_JOB_CONTROL
、_SC_SAVED_IDS
以及_PC_VDISABLE
的值不再表示可选功能。虽然XPG4和SUS早期版本要求支持这些选项,但从SUSv3起,不再需要这些功能,但这些符号仍然被保留,以便向后兼容。
(4)符合POSIX.1-2008的平台还要求支持下列选项:
_POSIX_ASYNCHRONOUS_IO
_POSIX_BARRIERS
_POSIX_CLOCK_SELECTION
_POSIX_MAPPED_FILES
_POSIX_MEMORY_PROTECTION
_POSIX_READER_WRITER_LOCKS
_POSIX_REALTIME_SIGNALS
_POSIX_SEMAPHORES
_POSIX_SPIN_LOCKS
_POSIX_THREAD_SAFE_FUNCTIONS
_POSIX_THREADS
_POSIX_TIMEOUTS
_POSIX_TIMERS
这些常量定义成具有值200809L。相应的_SC
符号同样是为了向后兼容而被保留下来的。
(5)如果对指定的pathname或fd已不再支持此功能,那么_PC_CHOWN_RESTRICTED
和_PC_NO_TRUNC
返回−1,而errno
不变,在所有符合POSIX的系统中,返回值将大于0(表示该选项被支持);
(6)_PC_CHOWN_RESTRICT
引用的文件必须是一个文件或者是一个目录。如果是一个目录,那么返回值指明该选项是否可应用于该目录中的各个文件。
(7)_PC_NO_TRUNC
和_PC_2_SYMLINKS
引用的文件必须是一个目录。
(8)_PC_NO_TRUNC
的返回值可用于目录中的各个文件名。
(9)_PC_VDISABLE
引用的文件必须是一个终端文件。
(10)_PC_ASYNC_IO
、_PC_PRIO_IO
和_PC_SYNC_IO
引用的文件一定不能是一个目录。
图2-20列出了若干配置选项以及在本书所讨论的4个示例系统上的对应值。如果系统定义了某个符号常量但它的值为−1或0,但是相应的sysconf
或pathconf
调用返回的是−1,就表示该项未被支持。可以看到,有些系统实现还没有跟上Single UNIX Specification的最新版本。
限制 | FreeBSD 8.0 | Linux 3.2.0 | Mac OS X 10.6.8 | Solaris 10 | |
---|---|---|---|---|---|
UFS 文件系统 |
PCFS 文件系统 |
||||
_POSIX_CHOWN_RESTRICTED |
1 | 1 | 200112 | 1 | 1 |
_POSIX_JOB_CONTROL |
1 | 1 | 200112 | 1 | 1 |
_POSIX_NO_TRUNC |
1 | 1 | 200112 | 1 | 不支持 |
_POSIX_SAVED_IDS |
不支持 | 1 | 200112 | 1 | 1 |
_POSIX_THREADS |
200112 | 200809 | 200112 | 200112 | 200112 |
_POSIX_VDISABLE |
255 | 0 | 255 | 0 | 0 |
_POSIX_VERSION |
200112 | 200809 | 200112 | 200112 | 200112 |
_XOPEN_UNIX |
不支持 | 1 | 1 | 1 | 1 |
_XOPEN_VERSION |
不支持 | 700 | 600 | 600 | 600 |
图2-20 配置选项的实例
注意,当用于Solaris PCFS
文件系统中的文件时,对于_PC_NO_TRUNC
,pathconf
返回−1。PCFS
文件系统支持DOS格式(软盘格式),DOS文件名按DOS文件系统所要求8.3格式截断,在进行此种操作时并无任何提示。
如前所述,头文件定义了很多POSIX.1和XSI符号。但是除了POSIX.1和XSI定义外,大多数实现在这些头文件中也加入了它们自己的定义。如果在编译一个程序时,希望它只与POSIX的定义相关,而不与任何实现定义的常量冲突,那么就需要定义常量_POSIX_C_SOURCE
。一旦定义了_POSIX_C_SOURCE
,所有POSIX.1头文件都使用此常量来排除任何实现专有的定义。
POSIX.1标准的早期版本定义了
_POSIX_SOURCE
常量。在POSIX.1的2001版中,它被替换为_POSIX_C_SOURCE
。
常量_POSIX_C_SOURCE
及_XOPEN_SOURCE
被称为功能测试宏(feature test macro)。所有功能测试宏都以下划线开始。当要使用它们时,通常在cc
命令行中以下列方式定义:
cc -D_POSIX_C_SOURCE=200809L file.c
这使得C程序在包括任何头文件之前,定义了功能测试宏。如果我们仅想使用POSIX.1定义,那么也可将源文件的第一行设置为:
#define_POSIX_C_SOURCE 200809L
为使SUSv4的XSI选项可由应用程序使用,需将常量_XOPEN_SOURCE
定义为700。除了让XSI选项可用以外,就POSIX.1的功能而言,这与将_POSIX_C_SOURCE
定义为200809L的作用相同。
SUS将c99
实用程序定义为C编译环境的接口。随之,就可以用如下方式编译文件:
c99 -D_XOPEN_SOURCE=700 file.c –o file
可以使用-std=c99
选项在gcc
的C编译器中启用1999 ISO C扩展,如下所示:
gcc -D_XOPEN_SOURCE=700 -std=c99 file.c -o file
历史上,某些UNIX系统变量已与某些C数据类型联系在一起,例如,历史上主、次设备号存放在一个16位的短整型中,8位表示主设备号,另外8位表示次设备号。但是,很多较大的系统需要用多于256个值来表示其设备号,于是,就需要一种不同的技术。(实际上,Solaris用32位表示设备号:14位用于主设备号,18位用于次设备号。)
头文件<sys/types.h>
中定义了某些与实现有关的数据类型,它们被称为基本系统数据类型(primitive system data type)。还有很多这种数据类型定义在其他头文件中。在头文件中,这些数据类型都是用C的typedef
来定义的。它们绝大多数都以_t
结尾。图2-21列出了本书将使用的一些基本系统数据类型。
用这种方式定义了这些数据类型后,就不再需要考虑因系统不同而变化的程序实现细节。在本书中涉及这些数据类型时,我们会说明为什么要使用它们。
类型 |
说明 |
---|---|
|
时钟滴答计数器(进程时间)(1.10节) |
|
压缩的时钟滴答(POSIX.1未定义;8.14节) |
|
设备号(主和次)(4.24节) |
|
文件描述符集(14.4.1节) |
|
文件位置(5.10节) |
|
数值组ID |
|
i节点编号(4.14节) |
|
文件类型,文件创建模式(4.5节) |
|
目录项的链接计数(4.14节) |
|
文件长度和偏移量(带符号的)( |
|
进程ID和进程组ID(带符号的)(8.2和9.4节) |
|
线程ID(11.3节) |
|
两个指针相减的结果(带符号的) |
|
资源限制(7.11节) |
|
能原子性地访问的数据类型(10.15节) |
|
信号集(10.11节) |
|
对象(如字符串)长度(不带符号的)(3.7节) |
|
返回字节计数的函数(带符号的)( |
|
日历时间的秒计数器(1.10节) |
|
数值用户ID |
|
能表示所有不同的字符码 |
图2-21 一些常用的基本系统数据类型
就整体而言,这些不同的标准之间配合得相当好。因为SUS基本说明和POSIX.1是同一个东西,所以我们不对它们进行特别的说明,我们主要关注ISO C标准和POSIX.1之间的差别。它们之间的冲突并非有意,但如果出现冲突,POSIX.1服从ISO C标准。然而它们之间还是存在着一些差别的。
ISO C定义了clock
函数,它返回进程使用的CPU时间,返回值是clock_t
类型值,但ISO C标准没有规定它的单位。为了将此值变换成以秒为单位,需要将其除以在<time.h>
头文件中定义的CLOCKS_PER_SEC
。POSIX.1定义了times
函数,它返回其调用者及其所有终止子进程的CPU时间以及时钟时间,所有这些值都是clock_t
类型值。sysconf
函数用来获得每秒滴答数,用于表示times
函数的返回值。ISO C和POSIX.1用同一种数据类型(clock_t
)来保存对时间的测量,但定义了不同的单位。这种差别可以在Solaris中看到,其中clock
返回微秒数(CLOCK_PER_SEC
是100万),而sysconf
为每秒滴答数返回的值是100。因此,我们在使用clock_t
类型变量的时候,必须十分小心以免混淆不同的时间单位。
另一个可能产生冲突的地方是:在ISO C标准说明函数时,可能没有像POSIX.1那样严。在POSIX环境下,有些函数可能要求有一个与C环境下不同的实现,因为POSIX环境中有多个进程,而ISO C环境则很少考虑宿主操作系统。尽管如此,很多符合POSIX的系统为了兼容性也会实现ISO C函数。signal
函数就是一个例子。如果在不了解的情况下使用了Solaris提供的signal
函数(希望编写可在ISO C环境和较早UNIX系统中运行的可兼容程序),那么它提供了与POSIX.1 sigaction
函数不同的语义。第10章将对signal
函数做更多说明。
在过去25年多的时间里,UNIX编程环境的标准化已经取得了很大进展。本章对3个主要标准——ISO C、POSIX和Single UNIX Specification进行了说明,也分析了这些标准对本书主要关注的4个实现,即FreeBSD、Linux、Mac OS X和Solaris所产生的影响。这些标准都试图定义一些可能随实现而更改的参数,但是我们已经看到这些限制并不完美。本书将涉及很多这种限制和幻常量。
在本书最后的参考书目中,说明了如何获得这些标准的方法。
2.1 在2.8节中提到一些基本系统数据类型可以在多个头文件中定义。例如,在FreeBSD 8.0中,size_t
在29个不同的头文件中都有定义。由于一个程序可能包含这29个不同的头文件,但是ISO C却不允许对同一个名字进行多次typedef
,那么如何编写这些头文件呢?
2.2 检查系统的头文件,列出实现基本系统数据类型所用到的实际数据类型。
2.3 改写图2-17中的程序,使其在sysconf
为OPEN_MAX
限制返回LONG_MAX
时,避免进行不必要的处理。