引言

在后台开发中,对于文件I/O我们通常不使用C语言封装的fopen、fread、fwrite标准I/O,而是直接使用Linux提供的系统调用函数。因为这些系统调用没有使用用户缓冲区,我们直接与内核打交道,效率更高,且可以自己定制一些符合应用场景的操作。下面介绍Linux用于文件I/O的数据结构,以及一些具体的系统调用函数。

文件描述符

所有打开的文件都通过文件描述符引用,文件描述符只在当前进程有效,因为每个进程有一个PCB结构体,PCB包含一个文件描述符表。

文件描述符0对应标准输入、1对应标准输出、2对应标准错误,这些是在进程创建时默认绑定的文件描述符。

以上分别对应unistd.h中的STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。

每个进程的最大文件描述符可通过ulimit -a命令查看,并可通过ulimit -n 数字设置。

无缓冲文件IO

我们这里所说的无缓冲IO指的是无用户缓冲区,如fopen、fwirte、fread等函数,进程会在用户进程空间维护缓冲区,然后内核还有缓冲区,最后才是磁盘。而无缓冲IO是指只有内核缓冲区而无用户缓冲区,并不是没有任何缓冲区。

内核用于文件I/O的数据结构

每个进程在进程表中都有一个记录项,记录项包含一张打开文件描述符表

在打开文件描述符表中,每个描述符占用一项:

  • 文件描述符标志:目前只有CLOEXEC。
  • 指向文件表项的指针

内核为所有打开文件维持一张文件表,每个文件表项包括:

  • 文件状态标志:包括文件类型和访问权限。
  • 当前文件偏移量
  • 指向该文件inode节点的指针

他们之间的关系如下:


不同的文件表项可以指向相同的文件(i节点即索引结点),这可以使不同的进程有它自己的对该文件的偏移量和打开访问权限。

不同的文件描述符可以指向相同的文件表项。如在fork后,父子进程的每个相同文件描述符指向同一个文件表项。

【PS】:注意文件描述符标志(file descriptor flags)和文件状态标志(file status flags)的区别。

open函数

#include <fcntl.h>

int open(char* path, int flag, .../* mode_t mode */);

返回值:
成功返回文件描述符
失败返回-1

flag参数可以为:

  • 必选参数:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_EXEC(只执行)、O_SEARCH(只搜索,应用于目录文件)。这5个参数互斥,即只能指定一个。
  • 可选参数,按位或:O_APPEND(每次时都追加到文件尾端)、O_CLOEXEC(将FD_CLOEXEC常量设置为文件描述符标志)、O_CREAT(若文件不存在则创建它,用该参数时需要指定第三个参数即文件的权限位mode如0644,存在则直接打开)、O_DIRECTORY(若path不是目录,则返回错误)、O_EXCL(与O_CREAT同时指定,若文件已经存在,则出错。可以原子地测试和创建文件。)、O_NONBLOCK(设置非阻塞)、O_SYNC(每次write等待物理I/O完成)、O_TRUNC(若文件存在,且以只写或读写打开,则将其长度截断为0)。

creat函数

#include <fcntl.h>

int creat(char* path, mode_t mode);

返回值:
成功返回以只写打开的文件描述符
失败则返回-1

creat函数的一个缺点是它以只写方式打开新创建的文件。

close函数

#include <fcntl.h>

int close(int fd);

返回值:
成功返回0
失败返回-1

当进程终止时,内核会自动关闭它打开的所有文件。

lseek函数

每个打开文件都有一个当前文件偏移量(在系统全局的打开文件表项中)。读、写操作都从当前偏移量开始,并使偏移量增加所读的字节数。按系统默认,当打开一个文件时,若没有指定O_APPEND选项,该偏移量被设置为0。

#include <fcntl.h>

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

返回值:
成功返回新的文件偏移量
失败返回-1

whence可以取以下值:

  • SEEK_SET:将文件偏移量设置为距文件开始处offset字节处,offset为正。
  • SEEK_CUR:将文件偏移量设置为距现在位置offset字节处,offset可正可负。
  • SEEK_END:将文件偏移量设置为距文件尾端offet字节处,offset可正可负。

文件偏移量可以大于文件当前长度,对该文件的下一次写将加长该文件,并在文件中形成空洞。位于文件中但没有写过的字节都被读为0.

read函数

#include <unistd.h>

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

返回值:
成功返回读到的字节数,若读到文件尾,返回0
失败返回-1

读操作从文件当前偏移量开始,读成功后,偏移量增加读到的字节数。

write函数

#include <unistd.h>

ssize_t write(int fd, void* buf, size_t nbytes);

返回值:
成功返回已写的字节数
失败返回-1

write返回值通常与参数nbytes的值相同,否则表示出错。

read和write的阻塞、非阻塞

read和write对于常规文件不会阻塞,一定会在有限时间内返回。read终端或网络设备时可能阻塞,终端在用户输入换行时才会刷新输入缓冲区,网络设备则不确定何时有输入。写网络设备可能阻塞。可以在open文件时指定O_NONBLOCK设定为非阻塞,则read时需要循环读

dup和dup2函数

#include <unistd.h>

int dup(int fd);
int dup2(int fd, int fd2); 返回值:
成功返回文件描述符
失败返回-1

dup返回最小未用的文件描述符,使返回的文件描述符指向和fd相同的文件表项。

dup2先关闭fd2,然后使fd2指向fd指向的文件表项,并返回fd2。

新的描述符的执行时关闭标志(close-on-exec)总是被dup函数清除

sync和fsync函数

#include <unistd.h>

void sync(void);
int fsync(int fd);

当向文件中写入数据时,内核先将数据复制到内核缓冲区,然后排入队列,晚些时候再写入磁盘,被称为延迟写

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

fsync只对由文件描述符fd指定的文件起作用,并且等待写磁盘操作结束后才返回,并不立即返回

stat函数

stat函数用于查看文件的属性信息。

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> int stat(char* file_path, struct stat* st);
int lstat(char* file_path, struct stat* st); // 相比于stat,对于符号链接,lstat获取的是符号链接本身的属性信息 参数:
file_path表示文件路径,st表示返回的stat结构体,为传出参数。
返回值:
成功返回0,失败返回-1。 stat定义如下: struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

获取文件类型

方法①:按位与。

switch(st.st_mode & S_IFMT){
case S_IFBLK: printf("block device\n"); break;
case S_IFCHR: printf("character device\n"); break;
case S_IFDIR: printf("directory\n"); break;
case S_IFIFO: printf("FIFO/pipe\n"); break;
case S_IFLNK: printf("symlink\n"); break;
case S_IFREG: printf("regular file\n"); break;
case S_IFSOCK: printf("socket\n"); break;
default: printf("unknown?\n"); break; }

方法②:使用宏。

if(S_ISREG(st.st_mode)){
...
}else if(S_ISBLK(st.st_mode)){
...
}
...

获取文件权限:按位与。

if(st.st_mode & S_IRUSR){ // 文件所有者是否有读权限
...
}
if(st.st_mode & S_IWGRP){ // 文件所有者组是否有写权限
...
}
if(st.st_mode & S_IXOTH){ // 其他用户是否对文件有执行权限
...
}
...

fcntl函数

#include <fcntl.h>

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

返回值:
成功返回值依赖于cmd
失败返回-1

cmd可以取以下值:

  • F_DUPFD、F_DUPFD_CLOEXEC复制描述符
  • F_GETFD、F_SETFD设置获取文件描述符标志,目前只能改变O_CLOEXEC
  • F_GETFL、F_SETFL设置获取文件状态标志,只能改变部分属性:O_APPENDO_ASYNCO_NONBLOCKO_DIRECTO_NOATIMEPS:改变文件描述符标志和文件状态标志都需要先获取原来的,然后在原来的基础上按位或(设置)、按位与非(取消设置),最后设置。
  • F_GETOWN、F_SETOWN设置获取异步IO所有权
  • F_GETLK、F_SETLK、F_SETLKW设置获取记录锁

getcwd函数

getcwd用于获取进程的当前工作目录。

#include <unistd.h>

char* getcwd(char* buf, size);

参数:
buf为预分配的内存区,size为buf的大小。
返回值:
进程的当前工作目录保存到buf中。
失败返回NULL。

chdir函数

chdir用于改变进程的当前工作目录。

#include <unistd.h>

int chdir(const char* path);

参数:
path为目标目录,可为绝对或相对路径。 返回值:
成功返回0。
失败返回-1。

opendir函数

opendir用于打开一个目录文件。

#include <sys/types.h>
#include <dirent.h> DIR* opendir(const char* path); 参数:
path为目录路径
返回值:
失败返回NULL。
成功返回DIR结构体指针。

readdir函数

readdir用于读取目录文件中的目录项。

#include <dirent.h>

struct dirent* readdir(DIR* dir);

返回值:
每次读取DIR指向的目录中的一个条目,若需要遍历所有条目,需要循环调用readdir。readdir还会读取到.和..即当前目录和父目录。 返回值:
失败返回NULL,并设置error。
若所有条目都读取完,则也返回NULL,但不设置error。 dirent定义如下: struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
}; d_ino为文件对应的索引结点编号。 d_type取值如下,可以用来判断文件类型: DT_BLK This is a block device. DT_CHR This is a character device. DT_DIR This is a directory. DT_FIFO This is a named pipe (FIFO). DT_LNK This is a symbolic link. DT_REG This is a regular file. DT_SOCK This is a UNIX domain socket. DT_UNKNOWN The file type could not be determined. d_name为文件名,以'\0'结尾,最大长度为256。

closedir函数

closedir用于关闭打开的目录文件。

#include <sys/types.h>
#include <dirent.h> int closedir(DIR* dir); 参数:
dir为目录结构体
返回值:
失败返回-1。

参考资料

  • 《APUE》
  • 《Linux内核设计与实现》

无缓冲文件IO和目录操作的更多相关文章

  1. Unix无缓冲文件操作函数、文件信息查询

    问题描述:         Unix无缓冲文件操作函数.文件信息查询 问题解决:        struct stat 结构体信息: 具体代码: 具体源文件:

  2. Linux下用文件IO的方式操作GPIO(/sys/class/gpio)

    通过sysfs方式控制GPIO,先访问/sys/class/gpio目录,向export文件写入GPIO编号,使得该GPIO的操作接口从内核空间暴露到用户空间,GPIO的操作接口包括direction ...

  3. Linux下用文件IO的方式操作GPIO(/sys/class/gpio)(转)

    通过sysfs方式控制GPIO,先访问/sys/class/gpio目录,向export文件写入GPIO编号,使得该GPIO的操作接口从内核空间暴露到用户空间,GPIO的操作接口包括direction ...

  4. TX2 用文件IO的方式操作GPIO

    概述 通过 sysfs 方式控制 GPIO,先访问 /sys/class/gpio 目录,向 export 文件写入 GPIO 编号,使得该 GPIO 的操作接口从内核空间暴露到用户空间,GPIO 的 ...

  5. GPIO编程1:用文件IO的方式操作GPIO

    概述 通过 sysfs 方式控制 GPIO,先访问 /sys/class/gpio 目录,向 export 文件写入 GPIO 编号,使得该 GPIO 的操作接口从内核空间暴露到用户空间,GPIO 的 ...

  6. [转] Linux下用文件IO的方式操作GPIO(/sys/class/gpio)

    点击阅读原文 一.概述 通过 sysfs 方式控制 GPIO,先访问 /sys/class/gpio 目录,向 export 文件写入 GPIO 编号,使得该 GPIO 的操作接口从内核空间暴露到用户 ...

  7. Java文件IO流的操作总结

    Java中的IO操作涉及到的概念及相关类很多,很容易弄混,今天特来整理总结一下,并附上一份完整的文件操作的代码. 概念解析 读和写 流就是管道,向管道里面写数据用输出流:write 从管道里面读数据, ...

  8. linux文件IO操作篇 (一) 非缓冲文件

    文件IO操作分为 2 种 非缓冲文件IO 和 缓冲文件IO 它们的接口区别是 非缓冲 open() close() read() write() 缓冲 fopen() fclose() fread() ...

  9. PHP 文件与目录操作函数总结

    >>>文件操作 打开 fopen();    打开文件 读取内容 fread();    从文件指针 handle 读取最多 length 个字节 readfile();    读入 ...

随机推荐

  1. C#验证对象中的属性是否为空的共通方法

    在后台接口处理时,经常需要对请求的参数做验证.因此提取了共通方法,方便进行判断. /// <summary> /// 数据验证工具类 /// </summary> public ...

  2. go实现pdf电子签名-自动识别签名位置

    一. 技术选型 由于要识别签名位置,所以得要能解析pdf的文本布局,要能得到每个布局元素的文本位置坐标.而最终的签名需要合成到pdf上,所以还需要有编辑pdf的需求. pdf布局分析:pdfminer ...

  3. JS获取当前页面的网址链接

    JavaScript获取当前页面的URL .链接地址 var currUrl = decodeURIComponent(location.href.split('#')[0]); java获取方式:h ...

  4. VS2015常用的字体 和 插件

    Inconsolata 字体 自己常使用 Inconsolata 作为代码字体, 阅读代码非常舒适. 其他插件 GoToDef.vsix , 查看 声明非常方便. (ctrl+鼠标左键) Viasfo ...

  5. 【九度OJ】题目1174:查找第K小数 解题报告

    [九度OJ]题目1174:查找第K小数 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1174 题目描述: 查找一个数组的第 ...

  6. 【LeetCode】475. Heaters 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历 日期 题目地址:https://leetcod ...

  7. 【LeetCode】380. Insert Delete GetRandom O(1) 解题报告(Python)

    [LeetCode]380. Insert Delete GetRandom O(1) 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxu ...

  8. 机器人的舞蹈(hdu 2232)

    机器人的舞蹈 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Subm ...

  9. Chapter 14 G-estimation of Structural Nested Models

    目录 14.1 The causal question revisited 14.2 Exchangeability revisited 14.3 Structural nested mean mod ...

  10. Java初学者作业——使用eclipse简单的输出(打印)游戏菜单

    返回本章节 返回作业目录 需求说明: 使用eclipse创建Java项目,在Java程序中输出游戏菜单. 为 Java 程序添加单行和多行注释. 实现思路: 在 eclipse 中创建项目及Java类 ...