断断续续地花了半年时间,终于看完了这本大部头。现在做一下整理,也算一次复习,以免忘记得更快:)
一.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的伪终端相关进程典型结构,伪终端从设备,通过终端行规程来与读写函数交互
典型用途包括:网络登录服务器,窗口系统终端模拟