引言
文件I/O的主要有五个常用的函数:
- open
- read
- write
- lseek
- close
本章主要介绍不带缓存的I/O,所谓不带缓存I/O是指每一个调用都是内核的一个系统调用
文件描述符
对于内核而言,所有打开的文件都是通过文件描述符引用的。文件描述符0-标准输入,1-标准输出,2-标准错误。在POSIX规范中,已经提供了STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO来替代0、1、2数字,这样更加便于开发者理解。这些常量定义在<unistd.h>
头文件中
函数open和openat
打开文件的函数如下:
#include <fctnl.h>
int open(const char* path, int oflag, ... /*mode_t mode*/)
int openat(int fd, const char* paht, int oflag, ... /*mode_t mode*/)
- path 打开或创建的文件名字
- oflag 打开的参数
- O_RDONLY 只读打开
- O_WRONY 只写打开
- O_RDWR 读写打开
- O_EXEC 只执行打开
- O_SEARCH 只搜索打开
- fd参数
- paht为绝对路径时,相当于open
- path为相对路径时,fd为相对路径名在文件系统中的开始地址
- path指定相对路径名
#define O_RDONLY 0x0000 /* open for reading only */
#define O_WRONLY 0x0001 /* open for writing only */
#define O_RDWR 0x0002 /* open for reading and writing */
#define O_ACCMODE 0x0003 /* mask for above modes */
creat函数
创建一个文件,一般不使用,用open代替
#include<fcntl.h>
int creat(const char* path, mode_t mode);
相当于
open(paht, O_WRONY | O_CREATE | O_TRUNC, mode)
函数close
关闭一个打开的文件
#include<unistd.h>
int close(int fd);
- 关闭一个文件会释放该进程加在该文件上的所有记录锁
函数lseek
当前文件偏移量,文件对写操作开始的位置。
正常非O_APPEND方式打开,偏移量会被重置为0。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
- whence 为SEEK_SET,文件偏移量为文件头开始的offset
- whence 为SEEK_CUR,文件偏移量为当前值加上offset
- whence 为SEEK_END,文件偏移量为文件长度加上offset
lseek可以形成文件空洞,实际上这个和内核实现无关,而是和文件系统相关,也就是说,允许空洞存在,如何存储空洞,都是归给文件系统的。目前大部分文件系统都是使用null填充。
#include "apue.h"
#include <fcntl.h>
char buf1[] = "abcdefghijk";
char buf2[] = "ABCDEFGHIJK";
int main()
{
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);
}
使用od命令查看
yanke@yanke-pc:~/yanke/Code/apue/ch3$ od -c file.hole
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0040000 A B C D E F G H I J
0040012
例子:判断一个标准输入能不能设置偏移量
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
if (lseek(STDIN_FILENO,0,SEEK_CUR) == 1)
printf("cannot seek\n");
else
printf("seek OK\n");
exit(0);
}
//$./a.out < /etc/passwd
//seek OK
函数read
从打开的文件读取数据
#include<unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes)
//返回值:读取到的字符数目,如果到达文件尾,返回0,如果出错,返回-1
函数wtrire
从打开的文件写入数据
#include<unistd.h>
ssize_t write(int fd, const void* buf, size_t nbytes);
//返回值:成功返回写入的字节数,出错返回-1
I/O效率
将标准输入复制到标准输出
#include "apue.h"
#include <unistd.h>
#define BUFFSIZE 4096
int main()
{
int n;
char buf[BUFFSIZE];
while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
{
if (write(STDOUT_FILENO, buf, n)!=n)
err_sys("write error");
}
if (n < 0)
{
err_sys("read error");
}
exit(0);
}
- Linux的ext4文件系统,磁盘块的大小为4096字节(4K)
- 大多数文件系统为了改善性能都使用了某种预读计数,当检测到正在顺序读取时,系统就试图读取比应用要求的更多数据。
文件共享
内核用于所用I/O的数据结构:内核使用3个数据结构表示打开的文件
1. 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表。包含文件描述符标志和指向文件表项的指针
2. 内核为所有打开文件维护一张文件表,每个文件表项包括:文件状态标志(读,写,添加,同步,阻塞等),当前文件偏移量,指向该文件的v节点。
3. 每个打开的文件有一个v节点,v节点包含文件类型和对此文件进行各种操作函数指针。对于大多数文件系统,v节点还包含i节点,这些i节点在文件打开时读入内存,包含文件的所有者,文件长度,指向文件实际数据快在磁盘所在位置的指针等。
如果两个独立的进程各自打开了同一个文件,则每个进程表指向自己的文件表项,文件表象指向相同的v节点。
原子操作
追加到一个文件
Unix提供一种原子操作,每次写操作前都要将进程的当前偏移量设置到该文件的尾端,于是每次写之前就不需要调用lseek了。
函数pread和pwrite
#include <unistd.h>
ssize_t pread(int fd, void* buf, size_t nbytes, off_t offset);
size_t pwrite(int fd, const void* buf, szie_t nbytes, off_t offset);
- 使用pread时,无法中断其定位和读操作
- 不更新当前文件偏移量
函数dup和dup2
用于复制一个文件:
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
dup执行后将会返回最小的可返回的文件描述符,dup2则是自定义文件描述符值,如果fildes2正在使用则关闭后再分配;如果fildes等于fildes2则只返回fildes2,且不关闭。
数据同步到磁盘
void sync(void);
int fsync(int fd);
int fdatasync(int fd);
- sync:将修改过的块缓冲区排入写队列然后返回
- fsync:对fd有作用,等待磁盘写操作完成才返回
- fdatasync:只影响文件的数据部分,FreeBSD系Unix实现不包含
文件控制函数
int fcntl(int fildes, int cmd, ...);
- 复制一个已存在的文件描述符(cmd=F_DUPFD、F_DUP、FD_CLOEXEC)
- 取得/设置已存在文件描述符的标志(cmd=F_GETFD、F_SETFD)
- 取得/设置已存在文件描述符的状态标志(cmd=GETFL、SETFL)
- 取得/设置异步I/O时文件的所有权(cmd=GETOWN、SETOWN+)
- 取得/设置异步锁(cmd=GETLK、SETLK、SETLKW)
cmd参数
- F_DUPFD:复制一个已存在的文件描述符,新文件描述符作为函数的返回值,其取值一般是不小于3且还没被打开的文件描述符。新旧两个文件描述符共享文件表,但是两个描述符有不同的fd标志位(进程表中每一个表项记录了一个文件描述符的fd标志位和指向文件表的指针)。使用F_DUPFD时,fd标志位的FD_CLOEXEC将被清零。
- F_DUPFD_CLOEXEC
复制一个已存在的文件描述符,与F_DUPFD不同之处在于其设置了FD_CLOEXEC。也就是在使用exec是文件被关闭。 - F_GETFD
返回文件标志位,目前文件标志位只定义了FD_CLOEXEC,因此返回值就是FD_CLOEXEC的值。 - F_SETFD
设置文件标志位(即设置FD_CLOEXEC),设置的值在第三个参数。 - F_GETFL
返回该文件描述符对应的文件状态标志位file status flag。文件状态标志位主要有O_RDONLY、O_WRONLY,本文后面进一步讨论。 - F_SETFL
设置文件状态标志。只有O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC、O_ASYNC其中状态标志可以被更改(设置)。 - F_GETOWN
取得当前正在接收SIGIO或者SIGURG信号的进程id或进程组id,进程组id返回的是负值(arg被忽略) - F_SETOWN
设置将接收SIGIO和SIGURG信号的进程id或进程组id,进程id和进程组id通过第三个参数arg传入(只能传一个,要么进程id,要么进程组id),arg为正值表示传入的是进程id,arg为负值则传入的是进程组id。 - F_GETLK
取得文件的锁定状态,如果被锁定了,则将锁定信息重写到第三个参数arg(一个指向flock的结构体的指针)。如果未被锁定状态,则除了struct flock的l_type被设置为F_UNLCK外,其他成员不变。 - F_SETLK
通过第三个参数arg(struct flock*)锁定文件。如果read lock和write lock设置失败,则返回EACCES or EAGAIN - F_SETLKW
类似F_SETLK,不同的是当设置锁发生阻塞时,它会等待(W是wait的意思),直至设置锁完成
例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
int val;
if (argc != 2)
{
printf("usage: a.out < descriptor# > \n");
exit(1);
}
if ((val=fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
{
printf("fcntl error for fd %d\n", atoi(argv[1]));
}
switch (val & O_ACCMODE)
{
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
{
printf("unkonwn access mode");
exit(1);
}
}
if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
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);
}