系统级I/O——(UNIX I/O 和C库标准I/O)

本文详细介绍了Unix系统I/O模型接口,包括基本IO接口、文件系统关联模型、重定位及fd的复制。深入讲解了open函数的flag参数、mode参数以及文件打开后的处理方式。此外,讨论了Unix系统IO的其他接口函数,如文件定位、分配输入和集中输出等。同时,对比分析了C语言标准I/O库,包括FILE结构、标准IO的接口函数和使用限制。最后,探讨了系统IO缓冲模型,包括内核高速缓存区和FILE结构中的缓冲区管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

一、Unix系统I/O模型接口

1、基本IO接口
  • 基本IO函数包含:建立(切断)与文件的连接(open、close);从文件中读取到内存read,从内存写入文件write;以及文件中定位lseek函数。
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
/* 打开文件,获得file descriptor;其中flag为访问模式,mode只有在O_CREAT时,才有效,表示文件
本身的访问权限设置。*/
int open(const char* filename, int flag, mode_t mode);
#include<unistd.h>
int close(int fd);

ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);

/* lseek是文件定位函数,whence是当前位置,有SEEK_SET, SEEK_CUR和 SEEK_END三个常量,
表示从开始、当前和结尾位置来定位,offset表示相对为whence的偏移量。 */
void lseek(int fd, off_t offset, int whence);
1.1、open函数的说明:
1.1.1、flag参数
  • 读写权限:O_RDONLY \ O_WRONLY \ O_RDWR
  • 文件默认定位:O_TRUNC \ O_APPEND;定义写入位置是截断还是每次写操作前都定位到文件结尾处。
  • 创建:O_CREAT 如果文件不存在,创建一个空文件,默认为O_TRUNC;可以设置为O_APPEND,此时需要定义mode参数,来作为新建文件的属性
  • 其他含有很多与设置同步性、阻塞、更新有关的flags,后面讲到相关的,再介绍。
1.1.2、mode参数

Unix中文件权限分为:usr(使用者、拥有者)、grp(使用者所在组的成员)、oth(其他任何成员)的权限,而权限本身又分为读、写、执行三种;组合共9种。

  • S_IRUSR \ S_IWUSR \ S_IXUSR
  • S_IRGRP \ S_IWGRP \ S_IXGRP
  • S_IROTH \ S_IWOTH \ S_IXOTH

每个进程都有一个umask,它是通过调用umask函数来设置的,文件的访问权限会被设置为:mode & ~umask;常用的umask 为 S_IWGRP | S_IWOTH。

1.1.3、文件打开后处理flag的方式
  • 基本的接口函数
#include<fcntl.h>
int fcntl(int fd, int cmd, ...);
  • 常用cmd:F_GETFL(此时…为空) 和 F_SETFL(此时…为flags);
  • F_SETFL可使用的flags:O_APPEND O_NONBLOCK O_NOATIME O_ASYNC O_DIRECT
  • 基本用法,先用F_GETFL得到原来的flags,然后用F_SETFL设置我们想用的flags,完成操作后,设置回原来的flags。
2、文件系统关联模型、重定位及fd的复制

下图描述了进程级fd、系统级打开文件的File Table及磁盘文件系统的i-node文件之间的关系:
文件系统和fd关系模型图片来源于网络,如有侵权,请联系删除
说明:

  1. 使用open函数,打开同一个文件上;系统不仅创建一个文件描述符表项,还创建了一个File Table 表项;此时两个file Table表项引用同一个文件,而两个File Table的文件偏移量,及其他File Table定义的内容是独立的。
  2. 我们使用dup函数,或是子进程继承父进程时;是两个fd表项指向同一个File Table表项;两个fd共享File Table中的内容。
2.1、文件描述符的内容
  • FD_CLOEXEC标志位:在子进程加载新的程序时,关闭父进程的fd;
  • 对系统级file table的引用
2.2、系统级File Table的内容
  1. 文件偏移量offset:lseek修改的就是这个值;
  2. 访问模式、文件状态标志、与信号驱动IO相关的设置:也就是通过flags来设置的
  3. 对i-node的引用
2.3、文件系统i-node表的内容
  1. struct stat设置的文件属性;
  2. 一个指针指向该文件所持有的锁的列表
2.3.1、文件元数据
  • 获取文件元数据的函数及元数据结构体的内容如下:
#include<unistd.h>
#include<sys/stat.h>

int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf );
/* 在处理符号链接时,不展开链接,而是将符号链接本身作为一个文件来处理 */
int lstat(const char *filename, struct stat *buf );

struct stat
{
  dev_t st_dev;   /* Device */
  ino_t st_ino;   /* inode  */
  mode_t st_mode; /* Protection and file type */
  nlink_t st_nlink; /* Number of hard links */
  uid_t st_uid;     /* User ID of owner */
  gid_t st_gid;     /* Group ID of owner */
  dev_t std_rdev;   /* Device type (if inode device) */
  off_t st_size;    /* Total size, in bytes */
  size_t st_blksize;  /* Block size for filesystem IO */
  size_t st_blocks;   /* Numbers of blocks allocated */
  time_t st_atime;    /* Last access time */
  time_t st_mtime;    /* Last modification time */
  time_t st_ctime;    /* Time of the struct stat changed */
};
2.4、重定位和复制文件描述符
2.4.1、重定位操作符 >
  • ls > foo.txt:STDOUT_FILENO绑定到 foo.txt
  • 2 > &1: STDERR_FILENO绑定到STDOUT_FILENO
2.4.2、dup函数
#include<unistd.h>

/*返回最小的未使用的fd引用oldfd引用的文件,并返回此fd */
int dup(int oldfd);

/* 如果oldfd无效,返回EBADF,不关闭newfd引用的文件;
oldfd == newfd不作处理;处理过程:先关闭newfd,然后使newfd引用oldfd引用的文件,
返回newfd;会忽略关闭newfd的错误。*/
int dup2(int oldfd, int newfd);

#define _GNU_SOURCE
/* 支持设置newfd的FD_CLOEXEC标志位 */
int dup3(int oldfd, int newfd, int flag);
2.4.3、通过fcntl函数实现复制文件描述符
/* newfd要大于等于startfd;可以通过改用F_DUPFD_CLOEXEC(cmd标志)来实现
复制过程同时设置FD_CLOEXEC标志 */
newfd = fcntl(oldfd, F_DUPFD, startfd);
3、Unix系统IO的其他接口函数
3.1、处理未纳入标志IO模型的设备和文件
#include<sys/ioctl.h>
/* request指定在fd上的操作,具体设备的头文件定义了可以传递给request的变量*/
int ioctl( int fd, int request, ... /* argp */);
3.2、文件定位与输入输出结合
#include<unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, void *buf, size_t count, off_t offset);
  • 这两个函数的作用就是把读写与定位在同一个原子操作内完成
3.3、分配输入和集中输出
#include<sys/uio.h>

ssize_t readv(int fd, struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

struct iovec
{
  void *iov_base; /* Start address of buf */
  size_t iov_len; /* Number of bytes to transfer to/from buffer */
};

使用这个函数的步骤:

  1. 定义一个 struct iovec的数组,每个数组元素,就对应这一块内存空间;
  2. 用我们实际使用内存空间的地址和大小来初始化该结构体的元素;
  3. 调用该函数实现分配输入和集中输出。
  4. 如果大小定义有问题:读入操作,先读满前面的元素,再读后面的元素;最后一个元素的可能存在不足值的问题。
3.4、截断文件
#include<unistd.h>

int truncate(const char *pathname, offset length);
int ftruncate( int fd, offset length);
  • 文件长度大于length,丢弃超出的部分;
  • 文件长度小于length,在文件结尾与length之间插入空字节;但如果文件在此处没有实际写入内容,则在完成函数调用后,不会对文件产生实际的影响。
3.5、O_NONBLOCK标识
  1. 未能立即打开,则返回错误;但FIFO可能会阻塞;
  2. 打开文件后,保证后续操作不阻塞,如发生阻塞则报错;
  3. 此标志主要针对管道、FIFO、嵌套字、设备等,普通磁盘文件的IO没有阻塞的问题,也就没有此标识
3.6、文件名删除与临时文件
3.6.1、文件名删除
#include <unistd.h>

int unlink(const char *pathname);
int rmdir(const char *pathname);

#include <fcntl.h>           /* Definition of AT_* constants */
#include <unistd.h>

int unlinkat(int dirfd, const char *pathname, int flags);

#include <stdio.h>

int remove(const char *pathname);

说明:

  1. rmdir函数:删除一个目录;
  2. unlink函数:从文件系统删除一个名字;
    • 如果pathname是最后一个指向文件的链接,而文件没有被打开,那么同时删除该文件。
    • 如果pathname是最后一个指向文件的链接,而文件被进程打开了,那么先删除文件名字,当文件关闭后,文件自动删除。
    • 如果pathname是一个符号链接,直接删除。
    • 如果pathname指向socket、FIFO或终端,那么文件名被删除,使用它的进程可以继续使用它。
  3. unlinkat函数:如果flags是AT_REMOVEDIR,等同于rmdir,否则等同于unlink;
    • 一个不同:如果pathname是相对路径,那么它是相对于dirfd的路径,而不是系统默认的进程运行的相对路径;绝对路径,则dirfd被忽略。
  4. remov函数:根据pathname是文件名还是路径,来调用unlink或rmdir。
3.6.2、临时文件的创建
#include <stdlib.h>
int mkstemp(char *template);

#include<stdio.h>
FILE* tmpfile(void);
  • mkstemp函数:返回打开文件的文件描述符,template后六位是:XXXXXX,系统会随机分配;我们可以在调用后通过template来得到它的值;这样也就违背了临时文件的属性,所以,我们在调用成功后,要手动调用unlink来删除template。
  • tmpfile函数的封装性更好一点:直接在内部调用remove函数来删除文件名;不需要我们手动操作,关闭文件流后,文件自动删除。

二、Robust I/O 接口

1、read、write传送的不足值问题
  1. read遇到EOF时;
  2. 从终端读取文本行时;每个read函数一次传送一个文本行,返回的不足值等于文本行的大小
  3. 读和写网络嵌套字(socket)时;内部缓冲约束和网络延迟可能造成不足值的问题;
  4. 对Linux管道调用read和write时,也可能出现不足值的问题,这是由进程间通信机制造成的
2、Robust I/O包介绍
  • 通过反复调用read和write来处理不足值的问题;
  • 提供无缓冲的输入输出函数:这些函数直接在内存和文件之间传递数据;
  • 提供带缓冲的输入函数:使用类似标准IO的应用级缓冲区,来缓存读取的数据。
3、Robust I/O包的代码
  • 头文件Robust_IO.h
#ifndef ROBUST_IO_H
#define ROBUST_IO_H
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include"tlpi_hdr.h"

#define RIO_BUFSIZE 8192
 
typedef struct 
{
  int rio_fd; /* Descriptor for this internal buf */
  int rio_cnt;  /* Unread bytes in internal buf */
  char *rio_bufptr; /* Next unread byte in internal buf */
  char rio_buf[RIO_BUFSIZE];  /* Internal buffer */
} rio_t;

/* read and write without buf */
ssize_t rio_readn(int fd, void *usrbuf, size_t n );
ssize_t rio_writen(int fd, void *usrbuf, size_t n );

/* read with buf */
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 );

#endif
  • Robust_IO.c
#include "Robust_IO.h"

/* read and write without buf */
ssize_t rio_readn(int fd, void *usrbuf, size_t n) {
  ssize_t nleft = n;
  ssize_t nread;
  char *bufp = (char *)usrbuf;
  while (nleft > 0) {
    if ((nread = read(fd, bufp, nleft)) < 0) {
      if (errno == EINTR)
        nread = 0; /* Interrupted by sig handler return and call read() again */
      else {       /* errno set by read()  */
        errMsg("read fault");
        return -1;
      }
    } else if (nread == 0) {
      break; /* EOF */
    }
    nleft -= nread;
    bufp += nread;
  }
  return (n - nleft); /* Return >= 0 */
}
ssize_t rio_writen(int fd, void *usrbuf, size_t n) {
  ssize_t nleft = n;
  ssize_t nwriten;
  char *bufp = (char *)usrbuf;

  while (nleft > 0) {
    if ((nwriten = write(fd, bufp, nleft)) <= 0) {
      if (errno ==
          EINTR) /* Interrupted by sig handler return and call write() again */
        nwriten = 0;
      else /* errno set by write() */
      {
        errMsg("write fault");
        return -1;
      }
    }
    nleft -= nwriten;
    bufp += nwriten;
  }

  return n;
}

/* read with buf */

/* Private function used by rio_readlineb() and rio_readnb()  */
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) {
  ssize_t nread;
  if (rp->rio_cnt <= 0) {
    while ((rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, RIO_BUFSIZE)) < 0) {
      if (errno != EINTR) /* Interrupted by sig hander */
      {
        errMsg("read fault");
        return -1;
      } else if (rp->rio_cnt == 0) /* EOF */
        return 0;
      else {
        rp->rio_bufptr = rp->rio_buf; // Reset buffer ptr
      }
    }
  }

  nread = n;
  if (rp->rio_cnt < n)
    nread = rp->rio_cnt;

  memcpy(usrbuf, rp->rio_bufptr, nread);
  rp->rio_bufptr += nread;
  rp->rio_cnt -= nread;
  return nread;
}
/* init rio_t struct with no data in it */
void rio_readinitb(rio_t *rp, int fd) {
  rp->rio_fd = fd;
  rp->rio_cnt = 0;
  rp->rio_bufptr = rp->rio_buf;
}
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) {
  ssize_t nleft = maxlen - 1;
  ssize_t nread;
  char c;
  char *bufptr = (char *)usrbuf;
  while (nleft > 0) {
    if ((nread = rio_read(rp, &c, 1)) == 1) {
      *bufptr++ = c;
      if (c == '\n') {
        break;
      }
      --nleft;
    } else if (nread == 0) /*这种情况书上做了分类,没读到数据或数据读到的不足,
      但实际上,通过返回值,可以看的出来 */
    {
      break;
    } else {
      *bufptr = '\0';
      return -1;
    }
  }
  *bufptr = '\0';
  return (maxlen - nleft - 1);
}
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) {
  ssize_t nleft = n;
  ssize_t nread;
  char *bufptr = (char *)usrbuf;
  while (nleft > 0) {
    if ((nread = rio_read(rp, bufptr, nleft)) <= 0) {
      if (nread < 0)
        return -1;
      else {
        break;
      }

    } else {
      nleft -= nread;
      bufptr += nread;
    }
  }
  return (n - nleft);
}

三、C语言标准I/O库

1、 FILE结构——对fd的封装
struct _iobuf {
        char *_ptr;   //文件输入输出的下一个位置
        int   _cnt;  //当前缓冲区剩余为读写的字符
        char *_base;  //指基础位置(即是缓冲区的开始位置)
        int   _flag;  //文件标志
        int   _file;   //文件描述符fd
        int   _charbuf;  //备用缓冲区,如果缓冲区_base申请失败,数据缓冲在这四个字节
        int   _bufsiz;   //缓冲区的大小
        char *_tmpfname;  //临时文件名
        };
typedef struct _iobuf FILE;

说明:

  1. 跟上面的Robust IO类似,FILE结构需要定义一个缓冲区_base和_bufsiz确定了这个缓冲区;而_ptr和_cnt定义了缓冲区的当前状态;_flag记录返回的文件描述符;
  2. _flag,用来记录需要传递给系统调用open函数的flags标志;此处也显示了它与Robust IO的不同,就是它是直接通过fopen返回FILE*的,而不是用open返回fd,再初始化结构体的方式。
  3. _charbuf:是用来处理缓冲区申请失败的备用缓冲区
  4. _tmpfname:用来处理临时文件的问题
    多数与C语言输入输出相关的函数在<stdio.h>中定义(C++中的)。
2、标准IO的接口函数汇总
2.1、文件访问
主要包含文件打开、关闭;此处使用C语言定义的字符串来表示flags
  • fopen
  • freopen
  • fclose
2.2、用户级缓存的管理
  • fflush:刷新缓存区;读操作丢弃缓存区剩余内容,下次读取,重新填充缓存区;写操作,调用系统调用write,将缓存区的内容写入内核;
  • setbuf/setvbuf:定义缓存区大小及缓存方式;
2.3、二进制输入/输出
类似于系统调用read和write;直接进行进程内存和FILE缓存区的数据交互
  • fread
  • fwrite
2.4、非格式化输入/输出
单个字符和一行字符输入输出,多用于文本输入输出;单个字符输入输出也可用于二进制数据
  • fgetc/getc
  • fputc/putc
  • ungetc
  • fgets
  • puts
2.5、格式化输入/输出
针对文本输入输出;含有12个类似版本:通过不定参数va_list传递参数的版本及限定最大字符数量的版本;
定义一组格式限定字符;格式限定字符要与数据类型相匹配
  • scanf/fscanf/sscanf
  • printf/fprintf/sprintf
2.6、文件定位
在主流的x86_64系统上,fseek和ftell使用64位的long类型来定义off_t类型,使得fgetpos和fsetpos函数没有用武之地
  • ftell
  • fseek
  • fgetpos
  • fsetpos
  • rewind
2.7、错误处理
测试文件流错误:EOF或其他;打印错误信息
  • feof
  • ferror
  • perror
2.8、文件操作
删除文件名、文件重命名和创建临时文件
  • remove
  • rename
  • tmpfile
2.9、stdio.h具体内容的链接

C语言stdio.h的具体内容

2.10、标准IO使用限制
2.10.1、在同一个流上执行输入和输出的限制
造成问题的关键就是:输入和输出公用缓存区,而两者使用缓存区的方式不同。
  1. 跟在输出函数之后的输入函数:中间必须插入fflush函数或文件定位函数;
  2. 跟在输入函数之后的输出函数:中间必须插入对文件定位函数的调用;除非该输入函数遇到了EOF(此时_cnt为0,执行写操作时,会自动重新填充缓冲区,所以没有问题)。
2.10.2、其他限制
  1. 对网络嵌套字的IO,不要使用,应该使用Robust IO;
  2. 专门应用与文本的输入,不要用于二进制文件的输入

四、系统IO缓冲模型

  • 读数据的模型
内核磁盘读操作
系统调用read
标准IO中调用read
标准IO的读入函数
磁盘或磁盘自带高速缓存区
内核高速缓存区
用户数据
FILE结构中的buf缓存区
  • 写数据的模型
内核磁盘写操作
系统调用write
标准IO中调用write
标准IO的输出函数
内核高速缓存区
磁盘或磁盘自带高速缓存区
用户数据
FILE结构中的buf缓存区
1、内核高速缓存区
内核高速缓存区的建立是利用数据访问的局部性,减少磁盘访问的次数;从而提供程序性能。
但缓存区的存在带来了数据不同步的问题。
1.1、内核缓冲同步的概念
主要针对磁盘写数据
  • 同步文件更新Synchronized I/O file intergrity completions:将需要写入磁盘的数据,及其文件对应的所有元数据,都在磁盘上更新;
  • 同步数据更新Synchronized I/O data intergrity completions:数据传入磁盘,用于获取数据的所有数据在磁盘上更新;不影响数据读出的元数据不需要更新(如文件大小没有改变,文件的元数据不要更新,只需将文件更改的内容更新,就不影响下次访问文件数据)
  • 同步数据更新,更新的数据更少,所以更高效;用于对元数据中不影响文件读取的数据更新及时性要求不高的场合。
1.2、文件同步的系统调用
#include<unistd.h>

/*同步文件更新*/
int fsync(int fd);
/*同步数据更新*/
int fdatasync(int fd);
/*会使包含更新文件信息的所有内核缓冲区都刷新,针对系统打开的所有文件,所有处理时间较长;Linux实现,完成刷新后返回;而SUSv3只是调用一下IO传递,直接返回,不保证完成*/
int sync(int fd);
1.3、文件同步的flags
  • O_SYNC:使fd保证每次读操作都实现同步文件更新
  • O_DSYNC:使fd保证每次写操作都实现同步数据更新
  • O_RSYNC:与前面的两个flags结合使用,在读操作前使前面的写操作同步,Linux尚未实现
1.4 直接IO O_DIRECT

使用flags(O_DIRECT)来调用open函数,得到的fd在读写操作时,是不使用内核缓存区的,这样就造成了访问效率上的降低,因而对使用有一些限制:

  1. 用于传递数据的缓冲区,其内存边界必须为块大小的整数倍;
  2. 数据传输的开始点,亦即文件或设备的偏移量必须为块的整数倍;
  3. 待传递数据的长度必须为块大小的整数倍。
2、FILE结构中的缓冲区
该缓冲区的作用是为了减少系统调用,从而提高程序效率
  • 管理内存区的标准函数
#include<stdio.h>
/* 其中mode可取:_IONBF、_IOLBF、_IOFBF */
int setvbuf(FILE* stream, char *buf, int mode, size_t size);

/*其中buf为NULL或者大小为BUF_SIZ的缓冲区	*/
int setbuf(FILE *stream, char *buf);

/* 参数为NULL,则刷新所有流的缓冲区 */
int fflush(FILE *stream);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值