目标
- 了解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库;
- 内核空间:内核空间包括系统调用,内核,以及与平台架构相关的代码。
- 内核仅仅是操作系统的一部分,是真正与硬件交互的那部分软件,与硬件交互包括读写硬盘、读写网盘、读写内存以及任何连接到系统中的硬件。除了与硬件交互外,内核还负责分配资源,分配什么资源呢?所谓资源就是硬件,比如CPU时间、内存、IO等等,这些都是资源。内核的职责就是以进程的形式来分配CPU时间,以虚拟内存的形式来分配物理内存,以文件的形式来管理IO设备。内核是给人用的,为了与内核交互,发明了命令行以及图形界面GUI。除了给普通用户提供使用的接口之外,操作系统还需要给程序员提供编写程序的接口,通过系统调用,我们可以像使用普通函数那样向操作系统请求服务。
- linux内核的主要组件有:系统调用接口、进程管理、内存管理、虚拟文件系统、网络堆栈、设备驱动程序、硬件架构的相关代码。
Linux内核的任务:
- 从技术层面讲,内核是硬件与软件之间的一个中间层。作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
- 从应用程序的层面讲,应用程序与硬件没有联系,只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。
- 内核是一个资源管理程序。负责将可用的共享资源(CPU时间、磁盘空间、网络连接等)分配到各个系统进程。
- 内核就像一个库,提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。
3.1.3 linux文件系统
- 操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。
- 通常文件系统是用于存储和组织文件的一种机制,便于对文件进行方便的查找与访问。
- 文件系统是对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护和检索的系统。
- 它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。
(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。
说明:
- Block Group:ext2 文件系统会根据分区的大小划分为数个 Block Group。而每个 Block Group 都有着相同的结构组成。
- Super Block(超级块):记录整个文件系统的信息,包括block与inode的总量,已经使用的inode和block的数量,未使用的inode和block的数量,inode和block的大小,文件系统的挂载时间,最近一次的写入时间,最近一次的磁盘检验时间等。super block 的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT(Group Descriptor Table:块组描述符表):描述块组属性信息。
- Block Bitmap(块位图):Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
- inode Bitmap(inode位图):每个 bit 表示一个 inode 是否空闲可用。
- inode Table(i节点表):用来记录文件的权限(r、w、x),文件的所有者和属组,文件的大小,文件的状态改变时间(ctime),文件的最近一次读取时间(atime),文件的最后一次修改时间(mtime),文件的数据真正保存的block编号。每个文件需要占用一个inode。
- 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就是唯一的,目录项也是定的!
- 进程与超级块、文件、索引结点、目录项的关系
3.1.5 linux目录结构
(1)文件结构是指文件在存储设备中的组织方式。主要体现在对文件和目录的组织上,目录提供了一个管理文件的有效而方便的途径。
(2)linux使用树状目录结构,在安装系统时,安装程序已经为用户创建了文件系统和完整而固定的目录组成形式,并指定每个目录的作用和其中的文件类型(如下图所示)。该结构的最上层是根目录,其他所有目录都是从根目录出发生成的。
(3)linux下一些主要目录的功能:
- /bin - 二进制可执行文件
/bin 目录存放系统启动和修复所需的最基本的二进制可执行文件。这些文件通常对于系统的基本操作是必不可少的,因此/bin 目录下的命令可以在系统启动时使用。
示例:/bin/ls 命令用于列出目录内容。
- /boot - 启动文件
/boot 目录包含了引导Linux系统所需的文件,包括启动菜单、内核文件和初始RAM磁盘镜像等。这些文件在系统引导时被加载。
示例:/boot/vmlinuz-5.4.0-1 是Linux内核文件。
- /dev - 外部设备
/dev 目录包含了Linux系统中的外部设备节点和特殊文件。访问这些设备等同于访问对应的硬件设备。
示例:/dev/sda 代表系统的第一个硬盘。
- /etc - 配置文件
/etc 目录包含系统的配置文件,用户和管理员可以通过修改这些文件来配置系统的行为。
示例:/etc/passwd 文件存储用户账户信息。
- /home - 用户主目录的父目录
/home目录是普通用户主目录的父目录,每个用户都可以在这个目录下拥有自己的主目录。
示例:/home/john 是用户John的主目录。
- /lib - 系统库文件
/lib 目录存放系统必要的库文件,这些库文件包含为系统程序提供API的代码。
示例:/lib/x86_64-linux-gnu/libc.so.6 是GNU C库。
- /media - 可移动设备挂载点
/media 目录是可移动设备如U盘、光驱等的挂载点,当可移动设备被挂载时,它们会出现在这个目录下。
示例:/media/usb0 可以用于挂载U盘。
- /mnt - 临时文件系统挂载点
/mnt 目录是系统提供的一个临时挂载文件系统的安装点,系统管理员可以手动将文件系统挂载在此目录下。
示例:可以将一个共享目录临时挂载在 /mnt/share 上。
- /opt - 可选插件软件包安装目录
/opt 目录存放着可选的软件包和附加的系统软件,这些软件包可以安装在 /opt 目录下。
示例:/opt/oracle/ 下可以安装Oracle数据库软件。
- /root - 超级用户主目录
/root 目录是系统超级用户(root)的主目录。root用户主要用于系统管理,其家目录为 /root。
示例:root用户的bash配置文件在 /root/.bashrc。
- /sbin - 系统管理员工具
/sbin 目录存放系统管理员使用的系统管理程序,只有root用户才能访问这些程序。
示例:/sbin/fdisk 命令用于分区管理。
- /srv - 服务数据目录
/srv 目录存放一些服务启动后需要提取的数据,可以根据服务的系统名划分子目录。
示例:/srv/cvs 对应CVS服务的数据目录。
- /tmp - 临时文件夹
/tmp目录用于存放各种临时文件,是公共的临时文件存储点。重要数据不应该存放在此目录。
示例:许多程序会在 /tmp 下创建临时工作文件。
- /usr - 用户应用和文件
/usr 目录存储用户应用程序和文件,主要包括可共享的可执行文件、库、文档等。
示例:/usr/bin 下存放各种应用程序。
- /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。
- 缓冲I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调用访问文件。(又称为标准IO)。
- 非缓冲I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。(又称为系统IO,底层IO)
注意,这里所说的“缓冲”,是指标准库内部实现的缓存。比方说,你可能见到过,很多程序遇到换行时才真正输出,而换行前的内容,其实就是被标准库暂时缓存了起来。
无论缓冲I/O还是非缓冲I/O,它们最终还是要经过系统调用来访问文件。我们知道,系统调用后,还会通过页缓存,来减少磁盘的I/O 操作。
(2)根据是否利用操作系统的页缓存,可以把文件I/O分为直接I/O与非直接I/O。
- 直接I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
- 非直接I/O 正好相反,文件读写时,先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。
想要实现直接I/O,需要你在系统调用中指定 O_DIRECT 标志。如果没有设置过,默认的是非直接I/O。
(3)根据应用程序是否阻塞自身运行,可以把文件I/O分为阻塞I/O和非阻塞I/O
- 阻塞I/O,是指应用程序执行I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
- 非阻塞I/O,是指应用程序执行I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
比方说,访问管道或者网络套接字时,设置 O_NONBLOCK 标志,就表示用非阻塞方式访问;而如果不做任何设置,默认的就是阻塞访问。
(4)根据是否等待响应结果,可以把文件I/O 分为同步I/O和异步I/O
- 同步I/O,是指应用程序执行I/O操作后,要一直等到整个I/O完成后,才能获得I/O响应。
- 异步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:出错 |
- 说明:
- 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()函数语法要点