时间统计
预计时间(7小时或者5小时)
课本阅读 2小时
博客编写 3小时 (五笔输入法)
博客编写 1小时
验证代码 2小时
实际时间(8小时)
课本阅读 3小时
博客编写 3小时 (五笔输入法) 已放弃。。。
博客编写 3小时
验证代码 2小时
第十章 系统级I/O
输入/输出是在主存和外部设备之间拷贝数据的过程。输入操作是从I/O设备拷贝数据到主存,输出操作是从主存拷贝数据到I/O设备。
10.1 Unix I/O
I/O设备:网络、磁盘和终端
Unix I/O :将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口。
描述符:打开文件时,内核返回一个小的非负整数。 Unix外壳创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)、标准错误(描述符为2)。 改变当前的文件位置:文件位置为k,初始为0。 seek操作:显式地设置文件的当前位置为k.
EOF:是一个条件,而不是一个符号
关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的存储器资源。
10.2 打开和关闭文件
1、open函数:打开一个已存在的文件或者创建一个新文件
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(char *filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。
返回的描述符总是在进程中当前没有打开的最小描述符。
O_ RDONLY :只读
O_ WRONLY :只写
O_ RDWR :可读可写
flag参数可以是一个或多个更多位掩码的或。
O_ CREAT:如果文件不存在,就创建它的一个截断的空文件
O_ TRUNC:如果文件已经存在,就截断它
O_ APPEND:在每次写操作前,设置文件位置到文件的结尾处。
mode参数指定了新文件的访问权限位。文件的访问权限位被设置为mode & ~umask
关闭一个已关闭的描述符会出错。
10.3 读和写文件
1、应用程序是通过分别调用read和write函数来执行输入和输出的。
#include <unistd.h> ssize_t read(int fd,void *buf,size_t n); ssize_t write(int fd,const void *buf,size_t n);
2、read函数:从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。
返回值:-1:一个错误;0:EOF;否则,返回值:实际传送的字节数量。
3、write函数:从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置。
#include "csapp.h" int main(void) { char c; while(Read(STDIN_FILENO,&c,1) != 0) Write(STDOUT_FILENO,&c,1); exit(0); }
lseek函数:应用程序能够显式地修改当前文件的位置。
不足值:read和write传送的字节比应用程序要求的少。
产生不足值的原因:
1、读时遇到EOF
2、从终端读文本行
3、读和写网络套接字
10.4 用RIO包健壮地读写
1、RIO包的实质:I/O包
2、RIO包提供的两种函数:
无缓冲的输入输出函数、带缓冲的输入函数(线程安全)
3、应用程序通过调用rioreadn和riowritten函数可以在存储器和文件之间直接传送数据。
#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);
rio_ readn函数在遇到EOF时,只能返回一个不足值;
rio_ writen函数后局不会返回不足值。
10.4.2 RIO的带缓冲的输入函数
1、一个文本行就是一个有换行符结尾的ASCII码字符序列。
在Unix系统中,换行符(‘\n’)与ASCII码换行符(LF)相同,数字值为0x0a。
计算文本文件中文本行的数量,更好地方法是:
调用一个包装函数(rio、readlineb),它从一个内部读缓冲区拷贝一个文本行,当缓冲区变空时,会自动地调用read重新填满缓冲区。
#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);
rio_ readn和rio_ writen函数详细代码如下:
ssize_t rio_readn(int fd,void *usrbuf,size_t n) { size_t nleft = 0; sszie_t nread; char *bufp = usrbuf; while(nleft > 0) { if((nread = read(fd,bufp,nleft))<0) { if (errno == EINTR) nread = 0; else return -1; } else if (nread == 0) break; nleft -= nread; bufp += nread; } return (n-nleft); } ssize_t rio_writen(int fd,void *usrbuf,size_t n) { size_t nleft = n; ssize_t nwritten; char *bufp = usrbuf; while(nleft > 0) { if((nwritten = write(fd,bufp,nleft)) <=0) { if (errno == EINTR) nwritten = 0; else return -1; } nleft -= nwritten; bufp += nwrittenl; } return n; }
2、每打开一个描述符都会调用一次rio_ readinitb函数,它将描述符fd和地址rp处的一个类型为rio_t的读缓冲区联系起来。
//一次一行地从标准输入拷贝一个文本文件到标准输出 #include "csapp.h" int main(int argc,char **argv) { int n; rio_t rio;//? char buf[MAXLINE]; Rio_readinitb(&rio,STDIN_FILENO); while((n = Rio_readlineb(&rio,buf,MAXLINE))!=0) { Rio_writen(STDOUT_FILENO,buf,n); } }
读缓冲区的定义
#define RIO_BUFSIZE 8192 typedef struct { int rio_fd; int rio_cnt;//缓冲区未读字节 char *rio_bufptr; char rio_buf[RIO_BUFSIZE]; }rio_t;
rio_readinitb函数
void rio_readinitb(rio_t *rp,int fd) { rp->rio_fd = fd; rp->rio_cnt = 0; rp->rio_bufptr = rp->rio_buf; }
3、rio_ read函数:从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。
返回值:-1:一个错误;0:EOF;不足值:要求的字节数超过了读缓冲区内未读的字节的数量。
static ssize_t rio_read(rio_t *rp,char *usrbuf,size_t n) { int cnt; while(rp->rio_cnt <=0) { rp->rio_cnt = read(rp->rio_fd,rp->rio_buf,sizeof(rp->rio_buf)); if (rp->rio_cnt<0) { if(errno !=EINTR) { return -1; } } else if (rp->rio_cnt ==0) return 0; else rp->rio_bufptr = rp->rio_buf; } cnt = n; if (rp->rio_cnt < n) cnt = rp->rio_cnt; mencpy(usrbuf,rp->rio_bufptr,cnt); rp->rio_bufptr += cnt; rp->rio_cnt -= cnt; return cnt; }
rio_ readlineb和rio_ readnb函数
ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen) { int n,rc; char c,*bufp = usrbuf; for(n=1;n<maxlen;n++) { if ((rc = rio_read(rp,&c,1))==1) { *bufp++ = c; if (c== '\n') break; } else if (rc == 0) { if (n ==1 ) return 0; else break; } else return -1; } *bufp = 0; return n; } ssize_t rio_readnb(rio_t *rp,void *usrbuf,size_t n) { size_t nleft = n; sszie_t nread; char *bufp = usrbuf; while(nleft >0) { if ((nread = rio_read(rp,bufp,bleft))<0) { if (errno == EINTR) nread =0; else return -1; } else if (nread == 0) break; nleft -= nread; bufp += nread; } return (n-nleft); }
10.5 读取文件元数据
1、检索文件信息(元数据):应用程序能够通过调用stat和fstat函数
#include <unistd.h> #include <sys/stat.h> int stat(const char *filename,struct stat *buf); int fstat(int fd,struct stat *buf);
stat函数以一个文件名作为输入,填写一个stat数据结构中的各个成员。
fstat函数以文件描述符而不是文件名作为输入。
st_ size成员包含了文件的字节数大小。
st_ mode成员则编码了文件访问许可位和文件类型。
文件类型包括:
普通文件:某种类型的二进制或文本数据。
目录文件:关于其他文件的信息。
套接字:一种用来通过网络与其他进程通信的文件。
宏指令::根据st_mode成员来确定文件的类型。
在sys/stat.h中定义:
S_ ISREG():这是一个普通文件吗?
S_ ISDIR():这是一个目录文件吗?
S_ ISSOCK():这是一个网络套接字吗?
#include "csapp.h" int main(int argc,char **argc) { struct stat stat; char *type,*readok; Stat(argv[1],&stat); if(S_ISREG(stat.st_mode)) type = "regular"; else if(S_ISDIR(stat.st_mode)) type = "directory"; else type = "other"; if ((stat.st_mode & S_IRUSR)) readok = "yse"; else readok = "no"; printf("type: %s,read: %s\n,type,readok); exit(0) }
10.6 共享文件
1、内核使用三个相关的数据结构来表示打开的文件:
描述符表:每个进程都有它独立的描述符表。 每个打开的描述符表项指向文件表中的一个表项。
文件表:所有进程共享这张表。每个文件表的表项组成包括有当前的文件位置、引用计数、以及一个指向v-node表中对应表项的指针。
直到引用计数为0,内核才会删除该文件表表项。
v-node表:所有进程共享这张v-node表。 2、多个描述符可以通过不同的文件表表项来引用同一个文件。
关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
3、在内核删除相应文件表项之前,父子进程必须都关闭了它们的描述符。
10.7 I/O重定向
1、Unix外壳提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。
unix> ls > foo.txt
2、I/O重定向是依靠dup2函数工作的。
#include <unistd.h> int dup2(int oldfd,int newfd);
dup2函数拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。若newfd已经打卡了。dup2会在拷贝oldfd之前关闭newfd。
10.8 标准I/O
1、标准I/O库将一个打开的文件模型化为一个流,一个流就是一个指向FILE类型的结构的指针。每个ANSIC程序开始都有三个打开的流stdin、stdout和stderr,分别对应于标准输入、标准输出、标准错误。
#include <stdio.h> extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;
2、类型为FILE的流是对文件描述符和流缓冲区的抽象。流缓冲区的目的和RIO读缓冲区的一样,就是开销较高的Unix I/O系统调用的数量尽量能的小。
10.9 I/O函数的使用
1、应用程序可以通过open、close、lseek、read、write和stat这样的函数来访问Unix I/O。
RIO函数:read和write的健壮的包装函数,自动处理不足值,为读文本行提供一种高效的带缓冲的方法。
标准I/O函数:提供了Unix I/O函数的一个更加完整的带缓冲的替代品,包括格式化的I/O例程。是磁盘和终端设备I/O之选。
2、套接字描述符:Unix对网络的抽象是一种称为套接字的文件类型,被称为套接字描述符。应用进程通过读写套接字描述符来与运行在其他计算机上的进程通信。
3、对流I/O限制是:
(1)跟在输出函数之后的输入函数,必须在其中间插入fflush、fseek、fsetpos或者rewind函数,后三个函数使用Unix I/O中的lseek函数来重置当前的文件位置。
(2)跟在输入函数之后的输出函数,必须在中间插入fseek、fsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个EOF。
4、解决对流I/O限制的方法是:
(1)采用在每个输入操作前刷新缓存区这样的规则来满足。
(2)对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写。
5、对套接字使用lseek函数是非法的。
6、在网络套接字上,使用RIO函数更常见。
参考资料
1、《深入理解计算机系统》课本第十章
2、实验楼实验指导书:https://www.shiyanlou.com/courses/413 实验11
3、每周重点:http://group.cnblogs.com/topic/73069.html
来源:https://www.cnblogs.com/java-stx/p/4933786.html