[转载]《UNIX环境高级编程》学习笔记(转)
这两天看了一下APUE(Advanced Programming in the UNIX,UNIX环境高级编程)。这是一本公认的好书,它详细的讲解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.
I/O效率
1.
2.
3.
标准I/O
标准I/O可以使用户不必像文件I/O那样考虑缓存及最佳I/O长度的选择。它提供缓存,目的是尽可能少的减少使用read和write的数量。它有三种类型的缓存:全缓存、行缓存和不带缓存。但是标准I/O缓存也是产生很多问题,引起很多混淆的一个领域。中文版的P142讲了一个很有意思的例子,由于fork的实现方式和IO缓存导致在终端和文件中输出结果的不同。
高级I/O
非阻塞I/O是调用不会永远阻塞的I/O操作,如果操作不能完成,则立即出错返回。记录锁(record
locking)的功能:一个进程正在读或修改文件的某个部分时,可以阻塞其他进程修改同一文件区。程序12-5讲了一个有趣的例子,让精灵进程阻止其多份副本同时运行,原理是这样的:精灵进程把它们的进程ID写道一个各自专有的PID文件上,通过对这个文件加写锁来保证只有一个副本在运行。I/O多路转接:
select
stat函数
第4章详细介绍了stat结构中的每一个成员。这使得我们对UNIX文件的各个属性都有所了解。对文件的所有属性以及对文件进行操作的所有函数都有完整的了解对各种UNIX程序设计都非常重要。
非局部转移
setjmp和longjmp函数这两个函数用于执行跨越函数的跳转功能。用于信号处理程序中做非局部转移时应用sigsetjmp和siglongjmp函数。
进程
特殊进程:进程ID为0的是调度进程,为1的是init进程。僵死进程:一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程称为僵死进程。孤儿进程:父进程已经终止的进程。init进程会领养这些进程。精灵进程:长生存期,系统引导装入时启动,关闭时终止,后台运行。fork函数创建新进程。子进程是父进程的复制品(如果正文段是只读的,则父、子进程共享正文段)。现在很多实现并不做一个父进程数据段和堆的完全拷贝,而是使用了在写时复制(Copy-On-Write,COW)技术。vfork用于创建一个用来exec一个新程序的新进程,所以它不将父进程的地址空间完全复制到子进程。vfork保证子进程会先运行。exec函数只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。进程有三种正常终止法及两种异常终止法。不管哪种情况,最后都会执行内核中的同一段代码。该段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等等。终止进程会通知其父进程它是如何终止的。
信号
信号是通知进程已发生某种条件的一种技术。信号是软件中断。信号提供了一种处理异步事件的方法。进程处理信号有三种选择:
1.
2.
3.
早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。好处:意味着已经发生某种事情,所以是个好机会应当唤醒阻塞的系统调用。产生信号后,内核通常在进程表中设置某种形式的一个标志。当做了这个动作,我们就说向一个进程递送了一个信号。在信号产生到递送之间的时间间隔,称为信号未决(pending)。进程可以阻塞某个信号。除非对被阻塞的信号的动作是忽略,否则该进程将此信号保持在未决状态,直到阻塞解除。进程调用sigpending函数将指定的信号设置为阻塞和未决。每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送的信号集。进程可以调用sigprocmask来检测和更改信号屏蔽字。kill函数将信号发送给进程(组)。raise函数则允许进程向自身发送信号。alarm函数设置闹钟时间,时间超过设置值时默认行为是终止进程。pause函数使进程挂起直到捕捉到一个信号。sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代UNIX早期版本中使用的signal函数。用于信号处理程序中作非局部转移时应sigsetjmp和siglongjmp函数。sigsupsend函数恢复信号屏蔽字,然后使进程睡眠。
可再入函数
可再入函数是信号处理程序可以放心调用的函数。像malloc,如果malloc过程捕捉到信号,信号处理程序又malloc,这就会带来问题,所以它不是可再入函数。10.6表10-3列出了所有的可再入函数。若在信号处理程序中调用一个不可再入函数,则其结果是不可预见的。
进程间通信
IPC
1)它们是半双工的。
2)它们只能在具有共同祖先的进程之间使用。
流管道没有管道的第一种限制,FIFO和命名流管道没有第二种限制。
main函数是如何调用的?
内核用exec函数启动C程序。在调用main前先调用一个特殊的启动例程(可执行程序文件将此启动例程指定为程序的起始地址——这是编译、链接的时候设置的)。启动例程从内核取得命名行参数和环境变量值,然后为调用main函数做好准备。从main返回后,启动例程会立即调用exit函数。
进程终止
_exit函数立即进入内核,而exit函数则先执行一些清除处理,再进入内核。
atexit函数登记至多32个函数,这些函数将由exit自动调用,它们被称为exit handler。
设置-用户-ID和设置-组-ID