输入输出是在主存和外部设备之间复制数据的过程。输入操作时从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。
Unix I/O
一个Linux文件就是一个m字节的序列:B0,B1....Bm - 1;
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种设备优雅地映射为文件的方式(甚至内核也被映射为文件),允许Linux内核引出一个简单、低级的应用接口,使得所有输入和输出都能以一种统一且一致的方式来执行:
- 打开和关闭文件:open(),close();
- 读写文件:read(),write();
- 改变当前文件的位置:指示文件要读写位置的偏移量;
文件
文件类型:
- 普通文件:包含任意数据,区分文本文件(只包含ASCⅡ或Unicode字符)和二进制文件(其他文件);
- 目录:一组连接文件的索引,每个目录字少含有两个条目(.是到该文件自身的链接,..是到目录层次结构中父目录的链接);
- 套接字:用来与另一个进程进行跨网络通信的文件;
- 命名通道、符号链接、字符和块设备;
目录层次结构
绝对路径以'/'开始,标识从根节点开始的路径;
相对路径名以文件名开始,标识从当前工作目录开始的路径;
打开文件
打开文件时通知内核准备访问文件,通过调用open函数来打开一个已存在的文件或者创建一个新文件。
open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符。fd == -1发生错误。
关闭文件
关闭文件通知内核结束访问一个文件,关闭一个已关闭的描述符会出错。
读文件
读文件从当前文件位置复制字节到内存位置,然后更新文件位置,调用read函数执行输入。
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值-1代表错误,而返回值0代表EOF,否则返回值表示的是实际传送的字节数量。
写文件
写文件从内存复制字节到当前文件位置,然后更新文件位置,调用write函数执行输出。
write函数从内存位置buf复制至多n个字节到描述符fd的当前位置。返回<0的数值代表发生错误。
一直允许不足值,反复处理不足值即可。
RIO包
RIO提供两种不同的函数:
- 无缓冲的输入输出函数:rio_readn和rio_written,每个字节都进内核;
- 带缓冲的输入函数:rio_readlineb和rio_readnb,带缓冲的RIO函数是线性安全的,他在同一个描述符上可以被交错地调用
读取文件元数据
应用程序调用stat和fstat函数,检索到文件的信息。
共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表:每个进程都有它独立的描述符表,他的表项由进程打开的文件描述符来索引。每个打开的描述符表项指向文件表中的一个表项。
- 文件表:打开文件的集合是由一张文件表来表示,所有的进程共享这张表,每个表项包括文件位置、引用计数,以及一个指向v-node表中对应表项的指针,关闭一个描述符会减少相应文件表表项中的引用计数。内核不会删除这个文件表表项,直到他的引用计数为零。
- v-node表:所有的进程共享这张v-node表,每个表项包含stat结构中的大多数信息。
两个不同的描述符通过两个不同的打开文件表表项来共享同一个磁盘文件。
子进程继承父进程的打开文件,共享相同的文件位置。调用fork之后,子进程的表与父进程的表相同,每一个refcnt+1.
I/O重定向
unix调用dup2(oldfd, newfd)函数来实现I/O重定向,dup2函数复制描述符表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
调用之前:
调用之后:
标准I/O
标准I/O函数示例:
- 打开和关闭文件(fopen和fclose)
- 读和写字节(fread和fwrite)
- 读和写字符串(fgets和fputs)
- 格式化的读和写(fscanf和fprintf)
标准I/O库将一个打开的文件模型化为流:对文件描述符和流缓冲区的抽象。
每个C程序在开始时都有三个打开的流:
- stdin标准输入
- stdout标准输出
- stderr标准错误
标准I/O的优点:
- 通过减少读和写系统调用的次数,有效增加内存;
- 自动处理不足值;
标准I/O的缺点:
- 没有提供访问文件元数据的函数;
- 标准I/O函数不是异步信号安全的,不适合用于信号处理;
- 不适合网络套接字的输入输出操作。
Unix I/O优点:
- Unix I/O是最通用,开销最低的,其他所有的I/O都是使用其实现的;
- 提供了访问文件元数据的函数;
- 异步信号安全,可以在信号处理程序中安全地使用;
Unix I/O缺点:
- 处理不足值时容易出错;
- 有效地读取文本行需要某种形式的缓冲,容易出错;
- 这两个问题都是由标准I/O和RIO包解决
I/O函数的选择:
- 一般规则:使用最高级别的I/O函数;
- 当使用磁盘文件和终端文件时使用标准I/O;
- 在信号处理程序中使用Unix I/O,因为异步信号安全;
- 当你准备读写网络套接字时使用RIO;