实际开始时间是2019年11月25号,以此来记录我的APUE学习之路,可能中间会有许多错误。
文件I/O
文件I/O是指不带缓冲的I/O,它直接调用内核的系统调用,这是与第5章标准I/O作对比,标准I/O是带缓冲的I/O。我是这么理解这两者的区别:
(1)文件I/O:程序(数据)—>内存缓冲—>硬盘(文件系统)。
(2)标准I/O:程序(数据)—>数组缓冲(buffer)—>内存缓冲—>硬盘。
也就是标准I/O完成操作后,实际没有写进内存,而还是写进寄存器,或者比内存更高级的缓存中?不知道正确与否。
1 文件描述符
unix系统万物皆为文件,在程序中每打开一个文件,都会返回一个文件描述符(int fd,实际非负整数),供内核引用。而相对于标准I/O,就是打开文件指针(FILE* fp)。
unix系统shell把文件描述符0、1、2分配给标准输入、标准输出、标准错误,并用符号常量STDIN_FILINO、STDOUT_FILENO、STDERR_FILENO来表示。
2 基础函数
(1)open和openat函数打开或创建文件,返回最小的未使用的文件描述符,若错误则返回-1。
(2)creat函数以只写创建文件。
(3)close函数关闭文件,不过当一个进程终止,内核会自动关闭。
(4)lseek设置当前文件偏移量,可以理解打开文件后,你的光标在哪。测试lseek是否出错,不要检测其是否<0,而要检测其返回值是否等于-1。
(5)read从当前文件偏移量处开始读文件,并将读取的文件放在buf中。
(6)write和read类似,只不过是写进buf中。
3 文件共享、I/O数据结构
unix可以在不同进程间共享打开文件,也就是不同进程可以同时打开同一个文件。
3.1 一个进程打开不同文件
I/O数据机构:
(1)每个进程都有一个进程表,其里面的记录项有一个一位fd标志(文件描述符标志)和一个指向文件表项的指针。
(2)内核为所有打开文件维持一个文件表,其里面的文件项如图所示。
(3)每个打开的文件都有都有一个v节点结构,包括v节点信息,也有指向i节点的指针。
3.2 两个进程打开同一个文件
注意点:
(1)多个进程打开同一个文件,每个进程都会获得各自的文件表,使每个进程都有自己的当前文件偏移量。
(2)但是对于打开的文件永远只有一个v节点表项。
4 原子操作
我们先看一个例子,当还没有O_APPEND追加写选项时,要想在文件尾部进行写,必须分成两部来进行。
(1)lseek定位到文件尾部;(2)进行write写。
if (lseek(fd, OL, 2) < 0)
perror("lseek error");
if (write(fd, buf, 100) != 100)
perror("write error");
这个对于只有一个进程时,是完全正确且没有错误的。可是一旦有多进程,就会出现问题。
让我们来假设有两个进程A和B,分别对同一个文件进行追加写。其可能的时间线是这样的:
(1)在进程A,lseek定位到文件尾(假设为1500)。
(2)此时内核进行进程切换,切换到B进程。进程B执行完全部过程,为文件写入了100字节,此时文件尾为1600。
(3)内核进行进程切换,进程A继续执行,因为不同进程打开同一文件都有各自的文件表,而之前A进程lseek将当前文件偏移量设置为1500,write函数理应也是从1500处写起,导致进程B写进的数据被覆盖。
所以这里提出原子操作的定义:原子操作指的是多步组成的一个操作。
lseek和write可以合成一个原子操作pwrite,先执行lseek,再执行write。要么执行完所有步骤,要么不执行。
5 进阶函数
(1)pread和pwrite,原子操作。
(2)dup和dup2复制现有的文件描述符到一个新的文件描述符。
注意:每个文件描述符都有它自己一套的文件描述符标志。
(3)fsync、fdatasync、sync函数,将内核的缓冲区文件写进磁盘,保证磁盘上的文件系统和缓冲区中的内容一致。
(4)fcntl函数可以改变已经打开文件的属性