系统级I/O
一、UNIX I/O
在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。
(ps:size_t和ssize_t的区别,前者是unsigned int,而后者是int)
有些情况下,read和write传送的字节比应用程序要求的要少,出现这种情况的原因如下:
- 读时遇到EOF。此时read返回0来发出EOF信号。
- 从终端读文本行。如果打开文件是与终端相关联,那么每个read函数将以此传送一个文本行,返回的不足值等于文本行的大小。
- 读和写网络套接字。可能会出现阻塞现象。(我一定会在进程间通信的时候弄清楚这个事情的前前后后,后后前前!!!)
实际上,除了EOF,在读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。然而,如果你想创建健壮的网络应用,就必须反复调用read和write处理不足值,直到所有需要的字节都传送完毕。(这一点在UNIX网络编程中已经领略过了!!)
四、用RIO包健壮地读写
这个包会处理上面的不足,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; }
六、共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表(descriptor table)每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
- 文件表(file table) 打开文件的描述符表项指向问价表中的一个表项。所有的进程共享这张表。每个文件表的表项组成包括由当前的文件位置、引用计数(既当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的应用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
- v-node表(v-node table)同文件表一样,所有的进程共享这张v-node表,每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
下面看几张图。
描述符1和4通过不同的打开文件表表项来引用两个不同的文件。这是典型的情况,没有共享文件,并且每个描述符对应一个不同的文件。
多个描述符也可以通过不同的文件表表项来应用同一个文件。如果同一个文件被open两次,就会发生上面的情况。关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
父子进程也是可以共享文件的,在调用fork()之前,父进程如第一张图,然后调用fork()之后,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。
七、I/O重定向
函数为:
函数解释:
(即:让描述符oldfd实现newfd的功能)
eg,dup2(field,1) 将标准描述符输出重定向到field描述符
假设在调用dup2(4,1)之前,我们的状态图10-11所示,其中描述符1(标准输出)对应于文件A(比如一个终端),描述符4对应于文件B(比如一个磁盘文件)。A和B的引用计数都等于1。图10-14显示了调用dup2(4,1)之后的情况。两个描述符现在都指向了文件B;文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了;文件B的引用计数已经增加了。从此之后,任何写到标准输出的数据都被重定向到文件B。
解析图如下:
八、I/O使用的抉择方法
上图中展现了几种I/O的关系模式,在应用程序中应该使用哪些函数呢?标准I/O函数是磁盘和终端设备I/O的首选。但是对网络套接字上尽量使用健壮的RIO或者系统I/O
小结:
Unix内核使用三个相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向v-node表中的表项。每个进程都有它自己单独的描述符表,而所有的进程共享同一打开文件表和v-node表。