深入探究文件I/O

读本文章前,必须先有一些通过I/O模型的系统调用的基础,即 open() , create() , read() , write() , close() , lseek() 函数的调用。

原子操作

在文件读写中,很容易有多个进程读取同一文件的情况,这时候竞争状态便不可避免。文件I/O的函数提供的一些参数配合系统调用的原子性很好的解决了这个问题。

来看一个关于竞争创建者的例子:

int main(int argc, char *argv[])
{
    int fd;
    fd = open(argv[1], O_WRONLY);   /*只写形式打开文件,若指定文件不存在则打开错误*/
    if(fd != -1)
    {
        printf("[PID %ld] File \"%s\" already exits", (long)getpid(), argv[1]);
        close(fd);
    }
    else
    {
        if( errno == ENOENT )  /*ENOENT错误指代文件不存在*/
        {
            printf("[PID %ld] File \"%s\" doesn‘t exits yet\n", (long)getpid(), argv[1]);
            if(argc > 2) {
                sleep(5);
                printf("[PID%ld] Done sleeping\n", (long)getpid());
            }
            fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);   /*加入参数O_CREAT,文件不存在则创建*/
            if(fd == -1)
            {
                printf("创建失败\n");
            }
            printf("[PID %lld] Created file \"%s\" exclusively\n", (long)getpid(), argv[1]);
        }
    }
    return 0;
}

看起来程序没有什么问题,先打开文件,如果文件不存在则加O_CREAT参数创建打开,但是如果此时两个进程同时运行这个代码,出现竞争问题(抢占CPU是不可控制的),这里利用sleep假定出进程B抢到资源的情况,运行中会出现问题。

运行结果:

图中可看出,先加参数运行代码,则此时进程A运行中会有5秒的睡眠,在这个时间开起线程B,此时file还未被创建,B创建文件,然后A睡醒,因为睡前判断过没有file,所以直接加参数open,此时file应为B创建,但A误以为是自己创建,所以,,恩,懵逼了,实际情况中是抢,谁知道哪个先抢到。。

所以这里要提的是原子性,因为系统调用本身具有原子性,所以这里就不需要封装阿啥的,只需要把参数加进去,让它承着函数的特性,实现原子性的操作。规避竞争状态。

还有文件偏移量的问题,多个进程向同一文件写入数据,然而这个进程写入数据引起的偏移量不会同步到另一进程,那么另一个进程在添加数据就会覆盖上个进程写入的数据。这里我们有两个函数解决这个问题。

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, void *buf, size_t count, off_t offset);

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

第三个参数解释:
  • 如果为SEEK_SET,文件偏移量将被设置为 offset。
  • 如果为SEEK_CUR,文件偏移量被设置为 当前位置+offset。可正可负。
  • 如果为SEEK_END,文件偏移量将被设置为文件长度加上offset。可正可负。
off_t orig;
orig = lseek(fd, 0, SEEK_CUR);  /*移动0,返回当前偏移量*/
lseek(fd, offset, SEEK_SET);   /*将偏移量移动到指定读取位置*/
s = read(fd, buf, len);   /*读文件*/
lseek(fd, orig, SEEK_SET);   /*移回来*/

上面的代码就能看出 pread() 的功能了,即指定位置读取文件但不更改原偏移量位置。pwrite() 同理。

进程管辖下的所有线程共享同一文件描述符表,这意味着每个已打开的文件被所有线程共享,所以有了这两个函数,线程就可以同时对同一文件描述符执行I/O操作,且不会因为其他线程修改文件偏移量而影响。

文件控制操作

这里里由一个函数比较强大,可对一个已经打开的文件描述符执行一系列控制操作。

#include <fcntl.h>

int fcntl(int fd, int cmd, ...);  /*第三个参数可以设置任意类型或者省略*/

因为文件在操作时是已经打开的,有时候不能直接通过上文得知打开模式,这里需要使用掩码 O_ACCMODE 与 flag 相与,然后与别的标志做比即可。示例代码如下:

accessMode = flags & O_ACCMODE;
if(accessMode == O_WRONLY || accessMode == O_RDWR)
    printf("file is writable\n);

修改文件的状态标志时,需要用到参数 F_GETFL ,代码是里如下,如下给文件添加 O_APPEND 标志。

int flags;
flags = fcntl(fd, F_GETFL);  /*获取当前标志的副本*/
if(flags == -1)  errExit("fcntl");
flags |= O_APPEND;     /*添加标志*/
if(fcntl(fd,F_SETFL,flags) == -1) errExit("fcntl");  /*更新状态*/

文件描述符和打开文件的关系

讲真,看这章前,已经见过不少关于文件描述符阿,文件句柄阿之类的名词,然后被搞得晕头转向,还好,终于遇上这章了,终于等到你还好我没放弃~~~

首先要明白,文件描述符和打开的文件不是一一对应!!不是!!

接着来看看内核维护的三个数据结构:
  • 进程级的文件描述符表(每个条目包含:控制文件描述符操作–close-on-exec标志 and 对打开的文件句柄的引用)
  • 系统级的打开文件表(每个条目包含:当前文件偏移量 and 打开文件时使用的状态 and 文件访问权限-一般为创建文件时设置 and 与信号驱动I/O相关的设置 and 对该文件 i-node 对象的引用)
  • 文件系统的i-node表(每个条目包含:文件类型和访问权限 and 一个指向该文件所持有的锁的列表的指针 and 文件各种属性)访问一个文件时,会在内存中为i-node创建一个副本。

来看一下三个数据结构的关系图:

现在就图中的几种情况举例说明关系:

进程A中,描述符 fd1 和 fd20 都指向同一个打开的文件句柄23,即同一进程中的不同描述符对应同一文件句柄,可通过dup(),dup2()或fcntl()形成。

进程A中 fd2 和进程B中fd2都指向同一个打开的文件句柄,这种情况出现在调用 fork() 后的父子进程。

进程A的 fd0 和 进程B的 fd3 分别指向不同的打开文件句柄,但两个句柄指向i-node表中的相同条目即相同文件,这种情况通常为两个进程各自对一份文件发起 open() 调用。

通过上述揭示两个要点:
  • 两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。
  • 获得和修改打开文件标志,可执行 fcntl() 的 F_GETFL 和 F_SETFL 操作。
  • 文件描述符标志(close-on-exec)为进程和文件描述符所私有。
#include <unistd.h>
 int dup(int oldfd);
 int dups(int oldfd, int newfd);
 int dup3(int oldfd, int newfd, int flags);

调用复制一个打开的文件描述符oldfd,并返回一个新描述符,二者指向同一打开的文件句柄,系统保证新描述符是编号值最低的未用文件描述符。dup2() 可指定新的文件描述符。

如果 oldfd 并非有效的文件描述符,那么 dups2() 将调用失败并返回错误EBADF ,且不关闭 newfd。

fcntl() 的 F_DUPFD 操作是复制文件描述符的另一接口,更具灵活性。

/*创建oldfd的副本,将使用大于等于startfd的最小未用的值作为编号*/
newfd = fcntl(oldfd,F_DUPFD,startfd);

文件描述符的正,副本之间共享同一打开文件句柄所含的文件偏移量和状态标志。dup3() 的 flags 参数支持一个标志 O_CLOEXEC , 促使内核为新文件描述符设置 close-on-exec 标志。

分散输入和集中输出

readv() 和 writev() 系统调用分别实现了分散输入和集中输出的功能。

#include <sys/uio.h>

struct iovec {
    void *iov_base;   /*缓冲区的开始位置*/
    size_t iov_len;   /*缓冲区大小*/
};
/*从文件中读取一片连续的字节,然后将其散置于 iov 指定的缓冲区中*/
ssize_t readv(int fd,const struct iovec *iov, int iovcnt);

/*将 iov 指定的所有缓冲区中的数据拼接起来,然后以连续的字节序列写入文件*/
ssize_t writev(int fd, struct iovec *iov, int iovcnt);

函数将缓冲区 iov 中的数据写入文件fd,iovcnt 指定缓冲区中有几个struct iovec结构体。可通过定义<limit.h >标准库中的 IOV_MAX 来对 iovcnt 加以限制。也可调用 sysconf(_SC_IOV_MAX) 获得此限额。此外,glibc 对 readv() 和 writev() 的封装函数做了额外处理,若系统因 iovcnt 过大而调用失败,外壳函数将临时分配一块缓冲区足够容纳 iov 所有成员的数据缓冲区,然后调用 read() 或 write() 调用。还有 preadv() 和 pwritev() 系统调用在文件指定位置读写。

截断文件

#include <unistd.h>

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

若文件长度大于参数 length,调用将丢弃超出部分,若小于参数 length,调用将在文件尾部添加一系列空字节或者一个文件空洞。区别在于一个通过路径打开,且对文件拥有可写权限,另一个通过描述符打开,此描述符必须有可写权限。

大文件I/O

通常我们用来存放文件偏移量的 off_t 为有符号的长整型,在32位机中,这将文件大小置于 2^31 -1 字节之下,(同理64位的理论上范围达到 2^63-1,基本已经超出磁盘容量)所以对文件的长度有限制。但有时候会有大文件然后长整型范围已经表示不了的情况。

所以提供了对LFS的支持。

要获取LFS功能,有两种办法:

1.在编译时加入 -D_FILE_OFFSET_BITS=64

2.在代码开头加入 #define _FILE_OFFSET_BITS 64

要使用过渡型的LFS API,必须在编译程序时定义 LARGEFILE64_SOURCE 功能测试宏。该 API 所属函数具有处理 64 位文件大小和文件偏移量的能力。命名为 fopen64(), open64(), lseek64(), truncate64(), stat64(), mmap64(), setrlimit64()。除了函数,好有数据类型:struct stat64, off64_t 。

注意,一旦使用LFS,off_t54输出时转换为 (long long) 型。

对于每个进程,内核提供一个特殊的虚拟目录/dev/fd,该目录包含/dev/fd/n形式的文件名,打开该目录下一个文件等同于复制相应的文件描述符。

fd = open("/dev/fd/1", O_WRONLY); 等同于 fd = dup(1);

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

在程序中,我们有时候需要创建一些临时文件,仅供其在运行期间使用。

#include <stdlib.h>

int mkstemp(char *template);

template 为路径名,其中最后6个字符必须为XXXXXX,这六个字符由系统自由分配,保证了文件名的唯一性,并通过template参数返回。文件用完后使用unlink系统调用返回。如下:

#include <stdio.h>
#include <stdlib.h>
int main( int argc, char *argv[] )
{
    int fd;
    char template[] = "/tmp/aaaXXXXXX";  /*必须为字符数组,不可为常量*/
    fd = mkstemp(template);   /*创建*/
    printf("chuangjian chengong\n");
    unlink(template);  /*删除*/
    close(fd);
    return 0;
}

同样作用的还有函数tmpfile():

#include <stdio.h>
FILE *tmpfile(void);

tmpfile() 会创建一个名称唯一的临时文件,将返回一个文件流供 stdio 库函数使用。

时间: 2024-10-10 21:47:33

深入探究文件I/O的相关文章

【Linux_Unix系统编程】chapter5 深入探究文件IO

Chapter5 深入探究文件I/O 本章节将介绍另一个与文件操作相关的系统调用:多用途的fcntl(),并展示其应用之一读取和设置打开文件的状态标志. 5.1 原子操作和竞争条件 所有系统调用都是以原子操作方式执行的.是以为内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断. 以独占方式创建一个文件: 当同时制定O_EXCL与O_CREAT作为open()标志位时,如果要打开的文件已存在,则open()将返回一个错误.保证了进程是打开文件的创建者.

TLPI(liunx/unix系统编程手册)笔记(四) 深入探究文件I/O

本章的重点我想就是原子操作,避免在几个进程在打开同一文件的时候造成的错误,了解一下时间片的概念会对本章有所帮助. (1)独占方式打开文件.(open     <-O_CREAT) 知道,open,可以创建文件并返回fd.当我们的进程运行到open这个函数时间片到了,另一个进程也对这个路径的文件open,那么时间片结束后两个进程都会认为自己是这个文件的拥有者.并未是独占创建打开的.在open 函数的第二个参数中有 O_EXCL 这种打开方式,可以解决独占的问题.另外可以在多进程对一个文件写的时候,

文件I/O与系统编程

文件IO与系统编程 本文是作者阅读TLPI(The Linux Programer Interface的总结),为了突出重点,避免一刀砍,我不会过多的去介绍基本的概念和用法,我重点会去介绍原理和细节.因此对于本文的读者,至少要求读过APUE,或者是实际有写过相关代码的程序员,因为知识有点零散,所以我会尽可能以FAQ的形式呈现给读者. 系统编程概览 如何确定glibc的版本? 可以分为下面两种方式: 第一种就是直接查看,先通过ldd来定位glibc的位置,然后通过直接运行glibc库就可以查看到其

mapreduce 中 map数量与文件大小的关系

学习mapreduce过程中, map第一个阶段是从hdfs 中获取文件的并进行切片,我自己在好奇map的启动的数量和文件的大小有什么关系,进过学习得知map的数量和文件切片的数量有关系,那文件的大小和切片的数量的有什么关系 ,下面我就进入Hadoop的源代码进行研究一下 文件的大小和切片的数量有什么关系. 文件获取和切片和一个InputFormat 这个抽象类有关系 ,这个抽象类 只有两个抽象的方法 分别是 第一个方法是用来过去切片,第二方法使用获取文件.获取切片与第一个方法有关,我们进入研究

最佳vim技巧

最佳vim技巧----------------------------------------# 信息来源----------------------------------------www.vim.org         : 官方站点comp.editors        : 新闻组http://www.newriders.com/books/opl/ebooks/0735710015.html : Vim书籍http://vimdoc.sourceforge.net/cgi-bin/vim

ASP.NET MVC - 探究应用程序文件夹

为了学习 ASP.NET MVC,我们将构建一个 Internet 应用程序. 第 2 部分:探究应用程序文件夹. MVC 文件夹 一个典型的 ASP.NET MVC Web 应用程序的文件夹内容如下所示:   应用程序信息 PropertiesReferences 应用程序文件夹 App_Data 文件夹Content 文件夹Controllers 文件夹Models 文件夹Scripts 文件夹Views 文件夹 配置文件 Global.asaxpackages.configWeb.conf

7、探究用HW2000句训好的模型文件,直接对XuXiaoQing的语料做fa可以吗?

事情的起因: 现在正在做实验,对XuXiaoQing语料进行强制对齐,HMM_training/yuqj/ZhiJun_XuXiaoQing-fa,发现训练的时间太长了,7个小时了,还在做Start embedded reestimation (monophone),这一步. 训练时间太长了,所以,我现在的想法是,想用年前吴老师让我调研的用2000句的HW语料训练好的HMM模型文件,来直接做强制对起这一步. 仔细分析HSMMAlign这一步的过程: /home/ningys/hts_instal

探究linux文件

一.Linux的文件: 文件名区分大小写:Linux没有文件拓展名:文件名支持长文件名,含空格,少部分标点符号. - _最好不要用空格 1 GUI图形用户界面:让简单的问题更加简单: CLI命令行界面:使完成负责的任务成为可能: 选择Linux而不是其他的系统,是因为其具有强大的命令行界面,可以使“完成复杂的任务成为可能”. 2.terminal 终端中 今天的日期date 现在的月份cal 磁盘的剩余 pf 显示空闲内存的数量free 结束终端回话exit 二.(一)完成以下操作: 1.cd到

ns2中程序未执行完无trace文件探究

最近几天在做仿真的过程中,程序执行了一点点就出错了,想分析一下trace文件发现还没有内容,这是为什么呢?不是MAC层的downtarget就是trace吗?明明已经从MAC层几进几出了为什么还是没有内容呢?带着这个疑问我查看了一下cmu-trace.cc文件,发现了这个: 原来trace文件的内容是先输入到pt_->buffer()这个缓冲区中,当缓冲区满后再输入到文件中,那么此时的缓冲区的长度是多少呢?在此,我继续在trace.h文件中找到pt_的定义 我们再看一下BaseTrace这个类中