【APUE】——文件I/O

linux基础

引言

在UNIX系统中可用的文件I/O函数——打开文件、读文件、写文件等。UNIX系统中大多数文件I/O只需用5个函数:open、read、write、lseek、close。

文件描述符

文件描述符贯穿sysio系统调用的类型

对Kernel而言,所有打开的文件都通过文件描述符引用。文件描述符就是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或create返回的文件描述符标识该文件,使用这个变量进行后续的文件操作。
UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准出错输出相关联。这可以理解为Shell的惯例,遵守就可以。
文件描述符的变化范围是0~OPEN_MAX,现在最多打开1023个。

open函数

调用oen函数可以打开或创建一个文件。

#include "fcntl.h"
int open(const char *pathname, int flags, mode_t mode);

该函数参数数量不定,根据具体的调用会有不同。对于open而言,仅当创建新文件的时候才使用第三个参数

  • pathname:是要打开或创建的文件的名字。

  • flags:此函数的选项参数,可以使用下面的一个或多个常量进行或运算:

    • O_RDONLY:只读打开。

    • O_WRONLT:只写打开。

    • O_RDWR:读写打开。

上面三个常量必须指定一个方式,下面还有可选参数:
O_APPEND:附加,每次写的时候都追加到文件的末尾。
O_CREATE:如果此文件不存在,则创建它。……

  • 返回值:返回的是文件描述符,其一定是最小的未用描述符数值。

create函数

#include <fcntl.h>
int creat(const char *pathname, mode_t mode);

这个相当于open函数,第二个参数为(O_CREATE | O_WRONLY | O_TRUNC)

close函数

关闭打开的文件,类似malloc - free中的free

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

fd:文件描述符。

read函数

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

返回值:若读取成功,返回读取到的字节数,若已到文件结尾返回0,错误返回-1

有很多情况可能是实际读到的字节数小于要求读的字节数:

  • 读普通文件时候,读到要求数量之前就已经到结尾了。

  • 从终端设备读时,一次只能读一行。

  • 从网络读时,网络中的缓冲机制造成返回值小于所要求读的字节数。

  • 读队列时,队列包含的字节小于所需的数量。

  • 从某些面向记录的设备时,一次只能读一个记录。

write函数

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

其返回值通常与count相同即写入的数据数量,不相同则表示出错。出错可能原因是磁盘已满……
对于普通文件,写操作从文件的当前偏移量开始。

lseek函数

每打开一个文件都会一个与相关联的“当前文件偏移量”。它通常是一个非负整数,用以标识从文件开始处计算的字节数。
通常,读、写操作都从当前文件的偏移量处开始,并使增加所读写写的字节数。系统默认打开文件偏移量为0。

可以调用lseek显式的设置打开文件的偏移量,seek为请求的意思。

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

参数fd为文件标识符。
参数offset的解释与参数whence有关

  • 如果whence是SEEK_SET(设置请求(偏移)),则该文件的偏移量设置为距文件开始处offset个字节处。

  • 如果whence是SEEK_CUR(当前请求(偏移)),则该文件的偏移量设置为当前值加offset(offset可以带负号)。

  • 如果whence是SEEK_END(结尾请求(偏移)),则该文件的偏移量设置为距文件结束的offset位置(可加可减)。

返回值用于确认设计的文件是否可以设置偏移量,如果文件描述符引用的是一个队列,或者网络套接字,则lseek返回返回-1。(我们判错的时候要判断返回值是不是等于-1,因为偏移量可以为负数)。

设置偏移量主要用于下一个读或写的操作。

文件共享

UNIX系统支持不同进程间共享打开的文件。

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

  • 每个进程在进程表中都有一个记录项,记录项中包含着一张打开文件描述标识符表,可将其视为一个适量,每个文件标识符占用一项,与每个文件描述符相关联的是:

    • 文件描述符。

    • 指向一个文件表项的指针。

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

    • 文件状态标志(读、写、添写、同步和非阻塞等)。

    • 当前文件偏移量。

    • 指向该文件v节点表项的指针。

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

在Linux中没有使用v节点,而是使用了通用的i节点结构。虽然两种实现有所不同,但在概念上,v和i是一样的,都是指向文件系统特有的i节点结构。

上图就是一个进程的三张表之间的关系。每个进程都有一个文件记录项,包含文件描述标识符和文件指针;内核中还有一个打开的文件表,包含文件状态标志,文件偏移量,v节点指针。每个打开的文件还有一个v节点,包含v节点信息,索引节点,当前文件长度。

原子操作

原子操作及不可再进行分割的操作。

添写到一个文件

再UNIX早期版本中并不支持open函数中的O_APPEND选项,所以需要程序员编写代码将偏移量移动到文件末尾,但是这时候代码执行过程中可能会被其他进程打断(竞争),从而出现错误。

所以对于单个进程而言,程序员编写的方式能正常工作,但如果多个进程同时使用这个方法将数据添加进入的话就会出现错误(竞争)。

问题出现在逻辑操作”定位到文件尾端处,然后写入数据“上,它使用了两个分开的函数。解决问题的方法就是使这两个操作对于其他进程而言成为一个原子操作。
任何一个需要多个函数调用的操作都不能是原子操作,因为再两个函数调用之间,内核可能会将该进程挂起。

UNIX系统提供了一种方法使这种操作成为原子操作,这方法是在打卡文件时设置O_APPEND标志。

pread/pwrite

pread/pwrite即允许原子性的定位搜索和执行I/O

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pread(int fd, void *buf, size_t count, off_t offset);

调用pread相当于顺序调用lseek和read;但是pread又与这种顺序调用有以下重要区别:

  • 调用pread时,无法中断其定位和读操作。(不可再分割)

  • 不更新文件指针。

调用pwrite相当于顺序调用lseek和write,但也与他们有类似的区别。

创建一个文件

对于open函数的O_REAT和O_EXCL选项进行说明时,我们以见到另一个有关原子操作的例子。当同时选择这两个条件的时候,而该文件已经存在时,open将失败。

一般而言,原子操作指的是由多部组成的操作。如果该操作原子的执行,则要么执行完所有步骤,要么一步也不执行

dup和dup2

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

上面两个函数用来复制一个现存的文件描述符。

dup返回的文件描述符一定是当前可用文件描述符的最小数值。

dup2则可用newfd参数指定新的描述符的数值.如果newfd已经打开,则先将其关闭。如若oldfd等于newfd,则dup2返回newfd,而不关闭它.

sync、fsync、fdatasync

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

延迟减少了磁盘读写的次数,但降低了文件内容的更新速度,是的要写入到文件的数据,没有第一时间写入到磁盘上;当系统发送故障的时,这种延迟可能操作文件更新内容丢失。

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

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

fsync函数只对由文件描述符指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。fsync可用于数据库这样的应用题程序,这种应用程序需要确保修改过的快文件立即写到磁盘上。

fdatasync函数类似于fsync,但它只影响文件的数据部分,除文件之外,fsync还会同步更新文件的属性

相关文章

迈进Makefile的世界(入门)

简介 Linux的`make`程序用来自动话编译大型源码,实现只需要一个`make`执行就可以全自动完成。 `make`能自动化完成,是因为项目路径下提供了一个`Makefile`文件,由该文件负责告诉`make`,应该去编译和链接该项目程序。 `make`起初只针对C语言开发,但它实际应用并不限定C语言,而是执行Linux命令去应用到任意项目,甚至不是编程语言。 >此外`make`...

linux基础

线程池

线程池的原理 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发得线程数量很多,并且每个线程都是执行一个时间很短得任务结束了,这样频繁得创建线程会降低系统的效率,因为频繁创建线程和销毁线程需要时间。 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都可以默认得堆栈大小,以默认优先级运...

linux基础

线程同步

线程同步的概念 多个线程对内存中的共享资源访问时,让线程进行线性的方式,有顺序的访问。线程对内存的这种访问方式就是线程同步。 下面是一个两个线程同时对变量num,进行加1的操作的demo,但是最终结果与预想结果,有很大差异。下面我们将分析并解决线程同步的问题。 ``` #include <pthread.h> #include <stdio.h> #include <unistd.h> #...

linux基础