Linux文件IO(九)-原子操作与竞争冒险

embedded/2024/9/24 16:14:56/

Linux 是一个多任务、多进程操作系统,系统中往往运行着多个不同的进程、任务,多个不同的进程就有可能对同一个文件进行 IO 操作,此时该文件便是它们的共享资源,它们共同操作着同一份文件;操作系统级编程不同于大家以前接触的裸机编程,裸机程序中不存在进程、多任务这种概念,而在 Linux 系统中,我们必须要留意到多进程环境下可能会导致的竞争冒险。

竞争冒险简介

本小节给大家竞争冒险这个概念,如果学习过 Linux 驱动开发的读者对这些概念应该并不陌生,也就意味着竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。

假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,此时,各数据结构之间的关系如图 3.8.2 所示。每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。假定此时进程 A 处于运行状态,B 未处于等待运行状态,进程 A 调用了 lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处(假设这里是文件末尾),刚好此时进程 A 的时间片耗尽,然后内核切换到了进程 B,进程 B 执行 lseek 函数,也将其对该文件的当前位置偏移量设置为 1500 个字节处(文件末尾)。然后进程 B 调用 write 函数,写入了 100 个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文件当前位置偏移量(1500 字节处)开始写入,此时文件 1500 字节处已经不再是文件末尾了,如果还从 1500字节处写入就会覆盖进程 B 刚才写入到该文件中的数据。

 其上述假设工作流程图如下图所示:

以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。

既然存在竞争状态,那么该如何规避或消除这种状态呢?接下来给大家介绍原子操作。

原子操作

在上一章给大家介绍 open 函数的时候就提到过“原子操作”这个概念了,同样在 Linux 驱动编程中,也有这个概念,相信学习过 Linux 驱动编程开发的读者应该有印象。

从上一小节给大家提到的示例中可知,上述的问题出在逻辑操作“先定位到文件末尾,然后再写”,它使用了两个分开的函数调用,首先使用 lseek 函数将文件当前位置偏移量移动到文件末尾、然后在使用 write函数将数据写入到文件。既然知道了问题所在,那么解决办法就是将这两个操作步骤合并成一个原子操作,所谓原子操作,是有多步操作组成的一个操作,原子操作要么一步也不执行,一旦执行,必须要执行完所有步骤,不可能只执行所有步骤中的一个子集。

(1)O_APPEND 实现原子操作

在上一小节给大家提到的示例中,进程 A 和进程 B 都对同一个文件进行追加写操作,导致进程 A 写入的数据覆盖了进程 B 写入的数据,解决办法就是将“先定位到文件末尾,然后写”这两个步骤组成一个原子操作即可,那如何使其变成一个原子操作呢?答案就是 O_APPEND 标志。前面已经给大家多次提到过了 O_APPEND 标志,但是并没有给大家介绍 O_APPEND 的一个非常重要的作用,那就是实现原子操作。当 open 函数的 flags 参数中包含了 O_APPEND 标志,每次执行 write 写入操作时都会将文件当前写位置偏移量移动到文件末尾,然后再写入数据,这里“移动当前写位置偏移量到文件末尾、写入数据”这两个操作步骤就组成了一个原子操作,加入 O_APPEND 标志后,不管怎么写入数据都会是从文件末尾写,这样就不会导致出现“进程 A 写入的数据覆盖了进程 B 写入的数据”这种情况了。

(2)pread()和 pwrite()

pread()和 pwrite()都是系统调用,与 read()、write()函数的作用一样,用于读取和写入数据。区别在于,pread()和 pwrite()可用于实现原子操作,调用 pread 函数或 pwrite 函数可传入一个位置偏移量 offset 参数,用于指定文件当前读或写的位置偏移量,所以调用 pread 相当于调用 lseek 后再调用 read;同理,调用 pwrite相当于调用 lseek 后再调用 write。所以可知,使用 pread 或 pwrite 函数不需要使用 lseek 来调整当前位置偏移量,并会将“移动当前位置偏移量、读或写”这两步操作组成一个原子操作。

pread、pwrite 函数原型如下所示(可通过"man 2 pread"或"man 2 pwrite"命令来查看):

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

 首先调用这两个函数需要包含头文件<unistd.h>。

函数参数和返回值含义如下:

  • fd、buf、count 参数与 read 或 write 函数意义相同。
  • **offset:**表示当前需要进行读或写的位置偏移量。
  • **返回值:**返回值与 read、write 函数返回值意义一样。

虽然 pread(或 pwrite)函数相当于 lseek 与 pread(或 pwrite)函数的集合,但还是有下列区别:

  • 调用 pread 函数时,无法中断其定位和读操作(也就是原子操作);
  • 不更新文件表中的当前位置偏移量。

关于第二点我们可以编写一个简单地代码进行测试,测试代码如下所示:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(void)
{unsigned char buffer[100];int fd;int ret;/* 打开文件 test_file */fd = open("./test_file", O_RDWR);if (-1 == fd) {perror("open error");exit(-1);}/* 使用 pread 函数读取数据(从偏移文件头 1024 字节处开始读取) */ret = pread(fd, buffer, sizeof(buffer), 1024);if (-1 == ret) {perror("pread error");goto err;}/* 获取当前位置偏移量 */ret = lseek(fd, 0, SEEK_CUR);if (-1 == ret) {perror("lseek error");goto err;}printf("Current Offset: %d\n", ret);ret = 0;err:/* 关闭文件 */close(fd);exit(ret);
}

 在当前目录下存在一个文件 test_file,上述代码中会打开 test_file 文件,然后直接使用 pread 函数读取100 个字节数据,从偏移文件头部 1024 字节处,读取完成之后再使用 lseek 函数获取到文件当前位置偏移量,并将其打印出来。假如 pread 函数会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是1024 + 100 = 1124;如果不会改变文件表中记录的当前位置偏移量,则打印出来的数据应该是 0,接下来编译代码测试:

从上图中可知,打印出来的数据为 0,正如前面所介绍那样,pread 函数确实不会改变文件表中记录的当前位置偏移量;同理,pwrite 函数也是如此,大家可以把 pread 换成 pwrite 函数再次进行测试,不出意外,打印出来的数据依然是 0。

如果把 pread 函数换成 read(或 write)函数,那么打印出来的数据就是 100 了,因为读取了 100 个字节数据,相应的当前位置偏移量会向后移动 100 个字节。

(3)创建一个文件

前面给大家介绍 open 函数的 O_EXCL 标志的时候,也提到了原子操作,其中介绍到:O_EXCL 可以用于测试一个文件是否存在,如果不存在则创建此文件,如果存在则返回错误,这使得测试和创建两者成为一个原子操作。接下来给大家,创建文件中存在着的一个竞争状态。

假设有这么一个情况:进程 A 和进程 B 都要去打开同一个文件、并且此文件还不存在。进程 A 当前正在运行状态、进程 B 处于等待状态,进程 A 首先调用 open("./file", O_RDWR)函数尝试去打开文件,结果返回错误,也就是调用 open 失败;接着进程 A 时间片耗尽、进程 B 运行,同样进程 B 调用 open("./file",O_RDWR)尝试打开文件,结果也失败,接着进程 B 再次调用 open("./file", O_RDWR | O_CREAT, ...)创建此文件,这一次 open 执行成功,文件创建成功;接着进程 B 时间片耗尽、进程 A 继续运行,进程 A 也调用open("./file", O_RDWR | O_CREAT, ...)创建文件,函数执行成功,如下图所示:

从上面的示例可知,进程 A 和进程 B 都会创建出同一个文件,同一个文件被创建两次这是不允许的,那如何规避这样的问题呢?那就是通过使用 O_EXCL 标志,当 open 函数中同时指定了 O_EXCL 和O_CREAT 标志,如果要打开的文件已经存在,则 open 返回错误;如果指定的文件不存在,则创建这个文件,这里就提供了一种机制,保证进程是打开文件的创建者,将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作,有了原子操作,就保证不会出现图 中所示的情况。


http://www.ppmy.cn/embedded/116165.html

相关文章

武汉正向科技 格雷母线定位系统生产厂家

为了适应机车无人化项目对地址高精度的要求&#xff0c;我们推出了高精度格雷母线&#xff0c;根据地址的检测原理&#xff0c;地址精度取决于格雷母线最小交叉环的精度&#xff0c;传统的格雷母线内胆采用柔性泡沫内胆&#xff08;图片1&#xff09;&#xff0c;格雷母线最小交…

【技术解析】wx.request 封装:优化小程序网络请求的最佳实践

在当今的小程序开发领域&#xff0c;网络请求是构建动态应用的核心。微信小程序提供的 wx.request API 虽然强大&#xff0c;但在面对复杂业务逻辑时&#xff0c;其直接使用方式可能会带来一系列问题。本文将深入探讨封装 wx.request 的必要性&#xff0c;并提供一套实用的封装…

python 人工智能,在机器学习回归任务中,模型的评估通常mse ,rmse 等等

在机器学习回归任务中&#xff0c;模型的评估通常涉及以下几个关键指标&#xff1a; 1. **均方误差 (MSE)**: 衡量预测值与真实值之间差的平方的平均数。MSE 越小&#xff0c;表示模型的预测准确度越高。计算公式为&#xff1a; \[ \text{MSE} \frac{1}{n} \sum_{i1}^{n} (y_…

网页设计html心得

一&#xff0c;认识网页 说到网页&#xff0c;其实大家并不陌生 1.1网页究竟是什么&#xff1f; 网页主要由文字、图像和超链接等元素构成。当然&#xff0c;除了这些元素&#xff0c;网页中还可以包含音频、视频以及Flash等。 1.2网页是如何形成的呢&#xff1f; 1.特殊的…

STM32之串口通信

什么是串口 串行通信接口&#xff1a;指按位发送和接收的接口&#xff0c;如RS232/422/485 RS232电平和COMS/TTL电平对比 RS232电平&#xff1a;逻辑1&#xff1a;-15V ~ -3V 逻辑0:3V ~ 15V CMOS电平: 逻辑1&#xff1a;3.3V 逻辑0&#xff1a;0V &#xff08;STM32使用&am…

【machine learning-13-线性回归的向量化】

向量化 向量化简洁并行计算 向量化 线性回归的向量化表示如下&#xff0c;其中w 和 x 都分别加了箭头表示这是个向量&#xff0c;后续不加也可以表示为向量&#xff0c;w和x点乘加上b&#xff0c;就构成了多元线性回归的表达方式&#xff0c;如下&#xff1a; 那么究竟为什么…

c++难点核心笔记(一)

文章目录 前言C的应用领域 核心编程内存分区模型1.程序运行前2.程序运行后3.new操作符引用 函数1.概述和函数原型2.函数的定义和参数3.使用函数处理不同类型的数据4.微处理器如何处理函数调用函数的分文件编写 指针和引用什么是指针动态内存分配使用指针时常犯的编程错误指针编…

5G 扬帆新质跃,技术蝶变开新篇-第七届“绽放杯”5G应用征集大赛 5G应用融合技术专题赛圆满收官

2024年9月13日,由中国信息通信研究院、中国电信集团有限公司、中国移动通信集团有限公司、中国联合网络通信集团有限公司主办,5G应用产业方阵承办的第七届“绽放杯”5G应用征集大赛  5G应用融合技术专题赛决赛在深圳成功举办。 本次专题赛以“5G扬帆新质跃,技术蝶变开新篇”为…