Linux入门(五)-I/O编程

文件属性、目录及文件系统简介 

Linux系统屏蔽了所有硬件的区别,把所有设备抽象成文件,然后提供统一的接口供用户使用。对于Linux操作系统而言,一切皆文件,应用层开发过程中经常涉及对文件的访问,因此文件是Linux操作系统的重要组成部分。Linux系统中应用最为广泛的知识:文件的属性特点与文件目录的操作以及文件系统的概念。

文件属性

文件的类型

Linux系统中的大多数文件是普通文件或目录,但也有一些其它的文件类型,如下:

(1)普通文件(regular file):是最常见的文件类型,其数据形式可以是文本或二进制数据。

(2)目录文件(directory file):包含其他类型文件的名字以及指向与这些文件有关的信息的指针。对一个目录文件具有读许可权的任一进程都可以读该目录文件的内容,但只有内核才有写目录文件的权限。

(3)字符设备文件(character special file):这种文件被视为对字符设备的一种抽象,它代表的是应用程序对硬件设备的访问接口,Linux应用程序通过对该文件进行操作来实现对设备的访问。

(4)块设备文件(block special file):类似于字符设备文件,只是它用于磁盘设备。Linux系统中的所有设备或者抽象为字符设备文件,或者为块设备文件。

(5)管道文件(pipe):这种文件用于进程间的通信,有时也将其称为命名管道。

(6)套接字文件(socket):这种文件用于进程间的网络通信。也可用于在一台宿主机上的进程之间的本地通信。

(7)符号连接(symbolic link):这种文件指向另一个文件。

字符设备与块设备文件一般在底层驱动时会使用到。套接字文件常用于实现进程间的网络通信。

文件系统

文件系统概念概述

文件系统是存放运行、维护系统所必须的各种工具软件、库文件、脚本、配置文件和其他特殊文件的地方,也可以安装各种软件包。简单地说,文件系统就是用于组织和管理计算机存储设备上大量文件的一种机制。在任何一个操作系统中,文件系统是其最重要的组件,也是整个操作系统中相对抽象、较难理解的部分。

文件系统的功能包括:管理和调度文件的存储空间,提供文件的逻辑结构、物理结构和存储方法;实现文件从标识到实际地址的映射,实现文件的控制操作和存取操作,实现文件信息的共享,并提供可靠的文件保密和保护措施。

比如生活中常用的U盘作为一种存储设备。在初始状态下,如果不设置分区格式化,那么U盘是无法直接存储各种资源(图片、文档、音视频资源等)的。其原因是,设备缺少了对文件进行管理的方式,没有规则不成方圆,文件系统就是用来提供文件管理方式的

以上只对文件系统概念概述。文件系统作为整个操作系统的重要组成部分,其格式繁多,每一种格式的文件系统对文件的管理方式细节不尽相同。

文件系统的类型

Linux是一种兼容性很高的操作系统,支持的文件系统的格式很多,大体可以分为几类。

1. 磁盘文件系统

磁盘文件系统指本地主机中实际可以访问到文件系统,或者说可以驻留在磁盘上的文件系统,包括硬盘、CD-ROM、DVD、USB存储器、磁盘阵列等。属于物理文件系统。其常见格式有:EXT3、EXT4、VFAT、FAT、FAT16、FAT32、NTFS等;其中,NTFS是目前Windows的主要文件系统格式。

2. 网络文件系统

网络文件系统指可以远程访问的文件系统,这种文件系统在服务器端仍是本地的磁盘文件系统,客户机通过网络远程访问数据。其常见的格式有NFS、Samda等。属于物理文件系统。

3. 虚拟文件系统

虚拟文件系统指不驻留在磁盘上的文件系统,也是比较抽象、难以理解的部分。虚拟文件系统(Virtual File System, VFS)是物理文件系统与服务应用之间的一个接口层,它对Linux的每个文件系统的所有细节进行抽象,在不同的文件系统格式上面做了一个抽象层,使得不同的文件系统在Linux内核以及系统中运行的其他进程看来都是相同的。这样我们对文件还是对设备的访问都变为了对抽象层的访问,此抽象层就叫做虚拟文件系统。

虚拟文件系统包括了几个抽象对象,这些抽象的对象就封装了底层的读写细节。其实就是使用了C语言的多态实现具体文件系统的读写接口。

上述各个文件系统,格式都不尽相同,功能差异也比较大,但我们都可以使用mount命令把他们挂载在某些目录下面。然后就可以用ls命令查看各个文件系统下面的目录和文件。ls命令查看目录和文件的时候,会发现各个不同系统下面的目录和文件的格式都是一样的。我们对这些目录和文件进行读写操作的时候方法都是一样的。

文件系统的结构

微软Windows操作系统的用户比较习惯将硬盘分区,并使用C、D、E、F等符号标识。存取文件时要清楚存放在哪个磁盘的哪个目录下。

而Linux的文件组织模式犹如一棵倒置的树,这与Windows文件系统有很大差别。所有存储设备作为这棵树的子目录。存取文件时只要确定目录就可以了,无须考虑物理存储位置。

Windows和Linux的文件系统的明显区别,就是分区和目录的关系:

  • 在Windows中,分区是磁盘存储空间的划分单元,具有独立的驱动器号,目录结构是分区中的文件组织方式。
  • 在Linux中,目录结构是整个文件系统的组织方式,而分区则是挂载到目录中的特定存储空间。

因为在Linux中,将所有硬件视为文件来处理,包括硬盘分区、CD-ROM、软驱以及其他USB移动设备等。为了能够按照统一的方式访问文件资源,Linux为每种硬件设备提供了相应的设备文件。当Linux系统可以访问到硬件时,就将硬件上的文件系统挂载到Linux目录树中的一个子目录中。例如,用户插入U盘,Linux系统自动识别之后,将其挂载到“/media/DISK_IMG”目录下,用户可以在该目录下访问到U盘,而不像Windows系统将USB存储器作为新的驱动器,如“F:”(一个新盘)

Linux文件系统和Windows文件系统的比较:

Linux文件I/O

Linux文件I/O分为系统IO和标准IO,常用于系统编程。

文件I/O的基本概念

标准IO与文件IO的区别:

系统I/O:是指直接使用操作系统提供的系统调用(例如open、read、write、close)来进行文件的读写操作。系统I/O通过文件描述符 fd 来操作文件。这种方式能够提供更底层、更直接的文件访问能力,适用于需要对文件进行低级别控制、高性能读写的场景。系统I/O操作的单位是字节。系统I/O函数通常被称为非缓冲I/O,因为它们直接对文件进行读写,没有使用额外的缓冲区。

标准I/O:是建立在系统I/O的基础上,进行间接系统调用,通过使用C标准库提供的函数(例如fopen、fread、fwrite、fclose)来进行文件的读写操作。标准I/O提供了一层缓冲,将文件数据读入缓冲区后再进行操作,以提高效率。标准I/O函数通常以文件流FILE *的形式操作,可以使用高级输入输出函数进行格式化的读写操作。标准I/O函数相对于系统I/O函数,提供了更高级、更方便的文件操作接口。

在使用文件I/O时,可以根据具体需求选择使用系统I/O或标准I/O进行文件的读写操作,以达到最合适的性能和便利性。

  

从定义来看:

系统IO:系统I/O称为不带缓存的IO。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O—操作系统提供的基本IO服务,与os绑定,特定于Linux或unix平台。

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

标准的I/O提供了三种类型的缓存:

  • 全缓存:当填满标准I/O缓存后才进行实际的I/O操作(有fread,fwrite)
  • 行缓存:当输入或输出中遇到新行符时(”\n“)或写满,标准I/O库执行I/O操作(有fgets,fputs,gets,puts,printf,scanf,fprintf,sprintf)
  • 不带缓存:只要用户调用这个函数,就会进行IO操作,stderr(标准错误)。

综合来看:

通过系统I/O读写文件时,每次操作都会执行相关系统调用。这样的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在系统I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。

学习和使用这些API可用系统自带的 man查看手册。

比如:查看系统IO可以用man 2 open, 查看标准I/O可以用man 3 fopen

关于linux中man 1 2 3 … 的区别 :

man 1:Standard commands (标准命令)
man 2:System calls (系统调用)
man 3:Library functions (库函数)
man 4:Special devices (设备说明)
man 5:File formats (文件格式)
man 6:Games and toys (游戏和娱乐)
man 7:Miscellaneous (杂项)
man 8:Administrative Commands (管理员命令)
man 9: 其他(Linux特定的), 用来存放内核例行程序的文档。

系统I/O

常用的系统I/O接口有:

  • open()
  • read()
  • write()
  • close()
  • lseek()

使用系统I/O需包含头文件:#inlcude <unistd.h>

1. open()

简要来说是打开或者创建一个文件。详细介绍可使用命令man 2 open查看使用手册。

使用open()函数需要包含如下头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

函数原型:    
int open(const char *pathname, int flags);    #指定的文件存在时
int open(const char *pathname, int flags, mode_t mode);   #指定的文件不存在时

参数:
pathname-文件路径;flags-打开文件的方式;mode-创建文件的权限

返回值:
返回值类型:int
调用成功时返回一个文件描述符fd;调用失败时返回-1,并设置errno

第二个参数flags可以指定为以下宏:

主模式(三选一):

  • O_RDONLY : 只读打开
  • O_WRONLY : 只写打开
  • O_RDWR : 读写打开

副模式(可多选):

  • O_APPEND:以追加模式打开,把此文件的读写位置设置到文件的尾巴。
  • O_CREAT:若文件不存在,则创建该文件,同时需要指定第三个参数
  • O_DIRECT:直接IO模式,此模式的文件数据不需要经过页缓存区,直接写入磁盘。
  • O_SYNC:使每次write都等到物理I/O操作完成,同步模式,不需要再调用sync函数,所有buf数据实时从页缓存区写入到磁盘文件。
  • O_TRUNC:若此文件存在,并以读写或只写打开,则文件长度为0(打开文件的同时将文件中的内容清除)
  • O_EXCL:若同时设置O_CREAT标志且文件已存在,则会出错。可用于测试文件是否存在
  • O_NOCTTY:若打开的文件是终端设备,则不将此设备设置为进程的控制终端。

文件IO五大模式:

  • 阻塞模式:文件IO无法正常读写文件,进入阻塞模式(休眠状态)。
  • 非阻塞模式:O_NONBLOCK:若打开的文件是管道、块设备文件或字符设备文件,则后续的I/O操作均设置为非阻塞方式。
  • IO多路复用
  • 异步IO
  • 信号驱动IO

第三个权限参数mode有两种表示方法:

第一种:可以指定为以下宏

S_IRWXU  00700  user (file owner) has read, write, and execute permission
S_IRUSR  00400  user has read permission
S_IWUSR  00200  user has write permission
S_IXUSR  00100  user has execute permission
S_IRWXG  00070  group has read, write, and execute permission
S_IRGRP  00040  group has read permission
S_IWGRP  00020  group has write permission
S_IXGRP  00010  group has execute permission
S_IRWX0  00007  others have read, write, and execute permission
S_IROTH  00004  others have read permission
S_IWOTH  00002  others have write permission
S_IXOTH  00001  others have execute permission

第二种:数字表示权限

4(读)-100;2(写)-010;1(执行)-001;7(三个权限全打开)-111。

返回值的文件描述符:fd

进程:每个程序本质上就是一个进程。在Linux里面,是通过task_struct对进程进行管理的。当调用open函数后,Linux就会去VFS里面找到要打开的文件对应的操作接口集合(就是具体文件系统把自身具体操作接口填充到虚拟系统里的),找到文件对应的操作接口集合之后,就会把文件对应的操作接口集合填充到task_struct->fd_array[]->fi;e_operation里面去。fd实际上就是进程中file_struct结构体成员fd_array[ ]的数组下标。

比如,open命令打开一个文件之后,会返回一个文件描述符fd,然后其它函数就可根据此文件描述符找到我们想要操作的文件,比如下面这个read函数的第一个参数。文件描述符fd就相当于一种特殊的索引。

2. read()

简要来说:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。详细执行过程:可使用命令man 2 read查看使用手册。

使用read()函数需要包含头文件:
#include <unistd.h>

函数原型:
ssize_t read(int fd, void *buf, size_t count);

参数:
fd-即将读取文件的文件描述符
buf-存储读入数据的缓冲区
count-将要读入的数据的个数(数据长度)

返回值:
类型是:ssize_t,32位机上等同于int
成功时返回读取到的的字节数。返回count:成功写入全部字节;返回0~count:写入期间被异步信号打断
出错时返回-1
3. write()

write()函数的作用是将buf中的数据写入到fd对应的文件或设备中,写入的字节数为count。可使用命令man 2 write查看使用手册

使用write()函数需要包含头文件:#include <unistd.h>

函数原型:
ssize_t write(int fd, const void *buf, size_t count);

参数:
fd-即将读取文件的文件描述符
buf-要写入的数据缓冲区
count-写入数据的个数,大小不应该大于buf大小

返回值:
返回值类型为:ssize_t
成功时返回写入的字节数
出错时返回EOF,读到文件末返回0
4. close()

使用命令man 2 close查看使用手册

使用close()时需要包含头文件: #include <unistd.h>

函数原型:
int close(int fd);

函数参数:
fd: 要关闭的文件描述符

返回值:
关闭成功时返回0
关闭失败返回-1
出错时返回EOF

程序结束时会自动关闭所有打开的文件。文件被close后,再对文件进行任何操作都是无意义的。

5. lseek()

用于移动文件读写指针,设置文件的读写位置。因此还有以下作用:扩展文件;获取文件大小:lseek(fd,0,SEEK_END)。使用命令man 2 lseek查看详细使用手册。

使用lseek()函数需要包含头文件:
#include <sys/types.h>
#include <unistd.h>

函数原型:
off_t lseek(int fd, off_t offset, int whence);

函数参数:
fd-文件描述符
offset-偏移量,可正可负
whence-指定一个基准点,基准点+偏移量等于当前位置

返回值:
返回值类型为:off_t, 32位机等同于int
成功时则返回目前的读写位置, 也就是距离文件开头多少个字节(1字节为单位)
出错时返回-1

第三个参数whence

  • SEEK_SET:基准点为文件开头,从文件头部开始偏移offset个字节。
  • SEEK_CUR:基准点为当前位置,从文件当前读写的指针位置开始,增加offset个字节的偏移量。
  • SEEK_END:基准点为文件长度(可以理解为文件末尾),从文件结尾开始,文件偏移量设置为文件的大小加上偏移量字节。
6. sync()

sync()函数是一个系统调用,用于将页缓冲区中的文件数据强制写入磁盘。使用命令man 2 sync查看详细使用手册。

使用sync函数需要包含头文件:
#include <unistd.h>

函数原型如下:
void sync(void);

sync()函数没有参数,它会将所有已修改但尚未写入磁盘的文件数据刷新到磁盘中,以确保数据的持久性。
7.缓存区机制

在写入文件时,为了提高性能和效率,操作系统通常会使用缓存(buffer)机制。

缓存是位于内存中的临时存储区域,用于暂时保存将写入磁盘的数据。当应用程序调用写入函数(如write())时,数据会首先被写入到缓存中,而不是直接写入磁盘。

使用缓存的主要原因是:

  • 提高性能:内存访问速度比磁盘访问速度快得多,通过使用缓存,可以减少对磁盘的直接访问次数,从而加快写入操作的速度。将多个小的写操作合并到一个较大的写操作,可以降低磁盘I/O的开销。
  • 减少磁盘碎片:频繁的小规模写入操作会导致磁盘碎片化,这会影响磁盘性能。通过将数据缓存在内存中并进行合并,可以减少碎片化的影响。
  • 数据一致性:缓存还提供了数据一致性的机制。当数据被写入缓存时,应用程序可以继续进行其他操作,而不需要等待磁盘I/O完成。操作系统会负责管理缓存中的数据,并确保在适当的时机将其写入磁盘,以保证数据的一致性。

虽然缓存可以提高写入性能,但也存在风险。如果一个应用程序将大量数据写入缓存但没有及时将其刷新到磁盘,例如发生系统崩溃或断电等情况,那么可能会导致数据丢失或不一致的情况。因此,在关键操作或需要持久化数据的时候,可以使用sync()fsync()fdatasync()等函数来确保数据被立即写入磁盘。

标准I/O

C标准库实现了一个IO缓存区。

常用的标准I/O接口有:

  • fopen()
  • fclose()
  • fread()
  • fwrite()
  • fseek()
  • fflush()

使用标准I/O只需包含头文件:#inlcude <stdio.h>

1. fopen()
函数原型:
FILE *fopen(const char *path, const char *mode);

函数参数:
path-打开/创建的文件名,或文件路径
mode-文件的打开模式

返回值:
成功时返回一个文件流指针:FILE*
失败时返回NULL

mode的选项

  • b:二进制文件

  • r:只读方式打开文件,文件必须存在

  • w或a:只写方式打开文件,不存在则创建

    区别:w等价于O_TRUNC,a等价于O_APPEND;

  • +:读写方式打开文件,文件必须存在

r    打开只读文件,该文件必须存在。
r+   打开可读写的文件,该文件必须存在。
rb   打开一个二进制文件,只读。
rb+  以读/写方式打开一个二进制文件。
w    打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+   打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
wb   打开一个二进制文件,只写。
wb+  以读/写方式建立一个新的二进制文件。
a    以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。
a+   以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。
ab   打开一个二进制文件,进行追加 。
ab+  以读/写方式打开一个二进制文件进行追加 。

函数返回值:FILE*

FILE*——文件流指针,类似于文件IO中的文件描述符。

FILE的定义:struct _IO_FILE(usr/include/libio.h),包含读写缓存的首地址、大小、位置指针等。

                        类型          File*                文件描述符fd
                标准输入(流)         stdin                        0
                标准输出(流)        stdout                        1
                标准错误(流)         stder                        2
2. fread()
函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

函数参数:
ptr     接收数据的地址,最小尺寸size*nmemb字节
size    每次读取的一个单元的大小,单位字节
nmemb   读取的单元个数
stream  即将读取文件的文件流

返回值:
返回值类型为:size_t,在32位机等价于unsigned int
成功时返回实际读取的单元个数
文件结束或读取出错时返回0或一个小于nmemb的数
3. fwrite()(全缓存)
函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

函数参数:
ptr    	要写入文件的数据的地址
size	每次写入的一个单元的大小
nmemb	写入的单元个数
stream	即将写入文件的文件流

返回值:
返回值类型为:size_t
成功时返回实际写入的单元个数
出错时返回一个与nmemb不同的数
4. fclose()(全缓存)
函数原型:
int fclose(FILE *stream);

函数参数:
stream: 要关闭的文件流指针

返回值:
成功时返回0
失败时返回EOF,并设置errno
5.fflush()函数

fflush()函数用于刷新流缓冲区,将缓冲区中的数据立即写入页缓存区。

头文件:
#include <stdio.h>

函数原型:
int fflush(FILE *stream);

参数
stream:表示要刷新的文件流指针。

返回值
fflush()函数的返回值是一个整数
如果成功刷新缓冲区,则返回0
如果发生错误或出现EOF标志,则返回非零值。
6.fgets函数(行缓存的读)
函数原型:
char* fgets(char* s,int size,FILE* stream)

参数:
s——缓存,即读到哪里去
size——读多少字节
stream——从哪读

函数返回值:
若成功,返回缓存的地址(即s)
失败或已经处于文件尾端返回NULL。
7.fputs函数(行缓存的写)
函数原型:
int fputs(const char*s,FILE* stream);

参数:
s——缓存,即要写什么
stream——写到哪

函数返回值:
成功返回非负值
失败或者文件尾端为EOF(-1)。

写在最后:本人初学Linux,看完正点第一期的视频后乍看野火的基础第一期的视频讲到的这段系统编程的知识很懵,然后在网上搜索学习汇总最后整理了此篇博文。希望本文可以帮助到各位读者,文章如有不足,欢迎大家指出,如果文章帮到你了,请一定帮忙点个赞哦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值