锁,原子操作,共享内存,CPU亲缘性总结

发布时间:2024年01月14日

互斥锁和自旋锁

在Linux中,自旋锁和互斥锁都是用于线程同步的机制,但它们有不同的特性和适用场景。

互斥锁(Mutex)

互斥锁是一种常用的线程同步机制,它确保在任何时刻只有一个线程可以访问共享资源。当一个线程锁定了互斥锁(通过调用pthread_mutex_lock),其他线程必须等待该线程释放锁之后才能获得对共享资源的访问权限。互斥锁使用系统调用来进行线程阻塞和唤醒,因此在资源竞争较为激烈的情况下,会引入一定的性能开销。

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 在代码中使用互斥锁
pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);

自旋锁(Spinlock)

自旋锁是另一种线程同步机制,它采用忙等待的方式,即线程在获取锁失败时不会被挂起,而是会一直循环检查锁是否可用。自旋锁适用于临界区很小且持有时间短暂的情况,因为长时间的自旋会占用处理器资源。

#include <pthread.h>

pthread_spinlock_t spinlock;

// 初始化自旋锁
pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);

// 在代码中使用自旋锁
pthread_spin_lock(&spinlock);
// 访问共享资源
pthread_spin_unlock(&spinlock);

总的来说,互斥锁适用于临界区较大或者持有时间较长的情况,而自旋锁适用于临界区较小且持有时间短暂的情况。选择合适的锁取决于具体的应用场景和性能要求。

原子操作

CAS(Compare and Swap)是一种原子操作,用于实现多线程环境下的无锁同步。它可以解决并发环境下的数据竞争问题。

CAS操作包含三个参数:内存地址(或变量)、期望值和新值。它的执行过程如下:

  1. 比较内存地址中的值与期望值是否相等。
  2. 如果相等,则将内存地址中的值更新为新值。
  3. 如果不相等,则说明其他线程已经修改了内存地址中的值,CAS操作失败。

CAS操作是原子的,意味着它在执行的过程中不会被其他线程中断。如果CAS操作失败,可以根据需要进行重试。

在C/C++中,可以使用一些特定的函数或指令来实现CAS操作,例如:

  • C++11引入了std::atomic模板,提供了原子操作的支持,包括std::atomic_compare_exchange_strongstd::atomic_compare_exchange_weak等函数。
  • GCC提供了一组原子操作的内建函数,如__sync_bool_compare_and_swap__sync_val_compare_and_swap等。
  • 在x86架构上,可以使用cmpxchg指令来执行CAS操作。

以下是一个使用GCC内建函数__sync_bool_compare_and_swap实现CAS操作的示例:

#include <stdbool.h>

bool compare_and_swap(int *ptr, int expected, int new_value) {
    return __sync_bool_compare_and_swap(ptr, expected, new_value);
}

在上面的示例中,compare_and_swap函数接受一个指向变量的指针ptr,并使用__sync_bool_compare_and_swap函数进行CAS操作。如果CAS操作成功(即内存地址中的值与期望值相等),函数将返回true,否则返回false

需要注意的是,CAS操作并不适用于所有情况,特别是在高度竞争的情况下,可能会导致自旋等待的开销较大。因此,在使用CAS操作时,需要根据具体情况进行评估和测试。

CPU亲缘性

CPU的亲缘性(Affinity)是指将特定的任务或线程绑定到特定的CPU核心上执行的能力。这可以通过操作系统提供的API来实现,允许开发者或系统管理员指定特定的CPU核心来处理特定的工作。即将某个进程绑定到某CPU上,这个进程就只在这个CPU来运行。

亲缘性的使用有以下几个方面的好处:

  1. 性能优化: 通过将任务绑定到特定的CPU核心上,可以避免在不同核心之间进行上下文切换,从而减少缓存失效和提高局部性,有助于提高程序的整体性能。这对于对性能敏感的应用程序,如高性能计算、实时系统等非常重要。

  2. 可预测性: 在实时系统或对任务执行时间敏感的应用中,通过亲缘性可以更好地控制任务的执行时间,提高系统的可预测性。这对于确保任务在特定时间内完成非常关键。

  3. 资源隔离: 在多任务环境中,通过亲缘性可以将任务隔离在特定的核心上,防止它们相互干扰。这有助于提高系统的稳定性和可靠性。

  4. 降低功耗: 在移动设备或节能要求较高的环境中,通过将任务绑定到部分CPU核心上,可以使系统在需要更高性能时激活更多核心,而在轻负载时只使用较少的核心,从而降低功耗。

在使用CPU亲缘性时,需要注意以下事项:

  • 负载均衡: 过度使用亲缘性可能导致不均衡的负载分布,一些核心可能被过度使用,而其他核心处于空闲状态。在一些情况下,动态调整亲缘性可能更有利于负载均衡。

  • 可移植性: 使用CPU亲缘性可能使程序在不同的硬件平台上表现不同。在需要考虑跨平台兼容性的情况下,需要权衡亲缘性带来的性能提升和可移植性之间的关系。

总的来说,CPU的亲缘性是一项有助于优化性能、提高可预测性和资源隔离的技术,但需要根据具体的应用场景和硬件环境来权衡使用的利弊。

示例:

#define _GNU_SOURCE

#include <stdio.h>

#include <unistd.h>

#include <sched.h>

#include <sys/syscall.h>

void process_affinity(int num)
{
    pid_t self_id = syscall(__NR_gettid);

    cpu_set_t mask;

    CPU_ZERO(&mask);

    CPU_SET(self_id % num, &mask);

    sched_setaffinity(self_id,sizeof(mask),&mask);

    while(1);    
}

int main()
{

    int num = sysconf(_SC_NPROCESSORS_CONF);

    printf("core num: %d \n",num);

    int i = 0;
    int pid;

    for (i = 0; i < num/2/2;i++)
    {
        pid = fork();
        if(pid <=0 ) //子进程退出
        {
            break;
        } 
        // printf("d\n");
    }
    // 子进程
    if(pid == 0)
    {
        process_affinity(num);
    }
    
    printf("%d\n",pid);

    while(1) usleep(1);
    return 0;
}

共享内存 mmap

一个大文件,快速读写?

使用mmap映射到内存中(一种共享内存的方式)

零拷贝就是说CPU不参与拷贝的过程

在传统IO操作方式中,CPU的参与主要涉及数据的复制和处理过程。以下是CPU在传统IO中的具体用处的总结:

  1. 内存复制: 在传统IO中,数据需要在内核缓冲区和用户程序之间进行复制。这包括从内核缓冲区复制到用户程序的地址空间(读取数据)和从用户程序的地址空间复制到内核缓冲区(写入数据)。这个复制操作需要CPU执行相应的指令,涉及寄存器和内存的读写。

  2. 系统调用: 用户程序通过系统调用与操作系统内核进行通信,请求IO操作。这涉及到CPU执行指令切换到内核模式,并执行相应的系统调用处理代码。CPU负责管理程序的状态切换。

  3. 中断处理: IO操作可能引发中断,例如缺页中断。当需要读取的数据不在内存中时,CPU接收到缺页中断,切换到内核模式,内核负责将相应的数据加载到内存中。这是CPU在IO操作中响应事件的一部分。

  4. 数据处理: 在用户程序处理数据时,CPU执行相关的指令来进行计算、逻辑处理等操作。这包括对从内核缓冲区复制过来的数据进行处理。

  5. IO控制器通信: CPU通过与IO控制器进行通信,协调数据在内存和外部设备之间的传输。这包括将数据传输到存储设备或从存储设备读取数据。

总体而言,CPU在传统IO中的参与是多方面的,涉及数据的复制、处理、系统调用、中断处理和与IO设备的通信。而mmap方式通过文件映射到虚拟地址空间,减少了显式的数据复制过程,从而提高了IO效率。

mmap使用的零拷贝技术实际上仍然会进行数据复制,但与传统IO方式相比,这个复制是在用户空间和内核空间之间进行的,而不涉及CPU的直接参与。

具体来说,mmap的零拷贝涉及以下步骤:

  1. 文件映射: 使用mmap系统调用,将文件映射到进程的虚拟地址空间。这意味着文件的内容在内核空间和用户空间之间建立了一种映射关系。

  2. 用户程序直接访问: 用户程序可以直接在虚拟地址空间中访问文件的内容,就像访问普通的内存一样。这消除了传统IO方式中需要在内核缓冲区和用户空间之间复制数据的步骤。

  3. 写入数据: 对于写入操作,数据首先被写入到进程的地址空间,而不是通过内核缓冲区。这样就避免了在传统IO方式中需要在用户空间和内核缓冲区之间复制的过程。

  4. 延迟复制: 实际的数据复制是由操作系统在必要时延迟执行的。这意味着如果两个进程共享同一文件映射,数据实际上可能不会被复制,而是在需要时共享相同的物理页面。

虽然mmap使用了零拷贝技术,但要注意的是,当实际发生数据写入时,操作系统可能仍然需要在内核中进行一些复制操作,但这是在用户空间和内核空间之间发生的,而不涉及到CPU在用户程序和内核之间直接进行数据复制。这可以提高IO的效率,特别是对于大量数据的读写。

总结:
mmap建立了文件在磁盘和内核空间之间的映射,而用户空间则直接映射到了这个内核空间,从而实现了零拷贝。具体来说:

  1. 文件到内核空间的映射: 使用mmap系统调用,将文件映射到进程的地址空间。这时,文件的内容在内核空间中也有一份映射。

  2. 用户空间到内核空间的映射: 用户程序的虚拟地址空间直接映射到了内核空间中相应文件的映射。这样,用户程序就可以通过直接访问内存来读写文件,而不需要在用户空间和内核空间之间进行显式的数据复制。

  3. 零拷贝写入: 对于写入操作,数据首先被写入到用户程序的虚拟地址空间,然后由操作系统在需要时延迟地将数据复制到内核空间。这避免了传统IO方式中在用户空间和内核缓冲区之间复制数据的开销。

总体而言,mmap建立了文件、内核空间和用户空间之间的映射,使得用户程序可以直接访问文件的内容,减少了传统IO方式中涉及CPU的显式数据复制。这带来了更高的IO效率,尤其是在处理大量数据时。

示例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("./affinity.c", O_RDONLY);

    if (fd == -1) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    unsigned char *addr = (unsigned char *)mmap(NULL, 779, PROT_READ, MAP_SHARED, fd, 0);

    if (addr == MAP_FAILED) {
        perror("Error mapping file");
        close(fd);
        return EXIT_FAILURE;
    }

    // Print the content byte by byte
    for (int i = 0; i < 779; ++i) {
        printf("%c", addr[i]);
    }

    if (munmap(addr, 779) == -1) {
        perror("Error unmapping file");
        close(fd);
        return EXIT_FAILURE;
    }

    close(fd);

    return EXIT_SUCCESS;
}

文章来源:https://blog.csdn.net/axyw_/article/details/135563506
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。