Linux/Unix系统编程 二:深入探究文件IO

一、原子操作、竞争

原子操作:一个独立而不可分隔的操作。

所有系统调用都是以原子操作方式执行的

原子操作规避了竞争状态。

竞争状态操作共享资源的两个进程(或线程),其结果取决于一个无法预测的顺序,即这些进程获得CPU使用权的先后相对顺序。

1、原子操作必要性说明

思考以下程序

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#ifndef BUF_SIZE
#define BUF_SIZE 1024
#endif

int main(int argc,char *argv[])
{
    int fd;
    // 以只写的方式打开文件,如果文件不存在则报错
    fd = open(argv[1],O_WRONLY);
    if(fd != -1){
        // 文件打开成功,说明文件已经存在
        printf("[PID %ld] file \"%s\" already exists\n",(long)getpid(),argv[1]);
        close(fd);
    }else{
        // 文件打开失败
        if(errno != ENOENT){
            // 其他原因打开文件失败
            printf("open\n");
        }else{
            // 文件存在打开失败
            printf("[PID %ld] file \"%s\" doesn't exist\n",(long)getpid(),argv[1]);
            if(argc > 2){
                sleep(5);
            }
            // 打开文件如果不存在则创建
            fd = open(argv[1],O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
            if(fd == -1)
                printf("open\n");   // 文件打开失败
            else
                printf("[PID %ld] Create file \"%s\" exclusively\n",(long)getpid(),argv[1]);
        }
    }
    return 0;
}

在这里插入图片描述
通过以上执行,可以看到 2个进程都认为文件是自己创建的。

分析
在这里插入图片描述
两个进程对同一个文件同时操作,产生竞争状态,使一个进程得到错误的结论

2、以独占方式创建一个文件

在使用 open() 系统调用时,同时指定 O_EXCL 和 O_CREAT 作为 open() 的标志位,保证进程是打开文件的创建者。对文件是否存在的检查创建文件属于同一原子操作

二、文件描述符和打开文件之间的关系

在这里插入图片描述
要理解文件描述符和打开文件之间的关系,需要查看内核维护的3个数据结构:

1、进程级文件描述符表

2、系统级打开文件表

3、文件系统i-node 表

1、文件描述符表

每一条目录都记录了单个文件描述符的相关信息。

  • 控制文件描述符操作一组标志(目前只有一个,即 close-on-exec标志)
  • 对打开文件句柄的引用

2、打开文件表

打开文件表中各条目称为打开文件句柄。一个打开文件句柄存储了与一个打开文件相关的全部信息。

  • 当前文件偏移量(调用 read() 和 write() 时更新,或使用 lseek() 直接修改)。
  • 打开文件时所使用的状态标志(即 open() 的 flags 参数)。
  • 文件访问模式(如调用 open() 时所设置的只读模式、只写模式或读写模式)。
  • 与信号驱动 I/O 相关的设置
  • 对该文件 i-node 对象的引用

3、i-node 表

  • 文件类型(例如,常规文件、套接字或FIFO)和访问权限
  • 一个指针,指向该文件所持有的锁列表。
  • 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。

i-node 在磁盘和内存中有所差异

3、文件控制操作:fcntl()

操纵文件描述符.

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

1、参数

1、fd
open() 系统调用返回的文件描述符。
2、cmd

命令含义
F_GETFL获取文件状态标志
F_SETFL修改打开文件的某些状态标志
F_DUPFD复制文件描述符
…………
  • F_GETFL
    判定文件的访问描述符有一点复杂,这是因为 O_RDONLY(0)、O_WRONLY(1) 和 O_RDWR(2) 这3个常量并不与打开文件状态标志中的单个比特位对应。因此,要判定访问模式,需要使用掩码 O_ACCMODE 与 flag 想与,将结果与这三个常量进行对比,示例代码如下:
access_mode = flags & O_ACCMODE;
if(access_mode == O_WRONLY || access_mode == RDWR)
	printf("file is writable\n");
  • F_SETFL
    允许更改状态标志 O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT,系统将忽略对其他标志的修改操作。
  • F_DUPFD
newfd = fcntl(oldfd,F_DUPFD,startfd);

该系统调用为 oldfd 创建一个副本,且将使用大于等于 startfd 的最小未用值作为描述符编号。总是能将 dup() 和 dup2() 调用改写为对 close() 和 fcntl() 的调用。

  • ……

2、返回值

3、备注

1、fcntl() 修改文件状态标志,适用如下场景
  • 文件不是由调用程序打开的,所以程序无法使用 open() 调用来控制文件的状态标志。
  • 文件描述符的获得通过 open() 之外的系统调用

三、其他系统调用

1、dup()

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int dup3(int oldfd, int newfd, int flags)

dup() 调用复制一个打开的文件描述符 oldfd,并返回一个新文件描述符,二者都指向同一个打开的文件句柄

dup2() 调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果 newfd 参数指定文件描述符已经打开,那么 dup2() 会先将其关闭。dup2() 会忽略 newfd 关闭期间出现的任何错误。

dup3() 系统调用完成的工作与 dup2() 相同,只是增加了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。

2、pread()、pwrite()

#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);

pread() 调用等同于将如下调用纳入同一原子操作

off_t orig;

orig = lseek(fd,0,SEEK_CUR);	// 保存当前文件偏移量
lseek(fd,offset,SEEK_SET);
s = read(fd,buf,len);
lseek(fd,orig,SEEK_SET);		// 恢复文件偏移量

对于 pread() 和 pwrite() 而言,fd 所指代的文件必须是可定位的

多线程应用为这些系统调用提供用武之地。进程中所有线程共享同一文件描述符表。这也意味着每个已打开的文件偏移量为所有线程共享。当调用 pread() 或 pwrite() 时,多个线程可以同时对同一文件描述符执行 IO 操作,且不会其他线程修改文件偏移量而受到影响。

3、readv()、writev()

struct iovec{
    void *iov_base;
    size_t iov_len;
};

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

ssize_t preadv(int fd, const struct iovec *iov, int iovcnt,
               off_t offset);

ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt,
                off_t offset);

ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt,
                off_t offset, int flags);

ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt,
                 off_t offset, int flags);

在这里插入图片描述

1、分散输入

readv() 系统调用实现分散输入功能:从文件描述符 fd 所指代的文件中读取一片连续的字节,然后将其散置(分散放置)于 iov 指定的缓冲区中。这一散置动作从 iov[0] 开始,依次填满每个缓冲区。

原子性是 readv() 的重要属性

2、集中输出

writev() 系统调用实现集中输出:将 iov 所指定的所有缓冲区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符 fd 所指代的文件中。对缓冲区中数据 “集中” 始于 iov[0] 所指代的缓冲区,并按数组顺序展开

原子性是 writev() 的重要属性

3、readv() 和 writev() 主要优势

readv() 调用和 writev() 调用的主要优势在于便捷。如下两种方案,任选其一都可替代对 writev() 的调用。

4、preadv()、pwritev()

preadv() 和 pwriev() 系统调用所执行的任务于 readv() 和 writev() 相同,但执行 I/O 的位置将由 offset 参数指定(类似 pread() 和 pwrite())。

4、truncate()、ftruncate()

#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

truncate() 系统调用和 ftruncate() 系统调用将文件大小设置为 length 参数指定的值。

若文件当前长度大于参数 length,调用将丢弃超出部分。若小于参数 length,调用将在文件尾部添加一系列空字节或一个文件空洞。

truncate() 调用通过路径名指定文件。

ftruncate() 通过文件描述符指定文件。

四、非阻塞I/O

在打开文件时指定 O_NONBLOCK 标志,目的有二:

  • 若 open() 调用未能立即打开文件,则返回错误,而非陷入阻塞。
  • 调用 open() 成功后,后续的 I/O 操作也是非阻塞的。若 I/O 系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回 EAGAIN 或 EWOULDBLOCK 错误。

管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。

因为无法通过 open() 来获取管道和套接字的文件描述符,所以要启用非阻塞标志,就必须使用 fcntl() 的 F_SETFL 命令

五、大文件IO

六、/dev/fd目录

对于每个进程进程,内核都提供有一个特殊的虚拟目录 /dev/fd。该目录中包含 “/dev/fd/n” 形式的文件名,其中 n 是与进程中的打开文件描述符相对应的编号。

/dev/fd 实际上是一个符号链接,链接到 Linux 所专有的 /proc/self/fd 目录。

七、创建临时文件

有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后立即删除。

1、mkstemp

#include <stdlib.h>

int mkstemp(char *template);

int mkostemp(char *template, int flags);

int mkstemps(char *template, int suffixlen);

int mkostemps(char *template, int suffixlen, int flags);

mkstemp() 函数生成一个唯一文件名并打开该文件,返回一个可用于 IO 调用的文件描述符。

2、tmpfile

#include <stdio.h>

FILE *tmpfile(void);

tmpfile() 执行成功,将返回一个文件流供 stdio 库函数使用。文件关闭后立即删除临时文件。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值