文件描述符
对于内核来说, 所有打开的文件都由文件描述符标识.
open和openat函数
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
both return: file descriptor if OK, -1 on error
oflag用于标识打开的类型, 以下五种类型必选其一:
O_RDONLY: 只读
O_WRONLY: 只写
O_RDWR: 读写
O_EXEC: 运行
O_SEARCH: 用于搜索(打开目录时使用)
以下参数为可选:
O_APPEND: 每次写入均追加到文件末尾.
O_CREAT: 如果文件不存在, 则创建.
O_DIRECTORY: 如果路径不是一个目录, 则报错.
O_EXCL: 如果存在O_CREAT参数并且文件已经存在, 则报错, 这是原子操作.
O_NOCTTY: 如果路径为终端, 则不要给此进程分配终端设备.(不太理解)
O_NONBLOCK: 如果路径指向一个FIFO, 一个阻塞文件, 或者特殊的字符文件, 则此选项将设置模式为非缓存.
O_SYNC: 设置为同步模式, 即每次写入都要等待物理I/O完成.
O_TRUNC: 如果文件已只读或只写打开, 并且此文件已经存在, 则将文件的长度截断为0.
openat函数和open函数的区别在于fd参数, 它具有以下属性:
1. 如果path参数是绝对路径, 则fd参数将被忽略, 这时候openat和open行为一样.
2. 如果path参数是相对路径并且fd参数为文件描述符. fd指向文件系统中的某个路径. 这样可通过fd+path确定具体的文件.
3. 如果path参数为相对路径并且fd为特殊值AT_FDCWD, 则path是相对于当前目录, 而openat和open行为类似.
openat函数主要解决两个问题: 1. 在多线程环境下, 提供相对路径来操作文件. 2. 解决time-of-check-to-time-of-use(TOCTTOU)错误.
TOCTTOU的意思是: 两个基于文件的函数A,B, 而B是基于A的结果而执行的, 但是两个函数的调用均不是原子的, 从而可能导致调用A后文件被修改, 从而导致调用B出错.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFSIZE 4096
int main(int argc, char *argv[])
{
int n;
char buf[BUFSIZE];
int fd = -1;
fd = open("./test.js", O_RDONLY | O_CREAT);
while ((n = read(fd, buf, BUFSIZE)) > 0) {
if (write(STDOUT_FILENO, buf, n) != n) {
printf("write error");
}
}
if (n < 0) {
printf("read error");
}
return 0;
}
create函数
创建一个文件:
#include <fcntl.h>
int creat(const char *path, mode_t mode);
returns: 以只读模式打开返回文件描述, 失败返回-1
此函数等价于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
close函数
关闭一个文件:
#include <unistd.h>
int close(int fd);
returns: 成功返回0, 失败返回-1
lseek函数
在一个文件中执行定位:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
returns: 成功返回新的文件偏移, 错误返回-1.
offset含义基于whence的取值:
1. whence为SEEK_SET时, 文件从开头偏移offset.
2. whence为SEEK_CUR时, 文件偏移从当前位置 + offset, offset可正可负.
3. whence为SEEK_END时, 文件偏移从文件末尾 + offset, offset可正可负.
以下代码可确定当前文件偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
我们可以编写以下代码, 用于测试路径是否可seek:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFSIZE 4096
int main(int argc, char *argv[])
{
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("cannot seek\n");
else
printf("seek OK\n");
return 0;
}
测试指令:
leicj@leicj:~/test$ ./a.out < /etc/passwd
seek OK
leicj@leicj:~/test$ cat < /etc/passwd | ./a.out
cannot seek
cat: write error: Broken pipe
考虑以下情况: 我们通过lseek制造空洞文件
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main( void )
{
int fd;
int n;
char buf[ 10 ] = "abcdefghij";
char buf1[ 10 ] = "ABCDEFGHIJ";
if ((fd = open("test", O_RDWR | O_CREAT)) < 0)
printf("open test file error\n" );
if (write(fd, buf, 10) != 10)
printf("write error\n" );
if ((n = lseek(fd, 40, SEEK_SET)) == -1)
printf("lseek error\n");
if (write(fd, buf1, 10) != 10)
printf("write error\n");
if (lseek(fd, 0, SEEK_SET) == -1)
printf("lseek error\n");
return 0;
}
运行程序后, 执行指令:
leicj@leicj:~/test$ cat test
abcdefghijABCDEFGHIJleicj@leicj:~/test$ od -c test
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
0000040 \0 \0 \0 \0 \0 \0 \0 \0 A B C D E F G H
0000060 I J
0000062
read函数
从一个打开文件中读取数据:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
returns: 所读取的字节数, 读取到文件末尾返回0, 错误返回-1
write函数
往打开的文件中写入数据:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
returns: 写入成功则返回所写入的字节数, 错误返回-1
文件共享
Unix系统支持在不同的进程中共享打开的文件.
内核使用三种数据结构来代表一个打开文件:
1. 每个进程都有一个入口表(process table entry), 表中的每条数据包含: 一个文件描述符, 一个指针指向文件表入口.
2. 文件表入口包含: 一个代表文件模式标志flag(read, write, append, sync等等), 当前文件的偏移量(offset), 一个指针指向v-node
3. v-node包含文件类型信息和保存操作文件的函数指针, 它同时包含i-node, i-node所包含的信息均读取自磁盘, 例如文件的拥有者, 文件的大小, 文件在磁盘中存储的位置等等.
如果两个进程同时操作一个文件, 则需考虑以下问题:
1) 当write操作完成时, 文件偏移量会增加. 如果write操作导致偏移量操作文件的大小(即扩充了文件大小), 则i-node中文件大小信息会同步被更新.
2) 如果文件使用O_APPEND参数打开, 则在每次write操作时候, 会将偏移量设置为当前文件大小, 以保证每次write都写到文件的末尾.
3) 当使用lseek将偏移量定位到文件末尾时, 效果类似使用O_APPEND, 但它非原子操作.
原子操作
文件后插入数据
在旧的Unix版本中, 不支持O_APPEND情况下编写如下代码执行append操作:
if (lseek(fd, 0L, 2) < 0)
err_sys("lseek error");
if (write(fd, buf, 100) != 100)
err_sys("write error")
这在单线程情况下正常, 在多线程情况下就会出错. 考虑线程A和B, A在文件偏移量1500位置准备插入数据, 这时候B在偏移量1500处成功插入数据后将偏移量增加到1600, 而接着执行A的操作, 则B所插入的数据将会被覆盖.
如果我们提供将lseek和write/read结合起来的函数, 则完美解决多线程的问题. Unix提供以下两个函数:
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
returns: 所读取的字节数, 如果到达文件末尾则返回0, 错误返回-1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
returns: 所写入的字节数, 错误返回-1
dup和dup2函数
一个存在的文件描述符可被重复通过dup/dup2函数:
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
returns: 成功返回新的文件描述符, 失败返回-1
新旧的文件描述符共享相同的file table:
对于dup来说, 新文件描述符保证是最小的可生成的文件描述符; 对于dup2来说, 如果fd2是打开的, 则它先被关闭. 如果fd等于fd2, 则函数返回fd2而不关闭它; 否则fd2中清除FD_CLOEXEC文件描述符, 当进程执行exec后打开它.
使用fcntl可达到类似的效果:
dup(fd) 等价于 fcntl(fd, F_DUPFD, 0)
dup2(fd, fd2) 等价于 fcntl(fd, F_DUPFD, fd2)
它们之间不同之处在于:
1. dup2是原子操作.
2. dup2和fcntl之间的errno不太一样.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int fd1;
int fd2;
char buf[1024];
int n;
if ((fd1 = dup(0)) == -1){
printf("dup error\n");
}
printf("%d\n", fd1);
if ((fd2 = open("test", O_RDWR)) == -1){
printf("open file error");
}
printf("%d\n", fd2);
if (dup2(1, fd2) < 0){
printf("dup2 error\n");
}
printf("%d\n", fd2);
while ((n = read(fd1, buf, 1024)) > 0){
if (write(fd2, buf, n ) != n){
printf("write error\n");
}
}
if (n < 0){
printf("read error\n");
}
return 0;
}
sync, fsync和fdatasync函数
传统的Unix系统中在磁盘I/O操作时都保留缓存, 当写入数据到文件中时都会先写入缓存中, 等待一段时间后再写入文件, 这是延时写入技术.
可通过以下函数保证缓存数据都写入到文件中:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
returns: 成功返回0, 失败返回-1
void sync(void);
sync函数将所有修改的缓存写入到目的文件中. fsync周期性的被调用, 保证刷新缓存. fdatasync和fsync类似, 但只刷新文件的数据部分.
fcntl函数
fcntl函数用于修改一个已打开文件的属性:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
returns: 成功则返回值基于cmd, 失败返回-1
fcntl函数主要有以下五种用途:
1. 复制已存在的文件描述符(cmd = F_DUPFD/F_DUPFD_CLOEXEC)
2. get/set文件描述符(cmd = F_GETFD/F_SETFD)
3. get/set文件状态标志(cmd = F_GETFL/F_SETFL)
4. get/set异步I/O所有权(cmd = F_GETOWN/F_SETOWN)
5. get/set记录锁(cmd = F_GETTLK, F_SETTLK, F_SETLKW)
F_DUPFD: 复制文件描述符fd, 并返回最小未使用的文件描述符. 如果提供第三个参数则要大于等于它. 新的文件描述符和fd共享file table, 并且FD_CLOEXEC文件描述符被清除(如果有的话).
F_DUPFD_CLOEXEC: 类似F_DUPFD, 设置FD_CLOEXEC关联到新的文件描述符.
F_GETFD: 获取文件描述符标志(以FD开头, 关联文件描述符)
F_SETFD: 设置文件描述符标志
F_GETFL: 返回文件的状态标志:
文件状态标志 | 描述 |
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
O_EXEC | 可执行 |
O_SEARCH | 打开文件夹用于搜索 |
O_APPEND | 插入 |
O_NONBLOCK | 非阻塞模式 |
O_SYNC | 等待写入完成(数据和属性) |
O_DSYNC | 等待写入完成(数据) |
O_RSYNC | 同步读写 |
O_FSYNC | 等待写入完成(FreeBSD/Mac OS X专用) |
O_ASYNC | 异步I/O操作(FreeBSD/Mac OS X专用) |
F_SETFL: 设置文件的状态标志, 仅可设置以下标志: O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC.
F_GETOWN: 接收SIGIO/SIGURG信号, 获取进程id和进程组id
F_SETOWN: 接收SIGIO/SIGURG信号, 设置进程id和进程组id
4. fcntl函数
fcntl用于改变已打开的文件的性质。
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd;
int val;
if (argc != 2)
printf("usage:a.out <descriptor#>");
if ((val = fcntl( atoi( argv[ 1 ]) , F_GETFL, 0)) < 0)
printf("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:
printf("unknown access mode");
break;
}
if (val & O_APPEND)
printf(",append");
if (val & O_NONBLOCK)
printf(",nonblocking");
#if defined(O_SYNC)
if (val & O_SYNC)
printf(", synchronous writes");
#endif
#if !defined(_POSIX_C_SOURCE) && defined(O_FDYNC)
if (val & O_FSYNC)
printf(",synchronous writes" );
#endif
putchar('\n');
return 0;
}
程序输出:
leicj@leicj:~/test$ ./a.out 0 < /dev/tty
read only
leicj@leicj:~/test$ ./a.out 2 2>>temp.foo
write only,append
leicj@leicj:~/test$ ./a.out 5 5<>temp.foo
read write
leicj@leicj:~/test$ ./a.out test
read write
修改文件描述符标志或文件状态标志时候要小心,先要取得现有的标志值,然后根据需要修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位的。
void set_fl(int fd, int flags)
{
int val;
if ((val = fcntl( fd, F_GETFL, 0 )) < 0)
printf("fcntl F_GETFL error");
val |= flags;
if (fcntl(fd, F_SETFL, val) < 0)
printf("fcntl F_SETFL error");
}