与文件相关的一些概念

在开始上图之前,先说明几个和 unix 文件密切相关的术语,方便后续讨论使用

  • 文件句柄 / 文件描述符 (file descriptor 或 FD):描述一个打开文件相关属性的类型;
  • 文件描述符表 (file descriptor table 或 FDT):每个进程拥有一个 FDT,其中每个表项是一个 FD,使用 FDT 的下标表示各个 FD(从 0 开始的整数);
  • 全局打开文件表 (open file table 或 OFT):系统只有一个 OFT,其中每个表项被 FD 所引用;
  • i 节点 (inode):描述文件系统上的一个文件,例如 所有者/大小/设备/起始位置 等,它只包含和文件系统相关的属性;
  • v 节点 (vnode):描述文件相关的操作,例如 读 / 写 / 移动相对偏移量 等,它只包含和文件系统无关的属性,用于统合各种不同类型的文件系统;

其中前三项只有文件被打开后才有相应的结构,而后两项只要文件存在就存在了,与文件是否打开没有关系。

文件相关概念之间的关系

它们之间的关系是怎样的呢,现在上图

图中左侧展示了两个进程,蓝色的为 ProcessA (PA),红色的为 ProcessB (PB),每个进程都有一个 FDT,其中包含若干个 FD,可以看到每个 FD 由两部分组成:

  • pflag :在进程中的标志位,目前只有一个标志位 O_CLOEXEC,置位的话表示在进程执行 exec 函数族后自动关闭此文件句柄,默认是不关闭的;
  • fileptr :指向 OFT 中相应的表项,来描述文件剩余的属性。

再观察 OFT 中表项的内容,可以看到它是由以下几部分组成:

  • oflag :文件打开标志位,除 O_CLOEXEC 之外的标志位,如权限位 O_RDONLY / O_WRONLY / O_RDWR,创建位 O_CREAT / O_EXCL,追加位 O_APPEND,截断位 O_TRUNC,异步位 O_NONBLOCK 等均由这个字段指定。
  • offset :当前文件偏移;
  • vnode :指向该文件的 v 节点。

再观察文件属性相关的节点,它一般由下面两部分组成:

  • vnode :文件的 v 节点信息,通常是一些操作的抽象,用于构建文件系统无关的 VFS;
  • inode :文件的 i 节点信息。

对于 vnode,你可以理解成是一组函数指针,例如在 Linux 上,它分别定义了 inode 与文件的操作函数:

 struct inode_operations {
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
void * (*follow_link) (struct dentry *, struct nameidata *);
int (*permission) (struct inode *, int);
struct posix_acl * (*get_acl)(struct inode *, int);
int (*readlink) (struct dentry *, char __user *,int);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *);
void (*truncate) (struct inode *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*truncate_range)(struct inode *, loff_t, loff_t);
int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start, u64 len);
} ____cacheline_aligned; struct file_operations {
  struct module *owner;//拥有该结构的模块的指针,一般为THIS_MODULES
loff_t (*llseek) (struct file *, loff_t, int);//用来修改文件当前的读写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);//从设备中同步读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);//向设备发送数据
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的读取操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);//初始化一个异步的写入操作
  int (*readdir) (struct file *, void *, filldir_t);//仅用于读取目录,对于设备文件,该字段为NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *); //轮询函数,判断目前是否可以进行非阻塞的读写或写入
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); //执行设备I/O控制命令
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //不使用BLK文件系统,将使用此种函数指针代替ioctl
  long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //在64位系统上,32位的ioctl调用将使用此函数指针代替
  int (*mmap) (struct file *, struct vm_area_struct *); //用于请求将设备内存映射到进程地址空间
  int (*open) (struct inode *, struct file *); //打开
  int (*flush) (struct file *, fl_owner_t id);
  int (*release) (struct inode *, struct file *); //关闭
  int (*fsync) (struct file *, struct dentry *, int datasync); //刷新待处理的数据
  int (*aio_fsync) (struct kiocb *, int datasync); //异步刷新待处理的数据
  int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化
  int (*lock) (struct file *, int, struct file_lock *);
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  int (*check_flags)(int);
  int (*flock) (struct file *, int, struct file_lock *);
  ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
  ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
  int (*setlease)(struct file *, long, struct file_lock **);
};

ext2 上的 read 与 nfs 的 read 实现肯定不同,但是这里通过函数指针来屏蔽了这种差异。注意:linux 上并没有 vnode 的概念,它使用与文件系统相关的 inode 和文件系统无关的 inode,后者就是我们这里说的 vnode。

上面的大图是最普通的场景,就是两个进程都打开不同的文件,相互之间没有共享,下面我们分几个场景来看一下共享文件时这里的关系是如何变化的。

一个进程多次打开同一个文件

使用 open 多次打开同一个文件(文件路径可能相同,也可能不同,考虑链接的情况)的场景如上图,每个 FD 都有独立的 OFT 对应项,虽然最后都是在操作同一个文件,但一个 FD 的文件偏移改变,不影响另外一个 FD 的文件偏移;同理与文件相关的 pflag、oflag 也是如此。

多个进程打开同一个文件

多个进程打开同一个文件的场景如上图,除了跨进程外,其它与进程内并无任何不同。这里着重考察一个具体场景,就是两个进程同时打开文件进行追加(O_APPEND)写。假设 PA 写入一些数据完成后,它的 offset 会被更新,如果这个值大于 inode 中的文件 size,则更新 inode.size 到 offset 表示文件增长了;然后 PB 开始写入数据,由于指定了 O_APPEND 标志位,在写入前,系统会先将它的 OFT 表项中的 offset 更新为当前 inode.size,这样就可以得到 PA 写入后的文件末尾位置,接着在这个位置写入 PB 的数据,写入完成后的逻辑与 PA 相同,会更新 offset、inode.size 来表示文件的最新增长。由于更新 offset 与 inode.size 是在一个 api 完成的,所以这个操作完全可以被某种锁保护起来,从而实现原子性。相对的,如果没有指定 O_APPEND 选项,而使用 lseek (fd, 0, SEEK_END) + write (fd, buf, size) 的方式,由于这个操作需要使用两个 api 来完成,无法跨 api 加锁使得这样的操作没有原子性保证,而可能产生的竞争会导致一个进程写入的数据被另一个进程所覆盖,从而丢失数据。

进程内文件句柄 dup

进程内文件句柄 dup 的场景如上图,执行的是 fd2 = dup(fd1) 语句,复制成功后,fd2 与 fd1 都将指向同一个 OFT 表项。而 pflag 不在复制之列,也就是说,如果 fd1 指定了 O_CLOEXEC,则复制后的 fd2 默认是没有设置这个标志位的。除此之外,与文件相关的其它属性完全一样,包括 oflag 的各种标志位、offset 和文件 inode 信息。如果修改 fd1 的 oflag,例如 O_NONBLOCK,则 fd2 也将变成非阻塞的;如果读写 fd2,则 fd1 的 offset 也会随之改变……

进程 fork

进程 PA 打开一个文件后 fork 产生子进程 PB 的场景如上图,之前打开的句柄将指向同样的 OFT 表项,这样的表现有点类似跨进程文件句柄 dup,除了 fd0 分属 PA 与 PB 两个不同进程外,其它方面与上一个场景完全相同。所以如果希望通过 fork 来共享某些文件数据,则在 PA 写入数据后,PB 并不能读到父进程刚刚写入的数据,这是因为它的 fd0 对应的文件偏移也被更新了的缘故。

进程间传递文件句柄

说到进程间传递文件句柄,很多人是不是第一反应是直接传递 FD 值啊?那就理解错了。关于在进程间如何传递文件句柄,请参考我之前写过的一篇文章:记一次传递文件句柄引发的血案,简单说的话,可以引用 apue 书中的一句话来解释:“在技术上,发送进程实际上向接收进程传送一个指向一打开文件表项的指针,该指针被分配存放在接收进程的第一个可用描述符项中”,其实非常类似 fork 所产生的效果,不同之处在于两点:

  • 发送与接收文件句柄的进程不一定是父子进程关系;
  • 原进程与新进程中复制的文件句柄值一般不同(fork 结果一般是相同)

上面的图展示了这种细节的差异,PA 发送的文件句柄是 fd0,PB 由于已经打开了 fd0,所以接收后新的文件句柄是 fd1,其它方面与 fork 场景的结论完全一致。

结语

其实判断两个句柄是在哪个级别共享的方法很简单,就是改变一个句柄的文件偏移,观察另外一个句柄的文件偏移是否变化。如果变了,则是在 OFT 层面共享的;如果没变,则只是打开同一个文件而已。另外,有些东西会随着时代而更新,有些原理则不会变,以本文开头的这张结构图来说,自 UNIX 的早期版本(1978)以来就没有发生过根本性的变化,可见学知识还是要学原理性的东西,万变不离其宗。

参考

[1]. inode_operations介绍

[2]. Linux字符设备驱动file_operations

[3]. 驱动程序操作的三个内核数据结构(file_operations、file、inode)

[apue] 一图读懂 unix 文件句柄及文件共享过程的更多相关文章

  1. 一张图读懂https加密协议

    搭建CA服务器和iis启用https:http://blog.csdn.net/dier4836/article/details/7719532 一张图读懂https加密协议 https是一种加密传输 ...

  2. 【Zigbee技术入门教程-02】一图读懂ZStack协议栈的基本架构和工作机理

    [Zigbee技术入门教程-02]一图读懂ZStack协议栈的基本架构和工作机理 广东职业技术学院  欧浩源  ohy3686@foxmail.com Z-Stack协议栈是一个基于任务轮询方式的操作 ...

  3. 一张图读懂PBN飞越转弯衔接DF航段计算

    飞越转弯衔接TF航段时,转弯外边界与旁切转弯相似,只是在拐角位置直接以风螺旋绘制外边界,大部分切点可以精确计算得到. 飞越转弯衔接DF航段时,转弯外边界全部由风螺旋和它的切线构成,又会有哪些神奇的事情 ...

  4. 一张图读懂PBN飞越转弯衔接TF/CF航段计算

    在PBN旁切转弯的基础上,再来看飞越转弯接TF(或CF)航段,保护区结构上有些相似,只是转弯拐角处的保护区边界有“简化”,其余部分是相近的. FlyOver接TF段的标称航迹有一个飞越之后转弯切入航迹 ...

  5. 【Zigbee技术入门教程-02】一图读懂ZStack协议栈的核心思想与工作机理

    [Zigbee技术入门教程-02]一图读懂ZStack协议栈的核心思想与工作机理 广东职业技术学院  欧浩源   Z-Stack协议栈是一个基于任务轮询方式的操作系统,其任务调度和资源分配由操作系统抽 ...

  6. 比传统事务快10倍?一张图读懂阿里云全局事务服务GTS

    近日,阿里云全局事务服务GTS正式上线,为微服务架构中的分布式事务提供一站式解决方案.GTS的原理是将分布式事务与具体业务分离,在平台层面开发通用的事务中间件GTS,由事务中间件协调各服务的调用一致性 ...

  7. 一图读懂k8s informer client-go

    概述 为什么要有k8s informer 我们都知道可以使用k8s的Clientset来获取所有的原生资源对象,那么怎么能持续的获取集群的所有资源对象,或监听集群的资源对象数据的变化呢?这里不需要轮询 ...

  8. 一张图读懂Java多线程

    1.带着疑问看图 1)竞争对象的锁和竞争CPU资源以及竞争被唤醒 2)何种情况下获取到了锁,何种情况下会释放锁 2.还是那张图 3.详细图解 1)Thread t = new Thread(),初始化 ...

  9. 一张图读懂PBN旁切转弯计算

    当DOC8168进入PBN章节以后,所有的保护区不再标注风螺旋的字母位置点.似乎ICAO已经有了精确计算的方法,只是没有告诉我们.沿着风螺旋的轨迹一路走来,切线与角度的换算方法想必已经相当熟悉了吧,这 ...

随机推荐

  1. CF 633 div1 1338 B. Edge Weight Assignment 构造

    LINK:Edge Weight Assignment 这场当时没打 看到这个B题吓到我了 还好当时没打. 想了20min才知道怎么做 而且还不能证明. 首先考虑求最小. 可以发现 如果任意两个叶子节 ...

  2. java -jar .jar中没有主清单属性

    pom里加上 <build> <plugins> <plugin> <groupId>org.springframework.boot</grou ...

  3. jmeter如何设置全局变量

    场景:性能测试或者接口测试,如果想跨线程引用(案例:A线程组里面的一个输出,是B线程组里面的一个输入,这个时候如果要引用),这个时候你就必须要设置全局变量;全链路压测也需要分不同场景,通常情况,一个场 ...

  4. What is 测试金字塔?

    我的女朋友是一名测试工程师,但她之前却不知道测试金字塔的概念,为此我曾经在家里的白板上画了一个图一层一层给她讲解过.我和同事在给团队面试测试和开发岗位时,也会必问到这个问题,想到可能有很多开发童鞋都不 ...

  5. map,reduce和filter函数

    numArray = [1, 2, 3, 4, 5] def ercifang(x): return x ** 2 def map_test(func, numArray): li = [] for ...

  6. 基于视频压缩的实时监控系统-sprint1基于epoll架构的采集端程序设计

    part1:产品功能 part2:epoll机制   select与epoll区别 1.select与epoll没有太大的区别.除了select有文件描述符限制(1024个),select每次调用都需 ...

  7. SSM框架整合Demo

    目前项目大都开始采用SSM结构进行搭建,因为涉及项目比较多,新来的需求都是从现有项目中迁移一份出来进行修改,有的时候两个项目差别还是比较大,并不完全需要原有项目的东西,进行删减也是一项费神费时的事情, ...

  8. 关于手机数码圈KOL的一两点感想

    复工以来,高峰时段9号线地铁上的人依旧不少,安全距离啥的肯定是不用想了,只是从原来的4G手机换成5G手机以后在某些站能接收到5G信号,我终于能在一些原来根本没信号的站里愉快的刷一刷微博和酷安了. 但是 ...

  9. java web应用启动报错:Several ports (8080, 8009) required by Tomcat v6.0 Server at localhost are already in use.

    Several ports (8080, 8009) required by Tomcat v6.0 Server at localhost are already in use. The serve ...

  10. 计算机网络要点---TCP

    计算机网络要点---TCP 浏览器在通过域名通过dns服务器找到你的服务器外网ip,将http请求发送到你的服务器,在tcp3次握手之后(http下面是tcp/ip),通过tcp协议开始传输数据,你的 ...