文章目录
一、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文件之间的关系:
图片来源于网络,如有侵权,请联系删除
说明:
- 使用open函数,打开同一个文件上;系统不仅创建一个文件描述符表项,还创建了一个File Table 表项;此时两个file Table表项引用同一个文件,而两个File Table的文件偏移量,及其他File Table定义的内容是独立的。
- 我们使用dup函数,或是子进程继承父进程时;是两个fd表项指向同一个File Table表项;两个fd共享File Table中的内容。
2.1、文件描述符的内容
- FD_CLOEXEC标志位:在子进程加载新的程序时,关闭父进程的fd;
- 对系统级file table的引用
2.2、系统级File Table的内容
- 文件偏移量offset:lseek修改的就是这个值;
- 访问模式、文件状态标志、与信号驱动IO相关的设置:也就是通过flags来设置的
- 对i-node的引用
2.3、文件系统i-node表的内容
- struct stat设置的文件属性;
- 一个指针指向该文件所持有的锁的列表
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 */
};
使用这个函数的步骤:
- 定义一个 struct iovec的数组,每个数组元素,就对应这一块内存空间;
- 用我们实际使用内存空间的地址和大小来初始化该结构体的元素;
- 调用该函数实现分配输入和集中输出。
- 如果大小定义有问题:读入操作,先读满前面的元素,再读后面的元素;最后一个元素的可能存在不足值的问题。
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标识
- 未能立即打开,则返回错误;但FIFO可能会阻塞;
- 打开文件后,保证后续操作不阻塞,如发生阻塞则报错;
- 此标志主要针对管道、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);
说明:
- rmdir函数:删除一个目录;
- unlink函数:从文件系统删除一个名字;
- 如果pathname是最后一个指向文件的链接,而文件没有被打开,那么同时删除该文件。
- 如果pathname是最后一个指向文件的链接,而文件被进程打开了,那么先删除文件名字,当文件关闭后,文件自动删除。
- 如果pathname是一个符号链接,直接删除。
- 如果pathname指向socket、FIFO或终端,那么文件名被删除,使用它的进程可以继续使用它。
- unlinkat函数:如果flags是AT_REMOVEDIR,等同于rmdir,否则等同于unlink;
- 一个不同:如果pathname是相对路径,那么它是相对于dirfd的路径,而不是系统默认的进程运行的相对路径;绝对路径,则dirfd被忽略。
- 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传送的不足值问题
- read遇到EOF时;
- 从终端读取文本行时;每个read函数一次传送一个文本行,返回的不足值等于文本行的大小
- 读和写网络嵌套字(socket)时;内部缓冲约束和网络延迟可能造成不足值的问题;
- 对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;
说明:
- 跟上面的Robust IO类似,FILE结构需要定义一个缓冲区_base和_bufsiz确定了这个缓冲区;而_ptr和_cnt定义了缓冲区的当前状态;_flag记录返回的文件描述符;
- _flag,用来记录需要传递给系统调用open函数的flags标志;此处也显示了它与Robust IO的不同,就是它是直接通过fopen返回FILE*的,而不是用open返回fd,再初始化结构体的方式。
- _charbuf:是用来处理缓冲区申请失败的备用缓冲区
- _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具体内容的链接
2.10、标准IO使用限制
2.10.1、在同一个流上执行输入和输出的限制
造成问题的关键就是:输入和输出公用缓存区,而两者使用缓存区的方式不同。
- 跟在输出函数之后的输入函数:中间必须插入fflush函数或文件定位函数;
- 跟在输入函数之后的输出函数:中间必须插入对文件定位函数的调用;除非该输入函数遇到了EOF(此时_cnt为0,执行写操作时,会自动重新填充缓冲区,所以没有问题)。
2.10.2、其他限制
- 对网络嵌套字的IO,不要使用,应该使用Robust IO;
- 专门应用与文本的输入,不要用于二进制文件的输入
四、系统IO缓冲模型
- 读数据的模型
- 写数据的模型
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在读写操作时,是不使用内核缓存区的,这样就造成了访问效率上的降低,因而对使用有一些限制:
- 用于传递数据的缓冲区,其内存边界必须为块大小的整数倍;
- 数据传输的开始点,亦即文件或设备的偏移量必须为块的整数倍;
- 待传递数据的长度必须为块大小的整数倍。
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);