[CSAPP笔记][第十章 系统级I/O]
第十章 系统级I/O
输入/输出(I/O)
: 是指主存
和外部设备
(如磁盘,终端,网络)之间拷贝数据过程。
高级别
I/O
函数scanf
和printf
<<
和>>
- 使用系统级
I/O
函数实现
系统级
I/O
函数。Q
:大多数时候高级别I/O
函数都运行良好,为什么我们还要学Unix I/O
A
:- 了解
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_FILENO
STDOUT_FILENO
STDERR_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系统里,换行符(
\n
)与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
,并用\0
字符结束这个文本行。rio_readlineb
最多读MAXLINE-1
个字符,其余被截断,末尾永远是\0
.
rio_readnb(&rio,buf,n)
rio_readnb
函数从rio
最多读n
个字节到buf
对同一描述符,对
rio_readlineb
和rio_readnb
的调用可以任意交叉。- 但是带缓冲的 和 不带缓冲的 不应该交叉引用。
剩余部分给出大量RIO
函数的实例。
- 图10-5展示了一个
读缓冲区
的格式,以及初始化它的rio_readinitb
的代码。rio_readinitb
函数创建了一个空的读缓冲区,并且将一个打开的文件描述符与之关联。
图10-6所示的
rio_read
函数是RIO读程序的核心。rio_read
是Unix read
函数的带缓冲版本。- 当调用
rio_read
要求读n
个字节. - 此时如果缓冲区为空,调用
read
填满,不过也未必会满。 - 读
缓冲区
内min(n,rp->rio_cnt)
个未读字节。
- 当调用
对于一个应用程序,
rio_read
和Unix read
函数拥有相同的语义。只是可能有时返回的
不足值
可能会不同。- 所以如果抛开不足值的话,两者是一样的。
- 即包装它,让他读满。
- 即后文的
rio_readn
和rio_readnb
。
两者的相似性,使得在某些情况也能互相替换。
- 如后文的
rio_readn
和rio_readnb
。
- 如后文的
10.5 读取文件元数据
应用程序能够通过调用stat
和fstate
函数,检索到关于文件的信息(有时也称为文件的元数据(metadata)
)
#include<unistd.h>
#include<sys/stat.h>
int stat(const char *filename,struct stat *buf);
int fstat(int fd,struct stat *buf);
//填写stat数据结构中的各个成员
返回 : 成功0 ,出错-1
st_size
成员包含了文件的字节数大小
。st_mode
成员则编码了文件访问许可位
和文件类型
- 文件类型
- 普通类型:就是我们一般所说的
文件
- 目录文件:包含关于其他文件的信息
- 套接字: 是一种用来通过网络与其他进程通信的文件。
- 普通类型:就是我们一般所说的
- Unix提供的
宏指令
根据st_mode
来确定文件类型,以下是其中一部分。S_ISREG()
#这是一个普通文件吗S_ISDIR()
#这是一个目录文件吗S_ISSOCK()
#这是一个网络套接字吗- 在
sys/stat.h
中定义
- 文件类型
图10-10展示了如何使用宏
和stat
函数来读取和解释
10.6 共享文件
除非你很清楚内核
是如何表示打开的文件,否则文件共享
的概念相当难懂。
内核
有三个相关的数据结构来表示打开的文件:
描述符表(descriptor table)
:- 每个进程都有它独立的
描述符表
。 - 它的
表项
是由进程打开的文件描述符
来索引的。 - 每个打开的
描述符表项
指向文件表
的一个表项。
- 每个进程都有它独立的
文件表
:打开文件的集合是由一张文件表
表示的。- 所有的进程共享这张表。
- 每个
文件表项
的部分组成是- 当前的文件位置
引用计数(reference count)
:即当前指向该表项的描述符项
数。- 关闭一个
描述符
会减少相应文件表表项
中的引用计数
。 - 当
引用计数
变为0
。内核会删除这个文件表表项。
- 关闭一个
- 以及一个指向
v-node
表中对应表项的指针。
v-node
- 所有的进程共享这张表。
- 每个表项包含
stat
结构的大多数信息。st_mode
st_size
打开文件有三种可能的情形:
最常见的类型
- 就是打开两个不同的文件,且文件磁盘位置也不一样。
- 没有进行共享.
共享情况1
- 多个
描述符
也可以通过引用不同的文件表表项
来引用同一个文件
。 - 内容相同,
文件位置
不同(指向的磁盘位置是同一块) - 例子
- 如果以同一个
filename
调用open
两次,就会出现这种情况。 - 每个
描述符
都有它自己的文件位置,所以对不同描述符
的读操作可以从文件的不同位置获取数据。
- 如果以同一个
子父进程共享情况
我们也能理解父子进程如何共享文件。
调用
fork
后,子进程有一个父进程描述符表
副本。父子进程共享相同的打开
文件表
。- 共享相同的
文件位置
。
- 共享相同的
一个很重要的结果
- 在内核删除对应文件表表项之前,父子进程必须都关闭它们的描述符。
- 不要以为父进程
close(fd 1)
就好了。- 子进程也要
close(fd 1)
- 子进程也要
10.7 I/O 重定向
Unix
外壳提供了I/O
重定向功能,允许用户将磁盘文件和标准输入输出联系起来。
例如
unix> ls > foo.txt
- 使得
shell
加载和执行ls
程序,将标准输出重定向到磁盘文件foo.txt
。
- 使得
一个
Web
程序代表客户端允许CGI
程序时,也执行一种相似类型的重定向。
I/O
重定向如何工作?
使用
dup2
函数#include<unistd.h> int dup2(int oldfd,int newfd); 返回:若成功则为非负的描述符,若出错则为-1
dup2
函数拷贝描述符表表项oldfd
到描述符表表项newfd
,覆盖newfd
。- 如果
newfd
已经打开,dup2
会在拷贝oldfd
之前关闭newfd
。(废话,不是肯定打开吗?)
- 如果
左边和右边的
hoinkies
右hoinkies
:>
左hoinkies
:<
10.8 标准I/O
ANSI C
定义了一组高级输入输出函数,称为标准I/O
库。
这个
库(libc)
提供了- 打开和关闭文件的函数(
fopen
和fclose
) - 读和写字节(
fread
和fwrite
) - 读和写字符串的函数(
fgets
和fputs
) - 以及复杂的格式化
I/O
函数 (scanf
和printf
)
- 打开和关闭文件的函数(
标准
I/O
库将一个打开的文件模型化为一个流
- 对于程序员来说,一个
流
就是一个指向FILE
类型的结构的指针。 - 每个
ANSI C
程序开始时都有三个打开的流
stdin
标准输入stdout
标准输出stdout
标准错误#include<stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
- 对于程序员来说,一个
类型为
FILE
的流是对文件描述符
和流缓冲区
的抽象。流缓冲区
的目的和RIO读缓冲区
的目的一样- 就是使开销较高的
Unix I/O
系统调用的数量尽可能的少。
- 就是使开销较高的
10.9 综合: 我该使用哪些 I/O 函数
图总结了我们讨论过的各种I/O
包。
Unix I/O
。RIO I/O
标准I/O
磁盘
和终端设备之选。- 在
网络输入输出
使用,有一些问题。Unix
对网络的抽象是一种叫做套接字
的文件类型。- 和任何
Unix
文件一样,套接字
也是用文件描述符
来引用的,称为套接字描述符
。 - 应用进程通过读写
套接字描述符
来与运行在其他计算机上的进程通信。
- 和任何
- 大多数C程序员,生涯中只用
标准I/O
标准I/O流
某种意义上而言是全双工
的,因为程序能够在同一个流
上执行输入输出。
然而,对流
的限制和对套接字
的限制,有时会互相冲突,而又极少有文档描述这些现象:
(不懂)
- 限制一: 跟在输出函数之后的输入函数。
- 如果中间没有插入对
fflush
,fseek
,fsetpos
或者rewind
的调用,一个输入函数不能跟在输出函数之后。fflush
函数清空与流相关的缓冲区。- 后三个函数使用
Unix I/O
的lseek
函数来重置当前的文件位置。
- 如果中间没有插入对
- 限制二: 跟在输入函数之后的输出函数。
- 如果中间没有插入对
fseek
,fsetpos
或者rewind
的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个`EOF。
- 如果中间没有插入对
看不懂,看完之后的再看。
因此,我们建议你在网络套接字
不要使用标准I/O
来进行输入和输出。而要使用RIO
如果需要格式化的输出
- 使用
sprintf
函数在存储器格式化一个字符串。 - 然后用
rio_writen
把它发送到套接口。
- 使用
如果需要格式化输入
- 使用
rio_readlinb
来读一个完整的文本行 - 然后使用
sscanf
从文本行提取不同的字段。
- 使用
10.10 小结
[CSAPP笔记][第十章 系统级I/O]的更多相关文章
- CSAPP:第十章 系统级I/O
CSAPP:第十章 系统级I/O 10.1 unix I/O10.2 文件10.3 读取文件元数据10.4 读取目录内容10.5 共享文件10.6 我们该使用哪些I/O函数? 10.1 unix I/ ...
- 第十章 系统级I/O
第十章 系统级I/O 一.Unix I/O 1.一个unix文件就是一个m个字节的序列 2.unix外壳创建的每个进程开始时都有三个打开的文件:标准输入(0) .标准输出(1)和标准错误(-1). 二 ...
- 深入理解计算机系统 第十章 系统级I/O
很多高级语言都提供了执行 I/O 的较高级别的函数.为什么我们还要学习 Unix I/O? 原因:1.由于 I/O 和其他系统概念之间有循环依赖关系,故了解 Unix I/O 将帮助我们理解其他的系统 ...
- 深入理解计算机系统 第十章 系统级I/O 第二遍
了解 Unix I/O 的好处 了解 Unix I/O 将帮助我们理解其他的系统概念 I/O 是系统操作不可或缺的一部分,因此,我们经常遇到 I/O 和其他系统概念之间的循环依赖.例如,I/O 在进程 ...
- 系统级I/O 第八周11.9~11.15
第十章 系统级I/O cp1 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include ...
- 系统级I/O 第八周11.1~11.8
第十章 系统级I/O 输入输出I/O是在主存和外部设备(如磁盘,网络和终端)之间拷贝数据的过程.输入就是从I/O设备拷贝数据到主存,而输出就是从主存拷贝数据到I/O设备. 所有语言的运行时系统都提供执 ...
- 系统级编程(csapp)
系统级编程漫游 系统级编程提供学生从用户级.程序员的视角认识处理器.网络和操作系统,通过对汇编器和汇编代码.程序性能评测和优化.内存组织层次.网络协议和操作以及并行编程的学习,理解底层计算机系统对应用 ...
- 第十章实践——系统级I/O代码运行
第十章实践——系统级I/O代码运行 实验代码清单如下: 1. cp1——复制一个文件到另一个文件中(两个已经存在的文件) 复制前: 执行后结果 2. setecho.echostate——改变.显示输 ...
- ARMV8 datasheet学习笔记4:AArch64系统级体系结构之Generic timer
1.前言 2.generate timer 2.1 概述 提供了一个系统计数器,用来实时测量流逝的时间: 提供了一个虚拟计数器,用来测量某个虚拟机上流逝的虚拟时间: 定时器,每隔一段时间会触发事件,支 ...
随机推荐
- ibatis中的resultClass,parameterClass,resultMap,resultType的使用与区别
parameterClass 是参数类.指定了参数的完整类名(包括包路径).可通过别名避免每次重复书写冗长的类名. resultClass 是结果类, 二.resultClass取值 1.result ...
- C# Interface显式实现和隐式实现
c#中对接口的实现方式有两种:隐式实现和显式实现,之前一直没仔细看过,今天查了些资料,在这里整理一下. 隐式实现的例子 interface IChinese { string Speak(); } p ...
- NUnit单元测试初试
创建项目,创建几个方法 创建测试类 开启NUnit测试工具,新建一个测试项目 打开测试的程序集 选择节点,点击测试,绿色通过,红色说明有错误
- HTML5 Canvas显示本地图片实例1、Canvas预览图片实例1
1.前台代码: <input id="fileOne" type="file" /> <canvas id="canvasOne&q ...
- (转)asp.net注册实现下一步
在asp.net中有两种容器控件,其中包括panel和placeholder控件. 使用panel控件可以对控件进行分组.一帮助组织web窗体也的内容,将控件组织在面板中,可提供有关在运行时控件应如何 ...
- (转)Phonegap VS AppCan
简介 Phonegap PhoneGap是一个用基于HTML,CSS和JavaScript的,创建移动跨平台移动应用程序的快速开发平台.它使开发者能够利用iPhone,Android,Palm,Sym ...
- NodeJs获取不到POST参数
NodeJs报错,从网页端获取不到POST参数,提示错误类似如下 TypeError: Cannot read property 'username' of undefined at C:\U ...
- iOS 使用GitHub托管代码(github desktop使用)
iOS 使用GitHub托管代码 代码托管 1.首先得有一个GitHub的账号,没有的话就去https://github.com注册一个吧. 2.下载GitHub Mac客户端:http://mac. ...
- [c#]控制台进度条的示例
看到[vb.net]控制台进度条的示例 感觉很好玩,翻译成C#版. using System; using System.Collections.Generic; using System.Linq; ...
- Sql Server添加用户
1.sa用户登陆之后,在安全性中新建登录名 2.添加登录名,下面的默认数据库选择该用户可访问的默认数据库 3.服务器角色中选择public 4.用户映射中选择该用户可访问的数据库,数据库角色一般选择p ...