Linux文件操作与系统调用

系列文章推荐

Linux文件系统目录结构
Linux必备基础
Linux构建一个deb软件安装包

前言

本文主要来自正点原子、野火Linux教程及本人理解,若有侵权请及时联系本人删除。如果本篇对您有帮助的话希望能一键三连,万分感谢。

文件系统

Linux一个重要的哲学是:一切皆文件。而文件与文件系统是密切相关的,文件系统粗略的分类:
在这里插入图片描述

根文件系统

根文件系统(rootfs)是内核启动时所 mount(挂载)的第一个文件系统,内核代码映像文件保存在根文件系中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。根文件系统的根目录/下有很多子目录:
在这里插入图片描述

存储设备文件系统(真文件系统)

提到文件系统时,我们首先想到的通常是 Windows 下的 FAT32、NTFS、exFAT 以及 Linux 下常用的 ext2、ext3 和 ext4 的类型格式。这些文件系统都是为了解决如何高效管理存储器空间的问题而诞生的。

从 EEPROM、Nor FLASH、NAND FLASH、eMMC 到机械硬盘,各种各样的存储器本质就是具有多个能够存储 0 和 1 数据单元的设备,存储内容时,程序需要直接访问这些存储单元的物理地址来保存内容。这样直接存储数据会带来极大的不便,如难以记录有效数据的位置,难以确定存储介质的剩余空间,以及应以何种格式来解读数据。就如同一个巨大的图书馆无人管理,杂乱无章地堆放着各种书籍,难以查找。

为了高效地存储和管理数据,文件系统在存储介质上建立了一种组织结构,这些结构包括操作系统引导区、目录和文件,就如同图书馆给不同类的书籍进行分类、编号,放在不同的书架上。不同的管理理念引出了不同的文件系统标准,上述的 FAT32、NTFS、exFAT、ext2/3/4 就是指不同类型的标准,除此之外,还有专门针对 NAND 类型设备的文件系统 jffs2、yaffs2 等等。

正是有了文件系统,计算机上的数据才能以文件的形式呈现给用户。下面简单介绍一下各种不同标准文件系统的特性:

  • FAT32 格式:兼容性好,STM32 等 MCU 也可以通过 Fatfs 支持 FAT32 文件系统,大部分SD 卡或 U 盘出厂默认使用的就是 FAT32 文件系统。它的主要缺点是技术老旧,单个文件不能超过 4GB,非日志型文件系统。
  • NTFS 格式:单个文件最大支持 256TB、支持长文件名、服务器文件管理权限等,而且 NTFS是日志型文件系统。但由于是日志型文件系统,会记录详细的读写操作,相对来说会加快FLASH 存储器的损耗。文件系统的日志功能是指,它会把文件系统的操作记录在磁盘的某个分区,当系统发生故障时,能够尽最大的努力保证数据的完整性。
  • exFAT 格式:基于 FAT32 改进而来,专为 FLASH 介质的存储器设计(如 SD 卡、U 盘),空间浪费少。单个文件最大支持 16EB,非日志文件系统。
  • ext2 格式:简单,文件少时性能较好,单个文件不能超过 2TB。非日志文件系统。
  • ext3 格式:相对于 ext2 主要增加了支持日志功能。
  • ext4 格式:从 ext3 改进而来,ext3 实际是 ext4 的子集。它支持 1EB 的分区,单个文件最大支持 16TB,支持无限的子目录数量,使用延迟分配策略优化了文件的数据块分配,允许自主控制是否使用日志的功能。
  • jffs2 和 yaffs2 格式:jffs2 和 yaffs2 是专为 FLASH 类型存储器设计的文件系统,它们针对FLASH 存储器的特性加入了擦写平衡和掉电保护等特性。由于 Nor、NAND FLASH 类型存储器的存储块的擦写次数是有限的(通常为 10 万次),使用这些类型的文件系统可以减少对存储器的损耗。

总的来说,在 Linux 下,ext2 适用于 U 盘(但为了兼容,使用得比较多的还是 FAT32 或 exFAT),日常应用推荐使用 ext4,而 ext3 使用的场景大概就只剩下对 ext4 格式的稳定性还有疑虑的用户了,但 ext4 从 2008 年就已结束实验期,进入稳定版了,可以放心使用。

Linux 内核本身也支持 FAT32 文件系统,而使用 NTFS 格式则需要安装额外的工具如 ntfs-3g。所以使用开发板出厂的默认 Linux 系统时,把 FAT32 格式的 U 盘直接插入到开发板是可以自动挂载的,而 NTFS 格式的则不支持。主机上的 Ubuntu 对于 NTFS 或 FAT32 的 U 盘都能自动识别并挂载,因为 U buntu 发行版安装了相应的支持。目前微软已公开 exFAT 文件系统的标准,且已把它开源至 Linux,未来 Linux 可能也默认支持 exFAT。

对于非常在意 FLASH 存储器损耗的场合,则可以考虑使用 jffs2 或 yaffs2 等文件系统。

在 Linux 下,可以通过如下命令查看系统当前存储设备使用的文件系统:

# 在主机或开发板上执行如下命令
df -T

如下图:
在这里插入图片描述
从上图可以看出,主机的硬盘设备为“/dev/sda1”,它使用的文件系统类型为 ext4,挂载点是根目录“/”。

伪文件系统

除了前面介绍的专门用于存储设备记录文件的文件系统外,Linux 内核还提供了 procfs、sysfs 和devfs 等伪文件系统。

伪文件系统存在于内存中,通常不占用硬盘空间,它以文件的形式,向用户提供了访问系统内核数据的接口。用户和应用程序可以通过访问这些数据接口,得到系统的信息,而且内核允许用户修改内核的某些参数。

procfs 文件系统(进程文件系统)

procfs 是“process filesystem”的缩写,所以它也被称为进程文件系统,它包含一个伪文件系统(启动时动态生成的文件系统),用于通过内核访问进程信息。procfs 通常会自动挂载在根目录下的/proc 文件夹。procfs 为用户提供内核状态和进程信息的接口,功能相当于 Windows 的任务管理器。由于 /proc 不是一个真正的文件系统,它也就不占用存储空间,只是占用有限的内存。

使用如下命令可以查看 proc 文件系统的内容:

# 查看 CPU 信息
cat /proc/cpuinfo
# 查看 proc 目录
ls /proc

如下图:
在这里插入图片描述
在这里插入图片描述
刚才我们查看了 CPU 的信息,而上图表示/proc 包含了非常多以数字命名的目录,这些数字就是进程的 PID 号,其它文件或目录的一些说明见下表。

表 /proc 各个文件的作用
在这里插入图片描述
下面我们以当前 bash 的进程 pid 目录,来了解 proc 文件系统的一些功能。使用如下命令来查看当前 bash 进程的 PID 号。

# 在主机上执行如下命令
ps

每个人的计算机运行运行状况不一样,所以得到的进程号也是不一样,如下图所示,当前得到bash 进程的 pid 是 3042。
在这里插入图片描述
根据这个 pid 号,查看 proc/3042 目录的内容,它记录了进程运行过程的相关信息。执行如下命令:

# 在主机上执行如下命令
# 把目录中的数字改成自己 bash 进程的 pid 号
ls /proc/3042

如下图:
在这里插入图片描述
该目录下的一些文件夹和文件的意义如下表。
在这里插入图片描述
如果是文件,可以直接使用 cat 命令输出对应文件的内容可查看,如查看进程名:

# 在主机上执行如下命令
# 把目录中的数字改成自己 bash 进程的 pid 号
cat /proc/3042/comm

在这里插入图片描述
可看到进程名为“bash”,实际上前面的“ps”命令也是通过“proc”文件系统获取到相关进程信息的。

sysfs 文件系统

上一节我们提及到的 procfs 是“任务管理器”,那 sysfs 同 procfs 一样,也是一个伪文件系统。

Linux 内核在 2.6 版本中引入了 sysfs 文件系统,sysfs 通常会自动挂载在根目录下的 sys 文件夹。sys 目录下的文件/文件夹向用户提供了一些关于设备、内核模块、文件系统以及其他内核组件的信息,如子目录 block 中存放了所有的块设备,而 bus 中存放了系统中所有的总线类型,有 i2c,usb,sdi o,pci 等。下图中的虚线表示软连接,可以看到所有跟设备有关的文件或文件夹都链接到了 device 目录下,类似于将一个大类,根据某个特征分为了无数个种类,这样使得/sys 文件夹的结构层次清晰明了。

如下图:
在这里插入图片描述
表 /sys 各个文件的作用
在这里插入图片描述
概括来说,sysfs 文件系统是内核加载驱动时,根据系统上的设备和总线构成导出的分级目录,它是系统上设备的直观反应,每个设备在 sysfs 下都有唯一的对应目录,用户可以通过具体设备目录下的文件访问设备。

sysfs 与 proc 相比有很多优点,最重要的莫过于设计上的清晰。sysfs 的设计原则是一个属性文件只做一件事情, sysfs 属性文件一般只有一个值,直接读取或写入。整个 /proc/scsi目录在2.6内核中已被标记为过时(LEGACY),它的功能已经被相应的 /sys 属性文件所完全取代。新设计的内核机制应该尽量使用 sysfs 机制,而将 proc 保留给纯净的“进程文件系统”。

devfs 文件系统

在 Linux 2.6 内核之前一直使用的是 devfs 文件系统管理设备,它通常挂载于/dev 目录下。devfs中的每个文件都对应一个设备,用户也可以通过/dev 目录下的文件访问硬件。在 sysfs 出现之前,devfs 是在制作根文件系统的时候就已经固定的,这不太方便使用,而当代的 devfs 通常会在系统运行时使用名为 udev 的工具根据 sysfs 目录生成 devfs 目录。在后面学习制作根文件系统时,就会接触到静态 devfs 以及使用 udev 动态生成 devfs 的选项。

/dev与/sys
  • /dev目录下
    对驱动程序熟悉的工程师可以使用,一个设备节点文件控制硬件全部特性
  • /sys目录下
    业余工程师使用,一个设备节点文件只控制硬件的一个特性,严格来说,它下面的文件是Linux内核导出到用户空间的硬件操作接口
dev、devfs、devtmpfs、udev、mdev、sysfs区别及关系

Linux 下对设备的管理方式主要有/dev和sysfs两种,前者是将设备注册为设备节点放入/dev目录下,而后者是在linux2.6内核后引入的新的文件系统。
/dev方式
关于/dev的管理方式,也经历了几代,下面介绍/dev管理方式的发展:

  • 静态/dev文件:
    在Linux中,老的设备管理方式是将设备通过设备节点放入/dev目录下,每个设备节点是/dev根目录下的一个文件,那么。如何区分这些设备节点,为了对这些设备节点进行命名,Linux通过主次设备号来指定不同的设备节点。有了主次设备号,如何指定主次设备号成了一个开发人员必须面临的问题。如果开发人员不打算将设备驱动程序与外界共享,那么指定什么号码都可以,只要她与当前设备内核使用的其他主设备不冲突即可。然而,如果开发人员想让驱动程序与外界共享(大多数Linux开发人员常常采用这一方法),那么这样随意指定设备号进行了,因为用户和其他开发人员并不知道哪个设备号对应于改设备,因此开发人员必须联系linux内核开发人员分配一个真实主设备好,这样在整个linux世界中,只有这个特定设备号才会被关联到那个特定的设备号(即每一个设备对应一个唯一的设备号),而这个设备号也被汇入Linux的发行版本的/dev目录中。基于这样的处理方式,就会产生以下问题。
    1、由于不断涌入的新设备,设备号会慢慢耗尽。
    2、这样的申请分配方式对于设备的管理比较麻烦。
    3、由于“正式”设备好不断汇入/dev目录。哪怕该设备在某些硬件上并不存在,这就导致很多设备号指向并存在的一些设备,在后期的/dev目录下甚至有上万个设备号。
  • Devfs
    linux kernel 2.4版本后引入devfs,devfs是一个虚拟的为念系统个,相比与静态的/dev文件主要有两点改进:
    1、允许使用自定的设备名称来注册设备节点,同时它兼容老的设备号,例如我们注册一个设备节点/dev/mydev
    2、所有的设备都由内核在系统启动时期创建并注册到/dev目录下,这就意味这/dev不在被成百个“无用”的设备节点充斥。
  • udev
    Devfs解决静态/dev管理的很多问题,但是它任然存在一定缺陷,基于此,在linux kernel 2.6.x版本后,Linux引入了udev.从而对devfs进行改进。udev是一个对/dev下设备节点进行动态管理的用户空间程序,她通过自身的守护进程和自定义的一些列规则来处理设备的加载,移除和热插拔等活动。相比与devfs ,它的主要改进如下:
    1、传统的devfs命名不够灵活,设备名称不可预知,而udev支持设备的固定命名。例如如果现在有两个硬盘,在devfs 中,他们们对应的设备节点分别是/dev/sda 和/dev/sdb ,那么我么就不知道硬盘对应于sda哪个又是sdb ,而udev 提供了存储设备的固定命名,任何硬盘根据其唯一的文件系统id ,磁盘名称及硬件链接的物理位置来进行识别。
    2、设备在热插拔的时候,用户态程序应该有办法得到通知。udev 运行在用户空间中,设备在热插拔时候,会通过netlink(linux 中内核空间和用户空间进程之间通信的方式)通知udev ,因此用户空间程序可以得到通知了,同时Udev运行在用户空间还可以减少内存的使用。
    3、devfs代码不灵活,只显示存在的设备列表,而有时候我们希望看到暂时不存在的设备名字
    4、major,minor 快被分配光了,我们需要考虑动态分配方法,而devfs不能支持。而当设备较多的时候,不能动态分配节点给设备注册造成很大的麻烦,需要不停尝试不同的设备节点以检查是否冲突。

sysfs
sysfs是Linux2.6引入的一种虚拟文件系统,挂载于/sys目录下,这个文件系统吧实际链接到系统上的设备,总线及其对应的驱动程序组织成分级的文件。从而将设备的层次结构映射到用户空间中,用户空间可以通过修改sysfs 中文件属性来修改设备属性值,从而与内核设备交互。

udev和sysfs 的关系
sysfs是对devfs改进,udev也是对devfs的改进。两者之间的区别与联系为:实际上用户的工具udev就是利用sysfs提供的信息来实现的:udev会根据sysfs里面的设备信息创建/dev目录下的相应设备节点。

mdev/udev/devfs的区别
mdev是udev的简化版本,是busybox中所带的程序,最适合用在嵌入式系统,而udev一般用在PC上的linux中,相对mdev来说要复杂些,devfs是2.4内核引入的,而在2.6内核中却被udev所替代,他们有着共同的优点,只是devfs中存在一些未修复的BUG,作者也停止了对他的维护,最显著的一个区别,采用devfs时,当一个并不存在的设备结点时,他却还能自动的加载对应的设备驱动,而udev则不能加载,因为加载浪费了资源。

dev 和mdev 是两个使用uevent 机制处理热插拔问题的用户空间程序,两者的实现机理不同。udev 是基于netlink 机制的,它在系统启动时运行了一个deamon程序udevd,通过监听内核发送的uevent 来执行相应的热拔插动作,包括创建/删除设备节点,加载/卸载驱动模块等等。mdev 是基于uevent_helper 机制的,它在系统启动时修改了内核中的uevnet_helper 变量(通过写/proc/sys/kernel/hotplug),值为“/sbin/mdev”。这样内核产生uevent 时会调用uevent_helper 所指的用户级程序,也就是mdev,来执行相应的热拔插动作。 udev 使用的netlink 机制在有大量uevent 的场合效率高,适合用在PC 机上。

devfs、tmpfs、devtmpfs
devfs是文件系统形式的device manager。tmpfs存在在内存和swap中,因此只能保存临时文件。devtmpfs是改进的devfs,也是存在内存中,挂载点是/dev/。

devtmpfs 文件系统

devtmpfs 的功用是在 Linux 核心 启动早期建立一个初步的 /dev,令一般启动程序不用等待 udev(udev 是Linux kernel 2.6系列的设备管理器。它主要的功能是管理/dev目录底下的设备节点。),缩短 GNU/Linux 的开机时间。

在devtmpfs出现之前,/dev/下面的设备节点应该都是udev-daemon收到内核的事件后用mknod程序或者直接调mknod()系统调用创建出来的;现在基本上不走udev了,几乎所有的设备文件(比如/dev/sda1)都是内核直接创建的。

虚拟文件系统

除了前面提到的存储器文件系统 FAT32、ext4,伪文件统/proc、/sys、/dev 外,还有内存文件系统ramfs,网络文件系统 nfs 等等,不同的文件系统标准,需要使用不同的程序逻辑实现访问,对外提供的访问接口可能也稍有差异。但是我们在编写应用程序时,大都可以通过类似 fopen、fread、fwrite 等 C 标准库函数访问文件,这都是虚拟文件系统的功劳。

Linux 内核包含了文件管理子系统组件,它主要实现了虚拟文件系统(Virtual File System,VFS),虚拟文件系统屏蔽了各种硬件上的差异以及具体实现的细节,为所有的硬件设备提供统一的接口,从而达到设备无关性的目的,同时文件管理系统还为应用层提供统一的 API 接口。

在 Linux 下,一个与文件操作相关的应用程序结构如下图所示。
在这里插入图片描述
上图解构如下:

  • 应用层指用户编写的程序,如我们的 hello.c。
  • GNU C 库(glibc)即 C 语言标准库,例如在编译器章节介绍的 libc.so.6 文件,它包含了printf、malloc,以及本章使用的 fopen、fread、fwrite 等文件操作函数。
  • 用户程序和 glibc 库都是属于用户空间的,本质都是用户程序。
  • 应用层的程序和 glibc 可能会调用到“系统调用层(SCI)”的函数,这些函数是 Linux 内核对外提供的函数接口,用户通过这些函数向系统申请操作。例如,C 库的 printf 函数使用了系统的 vsprintf 和 write 函数,C 库的 fopen、fread、fwrite 分别调用了系统的 open、read、write 函数,具体可以阅读 glibc 的源码了解。
  • 由于文件系统种类非常多,跟文件操作相关的 open、read、write 等函数经过虚拟文件系统层,再访问具体的文件系统。

在这里插入图片描述

总的来说,为了使不同的文件系统共存,Linux 内核在用户层与具体文件系统之前增加了虚拟文件系统中间层,它对复杂的系统进行抽象化,对用户提供了统一的文件操作接口。无论是 ext2/3/4、FAT32、NTFS 存储的文件,还是/proc、/sys 提供的信息还是硬件设备,无论内容是在本地还是网络上,都使用一样的 open、read、write 来访问,使得“一切皆文件”的理念被实现,这也正是软件中间层的魅力。

网络文件系统

NFS(Network File System),即网络文件系统,能使使用者访问网络上别处的文件就像在使用自己的计算机一样。其工作原理是使用客户端/服务器架构 :
在这里插入图片描述
服务器程序向其他计算机提供对文件系统的访问,其过程称为输出。NFS客户端程序对共享文件系统进行访问时,把它们从NFS服务器中“输送”出来。文件通常以块为单位进行传输。 在我们嵌入式Linux中,NFS的主要应用如:把主机的上文件(比如目标板的可执行文件)共享给目标板,这样目标板就很方便地运行程序。挂载网络文件系统中的实验框图:
在这里插入图片描述

Linux 系统调用

系统调用(System Call)是操作系统提供给用户程序调用的一组“特殊”函数
接口 API,文件操作就是其中一种类型。实际上,Linux 提供的系统调用包含以下内容:

  • 进程控制:如 fork、clone、exit 、setpriority 等创建、中止、设置进程优先级的操作。
  • 文件系统控制:如 open、read、write 等对文件的打开、读取、写入操作。
  • 系统控制:如 reboot、stime、init_module 等重启、调整系统时间、初始化模块的系统操作。
  • 内存管理:如 mlock、mremap 等内存页上锁重、映射虚拟内存操作。
  • 网络管理:如 sethostname、gethostname 设置或获取本主机名操作。
  • socket 控制:如 socket、bind、send 等进行 TCP、UDP 的网络通讯操作。
  • 用户管理:如 setuid、getuid 等设置或获取用户 ID 的操作。
  • 进程间通信:包含信号量、管道、共享内存等操作。

从逻辑上来说,系统调用可被看成是一个 Linux 内核与用户空间程序交互的中间人,它把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。它的存在就是为了对用户空间与内核空间进行隔离,要求用户通过给定的方式访问系统资源,从而达到保护系统的目的。

也就是说,我们心心念念的 Linux 应用程序与硬件驱动程序之间,就是各种各样的系统调用,所以无论出于何种目的,系统调用是学习 Linux 开发绕不开的话题。

标准I/O(C 标准库)

本小节讲解使用通用的 C 标准库接口访问文件,标准库实际是对系统调用再次进行了封装。使用 C 标准库编写的代码,能方便地在不同的系统上移植。

例如 Windows 系统打开文件操作的系统 API 为 OpenFile,Linux 则为open,C 标准库都把它们封装为 fopen,Windows 下的 C 库会通过 fopen 调用 OpenFile 函数实现操作,而 Linux 下则通过 glibc调用 open 打开文件。用户代码如果使用 fopen,那么只要根据不同的系统重新编译程序即可,而不需要修改对应的代码。

常用文件操作(C 标准库)

在开发时,遇到不熟悉的库函数或系统调用,要善用 man 手册,而不要老是从网上查找。C 标准库提供的常用文件操作简介如下:

fopen 函数

fopen 库函数用于打开或创建文件,返回相应的文件流。它的函数原型如下:

#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
  • pathname 参数用于指定要打开或创建的文件名。
  • mode 参数用于指定文件的打开方式,注意该参数是一个字符串,输入时需要带双引号:
  • “r”:以只读方式打开,文件指针位于文件的开头。
  • “r+”:以读和写的方式打开,文件指针位于文件的开头。
  • “w”:以写的方式打开,不管原文件是否有内容都把原内容清空掉,文件指针位于文件的开头。
  • “w+”:同上,不过当文件不存在时,前面的“w”模式会返回错误,而此处的“w+”则会创建新文件。
  • “a”:以追加内容的方式打开,若文件不存在会创建新文件,文件指针位于文件的末尾。与“w+”的区别是它不会清空原文件的内容而是追加。
  • “a+”:以读和追加的方式打开,其它同上。
  • fopen 的返回值是 FILE 类型的文件文件流,当它的值不为 NULL 时表示正常,后续的 fread、fwrite 等函数可通过文件流访问对应的文件。

fread 函数

fread 库函数用于从文件流中读取数据。它的函数原型如下:

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

stream 是使用 fopen 打开的文件流,fread 通过它指定要访问的文件,它从该文件中读取 nmemb 项数据,每项的大小为 size,读取到的数据会被存储在 ptr 指向的数组中。fread 的返回值为成功读取的项数(项的单位为 size)。

fwrite 函数

fwrite 库函数用于把数据写入到文件流。它的函数原型如下:

#include <stdio.h>
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);

它的操作与 fread 相反,把 ptr 数组中的内容写入到 stream 文件流,写入的项数为 nmemb,每项大小为 size,返回值为成功写入的项数(项的单位为 size)。

fclose 函数

fclose 库函数用于关闭指定的文件流,关闭时它会把尚未写到文件的内容都写出。因为标准库会对数据进行缓冲,所以需要使用 fclose 来确保数据被写出。它的函数原型如下:

#include <unistd.h>
int close(int fd);

fflush 函数

该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync(先调用fflush后调用fsync,否则不起作用)。fclose 函数本身也包含了 fflush 的操作。fflush 的函数原型如下:

#include <stdio.h>
int fflush(FILE *stream);
补充sync、fsync、fdatasync、fflush函数区别和使用举例

传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘 I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满 或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式被称为延迟写 (delayed write)(Bach [1986]第3章详细讨论了缓冲区高速缓存)。

延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到 磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了 sync、fsync和fdatasync三个函数。

  • sync:将所有修改过的快缓存区排入写队列,然后返回,并不等待实际写磁盘操作结束;
  • fsync函数:只对有文件描述符制定的单一文件起作用,并且等待些磁盘操作结束,然后返回;
  • fdatasync:类似fsync,但它只影响文件的数据部分。fsync还会同步更新文件的属性;
  • fflush:标准I/O函数(如:fread,fwrite)会在内存建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync。(先调用fflush后调用fsync,否则不起作用)。

示例:

fp = fopen(filename, "wb+");
gettimeofday(&start, NULL); //获取时间  struct timeval start
  if (fwrite(w_buf, g_block_size, 1, fp) <= 0)  //写入
    break;
 
fflush(fp); //可能有fwrite没写完的部分, flush 内存缓存 到 fp 缓存
fdatasync(fileno(fp));   //写入磁盘 ,也可以用fsync
gettimeofday(&end, NULL);
write_timeuse += 1000000 * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec;

fseek 函数

fseek 函数用于设置下一次读写函数操作的位置。它的函数原型如下:

#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);

其中的 offset 参数用于指定位置,whence 参数则定义了 offset 的意义,whence 的可取值如下:

  • SEEK_SET:offset 是一个绝对位置。
  • SEEK_END:offset 是以文件尾为参考点的相对位置。
  • SEEK_CUR:offset 是以当前位置为参考点的相对位置。

实验代码分析

下面我们使用 C 标准库进行文件操作实验,如下所示。
main.c 文件

1 #include <stdio.h>
2 #include <string.h>
3
4 //要写入的字符串
5 const char buf[] = "filesystem_test:Hello World!\n";
6 //文件描述符
7 FILE *fp;
8 char str[100];
9
10
11 int main(void)
12 {
13 //创建一个文件
14 fp = fopen("filesystem_test.txt", "w+");
15 //正常返回文件指针
16 //异常返回 NULL
17 if(NULL == fp){
18 printf("Fail to Open File\n");
19 return 0;
20 }
21 //将 buf 的内容写入文件
22 //每次写入 1 个字节,总长度由 strlen 给出
23 fwrite(buf, 1, strlen(buf), fp);
24
25 //写入 Embedfire
26 //每次写入 1 个字节,总长度由 strlen 给出
27 fwrite("Embedfire\n", 1, strlen("Embedfire\n"),fp);
28
29 //把缓冲区的数据立即写入文件
30 fflush(fp);
31
32 //此时的文件位置指针位于文件的结尾处,使用 fseek 函数使文件指针回到文件头
33 fseek(fp, 0, SEEK_SET);
34
35 //从文件中读取内容到 str 中
36 //每次读取 100 个字节,读取 1 次
37 fread(str, 100, 1, fp);
38
39 printf("File content:\n%s \n", str);
40
41 fclose(fp);
42
43 return 0;
44 }

Makefile 说明

Makefile 是跟工程目录匹配的,本实验仅有一个 main.c 文件,且与 Makefile 处于同级目录。它的工程文件结构如下图所示。
在这里插入图片描述
此处编写的 Makefile 与前面《多级结构工程的 Makefile》章节的基本一致,如下所示。
Makefile 文件

1 #生成可执行文件的名称
2 Target = file_demo
3 ARCH ?= x86
4 #编译器 CC
5 #根据传入的参数 ARCH,确定使用的编译器
6 #默认使用 gcc 编译器
7 #make ARCH=arm 时使用 ARM-GCC 编译器
8 ifeq ($(ARCH), x86)
9 CC = gcc
10 else
11 CC = arm-linux-gnueabihf-gcc
12 endif
13 #存放中间文件的路径
14 build_dir = build_$(ARCH)
15 #存放源文件的文件夹
16 src_dir = .
17 #存放头文件的文件夹
18 inc_dir = includes .
19
20 #源文件
21 sources = $(foreach dir,$(src_dir),$(wildcard $(dir)/*.c))
22 # 目标文件(*.o)
23 objects = $(patsubst %.c,$(build_dir)/%.o,$(notdir $(sources)))
24 # 头文件
25 includes = $(foreach dir,$(inc_dir),$(wildcard $(dir)/*.h))
26 # 编译参数
27 # 指定头文件的路径
28 CFLAGS = $(patsubst %, -I%, $(inc_dir))
29
30 # 链接过程
31 # 开发板上无法使用动态库,因此使用静态链接的方式
32 $(build_dir)/$(Target) : $(objects) | create_build
33 $(CC) $^ -o $@
34
35 # 编译工程
36 # 编译 src 文件夹中的源文件,并将生成的目标文件放在 objs 文件夹中
37 $(build_dir)/%.o : $(src_dir)/%.c $(includes) | create_build
38 $(CC) -c $(CFLAGS) $< -o $@
39
40
41 # 以下为伪目标,调用方式:make 伪目标
42 #clean:用于 Clean Project
43 #check:用于检查某个变量的值
44 .PHONY:clean cleanall check create_build
45 # 按架构删除
46 clean:
47 rm -rf $(build_dir)
48
49 # 全部删除
50 cleanall:
51 rm -rf build_x86 build_arm
52
53 # 命令前带"@", 表示不在终端上输出执行的命令
54 # 这个目标主要是用来调试 Makefile 时输出一些内容
55 check:
56 @echo $(CFLAGS)
57 @echo $(CURDIR)
58 @echo $(src_dir)
59 @echo $(sources)
60 @echo $(objects)
61
62 # 创建一个新目录 create,用于存放过程文件
63 create_build:
64 @mkdir -p $(build_dir)

与前面讲解的 Makefile 的差异主要如下:

  • 第 2 行:本工程编译后的可执行文件名为 file_demo,以后我们的 Makefile 都在此处定义可执行文件名,配合第 14 行的 build_dir 变量的值可找到编译后生成的应用程序的目录。
  • 第 16 行:本工程的源文件跟 Makefile 在相同的目录,所以表示源文件的 src_dir 变量赋值为“.”,表示当前目录。
  • 54~55 行:定义了一个名为 create_build 的伪目标,它执行的 Shell 命令为创建编译目录,在下面说明的代码中会被用到。
  • 第 32 行和第 37 行:分别是生成最终目标文件和 *.o 文件的依赖,与之前不同的时它们的末尾都新增了“| create_build”的内容,其中“|”在此处的意义为前置依赖,create_build 为上面说明的伪目标,合起来的意义是 create_build 这个伪目标要先被执行,即要先创建编译目录。

编译及测试

x86 架构

本实验支持 x86 和 ARM 架构,在 x86 上的编译及测试命令如下:

# 在主机的实验代码 Makefile 目录下编译
# 默认编译 x86 平台的程序
make
tree
# 运行
./build_x86/file_demo
# 程序运行后本身有输出,并且创建了一个文件
ls
# 查看文件的内容
cat filesystem_test.txt

如下图
在这里插入图片描述

ARM 架构

对于 ARM 架构的程序,可使用如下步骤进行编译:

# 在主机的实验代码 Makefile 目录下编译
# 编译 arm 平台的程序
make ARCH=arm

编译后生成的 ARM 平台程序为 build_arm/file_demo,使用网络文件系统共享至开发板,在开发板的终端上运行即可,如下图所示。
在这里插入图片描述

标准IO的文件操作五大模式

  • 阻塞模式,当我们的文件I/O无法正常读写数据时,它就会处于休眠状态
  • 非阻塞模式,即使文件I/O无法正常读写数据,它也要立刻返回,不能处于休眠状态
  • IO多路复用
  • 异步IO
  • 信号驱动IO

文件I/O(系统调用)

常用文件操作(系统调用)

Linux 提供的文件操作系统调用常用的有 open、write、read、lseek、close 等。

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);

Linux 使用 open 函数来打开文件,并返回该文件对应的文件描述符。函数参数的具体说明如下:

  • pathname:要打开或创建的文件名;
  • flag:指定文件的打开方式,具体有以下参数,见下表 flag 参数值。
  • 返回值
    成功:文件描述符
    失败:-1

文件打开模式分为
主模式:

  • O_RDONLY:只读模式
  • O_WRONLY:只写模式
  • O_RDWR:读写,模式

副模式:

  • O_CREAT:当文件不存在,需要去创建文件
  • O_APPEND:追加模式,把文件读写位置设置到文件末尾
  • O_DIRECT:直接IO模式,处于该模式下read、write函数不经过页缓存区直接写入磁盘
  • O_SYNC:同步模式,处于该模式不用再手动调用sync函数,所有数据都会实时从页缓存区写入磁盘
  • O_NOBLOCK:非阻塞模式,文件I/O无法正常读写数据时要立刻返回,不能处于休眠状态

表 flag 参数值
在这里插入图片描述
C 库函数 fopen 的 mode 参数与系统调用 open 的 flags 参数有如下表中的等价关系。

表 fopen 的 mode 与 open 的 flags 参数关系
在这里插入图片描述

  • mode:当 open 函数的 flag 值设置为 O_CREAT 时,必须使用 mode 参数来设置文件与用户相关的权限。mode 可用的权限如下表所示,表中各个参数可使用“| ”来组合。

表文件权限
在这里插入图片描述
举例

当文件存在时
open(argv[1],O_RDONLY);
当文件不存在时,0666表示为可读可写
open(argv[2],O_RDONLY|O_CREAT, 0666);

read 函数

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

read 函数用于从文件中读取若干个字节的数据,保存到数据缓冲区 buf 中,并返回实际读取的字节数,具体函数参数如下:

  • fd:文件对应的文件描述符,可以通过 fopen 函数获得。另外,当一个程序运行时,Linux
    默认有 0、1、2 这三个已经打开的文件描述符,分别对应了标准输入、标准输出、标准错
    误输出,即可以直接访问这三种文件描述符;
  • buf:指向数据缓冲区的指针;
  • count:读取多少个字节的数据。
  • 返回值
    失败:
    -1,读取错误
    成功:
    count:成功读取全部字节
    0~count:
    • 剩余文件长度小于count
    • 读取期间被异步信号打断

write 函数

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

write 函数用于往文件写入内容,并返回实际写入的字节长度,具体函数参数如下:

  • fd:文件对应的文件描述符,可以通过 fopen 函数获得。
  • buf:指向数据缓冲区的指针;
  • count:往文件中写入多少个字节。
  • 返回值
    失败:
    -1,读取错误
    成功:
    • count:成功写入全部字节
    • 0~count:
      • 写入期间被异步信号打断

close 函数

#include <unistd.h>
int close(int fd);

当我们完成对文件的操作之后,想要关闭该文件,可以调用 close 函数,来关闭该 fd 文件描述符对应的文件。

  • 返回值:
    成功:0
    失败:-1

lseek 函数

lseek 函数可以用与设置文件指针的位置,并返回文件指针相对于文件头的位置。其函数原型如下:

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
  • 返回值
    成功:文件偏移位置值
    失败:-1

它的用法与 flseek 一样,其中的 offset 参数用于指定位置,whence 参数则定义了 offset 的意义,whence 的可取值如下:

  • SEEK_SET:offset 是一个绝对位置(基准点为文件开头)
  • SEEK_END:offset 是以文件尾为参考点的相对位置(基准点为当前位置)
  • SEEK_CUR:offset 是以当前位置为参考点的相对位置(基准点为文件末尾)

sync函数

页缓存和回写,用于强制把修改过的页缓存区数据写入磁盘,其函数原型如下:

#include <unistd.h>
void sync(void);

返回值

使用示例

void sync();

实验代码分析

main.c 文件

1 #include <sys/stat.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <stdio.h>
5 #include <string.h>
6
7 //文件描述符
8 int fd;
9 char str[100];
10
11
12 int main(void)
13 {
14 //创建一个文件
15 fd = open("testscript.sh", O_RDWR|O_CREAT|O_TRUNC, S_IRWXU);
16 //文件描述符 fd 为非负整数
17 if(fd < 0){
18 printf("Fail to Open File\n");
19 return 0;
20 }
21 //写入字符串 pwd
22 write(fd, "pwd\n", strlen("pwd\n"));
23
24 //写入字符串 ls
25 write(fd, "ls\n", strlen("ls\n"));
26
27 //此时的文件指针位于文件的结尾处,使用 lseek 函数使文件指针回到文件头
28 lseek(fd, 0, SEEK_SET);
29
30 //从文件中读取 100 个字节的内容到 str 中,该函数会返回实际读到的字节数
31 read(fd, str, 100);
32
33 printf("File content:\n%s \n", str);
34
35 close(fd);
36
37 return 0;
38 }

执行流程

本实验与 C 库文件操作类似,也是创建文件、写入内容然后读出,不过此处使用的都是系统调用函数如 open、write、lseek、read、close,具体说明如下:

  • 代码中先调用了 open 函数以可读写的方式打开一个文本文件,并且 O_CREAT 指定如果文件不存在,则创建一个新的文件,文件的权限为 S_IRWXU,即当前用户可读可写可执行,当前用户组和其他用户没有任何权限。
  • open 与 fopen 的返回值功能类似,都是文件描述符,不过 open 使用非负整数来表示正常,失败时返回-1,而 fopen 失败时返回 NULL。
  • 创建文件后调用 write 函数写入了“pwdn”、“lsn”这样的字符串,实际上就是简单的 Shell命令。
  • 使用 read 函数读取内容前,先调用 lseek 函数重置了文件指针至文件开头处读取。与 C 库文件操作的区别 write 和 read 之间不需要使用 fflush 确保缓冲区的内容并写入,因为系统调用的文件操作是没有缓冲区的。
  • 最后关闭文件,释放文件描述符。

头文件目录

示例代码中的开头包含了一系列 Linux 系统常用的头文件。今后学习 Linux 的过程中,我们可能会接触各种各样的头文件,因此了解一下 Linux 中头文件的用法十分有必要。

在 linux 中,大部分的头文件在系统的“/usr/include”目录下可以找到,它是系统自带的 GCC 编译器默认的头文件目录,如下图所示,如果把该目录下的 stdio.h 文件删除掉或更改名字(想尝试请备份),那么使用 GCC 编译 hello world 的程序会因为找不到 stdio.h 文件而报错。
在这里插入图片描述
代码中一些头文件前包含了某个目录,比如 sys/stat.h,这些头文件可以在编译器文件夹中的目录下找到。我们通常可以使用 locate 命令来搜索,如:

# 在 Ubuntu 主机下执行如下命令:
locate sys/stat.h

如下图:
在这里插入图片描述
在上图中我们查找出 sys/stat.h 存在三个位置,这是因为示例的主机安装了三个版本的编译器,每个编译器都有自己的头文件,编译器运行时默认是采用自己目录下的头文件编译的。

常用头文件

在后面的学习中我们常常会用到以下头文件,此处进行简单说明,若想查看具体的头文件内容,使用 locate 命令找到该文件目录后打开即可:

  • 头文件 stdio.h:C 标准输入与输出(standard input & output)头文件,我们经常使用的打印函数 printf 函数就位于该头文件中。
  • 头文件 stdlib.h:C 标准库(standard library)头文件,该文件包含了常用的 malloc 函数、free函数。
  • 头文件 sys/stat.h:包含了关于文件权限定义,如 S_IRWXU、S_IWUSR,以及函数 fstat 用于查询文件状态。涉及系统调用文件相关的操作,通常都需要用到 sys/stat.h 文件。
  • 头文件 unistd.h:UNIX C 标准库头文件,unix,linux 系列的操作系统相关的 C 库,定义了unix 类系统 POSIX 标准的符号常量头文件,比如 Linux 标准的输入文件描述符(STDIN),标准输出文件描述符(STDOUT),还有 read、write 等系统调用的声明。
  • 头文件 fcntl.h:unix 标准中通用的头文件,其中包含的相关函数有 open,fcntl,close 等操作。
  • 头文件 sys/types.h:包含了 Unix/Linux 系统的数据类型的头文件,常用的有 size_t,time_t,pid_t 等类型。

编译及测试

本实验使用的 Makefile 与上一小节的完全一样,不再分析。

x86 架构

本实验支持 x86 和 ARM 架构,在 x86 上的编译及测试命令如下:

# 在主机的实验代码 Makefile 目录下编译
# 默认编译 x86 平台的程序
make
tree
# 运行
./build_x86/file_demo
# 程序运行后本身有输出,并且创建了一个文件
ls
# 查看文件的内容
cat testsript.sh
# 执行生成的 testscript.sh 文件
./testscript.sh

如下图:
在这里插入图片描述
从上图可看到,file_demo 程序执行后,它创建的 testscript.sh 文件带有可执行权限,运行./testscript.sh可执行该脚本。

ARM 架构

对于 ARM 架构的程序,可使用如下步骤进行编译:

# 在主机的实验代码 Makefile 目录下编译
# 编译 arm 平台的程序
make ARCH=arm

编译后生成的 ARM 平台程序为 build_arm/file_demo,使用网络文件系统共享至开发板,在开发板的终端上测试即可。

如下图:
在这里插入图片描述

如何决择

既然 C 标准库和系统调用都能够操作文件,那么应该选择哪种操作呢?考虑的因素如下:

  • 使用系统调用会影响系统的性能。执行系统调用时,Linux 需要从用户态切换至内核态,执行完毕再返回用户代码,所以减少系统调用能减少这方面的开销。如库函数写入数据的文件操作 fwrite 最后也是执行了 write 系统调用,如果是写少量数据的话,直接执行 write 可能会更高效,但如果是频繁的写入操作,由于 fwrite 的缓冲区可以减少调用 write 的次数,这种情况下使用 fwrite 能更节省时间。
  • 硬件本身会限制系统调用本身每次读写数据块的大小。如针对某种存储设备的 write 函数每次可能必须写 4kB 的数据,那么当要写入的实际数据小于 4kB 时,write 也只能按 4kB 写入,浪费了部分空间,而带缓冲区的 fwrite 函数面对这种情况,会尽量在满足数据长度要求时才执行系统调用,减少空间开销。
  • 也正是由于库函数带缓冲区,使得我们无法清楚地知道它何时才会真正地把内容写入到硬件上,所以在需要对硬件进行确定的控制时,我们更倾向于执行系统调用。

浅谈linux系统下C标准库IO缓存区和内核缓存区的区别

整体框图

在这里插入图片描述

C标准库的I/O缓冲区

UNIX的传统是Everything is a file,键盘、显示器、串口、磁盘等设备在/dev 目录下都有一个特殊的设备文件与之对应,这些设备文件也可以像普通文件(保存在磁盘上的文件)一样打开、读、写和关闭,使用的函数接口是相同的。用户程序调用C标准I/O库函数读写普通文件或设备,而这些库函数要通过系统调用把读写请求传给内核 ,最终由内核驱动磁盘或设备完成I/O操作。

C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE 结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc / fputc 为例,当用户程序第一次调用fgetc 读一个字节时,fgetc 函数可能通过系统调用 进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指 向I/O缓冲区中的第二个字符,以后用户再调fgetc ,就直接从I/O缓冲区中读取,而不需要进内核 了,当用户把这1K字节都读完之后,再次调用fgetc 时,fgetc 函数会再次进入内核读1K字节 到I/O缓冲区中。

在这个场景中用户程序、C标准库和内核之间的关系就像在“Memory Hierarchy”中 CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc 通常只是写到I/O缓 冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲 区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻 传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件 之前也会做Flush操作。

我们知道main 函数被启动代码这样调用:exit(main(argc, argv));。main 函数return时启动代码会调用exit ,exit 函数首先关闭所有尚未关闭的FILE *指针(关闭之前要做Flush操作),然后通 过_exit 系统调用进入内核退出当前进程.

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。

  • 全缓冲
    如果缓冲区写满了就写回内核。常规文件通常是全缓冲的(缓冲区一般为4096个字节)。
  • 行缓冲
    如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入流 (stdin) 和标准输出流 (stdout) 对应终端设备时通常是行缓冲的。注意换行符也被读入缓冲区(缓冲区一般为1024个字节)。
  • 无缓冲
    用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出流 (stderr) 通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

除了写满缓冲区、写入换行符之外,行缓冲还有两种情况会自动做Flush操作。如果:

  • 用户程序调用库函数从无缓冲的文件中读取
  • 或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据

如果用户程序不想完全依赖于自动的Flush操作,可以调fflush函数手动做Flush操作。

#include <stdio.h>

int fflush(FILE *stream);

返回值:成功返回0,出错返回EOF并设置errno

fflush函数用于确保数据写回了内核,以免进程异常终止时丢失数据,如fflush(stdout); 作为一个特例,调 用fflush(NULL)可以对所有打开文件的I/O缓冲区做Flush操作。

对缓冲区操作的函数(C语言)
标准输出函数:printf、puts、putchar等。
标准输入函数:scanf、gets、getchar等。
IO_FILE:fopen、fwrite、fread、fseek等

用户程序的缓冲区

在函数栈上分配的如char buf[10];之类的缓冲区, strcpy(buf, str); str 所指向的字符串有可能超过10个字符而导致写越界,这种写越界可能当时不出错, 而在函数返回时出现段错误,原因是写越界覆盖了保存在栈帧上的返回地址, 函数返回时跳转到非法地址,因而出错。

像buf 这种由调用者分配并传给函数读或写的一段内存通常称为缓冲区(Buffer),缓冲区写越界的错误称为缓冲区溢出(Buffer Overflow)。如果只是出现段错误那还不算严重,更严重的是缓冲区溢出Bug经常被恶意用户利用,使函数返回时跳转到一个事先设好的地址,执行事先设好的指令,如果设计得巧妙甚至可以启动一个Shell,然后随心所欲执行任何命令,可想而知,如果一个用root权限执行的程序存在这样的Bug,被攻陷了,后果将很严重。

下图以fgets / fputs 示意了I/O缓冲区的作用,使用fgets / fputs 函数时在用户程序中也需要分配缓冲 区(图中的buf1 和buf2 ),注意区分用户程序的缓冲区和C标准库的I/O缓冲区。
在这里插入图片描述

内核缓冲区

1、终端缓冲
终端设备有输入和输出队列缓冲区,如下图所示
在这里插入图片描述
以输入队列为例,从键盘输入的字符经线路规程过滤后进入输入队列,用户程序以先进先出的顺序从队列中读取字符,一般情况下,当输入队列满的时候再输入字符会丢失,同时系统会响铃警报。终端可以配置成回显(Echo)模式,在这种模式下,输入队列中的每个字符既送给用户程序也送给 输出队列,因此我们在命令行键入字符时,该字符不仅可以被程序读取,我们也可以同时在屏幕上看到该字符的回显。

注意上述情况是用户进程(shell进程也是)调用read/write等unbuffer I/O函数的情况,当调用printf/scanf (底层实现也是read/write)等C标准I/O库函数时,当用户程序调用scanf读取键盘输入时,开始输入的字符都存到输入队列,直到我们遇到换行符(标准输入和标准输出都是行缓冲的)时,系统调用read将输入队列的内容读到用户进程的I/O缓冲区; 当调用printf 打印一个字符串时,如果语句中带换行符,则立刻将放在I/O缓冲区的字符串调用write写到内核的输出队列,打印到屏幕上,如果printf语句没带换行符,则由上面的讨论可知,程序退出时会做fflush操作.

2、虽然write 系统调用位于C标准库I/O缓冲区的底 层,被称为Unbuffered I/O函数,但在write 的底层也可以分配一个内核I/O缓冲区,所以write 也不一定是直接写到文件的,也 可能写到内核I/O缓冲区中,可以使用fsync函数同步至磁盘文件,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别 的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的, 而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的.

3、为了减少读盘次数,内核缓存了目录的树状结构,称为dentry(directory entry(目录下项) cache

4、FIFO和UNIX Domain Socket这两种IPC机制都是利用文件系统中的特殊文件来标识的。FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,各进程可以打开这个文件进行read / write ,实际上是在读写内核通道(根本原因在于这个file 结构体所指向的read 、write 函数指针和常规文件不一样),这样就实现了进程间通信。UNIX Domain Socket和FIFO的原理类似,也需 要一个特殊的socket文件来标识内核中的通道,文件类型s表示socket,这些文件在磁盘上也没有数据块。UNIX Domain Socket是目前最广泛使用 的IPC机制.如下图:
在这里插入图片描述
注意:stack overflow 无穷递归或者定义的极大数组都可能导致操作系统为程序预留的栈空间耗尽 程序崩溃(段错误)

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只嵌入式爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值