文件I/O
引言
本章描述的函数经常被称为不带缓冲的I/O,先说明可用的文件I/O函数,然后,进一步讨论在多个进程间如何共享文件,以及所涉及的内核有关数据结构,最后,将说明dup、fcntl、sync、fsync和ioctl函数。
文件描述符
内核通过文件描述符引用打开的文件,并且规定,文件描述符0预留给标准输入,1预留给标准输出,2预留给标准错误,为了提高可读性,习惯用符号常量STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO表示,文件描述符的变化范围是0~OPEN_MAX-1,允许每个进程最多打开的文件数量为63。
函数open、openat和create
用函数open、openat打开或可能创建一个文件,函数create创建一个新文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
1. pathname是要打开或创建 文件的名字。
文件名和路径名截断
以下示例演示用pathconf函数到底是截断过长的文件名还是出错
#include <limits.h>
#include <unistd.h>
#include <stdio.h>
#define PATH "/home/apue.3e/fileio"
char *info[_POSIX_NAME_MAX] = {
"name is trunc!",
"generates an error!",
NULL
};
int main(void)
{
long int filenameLimit = pathconf(PATH,_PC_NAME_MAX);
long int pathLimit = pathconf(PATH,_PC_PATH_MAX);
long int trucFlag = pathconf(PATH,_PC_NO_TRUNC);
printf("the maximum length of a filename in the directory:%ld\n"
"the maximum length of a relative pathname:%ld\n"
"trucFlag:%s\n", filenameLimit,pathLimit,(trucFlag==0) ? info[0]:info[1]);
return 0;
}
运行该示例,结果如下:
在我的linux系统环境下面,当文件名过长时候,目录行为是“产生错误”,
2. open和openat函数返回的文件描述符一定是最小的未用的值
3. flags参数指定文件访问模式 :O_RDONLY,O_WRONLY, or O_RDWR
4. creat函数创建一个新文件 (以只写方式)
函数close
close函数关闭一个文件,会释放该进程加在该文件上的所有记录锁。一个进程终止时,内核自动关闭它所有打开的文件。
函数lseek
- 当前文件偏移量,通常是一个非负值,给出读写文件的位置,也有为负值的例外
- lseek函数显式的为一个打开文件设置偏移量
- lseek函数返回新的文件偏移量,对于网络套接字,管道或FIFO而言,返回-1,不支持设置偏移量
以下示例是测试标准输入是否可设置偏移量
#include "apue.h"
int
main(void)
{
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("cannot seek\n");
else
printf("seek OK\n");
exit(0);
}
运行结果
以下示例程序创建一个具有空洞的文件
#include "apue.h"
#include <fcntl.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int
main(void)
{
int fd;
if ((fd = creat("file.hole", FILE_MODE)) < 0)
err_sys("creat error");
if (write(fd, buf1, 10) != 10)
err_sys("buf1 write error");
/* offset now = 10 */
if (lseek(fd, 16384, SEEK_SET) == -1)
err_sys("lseek error");
/* offset now = 16384 */
if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
/* offset now = 16394 */
exit(0);
}
运行该示例,其结果如下:
文件中间的30个未写入字节都被读成0
函数read
从打开文件中读数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
count参数 指定想要从打开文件中读多少字节数据,read函数返回实际从文件中读到的字节数据,实际读到的数据一定是不大于count指定值的。出错的时候,read函数返回-1.
函数write
向打开文件写数据
ssize_t write(int fd, const void *buf, size_t count);
返回值通常和count相同
以下是一个读写文件的测试程序,仅供参考!
void test_RDWR(void)
{
char buf[BUF_SIZE]={};
ssize_t ret = 0;
int fd = 0;
fd = open(FILE_NAME, O_RDWR);
if (-1 == fd)
{
perror("open errors!");
exit(0);
}
ret = read(fd, buf, BUF_SIZE-1);
if (-1 == ret)
{
perror("read errors!");
exit(0);
}
printf(" the number of bytes read:%d\n"
"bytes read:%s\n",ret,buf);
ret = write(fd, buf, BUF_SIZE - (ssize_t)1);
if (-1 == ret)
{
perror("write errors!");
exit(0);
}
printf(" the number of bytes read:%d\n",ret);
fclose(fd);
}
I/O的效率
文件共享
Unix系统支持在不同进程间共享打开文件,其内核用3种数据结构表示打开文件
- 进程表项包含一张打开文件描述符表,其成员文件描述符标志和指向一个文件表项的指针
- 文件表项,其成员是文件状态标志,当前文件偏移量和指向该文件V节点表项的指针
- V节点表项,其成员是文件类型,对此文件进行各种操作函数的指针和该文件的i节点
下图表示了这三种数据结构的关系
两个独立进程各自打开同一文件的情形
从上图可以看出,对一个给定的文件只有一个v节点表项,而每个进程都获得各自的一个文件表项,这样可以使得每个进程都有它自己对该文件的当前偏移量。所以当多个进程写同一文件时,可能会产生预想不到的结果,为了避免这种结果,我们需要考虑到原子操作。
多个文件描述符项指向同一文件表项的情形,如dup函数,fork函数等。
原子操作
原子操作指的是由多步组成的一个操作。
- 追加到一个文件
考虑两个独立的进程A和B都对同一文件进行追加写操作的情形,操作是分两步(先定位到文件尾端,然后写),如果进程A先定位到文件尾端,内核切换到进程B,进程B定位到文件尾端,完成写后返回到进程A,进程A写就会覆盖先前进程B写的数据。这便产生了不是我们预料的结果。 - 函数pread和pwrite
这两个函数实现了原子地定位并执行I/O
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
- 创建一个文件
函数dup和dup2
赋值一个现有的文件描述符
#include <unistd.h>
int dup(int oldfd);//返回的一定是当前可用文件描述符中的最小数值
int dup2(int oldfd, int newfd);//newfd指定了新文件描述符的值
执行dup(1)后的内核数据结构
我们看到了两个文件描述符指向同一文件表项。
f’cntl函数也能复制一个文件描述符,有如下等效式
dup(fd) 等同于 fcntl(fd, F_DUPFD, 0)
dup2(fd,fd2)等同于close(fd2) fcntl(fd, F_DUPFD, fd2)但是dup2()是原子操作。
函数sync、fsync和fdatasync
这几个函数保证磁盘上实际文件系统和缓冲区内容的一致性
#include <unistd.h>
void sync(void);//不等实际写磁盘操作结束
int fsync(int fd);//会等实际写磁盘操作结束
int fdatasync(int fd);//只影响文件的数据部分
函数fcntl
可改变已经打开文件的属性,
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
以下示例把第一个参数指定为文件描述符,并对该描述符打印其所选择的文件标志说明
#include "apue.h"
#include <fcntl.h>
int
main(int argc, char *argv[])
{
int val;
if (argc != 2)
err_quit("usage: a.out <descriptor#>");
if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)//F_GETFL 读文件描述符标志。
err_sys("fcntl error for fd %d", atoi(argv[1]));
switch (val & O_ACCMODE) { //O_ACCMODE取出文件状态标志的前两位
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unknown access mode");
}
if (val & O_APPEND) //在每次写之前,都将标志位移动到文件的末端
printf(", append");
if (val & O_NONBLOCK) //人为 的设置读写操作为非阻塞方式
printf(", nonblocking");
if (val & O_SYNC) //每次write系统调用后都等待实际的物理I/O完成
printf(", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif
putchar('\n');
exit(0);
}
运行该程序结果如下:
5<>temp.foo表示在文件描述符5上打开以供读写
/dev/tty 表示当前控制台,就是当前进程控制台的设备文件,输入命令“tty”可查看当前映射终端,
以下示例是对一个文件描述符设置一个或多个文件状态标志,
#include "apue.h"
#include <fcntl.h>
void
set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl F_GETFL error");
val |= flags; /* turn on flags */
if (fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl F_SETFL error");
}
调用set_fl(STDOUT_FILENO, O_SYNC)就能开启同步写标志,实现write等待数据已经写到磁盘上再返回。
函数ioctl
该函数实现其他I/O操作,每个设备驱动程序可定义它自己专用的一组ioctl命令,系统则为不同种类的设备提供通用的ioctl命令。
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
/dev/fd
打卡文件“/dev/fd/n” 等效于复制描述符n