UNIX环境高级编程--文件I/O(一)

时间:2021-01-24 22:01:59

这里讲述的I/O包括四部分的:文件I/O、标准I/O库、高级I/O,终端I/O。这四部分在《unix环境高级编程》中是分开的三章,这里提取放在一块说更有课比性。

一、文件I/O

           在对文件进行读写操作前,需要先打开该文件。内核为每个进程维护一个打开文件的列表,该表被称为文件表(file table)。该表由一些叫做文件描述符(file descriptors)的非负整数进行索引。列表中的每项均包括一个打开文件的信息,其中包括一个指向文件备份inode内存拷贝的指针和元数据(例如文件位置和访问模式等)。用户空间和内核空间都把文件描述符作为每个进程的唯一cookies。打开一个文件返回一个文件描述符,接下来的操作(读写等等)则把文件描述符作为基本参数。

    子进程默认会获得一份父进程的文件表拷贝。其中打开文件列表、访问模式,当前文件位置等信息都是一致的。进程中文件表的变化(例如子进程关闭文件)也不会影响其他进程的文件表。可以让子进程和父进程共享后者的 文件表。

    文件描述符不仅仅用于普通文件的访问,也用于访问设备文件、管道、目录以及快速用户空间锁、FIFOs和套接字。遵循一切皆文件的理念,任何你能读写的东西都可以用文件描述符来访问。

    可用的文件I/O函数--打开文件、读文件、写文件等、unix系统中的大多数I/O需用到的五个函数:open、read、write、lseek以及close..这些函数经常被称为不带缓冲区的I/O(与下面将要讲述的标准I/O相对照)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,但是,他们是POSIX.1和Single UNIX Specification的组成部分。还需要通过文件I/O和open函数的参数来讨论原子操作的概念,只要涉及在多个进程间共享资源,这个概念就很重要。多个进程间是如何贡献文件,以及所涉及的内核数据结构。还会说明dup、fcntl、sync、fsync和ioctl函数。

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char *name,int flags);

int open(const char *name,int flags,mode_t mode);

flags必须是以下之一:O_RDONLY、O_WRONLY、O_RDWR

O_ASYNC

当指定文件可写或者可读时产生一个信号(默认为SIGIO)。这个标志仅用于终端盒套接字,不能用于普通文件。

O_CREAT

若此文件不存在,则创建它。使用此选项时,需要第三个参数mode,用其指定该新文件的访问权限位。

O_EXCL

如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是都存在,如果不存在,则创建此文件,这使此时和创建两者称为一个原子操作。

O_TRUNC

如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0

O_NONBLOCK

如果pathname值的是一个FIFO,一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞模式。

O_NOCTTY

如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端。

O_DSYNC

使每次write等待物理I/O操作完成,但是如果写操作并不影响读取放写入的数据,则不等待文件属性被更新

O_RSYNC

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

O_SYNC

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

 当创建文件时,mode参数提供新建文件的权限。系统并不在该次打开文件时检查权限,mode参数是常见的UNIX权限位集合,像八进制数0644。当O_CREAT给出时则需要mode参数,一定要谨记。

O_WRONLY|O_CREAT|O_TRUNC组合经常被使用,以至于专门有个系统调用来实现。

#include <sys/types.h>

#include <sys/stat.h>

int creat(const char *name,mode_t mode);

fd=creat(file,0644);

等价于:

fd=open(file,O_WRONLY|O_CREATE|O_TRUNC,0644);

上述两个函数出错都返回-1,成功为一个文件描述符。

#include <unistd.h>

ssize_t read(int fd,void *buf,size_t len);

该系统调用从有fd指向的文件的当前偏移量至多读len个字节到buf中,成功后,将返回写入buf中的字节数。出错则返回-1,并设置errno。有些错误时可以恢复的,例如,当read()调用在未读取任何字符前被一个信号打断,它会返回-1(如果是0,则可能和EOF混),并设置errno为EINTR。在这种情况下,可以重新提交读取请求。

read()返回结构的可能值:

1)调用返回一个等于len的值。所有len字节存储到buf中。

2)返回一个大于零小于len的值。读取的字节存入buf中。这种情况出现在一个信号打断了读取过程,或在读取中发生了一个错误,有效字节大于零,但比len字节少时,或者在读入len个字节前已抵达EOF。再次进场读取(更新buf和len的值)将读入剩余字节到buf的剩余空间中,或者之处问题发生的原因(有待验证)。

3)返回0。标志着EOF,没有数据可读。

4)调用阻塞了,因为没有可用的用来读取的数据,这在非阻塞模式下不会发生。

5)返回-1,且errno为EINTR。表示在读入字节之前收到了一个信号,可以重新进行调用。

6)返回-1,且errno为EAGAIN。表示读取会因没有可用的数据而阻塞,而读请求应该在之后重开,在非阻塞模式下发生。

7)返回-1,且errno设置不同于EINTR或EAGAIN的值,表示某种更严重的错误。

#include <unistd.h>

ssize_t write(int fd,const void *char,ssize_t len);

write调用从由文件描述符fd引用文件的当前位置开始,将buf中至多count个字节写入文件中,成功时,返回写入字节数,并更新文件位置。错误时,返回-1,并将errno设置为响应的值。一个write()可以返回0,只是表示写入了另个字节。write()不太可能返回一个部分写的结果。而且,对write()系统掉哟个来说没有EOF情况,对于普通文件,除非发生一个错误,否则write()将保证写入所有的请求,但是write返回出现-1情况也有可能是应为EINTR的中断的原因。

    在通过O_APPEND打开时,写操作从当前文件末尾开始写,这在两个进程同时操作一个文件时比较有用。它保证文件位置总是指向文件末尾,这样所有的写操作总是追加的,即便是多个写着。可以认为每个写请求之前的文件位置更新操作时原子操作。

    当一个write()调用返回时,内核已将所提供的缓冲区数据复制到了内核缓冲区中,但却没有保证数据已写到目的文件。

    文件偏移:按系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0.

#inlcude <unistd.h>

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

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

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

若whence是SEEK_CUR,则该文件的偏移量为从当前位置开始offset个字节,可正可负。

若whence是SEEK_END,则该文件的偏移量为从文件的末尾开始offset个字节,可正可负。

    文件偏移量可以大于文件的当前长度,在这种情况下,对该问价的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0.文件中的空洞并不要求在磁盘上占用存储区。

#include <unitd.h>

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

这个调用从文件描述符fd的pos文件位置读取count个字节到buf中。

ssize_t pwrite(int fd,const void * buf,size_t count,off_t pos);

这个调用从文件描述符fd的pos文件位置写count个字节到buf中。

    在调用结束后,它们不会修改文件位置。相当于在调用read()和write()前使用lseek()进行定位,不过这种操作性强;操作结束后不修改文件位置指针;同时也避免了任何在使用lseek()时可能出现的潜在竞争。

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

    UNIX系统支持在不同进程间共享打开的文件。在说明共享之前,先介绍内核用于所有IO的数据结构。

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

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

  • 文件描述符标志。
  • 指向一个文件表项的指针。

    2)内核为所有打开文件维护一张文件表,每个文件表项包含:

  • 文件状态标志(读、写、添写、同步和非阻塞等)
  • 当前文件偏移量
  • 指向该文件v节点表项的指针。

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

    下图显示了一个进程的三张表之间的关系。该进程有两个不同的打开的文件:一个文件打开为标准输入,另一个打开为标准输出,这张安排对于不同进程之间共享文件的方式非常重要。

UNIX环境高级编程--文件I/O(一)

     如果两个独立进程各自打开了同一个文件,假定第一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由:这种安排使每个进程都有他自己的对文件的当前偏移量。

1)在完成每个write后,在文件表项中的当前文件偏移量既增加缩写的字节数。如果这使文件偏移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量(也就是该文件加长了)

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

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

4)lseek函数只修改文件表项中的当前文件偏移量,没有进行任何I/O操作。UNIX环境高级编程--文件I/O(一)

    注意文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个描述符,而后者则适用于指向给定文件表项的任何进程中的所有描述符。注意理解两者的区别。

    上面讲述的一切对于多个进程读同一个文件都是能正确工作,每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一个文件时,则可能预期不到的结果。为此,原子操作的概念也就产生了,同时产生了两个函数。

    同时UNIX实现在内核中设有缓冲区告诉缓存或页面告诉缓存,大多数磁盘I/O通过缓冲进行。当将数据写入文件时,内核通常先将数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待期写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达对首时,才进行实际的I/O操作。


#include <unistd.h>

int dup(int filedes);

int dup2(int filedes,int fileses2);

由dup返回的新文件描述符一定是当前可用文件描述符中最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果fiedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回fiedes2,而不关闭它。这些函数返回的新文件描述符与参数filedes共享同一个文件表项。

例如:newfd=dup(1);

当此函数开始执行时,假设下一个可用的描述符为3,。因为两个描述符向同一个文件表项,所以他们共享同一个文件状态标志(读、写、添写等)以及同一当前文件偏移量。

    复制一个描述符的另一个方法是fcntl函数:

调用dup(filedes);

等效于fcntl(filedes,F_DUPFD,0);

而调用dup2(filedes,filedes2);

等效于

close(filedes2);

fcntl(filedes,F_DUPFD,filedes2);

在后一种情况下,dup2并不完全等同于close加上fcntl。区别为:

    dup2是一个原子操作,而close及fcntl则包括两个函数调用。有可能在close和fcntl之间插入执行信号捕获函数,它可能修改文件描述符。

    dup2和fcnlt有某些不同的errno。


#include <unistd.h>

int fsync(int filedes);

int fdatasync(int filedes); //返回值:若成功则返回0,若出错则返回-1

voud sync(void);


PS:

    当一个应用发起一个read()系统调用,就开始ile一个奇妙的路程,C库提供了系统调用的定义,而在编译器调用转化为适当的陷阱态。当一个用户空间进程转入内核态,则转交系统调用处理器处理,最终交给read()系统调用,内核确认文件描述符所对应的对象类型。然后内核调用与相关类型对应的read()函数、对于文件系统而言,这个函数是文件系统代码的一部分。然后该函数继续其工作---举例来说,从文件系统中读取数据并把数据返回给用户空间的read()调用,该调用返回复制数据到用户空间的系统调用处理器,然后将数据复制到用户空间,最后read()系统调用返回而进程继续。

    其中还需要注意的一点就是:对于unix内核而言,文本文件和二进制代码文件没有区别(但是在下一节中的标准库I/O就会有所不同)


两个重要的函数:

#include <unistd.h>

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

//若成功则依赖于cmd,若出错则返回-1。

第三个参数可能是一个整数,也有可能是一个指向结构的指针。

函数的五个功能:

1)复制一个现有的描述符(cmd=F_DUPFD)

2)获得/设置文件描述符标记(cmd=F_FETFD或F_SETFD)

3)

    #include <unistd.h>

#include <sys/ioctl.h>

#include <stropts.h>

int ioctl(int filedes,int request,.....); //若出错则返回-1,若成功则放回其他值。