二、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库函数使用。文件流关闭后自动删除临时文件。
浙公网安备 33010602011771号