APUE: File I/O

文件描述符

对于内核来说, 所有打开的文件都由文件描述符标识.

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系统支持在不同的进程中共享打开的文件.

内核使用三种数据结构来代表一个打开文件:

124332_wC2x_1017135.png

1. 每个进程都有一个入口表(process table entry), 表中的每条数据包含: 一个文件描述符, 一个指针指向文件表入口.

2. 文件表入口包含: 一个代表文件模式标志flag(read, write, append, sync等等), 当前文件的偏移量(offset), 一个指针指向v-node

3. v-node包含文件类型信息和保存操作文件的函数指针, 它同时包含i-node, i-node所包含的信息均读取自磁盘, 例如文件的拥有者, 文件的大小, 文件在磁盘中存储的位置等等.

如果两个进程同时操作一个文件, 则需考虑以下问题:

125226_RhNL_1017135.png

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:

191727_jSCI_1017135.png

对于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");
}

 

转载于:https://my.oschina.net/voler/blog/336835

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值