UNIX环境高级编程(第三版)读书笔记第一部分

2016-12-03 23:32:23

断断续续地花了半年时间,终于看完了这本大部头。现在做一下整理,也算一次复习,以免忘记得更快:)


一.UNIX各流派,标准化


1.1 作者介绍

该书第一版由W.Richard Stevens撰写(该作者同时还出版了《TCP/IP详解三卷本》以及《UNIX网络编程两卷本》),从第二版开始由贝尔实验室的开发人员Rago加入,第三版Rich以及仙去,Rago负责修订。


对于UNIX生态程序员,查找系统函数会用《UNIX程序员手册》,然而该书只罗列了接口,没有给出实例及基本原理。

——这即是本书的使命。


该书的前言写在2013年1月。


1.2 标准化

Open Group与SUS

Single UNIX Specification(下文中称SUS)是Open Group的出版物,Open Group是两个工业社团X/Open和OSF(Open System Software Foundation)在1996年合并成的。X/Open在1994年发布了SUS第一版,当时包含约1170个接口(成为Spec 1170)。1997年Open Group发布了SUSv2,2001年发布了SUSv3,2008年发布了SUSv4。


IEEE,ISO与POSIX

IEEE学会就不用介绍了,话说我系王志华老师刚刚荣膺IEEE FELLOW(据说这个title要比两院院士更牛),在此在背后也恭喜一下,呵呵

POSIX(Portable Operating System Interface)指的是IEEE标准1003.1-1988(操作系统接口)。

该标准1988版提交给ISO,通过审核后,1990年版[IEEE 1990]成为ISO/IEC 9945-1:1990。该标准即为大名鼎鼎的POSIX.1

该标准经过20多年的发展,已经较为成熟,由Austin Group负责维护。


ISO C

这里要特地强调一下C标准库与UNIX标准之间的关系。因为当代的UNIX系统都支持C标准库!

ISO C标准库重要版本包括C90,C99, C11。本书基于C99。

C99定义的各个头文件可将ISO C库分成24个区。所有的UNIX实现库都包括了这些头文件。


1.3 UNIX系统流派

本书介绍了四大流派:

a)FreeBSD 8.0

b)Linux3.2.0

c)  Mac OS X10.6.8(Darwin10.8.0)

d)Solaris 10

只有c),d)两者经过了UNIX认证。不过四种实际上的UNIX 实现都提供了UNIX编程环境。由于工作关系,我重点关注Linux与Darwin,POSIX.1


1.4 限制

各大流派之间的差异导致标准必须做一些限制,包括两类:

a)编译时限制(如短整型最大值是多少)

b)运行时限制(如文件名有多少个字符)

函数sysconf,pathconf,fpathconf来实现运行时限制


1.5 基本系统数据类型

各大系统都定义了头文件<sys/types.h>,都是用C的typedef来定义的基本数据类型,绝大多数以_t结尾。


1.6 标准之间的冲突

各大标准如果出现冲突,POSIX.1服从ISO C标准。

ISO C定义了clock函数,返回clock_t,但是ISO C没有规定它的单位,于是<time.h>定义了CLOCKS_PER_SEC。

在使用clock_t的时候必须注意以免混淆不同的时间单位。


1.7 UNIX体系结构

应用程序

|-shell  -system call      

|-system call

|-通用函数库 -system call


system call再直接与内核打交道

《UNIX程序员手册》第二部分描述了C语言定义的系统调用接口。

《UNIX程序员手册》第三部分描述了通用库函数


1.8 时间值

历史上,UNIX系统使用过两种时间值。

(1)日历时间,timestamp。我的另一篇博文已经讨论过。

(2)进程时间。包括三部分:时钟时间,用户CPU时间,系统CPU时间。进程时间以滴答计算,除以每秒滴答数获得秒数。

时钟时间指进程运行的总时间,包括其他同时运行的进程时间。

用户CPU时间指运行用户程序指令的时间。

系统CPU时间则指系统服务所用的时间,比如read函数,内核执行该服务所花费的时间就计入该进程的系统CPU时间。

用户CPU时间+系统CPU时间 = CPU时间。

当然,书中对CPU时间的描述有点矛盾,因为进程时间有时也被称为CPU时间,这一点要注意。



I/O

这一部分对比总结了第3,4,5,6,14,18中的要点


2.1 基本I/O函数

2.1.1 函数介绍

在UNIX的哲学里,希望对一切与外部的I/O都能统一处理。比如文件,网络,设备。

因此有一套基本的I/O函数,是对file, socket, device通用的。这就是不带缓冲的I/O.所谓不带缓冲就是指每次read和write都调用内核中的一个system call!

这类I/O函数使用最多的是五个基本的函数open, read, write, lseek, close。此外还有create也较多使用。

另外不常用的还有dup, fcntl, sync, fsync 以及ioctl。

这里简单介绍下五个最常用的函数接口:

int open(const char* path, int oflags, …);

int close(int fd);

off_t seek(int fd, off_t offset, int whence);

ssize_t read(int fd, void *buf, size_t nbytes);

ssize_t write(int fd, void *buf, size_t nbytes);


要注意,sync和fsync是不同的。sync只是同步到写队列中,fsync则等写队列输出到硬盘才返回。数据库等操作中要尤其注意。


2.1.2 I/O效率

当然,要注意,不带缓冲的I/O并不是指要一个字节一个字节读写。也是要有bufsize的选择的。只不过是每次函数调用都会调用内核中的一个system call。

如read(STDIN_FILENO, buf, BUFFSIZE)

这里的BUFFSIZE选择会影响I/O效率。这个大小的选取与文件系统的磁盘块大小有关。

比如Linux ext4文件系统,磁盘块长度为4096字节,所以当BUFFSIZE选择4096之后,系统CPU时间已经达到最小值,并基本没有变化。

然而大多数文件系统采用了预读取技术,当顺序读取时会一次性读更多的磁盘数据到缓存中,因此对于时钟时间,基本地,BUFFSIZE选用32以上的值时,差别不大。


这里要注意区分三个时间,时钟时间,用户CPU时间与系统CPU时间。


2.1.3 inode与vnode

每个进程在进程表中都有一个记录项,包含一张打开文件描述符表。每个打开文件都有一个vnode,vnode又指向了系统索引节点inode。

在两个进程同时打开一个文件时,每个进程有各自的vnode,但是指向同一个inode。


硬链接与软链接

硬链接即直接指向inode,inode会做一个引用计数,只有当引用计数为0时,才会从硬盘删除。这就是硬链接与软连接的不同之处。

同时由于目录项的inode编号指向同一个文件系统中的inode,因此硬链接不能与跨越文件系统。

而软连接即符号链接,实际上该文件的内容只是包含了该符号链接所指向的文件的名字。

关于UNIX文件系统的基本结构详见第四章P91.


2.1.4 /dev/fd

新的系统都提供名为/dev/fd的目录。打开文件/dev/fd/n等效于复制文件描述符n。



2.2 标准I/O

标准I/O库不局限于UNIX,由于它是被ISO C标准说明的。因此其他操作系统也有实现。

2.2.1 流和FILE对象

基本I/O函数都是围绕fd来实现的。而标准I/O则围绕FILE和stream进行。当使用标准I/O库打开或创建一个文件时,已使一个流于一个文件相关联。

也即是说FILE这个结构体,包含了其指向的流。具体地它包括了fd,

该流缓冲区的指针,缓冲区的长度,当前在缓冲区的字符数以及出错标志等。


对一个进程预定义了三个流,对应标准输入,标准输出,标准错误STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO。


2.2.2 缓冲

标准I/O提供缓冲的目的是尽可能地减少使用read和write调用的次数。标准I/O提供了以下3种类型的缓冲。

(1)全缓冲,在填满缓冲区后才进行实际的I/O操作。

(2)行缓冲

(3)不带缓冲

注意,缓冲区可由标准I/O例程自动地冲洗,或者可以调用函数fflush冲洗一个流。

flush这个词要注意:在I/O时指缓冲区到硬盘。在终端驱动时指废弃缓冲区。

可以用setbuf来设置缓冲区,setvbuf还可以设置缓冲类型:_IOFBF,_IOLBF,_IONBF.


其他没什么了,由于标准I/O本身使用比较多,直接上函数吧。

打开:fopen, freopen, fdopen.

字符读写:int getc(FILE),int  fgetc(FILE), getchar()==getc(stdin)。

行读写: char gets(char * buf); char *fgets(char* buf, int n, FILE *restrict fp );

 这里要注意,getc是不推荐使用的函数,因为getc不能指定缓冲区长度,容易引起缓冲区溢出,造成系统漏洞,比如1988年的因特网糯虫事件。

二进制读写:

size_t read(void * restrict ptr, size_t size, size_t noblj, File* fp);

size_t write(void * restrict ptr, size_t size, size_t noblj, File* fp);

定位流:ftell, fseek, rewind

格式化读写: printf, sprintf, fprintf, dprintf, snprintf

内存流:fmemopen


2.2.3 标准I/O效率

对文件大量读写的时候,由于缓冲区的存在,系统调用时间小于基本I/O,所以标准I/O效率较高。

然而由于需要两次复制数据:内核与标准I/O缓冲区(系统调用read/write时),标准I/O缓冲区与用户程序中的行缓冲区之间。所以效率不高。

一些嵌入式操作系统对合理内存要求高于可移植性等,做了相关优化,比如Newlib C库。


2.3 高级I/O

这里主要讨论非阻塞I/O,记录锁,I/O多路转换,异步I/O,readv/writev以及存储映射I/Ommap


2.3.1   非阻塞I/O

对于基本I/O接口,I/O操作是阻塞的,但是可以设置为非阻塞,方法如下:

(1)调用open函数时指定O_NONBLOCK

(2)调用fcntl,打开O_NONBLOCK标志


2.3.2     记录锁

为支持多进程同时编辑一个文件,提供记录锁,也称为byte-range locking。因为它锁定的是文件的一个区域。

POSIX.1提供了fcntl方法来实现:

三个相关命令:F_GETLK, F_SETLK, F_SETLKW

要注意保持加锁顺序,避免死锁。同时对于锁的隐含继承和释放要注意(P396)


2.3.3    I/O多路转换

解决同时要读写多个设备,不能在一个设备上阻塞以影响另一个设备的读写。所以有了解决方法I/O多路转换:

select与select

poll

select平时用得比较多,这里重点注意一下poll:

int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

struct pollfd{

int fd;

short events;关心的事件

short events;事件结果

 }


2.3.4    异步I/O

对于select与poll,其实本质还是在轮询,系统并不直接主动告诉我们信息,异步I/O即利用信号来实现异步形式通知某一事件的发送。

POSIX的实现就是利用AIO控制块,进程中初始化AIO控制块,相当于向系统注册,告诉系统我要等这个信号然后再读写,然后调用aio_read和aio_write来异步读写。

相关接口还有:aio_fsync(), aio_error(), aio_return(), aio_suspend(), Caio_cancel(), lio_listio()


2.3.5    readv和writev

一次函数调用读写多个非连续缓冲区。也称为散布读(scatter read)和聚集写(gather write)


2.3.6    readn/writen

按需多次调用read和write,直至读写了N字节


2.3.7    存储映射I/O

mmap,能够将一个磁盘文件映射到存储空间的一个缓冲区上,比如显卡驱动的framebuffer就经常采用这个策略。



2.4 终端I/O

2.4.1 终端I/O

终端I/O主要有两种工作模式:

(1)规范模式输入处理,终端输入以行为单位

(2)非规范模式输入处理,输入字符不装配成行

shell是第一种,vi是第二种


POSIX.1定义了11个特殊输入字符,其中9个可以更改。不能更改的两个是CR回车,NL换行。P552给出了具体的11个特殊输入字符解释


终端I/O还要关注的就是终端属性,可以通过如下函数获得和设置:

tcgetattr, tcsetattr。P555给出了各属性的含义


2.4.2 伪终端

注意P581图19-1的伪终端相关进程典型结构,伪终端从设备,通过终端行规程来与读写函数交互


典型用途包括:网络登录服务器,窗口系统终端模拟