嵌入式Linux系统编程 — 1.4 原子操作与竞争冒险

目录

1 竞争冒险

1.1 竞争冒险由来

1.2 竞争冒险理解

2 原子操作 

2.1 O_APPEND 实现原子操作

2.2 pread()和 pwrite()

2.3 O_EXCL 标志创建文件


1 竞争冒险

1.1 竞争冒险由来

Linux 是一个支持多任务和多用户同时运行的操作系统,它允许多个进程同时执行。在这种环境下,可能会有多个进程同时对同一个文件执行输入输出(IO)操作,使得该文件成为这些进程共享的资源。与直接在硬件上运行的裸机编程不同,Linux 操作系统提供了进程管理和多任务处理的功能。在裸机编程中,通常不会有进程和多任务的概念,然而在 Linux 系统中,需要特别注意在多进程环境中可能发生的竞争冒险,因为不同的进程可能会同时试图访问和修改共享资源,这可能导致数据不一致或程序行为异常。

1.2 竞争冒险理解

下面代码是一个典型的多线程竞争冒险场景。程序中定义了一个共享的整型变量shared_counter,并将其声明为volatile以防止编译器进行优化。increment函数是线程执行的主体,它在每个线程中运行1000次,每次循环中调用usleep(10)来模拟CPU密集型操作,然后对shared_counter进行递增。在main函数中,创建了10个线程,每个线程都执行increment函数。程序使用pthread_create来创建线程,并通过pthread_join等待所有线程完成。最后,程序打印出预期的shared_counter值(即NUM_THREADS * 1000),以及实际的shared_counter值。由于shared_counter的递增操作不是原子的,并且没有使用同步机制来防止多个线程同时修改它,因此实际值可能会小于预期值,展示了竞争冒险的效果。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h> // 引入usleep函数

// 共享资源
volatile int shared_counter = 0; // 使用volatile关键字防止编译器优化

// 线程函数
void* increment(void* arg) {
    for (int i = 0; i < 1000; ++i) {
        // 模拟一些CPU密集型的操作
        usleep(10); // 让线程暂停一小段时间,增加线程调度的不确定性

        // 非原子操作,可能导致竞争冒险
        shared_counter++;
    }
    return NULL;
}

int main() {
    const int NUM_THREADS = 10; // 增加线程数量
    pthread_t threads[NUM_THREADS];

    // 创建多个线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        if (pthread_create(&threads[i], NULL, increment, NULL) != 0) {
            perror("Error creating thread");
            return 1;
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }

    // 输出共享变量的值
    printf("Expected value: %d\nActual value: %d\n", NUM_THREADS * 1000, shared_counter);

    return 0;
}

在这个修改后的程序中,每个线程只执行1000次递增操作,同时在每次递增操作前后添加了usleep(10),以模拟一些CPU密集型的操作并增加线程调度的不确定性。由于竞争冒险,shared_counter的实际值很可能小于预期的值(在这个例子中是10000)。通过运行这个程序多次,你应该能够观察到不同的输出值,这些值通常小于预期值,从而演示了竞争冒险的影响。运行的结果如下:

2 原子操作 

在Linux系统中,原子操作是一种确保在多线程环境下对共享数据进行安全访问的技术。原子操作的关键在于它们是不可分割的最小执行单元,即在执行过程中不会被其他线程中断。这使得原子操作成为避免竞争冒险(race conditions)和确保数据一致性的重要工具。

使用原子操作时,程序员不需要手动管理锁,因为原子操作本身提供了必要的同步。这简化了编程模型,并有助于避免死锁和优先级反转等并发问题。

2.1 O_APPEND 实现原子操作

O_APPEND 是一个在 Linux 系统中打开文件时使用的文件状态标志,它确保所有写操作都追加到文件末尾。虽然 O_APPEND 本身并不直接实现原子操作,但它确实保证了写操作的原子性,因为它防止了写入位置在多个进程或线程之间的竞争。

当多个进程或线程打开同一个文件并指定 O_APPEND 标志时,内核会负责管理文件写入指针,确保每个写操作都从文件的当前末尾开始。这意味着即使多个进程或线程同时写入同一个文件,每个写入操作也会被追加到文件的末尾,而不会相互干扰。

这里是一个简单的例子,演示如何使用 O_APPEND 来确保写操作的原子性:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("example.txt", O_WRONLY | O_APPEND);
    if (fd == -1) {
        perror("Failed to open file");
        return 1;
    }

    const char* message = "This is a test message\n";
    if (write(fd, message, strlen(message)) == -1) {
        perror("Failed to write to file");
        close(fd);
        return 1;
    }

    close(fd);
    return 0;
}

在这个例子中,使用 open 函数打开一个名为 "example.txt" 的文件,并指定 O_WRONLY(只写模式)和 O_APPEND 标志。这确保了所有的 write 调用都会将数据追加到文件末尾。即使多个进程同时运行这段代码,每个进程的写操作也会被顺序地追加到文件末尾,而不会覆盖其他进程的写入内容。

需要注意的是,虽然 O_APPEND 确保了写入操作的顺序性,但它并不保证数据的完整性或写入操作的原子性。例如,如果一个写操作被分割成多个部分,O_APPEND 不能保证这些部分会连续地写入文件。对于需要确保数据完整性的场景,可能需要使用其他同步机制,如互斥锁(mutexes)或文件锁(file locking)。

2.2 pread()和 pwrite()

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

(1)pread()函数

pread() 用于从文件中读取数据,而不改变文件的当前读指针。这意味着每次调用 pread(),都会从指定的文件位置开始读取数据,而不影响其他读/写操作。

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

参数说明:

  • fd: 文件描述符,表示要读取的文件。
  • buf: 指向一个缓冲区的指针,用于存储从文件中读取的数据。
  • count: 要读取的字节数。
  • offset: 文件中的偏移量,从这个位置开始读取数据。

(2)pwrite()函数

pwrite()pread() 相对应,用于在文件中的指定位置写入数据,而不改变文件的当前写指针。

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

参数说明:

  • fd: 文件描述符,表示要写入的文件。
  • buf: 指向要写入的数据的缓冲区的指针。
  • count: 要写入的字节数。
  • offset: 文件中的偏移量,从这个位置开始写入数据。

 (3)示例代码

下面的示例代码展示了如何使用 pread()pwrite() 函数:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int fd;
    const char *data = "Sample data";
    char buffer[20];
    ssize_t bytes_read, bytes_written;

    // 创建或打开文件
    fd = open("testfile.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 使用 pwrite() 写入数据
    bytes_written = pwrite(fd, data, strlen(data), 0);
    if (bytes_written == -1) {
        perror("pwrite");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 移动文件指针到文件中间的某个位置
    if (lseek(fd, 5, SEEK_SET) == -1) {
        perror("lseek");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 使用 pread() 从指定位置读取数据
    bytes_read = pread(fd, buffer, sizeof(buffer), 5);
    if (bytes_read == -1) {
        perror("pread");
        close(fd);
        exit(EXIT_FAILURE);
    }
    buffer[bytes_read] = '\0'; // 确保字符串终止

    // 打印读取的数据
    printf("Read data: %s\n", buffer);

    // 关闭文件
    close(fd);

    return 0;
}

代码中首先打开(或创建)一个名为 testfile.txt 的文件用于读写。然后,我们使用 pwrite() 在文件开头写入一个字符串。接着,我们使用 lseek() 将文件指针移动到文件的第5个字节,然后使用 pread() 从这个位置读取数据到缓冲区中。最后,我们打印出读取的数据,并关闭文件。运行的结果如下:

2.3 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 返回错误;如果指定的文件不存在,则创建这个文件,这里就提供了一种机制,保证进程是打开文件的创建者,将“判断文件是否存在、创建文件”这两个步骤合成为一个原子操作。
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/679591.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

京东笔试-校招

2022京东数据分析笔试&#xff08;0821&#xff09; 一、选择题&#xff1a;30道 1.解决数据不平衡的方法主要有&#xff08;pca&#xff1f;&#xff09; 2.等频&#xff08;等宽&#xff09;划分问题 3.参数估计&#xff1a;矩估计与极大似然估计的用法&#xff0c;问题分…

kill 不管用时,类型为C

当使用nvidia-smi时看到类型为C的进程时&#xff0c;使用 kill -9 PID&#xff0c;却不管用&#xff0c;这时需要先使用如下命令&#xff0c;找出运行的脚本对应的所有PID: ps -aux | grep train.py 接着就会把train.py对应运行的进程全部展示出来&#xff1a; 接着就是使用 …

25、DHCP FTP

DHCP 动态主机配置协议 DHCP定义&#xff1a; 服务器配置好了地址池 192.168.233.10 192.168.233.20 客户端从地址池当中随机获取一个ip地址&#xff0c;ip地址会发生变化&#xff0c;使用服务端提供的ip地址&#xff0c;时间限制&#xff0c;重启之后也会更换。 DHCP优点&a…

android-JNI

1.2【静态库】的特点&#xff1a; &#xff08;.a&#xff09; ①静态库对函数库的链接是在编译期完成的。执行期间代码装载速度快。 ②使可执行文件变大&#xff0c;浪费空间和资源&#xff08;占空间&#xff09;。 ③对程序的更新、部署与发布不方便&#xff0c;需要全量更新…

【TB作品】msp430g2553单片机,OLED,PCF8591,ADC,DAC

硬件 OLED PCF8591 /** OLED* VCC GND* SCL接P2^0* SDA接P2^1*//** PCF8591* VCC GND* SCL接P1^4* SDA接P1^5*//* 板子上按键 P1.3 *//* 单片机ADC输入引脚 P1.1 *//* 说明&#xff1a;将PCF8591的DAC输出接到单片机ADC输入引脚 P1.1&#xff0c;单片机采集电压并显示 */功能…

Angular 由一个bug说起之六:字体预加载

浏览器在加载一个页面时&#xff0c;会解析网页中的html和css&#xff0c;并开始加载字体文件。字体文件可以通过css中的font-face规则指定&#xff0c;并使用url()函数指定字体文件的路径。 比如下面这样: css font-face {font-family: MyFont;src: url(path/to/font.woff2…

MySQL 关键特性一:插入缓冲、双写缓冲

前言 ​ 本文主要介绍 mysql 的几大特性之几&#xff0c;如&#xff1a;双写缓冲和插入缓存。 双写缓冲 基本概念 ​ 双写缓冲&#xff08;doublewrite buffer&#xff09;是MySQL/InnoDB中用于支持原子页面更新的一种机制。在传统的数据库系统中&#xff0c;为了保证数据的…

C++ XML文件和解析

XML&#xff08;可扩展标记语言&#xff09;是一种用于存储和传输数据的标记语言。它具有自描述性和平台无关性的特点。XML 文档的格式主要由一组嵌套的元素和属性构成&#xff0c;结构清晰&#xff0c;易于理解和解析。 XML 文档的基本格式 一个 XML 文档通常包括以下部分&a…

React 中的 ForwardRef的使用

React 中的 forwardRef Hooks 是指将子组件的 Dom 节点暴露给给父组件&#xff0c;在 React 中如果想要访问 Dom 节点是通过 useRef 这个 hooks&#xff0c;而 forwardHook 在 useRef 做了扩展。useRef 是当前组件中间中的节点&#xff0c;而 forwardRef 相当于做了一层封装将父…

屏幕录制工具分享6款,附上详细电脑录屏教程(2024全新)

当你即将参加一个重要的在线会议或一堂关键的直播课&#xff0c;但又担心错过关键点或无法及时做笔记时&#xff0c;屏幕录制无疑是最好的方法之一。屏幕录制是一项非常有价值的技能&#xff0c;它能让你出于各种目的捕捉屏幕上的活动。无论你的目的是创建教程、演示软件功能、…

重学java 62.IO流 字节流 ③ 字节输入流

告别这种事情&#xff0c;没有道理可讲 —— 24.6.4 一、字节输入流的介绍以及方法的使用 1.概述: 字节输入流 InputStream,是一个抽象类 子类:FileInputStream 2.作用: 读数据,将数据从硬盘上读到内存中来 3.构造: FileInputstream(File file) FileInputstream(String path…

容器中运行ifconfig提示bash: ifconfig: command not found【笔记】

容器中运行ifconfig提示bash: ifconfig: command not found 这个问题是因为在容器中没有安装ifconfig命令。 在容器中安装ifconfig命令&#xff0c;可以使用以下命令&#xff1a; 对于基于Debian/Ubuntu的容器&#xff0c;使用以下命令&#xff1a; apt-get update apt-get …

Spring Boot 使用自定义注解和自定义线程池实现异步日志记录

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

leetcode739 每日温度

题目 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 示例 输入: tempe…

探秘Android系统启动的神秘面纱

系统启动过程对于任何操作系统来说都是一个非常关键的环节&#xff0c;Android作为移动设备领域中占据主导地位的操作系统&#xff0c;其启动过程也是个值得深入研究的重点话题。本文将为您解开Android启动过程的神秘面纱&#xff0c;详细剖析其中的每一个步骤&#xff0c;并通…

vue 将echart 下载为base64图片

1 echart是页面的子组件&#xff0c; 2 页面有多个echart 3 将多个echart下载为base64图片 // 子组件 echart&#xff0c;要保存echartconst chart this.$echarts.init(this.$refs.chart, light) this.chartData chart; //保存数据&#xff0c;供父组件alarmReport调用(th…

专业130+总分400+四川大学951信号与系统考研经验川大电子信息与通信工程,真题,大纲,参考书。教材。

今年四川大学951信号与系统专业课130&#xff08;据我所知没有140以上的今年&#xff09;&#xff0c;总分400&#xff0c;顺利上岸川大&#xff0c;回顾一下自己这一年的复习&#xff0c;希望自己的经历可以对大家复习有所借鉴&#xff0c;也是对自己的考研画上句话。专业课&a…

重庆耶非凡科技业务大盘点:这些领域你都了解吗?

重庆耶非凡科技有限公司&#xff0c;这家位于重庆市经开区的企业&#xff0c;以其独特的业务模式和专业的技术实力&#xff0c;赢得了业界的广泛认可。它的主要业务涵盖了选品师项目和人力RPO项目两大领域。 首先&#xff0c;我们不得不提的是耶非凡科技的选品师项目 在当今消费…

算法004:盛水最多的容器

这道题比较简单&#xff0c;使用双指针。 要求的是最大面积&#xff0c;对于一个水桶&#xff08;水杯来说&#xff09;&#xff0c;面积的算法是固定的&#xff0c;就是底乘以高。 在这个题中&#xff0c;我们把左边的位置设为left&#xff0c;右边的位置设为right&#xff…

vue3 + echarts 二次开发百分比饼图

效果图&#xff1a; 安装 pnpm i echarts 公共模块组件 <divclass"pie"ref"percent"style"width: 100%; height: calc(100% - 48px)"></div> import { ref, onMounted } from vue import * as echarts from echarts const prop…