0%

Linux系统编程-文件I/O


专注于用户空间的系统级编程–即内核之上的所有内容。

open()

函数头文件及原型:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

参数pathname指向欲打开的文件路径字符串。文件打开方式是根据参数flags值来确认的。

  • O_RDONLY 以只读方式打开文件
  • O_WRONLY 以只写方式打开文件
  • O_RDWR 以可读写方式打开文件

上述三种旗标是互斥的, 也就是不可同时使用, 但可与下列的旗标利用OR(|)运算符组合.

  • O_CREAT 若欲打开的文件不存在则自动建立该文件.
  • O_EXCL 如果O_CREAT 也被设置, 此指令会去检查文件是否存在. 文件若不存在则建立该文件, 否则将导致打开文件错误. 此外, 若O_CREAT 与O_EXCL 同时设置, 并且欲打开的文件为符号连接, 则会打开文件失败.
  • O_NOCTTY 如果欲打开的文件为终端机设备时, 则不会将该终端机当成进程控制终端机.
  • O_TRUNC 若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为0, 而原来存于该文件的资料也会消失.
  • O_APPEND 当读写文件时会从文件尾开始移动, 也就是所写入的数据会以附加的方式加入到文件后面.
  • O_NONBLOCK 以不可阻断的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中.
  • O_NDELAY 同O_NONBLOCK.
  • O_SYNC 以同步的方式打开文件.
  • O_NOFOLLOW 如果参数pathname 所指的文件为一符号连接, 则会令打开文件失败.
  • O_DIRECTORY 如果参数pathname 所指的文件并非为一目录, 则会令打开文件失败。

参数mode则有下列数种组合, 只有在建立新文件时才会生效, 此外真正建文件时的权限会受到umask值所影响, 因此该文件权限应该为 (mode-umaks).

  • S_IRWXU00700 权限, 代表该文件所有者具有可读、可写及可执行的权限.
  • S_IRUSRS_IREAD, 00400 权限, 代表该文件所有者具有可读取的权限.
  • S_IWUSRS_IWRITE, 00200 权限, 代表该文件所有者具有可写入的权限.
  • S_IXUSRS_IEXEC, 00100 权限, 代表该文件所有者具有可执行的权限.
  • S_IRWXG 00070 权限, 代表该文件用户组具有可读、可写及可执行的权限.
  • S_IRGRP 00040 权限, 代表该文件用户组具有可读的权限.
  • S_IWGRP 00020 权限, 代表该文件用户组具有可写入的权限.
  • S_IXGRP 00010 权限, 代表该文件用户组具有可执行的权限.
  • S_IRWXO 00007 权限, 代表其他用户具有可读、可写及可执行的权限.
  • S_IROTH 00004 权限, 代表其他用户具有可读的权限
  • S_IWOTH 00002 权限, 代表其他用户具有可写入的权限.
  • S_IXOTH 00001 权限, 代表其他用户具有可执行的权限.

例如:S_IWUSR|S_IRUSR|S_IWGRP|S_IRGRP|S_IROTH 等效于 0664

返回值:若所有欲核查的权限都通过了检查则返回0 值, 表示成功, 只要有一个权限被禁止则返回-1. 错误代码:

  • EEXIST 参数pathname 所指的文件已存在, 却使用了O_CREAT 和O_EXCL 旗标.
  • EACCESS 参数pathname 所指的文件不符合所要求测试的权限.
  • EROFS 欲测试写入权限的文件存在于只读文件系统内.
  • EFAULT 参数pathname 指针超出可存取内存空间.
  • EINVAL 参数mode 不正确.
  • ENAMETOOLONG 参数 pathname 太长.
  • ENOTDIR 参数pathname 不是目录.
  • ENOMEM 核心内存不足.
  • ELOOP 参数pathname 有过多符号连接问题.
  • EIO I/O 存取错误.

creat()

O_WRONLY | O_CREAT | O_TRUNC 的组合经常被使用,因此专门有个系统调用提供这个功能:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

典型的creat()调用如下:

int fd;
fd = creat(filename, 0664);

等效于:

int fd;
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0664);

read()

每次调用read()函数,会从fd指向的文件的当前偏移开始读取len字节到buf所指向的内存中。调用成功则返回写入buf中的字节数;
出错时,返回-1,并设置errno值。函数原型:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t len);

调用read()的返回值:

  • 返回值等于len,结果和预期一致
  • 返回值大于0,但小于len,有很多原因:读取过程中信号中断、出错、提前到达EOF等
  • 返回值为0,EOF,没有更多可读数据
  • 返回值为-1,errno为EINTR,表示读取任何字节之前接收到信号。调用可以重新执行
  • 返回值为-1,errno为EAGAIN,表示当前没有数据可用,读取操作会阻塞,请求应稍后重新执行
  • 返回值为-1,errno非EINTR或EAGAIN,表示更严重错误,重新执行也不会成功

读取所有字节且处理了异常情况的示例:

ssize_t ret;
while(len != 0 && (ret = read(fd, buf, len)) != 0) {
    if(ret == -1) {
        if(errno == EINTR)
            continue;
        perror("read");
        break;
    }
    len -= ret;
    buf += ret;
}

write()

write()调用会从文件描述符fd指向的文件的当前位置开始,将buf中至多count个字节写入到文件中。
执行成功时返回写入的字节数,并更新文件位置。出错时,返回-1,并设置errno值。函数原型:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

同步I/O

为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了系统调用sync()、fsync()和fdatasync(),函数原型:

#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);

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

fsync函数只对由文件描述符fd指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
该效用会回写数据和元数据,如创建的时间戳以及索引节点中的其他属性。
因为文件的数据和metadata通常存在硬盘的不同地方,因此fsync至少需要两次IO写操作。

fdatasync函数类似于fsync,区别在于fdatasync只会写入数据以及之后要访问文件所需要的元数据,
例如,会将文件大小回写,但是不保证所有非基础的元数据也写到磁盘上,比fsync函数执行更快。

关闭文件

当程序完成对某个文件的操作后,可以通过系统调用close()取消文件描述符到对应文件的映射:

#include <unistd.h>
int close(int fd);

调用之后,先前给定的文件描述符不再有效,内核可以重用它。

lseek()

每一个已打开的文件都有一个读写位置, 当打开文件时通常其读写位置是指向文件开头,
若是以附加的方式打开文件(如O_APPEND), 则读写位置会指向文件尾.
当read()或write()时, 读写位置会随之增加,lseek()便是用来控制该文件的读写位置.
参数fd为已打开的文件描述词, 参数pos为根据参数origin来移动读写位置的位移数.

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t pos, int origin);

参数 origin 为下列其中一种:

  • SEEK_SET 参数pos即为新的读写位置.
  • SEEK_CUR 以目前的读写位置往后增加pos个位移量.
  • SEEK_END 将读写位置指向文件尾后再增加pos个位移量. 当origin值为SEEK_CUR 或SEEK_END 时, 参数pos允许负值的出现.

示例:

  • 欲将读写位置移到文件开头时:lseek(int fd, 0, SEEK_SET);
  • 欲将读写位置移到文件尾时:lseek(int fd, 0, SEEK_END);
  • 想要取得目前文件位置时:lseek(int fd, 0, SEEK_CUR);

返回值:当调用成功时则返回目前的读写位置, 也就是距离文件开头多少个字节. 若有错误则返回-1, errno 会存放错误代码.

文件截短

Linux提供两个系统调用支持文件截短,原型如下:

#inclide <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t len);
int truncate(const char *path, off_t len);

这两个系统调用都将文件截短为参数len指定的长度。成功时返回0,出错时返回-1并设置相应errno值。
len小于原文件大小时,新文件长度变为len,介于之前len和原文件大小之间的数据被丢弃,不可再读。
len大于原文件大小时,新扩展出来的字节使用0来填充。