你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

学习 Linux 文件模型相关的知识

[复制链接]
gaosmile 发布时间:2020-7-18 18:20

写作目的:

  • 学习 Linux 文件模型相关的知识。
    ' I; F5 Y9 a5 ^: E/ X( {# j! ~

正文目录:

1. Linux 的两大抽象
8 I0 Z' E% v% D1 S* Q5 {4 w; [0 R2 K# ^7 W( y
2. 文件类型
" b% z2 S# i' f) r" J9 |4 h! I- g7 D0 x- D: B& y5 Y- [8 _  O& H9 Q
3. 文件描述符
! Q9 {* \$ \7 P7 L# Z( S+ s3 q; c; k6 S( c" C6 _, }$ P' c
4. 通用文件模型:简介
2 D3 [3 r* d/ W3 i% a9 |    4.1 演示 demo  X3 h& `: ^5 r3 t* i9 x5 c' |8 A
    4.2 相关要点: 与 VFS 的关系, P) G2 N6 H5 g1 s

6 y8 l+ A2 c, O$ B) x: v( p5. 通用文件模型:文件描述符和打开文件的关系
: {& w8 _! k1 V0 H! q) ?! z    5.1 相关的内核数据结构
# l: l6 D7 H3 D. \    5.2 列举几种打开文件的情景

  N1 I% }1 J/ N0 v1. Linux 的两大抽象
  • 文件是 Linux 系统中最基础最重要的抽象。Linux 遵循一切皆文件的理念。很多交互操作是通过读写文件来完成,即使所涉及的对象看起来并非普通文件。

  • 另外一大抽象是进程。如果说文件是 Linux 系统最重要的抽象概念,进程则仅次于文件。

  • 进程相关的实现复杂且多变,而文件 IO 的实现则相对稳定很多,且更贴近我们的日常操作,所以 以文件作为学习 Linux 内核的切入点是个更好的选择。

    + C# o- l% ]# r, v
2. 文件类型

Linux 系统的大多数文件是普通文件或目录,但是也有另外一些文件类型,具体包括如下几种:

  • 普通文件 ( regular file )。

    5 [; D1 N1 g4 ]: t& |$ N3 O2 T8 W1 d
    • 最常用的文件类型,包含了某种形式的数据。至于这种数据是文本还是二进制数据,对于 Linux 内核而言并无区别。

    • 文件中包含的字节可以是任意值,可以以任意方式进行组织。在系统层,除了字节流,Linux 对文件结构没有特定要求。

    • 对普通文件内容的解释由处理该文件的应用程序进行。

    • 文件虽然是通过文件名访问,但文件本身其实并没有直接和文件名关联。相反地,与文件关联的是索引节点 (inode,是index node 缩写)。针对驻留于文件系统上的每个文件,文件系统都会为其分配一个 inode。inode 中会保存和文件相关的元数据,如文件修改时间戳、所有者、类型、长度以及文件数据的位置,但不含文件名,文件名由目录文件负责。

    • inode 由 inode number 来标识,可以通过 “ls –li” 查看文件的 inode number。

      # ls -li minicom.log
      . t& I+ V- |! r: @) o0 ~12582945 -rw-r--r-- 1 root root 665 Jul 10 18:47 minicom.log
      8 G/ z. I; B4 T9 m, f8 p6 j( K% g

      ) ^6 a- i& y0 S! _/ T; d
  • 目录文件 ( directory file )。


    0 S' ^$ B7 G0 T  V
    • 目录也是一种文件类型,这种文件包含了其他文件的文件名以及 inode number。文件通常是通过文件名从用户空间打开,目录用于提供访问文件时需要的名称。

    • 文件名和 inode 之间的配对称为链接 (link)。映射在物理磁盘上的形式,如简单的表或散列,是通过特定文件系统的内核代码来实现和管理的。

    • 如果用户空间的应用请求打开指定文件,内核会打开包含该文件名的目录,然后根据文件名获取 inode number。通过 inode number 可以找到 inode。inode 包含和文件关联的元数据,其中包括文件数据在磁盘上的存储位置。

      % O) g& @) j9 q( F; Q6 ^: T
  • 硬链接 ( hard link )。

    & F: Y) A; c- Z6 ]2 O0 D
    • 不同的文件名可以链接到到同一个 inode。当不同名称的多个链接映射到同一个索引节点时,我们称该链接为硬链接。

    • 硬链接通常要求链接和文件位于同一文件系统中。

    • 在底层文件系统支持的前提下,也只有超级用户才能创建指向目录的硬链接。


      . i: G% t! d0 b- I3 ~/ c
  • 符号链接 ( symbolic link )。

    + r( w: i1 X- m7 E0 e7 f/ @% W
    • 符号链接是对一个文件的间接指针,它与硬链接有所不同,硬链接直接指向文件的 inode。引入符号链接的原因是为了避开硬链接的一些限制。

    • 硬链接不能跨越多个文件系统,因为 inode number在自己的文件系统之外没有任何意义。为了跨越文件系统建立链接,Linux 系统实现了符号链接。


      0 t+ d" e3 S8 C+ l
  • 特殊文件 (special file)。


    ) K6 y! N( e" n. K
    • 特殊文件是使得某些抽象可以适用于文件系统,贯彻一切皆文件的理念。

    • Linux 只支持四种特殊文件:块设备文件、字符设备文件、命名管道 以及 UNIX域套接字。

    • 块特殊文件 ( block device file )。提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。

    • 字符特殊文件 ( character device file )。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。

    • 命名管道 ( named pipes ),通常称为 FIFO,是以文件描述符作为通信信道的 IPC 机制,它可以通过特殊文件来访问。

    • 套接字 ( socket ) 是最后一种特殊文件。socket 是进程间通信的高级形式,支持不同进程间的通信,这两个进程可以在同一台机器,也可以在不同机器。socket 是网络和互联网编程的基础。


        z$ M$ _+ {6 J4 ?9 V9 y3 A& k- O

在 Linux,可以用 ls/stat 命令 和 stat() 系统调用确定文件类型。

$ ls -li , c( C, D3 M( V( k- L
12587634 drwxr-xr-x 26 root root      4096 Mar 16 07:49 1.opensource
2 e/ z; g5 B" P& z$ ]27396428 lrwxrwxrwx  1 root root        12 Nov 17  2017 Link to ssd_dvd -> /mnt/ssd_dvd: l  \; W$ J) @$ M3 n$ K  U/ m: S
12582945 -rw-r--r--  1 root root       665 Jul 10 18:47 minicom.log  g; \% B6 u0 I$ }5 X3 r1 A
, V# l& r, X+ X. s
$ stat minicom.log
2 z8 U& _) Y1 k, J  File: 'minicom.log'/ N# J* I1 n3 ^7 M
  Size: 665        Blocks: 8          IO Block: 4096   regular file% m4 Q  ?( y$ _- ?0 d
Device: 822h/2082d Inode: 12582945    Links: 1
3 @9 c& w2 v: t# M# I* ?Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)! u; K4 i, K, {  g. S$ r
Access: 2020-01-09 09:44:07.101177618 +0800
, K( J/ t% y3 a5 X$ P5 V/ WModify: 2020-07-10 18:47:20.073532673 +0800
% _2 ~; D  q5 @( ?' tChange: 2020-07-10 18:47:20.073532673 +0800
6 u$ B9 x5 G6 r4 w3 j
3. 文件描述符

在 Linux 中,文件必须先打开才能访问。对于内核而言,所有打开的文件都通过文件描述符 ( file descriptor,简称fd ) 引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用 open() 或 creat() 返回的文件描述符标识该文件,将其作为参数传送给 read() 或 write()。

  • Linux 系统编程的大部分工作都会涉及打开、操纵、关闭以及其他文件描述符操作;

  • Linux 系统的 Shell 把文件描述符 0 与进程的标准输入 stdin 关联,文件描述符 1 与标准输出 stdout 关联,文件描述符 2 与标准错误 stderr 关联。这是各种 Shell 以及很多应用程序使用的惯例,与 Linux 内核无关。如果不遵循这种惯例,很多 Linux 系统应用程序就不能正常工作;

  • 用户可以重定向文件描述符,甚至可以通过管道把一个程序的输出作为另一个程序的输入。Shell 就是通过这种方式实现重定向和管道的。

  • 在 POSIX 标准中,幻数 0、1、2 虽然已被标准化,但应当把它们替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可读性;

  • 文件描述符的范围是 0 ~ OPEN_MAX-1;

  • 文件描述符并非局限于访问普通文件。实际上,文件描述符也可以访问设备文件、管道、FIFO、Socket等。遵循一切皆文件的理念,几乎任何能够读写的东西都可以通过文件描述符来访问。

    7 e- h! |! x# J7 L* ^8 C1 B
4. 通用文件模型:简介

Linux 通用文件模型最为显著的特性之一就是 I/O 通用性。也就是说,同一套系统调用 open()、read()、write()、close() 等所执行的 I/O 操作,可施之于所有文件类型,包括设备文件在内。应用程序发起的I/O请求,内核会将其转化为相应的文件系统操作,或者设备驱动程序操作,以此来执行针对目标文件或设备的I/O操作。因此,采用这些系统调用的程序能够处理任何类型的文件。

演示 demo (copy.c):

int main(int argc, char *argv[])( X+ r5 ], k# U! A( l/ }
{0 U* H4 z5 q  F- ~
    int inputFd, outputFd, openFlags;
( c+ s* \& S5 b9 @) B6 H, ?+ o, m& H    mode_t filePerms;! Y/ f  G7 N- ~$ X+ i
    ssize_t numRead;' }5 h* I% }/ e4 Z
    char buf[BUF_SIZE];
3 E& }- I& E+ m0 X7 n3 ~& g. Y; ~" X2 t3 q) X* K
    if (argc != 3 || strcmp(argv[1], "--help") == 0)
3 S1 d& J1 h, B0 V        usageErr("%s old-file new-file\n", argv[0]);
0 E, V, h- z% W. U( j5 ~
  `7 l* u# Q& _3 U: r  y# `    /* Open input and output files */& N' s0 ?/ c. y5 T& Q0 h

4 _* a, ?% S# R9 y; \5 M# ?    inputFd = open(argv[1], O_RDONLY);
: |8 B/ r2 d, v9 k( A7 k" A: C    if (inputFd == -1)( m8 P- S4 j- {, c5 L
        errExit("opening file %s", argv[1]);
2 u% A" d: ~5 M7 B4 q
  L' g2 d/ p: T* |% Y9 q    openFlags = O_CREAT | O_WRONLY | O_TRUNC;) H3 L+ j( ]; Q8 O' p
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
% i$ n' j0 e2 a# C2 ^, f                S_IROTH | S_IWOTH;      /* rw-rw-rw- */" d2 q$ ]8 j4 Y. v* _( {  n7 d
    outputFd = open(argv[2], openFlags, filePerms);4 D# {8 D( {  y! H" t+ U: u: @
    if (outputFd == -1)1 w) T/ T/ _7 {4 X) n
        errExit("opening file %s", argv[2]);8 T6 x: P0 O' L6 C

6 R2 P$ o. S2 ~- E    /* Transfer data until we encounter end of input or an error */; K' E# q+ M/ C9 r  @# g) p
8 k" [7 P, s/ R2 \# _8 T7 |4 ?
    while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
3 I* J8 u/ I4 D% E4 @0 H        if (write(outputFd, buf, numRead) != numRead)* G, `: k  D" P: e  U# x
            fatal("write() returned error or partial write occurred");
, z7 j# Y0 y( v$ H* F    if (numRead == -1)
, Y* X0 H8 v3 }$ s" p9 r0 k, H        errExit("read");/ i2 x9 Z% |/ c) b1 S3 K

0 g$ }! }. X( B! A/ J  e5 G    if (close(inputFd) == -1)
) c6 }. @4 z3 x$ n' ~        errExit("close input");; ~7 x9 \. d/ N9 K$ N6 ]
    if (close(outputFd) == -1)3 ]7 r- ~9 B- d* W8 ^' p" g
        errExit("close output");
- Z: N) P0 z  A0 e. o: ?) e
/ X7 z% K7 v+ w$ F    exit(EXIT_SUCCESS);6 G6 \% k; @$ d2 O: k% G2 P
}

7 ^2 ]% F+ B; m! }9 E" \* }$ o

运行效果:

$ ./copy test test.old% e% L' c5 I: P6 Q6 t  [+ z
$ ./copy test /dev/tty
, m% Z& J. H0 |. ?5 n* K/ N  ^$ ./copy /dev/tty abc.txt
8 {: m. l) ]2 b2 y3 E* r* Z

相关要点:

  • 要实现通用 I/O,就必须确保每一种文件系统和每一种文件类型(包括设备文件)都实现了相同的 I/O 系统调用集。由于文件系统或设备文件所特有的操作细节在内核中处理,在编程时通常可以忽略设备专有的因素。一旦应用程序需要访问文件系统或设备的专有功能时,可以选择瑞士军刀般的 ioctl() 系统调用,该调用为通用 I/O 模型之外的专有特性提供了访问接口。

  • 提到通用 I/O,就必须提起虚拟文件系统 (VFS)。为支持各种本机文件系统,且在同时允许访问其他操作系统的文件,Linux 内核在用户进程和文件系统实现之间引入了一个抽象层 VFS。虚拟文件系统基于文件通用模型(common file model,简称CFM)实现这种抽象,它是 Linux 上所有文件系统的基础。

  • 一方面,VFS 提供了一种操作文件、目录及其他对象的统一方法。另一方面,它与各种具体的文件系统的实现达成妥协。我们可以认为,是虚拟文件系统 (VFS) 和通用文件模型 (CFM) 的共同作用为 Linux 提供了访问不同文件系统以及不同类型的文件的 统一API (open()、read()、write()、close())。在本文中,我们将重点放在文件上,忽略文件系统相关的东西。

  • 在 VFS 中,并非所有文件系统都支持同样的功能,有些操作对普通文件是不可缺少的,对某些对象则完全没有意义。即并非每一种文件系统都支持 VFS 中的所有抽象。

  • Linux VFS 的实现: 参考 ext2 文件系统,提供一种结构模型,该文件系统模型包含了一个强大文件系统所应具备的所有组件。但该模型是虚拟的,它适应于各种真实的文件系统。所有实现都必须提供可以适应 VFS 定义的结构体的 routines,因此可以充当两个视图之间的过渡。

  • 在 VFS 中,每个文件都关联到一个 inode,我们可以 以 inode 和 inode->file_operations 作为学习通用文件模型和虚拟文件系统的切入点。

      ~- [8 E( ?6 r  q# _# R0 w
struct inode {% [1 ^% n9 ^' n6 H8 Z6 R: H
    umode_t   i_mode;) s# @. l: w* A% \- M
    ...
0 d6 b- e3 `" _9 w( z    const struct file_operations *i_fop;& ], w6 o; \' J
    .../ J$ s- w1 d0 O  C* o; @
}

0 r% h$ I# t3 l7 tstruct file_operations {
, k1 t' f$ Z% x8 M# B7 `( a" J    struct module *owner;
5 G$ s  u( A) D8 F* m& z    loff_t (*llseek) (struct file *, loff_t, int);  N/ u# S3 t& s8 k2 A
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);! V7 p, |( V% w9 R
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
' c# q. \; l9 @. u7 e1 k    ...
# U: l( V7 U$ z$ Y+ |: [    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
) j! C3 e7 h  _* f    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);5 q4 o% u3 }' w! g( B
    int (*mmap) (struct file *, struct vm_area_struct *);/ a( s! Z4 y: |, f. V, R" y' ~
    int (*open) (struct inode *, struct file *);
2 P( s& D& I! e( s9 W1 E" J    int (*flush) (struct file *, fl_owner_t id);
( U! w( }$ q7 R" R$ p...
" k( U$ U! p: S8 {+ _& u% c  z} __randomize_layout;
$ S$ {) s1 {5 ?5 m. S+ F
5. 通用文件模型:文件描述符和打开文件的关系5.1 相关的内核数据结构内核使用 3 种数据结构来表示一个被打开的文件:
  • 进程级的文件描述符表 ( file descriptor table )。

  • 系统级的打开文件表 ( open file table ) 。

  • 文件系统的 i-node 表 ( i-node table )。

    2 H) V; D# O- K% G
1) 进程级的文件描述符表 ( file descriptor table )

每个进程在进程表 (process table) 中都有一个记录项 (process table entry),即 struct task_struct,内核用它来描述一个进程。在 struct task_struct 中包含了一张打开文件描述符表 (open file descriptors table),由 struct files_struct 里的 struct fdtable 来表示 (Linux-4.14):

struct task_struct {
! p2 _- J: K1 `    ...! w; T# s8 ~4 K9 e5 P
    /* Filesystem information: */
7 ]7 ~% T4 a/ l6 w3 v struct fs_struct  *fs;7 I8 H" t0 c' Y( A
# H- J3 P7 [3 B  d1 X
/* Open file information: */  b: s, C, W" _+ }
struct files_struct  *files;0 t7 R+ Y. J: I: I
        -> struct fdtable *fdt;
$ f% v& }: Z8 a    ...
" _. ~# Y, O5 d9 L# v% R}

( J- }7 M$ s6 g

每个文件描述符包含:

  • 1> 文件描述符标志 ( file descriptor flags,目前只有一个:close_on_exec,暂不关心 );
  • 2> 指向一个打开文件表项 ( open file table entry) 的指针。
    3 r1 j2 j( z' W1 a
struct fdtable {" f1 m1 n( {. s3 @2 L. f5 l5 J
    ...
5 ^. D2 N2 C9 u( [, }: Z% m1 S0 W8 \ struct file **fd;      /* current fd array */' Z1 V6 o8 _0 S/ `# o% H) y5 |
unsigned long *close_on_exec;
. x, S9 [% y( l) `$ l* X ...
5 C! J4 e9 n( M% N2 g1 y3 G' u};
" F8 i' t* [5 b5 m$ E# D$ U
微信图片_20200718181902.png
2) 系统级的打开文件表 ( open file table )0 f/ T8 c$ d8 p& Y6 g; v5 }

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

  • 1> 文件状态标志 ( file status flags,即 open() 的 flags 参数);

  • 2> 当前文件偏移量 ( current file offset );

  • 3> 指向该文件 inode 表项的指针 (在某些 UNIX 系统中是 vnode pointer,在 Linux 中是 inode pointer)。


    7 c, s( G3 r, y3 M

inode 结构体和 vnode 结构体名称虽然不同,但是 2 者其实是同一个概念,它们都用于描述存储在硬盘中的文件系统的 inode 数据。注意区别内存里的 inode 结构体对象和硬盘中的 inode 数据。

3) 文件系统的 i-node 表 ( i-node table )

每个打开文件都有一个 inode 对象。inode 对象包含了:

  • 文件类型和对此文件进行各种操作函数的指针。

  • 对于大多数文件,inode 对象还包含了指向该文件系统 inode 数据的指针。


    9 N6 ?& k6 i. ]& n& }: m
struct inode {) @- {, E4 K& `  [8 l
    ...
0 K+ t) e! W/ e) |9 ^    /* Stat data, not accessed from path walking */
( {5 ~6 C; ?6 L    unsigned long  i_ino;3 V" _6 T3 k/ g3 U( f* F
% Q1 X8 O! E$ O0 R+ m
    ...
: v7 A& |& N. |0 q0 ?9 O    /* former ->i_op->default_file_ops */
: r; ]5 |$ `" e% B/ F3 t3 K" ~    const struct file_operations *i_fop;
6 X1 L8 [6 q! [! ~}

) }( @$ B& v" j. i0 d3 X, _. ]

这些信息是在打开文件时从硬盘上读入内存的,所以,文件的所有相关信息都是随时可用的。即 inode 对象包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。

上述三张表的完整关系如下:

微信图片_20200718181907.png
5.2 列举几种打开文件的情景1) 两个独立进程各自打开同一个文件

两个独立进程各自打开了同一文件,则有如下关系:

微信图片_20200718181911.png

第一个进程在文件描述符 3 上打开该文件,而另一个进程在文件描述符 4 上打开该文件。打开该文件的每个进程都获得各自的一个打开文件表项,但对一个给定的文件只有一个 inode 节点表项。

之所以每个进程都获得自己的打开文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。

2) dup(1) 复制文件描述符

dup() 用来复制一个现有的文件描述符。

$ man 2 dup
" z, i9 A: q+ z( n- B' [& u       #include <unistd.h>3 d, N- ^  ~  ?" l# ]/ v
       int dup(int oldfd);

* N- e$ \, l! ]" a1 @+ \

dup(1)后的内核数据结构:

微信图片_20200718181915.png

dup() 返回的新文件描述符与参数 oldfd 共享同一个打开文件表项。

3) fork 之后父进程和子进程之间对打开文件的共享
微信图片_20200718181918.png
9 Q# @! {3 o( t5 M+ |! n
; k4 v7 A' ^( p# Q# s

假定所用的描述符是在fork之前打开的,如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,如使父进程等待子进程,那么它们的输出就会相互混合。


' ?( \( H( C* y! K" a0 g

三、总结

不好意思,这周身体不太舒服,文章拖更了,各位见谅。

鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容请大家先自行去阅读吧,不是自己理解到的东西是消化不了的。有机会的话我会把更多的读书心得放在后面的文章。

赞 收藏 评论1 发布时间:2020-7-18 18:20

举报

1个回答
李康1202 回答时间:2020-7-19 09:10:02
顶一下
mark.png
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版