Unix系统中大多数文件I/O只需用到五个函数:open、read、write、lseek、close。本章介绍的I/O是不带缓冲的,即:每个read和write都调用内核中的一个系统调用。它们不是ISO C的组成部分。
对于内核而言,所有打开的文件都通过文件描述符引用;
当打开或创建文件时,内核向进程返回一个文件描述符;
读写文件时,文件描述符将作为read和write的参数。
在 unistd.h中定义三个标准的文件描述符:
STDIN_FILENO (0) 标准输入
STDOUT_FILENO (1) 标准输出
STDERR_FILENO (2) 标准出错输出
文件描述符的变化范围是0~OPEN_MAX, 大多数系统的OPEN_MAX为63。
具体函数描述:在 fcntl.h 头文件下。
int open(const char *path, int oflag, ... ); //打开或者创建一个文件
oflag参数可用来说明此函数的多个选项,一个或多个常量通过“或”运算构成oflag。
必须的参数: O_RDONLY, O_WRONLY, O_RDWR,这三个必须选一个。
此外还有其他可选的参数,这里不一一列举。
int creat(const char *path, mode_t mode); //创建一个文件
int close(int fildes);//关闭一个打开的文件
off_t lseek(int fildes, off_t offset, int whence); //为打开的文件设置偏移量
whence可选:SEEK_SET, SEEK_CUR, SEEK_CUR,分别表示绝对偏移量,相对于当前位置的偏移量,相对文件尾端的偏移量。
通常偏移量是一个非负整数,但是对有些设备来说可以是负的,所以为安全起见,对返回值不要测试它是否小于0,而要测试它是否等于-1。
当文件偏移量大于文件的长度时,对文件的下一次写将加长该文件,并在文件中构成一个空洞(没有写过的字节被读为0),文件的空洞并不占用磁盘的存储区。
ssize_t read(int fd, void *buf, size_t count);
返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1。
ssize_t write(int fildes, const void *buf, size_t nbyte);
返回值:若成功则返回已写的字节数,若出错则返回-1。
系统一般会采用预读技术以改善性能(实际读入的数据比应用程序要求的多)。
一个综合的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
char *filename = ".//file";
char buf[100];
int fd;
memset(buf,0,100);
printf("Open file to write\n");
//打开文件,不存在则新建
if((fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
{
perror("Cannot open file\n");
exit(1);
}
printf("Open file successfully.\n");
printf("Input a string: ");
gets(buf);
//写入文件
write(fd,buf,strlen(buf));
close(fd);
printf("Open file to read.\n");
if((fd = open(filename, O_RDONLY)) == -1)
{
perror("Cannot open file\n");
exit(1);
}
//从文件中读取
read(fd,buf,100);
printf("Read from file is: %s\n",buf);
close(fd);
return 0;
}
打开文件的内核数据结构:
×linux没有使用v节点,而是使用了通用的i节点结构。
两个独立进程各自打开同一个文件:
每个进程都有自己的文件表项的一个理由是:使得每个进程都有它自己的对该文件的当前偏移量。
×原子操作:由于文件的共享,所以操作要考虑原子性。
pread相当于lseek 和 read, 但是调用pread时,无法中断其定位和读操作,它具有原子性,同样的有pwrite。
原子:要么全部执行成功,要么全都不执行。
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
//返回值:成功则返回新的文件描述符,出错则返回-1。
dup返回的新文件描述符是当前可用文件描述符中的最小值,dup2则是用fildes2参数指定新文件描述符的数值,若fildes2打开则先将其关闭,若fildes等于fildes2则不关闭,返回fildes2。在CGI程序用dup2将文件描述符重新定位到标准输出和标准输入。即:dup2(fd,STDOUT_FILENO)或dup2(fd,STDIN_FILENO)。
另一个方法:使用fcntl函数:
dup(filedes) 等效于 fcntl(filedes, F_DUPFD, 0);
dup(filedes, filedes2) 等效于 close(filedes2); fcntl(filedes, F_DUPFD, filedes2);
int fcntl(int fd, int cmd, ... /* arg */ );
函数功能:
复制一个现有的描述符(cmd=F_DUPFD)
获得或设置文件描述符(cmd=F_GETFD|F_SETFD)
获得或设置文件状态标志(cmd=F_GETFL|F_SETFL)
获得或设置异步I/O所有权(cmd=F_GETOWN|F_SETOWN)
获得或设置记录锁(cmd=F_GETLK|F_SETLK、F_SETLKW)
可以用fcntl函数设置文件状态,常用设置套接字描述符为非阻塞O_NONBLOCK。
下面写个程序完成打印出指定文件描述符的文件状态标志:
#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)
err_sys("fcntl error for fd %d", 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:
err_dump("unknown access mode");
}
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);
}
测试结果:
$ ./a.out 0 < /dev/tty
read only
$ ./a.out 1 > temp.foo
$ cat temp.foo
write only
$ ./a.out 2 2>>temp.foo
write only, append
$ ./a.out 5 5<>temp.foo
read write
$ ./a.out 0 < /dev/tty 理解:用tty设备,取代文件0(stdin),来运行a.out,也就是说a.out的所有需要输入的信息,都是通过tty设备获得的,而不是用户终端(有可能用户终端也是tty)。
$ ./a.out 1 > temp.foo 理解:将输出重定位到temp.foo中。
$ ./a.out 2 2>>temp.foo 理解:标准错误以追加的形式输出到temp.foo中。
$ ./a.out 5 5<>temp.foo 理解:在文件描述符5上打开文件temp.foo以供读和写。
大多数磁盘I/O都通过缓冲进行,比如延迟写(缓冲区写满时才写)。
延迟写减少了磁盘读写次数,但是降低了文件内容的更新速度,可能因系统故障而产生数据不一致。
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
void sync(void);
sync只是将修改过的块缓冲区排入写队列,然后返回,并不等待实际写磁盘操作结束。
通常守护进程update每隔30秒就会调用sync函数。
fsync只对由文件描述符filedes指定的单一文件起作用,并等待磁盘操作结束才返回。
fdatasync类似与fsync,但是只影响文件的数据部分,而fsync还会同步更新文件的属性。
ioctl函数是I/O操作的杂物箱,较多用于终端I/O。
/dev/fd的目录项是名为0,1,2等的文件,打开文件/dev/fd/n等效于复制描述符n(前提是描述符n是打开的)。
在cat(1)程序中,它将单独的一个字符’-‘解释为标准输入,如:
filter file2 | cat file1 - file3 | lpr
cat先读file1,再读标准输入(及filter file2的输出),然后读file3。
上述命令等价于:
filter file2 | cat file1 /dev/fd/0 file3 | lpr
这样子可以提高文件名参数的一致性,更加清晰。 因为使用‘-’看起来像是一个选项。