进程表
谈linux下的文件操作,我们首先需要了解进程表,这是被每个进程所维护的一张打开文件的映射表,参照下图。
索引值是文件描述符,与之相关联的节点包含fd flag以及一个指向file table的指针。其中fd flag主要包含close-on-exec标记,该标记的作用在于当开辟其他进程调用exec()族函数时,如果该标记的最后一位被置1,则在调用exec函数之前将为exec族函数释放对应的文件描述符。而其后的指针指向的是一个file table。该节点时被kernel所维护的,它包含了当前文件的一些打开信息,offset值以及一个指向v节点的指针(在linux中是泛化的i节点)。
v-node-table是从硬盘上载入的,而v节点包含的是主要是文件的类型信息,其后跟随的是i节点,它包含文件的大小,属主,以及指向数据块节点的指针等信息。
不同的进程是可以共享同样的文件的。
从上图可以看出,这两个进程虽然指向不同的file table表(这允许它们拥有不同的打开类型以及offset值等),但是,它们都指向同样的v-node table。
当然,我们也可以通过dup等函数实现让同一个进程内的不同文件描述符关联上相同的file table,也就是说offset值将被它们共享。
需要我们注意的是,file table是被kernel所维护的,因此,不同的进程是可以共享file table的,实际上父子进程在默认下就是共享file table的。默认的(通过shell),所有的进程在一开始的时候,其进程表都会被填上3个值,即0,1,2.它们分别与标准输入,标准输出,以及标准错误相关联。而且,这些进程关于这些文件全都共享相同的file table,如下图所示。
基本文件操作
linux的文件操作可以使用系统调用或是库函数,这里只简单介绍下系统调用,它们都是无缓存IO。
首先是open函数,用于打开文件,它的返回值是文件描述符。看起声明:
int open(const char *pathname, int oflag, ... /* mode_t mode */ );
第一个参数时文件路径,第二个参数是打开方式(如只读,只写等),如果第二个参数还同时指定了新建,则可通过第三个参数指定文件的权限等值。
然后是lseek函数,用于指定offset值。其声明如下:
off_t lseek(int filedes, off_t offset, int whence);
第二个参数是偏移量,第三个参数是偏移基点(可取值:SEEK_SET[文件开始位置]、SEEK_CUR[当前位置]、SEEK_END[文件结束位置])在偏移基点取第一个值时,offset只能去整数,而取后两者值时,则可正可负。
当然还有读写函数,这些都是没有缓存的。
atomic 原子性
原子性是指同一个操作的不同逻辑部分要嘛全执行,要嘛全不执行,中间不允许被打断。
考虑这样的情况,如果我们希望每次都在一个文件的末尾添加内容,可以使用open函数直接指定打开方式为追加,也就是使用O_APPEND选项。当然后,我们也可以在每次写操作之前先通过lseek将offset置到文件结束位置。但是,第二种方式存在一个问题,即,如果在lseek函数和write函数之间发生了处理器切换,并且在这空挡有进程修改了该文件,则我们执行写操作时可能就不是在文件的末尾增加了。所以,执行这样的逻辑,我们更倾向于第一种方式,因为它具备原子性(atomic),而这种原子性可以避免我们调用lseek函数和write函数之间时的处理器被切换,从而导引发的逻辑错误。
同样具有原子性的可替代读写函数有:
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
dup函数
前面说到,dup函数的作用是使不同的文件描述符关联上相同的file table,我们先看它的一个声明:
int dup2(int filedes, int filedes2);
该函数首先关闭filedes2(如果打开了),然后令该文件描述符关联上与filedes同样的filetable。
该操作是原子性,也就是说在关闭文件描述符和关联这两个动作之间是不允许被打断的。