linux系统编程:文件操做

文件描述符前端

对于内核而言,全部打开的文件都经过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或建立一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或creat返回的文件描述符标识该文件,将其做为参数传递给read或write。shell

    按照惯例,UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准出错相关联。这是各类shell以及不少应用程序使用的惯例,而与UNIX内核无关,若是不遵照这种惯例,那么不少UNIX系统应用程序就不能正常工做。数据库

    在依从POSIX的应用程序中,幻数0、一、2应当替换成符号常量STDIN_FILENO,数组

STDOUT_FILENO,缓存

STDERR_FILENO。网络

这些常量都定义在头文件<unistd.h>中。数据结构

    文件描述符的变化范围0~OPEN_MAX。早期的UNIX系统实现采用的上限值是19(容许每一个进程最多打开20个文件),但如今不少系统则将其增至63个。异步

OPEN函数async

    调用open函数能够打开或建立一个文件。ide

#include<fcntl.h>

int open(const char *pathname, int oflag,...)

咱们将第三个参数写为...,ISO C用这种方法代表余下参数的数量及其类型根据具体的调用会有所不一样。对于open函数而言,仅当建立新文件时才使用第三个参数。在函数原型中将此参数放置在注释中。

pathname是要打开或建立文件的名字。oflag参数可用来讲明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数:

O_RDONLY            //只读打开

O_WRONLY           //只写打开

O_RDWR               //读、写打开

在这三个常量中必须指定一个且只能指定一个。下列常数则是可选择的。

O_APPEND             //每次写时都追加到文件的尾端          

O_CREAT               //若此文件不存在,则建立它。使用时,须要第三个参数

O_EXCL                //若是同时指定了O_CREAT,而文件已经存在,则会出错。用此能够测试一个文件是否存在,若是不存在,则建立此文件,这使测试和建立者二者成为一个原子操做。

O_TRUNC              //若是此文件存在,并且为只写或读写成功打开,则将其长度截短为0。

O_NOCTTY             //若是pathname指的终端设备,则不将该设备分配做为此进程的控制终端。

O_NONBLOCK        //若是pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次操做和后续的I/O操做设置非阻塞模式。

O_DSYNC              //使每次write等待物理I/O操做完成,可是若是写操做并不影响读取刚写入的数据。则不等待文件属性被更新。

O_RSYNC              //使每个以文件描述符做为参数的read操做等待,直到任何对文件同一部分进行的未决写操做都完成。

O_SYNC                //使每次write都等到物理I/O操做完成,包括由write操做引发的文件属性更新所需的I/O。

    由open返回的文件描述符必定是最小的未用描述符数值。这一点被某些应用程序在 标准输入,标准输出,标准出错输出上打开新的文件。例如,一个应用程序能够先关闭标准输出(一般是文件描述符1),而后打开另外一个文件,这行打开操做前就能了解到该文件必定会在文件描述符1上打开。

文件名和路径名截短

    若是NAME_MAX是14,而咱们却试图在当前目录中建立一个其文件名包含15个字符的新文件,此时会发生什么哪?按照传统,早期的系统V版本容许这种使用方法,但老是将文件名截短为14个字符,并且不给出任何信息,而BSD类的系统则返回出错状态,并将errno设置为ENAMETOOLONG。无声无息地截短文件名会引发问题,并且他不只仅影响到建立新文件。若是NAME_MAX是14,而且存在一个其文件名刚好就是14个字符的文件,那么以pathname做为其参数的任意函数都没法肯定该文件的原始名是什么? 其缘由是这些函数没法判断该文件名是否被截短过。

    在POSIX.1种,常量_POSX_NO_TRUNC决定了是要截短过茶国内的文件名或路径名,仍是返回一个出错。

creat函数

也可调用creat函数建立一个新文件

#include<fcntl.h>

int creat (const char *pathname, mode_t mode)

    此函数至关于

open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode)

    create的不足之处是它以只写方式打开所建立的文件。在提供open的新版本以前若是要建立一个临时文件,并要写该文件,而后又读该文件,则必须先调用creat、close、open。如今则可用下列方式调用open

open(pathname, O_RDWR|O_CREAT|O_TRUNC, mode)

close函数

    可调用close函数关闭一个打开的文件:

#include<unistd.h>

int close(int filedes)

    关闭一个文件时还会释放该进程加在该文件上的全部记录锁

    当一个进程终止时,内核自动关闭它全部打开的文件。不少程序都利用了这一功能而不显示地用close关闭打开文件。

lseek函数

    每一个打开的文件都有一个与其相关联的“当前文件偏移量”。它一般是一个非负数,用以度量从文件开始处计算的字节数。一般,读、写操做都从当前文件偏移量处开始,并使偏移量增长所读写的字节数。按系统的默认的状况,当打开一个文件时,除非指定O_APPEND选项,不然该偏移量被设置为0。

    能够调用lseek显示地位一个打开的文件设置其偏移量。

#include<unistd.h>

off_t lseek(int filedes, off_t offset, int whence);

    对参数offset的解释与参数whence的值有关。

    若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处的offset个字节。

    若whence是SEEK_CUR,则该文件的偏移量设置为其当前值加offset,offset可为正或负。

    若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负。

    若lseek成功执行,则返回新的文件偏移量,为此能够用下列方式肯定打开文件的当前偏移量:

    off_t    currpos;

    currpos = lseek(fd, 0, SEEK_CUR);

    这种方法也可用来肯定所涉及的文件是否能够设置偏移量。若是文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。

    实例:程序用于测试可否对其标准输入设置偏移量。

    #include"apue.h"

    int main()

    {

         if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)

              printf("cannot seek/n");

          else

              printf("seek OK/n");

          exit(0);

     }

    一般,文件的偏移量应当是一个非负整数,可是,某些设备也可能容许负的偏移量。但对于普通文件,则其偏移量必须是非负值。由于偏移量多是负值,因此在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。

    lseek仅将当前的文件偏移量记录在内核中,它并不引发任何I/O操做。而后,该偏移量用于下一个读或写操做。

    文件偏移量能够大于文件的当前长度,在这种状况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是容许的。位于文件中但没有写过的字节都被读为0。

    文件中的空洞并不要求在磁盘上占用储存区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端以后写时,对于新写的数据须要分配磁盘块,可是对于原文件尾端和新开始写位置之间的部分则不须要分配磁盘块。

实例 建立一个具备空洞的文件

    #include"apue.h"

    #include"fcnl.h"

    char    buf1[] = "abcdefghij";

    char    buf2[] = "ABCDEFGHIJ";

    int main()

    {

        int    fd;

        if ((fd = creat("file.hole", FILE_MODE)) < 0)

            err_sys("creat error");

        if (write (fd, buf1, 10) != 10)

            err_sys("buf1 write error");

        if (lseek(fd,16384, SEEK_SET) == -1)

            err_sys("lseek error");

        if (write(fd, buf2, 10) != 10)

            err_sys("buf2 write error");

        exit(0);

    }

read函数

    调用read函数从打开文件中读数据。

#include<unistd.h>

ssize_t read(int filedes, void *buf, size_t nbytes);

    有多种状况可以使实际读到的字节数少于要求读的字节数:

    读普通文件时,在读到要求字节数以前已经到达了文件尾端。例如,若在到达文件尾端以前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将回0。

    当从终端设备读时,一般一次最多读一行

    当从网络读时,网络中的缓冲机构可能形成返回值小于所要求读的字节数。

    当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。

    当从某些面向记录的设备(例如磁盘)读时,一次最多返回一个记录。

    当某一信号草成终端,而已经读了部分数据量时。读操做从文件的当前偏移量出开始,在成功返回以前,该偏移量将增长实际独到的字节数

write函数

    调用write函数向打开的文件写数据。

#include"unistd.h"

ssize_t write(int filedes, const void *buf, size_t nbytes)

    对于普通文件,写操做从文件的当前偏移量处开始。若是在打开该文件时,指定了O_APPEND选项,则在每次写操做以前,将文件偏移量设置在文件的当前结尾处。在一次成功写以后,该文件偏移量增长实际写的字节数。

I/O的效率

    程序清单3-3中的程序使用read和write函数负值一个文件。关于该程序应注意下列各点:

    它从标准输入读,写至标准输出,这就假定在执行本程序以前,这些标准输入、输入已由shell安排好。确实,全部经常使用的UNIX系统shell都提供一种方法,他在标准输入上打开文件用于读,在标准输出上建立一个文件。这使得程序没必要自行打开输入和输出文件。

    不少应用程序假定标准输入是文件描述符0,标准输出是文件描述符1。本实例中则使用STDIN_FILENO和STDOUT_FILENO.

    为考虑进程终止时,UNIX系统内核会关闭该进程的全部打开文件描述符,因此此实例并不会关闭输入和输出的文件。

    对UNIX系统内核而言,文本文件和二进制代码文件并没有区别,因此本实例对这两种文件都能工做

实例 将标准输入复制到标准输出

    #include"apue.h"

    #define    BUFFSIZE    4096

    int main()

    {

        int    n;

        char    buf[BUFFSIZE];

        while ((n = read(STDIN_FILENO, buf ,BUFFSIZE)) >0)

            if ( write(STDOUT_FILENO,buf, n) !=n)

                err_sys("write error");

        if (n <0)

             err_sys("read error");

        exit(0);

    }

    咱们没有回答的一个问题是如何选取BUFFSIZE值。在回答此问题以前,让咱们先用各类不一样的BUFFSIZE值来运行此程序。

    用程序读文件,其标准输出被从新定向到/dev/null上。此测试所用的文件时Linux ext2文件系统,其块长为4096字节。系统CPU时间的最小值出如今BUFFSIZE为4096处,继续增长缓冲区几乎没有影响。

    大多数文件系统为改善其性能都采用某种预读技术,当检测到正进行顺序读取时,系统就试图读入鼻应用程序所要求的更多数据,并假想应用程序很快就会读这些数据。当BUFFSIZE为128KB后,预读中止了,这对读操做的性能产生了影响。

文件共享

    UNIX系统支持在不一样进程间共享打开的文件。在介绍dup以前,先要说明这种共享。为此先介绍内核用于全部I/O的数据结构。

    内核使用三种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对另外一个进程可能产生的影响。

    (1)每一个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每一个描述符占用一项。与每一个文件描述符向关联的是:

    文件描述符标志。

    指向一个文件表的指针。

    (2)内核为全部打开文件维持一张文件表。每一个文件表项包含:

    文件状态标志(读、写、添写、同步和非阻塞等)。

    当前文件偏移量。

    指向该文件v节点表项的指针。

    (3)每一个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各类操做的函数的指针。对于大多数文件,v节点还包含了该文件的i节点。这些信息是在打开文件时从磁盘上读入内存的,因此全部关于文件的信息都是快速可提供使用的。例如,i节点包含了 文件的全部者 、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等等。

    咱们忽略了某些实现细节,但这并不影响咱们的讨论。例如,打开文件描述符表可存放在用户空间。而非进程表中。这些表也能够用多种方式实现,没必要必定是数组;例如,可将他们实现为结构的链表。

    图3-1显示了一个进程的三张表之间的关系。该进程有两个不一样的打开文件:一个文件打开为标准输入,另外一个打开为标准输出。从UNIX系统的早期版本以来,这三张表之间的基本关系一直保持至今。这种安排对于在不一样进程之间共享文件的方式很是重要。

    若是两个独立进程各自打开了同一个文件,则有图3-2所示的安排。咱们假定第一个进程在文件描述符3上打开该文件,而另外一个进程则在文件描述符4上打开该文件。打开该文件的每一个进程都获得一格文件表项,但对一个给定的文件只有一个v节点表项。每一个进程都有本身的文件表项的一个理由是:这种安排是每一个进程都有它本身的对该文件的当前偏移量。

    给出了这些结构后,如今对前面所述的操做做进一个说明。

    在完成每一个write后,在文件表项中的当前文件偏移量即增长所写的字节数。若是这使当前文件偏移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量。

    若是用O_APPEND标志打开了一个文件。则相应标志也被设置到文件表项的文件状态标志中。每次对这种具备添写标志的文件执行写操做时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据添加到文件的当前端处。

    若一个文件用lseek定位到卫检当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。

    lseek函数只修改文件表项中的当前文件偏移量,没有进行任何i/O操做。

    可能有多个文件描述符项指向同一个文件表项。    //后续介绍

    注意,文件描述符标志和文件状态标志在做用域方面的区别,前者只用于一个进程的一个描述符,然后者则适用于指向该给定文件表项的任何进程中的全部描述符。   //后续介绍

     本节上面所述的一切对于多个进程读同一个文件都能正常工做。每一个进程都有它本身的文件表项,其中也有本身的当前文件偏移量,。可是,当多个进程写同一个文件时,则可能产生预期不到的效果。为了说明如何避免这种状况,须要理解原子操做的概念。

原子操做

添写至一个文件

    考虑一个进程,他要将数据添加到一个文件尾端。早期的UNIX系统版本并不支持open的O_APPEND选项,因此程序被编写为下列形式:

    if(lseek(fd,0L,2) < 0)

        err_sys(lseek error);

    if(write(fd , buf, 100) !=100)

        err_sys("write error");

    对单个进程而言,这段程序能正常工做,但如有多个进程同时使用这种方法将数据添加到用一个文件,则会擅胜问题。(例如,若此程序由多个进程同时执行,各自将消息添加到一个日志文件中,就会产生这种状况)。

    假定有两个独立的进程A和B都对同一个文件进行添加操做。每一个进程都己打开了该文件,但未使用O_APPEND标志。此时,各数据结构之间的关系如图3-2所示。每一个进程都有它本身的文件表项,可是共享一个v节点项。假定进程A调用了lseek,他将进程A的该文件当前偏移量设置为1500字节。而后内核切换进程是进程B运行。进程B执行lseek也将其对该文件的当前便宜设置为1500字节。而后B调用write,他将B的该文件当前文件的增至1600。由于该文件的长度已经增长了,因此内核对v节点中的当前文件长度更新为1600。而后内核又进行进程切换使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1500)处将数据写到文件中去。这样也就替换了进程B刚写到该文件中的数据。

    问题出在逻辑操做“定位到文件尾端处,而后写”上,他使用了两个分开的函数调用。解决问题的方法是使这2两个操做对于其余进程而言成为一个原子操做。任何一个须要多个函数调用的操做都不多是原子操做,由于在两个函数调用之间,内核有可能会临时挂起该进程。

    UNIX系统提供了一种方法使这种操做成为原子操做,该方法是在打开文件时设置O_APPEND标志。正如前一节中所述,这就使内核每次对这种文件进行写以前,都将进程的当前偏移量设置到该文件的尾端处,因而在每次写以前就再也不须要调用lseek。

pread和pwrite函数

#include<unistd.h>

ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);

ssize_t pwrite(int filedes, void *buf, size_t nbytes, off_t offset);

     调用pread至关于顺序调用lseek和read,可是pread又与这种顺序调用有下列重要区别:

    调用pread时,没法中断其定位和读操做。

    不更新文件指针。

    调用pwrite至关于顺序调用lseek和write,但也与他们有相似的区别。

建立一个文件

在对open函数的O_CREAT和O_EXCL选项进行说明时,咱们己见到另外一个有关原子操做的例子。当同时指定这两个选项,而该文件又已经存在时,open将失败。咱们曾说起检查该文件是否存在以及建立该文件这两个操做时做为一个原子操做执行的。若是没有这样一个原子操做,那么可能会编写下列程序段:

    if((fd = open(pahtname, O_WRONLY)) < 0){

        if(errno == ENOENT){

            if((fd = creat(pathname,mode)) < 0)

                err_sys(...);

         }else{

                 err_sys(...);

        }

    }

    若是在open和creat之间,另外一个进程建立文件,那么就会引发问题。例如,若在这2两个函数调用之间,另外一个进程建立了该文件,而且写进了一些数据,而后,原先的进程执行这段程序中的creat,这时,刚由另外一个进程写上去的数据会被擦去。如若将这二者合并在一个原子操做中,这种问题也就不会产生。

    通常而言,原子操做指的是由多步组成的操做。若是该操做原子地执行,则那么执行完全部步骤,要么一步也不执行,不可能只执行全部步骤地一个子集。

dup和dup2函数

    下面两个函数均可用来复制一个现存的文件描述符:

#include"unistd.h"

int dup(int filedes);

int dup2(int filedes, int filedes2)

    有dup返回的新文件描述符必定是当前可用文件描述符中的最小数值。用dup2则能够用filedes2参数指定新描述符。若是filedes2已经打开,则将其关闭。若filedes等于filedes2,则dup2返回filedes2,而不关闭它。

    这些函数返回的新文件描述符与参数filedes共享同一个文件表项。图3-3显示了这种状况。

    newfd = dup(1);

    此函数开始执行时,假定下一个可用的描述符是3。由于两个描述符指向同以文件表项,因此他们共享同一文件状态以及同以当前便宜量。

    每一个文件描述符都有它本身的一套文件描述符标志。正如咱们将在下一节中说明的那样,新描述符的执行时关闭标志老是有dup函数清除。

    复制一个描述符的另外一种方法是使用fcntl函数实际上调用

dup(filedes);

    等效于

fcntl(filedes, F_DUPFD, 0);

    而调用

dup2(filedes,filedes2);

    等效于

close(filedes2);

fcntl(filedes,F_DUPFD,filedes2);

    后一种状况下,dup2并不彻底等同于close加上fcntl。

sync、fsync和fdatasync函数

    传统的UNIX实如今内核中没有缓冲区高速缓存或叶面高速缓存,大多数磁盘I/O都经过缓冲进行。当将数据写入文件时,内核一般先将该数据复制到其中一个缓冲区中,若是该缓冲区还没有写满,则并不将其排入输出队列,而是等待其写满或当内核须要充用该缓冲区以便存放其余磁盘块数据时,在将该缓冲排入输出队列,而后待其到达队首时,才进行实际的I/O操做。这种输出方式被称为延迟写。

    延迟写减小了磁盘读写次数,可是却下降了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并无写道磁盘上。当系统发生故障时,这种延迟可能形成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync函数。

#include"unistd.h"

int fsync(int filedes);

int fdatasync(int filedes);

void sync();

    sync函数只是将全部修改过的块缓冲区排入写队列,而后就返回,它并不等待实际写磁盘操做结束。

    一般称为update的系统受沪进程会周期性地调用sync函数。这就保证了按期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。

    fsync函数只对由文件描述符filedes指定的单一文件起做用,而且等待写磁盘操做结束,而后返回。fsync可用于数据库这样的应用程序,这种应用程序须要确保将修改过的块当即写到磁盘上。

    fdatasync函数相似于fsync,但它只影响文件的数据部分。而除数据外fsync还会同步更新文件的属性。

fcntl函数

    fcntl函数能够改变已打开的文件的性质。

#include"fcntl.h"

int fcntl(int filedes, int cmd,...);

    fcntl函数有5种功能:

    复制一个现有的描述符(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)

    咱们先说明这10种cmd值中前7种咱们将涉及与进程表项中各文件描述符向关联的文件描述符标志,以及每一个文件表项中的文件状态标志

    F_DUPFD:

    复制文件描述符filedes。新文件描述符做为函数值返回。它是还没有打开的各描述符中最小值。新描述符与filedes共享同一文件表项。可是,新描述符有它本身的一套文件描述符标志,其中FD_CLOEXEC文件描述符被清除(这表示该描述符在经过一个exec时仍保持有效)

    F_GETFD:

    对应于filedes的文件描述符标志做为函数值返回。当前只订一乐一个文件描述符标志FD_CLOEXEC

    F_SETFD

    对于filedes设置文件描述符标志。新标志值按第三个参数设置。

    F_GETFL

    对应于filedes的文件状态标志做为函数值返回。在说明open函数时,已说明了文件状态。

    不幸的是,三个访问方式标志(O_RDONLY O_WRONLY O_RDWR)并不各占1位。所以首先必须用屏蔽字O_ACMODE取得访问模式位,而后将结果与这三种值中的任一种做比较。

    F_SETFL

    将文件状态标志设置为第三个参数的值(取为正数值)。能够更改的几个标志是:O_APPEND/O_NONBLOCK/O_SYNC/O_DYSNC/O_RSYNC/O_FSYNC/O_ASYNC。

    F_GETOWN

    取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。

    F_SETOWN

    设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。

相关文章
相关标签/搜索