第十章 系统级I/O
输入/输出(I/O) : 是指主存和外部设备(如磁盘,终端,网络)之间拷贝数据过程。
-
高级别
I/O函数scanf和printf<<和>>- 使用系统级
I/O函数实现
-
系统级
I/O函数。Q:大多数时候高级别I/O函数都运行良好,为什么我们还要学Unix I/OA:- 了解
Unix I/O将帮助你理解其他的系统概念。- 要深入理解其他概念,必须理解
I/O。
- 要深入理解其他概念,必须理解
- 有时你除了使用
Unix I/O别无选择- 标准
I/O库没有提供读取文件元数据的方式。- 如
文件大小或文件创建时间。
- 如
- 用于
网络编程十分冒险。
- 标准
- 了解
10.1 Unix I/O
-
一个
Unix 文件就是一个m个字节的序列:
- 所有
I/O设备都被模型化为文件。 - 而所有的输入和输出都被当做相应文件的读和写。
- 所有
-
设备优雅地映射成文件,允许Unix内核引出一个简单,低级的应用接口。叫做Unix I/O- 使得所有的输入输出都能以一种统一且一致的方式来执行。
-
打开文件: 应用程序要求内核打开文件
-
内核返回一个小的非负整数,叫做描述符- 等于
内核分配一个文件名,来标示当前的文件。 内核记录有关这个打开文件的所有信息。应用程序只需要记住标示符。
- 等于
-
Unix外壳创建进程时都有三个打开的文件- 标准输入(标示符
0) - 标准输出(标示符
1) - 标准错误(标示符
2) - 头文件
<unistd.h>定义了常量代替显式的描述符值STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO
- 标准输入(标示符
-
-
改变当前文件的位置(非文件目录)
-
对于每个打开的文件,内核保持一个
文件位置k- 初始为
0。 文件位置即是从文件开头起始的字节偏移量。
- 初始为
-
执行
lseek操作,可以显式地设置文件位置。
-
-
读写文件。
-
一个读操作就是从文件拷贝
n个字节到存储器,然后将k增加到k+n。- 给定一个大小为
m字节的文件,当k>=m时执行读操作会触发一个称 为end-of-file(EOF)的条件。- 应用程序能检测到这个
条件(或者说信号?) - 文件结尾并没有这样的符号。
- 应用程序能检测到这个
- 给定一个大小为
-
写操作就是从存储器拷贝n个字节到一个文件,从当前文件位置k开始,然后更新k。
-
-
关闭文件 :当应用程序完成了文件的访问,通知
内核关闭文件。-
响应
内核释放文件打开时创建的数据结构。- 将
描述符恢复到可用的描述符池中。
-
无论一个进程因为何种原因被关闭,内核会关闭所有它打开的文件。
-
-
- 使得所有的输入输出都能以一种统一且一致的方式来执行。
10.2 打开和关闭文件
进程是通过调用 open函数来打开一个已存在的文件或者创建一个新文件的
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int open(char *filename,int flags,mode_t mode);
//返回:若成功则为新文件描述符,若出错为-1
open函数将filename转换为一个文件描述符,并且返回描述符数字。
-
返回的
描述符总是在进程当前没有打开的最小描述符。 -
flags参数指明了进程打算如何访问这个文件:-
可是以一个多个
掩码的或。(拿二进制思想思考) -
O_RDONLY: 只读 -
O_WRONLY: 只写O_CREAT: 如果文件不存在,就创建一个截断的(truncated)(空)文件。O_TRUNC: 如果文件已存在,就截断它(长度被截为0,属性不变)O_APPEND: 在每次写操作前,设置文件位置到文件的结尾
-
O_RDWR: 可读可写例子代码 //已只读模式打开一个文件 fd = Open("foo.txt",O_RDONLY,0); //打开一个已存在的文件,并在后面面添加一个数据 fd = Open("foo.txt",O_WRONLY|O_APPEND,0);
-
-
mode参数指定了新文件的访问权限位。-
每个进程都有
umask权限掩码,或权限屏蔽字- 所有被设置的权限都要减去这个
权限掩码才是实际权限。777-022=755或者是777&~022。
- 通过
umask()函数设置
-
mode并不是实际权限- 文件的权限位被设置为
mode & ~umask,也可以表示两者相减。
- 文件的权限位被设置为
-
例子
#define DEF_MODE S_IRUSR|S_IWUSER|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH //所有人都能读和写 #define DEF_UMASK S_IWGRP|S_IWOTH //屏蔽了用户组的写和其他人的写 umask(DEF_UMASK); fd=oepn("foo.txt",O_CREAT|O_TRUNC|O_WRONLY,DEF_MODE); //创建了一个新文件,文件的拥有者有读写权利,其他人只有读权限。(屏蔽了用户组的写和其他人的写)
-
close函数关闭一个打开的文件
#include <unistd.h>
int close(int fd);
//返回: 若成功则为0,若出错则为-1
关闭一个已关闭的描述符会出错。
10.3 读和写文件
调用read和write完成输入输出
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
//read函数从描述符fd的当前文件位置拷贝最多n个字节到存储器buf
返回:若成功则为读的字节数,若EOF则为0,若出错为 -1.
ssize_t write(int fd,const void *buf,size_t n)
//write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置
返回:若成功则为写的字节数,若出错则为-1
展示了一个程序使用read和write调用一次一个字节的从标准输入拷贝到标准输出。


通过调用lseek函数,应用程序能够显示地修改当前文件的位置
ssize_t 和 size_t 有什么区别
- size_t:被定义为
unsigned int- ssize_t:被定义为
int
- 为了出错的时候,返回-1.
- 有趣的是,因为这个-1,使得read的最大值减小了一半。
在某些情况,read和write传送的字节比应用程序要求的要少,有以下原因。
这样的情况返回的值叫做不足值。
-
读时遇到EOF。
-
从终端读文本行(
stdin和STDIN_FILENO)不足值等于文本行的大小。
-
读和写网络套接字(
socket)- 内部缓冲约束和较长的网络延迟会引起
read和write返回不足值。 - 你向创建健壮的诸如
Web服务器这样的网络应用,就必须反复调用read和write处理不足值,知道所有需要的字节传送完毕。
- 内部缓冲约束和较长的网络延迟会引起
一般的磁盘文件除了EOF外,一般不会遇到不足值的问题。
10.4 用RIO包健壮地读
RIO包: 全称 Robust I/O包,健壮的I/O包。会自动的处理上文中所述的不足值。
提供两类不同的函数:
-
无缓冲的输入输出函数
- 直接在存储器和文件之间传送数据,没有应用级缓冲。
- 他们对二进制读写到网络和从网络读写二进制数据尤其有用。
-
带缓冲的输入函数
- 允许你高效地从文件中读取文本行和二进制数据。
- 这些文件内容
缓存到应用级缓冲区内。
-
带缓冲的
RIO输入函数是线程安全的,在同一个描述符可以交错调用
10.4.1 RIO的无缓冲的输入输出函数

- 与普通
read,write区别- 在读写
网络套接字的时候不会产生不足值- 即
rio_writen不可能返回不足值
- 即
- 线程安全的。
- 当
wirte,read被应用信号处理程序的返回中断时,允许手动重启。
- 在读写
源代码

10.4.2 RIO的带缓冲的输入函数
一个文本行就是由一个换行符结尾的ASCII码字符序列。
- 在Unix系统里,换行符(
)与ASCII码换行符LF相等,数字值为0x0a
rio_readnb和rio_readlineb 引入
假设我们要编写一个程序计算文本文件中文本行的数量如何实现?
-
方案1:
read函数一次一个字节地从文件传送到用户存储器,检查每个字节来查找换行符。- 效率低,每读取文件中的一个字符都要求陷入内核。
-
更好的方法是调用一个包装函数
rio_readlineb。-
它从一个内部
读缓冲区拷贝一个文本行。- 当缓冲区变空时,会调用
read重新填满缓冲区。
- 当缓冲区变空时,会调用
-
为什么这样子更快?
- 利用了空间局部性原理
-
-
使用
rio_readn的带缓冲区版本rio_readnb.- 对于即包含文本行也包含二进制数据的文件(例如 11.5.3节会提到的
HTTP响应). - 和
rio_readlineb一样的读缓冲区中传送原始字节。
- 对于即包含文本行也包含二进制数据的文件(例如 11.5.3节会提到的
rio_readinitb 和 rio_readnb,rio_readlineb 实例
每打开一个描述符都要调用一次rio_readinitb函数。
- 它将描述符
fd和 地址rp处的一个类型为rio_t的读缓冲区联系起来。

-
rio_readlineb(&rio,buf,MAXLINE)函数rio_readlineb函数从rio(缓冲区)读出一个文本行(包括结尾的换行符),将它拷贝到存储器位置buf,并用