`
xitongyunwei
  • 浏览: 924762 次
文章分类
社区版块
存档分类
最新评论

UNIX 环境高级编程(阅读笔记)

 
阅读更多
W.Richard Stevens 先生所著之书比较适合有经验的人进一步深入学习

W.Richard Stevens 先生所著的UNIX 环境高级编程、UNIX网络编程、TCP/IP详解是难得的入门好书,但这里的入门指得是研究生级别的入门。Stevens 先生所著之书多半是为了给研究生上课时用做教材,因此 Stevens 先生假设阅读这些书的人至少已经接受过了系统的计算机技术本科教育,已经拥有了比较系统的计算机相关基础理论知识。在书中 Stevens 先生假设读者拥有了最基础的操作系统理论,网络技术理论,以及UNIX理论和实践经验,并拥有数种业界常用的UNIX系统作为学习环境(在国外的大学里很容易获得),这样才能体会书中所述的细节。因此这些书刚接触时看似通俗易懂,但在阅读细节上却对读者要求甚高,如果没有足够的专业理论和系统实践是很难真正读进去的。

所以,对于没有接受过系统的计算机专业教育的爱好者,或者主要以 linux 系统维护,或以linux 桌面应用为主的普通用户,Stevens 先生的书并不是很合适的阅读对象。

因此,就我个人的看法,掌握任何一种知识都是需要投入的。我们需要花费金钱去得知识来源(花钱买书,花钱上网,花钱培训),需要花费宝贵的时间去阅读(人生匆匆几十年,没多少时间可以给我们去浪费),需要花费精力去理解书中的内容(基础知识越扎实,理解所学内容越快,但累积基础同样需要大量时间和精力),需要找到合适的环境(光看不实践心里是不会有底的)去验证自己掌握的东西。因此,如果在决定投入之前,先要摸清楚自己到底想要达到什么样的目标,这个目标是否切合自己的实际(相信一个在流水线上辛苦劳作的普通工人,即使学会了计算机编程也是没有用武之地的,除非他有机会离开流水线)。自己是否有足够的资源承载自己的理想(如对于温饱问题尚未解决之人,要他花上千上万的钱去学MBA显然是不现实的)。

同时,任何一本书都会对读者提出一个基本的知识架构和程度上的要求,比方说即使是文学博士,让他去读高能物理学方面的书也会要他的命的。因此在选择所读之书前,先要对自己的实际能力做一次详细的评估,看自己目前到了哪种阶段,是否有能力去掌握自己想要掌握的东西。千万不要因为好高骛远而勉力为之。选择适合自己当前水平的书去阅读并理解,远好过拿着大师所著的神作干瞪眼。

如果LZ确实象帖中所描述的,主要希望掌握UNIX下的编程技术,尤其是网络编程技术的话。那以我本人的经验,可以有两条路走:

1、以实际需要完善知识架构:为自己做一个项目,比方说自己写个简单的 Web 服务器,因为现在 Web 应用非常丰富,既有成熟的客户端如浏览器可以配合,又有足够全的文档如 RFC文档可供参考,再加上最基本的 socket 编程经验,就可以开始做自己的 Web 服务器。一步一步的为 Web 服务器添加流行的功能,如支持后台 FastCGI 接口,支持 WebDAV,支持流媒体等。在这个过程中不断地学习和掌握相关的理论知识,有时在发觉设计上的不足时甚至需要推翻全盘重新架构。当最终一个完全符合自己心意且足够实用的 Web 服务器做成时,就拥有了可以由自己支配和修改的服务器,同时也掌握了相关的专业理论。这种方式比较适合有一定专业基础的人使用。优点是直观,方向明确所以学习效率高。缺点是需求驱动,形成知识架构不容易完整。

2、系统掌握计算机专业理论:最好的书就是大学里的理论教科书,这些书不会去讲解过于具体的计算机应用,而是从概念开始讲起,力图使学习者获得一个完整的知识体系。只要以后碰到的工作和这个知识体系相关,自然就能很快学会。这种方式比较适合没有基础,或者希望深入学习的人使用,优点是知识架构会逐渐趋于完整,理论功底扎实,后劲足。缺点是花费时间长,方向不明确所以学习效率低,初期会因缺少实践而进展缓慢。

因为不很清楚LZ目前的计算机专业技术水平到了哪种程度,也不清楚LZ最终希望自己达到什么样的目标,所以也很难为LZ提供什么有价值的经验。如果LZ能详细描述一下自己目前的实际水平,以及希望达到什么样的目标,我也许可以为LZ提供一些建议。

这两天看了一下APUEAdvanced Programming in the UNIXUNIX环境高级编程)。这是一本公认的好书,它详细的讲解200多个函数、提出并解决各种可能存在的问题,这绝对是UNIX程序员居家旅行必备宝典。不过对我来说它讲得太细太繁杂了,因为我并没有在UNIX/Linux下编程的需要,我只想解一UNIX/Linux下编程大概是怎么回事。我走马观花看了部分章节,记了一点笔记。下面是笔记的简化版本。

本书印象

本书覆盖了UNIX上数百个系统调用及函数,但是实际上大多数的内容集中在I/O和进程两大方面上。这两方面的内容大概也是用得最广的吧。I/O方面的内容包括了不带缓存的I/O、标准I/O库、终端I/O、高级I/O。进程方面的内容包括UNIX进程的环境、进程控制、信号、进程间通信、精灵进程。终端I/O、数据库函数库、打印机通信、调制解调器、伪终端这些章节感觉不重要,所以没看。进程关系这一章好像也不重要,只有编写精灵进程会涉及到该章的部分知识。其他章节应该都是很重要。

原子操作

只要涉及到多个进程共享资源,原子操作的概念就变成非常重要。原子操作(atomic operation)指的是由多步组成的操作。如果该操作原子地执行,则或者执行完所有步,或者一步也不执行,不可能只执行所有步的一个子集。竞态条件(race condition):当多个进程都企图对共享数据进行某种处理,而最后结果又取决于进程运行的顺序时,则我们认为发生了竞态条件。与之相关的有一个概念,叫时间窗口,它就是竞态条件下发生的。时间窗口大概就是说进程某一个操作与其下一个操作本来应该是连续执行,但是因为进程的切换,导致操作的不连续,导致共享资源被修改,导致程序运行偏离预期。书中10.4节介绍了早期的信号及一些经典的处理案例,这些案例都是由于时间窗口的存在而暗藏隐患。

出错处理

UNIX函数出错,通常返回一负值,并把全局变量errno设置为具有特定信息的值。<errno.h>定义了errno以及可以赋予它的各种常数。对于errno应当知道两条规则:

1. 如果没有出错,其值不会被一个例程清除。因此,仅当函数的返回值指明出错时,才检验其值。

2. 任一函数都不会将errno值设为0<errno.h>中定义的所有常数都不为0。由于每个进程只有一个errno变量,一个通用的规则是,当在信号处理程序中调用库函数或系统调用时,应当在其前保存errno,然后在其后恢复。

I/O效率

1. 对于不带缓存的I/O,当缓存长度等于文件系统的块长时,读操作的时间是最小;缓存长度小于块长,读操作时间增多;大于块长,也不会提高读操作的效率。

2. 标准I/O可以使用户不必像文件I/O那样担心如何选择正确的缓存长度。标准I/O并不比文件I/O慢很多。5.8讲标准I/O的效率。

3. 比起多次readwritereadvwritev更好。12.7readvwritev4. mmap/memcpy方式比read/write方式效率高,见12.9

标准I/O

标准I/O可以使用户不必像文件I/O那样考虑缓存及最佳I/O长度的选择。它提供缓存,目的是尽可能少的减少使用readwrite的数量。它有三种类型的缓存:全缓存、行缓存和不带缓存。但是标准I/O缓存也是产生很多问题,引起很多混淆的一个领域。中文版的P142讲了一个很有意思的例子,由于fork的实现方式和IO缓存导致在终端和文件中输出结果的不同。

高级I/O

非阻塞I/O是调用不会永远阻塞的I/O操作,如果操作不能完成,则立即出错返回。记录锁(record locking)的功能:一个进程正在读或修改文件的某个部分时,可以阻塞其他进程修改同一文件区。程序12-5讲了一个有趣的例子,让精灵进程阻止其多份副本同时运行,原理是这样的:精灵进程把它们的进程ID写道一个各自专有的PID文件上,通过对这个文件加写锁来保证只有一个副本在运行。I/O多路转接: select poll,网络编程用得多。readvwritev函数用于在一个函数调用中读、写多个非连续缓存。readnwriten读、写指定的N字节数据,并处理返回值小于要求值得情况。存储映射I/O mmap使一个磁盘文件与存储空间中的一个缓存相映射。当从缓存中取数据,就相当于读文件中的相应字节,类似的,将数据存入缓存,则相应字节就自动地写入文件。

stat函数

4章详细介绍了stat结构中的每一个成员。这使得我们对UNIX文件的各个属性都有所了解。对文件的所有属性以及对文件进行操作的所有函数都有完整的了解对各种UNIX程序设计都非常重要。

非局部转移

setjmplongjmp函数这两个函数用于执行跨越函数的跳转功能。用于信号处理程序中做非局部转移时应用sigsetjmpsiglongjmp函数。

进程

特殊进程:进程ID0的是调度进程,为1的是init进程。僵死进程:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程称为僵死进程。孤儿进程:父进程已经终止的进程。init进程会领养这些进程。精灵进程:长生存期,系统引导装入时启动,关闭时终止,后台运行。fork函数创建新进程。子进程是父进程的复制品(如果正文段是只读的,则父、子进程共享正文段)。现在很多实现并不做一个父进程数据段和堆的完全拷贝,而是使用了在写时复制(Copy-On-WriteCOW)技术。vfork用于创建一个用来exec一个新程序的新进程,所以它不将父进程的地址空间完全复制到子进程。vfork保证子进程会先运行。exec函数只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。进程有三种正常终止法及两种异常终止法。不管哪种情况,最后都会执行内核中的同一段代码。该段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等等。终止进程会通知其父进程它是如何终止的。

信号

信号是通知进程已发生某种条件的一种技术。信号是软件中断。信号提供了一种处理异步事件的方法。进程处理信号有三种选择:

1. 忽略该信号。

2. 按系统默认方式处理。

3. 提供一个函数,信号发生时则调用该函数。

早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。好处:意味着已经发生某种事情,所以是个好机会应当唤醒阻塞的系统调用。产生信号后,内核通常在进程表中设置某种形式的一个标志。当做了这个动作,我们就说向一个进程递送了一个信号。在信号产生到递送之间的时间间隔,称为信号未决(pending)。进程可以阻塞某个信号。除非对被阻塞的信号的动作是忽略,否则该进程将此信号保持在未决状态,直到阻塞解除。进程调用sigpending函数将指定的信号设置为阻塞和未决。每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送的信号集。进程可以调用sigprocmask来检测和更改信号屏蔽字。kill函数将信号发送给进程(组)。raise函数则允许进程向自身发送信号。alarm函数设置闹钟时间,时间超过设置值时默认行为是终止进程。pause函数使进程挂起直到捕捉到一个信号。sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代UNIX早期版本中使用的signal函数。用于信号处理程序中作非局部转移时应sigsetjmpsiglongjmp函数。sigsupsend函数恢复信号屏蔽字,然后使进程睡眠。

可再入函数

可再入函数是信号处理程序可以放心调用的函数。像malloc,如果malloc过程捕捉到信号,信号处理程序又malloc,这就会带来问题,所以它不是可再入函数。10.610-3列出了所有的可再入函数。若在信号处理程序中调用一个不可再入函数,则其结果是不可预见的。

进程间通信

IPC InterProcess Communication)。作者建议:学会使用管道和FIFO,尽可能避免使用消息队列以及信号量,而应当考虑流管道和记录锁。管道、FIFO、流管道、命名流管道、消息队列、信号量、共享存储这7IPC通常限于同一台主机的各个进程间的IPC。套接字和流则支持不同主机上各个进程间IPC。管道是UNIX IPC最老的形式,有两种限制:

1)它们是半双工的。

2)它们只能在具有共同祖先的进程之间使用。

流管道没有管道的第一种限制,FIFO和命名流管道没有第二种限制。

main函数是如何调用的?

内核用exec函数启动C程序。在调用main前先调用一个特殊的启动例程(可执行程序文件将此启动例程指定为程序的起始地址——这是编译、链接的时候设置的)。启动例程从内核取得命名行参数和环境变量值,然后为调用main函数做好准备。从main返回后,启动例程会立即调用exit函数。

进程终止

_exit函数立即进入内核,而exit函数则先执行一些清除处理,再进入内核。

atexit函数登记至多32个函数,这些函数将由exit自动调用,它们被称为exit handler

设置-用户-ID和设置--ID

对各种不同的用户ID和组ID(实际、有效和保存的)的理解和编写安全的设置-用户-ID程序是至关重要的。运行设置-用户-ID程序的进程通常得到额外的权限,所以编写这种程序要特别谨慎。

源地址:http://blog.renren.com/GetEntry.do?id=484380273&owner=334901290


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics