Unix 中关于文件的访问方式 ,总体上可以分为两类:
不带缓存的I/O函数对文件的描述符进行调用达到对文件进行操作的目的,耗费的CPU时间长;标准I/O函数围绕流对文件进行存储和调用,是一种带缓存的函数,输入的数据先存入内存中,当填满缓存或者刷新一个流时才将数据写到磁盘上
问题:为什么不带缓存的函数耗费的 CPU 时间长?
为了回答这一问题,首先需要了解缓存。
缓存:目的是尽可能的减少使用不带缓存的I/O函数,提高运行效率;对于标准I/O流来说,缓存可以进行自动的缓存管理,所以标准 I/O 无需担心选取最佳的缓存长度。
标准I/O提供了三种类型的缓存:
int open (const char*pathname,int oflag);
int creat (const char *pathname,mode_t mode);
int close(int filedes);
off_t lseek(int filedes, off_t offset, int whence);
ssize_t read(int filedes,void*buff,size_t nbytes);
ssize_t write(int filedes,const void*buff,size_t nbytes);
下来对典型函数 open 和 lseek 做一个具体的介绍;
open (const char*pathname,int oflag);
功能:打开一个文件。oflag由下面一个或者多个进行或运算构成 O_RDONLY、O_WRONLY、O_RDWR 三个中指定一个。
下列选项可选:
off_t lseek(int filedes, off_t offset, int whence);
功能:求当前文件的文件位移量。参数 offset 的解释与 whence 的取值有关:
注意:当位移量为非负值整数时,表示它是一个普通文件,但是某些设备允许位移量为负值,故判断返回值时,采用是否等于 -1 进行判断。
举例,采用 lseek 函数创建一个具有空洞的文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include “ourhdr.h”
char buf1[ ] = “abcdefghij”
char buf2[ ] = “ABCDEFGHIJ” // 定义两种字符数组
main (void)
{
int fd;
if ((fd = creat (“fifle.hole”,FIFE_MODE ))<0)
err_sys(“creat error”);
if (write( fd , buf1 , 10) != 10) //此时的文件位移量为10
err_sys(“write error”);
if (lseek ( fd , 40 ,SEEK_SET) == -1) //此时文件位移量为40
err_sys(“lseek error”);
if (write ( fd , buf2 , 10) != 10) //c此时文件位移量为50
err_sys(“buf2 write error”);
exit(0);
}
可以知道,文件 file.hole 创建成功的话前10个元素为 abcdefghij,而第11个到第39个元素为0,后10个元素为 ABCDEFGHIJ。
打开一个标准I/O流:
FILE*fopen(const char*pathname,const char *type);
FILE*freopen(const char*pathname,const char *type,FILE*fp);
FILE*fdopen(int filedes,const char*type);
流结构包含的内容:实际文件描述符,指向缓存的指针,缓存的长度,缓存中的实际字节数等。
参数 type 指定对该 I/O 流的读写方式:
| type | 说明 |
|---|---|
| r或rb | 为读而打开 |
| w或wb | 使文件长度为0,或为写而创建 |
| a或ab | 添加;为在文件尾写而打开,或为写而创建 |
| r+或r+b或rb+ | 为读和写而打开 |
| w+或w+b或wb+ | 使文件长度为0,或为读和写而打开 |
| a+或a+b或ab+ | 为文件读和写而打开或创建 |
关闭一个打开的流:int fclose(FILE*fp);
读和写流:
一次读一个字符 一次写一个字符
int getc(FILE*fp); —— int putc(intc,FILE*fp);
int fgetc(FILE*fp); —— int fputc(intc,FILE*fp);
int getchar(void); —— int putchar(int c);
一次读一行字符 一次写一行字符
char*fgets(char*buf,int n.FILE*fp); ——int fputs(const char*str,FILE*fp);
char*gets(char*buf); ——int puts(const char*str);
注意:getc 和 fgetc 的区别是 getc 可被实现为宏,而 fgetc 则不能实现为宏,调用 fgetc 所需时间很可能长于调用 getc,因为调用函数通常所需的时间长于调用宏。gets 不建议使用,因为未指明名缓存长度 n,会造成缓存越界:1988 年的因特网蠕虫事件就是由此函数导致的。
格式化输出:
int printf (const char*format ,…);
int fprintf(FILE *fp, const char *format,…);
int sprintf(char*buf, const char * format,…);
printf 将格式化数据写到标准输出;fprintf 写至指定的流;sprintf 将格式化的字符送入数组 buffer 中。
格式化输入:
int scanf (const char*format ,…);
int fscanf(FILE *fp, const char *format,…);
int sscanf(char*buf, const char * format,…);
阻塞I/O:下面介绍几种可能会使进程阻塞的情况
I/O多路转接
首先看一段程序如下:
while((n = read (STDIN_FILENO,buf,BUFSIZE)) > 0)
if(write(STDOUT_FILENO,buf,n) != n)
err_sys(write error);
此处为阻塞I/O,当此段代码读一个文件描述符然后写入另一个文件描述符时,没有任何问题,但是当要求读两个或者更多文件描述符时,就会出现其中有的文件描述符的数据得不到及时处理。
解决方法:使用非阻塞I/O,对第一个描述符进行处理,无数据立即返回,然后对第二个描述符进行处理,等待若干秒接着去执行第一个描述符,此种方式称为轮询;
缺点:浪费大量CPU时间去执行误用操作,此处两个描述符暂可行,但是若更多,则避免使用
I/O多路转接基本思想:先构造一张有关描述符的表,然后调用一个函数,它要到这些描述符中检查,若有一个描述符准备好进行I/O就返回。在返回时,它告诉进程哪一个描述符已准备好可以进行I / O。
int select(int maxfdp1, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval *tvptr);
功能:测试描述符集。返回已经准备好的描述符的数量和哪一个描述符已准备好读,写或异常。
参数说明:先看最后一个参数tvptr,此参数指向一个结构,结构定义如下:
struct timeval{
long tv_sec; //秒
long tv_usec; //毫秒
}
对于 tvptr 解释如下:
readfds、writefds 和 exceptfds 是指向描述符集的指针,这三个描述符集说明了我们关心的可读、可写或处于异常条件的各个描述符,数据类型为 fd_set。
对于数据 fd_set 可以进行的操作为(a)分配一个这种类型的变量;(b)将这种类型的一个变量赋与同类型的另一个变量;(c)对于这种类型的变量使用下列四个宏
FD_ZERO(fd_set *fdset); //清空;
FD_SET(int fd , fd_set *fdset); //添加fd;
FD_CLR(int fd, fd_set *fdset); // 删除fd;
FD_ISSET(int fd, fd_set *fdset); //检测fd;
举例:分析下列代码理解 select 函数
fd_set readset, writeset; // 定义变量
FD_ZERO (&readset); // 清空变量内容
FD_ZERO (&writeset );
FD_SET(0,&readset); // 设置 readset 第1位
FD_SET(3,&readset); // …….4…….
FD_SET(1,&writeset); // …writeset…2位
FD_SET(2,&writeset); // ……3…
select(4,&readset,&writeset,NULL,NULL);
select 第一个参数 maxfdp1 的意思是 最大f d加1。
fd0 fd1 fd2 fd3 ...
readset: 1 0 0 1
|->
writeset: 0 1 1 0
(注:maxfdp1=4)
int poll(struct pollfd fdarray[ ], unsigned long nfds, int timeout);
功能:同 select,测试描述符集。返回已经准备好的描述符的数量和哪一个描述符已准备好读,写或异常。只是调用形式不一样。
poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,此数组元素指定描述符编号和发生事件;结构如下:
struct pollfd {
int fd;
short events; //告诉内核我们关心的事件
short revents; //内核返回所发生的事件
};
nfds 说明了数组中的元素个数;
timeout 如同 select 一样,有三种不同的情形:
存储映射 I/O:存储映射I/O使一个磁盘文件与存储空间中的一个缓存相映射,当从缓存中取数据,就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。
目的:可以在不使用 read 和 write 的情况下执行 I/O。
caddr_t mmap(caddr_t addr, size_t len,int prot,int flag,int filedes,off_t off);
功能:将一个给定的文件映射到一个存储区域中。返回映射区的起始地址;
addr 参数用于指定映射存储区的起始地址,filedes指定要被映射文件的描述符,len 是映射的字节数,off 是要映射字节在文件中的起始位移量,flag参数说明映射存储区的多种属性,prot 说明存储区的保护要求。
int munmap(caddr_t addr,size_t len) ;
功能:此函数删除存储映射区。
注意:关闭文件描述符 filedes 并不解除映射区。
几种I/O处理常用函数
int dup (int filedes);
int dup2 (int filedes , int filedes2);
功能:复制文件描述符。
dup 函数返回新的文件描述符,而且是当前可用描述符的最小值; dup2 用参数 filedes2 指定新描述符的数值,若 fildes2 已经打开,则先关闭,filedes 等于 filedes2,则 dup2 返回 filedes2,而不关闭它。
注意:返回的新文件描述符与 filedes 共享同一个文件表项。
int fcntl ( int filedes, int cmd , … /*int arg */);
功能:此函数用来改变已打开文件的性质。功能依赖于 cmd:
fcntl 函数举例:对一个文件描述符打开一个或者多个文件状态标志
# include <fcntl.h>
# include”ourhdr.h”
Void
set_f1(int fd , int flags) //flags为文件状态标志
{
int val ;
if ( (val = fcntl (fd, F_GETFL, 0))<0 ) //取得文件fd的文件状态标志
err_sys(“fcntl F_GETFL error”);
val |= flag; //为文件设置多个文件状态标志
if (fcntl(fd ,F_SETFL, val)<0)
err_sys(“fcntl F_SETFL error”);
}
int ioctl(int filedes, int request, … );
功能:对文件的I/O进行多种操作,这是一个杂项函数。
例如:在 SVR4 中,使用 ioctl 可对流执行29种不同的操作,request 说明执行29中的哪一个,所有request都以I_开始。第三个参数与request有关,有时是一个整型值,有时是一个指针。
例如:使用I_CANPUT ioctl来测试由第三个参数说明的优先段是否可写,若可写,则不对流做任何改变。
# include <stropts.h>
# include <unistd.h>
int
isastream (int fd){
return (ioctl(fd, I_CANPUT, 0) != 1);
}
