关于I/O可以先参考这些文章,但是这里可能还是有所不同。分析系统级别的I/O有什么不一样的地方。
文件I/O
高级I/O
标准库I/O
开篇介绍了三个级别的I/O的区别之处。所有语言的运行时系统都提供执行I/O的较高级别的工具。例如,标准I/O库;在UNIX系统中,是通过使用由内核提供的系统级I/O函数来实现这些较高级别的I/O函数的。介绍UNIX I/O和标准I/O的一般概念,展示在C程序中如何可靠地使用它们。
在UNIX系统中有一个说法,一切皆文件。所有的I/O设备,如网络、磁盘都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许UNIX内核引出一个简单、低级的应用接口,称为UNIX I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
关于打开文件的基本操作,这里就不再累述,就是关于几个函数的解释,在上面的三篇文章中有解释。
int open(char *filename,int flags,mode_t mode);
其中打开标志flags有三种基本标志:O_RDONLY、O_WRONLY、O_RDWR。也可以和其他三种(O_CREAT、O_TRUNC、O_APPEND)组合使用。mode参数指定了新文件的访问权限位。(这次终于看到完全的mode参数的使用方法了)
在系统I/O中读写文件用的系统函数为read()和write()函数来执行。
#include <unistd.h> ssize_t read(int fd,void * buf,size_t n); ssize_t write(int fd,void *buf,size_t n);
read函数从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。而write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置。返回值要么为-1要么为写入的字节数目。
/* $begin cpstdin */ #include "csapp.h" int main(void) { char c; while(Read(STDIN_FILENO, &c, 1) != 0) Write(STDOUT_FILENO, &c, 1); exit(0); } /* $end cpstdin */关于在文件中定位使用的函数为lseek,在I/O库中使用的函数为fseek。
有些情况下,read和write传送的字节比应用程序要求的要少,出现这种情况的原因如下:
这个包会处理上面的不足,RIO提供了方便、健壮和高效的I/O。提供了两类不同的函数:
ssize_t rio_readn(int fd,void *usrbuf,size_t n); ssize_t rio_writen(int fd,void *usrbuf,size_t n);
对同一个描述符,可以任意交错地调用rio_readn和rio_writen。一个问本行的末尾都有一个换行符,那么像读取一个文本中的行数怎么办,使用read读取换行符这个方法不是很妥当,可以调用一个包装函数(rio_readineb),它从一个内部读缓冲区拷贝一个文本行,当缓冲区为空时,会自动地调用read重新填满缓冲区。也就是说,这些函数都是缓冲区操作而言的。
应用程序能够通过调用stat和fstat函数检索到关于文件的信息(有时也称为文件的元数据)
#include <sys/stat.h> #include <unistd.h> int stat(const char *filename,struct stat *buf); int fstat(int fd,struct stat *buf);
若成功,返回0,若出错则为-1.stat以一个文件名为输入,并且填充buf结构体。fstat函数只不过是以文件描述符而不是文件名作为输入。
struct stat { #if defined(__ARMEB__) unsigned short st_dev; unsigned short __pad1; #else unsigned long st_dev; #endif unsigned long st_ino; unsigned short st_mode; unsigned short st_nlink; unsigned short st_uid; unsigned short st_gid; #if defined(__ARMEB__) unsigned short st_rdev; unsigned short __pad2; #else unsigned long st_rdev; #endif unsigned long st_size; unsigned long st_blksize; unsigned long st_blocks; unsigned long st_atime; unsigned long st_atime_nsec; unsigned long st_mtime; unsigned long st_mtime_nsec; unsigned long st_ctime; unsigned long st_ctime_nsec; unsigned long __unused4; unsigned long __unused5; };其中st_size成员包含了文件的字节大小。st_mode为文件访问许可位。UNIX提供的宏指令根据st_mode成员来确定文件的类型:S_ISREG(),这是一个普通文件么;S_ISDIR(),这是一个目录文件么;S_ISSOCK()这是一个网络套接字么。使用一下这个函数
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int main() { int fd,size; struct stat buf_stat; memset(&buf_stat,0x00,sizeof(buf_stat)); fd=stat("stat.c",&buf_stat); printf("%d\n",(int)buf_stat.st_size); return 0; }
内核用三个相关的数据结构来表示打开的文件:
下面看几张图。
描述符1和4通过不同的打开文件表表项来引用两个不同的文件。这是典型的情况,没有共享文件,并且每个描述符对应一个不同的文件。
多个描述符也可以通过不同的文件表表项来应用同一个文件。如果同一个文件被open两次,就会发生上面的情况。关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
父子进程也是可以共享文件的,在调用fork()之前,父进程如第一张图,然后调用fork()之后,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。
标准I/O库将一个打开的文件模型化为一个流。对于一个程序而言,一个流就是一个指向FILE类型的结构的指针。类型为FILE的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和RIO读缓冲区的一样:就是使开销较高的UNIX I/O系统调用的数量尽可能的小。例如,当第一次调用getc时,库通过调用一次read函数来填充流缓冲区,然后将缓冲区总的第一个字节返回给应用程序。只要缓冲区还有未读的字节,接下来对getc的调用就能直接从流缓冲区得到服务。
上图中展现了几种I/O的关系模式,在应用程序中应该使用哪些函数呢?标准I/O函数是磁盘和终端设备I/O的首选。但是对网络套接字上尽量使用健壮的RIO或者系统I/O