Skip to content

文件系统概述

从本章开始学习各种Linux系统函数,这些函数的用法必须结合Linux内核的工作原理来理解, 因为系统函数正是内核提供给应用程序的接口, 而要理解内核的工作原理,必须熟练掌握C语言, 因为内核也是用C语言写的,我们在描述内核工作原理时必然要用指针、结构体、链表这些名词来组织语言,就像只有掌握了英语才能看懂英文书一样,只有学好了C语言才能看懂我描述的内核工作原理。

系统调用

由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。

一个helloworld如何打印到屏幕

每一个FILE文件流(标准C库函数)都有一个缓冲区buffer,默认大小8192Byte。Linux系统的IO函数默认是没有缓冲区。

库函数和系统函数

  • 库函数是 C 语言提供的函数,这些函数是由编译器提供的,我们可以直接使用这些函数而不需要自己实现,并且这些函数是能够跨平台运行的。

  • 系统函数是操作系统提供的函数,这些函数是由操作系统提供的,我们也可以直接使用这些函数而不需要自己实现,但是这些函数只能够在特定的操作系统上运行。

库函数是对系统函数的封装,库函数的实现是通过系统函数来实现的。而系统函数是由操作系统提供的,是操作系统的接口,其底层又是通过系统调用来实现的。

IO库函数的工作流程

使用fopen函数打开一个文件,返回一个FILE* fp,这个指针指向的结构体有三个重要的成员。

  • 文件描述符: 通过文件描述可以找到文件的inode,通过inode可以找到对应的数据块
  • 文件指针: 读和写共享一个文件指针,读或者写都会引起文件指针的变化
  • 文件缓冲区: 读或者写会先通过文件缓冲区,主要目的是为了减少对磁盘的读写次数,提高读写磁盘的效率。
  • 头文件 stdio.h 的第48行处:typedef struct _IO_FILE FILE;
  • 头文件 libio.h 的第241行处:struct _IO_FILE,这个接头体定义中有一个_fileno成员,这个就是文件描述符

虚拟地址空间

进程的虚拟地址空间分为用户区和内核区,其中内核区是受保护的,用户是不能够对其进行读写操作的;内核区中很重要的一个就是进程管理,进程管理中有一个区域就是PCB(本质是一个结构体);PCB中有文件描述符表,文件描述符表中存放着打开的文件描述符,涉及到文件的IO操作都会用到这个文件描述符。

pcb和文件描述符表

备注:

pcb:结构体:task_stuct,该结构体在:

/usr/src/linux-headers-4.4.0-97/include/linux/sched.h:1390

一个进程有一个文件描述符表:1024

  • 前三个被占用,分别是STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO
  • 文件描述符作用:通过文件描述符找到inode,通过inode找到磁盘数据块。

虚拟地址空间内核区PCB文件描述表文件描述符文件IO操作使用文件描述符

PCB

在内核中,保存进程状态的数据结构叫做 进程控制段 (英语:Process Control Block,PCB)。它包含了进程的很多信息,如:进程当前状态,程序计数器, CPU寄存器的值(当调度器暂停当前进程准备让其他进程执行时,将CPU寄存器中的数据现场保存),CPU调度信息,内存 信息(页表),I/O状态(打开的文件和I/O设备等)。在Linux中,PCB就是我们在上一节中提到的保存在双向循环链表中的 task_struct结构,PCB是在各个操作系统中通用的概念。

总之,PCB保存的就是进程间不同的数据,其作用就是在调度器切换执行进程时保存/恢复数据现场。

参考链接:

文件描述符

Linux 系统中,把一切都看做是文件(一切皆文件),当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。

文件描述符的使用

  • 一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符进行管理;通常默认大小为1024,即能容纳1024个文件描述符,可以使用 limit -a 进行查看;
  • 文件描述符表中0、1、2三个位置对应的文件描述符固定不变,标准输入、标准输出、标准错误;
  • 当打开一个文件时,内核会自动在文件描述符表中寻找一个空闲且最小的文件描述符;
  • 同一个文件可以被多次打开,但是每打开一次都需要一个新的文件描述符;
  • 已经被占用的文件描述符在被释放后,可以后重新被占用;

文件描述符的优点

  • 基于文件描述符的I/O操作兼容POSIX标准。
  • 在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符。

文件描述符的缺点

  • 在非UNIX/Linux操作系统上(如Windows NT),无法基于这一概念进行编程。
  • 由于文件描述符在形式上不过是个整数,当代码量增大时,会使编程者难以分清哪些整数意味着数据,哪些意味着文件描述符。因此,完成的代码可读性也就会变得很差。

文件描述符、文件、进程间的关系

  • 每个文件描述符会与一个打开的文件相对应;
  • 不同的文件描述符也可能指向同一个文件;
  • 相同的文件可以被不同的进程打开,也可以在同一个进程被多次打开;

文件描述符表

每个进程都维护着自己的一个文件描述符表,每个文件描述符占其中一。该表记录进程打开的文件相关信息,因文件描述符为进程所有,文件描述符表也为进程内共享;文件表结构体内存在一个指针变量指向存放在内核空间的文件结构表。

阻塞和非阻塞

普通文件默认是非阻塞的,无论文件是否有数据,read 函数都会立即返回,不会等待。

终端设备、管道和套接字默认阻塞,若数据未就绪(如终端无输入、网络无数据包),read 函数会一直阻塞,直到数据到达或发生错误。

思考:阻塞和非阻塞是文件的属性还是read函数的属性?

查看 read 函数的函数原型,测试读取普通文件和终端设备的阻塞和非阻塞特性。普通文件可以自己新建一个,也可以使用 touch 命令创建一个空文件。终端设备可以读取 /dev/tty 设备。

写代码前的准备工作

在Linux系统编程阶段有很多的判断代码是需要重复编写的,比如使用open函数打开一个文件要判断文件打开是否成功,又比如使用read函数读取文件内容要判断是否读取成功等等。

我们可以使用宏定义来简化判断代码的编写,将宏定义定义在一个头文件当中,在.c源代码中引入该头文件即可。

但是,头文件如果放在源代码同级目录中,那么在其他文件夹中的源代码使用此头文件就有点困难!我们可以将自己编写的头文件移动到系统头文引用的目录中(Linux中路径为:/usr/include,Mac中路径为:/usr/local/include【如果Mac的/usr/local/中没有include目录自己新建一个即可】),需要主要的是该目录下的文件需要root权限才能操作!因此将头文件移动到该目录后还需要将该头文件的权限改为其他用户可读写

shell
sudo touch /usr/include/syssLinuxHeader.h
sudo chmod o+w /usr/include/syssLinuxHeader.h
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 校验程序运行参数
#define ARGS_CHECK(argc, num)                                                  \
  {                                                                            \
    if (argc != num) {                                                         \
      fprintf(stderr, "args error\n");                                         \
      return -1;                                                               \
    }                                                                          \
  }

编写一个.c 源文件进行测试

c
#include <syssLinuxHeader.h>

int main(int argc, char *argv[]) {
    ARGS_CHECK(argc, 2);

    return 0;
}

看到程序正常编译运行即可,如需其他重复编写的校验语法可以继续在头文件中进行宏定义。