输入/输出(I/O)
: 是指主存
和外部设备
(如磁盘,终端,网络)之间拷贝数据过程。
-
高级别
I/O
函数scanf
和printf
<<
和>>
- 使用系统级
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.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
。
- 如果
10.8 标准I/O
ANSIC
定义了一组高级输入输出函数,称为标准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
系统调用的数量尽可能的少。
- 就是使开销较高的