第10章--系统级IO

输出/输出(I/O)实在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。

一、Unix I/O

一个Linux文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅的映射到文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

  • 打开文件。一个应用程序通过要求内核打开相应地文件,来宣告他想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它们可用来代替现实的描述符值。
  • 改变当前的文件位置。对于每个打开的文件,内和保持着一个文件位置k,初始为0。这个文件位置是从文件开头开始的字节偏移量。应用程序能够通过执行seek操作,显式的设置文件的当前位置为k。
  • 读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。类似的,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。

二、文件

每个Linux文件都有一个类型来表明他在系统中的角色:

  • 普通文件包含任意数据。应用程序常常要区分文本文件和二进制文件,文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件是所有其他的文件。对内核而言,文本文件和二进制文件没有区别。
  • 目录是包含一组链接的文件,其中每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。每个目录至少含有两个条目:.是到该目录自身的链接,以及..是到目录层次结构中父目录的链接。
  • 套接字是用来与另一个进程进行跨网络通信的文件。

其他文件类型还包括命名通道、符号链接,以及字符和块设备。

三、打开和关闭文件

进程是通过调用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_RDWR:可读可写。

四、读和写文件

应用程序是通过分别调用read和write函数来执行输入和输出的。

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t n); // 若成功返回读的字节数,若EOF则为0,若出错为-1
ssize_t write(int fd, const void *buf, size_t n);  // 若成功返回写的字节数,若出错为-1

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。write函数从内存位置buf复制最多n个字节到描述符fd的当前文件位置。

在某些情况下,read和write传送的字节比应用程序要求的要少。这些不足值不表示有错误。出现这样情况的原因有:

  • 读时遇到EOF。假设我们准备读一个文件,该文件从当前文件位置开始只含有20个字节,而我们以50个字节的片进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回不足值0来发出EOF信号。
  • 从终端读文本行。如果打开文件是与终端相关联的(如键盘和显示器),那么每个read函数将一次传送一个文本行,返回的不足值等于文本行的大小。
  • 读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。

五、用RIO包健壮地读写

在这一小节里,我们会讲述一个I/O包,称为RIO(Robust I/O)包,它会自动为你处理上文中所述的不足值。在像网络程序这样容易出现不足值的应用中,RIO包提供了方便、健壮和高效的I/O。RIO提供了两类不同的函数。

  • 无缓冲的输入输出函数。这些函数直接在内存和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
  • 带缓冲的输入函数。这些函数允许你高效的从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于为printf这样的标准I/O函数提供的缓冲区。

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);  // 若成功返回传送的字节数,若EOF则为0(只对rio_readn而言),若出错则为-1

rio_readn函数从描述符fd的当前文件位置最多传送n个字节到内存位置usrbuf。类似的,rio_writen函数从位置usrbuf传送n个字节到描述符fd。rio_readn函数在遇到EOF时只能返回一个不足值。rio_writen函数绝不会返回不足值。对同一个描述符,可以任意交错的调用rio_readn和rio_writen。

2. RIO的带缓冲的输入函数

#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

每打开一个描述符,都会调用一次rio_readinitb函数。它将描述符fd和地址rp处的一个类型为rio_t的读缓冲区联系起来。

rio_readlineb函数从文件rp读出下一个文本行(包括结尾的换行符),将它复制到内存位置usrbuf,并且用NULL字符来结束这个文本行。rio_readlineb函数最多读maxlen-1个字节,余下的一个字符留给结尾的NULL字符。超过maxlen-1字节的文本行被截断,并用一个NULL字符结束。

rio_readnb函数从文件rp最多读n个字节到内存位置usebuf。对同一描述符,对rio_readlineb和rio_readnb的调用可以任意交叉进行。然而,对这些带缓冲的函数的调用却不应和无缓冲的rio_readn函数交叉使用。

六、读取文件元数据

应用程序能够通过调用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函数以一个文件名为输入,并填写如下所示的一个stat数据结构中的各个成员。fstat杉树是相似的,只不过是以文件描述符而不是文件名作为输入。

struct stat {
    dev_t         st_dev;      /* Device */
    ino_t         st_ino;      /* Inode */
    mode_t        st_mode;     /* Protection and file type */
    nlink_t       st_nlink;    /* Number of hard links*/
    uid_t         st_uid;      /* User ID of owner */
    gid_t         st_gid;      /* Group ID of owner */
    dev_t         st_rdev;     /* Device type (if inode device) */
    off_t         st_size;     /* Total size, in bytes */
    unsigned long st_blksize;  /* Block size for filesystem I/O */
    unsigned long st_blocks;   /* Number of blocks allocated */
    time_t        st_atime;    /* Time of last access */
    time_t        st_mtime;    /* Time of last modification */
    time_t        st_ctime;    /* Time of last change */
}

七、读取目录内容

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *name);  // 若成功返回指向目录流的指针,若出错则为NULL

函数opendir以目录名为参数,返回指向目录流的指针。流是对条目有序列表的抽象,在这里是指目录项的列表。

#include <dirent.h>

struct dirent *readdir(DIR *dirp);  // 若成功返回指向下一个目录项的指针,若没有更多的目录项或出错,则为NULL

每次对readdir的调用返回的都是指向流dirp中下一个目录项的指针,或者,如果没有更多的目录项则返回NULL。每个目录项都是一个结构,其形式如下:

struct dirent {
    ino_t d_ino;       /* inode number */
    char d_name[256];  /* Filename */
}

八、共享文件

可以用许多不同的方式来共享Linux文件。除非你很清楚内核是如何表示打开的文件,否则文件共享的概念相当难懂。内核用三个相关的数据结构来表示打开的文件:

  • 描述符表。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包含当前的文件位置、引用计数(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少对应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node表。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。

九、I/O重定向

Linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。例如,键入ls > foo.txt使得shell加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt。那么I/O重定向是如何工作的呢?一种方式是使用dup2函数。

#include <unistd.h>

int dup2(int oldfd, int newfd);  // 若成功返回非负的描述符,若出错则为-1

dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。

十、标准I/O

C语言定义了一组高级输入输出函数,称为标准I/O库,为程序员提供了Unix I/O的较高级别的替代。这个库(libc)提供了打开和关闭文件的函数(fopen和fclose)、读和写字节的函数(fread和fwrite)、读和写字符串的函数(fgets和fputs)、以及复杂的格式化的I/O函数(scanf和printf)。

标准I/O库将一个打开的文件模型化为一个流。对于程序员而言,一个流就是一个指向FILE类型的结构的指针。每个ANSI C程序开始时都有三个打开的流stdin、stdout和stderr,分别对应于标准输入、标准输出和标准错误:

#include <stdio.h>

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

类型为FILE的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的Linux I/O系统调用的数量尽可能的小。

十一、综合:我该使用哪些I/O函数?

Unix I/O模型是在操作系统内核中实现的。应用程序可以通过诸如open、close、lseek、read、write和stat这样的函数来访问Unix I/O。较高级别的RIO和标准I/O函数都是基于Unix I/O函数来实现的。RIO函数是专为本书开发的read和write的健壮的包装函数。它们自动处理不足值,并且为读文本行提供一种高效的带缓冲区的方法。标准I/O函数提供了Unix I/O函数的一个更加完整的带缓冲的替代品,包括格式化的I/O例程,如printf和scanf。

那么,在你的程序中该使用这些函数中的哪一个呢?下面是一些基本的指导原则:

  • G1:只要有可能就使用标准I/O。对磁盘和终端设备I/O来说,标准I/O函数是首选方法。
  • G2:不要使用scanf或rio_readlineb来读二进制文件。这样的函数是专门设计来读取文本文件的。
  • G3:对网络套接字的I/O使用RIO函数。如果使用标准I/O用于网络的输入输出时,会出现一些令人讨厌的问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值