二、I/O模型

1、六个通用I/O函数

(1)fd = open(pathname, flags, mode):如果pathname的文件不存在,根据flags的参数,可以创建,mode制定新文件的权限,可以为空。返回文件描述符fd,如果打开时发生错误,返回-1,错误号errno标识错误原因。

  早期创建文件是用creat(pathname, mode)这个函数来实现的,现在它等价于:fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)

(2)numread = read(fd, buffer, count):从fd指代的文件中每次最多读取count字节的数据,存储到buffer中。返回实际读取到的字节数。

(3)numwrite = write(fd, buffer, count):从buffer中读取最多count字节的数据,写入到fd指代的文件中。返回实际写入的字节数。

(4)status = close(fd):释放fd以及与之相关的内核资源。

(5)off_t lseek(int fd, off_t offset, int whence):whence参数的值可为SEEK_SET(起点)、SEEK_CUR(当前文件偏移量)、SEEK_END(末尾),offset表示一个数字,加上whence指向实际文件的偏移位置。

(6)#include<sys/ioctl.h>   int ioctl(int fd, int request, .../*argp*/);   多用途I/O操作函数。

  request参数指定了将在fd上执行的控制操作,许多操作可能会用到后面...的不定参数。

小知识:文件空洞:如果文件偏移量已经跨越了文件结尾,再执行I/O操作。那么从文件结尾到新写入这段空间被称为文件空洞。读取文件空洞将返回以0填充的缓冲区。然而,文件空洞并不占用任何磁盘空间。核心转储文件是包含空洞文件常见的例子。

 

关于六个通用I/O函数的原型:

 #include <sys/stat.h>

 #include <fcntl.h>

 (1)int open(const char *pathname, int flags, .../*mode_t mode*/);

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

 #include <unistd.h>

 (2)ssize_t read(int fd, void *buffer, size_t count);

 (3)ssize_t write(int fd, void *buffer, size_t count);

 (4)int close(int fd);

 (5)off_t lseek(int fd, off_t offset, int whence);

 #include<sys/ioctl.h> 

 (6)int ioctl(int fd, int request, .../*argp*/);   

 

2、原子操作和竞争条件

竞争状态:操作共享资源的两个进程或线程,其结果将是一个无法预期的顺序。

原子操作:在多进程(线程)访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源)访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。

  例子一:以独占方式创建文件:使用open()时,若同时指定O_EXCEL与O_CREAT时,若要打开的文件已经存在,就会返回一个错误。这里保证进程是打卡文件的创建者。

  例子二:多个进程或线程同时往一个文件后面添加数据。必须保证原子操作,否则可能导致脏数据。

 

3、获取、修改文件的状态标志

#include<fcntl.h>  int fcntl(int fd, int cmd, ...);

(1)获取文件的状态标志:

  flags = fcntl(fd, F_GETFL);

  if (flags == -1)

    errExit("fcntl");

  测试文件是否以同步写方式打开:

  if (flags & O_SYNC)

    print("writes are synchronized\n");

(2)判断文件的访问模式:

  accessMode = flags & O_ACCMODE;

  if (accessMode == O_WRONLY || accessMode == O_RDWR)

    print("file is writable\n");

(3)修改文件的状态标志,仅能修改O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT,系统被会忽略对其它标志的修改操作:

  ●文件不是调用程序打开的

  ●文件描述符是通过open()之外的系统调用获取的。比如pipe()(创建一个管道,并返回两个fd分别对应管道的两端)、socket()(创建一个套接字并返回指向该套接字的fd)

  修改方法:

  int flags;

  flags = fcntl(fd, F_GETFL);

  if (flags == -1)

    errExit("fcntl");

  flags |= O_APPEND;

   if (fcntl(fd, SETFL, flags) == -1)

     errExit("fcntl");

 

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

(1)文件描述符表

●文件描述符标志:只有O_CLOEXEC(即close-on-exec标志)。

●文件指针:对打开文件句柄的引用。

(2)打开文件表

内核对所有打开文件维护了一个系统级的描述表格,存储了与一个打开文件相关的全部信息。

●文件偏移量:当前文件偏移,可在调用write()、read()、lseek()时进行更新。

●文件状态标志:O_NONBLOCK、O_ASYNC等,open()的flags参数。

●文件的访问模式:open()时所设置的只读、只写、读写模式。

●与信号驱动I/O相关的设置。

●对该文件i-node对象的引用。

(3)i-node表

●文件类型(常规文件、套接字、FIFO)和访问权限。

●一个指针,指向该文件所持有的锁的列表。

●文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。

 

5、复制文件描述符

#include <unistd.h>

(1)int dup(int oldfd);

假定正常情况下shell已经代表程序打开了0,1,2,那么在调用newfd=dup(1)的时候就会创建文件描述符1的副本,返回文件描述符的编号为3。

这个时候如果想让他返回文件描述符2,那么可以这样做:

close(2);

newfd = dup(1);

如果需要简化上述代码,可简化为dup2(1, 2);。

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

它会为oldfd参数所指定的文件描述符创建副本,其编号由newfd指定。如果newfd参数所指定的文件描述符之前已经打开,那么dup2()会首先将其关闭(dup2会忽略在newfd关闭期间的任何错误,所以更安全的做法依然是显式的close()将其关闭)。

如果oldfd并非有效的文件描述符,那么dup2()调用失败,返回错误EBADF,且不关闭newfd如果oldfd与newfd值相等,那么dup2()什么也不做,直接将其作为结果返回。

(3)newfd = fcntl(oldfd, F_DUPFD, startfd);

为oldfd创建一个副本,并且将使用大于等于startfd的最小未用值作为文件描述符编号。

总是能将dup()和dup2()改写为对close()和fcntl()的调用,虽然前者更简洁。

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

这里的flags只支持一个标志O_CLOEXEC,这将促使内核为新文件描述符设置close-on-exec标志(FD_CLOEXEC)。

 

6、在文件特定偏移处操作的I/O: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);

这里要求fd所指代的文件必须是可定位的,允许执行lseek调用。多线程应用为这些系统调用提供了用武之地,多个线程可同时对同一文件进行I/O操作,互相不会因为修改文件偏移量而受到影响。

 

7、分散输入和集中输出I/O:readv()和writev()

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

这些系统调用并不是对单个文件缓冲区进行读写操作,而是一次传输多个缓冲区的数据。iov定义了缓冲区:

struct iovec{

  void *iov_base;  //缓冲区的开始地址

  size_t iov_len;   //写进缓冲区或者从缓冲区读出的字节数大小

};

以下调用都实现了原子操作:

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

(2)writev()实现了集中输出,将iov所指代的所有缓冲区的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符fd所指代的文件中。这个动作从iov[0]所指定的缓冲区开始,按照数组顺序展开。

(3)现代BSD进一步提供了以下两个调用:

#include <sys/uio.h>

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

既能使用分散/集中I/O,又不受制于当前文件偏移量。

 

8、截断文件:truncate()和ftruncate()系统调用

#include <unistd.h>

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

int ftruncate(int fd, off_t length);

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

 

9、非阻塞I/O

在打开文件时指定O_NONBLOCK标志:

●若open()调用未能立即打开文件,则返回错误,而非陷入阻塞。有一种情况例外,调用open()操作FIFO可能会陷入阻塞。

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

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

 

10、大文件I/O

文件偏移off_t为一个有符号整型(负数-1代表错误),这样在32位系统下面最大支持的文件大小只有2^31-1(2GB),对于大文件的支持(LFS),有了如下两种办法:

(1)使用大文件操作的API:fopen64(),open64(),lseek64(),trancate64(),stat64(),mmap64(),setrlimit64()。增加的新数据类型:struct stat64,off64_t。但是这些用法现在已经过时。

(2)在编译程序时,将宏FILE_OFFSET_BITS的值定义为64。这个方法更可取,因为这使得无需修改源码即可获得LFS的功能。

 使用方法:

(a)编译的时候,cc -D_FILE_OFFSET_BITS=64 prog.c

(b)在源文件头文件前加入:#define _FILE_OFFSET_BITS 64

另外,这时off_t要在printf中打印出来的话,就需要long long类型(%lld)进行传递了。

 

11、创建临时文件的函数

(1)#include <stalib.h>

int ktemp(char *template);

模板参数采用路径名的形式,其中最后6个字符必须为XXXXXX。这6个字符将被替换,以保证文件名的唯一性。

文件拥有者必须对所建立的文件拥有读写权限,其它用户则没有任何操作权限。

打开文件时使用了O_EXCL标志,以保证调用者以独占方式访问文件。

通常打开临时文件不久后,就会调用ulink()系统调用将其删除。

#include <stdio.h>

FILE *tmpfile(void);

tmpfile函数执行成功后返回一个文件流供stdio库函数使用。文件流关闭后自动删除临时文件。

 

posted on 2018-08-02 02:32  Kidyy  阅读(130)  评论(0)    收藏  举报

导航