您当前的位置:首页 > 计算机 > 系统应用 > Linux

Linux 文件 I/O 访问

时间:12-14来源:作者:点击数:

1 文件的访问方式

Unix 中关于文件的访问方式 ,总体上可以分为两类:

  • 以文件描述符对文件进行的存储和调用;
  • 围绕流对文件进行存储和调用。

不带缓存的I/O函数对文件的描述符进行调用达到对文件进行操作的目的,耗费的CPU时间长;标准I/O函数围绕流对文件进行存储和调用,是一种带缓存的函数,输入的数据先存入内存中,当填满缓存或者刷新一个流时才将数据写到磁盘上

问题:为什么不带缓存的函数耗费的 CPU 时间长?

为了回答这一问题,首先需要了解缓存。

缓存:目的是尽可能的减少使用不带缓存的I/O函数,提高运行效率;对于标准I/O流来说,缓存可以进行自动的缓存管理,所以标准 I/O 无需担心选取最佳的缓存长度。

标准I/O提供了三种类型的缓存:

  • 全缓存,就是在填满标准 I/O 缓存后才进行实际 I/O 操作,第一次执行 I/O 操作时,相关函数调用 malloc 获得需使用的内存。对于驻在磁盘上的文件,通常采用全缓存
  • 行缓存,在标准输入或者输出遇到新行符时,才执行实际 I/O 操作,对于涉及到终端的 I/O 操作,一般使用行缓存
  • 不带缓存,标准 I/O 对于单个字符进行不带缓存的操作。

2 不带缓存的 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 三个中指定一个。

下列选项可选:

  • O_APPEND 每次写时都加到文件的尾端
  • O_TRUNC 如果此文件存在,而且为只读或者只写成功打开,则将其截短为 0
  • O_NOCTTY 如果 pathname 指的是终端设备,则不将此设备分配作为此进程的控制终端
  • O_NONBLOCK 如果 pathname 指的是一个 FIFO,一个块特殊文件或一个字符特殊文件,则此选项为此文件的本次打开操作和后续的 I/O 操作设置非阻塞方式。
off_t lseek(int filedes, off_t offset, int whence);

功能:求当前文件的文件位移量。参数 offset 的解释与 whence 的取值有关:

  • 若 whence=SEEK_SET 将该文件的位移量设置为距文件开始处 off_set 个字节
  • 若 whence=SEEK_CUR 将该文件的位移量设置为当前位置加 offsetoffset 可为正或者负
  • 若 whence=SEEK_END 将该文件的位移量设置为文件长度加 offsetoffset 可为正或者负

注意:当位移量为非负值整数时,表示它是一个普通文件,但是某些设备允许位移量为负值,故判断返回值时,采用是否等于 -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

3 标准 I/O 函数(带缓存)

打开一个标准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 年的因特网蠕虫事件就是由此函数导致的。

4 格式化I/O

格式化输出:

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,…);

5 高级I/O:阻塞式I/O

阻塞I/O:下面介绍几种可能会使进程阻塞的情况

  • 如果数据并不存在,则读文件可能会使调用者永远阻塞
  • 如果数据不能立即被接受,则发送此数据的进程可能会永远阻塞
  • 在某些条件发生之前,打开文件会被阻塞,例如若以只写方式打开 FIFO,那么在没有其他进程已用读方式打开该FIFO时也要等待
  • 对已经加上强制性记录锁的文件进行读、写
  • 某些 ioctl 操作,和 select 调用。
  • 某些进程间通信函数

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 解释如下:

  • tvptr == NULL,永远等待,当有描述符准备好或者捕捉到信号返回
  • tvptr ->tv_sev == 0 && tvptr ->tv_usev == 0 ,完全不等待,测试所有描述符后立即返回
  • tvptr ->tv_sev != 0 || tvptr ->tv_usev != 0 ,等待指定的秒数或者微秒数,有描述符准备好或超时则返回(超时返回0)

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 一样,有三种不同的情形:

  • timeout == INFTIM,永远等待,当所指定的描述符中的一个已准备好,或捕捉到一个信号则返回
  • timeout == 0 不等待。测试所有描述符并立即返回
  • timeout > 0 等待 timeout 毫秒。

存储映射 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:

  • cmd = F_DUPFD 复制一个现存的文件描述符;新文件描述符作为函数值返回,新描述符有自己的一套文件描述符标志
  • cmd = F_GETFD或F_SETFD 获得/设置文件描述符标记(FD_CLOEXEC);
    设置时一般将此标记设置为0(exec不关闭)或者1(exec关闭)
  • cmd = F_GETFL或F_SETFL 获得/设置文件状态标志;文件状态标志:
    只读,只写,读/写;一般设置为:非阻塞,等待写完成,写时添加至文件尾等等
  • cmd = F_GETOWN 或 F_SETOWN 获得/设置异步I/O有权;取得或者设置接受信号SIFIO和SIGURG信号的进程ID或者进程组ID
  • cmd = F_GETLK,F_SETLK或F_SETLKW 获得/设置记录锁;

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);
}
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐