第03章 文件编程

目标

  • 了解Linux系统文件IO/标准IO基本概念
  • 掌握Linux系统文件IO/标准IO常用函数
  • 掌握Linux系统文件属性常用函数
  • 掌握Linux系统目录文件常用函数

3.1 Linux系统概述

3.1.1 预备知识(相关概念)

(1)应用程序 和 内核程序

应用程序是应用开发者在应用层实现的用户程序,内核程序是操作系统的内部程序,众多内核程序组成了操作系统内核(因此内核本质上就是一堆程序)。

(2)特权指令 和 非特权指令

CPU运行的指令分为特权指令和非特权指令两种,应用程序只能使用非特权指令,如加法指令等。内核程序作为计算机管理者会让CPU执行一些特权指令,如内存清零指令等。

CPU在运行一条指令前就能判断出该指令是特权指令还是非特权指令。

(3)用户空间(用户态) 和 内核空间(内核态)

为了保护内核的安全,操作系统一般都限制用户进程不能直接操作内核,在32位操作系统总的地址空间4G(2^32 = 4GB),实现这个限制的方式就是操作系统将总的地址空间(虚拟地址空间)分为两个部分,对于Linux操作系统:

  • 高位的1G空间(0xC000 0000 - 0xFFFF FFFF)分配给内核,称为内核空间,内核程序运行在内核空间,对应的进程就处于内核态。
  • 另外3G空间(0x0000 0000 - 0xBFFF FFFF)分配给用户使用,称为用户空间,用户程序运行在用户空间,对应的进程处于用户态。

总之,有1G的内核空间是每个进程共享的,剩下的3G是进程自己使用的。

在内核态下,CPU可以执行指令系统的全集,也就是说内核态进程可以调用系统的一切资源,但是特权指令只能在内核态下执行,它不直接提供给用户使用,用户态下只能使用非特权指令,也就是说用户态进程只能执行简单运算,不能直接调用系统资源。

(1)从大的方面讲,Linux体系结构(就是Linux系统的构成)可以分为两块:

  • 用户空间:用户空间包括用户的应用程序,C库;
  • 内核空间:内核空间包括系统调用,内核,以及与平台架构相关的代码。
  1. 内核仅仅是操作系统的一部分,是真正与硬件交互的那部分软件,与硬件交互包括读写硬盘、读写网盘、读写内存以及任何连接到系统中的硬件。除了与硬件交互外,内核还负责分配资源,分配什么资源呢?所谓资源就是硬件,比如CPU时间、内存、IO等等,这些都是资源。内核的职责就是以进程的形式来分配CPU时间,以虚拟内存的形式来分配物理内存,以文件的形式来管理IO设备。内核是给人用的,为了与内核交互,发明了命令行以及图形界面GUI。除了给普通用户提供使用的接口之外,操作系统还需要给程序员提供编写程序的接口,通过系统调用,我们可以像使用普通函数那样向操作系统请求服务。
  2. linux内核的主要组件有:系统调用接口、进程管理、内存管理、虚拟文件系统、网络堆栈、设备驱动程序、硬件架构的相关代码。

Linux内核的任务:

  • 从技术层面讲,内核是硬件与软件之间的一个中间层。作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
  • 从应用程序的层面讲,应用程序与硬件没有联系,只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。
  • 内核是一个资源管理程序。负责将可用的共享资源(CPU时间、磁盘空间、网络连接等)分配到各个系统进程。
  • 内核就像一个库,提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。

3.1.3 linux文件系统

  1. 操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。
  2. 通常文件系统是用于存储和组织文件的一种机制,便于对文件进行方便的查找与访问。
  3. 文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。
  4. 它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。

(5)存储设备(块设备,像硬盘、flash等)是分块(扇区)的,物理上底层去访问存储设备时是按照块号(扇区号)来访问的。这就很麻烦。文件系统的设计理念是通过文件系统将底层难以管理的物理磁盘扇区式访问,转换成目录+文件名的方式来访问。

(6)文件系统是一些代码,是一套软件,这套软件的功能就是对存储设备的扇区进行管理,将这些扇区的访问变成了对目录和文件名的访问。我们在上层按照特定的目录和文件名去访问一个文件时,文件系统会将这个目录+文件名转换成对扇区号的访问。

(7) linux常见文件系统如下表:

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ext2: 早期linux中常用的文件系统 ext3: ext2的升级版,带日志功能 RAMFS: 内存文件系统,速度很快 NFS: 网络文件系统,由SUN发明,主要用于远程文件共享 MS-DOS: MS-DOS文件系统 VFAT: Windows 95/98 操作系统采用的文件系统 FAT: Windows XP 操作系统采用的文件系统 NTFS: Windows NT/XP 操作系统采用的文件系统 HPFS: OS/2 操作系统采用的文件系统 PROC : 虚拟的进程文件系统 ISO9660 : 大部分光盘所采用的文件系统 ufsSun : OS 所采用的文件系统 NCPFS: Novell 服务器所采用的文件系统 SMBFS: Samba 的共享文件系统 XFS: 由SGI开发的先进的日志文件系统,支持超大容量文件 JFS: IBM的AIX使用的日志文件系统 ReiserFS : 基于平衡树结构的文件系统 udf: 可擦写的数据光盘文件系统 |

(8)文件系统的创建 磁盘格式化就是创建文件系统的过程。

元数据存储区存放文件Metadata,包含:inode表、inode位图和块位图。

每个块对应一个存储位可以找空闲块,元数据存储和数据要匹配大小。

数据存储区 划分成块,每个块可以设置为1kB、2KB、4KB。

说明:

  1. Block Group:ext2 文件系统会根据分区的大小划分为数个 Block Group。而每个 Block Group 都有着相同的结构组成。
  2. Super Block(超级块):记录整个文件系统的信息,包括block与inode的总量,已经使用的inode和block的数量,未使用的inode和block的数量,inode和block的大小,文件系统的挂载时间,最近一次的写入时间,最近一次的磁盘检验时间等。super block 的信息被破坏,可以说整个文件系统结构就被破坏了。
  1. GDT(Group Descriptor Table:块组描述符表):描述块组属性信息。
  2. Block Bitmap(块位图):Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
  3. inode Bitmap(inode位图):每个 bit 表示一个 inode 是否空闲可用。
  4. inode Table(i节点表):用来记录文件的权限(r、w、x),文件的所有者和属组,文件的大小,文件的状态改变时间(ctime),文件的最近一次读取时间(atime),文件的最后一次修改时间(mtime),文件的数据真正保存的block编号。每个文件需要占用一个inode。
  5. Data Block(数据块,也称为block):用来实际保存数据的,block的大小(1KB、2KB或4KB)和数量在格式化后就已经决定,不能改变,除非重新格式化。每个block 只能保存一个文件的数据,要是文件数据小于一个block块,那么这block的剩余空间不能被其他文件使用;要是文件数据大于一个block块,则占用多个block块。Windows的磁盘碎片整理工具的原理就是把一个文件占用的多个block块尽量整理到一起,这样可以加快读写速度。

3.1.4 虚拟文件系统VFS

(1) VFS(Virtual Filesystem Switch)称为虚拟文件系统或虚拟文件系统转换,是一个内核软件层,在具体的文件系统之上抽象的一层,表现为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统,同时也为不同文件系统的通信提供了媒介。

(2) VFS并不是一种实际的文件系统,它只存在于内存中,不存在任何外存空间,VFS在系统启动时建立,在系统关闭时消亡。

(3) VFS在linux架构中的位置

VFS在整个Linux系统中的架构视图如下:

从这张图中,我们可以看出,系统调用函数并不是直接操作真正的文件系统,而是通过一层中间层,也就是我们说的虚拟文件系统。

(4)为什么要有虚拟文件系统?

不同的文件系统格式是不一样的,也就是说如果不通过虚拟文件系统,直接对真正的文件系统进行读取,有几种类型的文件系统,你就得写几种相对应的读取函数,所以说虚拟文件的出现(VFS)就是为了通过使用同一套文件 I/O 系统调用即可对Linux中的任意文件进行操作而无需考虑其所在的具体文件系统格式。

(5)VFS的数据结构

VFS依靠四个主要的数据结构和一些辅助的数据结构来描述其结构信息。

1)超级块对象

存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统;每次一个实际的文件系统被安装时, 内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。

2)索引节点对象

索引节点inode:保存的其实是实际的数据的一些信息,这些信息称为"元数据"(也就是对文件属性的描述)。( 注意数据分成:元数据+数据本身 )

例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。

同时注意:inode有两种,一种是VFS的inode,一种是具体文件系统的inode。前者在内存中,后者在磁盘中。所以每次其实是将磁盘中的inode调进填充内存中的inode,这样才是算使用了磁盘文件inode。

当创建一个文件的时候,就给文件分配了一个inode。一个inode只对应一个实际文件,一个文件也会只有一个inode。

当我们打开一个文件的时候,首先,系统找到这个文件名对应的inode号;然后,通过inode号,得到inode信息,最后,由inode找到文件数据所在的block,现在可以处理文件数据了。

3)目录项对象

引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是 普通的文件,都是一个目录项对象。

如:在路径 /home/source/test.c 中,目录 /、home、source 和文件 test.c都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。

注意:目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。4)文件对象

文件对象描述的是进程已经打开的文件,主要用于建立进程和磁盘上的文件的对应关系。因为一个文件可以被多个进程打开,所以一个文件可以存在多个文件对象。但是由于文件是唯一的,那么inode就是唯一的,目录项也是定的!

  1. 进程与超级块、文件、索引结点、目录项的关系

3.1.5 linux目录结构

(1)文件结构是指文件在存储设备中的组织方式。主要体现在对文件和目录的组织上,目录提供了一个管理文件的有效而方便的途径。

(2)linux使用树状目录结构,在安装系统时,安装程序已经为用户创建了文件系统和完整而固定的目录组成形式,并指定每个目录的作用和其中的文件类型(如下图所示)。该结构的最上层是根目录,其他所有目录都是从根目录出发生成的。

(3)linux下一些主要目录的功能:

  1. /bin - 二进制可执行文件

/bin 目录存放系统启动和修复所需的最基本的二进制可执行文件。这些文件通常对于系统的基本操作是必不可少的,因此/bin 目录下的命令可以在系统启动时使用。

示例:/bin/ls 命令用于列出目录内容。

  1. /boot - 启动文件

/boot 目录包含了引导Linux系统所需的文件,包括启动菜单、内核文件和初始RAM磁盘镜像等。这些文件在系统引导时被加载。

示例:/boot/vmlinuz-5.4.0-1 是Linux内核文件。

  1. /dev - 外部设备

/dev 目录包含了Linux系统中的外部设备节点和特殊文件。访问这些设备等同于访问对应的硬件设备。

示例:/dev/sda 代表系统的第一个硬盘。

  1. /etc - 配置文件

/etc 目录包含系统的配置文件,用户和管理员可以通过修改这些文件来配置系统的行为。

示例:/etc/passwd 文件存储用户账户信息。

  1. /home - 用户主目录的父目录

/home目录是普通用户主目录的父目录,每个用户都可以在这个目录下拥有自己的主目录。

示例:/home/john 是用户John的主目录。

  1. /lib - 系统库文件

/lib 目录存放系统必要的库文件,这些库文件包含为系统程序提供API的代码。

示例:/lib/x86_64-linux-gnu/libc.so.6 是GNU C库。

  1. /media - 可移动设备挂载点

/media 目录是可移动设备如U盘、光驱等的挂载点,当可移动设备被挂载时,它们会出现在这个目录下。

示例:/media/usb0 可以用于挂载U盘。

  1. /mnt - 临时文件系统挂载点

/mnt 目录是系统提供的一个临时挂载文件系统的安装点,系统管理员可以手动将文件系统挂载在此目录下。

示例:可以将一个共享目录临时挂载在 /mnt/share 上。

  1. /opt - 可选插件软件包安装目录

/opt 目录存放着可选的软件包和附加的系统软件,这些软件包可以安装在 /opt 目录下。

示例:/opt/oracle/ 下可以安装Oracle数据库软件。

  1. /root - 超级用户主目录

/root 目录是系统超级用户(root)的主目录。root用户主要用于系统管理,其家目录为 /root。

示例:root用户的bash配置文件在 /root/.bashrc。

  1. /sbin - 系统管理员工具

/sbin 目录存放系统管理员使用的系统管理程序,只有root用户才能访问这些程序。

示例:/sbin/fdisk 命令用于分区管理。

  1. /srv - 服务数据目录

/srv 目录存放一些服务启动后需要提取的数据,可以根据服务的系统名划分子目录。

示例:/srv/cvs 对应CVS服务的数据目录。

  1. /tmp - 临时文件夹

/tmp目录用于存放各种临时文件,是公共的临时文件存储点。重要数据不应该存放在此目录。

示例:许多程序会在 /tmp 下创建临时工作文件。

  1. /usr - 用户应用和文件

/usr 目录存储用户应用程序和文件,主要包括可共享的可执行文件、库、文档等。

示例:/usr/bin 下存放各种应用程序。

  1. /var - 变量数据目录

/var 目录用于存放系统在运行过程中经常变化的文件,如日志、缓存、邮箱等。

示例:/var/log/ 存放各种服务日志文件。

(4)windows也是采用的树形目录结构,但是windows的树形结构的根目录是磁盘分区的盘符,有几个分区就有几个树形结构,他们之间的关系是并列的,而在linux操作系统中根目录只有一个,这是两种操作系统在文件结构上的主要不同。

3.1.6 系统调用与C标准库

(1)什么是系统调用

系统调用是操作系统提供给应用程序调用的特殊函数,应用程序可以通过系统调用请求操作系统提供的基层服务(如操控硬件),而无需关心底层如何实现。

简单来说,系统调用是应用程序和硬件之间的接口。

(2)为什么需要系统调用

因为用户进程直接操作硬件可能发生不可预知的错误和不安全的非法操作,所以硬件等资源必须由操作系统内核管理,用户进程想要操控硬件等资源必须委托操作系统完成,而无法直接使用和操控硬件等资源。内核会对各个进程的资源请求进行协调处理。

正因为用户进程无法直接操控硬件,因此需要操作系统提供系统调用这样的接口给用户进程间接的有权限限制的操控资源。

(3)什么功能会用到系统调用

凡是与共享资源和设备有关的操作(如存储分配、IO操作、文件管理等),都必须通过系统调用向操作系统请求服务,由内核代为完成。

(4)C标准库

就是存放在函数库中的函数,具有明确的功能、入口调用参数和返回值。例如:屏幕打印printf,字符串拷贝strcpy;像这些基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。C库函数是由编译器的厂商提供实现。C语言常用的库函数都有:IO函数、字符串操作函数、字符操作函数、内存操作函数、时间/日期函数、数学函数和其他库函数。注意:使用库函数,必须包含 #include 对应的头文件。

(5)系统调用和库函数的区别

系统调用是内核程序,而库函数一般是高级语言自定义的函数,但系统调用可以被封装在库函数中以隐藏一些细节使程序员编程更方便,用户程序可以调用这些库函数间接调用这些系统调用,也可以直接进行系统调用。

3.2 关于IO的认识

3.2.1 IO是什么

(1)IO是指Input/Output,即输入和输出。

(2)IO从广义上说,是数据流动的过程。

(3)从计算机架构上讲,CPU和内存与其他外部设备之间的数据转移过程就是IO。

(4)从用户进程的角度理解IO。用户进程要完成IO读写,需要对内核发起IO调用,内核执行IO任务,返回IO结果,即完成一次IO。内核为每个IO设备维护一个内核缓冲区。

3.2.2 IO的分类

文件读写方式的各种差异,导致I/O的分类多种多样。最常见的有:缓冲与非缓冲I/O 、直接与非直接I/O、阻塞与非阻塞I/O、同步与异步I/O。

(1)根据是否利用标准库缓存,可以把I/O分为缓冲I/O与非缓冲I/O。

  1. 缓冲I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调用访问文件。(又称为标准IO)。
  2. 非缓冲I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。(又称为系统IO,底层IO)

注意,这里所说的"缓冲",是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。

无论缓冲I/O还是非缓冲I/O,它们最终还是要经过系统调用来访问文件。我们知道,系统调用后,还会通过页缓存,来减少磁盘的I/O 操作。

(2)根据是否利用操作系统的页缓存,可以把文件I/O分为直接I/O与非直接I/O。

  1. 直接I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
  2. 非直接I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。

想要实现直接I/O,需要你在系统调用中指定 O_DIRECT 标志。如果没有设置过,默认的是非直接I/O。

(3)根据应用程序是否阻塞自身运行,可以把文件I/O分为阻塞I/O和非阻塞I/O

  1. 阻塞I/O,是指应用程序执行I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
  2. 非阻塞I/O,是指应用程序执行I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。

比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。

(4)根据是否等待响应结果,可以把文件I/O 分为同步I/O和异步I/O

  1. 同步I/O,是指应用程序执行I/O操作后,要一直等到整个I/O完成后,才能获得I/O响应。
  2. 异步I/O,是指应用程序执行I/O操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次I/O完成后,响应会用事件通知的方式,告诉应用程序。

例如,在操作文件时,如果设置了O_SYNC或者O_DSYNC标志,就代表同步I/O。如果设置了O_DSYNC,就要等文件数据写入磁盘后,才能返回;而O_SYNC,则是在O_DSYNC 基础上,要求文件元数据也要写入磁盘后,才能返回。

3.2.3 文件IO与标准IO

(1)文件IO(又叫系统IO、低级IO)称之为不带缓存的I/O(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。

(2)标准I/O 是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存。

  • 全缓存:当填满标准I/O缓存后才进行实际的I/O操作。
  • 行缓存:当输入或输出中遇到新行符时,标准I/O库执行I/O操作。
  • 无缓存:指标准I/O库不对字符进行缓存,直接调用系统调用。

(3)标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。

(4)文件I/O中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等;标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。

(5)文件IO无缓冲输出, 一般用于操作设备文件(实时刷新);标准IO有缓冲输出, 一般用户操作普通文件(不需要实时刷新)。

3.2.4 流和FILE对象

(1)流(stream)对应自然界的水流,在文件操作中,文件类似是一个大包裹,里面装了一堆字符,但文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑儿的读写,则一个文件中N多个字符被挨个依次读出/写入时,这些字符就构成了一个字符流。

(2)流这个概念是动态的,不是静态的;编程中提到流这个概念,一般都是IO相关的,所以经常叫IO流;文件操作时就构成了一个IO流。

(3)Linux一切皆文件,一切都是流。用户进程都是对这些流进行读写操作,实现数据交换。

(4)准确地说,流是带缓冲的IO(标准IO)才有的概念。

流有方向。对流的读写操作,可以理解为IO操作。如图所示。

如果流中没有数据,读取,就阻塞。进一步说,是用户缓冲区没有数据,无法读取数据。

如果流中数据已满,写入,就阻塞。进一步说,是用户缓冲区数据已满,无法写入数据。

(5)文件IO主要是针对文件描述符的,而标准IO的操作主要是围绕流进行的,当用标准IO打开或创建一个文件时,就使得一个流与对应的文件相结合。

(6)当打开一个流时,标准IO函数fopen()会返回一个FILE类型指针,该对象通常是一个结构体,它包含了标准IO库为管理该流所需要的所有信息:用于物理IO的文件描述符、 指向流缓存的指针、 缓存指针长度、当前在缓存中的字符数、出错标识等。

(7)默认情况下,每个进程都会自动打开三个流:标准输入、标准输出和标准错误。stdio.h头文件将这三个标准IO流通过文件指针stdin、stdout和stderr加以引用,以供每个进程自由使用。

3.2.5 IO操作

对于用户进程的一个读IO操作,包括以下阶段:

(1)用户进程调用IO系统调用读数据。

(2)内核先看下内核缓冲区是否有数据,如果没有数据,则从设备读取,先加载到内核缓冲区,再复制到用户进程缓冲区;如果有数据,直接复制到用户进程缓冲区(对于标准IO)。

3.3 文件操作

3.3.1 文件操作接口

(1)系统调用接口是一些函数,这些函数由linux系统提供支持,由应用程序来使用。应用层程序通过调用系统调用接口函数来调用操作系统中的各种功能,实现具体的任务。学习一个操作系统,其实就是学习使用这个操作系统的系统调用接口函数。

(2)使用系统调用进行操作文件的常用的接口函数:open、close、read、write、lseek等。

(3)使用C库进行操作文件的常用的接口函数:fopen、fclose、fread、fgetc、fgets、fwrite、fputc、fputs、fseek等。

3.3.2 文件操作的一般步骤

(1)在linux系统中要操作文件,一般是先open打开文件,得到文件描述符,然后对文件进行读写操作(或其它操作),最后close关闭文件即可。

(2)我们对文件进行操作时,一定要先打开文件,打开成功后才能去操作,如果打开失败,后面就无法操作了; 最后读写完成后一定要close关闭文件,否则可能会造成文件损坏。

(3)文件平时是存放在块设备中的文件系统中的,这种文件称为静态文件。当我们使用open打开文件时,linux内核做的操作包括:内核在进程中建立了打开该文件的数据结构,记录下我们

打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放,这种文件称为动态文件。

(4)当打开文件后,我们针对该文件的所有读写操作,都是针对内存中存放的动态文件的;当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了;当我们close关闭动态文件时,close内部内核会将内存中的动态文件的内容同步到块设备中的静态文件。

(5)内核设计文件操作的原理:因为块设备本身以块为单位进行读写操作的特性就决定了内核对块设备进行操作非常不灵活;而内存可以按字节为单位进行操作,并且可随机操作,非常灵活。

3.3.3 文件描述符

(1)文件描述符的本质是一个数字,该数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index索引查表得到文件表指针,再间接访问得到该文件对应的文件表。

(2)文件描述符是open系统调用内部由操作系统自动分配的,操作系统规定fd从0开始依次增加;fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件;可以通过指令ulimit -n在终端查看最大打开文件限制数。linux中文件描述符表是个指针数组(不是链表),其中fd是index,文件表指针是value。

(3)当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回;即如果之前fd已经占满了0-9,那我们下次open得到的一定是10(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了)。

(4)fd中0、1、2已经默认被系统内核占用了,因此用户进程得到的最小的fd就是3了;当我们运行一个程序得到一个进程时,内部默认已打开3个文件,其对应的fd就是0、1、2;这3个文件分别叫stdin、stdout、stderr,即标准输入、标准输出、标准错误。

(5)标准输入一般对应的是键盘(0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(1对应LCD的设备文件);printf函数其实就是默认输出到标准输出stdout上了,fpirntf函数可以指定输出到哪个文件描述符中。

(6)open返回的fd必须保存好,以后对该文件的所有操作都要靠该fd去对应该文件,最后关闭文件时也需要fd去指定关闭该文件。如果在该文件关闭前丢掉了fd,那么该文件就无法读写和关闭了。

3.3.4 实时查询man手册

(1)当我们编写应用程序时,很多API原型都不可能记得,所以要实时查询man手册。

(2)man 1 xx查shell命令,man 2 xx查系统调用函数,man 3 xx查库函数。

3.3.5 打开文件

open()函数用于打开或创建文件,在打开或创建文件时可以指定用户的属性及用户的权限等各

种参数。

open()函数语法要点

|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 头文件 | #include<sys/types.h> /* 提供类型pid_t的定义 */ #include<sys/stat.h> #include<fcntl.h> |
| 函数原型 | int open(const char *pathname, int flags, mode_t mode); |
| 函数传参 | Pathname: 被打开的文件名(包括路径名) flag:文件打开的方式 O_RDONLY:以只读方式打开文件 O_WRONLY:以只写方式打开文件 O_RDWR:以读写方式打开文件 O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三个参数为其设置权限 O_EXCL:如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。此时open是原子操作,防止多个进程同时创建同一个文件 O_TRUNC:若文件已经存在,那么会删除文件中的所有数据,并设置文件大小为0 O_APPEND:以添加方式打开文件,在打开文件的同时,文件指针指向文件的末尾,即将写入的数据添加到文件的末尾 mode:被打开文件的存取权限 可以用一组宏定义:S_I(R/W/X)(USR/GRP/OTH) 其中R/W/X分别表示读/写/执行权限 USR/GRP/OTH分别表示文件所有者/文件所属组/其他用户 例如,S_IRUSR|S_IWUSR表示设置文件所有者的可读可写属性。 八进制表示法中600也表示同样的权限 |
| 返回值 | 成功:返回文件描述符 失败:-1 |

注意:在open()函数中,flag参数可通过"|"组合构成,但前3个标志常量(O_RDONLY、O_WRONLY以及O_RDWR)不能相互组合。mode是文件的存取权限,既可以用宏定义表示法,也可以用八进制表示法。

3.3.6 读取文件内容

read()函数用于将从指定的文件描述符中读出数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前指针位置开始。当从终端设备文件中读出数据时,通常最多一次读一行。

read()函数语法要点

|------|----------------------------------------------|
| 头文件 | #include<unistd.h> |
| 函数原型 | ssize_t read(int fd,void *buf,size_t count) |
| 函数传参 | fd:文件描述符 buf:指定存储器读出数据的缓冲区 count:指定读出的字节数 |
| 返回值 | 成功:读到的字节数 0:已到达文件尾 -1:出错 |

  1. 说明:
  • fd表示要读取哪个文件,fd一般由前面的open返回得到。
  • buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容。
  • count是我们想要读取的字节数。
  • 返回值表示实际成功读取的字节数。

(2)返回值ssize_t类型是linux内核用typedef重定义的一个类型,其实就是int。其目的是为了构建平台无关代码,方便程序迁移平台,使代码具有更好的可移植性。

3.3.7 向文件写入

write()函数用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败。

write()函数语法要点

|------|-----------------------------------------------|
| 头文件 | #include<unistd.h> |
| 函数原型 | ssize_t write(int fd,void *buf,size_t count) |
| 函数传参 | fd:文件描述符 buf:指定存储器写入数据的缓冲区 count:指定写入的字节数 |
| 返回值 | 成功:已写入的字节数 -1:出错 |

注意

  • const在buf前面的作用是该参数buf是作为输入型参数,输入型参数在函数中是只读的,不能更改。
  • buf的指针类型为void空类型,即该函数操作的数据流没有明确的类型,可操作所有类型的数据。

该示例程序中刚才成功写入14字节,然后读出结果读出是0(但是读出成功了),可考虑该问题的原因是啥。

3.3.8 关闭文件

close()函数用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自

动关闭,很多程序都使用这一功能而不显示地关闭一个文件。

close()函数语法要点

|------|----------------------|
| 头文件 | #include<unistd.h> |
| 函数原型 | int close(int fd) |
| 函数传参 | fd:文件描述符 |
| 返回值 | 0:成功 -1:出错 |

3.4 open函数的flag详解

3.4.1 文件读写权限

(1)linux中文件有读写权限,我们在open打开文件时也可以附带一定的权限说明(譬如O_RDONLY就表示以只读方式打开,O_WRONLY表示以只写方式打开,O_RDWR表示以可读可写方式打开);当我们在open文件时附带了某种权限后,打开的文件就只能按照该权限来操作。

3.4.2 更改文件内容

(1)当我们open已经存在并且内部有内容的文件可能会出现4种情况:

① 原来的内容消失了(使用O_TRUNC标志);

② 新内容添加在原内容后面(使用O_APPEND标志);

③ 新内容添加在原内容前面;

④ 不读写该文件时原来该文件的内容不变(默认不使用O_APPEND和O_TRUNC标志)。

如果O_APPEND和O_TRUNC同时出现,经代码验证O_TRUNC标志起作用,O_APPEND标志作用被屏蔽了。

3.4.3 打开不存在的文件

(1)当我们open某文件时针对该文件是否存在会出现2种情况:

①创建并且打开并不存在的文件,若该文件存在则该文件会被修改重置(使用O_CREAT标志);②创建并且打开并不存在的文件,若该文件存在则会报错,不会创建修改该文件(同时使用O_EXCL标志和O_CREAT标志)。

(2)open函数在使用O_CREAT标志去创建文件时,可以使用第3个参数mode来指定要创建的文件的权限;mode使用4个数字来指定权限,其中后面3个很重要,对应我们要创建的目标文件的权限标志;譬如创建一个可读可写不可执行的文件就用0666。

3.4.4 退出进程或程序

(1)当程序(进程)在前面的操作执行失败导致后面的操作都不可能进行下去时,我们应在前面的错误监测程序中结束整个程序(进程),不应该让程序(进程)继续运行下去。

(2)在main函数中使用return关键字,一般原则是程序正常终止return 0,程序异常终止return -1,不常用。

(3)正式终止进程(程序)应使用exit或_exit或_Exit之一,一般原则是进程正常终止exit(0),程序异常终止exit(-1),经常使用。exit()会执行一些清理工作,最后调用_exit()函数,是一个C库函数(调用_exit()_Exit()函数会清除其使用的内存空间,并销毁其在内核中的各种数据结构,关闭进程的所有文件描述符,并结束进程、将控制权交给操作系统)。

3.4.5 阻塞与非阻塞

(1)如果某个函数是阻塞式的,则我们调用该函数时当前进程有可能被阻塞住(实质是该函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住了就不能立刻返回;如果某个函数是非阻塞式的,那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。

(2)阻塞和非阻塞是两种不同的设计思路,并没有好坏;总的来说,阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但结果没保障。

(3)操作系统提供的系统调用函数和由系统调用函数封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的,所以我们编写程序调用这些函数的时候必须明确该函数是阻塞式还是非阻塞式。

(4)默认情况下,我们open某个文件时是阻塞式的(在打开该文件后读写该文件时若出现问题则会导致阻塞),如果希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志;阻塞与非阻塞只作用于设备文件(linux的硬件设备如串口、I2C通讯器件、LCD),而不作用于普通文件。

3.4.6 底层阻塞和非阻塞

(1)write默认只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候自动会将底层缓冲区中的内容一次性的同步到硬盘中;该设计机制是为了提升硬件操作的性能和硬件寿命;但有时候我们希望硬件不要等待,直接将我们的内容写入硬盘中,则使用O_SYNC标志(write阻塞等待底层完成写入才返回到应用层)。

3.5 文件读写细节及文件管理

3.5.1 errno和perror

(1)errno(error_number即错误号码),linux系统中对各种常见错误做了编号表,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们该函数到底哪里错了(前提是该函数返回时会设置errno);errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。

(2)errno本身实质是一个int类型的数字,每个数字编号对应一种错误,当我们只看errno时只能得到一个错误编号数字(譬如-37);linux系统提供了一个函数perror(print_error),perror函数内部会读取errno并且将其直接转成对应的错误信息字符串,然后print打印出来。

3.5.2 read和write的count

(1)count和返回值的关系;count参数表示我们想要写或者读的字节数,返回值表示实际完成的要写或者读的字节数;实际的有可能等于想要读写的,也有可能小于(说明没完成任务)。

(2)count再和阻塞非阻塞结合起来,就会更加复杂,如果某个函数是阻塞式的,则我们要读取30个字节数据,结果暂时只有20个字节数据时就会被阻塞住,等待剩余的10个字节数据可以读。

(3)有时候我们写正式程序时,我们要读取或者写入的是一个很庞大的文件(譬如文件有2MB),我们不可能把count设置为2*1024*1024,而应该去把count设置为一个合适的数字(譬如2048、4096),然后通过多次读取来实现全部读完。

3.5.3 静态文件和inode节点

(1)文件平时都存放在硬盘中,硬盘中存储的文件以一种固定的方式存放,其被称为静态文件。

(2)1块硬盘区域可分为两块区域,一块是硬盘内容管理表项,另一块是真正存储内容的区域;操作系统最初手里的信息是文件名,最终得到的是文件内容;操作系统访问文件时首先读取硬盘内容管理表,从中找到目标文件的扇区级别的信息,然后利用该信息去查询真正存储内容的区域,最后得到我们要的文件内容。

(3)硬盘内容管理表中以文件为单位记录了各个文件的各种信息,每个文件都有一个信息列表(即inode,i节点,其实质是一个结构体,该结构体中有很多元素,每个元素记录了该文件的一些信息,包括文件的i节点、文件的属性信息、文件在硬盘上对应的扇区号、块号...);硬盘资源管理是以文件为单位进行的,每个文件对应一个inode,每个inode对应一个数字编号,每个数字编号对应一个结构体,结构体中记录了该文件的各种信息(但不包括文件名)。

(4)联系平时实践,大家格式化硬盘(U盘)时发现有快速格式化和底层格式化两个选项;快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢;快速格式化就是只删除了U盘中的硬盘内容管理表(即inode),真正存储的内容没有动,这种格式化的内容是有可能被找回的。

3.5.4 动态文件及操作过程

(1) 一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程,这个被打开的文件就称为动态文件。

(2)内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述表中每一项都是一个指针,指向一个用于描述打开的文件的file结构体,file结构体中描述了文件的打开模式、读写位置等重要信息,当进程打开一个文件时,内核就会创建一个新的file结构体。

(3)file结构体中包含一个指针,指向dentry结构体。dentry结构体代表一个独立的文件路径。如果一个文件路径被打开多次,那么会建立多个file结构体,但它们都指向同一个dentry结构体。

(4)dentry结构体中又包含一个指向inode结构体的指针。inode结构体代表一个独立文件。包含了最终对文件进行操作所需的所有信息,如文件系统类型、文件的操作方法、文件的权限、访问日期等,也就是说inode结构体保存着从磁盘inode读上来的信息。

(5)打开文件后,进程得到的文件描述符实质上就是文件描述符表的下标,内核根据这个下标值去访问相应的文件结构体,从而实现对文件的操作。

3.6 lseek函数及应用

3.6.1 lseek函数介绍

系统会记录每个打开文件的位置偏移量,读写操作后,同样会记住操作后的位置偏移量,位置偏

移量用于指示read或write操作时文件的起始位置,以相对于文件头部(即0开始)的位置偏

移量进行表示。lseek()函数用于在指定的文件描述符中将文件指针指定到相应的位置。它只能用在可定位(可随机访问)文件操作中。管道、套接字和大部分字符设备是不可定位的,所以在这些文件的操作中无法使用lseek()调用。

lseek()函数语法要点

|------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 头文件 | #include<unistd.h> #include <sys/types.h> |
| 函数原型 | off_t lseek(int fd,off_t offset,int whence) |
| 函数传参 | fd:文件描述符 offset:偏移量,每一读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移) whence:定义偏移量对应的参考值 SEEK_SET:当前位置为文件的开头,新位置为偏移量的大小 SEEK_CUR:当前位置为文件指针的位置,新位置为当前位置加上偏移量 SEEK_END:当前位置为文件的结尾,新位置为文件的大小加上偏移量的大小 |
| 返回值 | 成功:从文件头部开始算的位置偏移量 -1:出错 |

(1)当我们要对一个文件进行读写时,首先需打开该文件,则我们读写的所有文件都是动态文件;动态文件在内存中就是以文件流的形式存在的。

(2)文件流很长,里面有很多个字符,我们需要确定当前正在操作的是哪个位置;GUI模式下的软件用光标来标识当前正在操作文件流的哪个位置;在动态文件中则通过文件位置指针来标识当前正在操作文件流具体位置;文件位置指针不能被直接访问,linux系统使用lseek函数来访问该文件位置指针。

(3)当我们打开一个空文件时,默认情况下文件指针指向文件流的起始位置,则这时候去读写该文件时就是从文件流的起始位置开始的;write和read函数本身自带移动文件位置指针的功能,所以当我读写了n个字节后,文件指针会自动向后移动n位;如果需要人为的随意更改文件位置指针,那就只能通过lseek函数了。

(4)read和write函数都是从当前文件指针处开始操作的,所以当我们用lseek显式的将文件位置指针移动后,那么再去read/write时就是从移动过后的位置开始的。所以当我们操作一个空文件时先write写了12字节,然后read时是空的,但是此时我们打开文件后发现12字节确实写进来了。

3.6.2 计算文件长度和构建空洞文件

(1)linux中并没有一个函数可以直接返回一个文件的长度,但是我们做项目时经常会需要知道一个文件的长度,我们自己利用lseek来写一个函数得到文件长度即可。

(2)空洞文件即该文件中有一段是空的;普通文件中间是不能有空的,因为我们write时文件指针是依次从前到后去移动的,我们不可能绕过前面直接到后面;我们打开某个文件后,用lseek往后跳过一段,再write写入一段,则会构成一个空洞文件。

(3)空洞文件方法对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果从头开始依次构建时间很长,有一种思路就是将文件分为多段,然后多线程来操作,每个线程负责其中一段的写入。

3.7 文件复制描述符及实现文件共享

3.7.1 dup、dup2函数介绍

(1)dup函数对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,则返回的fd就是4);dup函数不能指定复制后得到的fd的数值,而是由操作系统内部自动分配的,分配的原则遵守fd分配的原则;dup2和dup的作用相同,都是复制一个新的文件描述符,但dup2允许用户指定新的文件描述符的数字。

(2)dup、dup2返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件,即构成了文件共享(fd和oldfd均指向同一个文件表,即两个文件指针相同);dup返回的fd和原来的oldfd同时向一个文件读/写时,结果是接续读/写。

(3)我们知道0、1、2这三个fd被标准输入、标准输出、标准错误通道占用,而且我们可以关闭这三个fd,则我们可使用close和dup配合进行文件的重定位;首先通过close(1)关闭标准输出(关闭后printf输出到标准输出的内容就看不到了),然后使用dup重新复制得到1这个fd(即把oldfd对应的文件和1这个标准输出通道给绑定起来了),即实现了标准输出的重定位。

重定位命令>的实现原理即利用open+close+dup,open打开一个文件test.txt,然后close关闭stdout,然后dup将1和test.txt文件关联起来即可。

3.7.2 重复打开同一文件读写

(1)一个进程中两次打开同一个文件,然后分别读写,一种情况是fd1和fd2分别读写,另一种

情况是接续读写;经过实验验证,证明了结果是fd1和fd2分别读写。

(2)分别读写说明我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针(fd->文件表指针->文件表->文件指针)是不同的两个独立的指针;文件指针是包含在文件表中的,所以可以看出linux系统的进程中不同fd对应的是不同的独立的文件表。

(2)正常情况下我们有时候需要分别写,有时候又需要接续写,所以这两种本身是没有好坏之分的,关键看用户需求;有时候我们希望接续写而不是分别写,办法就是在open时加O_APPEND标志即可。

3.7.3 O_APPEND实现原理及原子操作

(1)分别写的内部原理就是两个fd拥有不同的文件指针,并且彼此只考虑自己的位移;但是O_APPEND标志可以让write和read函数内部多做一件事情,即移动自己的文件指针的同时也移动别人的文件指针(即fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动)。

(2)原子操作的含义即整个操作一旦开始是不会被打断的,必须直到操作结束其它代码才能得以调度运行,这就叫原子操作;每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行;O_APPEND对文件指针的影响,对文件的读写是原子的。

3.7.4 文件共享及实现方式

(1)文件共享即同一个文件(即同一个inode,同一个pathname)被多个独立的读写体(即多个文件描述符)同时(一个已打开尚未关闭的同时另一个去操作)操作;文件共享的意义有很多,譬如我们可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。

(2)文件共享的核心即制造多个文件描述符指向同一个文件;常见的有三种文件共享的情况,第一种是同一个进程中多次使用open打开同一个文件;第二种是在不同进程中去分别使用open打开同一个文件(两个fd在不同的进程中,则两个fd的数字可以相同也可以不同);第三种情况是linux系统提供了dup和dup2两个系统调用函数来让进程复制文件描述符;分析文件共享时的核心关注点在于确认是分别写/读还是接续写/读。

(3)当两个文件指针分别独立且互不关联->分别写/读;当两个文件指针分别独立且相互关联/两个文件指针相同->接续写/读。

3.7.5 fcntl的原型和作用

(1)fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参;第1个参数是fd表示要操作哪个文件,第2个参数是cmd表示要进行哪个命令操作,变参是用来传递参数的,要配合cmd来使用。

(2)cmd的格式类似于F_XXX,不同的cmd具有不同的功能,学习时没必要去把所有的cmd的含义都弄清楚,只需要弄明白一个作为案例,搞清楚它怎么看怎么用就行了,其它的是类似的,其它的当我们在使用中碰到了一个fcntl的不认识的cmd时再去查man手册即可。

(3)F_DUPFD该cmd的作用是复制文件描述符(类似dup和dup2);该命令的功能是从可用的fd数字列表中找1个>=arg的数字作为oldfd的1个复制的fd;dup2返回的是我们指定的那个newfd否则就会出错,但F_DUPFD命令返回的是>=arg的最小的那1个数字。

3.8 文件类型和文件属性获取

3.8.1 普通文件

(1)普通文件(- regular file)包括文本文件+二进制文件。

(2)文本文件即文件中的内容是由文本构成的,文本即经过某种编码的字符(譬如ASCII码字符);所有文件的内容本质上都是数字,而文本文件中的数字本身应理解为该数字所对应的编码字符(譬如ASCII码对应的字符);常见的.c文件和.h文件和.txt文件等都是文本文件;文本文件的好处是可以被人轻松读懂和编辑,则文本文件天生就是为人类发明的。

(3)二进制文件即文件中存储的内容本质上也是数字,但这些数字并非字符对应的数字编码,而是真正的数字;常见的可执行程序文件(gcc编译生成的a.out;arm-linux-gcc编译生成的.bin)都是二进制文件。

(4)从本质上来看(刨除文件属性和内容的理解)文本文件和二进制文件并没有任何区别,都是1个文件里面存放了数字;区别是理解方式不同,若把这些数字就当作数字处理则就是二进制文件,若把这些数字按照某种编码格式去解码成文本字符,则就是文本文件。

(5)在linux系统层面是不区分文本文件和二进制文件的,我们无法从文件本身准确知道该文件属于哪种;我们只能事先就知道该文件的类型然后使用该种文件类型的用法去使用它;有时候也会用一些后缀名来人为的标记文件的类型。

(6)使用文本文件时,常规做法是使用文本文件编辑器(vim、gedit、notepad++、SourceInsight)去打开并编辑它,编辑器会read读出文件二进制数字内容,然后按照编码格式去解码将其还原成字符展现给我们;如果使用文本文件编辑器去打开某个二进制文件,则编辑器会以为该二进制文件还是文本文件然后试图去将其解码成文字,但解码过程很多数字并不对应有意义的字符所以成了乱码;使用二进制阅读工具去读取文本文件,则呈现的是文本文件中字符所对应的二进制的数字编码。

3.8.2 目录文件和设备文件

(1)目录文件(directory)就是文件夹,文件夹在linux中是一种特殊文件;用vi打开某个文件夹,则里面存的内容包括该目录文件的路径+目录文件夹里面的文件列表;目录文件比较特殊,本身并不适合用普通的方式来读写,linux中使用特殊的函数来专门读写目录文件。

(2)设备文件包括字符设备文件(character)+ 块设备文件(block);设备文件对应的是硬件设备,即该文件虽然在文件系统中存在,但并不是真正存在于硬盘上的某个文件,而是文件系统虚拟制造出来的(譬如/dev /sys /proc等);这些虚拟的文件系统中的文件大多数不能或者说不用直接读写的,而是用特殊的函数产生或者使用的。

(3)管道文件(pipe)+套接字文件(socket)后续用到的时候会详细分析。

3.8.3 链接文件

(1) 用ln命令在文件之间创建链接。这种操作实际上是给系统中已有的某个文件指定另外一个

可用于访问它的名称。对于这个新的文件名,我们可以为之指定不同的访问权限,以控制对信息的共享和安全性的问题。如果链接指向目录,用户就可以利用该链接直接进入被链接的目录而不用打一大堆的路径名。而且,即使我们删除这个链接,也不会破坏原来的目录。

(2) 使用方式 :ln [option] source_file dist_file 。链接有两种,一种被称为硬链接(Hard Link),另一种被称为软链接即符号链接(Symbolic Link)。建立硬链接时,链接文件和被链接文件必须位于同一个文件系统中,并且不能建立指向目录的硬链接。而对符号链接,则不存在这个问题。默认情况下,ln产生硬链接。如果给ln命令加上- s选项,则建立符号链接。譬如:ln -s abc cde 建立abc 的软连接,ln abc cde 建立abc的硬连接。

(3) 软链接与硬链接的区别:通俗来讲,硬链接可认为是一个文件拥有两个文件名;而软链接则是系统新建一个链接文件,此文件指向其所要指的文件。硬链接,只能应用于文件,而不能应用于目录,而且不能跨文件系统(即分区);符号链接,可以应用于文件,而且可以应用于目录和可跨文件系统(分区),其作用类似于Windows系统中的快捷方式。

(4) 底层的区别:当我们创建了一个文件的硬链接时,硬链接会使用和文件相同的inode号,此时我们发现,原来的文件的inode连接数由最初的1变为了2,实际上硬链接和文件使用了相同的inode,只不过是inode连接数增加了,删除文件不会影响硬链接,硬链接的inode数会从2变为1;而在创建文件的软链接时,软链接会使用一个新的inode,所以软链接的inode号和文件的inode号不同,软链接的inode里存放着指向文件的路径,删除文件,软链接也无法使用了,因为文件的路径不存在了;当我们再次创建这个文件时(文件名与之前的相同),软链接又会重新指向这个文件(inode号与之前的不同了),而硬链接不会受其影响。

(5) linux中使用专用的函数来创建硬链接和软链接文件。系统调用函数link()和 unlink(),用于创建和删除硬链接,系统调用函数symlink()和 readlink(),创建和读取软链接。

3.8.4 stat和fstat及lstat函数

stat()函数用来获取文件的属性信息,该信息将会写入一个buf 结构中。

stat()函数语法要点

|------|--------------------------------------------------------------------------------------------------------------------------------------------|
| 头文件 | #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> |
| 函数原型 | int stat(const char *path, struct stat *buf); int fstat(int fd, struct stat *buf); int lstat(const char *pathname, struct stat *buf); |
| 函数传参 | path:需要获取文件属性所对应的文件 buf:如果成功获取将会存放到结构体 |
| 返回值 | 0:成功 -1:出错 |

注:struct stat {

dev_t st_dev; /* ID of device containing file */

ino_t st_ino; /* inode number */

mode_t st_mode; /* protection */

nlink_t st_nlink; /* number of hard links */

uid_t st_uid; /* user ID of owner */

gid_t st_gid; /* group ID of owner */

dev_t st_rdev; /* device ID (if special file) */

off_t st_size; /* total size, in bytes */文件大小

blksize_t st_blksize; /* blocksize for filesystem I/O */

blkcnt_t st_blocks; /* number of 512B blocks allocated */

time_t st_atime; /* time of last access */

time_t st_mtime; /* time of last modification */

time_t st_ctime; /* time of last status change */

};

  1. 每个文件中都附带了该文件的一些属性信息(属性信息是存在于文件本身中的,其只能被专用的函数打开看到);文件属性信息查看函数有stat、fstat、lstat,其作用一样,参数不同,细节略有不同;linux命令行下可以使用stat命令去查看文件属性信息,实际上stat命令内部

就是使用了stat系统调用来实现的(见图1);struct stat是内核定义的一个结构体,在sys/stat.h

中声明,该结构体中的所有元素加起来就是文件属性信息。

(2)stat这个函数的作用就是让内核将我们要查找的目的文件的属性信息结构体的值放入我们传递给stat函数的buf中,当stat这个函数调用从内核返回时buf中被填充了该文件的正确的属

性信息,然后我们通过查看buf这种结构体变量的元素即可得知该文件的各种属性了。

(3)stat是从文件名出发得到文件属性信息结构体,而fstat是从一个已打开的文件fd出发得到文件的属性信息;则若文件没有打开需要读取文件信息即使用stat,若文件已经被打开需要读取文件信息即使用fstat效率会更高;stat是从磁盘去读取静态文件,而fstat是从内存读取动态文件;对于符号链接文件,stat和fstat查阅的是符号链接文件指向的文件的属性,而lstat查阅的是符号链接文件本身的属性。

(4)通过stat判断文件具体类型(譬如-、d、l),在struct stat结构体的mode_t st_mode元素中记录了文件类型权限等信息,该元素中的每个bit位代表不同的含义,但这些位定义不容易记住,则linux系统事先定义多个宏来进行相应操作(譬如S_ISREG宏返回值是1表示该文件是普通文件,若该文件不是普通文件则返回0)。

(5)独立判断文件权限设置,st_mode记录了文件权限信息,linux并没有给文件权限测试提供宏操作,而只是提供了位掩码,则我们只能用位掩码来判断文件是否具有相应权限。

3.8.5 文件的权限管理

(1)st_mode本质上是1个32位的数(类型unsinged int),该数中的每个位表示1个含义;文件类型和文件权限都记录在st_mode中,我们使用专门的掩码去取出相应的位即可得知相应的信息。

(2)ls-l打印出的权限列表中共9位分为3组;第1组表示文件的属主(owner、user)对该文件的可读、可写、可执行权限;第2组表示文件的属主所在的组(group)对该文件的权限;第3组3个位表示其它用户(others)对该文件的权限;属主即该文件属于谁,一般来说是创建该文件的用户;但某个文件创建之后可用chown命令去修改该文件的属主,也可用chgrp命令去修改该文件所在的组。

(3)文件操作时的权限检查规则即当某个程序a.out被执行,a.out试图去操作文件test.txt,判定a.out是否具有对test.txt的某种操作权限;首先test.txt具有9个权限位,规定了3种人(user、group、others)对该文件的操作权限;我们判定test.txt是否能被a.out来操作,关键先搞清楚a.out被谁执行,即当前程序(进程)是哪个用户的进程。

(4)文件权限管控其实蛮复杂,我们很难轻易的确定当前用户对某个文件是否具有某种权限;软件应在操作某个文件之前先判断当前用户是否有权限做相应操作,若有相应权限即可进行操作,若没有相应权限则提供错误信息给用户;access函数可以测试得到当前执行程序的那个用户在当前环境下对目标文件是否具有某种操作权限。

(5)chmod/fchmod与权限修改,chmod是一个linux命令,用来修改文件的各种权限属性,chmod命令只有root用户才有权利去执行修改,chmod命令其实内部是用linux的一个叫chmod的函数实现的。

(6)chown/fchown/lchown与属主修改,linux中有个chown命令来修改文件属主,需以root身份运行(譬如chown root test.txt),chown命令是用chown 函数实现的;linux中有个chgrp命令来修改文件属主所在的组(譬如chgrp root test.txt)。

(7)umask与文件权限掩码,文件权限掩码是linux系统中维护的一个全局设置,umask的作用是用来设定我们系统中新创建的文件的默认权限的(譬如umask 0000即修改umask值;umask即查询umask值),umask命令就是用umask 函数实现的。

3.9 标准IO和目录文件操作

3.9.1 标准IO及常用函数

(1)标准IO是C库函数,而文件IO是linux系统的API;C语言库函数是由系统API封装而来的,但库函数因为多了1层封装,则比系统API要更加好用一些;系统API在不同的操作系统之间是不能通用的,但C库函数在不同操作系统中几乎是一样的,则C库函数具有可移植性而API不具有可移植性;性能上和易用性上看,C库函数一般要好一些,譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。

(2)常见的标准IO库函数有fopen、fclose、fwrite、fread、ffulsh(清缓存)、fseek。

fopen()函数语法要点

|------|-------------------------------------------------------------------------------------------------------------|
| 头文件 | #include <stdio.h> |
| 函数原型 | FILE *fopen(const char *path, const char *mode); |
| 函数传参 | path: 需要打开的路径名 mode:打开文件的权限 "r" 只读 "r+" 读写 "w" 只写,带创建 :存在清空 "w+" 读写, 带创建 :存在清空 "a" 只写并追加 带创建 "a+" 读写并追加 带创建 |
| 返回值 | 成功:返回文件指针 失败:返回 NULL |

fread()函数语法要点

|------|-------------------------------------------------------------------------------------------|
| 头文件 | #include <stdio.h> |
| 函数原型 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) |
| 函数传参 | 参数一:存放数据的缓存地址(ptr) 参数二:数据类型大小 / 数据块大小(size) 参数三:多少个这样的数据(块) (nmemb) 参数四:需要读取的文件指针(stream) |
| 返回值 | 成功:读取到的字节块数量(注意:不是字节数) 失败:返回 0 或者 -1 |

fwrite()函数语法要点

|------|---------------------------------------------------------------------------|
| 头文件 | #include <stdio.h> |
| 函数原型 | size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream); |
| 函数传参 | 参数一:需要写入的数据的缓存地址 参数二:写入的数据类型大小 / 数据块大小 参数三:写入数据块的数量 参数四:需要写入的文件指针 |
| 返回值 | 成功:返回写入数据块的个数(注意:不是字节数!) 失败: 返回 -1 |

fseek()函数用于设置文件读写位置偏移量

光标移动fseek()函数语法要点

|------|----------------------------------------------------------------------------------------------------------|
| 头文件 | #include <stdio.h> |
| 函数原型 | int fseek(FILE *stream, long offset, int whence); |
| 函数传参 | 参数一:文件指针(stream) 参数二:光标偏移量(offset) 参数三:光标位置来源(whence) SEEK_SET 文件头开始 SEEK_CUR 光标的当前位置开始 SEEK_END 从文件末尾开始 |
| 返回值 | 成功:返回写入数据块的个数(注意:不是字节数!) 失败: 返回 -1 |

fclose()函数语法要点

|------|----------------------------|
| 头文件 | #include<stdio.h> |
| 函数原型 | int fclose(FILE *stream); |
| 函数传参 | 参数一:文件指针(stream) |
| 返回值 | 成功:返回 0 失败:返回 EOF |

3.9.2 目录文件及操作

在Linux中,目录也是文件,我们在使用linux系统时,一般使用mkdir命令创建新的目录,跟命令相对应的也有创建目录、删除目录的函数。

  1. 用mkdir函数创建目录,用rmdir函数删除目录。注意:当我们在创建普通文件时,一般指定文件的mode为读、写权限,但对于目录,我们至少要设置1个执行权限,以允许访问该目录中的文件名。用rmdir函数删除一个空目录。

创建目录mkdir()函数语法要点

|------|---------------------------------------------------------|
| 头文件 | #include <sys/stat.h> #include <sys/types.h> |
| 函数原型 | int mkdir(const char *pathname, mode_t mode); //创建一个目录 |
| 函数传参 | 参数一:pathname:路径名 参数二:mode:目录权限 |
| 返回值 | 成功:返回 0 失败:返回 -1 |

mode方式:

S_IRWXU 00700权限,代表该文件所有者拥有读,写和执行操作的权限
S_IRUSR(S_IREAD) 00400权限,代表该文件所有者拥有可读的权限
S_IWUSR(S_IWRITE) 00200权限,代表该文件所有者拥有可写的权限
S_IXUSR(S_IEXEC) 00100权限,代表该文件所有者拥有执行的权限
S_IRWXG 00070权限,代表该文件用户组拥有读,写和执行操作的权限
S_IRGRP 00040权限,代表该文件用户组拥有可读的权限
S_IWGRP 00020权限,代表该文件用户组拥有可写的权限
S_IXGRP 00010权限,代表该文件用户组拥有执行的权限
S_IRWXO 00007权限,代表其他用户拥有读,写和执行操作的权限
S_IROTH 00004权限,代表其他用户拥有可读的权限
S_IWOTH 00002权限,代表其他用户拥有可写的权限
S_IXOTH 00001权限,代表其他用户拥有执行的权限

删除目录rmdir()函数语法要点

|------|------------------------------------------|
| 头文件 | #include <unistd.h> |
| 函数原型 | int rmdir(const char *pathname); //删除目录 |
| 函数传参 | 参数:pathname:目录名 |
| 返回值 | 成功:返回 0 失败:返回 -1 |

(2) 对某个目录具有访问权限的任一用户都可读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录。一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,他们并不代表能否写目录本身。对于普通文件来讲,要想读写一个文件,必须先用open函数得到文件描述符;类似的对于目录来讲,也必须先用opendir函数得到目录的DIR指针,之后,利用该指针作为readdir等函数的参数来对目录进行其他操作。

opendir()函数语法要点

|------|------------------------------------------------|
| 头文件 | #include <sys/types.h> #include <dirent.h> |
| 函数原型 | DIR *opendir(const char *name); //打开一个目录 |
| 函数传参 | 参数:name:目录名 |
| 返回值 | 成功:返回指向该目录结构体指针 失败: 返回 NULL |

closedir()函数语法要点

|------|------------------------------------------------|
| 头文件 | #include <sys/types.h> #include <dirent.h> |
| 函数原型 | int closedir(DIR *dirp); //关闭目录 |
| 函数传参 | 参数:dirp:opendir返回的指针 |
| 返回值 | 成功:返回0 失败: 返回 -1 |

readdir()函数语法要点

|------|---------------------------------------------|
| 头文件 | #include <dirent.h> |
| 函数原型 | struct dirent *readdir(DIR *dirp); //读取目录 |
| 函数传参 | 参数:dirp:opendir的返回值 |
| 返回值 | 成功:返回指向该目录结构体指针 失败: 返回 null |

注:struct dirent的定义如下:

struct dirent {

ino_t d_ino; /* Inode number索引节点号*/

ff_t d_off; /* offset to this dirent 在目录文件中的偏移*/

unsigned short d_reclen; /* Length of this record文件名长*/

unsigned char d_type; /* Type of file; not supported by all filesystem types文件类型*/

char d_name[256]; /* Null-terminated filename文件名,最长255字符*/

};

(3)opendir打开某个目录后得到一个DIR类型的指针给readdir函数使用;readdir函数调用一次就会返回一个struct dirent类型的指针,该指针指向的结构体变量里面记录了一个目录项(即目录中的一个子文件);readdir函数调用一次只能读出一个目录项,要想读出目录中所有的目录项必须多次调用readdir函数;readdir函数内部会记住哪个目录项已经被读过了哪个还没读,则多次调用后不会重复返回已经返回过的目录项;当readdir函数返回NULL时就表示目录中所有的目录项已经读完了。

(4)readdir函数和我们前面接触的某些函数是不同的,首先readdir函数直接返回了一个结构体变量指针,因为readdir内部申请了内存并且给我们返回了地址,多次调用readdir其实readir内部并不会重复申请内存而是使用第一次调用readdir时分配的那个内存,该设计方法是readdir不可重入的关键;readdir在多次调用时是有关联的,该关联也标明readdir函数是不可重入的。

相关推荐
摸鱼也很难1 分钟前
Docker 镜像加速和配置的分享 && 云服务器搭建beef-xss
运维·docker·容器
watermelonoops3 分钟前
Deepin和Windows传文件(Xftp,WinSCP)
linux·ssh·deepin·winscp·xftp
woshilys35 分钟前
sql server 查询对象的修改时间
运维·数据库·sqlserver
疯狂飙车的蜗牛1 小时前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
恩爸编程2 小时前
探索 Nginx:Web 世界的幕后英雄
运维·nginx·nginx反向代理·nginx是什么·nginx静态资源服务器·nginx服务器·nginx解决哪些问题
Michaelwubo3 小时前
Docker dockerfile镜像编码 centos7
运维·docker·容器
远游客07133 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<3 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟3 小时前
centos-stream9系统安装docker
linux·docker·centos
好像是个likun4 小时前
使用docker拉取镜像很慢或者总是超时的问题
运维·docker·容器