第十章 系统级I/O
输入/输出(I/O)是在主存和外部设备之间拷贝数据的过程。输入操作是从I/O设备拷贝数据到主存,输出操作是从主存拷贝数据到I/O设备。
10.1 Unix I/O
一、每个unix文件都是一个m字节的序列;所有I/O设备,如网络、磁盘和终端都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。
二、unix系统中输入输出的操作:
1、打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符。unix系统创建每个进程的时候都有三个打开的文件:标准输入;标准输出,标准错误。
2、改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k(从文件开头起始的字节偏移量)。
3、读写文件。读操作就是从文件拷贝n>0个字节到存储器,从当前文件位置k开始,然后将k增加到k+n。
4、关闭文件。应用通知内核关闭这个文件;作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池当中。
10.2 打开和关闭文件
一、进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的。
(1)函数定义:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
(2)参数解析:
返回:成功则为新文件描述符,出错为-1。
open函数将filename转化为一个文件描述符,并且返回描述符数字,返回的描述符数字总是在进程中当前没有打开的最小描述符。
1、flags参数指明进程打算如何访问这个文件:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:
O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件。
O_TRUNC:如果文件存在,就截断它。
O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
2、mode参数指定了新文件的访问权限位,符号名称如下:
二、进程通过调用close函数关闭一个打开的文件。
(1)函数定义:
#include <unistd.h>
int close(int fd);
(2)参数解析:
返回:成功返回0,出错返回-1。
关闭一个已经关闭的描述符会出错
fd:即文件的描述符。
10.3 读和写文件
一、read函数
(1)函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
(2)参数解析:
返回:若成功则为读的字节数,若EOF则为0,若出错为-1。
fd:文件描述符
buf:存储器位置
n:最多从当前文件位置拷贝n个字节到存储器位置buf
二、write函数
(1)函数原型:
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t n);
(2)参数解析:
返回:若成功则为写的字节数,若出错为-1。
fd:文件描述符
buf:存储器位置
n:最多从存储器位置buf拷贝n个字节到当前文件位置
三、通过调用lseek函数,应用程序能够显式地修改当前文件的位置
四、不足值
在某些情况下,read和write传送的字节比应用程序要求的要少,这些不足值不表示有错误,出现这种情况的原因如下:
1、读时遇到EOF
2、从终端读文本行
3、读和写网络套接字
10.4 用RIO包健壮地读写
一、RIO(Robust I/O,健壮的I/O)包,它会自动处理不足值。
二、RIO提供了两类不同的函数:
1.RIO的无缓冲的输入输出函数。
通过调用rio_readn和rio_writen函数,应用程序可以在存储器和文件之间直接传送数据。
#include "csapp.h"
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
参数:
fd:文件描述符
usrbuf:存储器位置
n:传送的字节数
返回值:rio_readn成功则返回传送的字节数,EOF为0(一个不足值),出错为-1
rio_writen成功则返回传送的字节数,出错为-1,没有不足值。
2.RIO的带缓冲的输入函数
一个文本行就是一个由换行符结尾的ASCII码字符序列。
范例:如何统计文本文件中文本行的数量——通过计算换行符。需要用到的函数:
#include "csapp.h"
void rio_readinitb(rio_t *rp, int fd);
ssize_t rio_readlineb(rio_t *rp,void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
返回:若成功则为读的字节数,若EOF则为0,若出错则为-1。
10.5 读取文件元数据
一、应用程序通过调用stat和fstat函数,检索到关于文件的信息(有时也称为文件的元数据)。
#include <unistd.h>
#include <sys/stat.h>
int stat(const char *filename, struct stat *buf);
int fstat(int fd,struct stat *buf);
返回值:若成功则为0,若出错则为-1
参数:
stat需要输入文件名,而fstat需要输入的是文件描述符。
二、关于stat数据结构如下图:
st_size:包含了文件的字节数大小
st_mode:编码了文件访问许可位和文件类型。
普通文件包括某种类型的二进制或文本数据(对内核而言,毫无区别) S_ISREG()
目录文件包含关于其他文件的信息 S_ISDIR()
套接字是一种用来通过网络与其他进程通信的文件 S_ISSOCK()
10.6 共享文件
一、内核用三个相关的数据结构来表示打开的文件:
1、描述符表。每个进程都有独立的描述符表;它的表项是由进程打开的文件描述符来索引的。
2、文件表。打开文件的集合是由一张文件表来表示的;所有的进程共享这张表。每个文件表的表项有:文件位置、引用计数、指向v-node表中对应表项的指针。
3、v-node表。同文件表一样,所有进程共享这张v-node表。每个表项包含stat结构中的大多数信息。
二、文件共享方式:
1、没有共享文件,每个描述符对应一个不同文件。
2、多个描述符也可以通过不同的文件表表项来引用同一个文件。(每个描述符都有自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据)
3、父子进程可以共享文件。子进程会有一个父进程描述符表项的副本,父子进程打开相同的文件表集合,共享相同的文件位置。在内核删除相应的文件表表项之前,父子进程都必须关闭相应的描述符表项。
示例:
无共享:
文件共享:
子进程继承父进程的打开文件:
10.7 I/O重定向
一、Unix外壳提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。
unix > ls > foo.txt
使外壳加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt。
二、I/O重定向函数: dup2
函数定义为:
#include <unistd.h>
int dup2(int oldfd, int newfd);
返回:若成功则为非负的描述符,若出错则为-1。
dup2函数拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容,如果newfd被打开了,dup2会在拷贝oldfd之前关闭newfd。
10.8 标准I/O
一、ANSI C定义了一组高级输入输出函数,称为标准I/O库,包含:
1、fopen、fclose,打开和关闭文件
2、fread、fwrite,读和写字节
3、fgets、fputs,读和写字符串
4、scanf、printf,复杂的格式化的I/O函数
二、标准I/O库将一个打开的文件模型化为一个流。对于程序员而言,一个流就是一个指向FILE类型的结构的指针。
三、每个ANSI C程序开始的时候都有三个打开的流:stdin、stdout、stderr,分别对应于标准输入、标准输出和标准错误:
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
类型为FILE的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的Unix I/O系统调用的数量尽可能的小。
10.9 综合:我该使用哪些I/O函数
一、Unix I/O是在操作系统内核中实现的。应用程序可以通过open、close、lseek、read、write和stat这样的函数来访问Unix I/O。
二、Unix对网络的抽象是一种称为套接字的文件类型。和任何Unix文件一样,套接字也是用文件描述符来引用的,在这种情况下称为套接字描述符。应用进程通过读写套接字描述符来与运行在其他计算机上的进程通信。
三、标准I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。然而 ,对流的限制和对套接字的限制,有时候会互相冲突,而又极少有文档描述这些现象:
限制一:跟在输出函数之后的输入函数。
限制二:跟在输入函数之后的输出函数。
四、对套接字使用lseek是非法的。
五、在网络套接字上不要使用标准I/O函数来进行输入和输出,而是使用健壮的RIO函数。
参考资料
《深入理解计算机系统》
学习总结
这一章的内容数量不是很多,但内容的含义不是很容易理解,看完第一遍书之后对内容理解很模糊,于是认真地看了第二遍。