linux 内核同步互斥技术之每处理器变量

在多处理器系统中,每处理器变量为每个处理器生成一个变量的副本,每个处理器访问自己的副本,从而避免了处理器之间的互斥和处理器缓存之间的同步,提高了程序的执行速度。

每处理器变量分为静态和动态两种。

静态每处理器变量

使用宏“DEFINE_PER_CPU(type, name)”定义普通的静态每处理器变量,使用宏“ DECLARE_PER_CPU(type, name)”声明普通的静态每处理器变量。
把宏“DEFINE_PER_CPU(type, name)”展开以后是:
__attribute__((section(".data..percpu"))) __typeof__(type) name

可以看出,普通的静态每处理器变量存放在“ .data..percpu”节中。

定义静态每处理器变量的其他变体如下。
(1)使用宏“DEFINE_PER_CPU_FIRST(type, name)”定义必须在每处理器变量集合中最先出现的每处理器变量。
(2)使用宏“DEFINE_PER_CPU_SHARED_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,仅仅在 SMP 系统中需要和处理器缓存行对齐。
(3)使用宏“DEFINE_PER_CPU_ALIGNED(type, name)”定义和处理器缓存行对齐的每处理器变量,不管是不是 SMP 系统,都需要和处理器缓存行对齐。
(4)使用宏“DEFINE_PER_CPU_PAGE_ALIGNED(type, name)”定义和页长度对齐的每处理器变量。
(5)使用宏“DEFINE_PER_CPU_READ_MOSTLY(type, name)”定义以读为主的每处理器变量。

如果想要静态每处理器变量可以被其他内核模块引用,需要导出到符号表:
(1)如果允许任何内核模块引用,使用宏“EXPORT_PER_CPU_SYMBOL(var)”把静态每处理器变量导出到符号表。
(2)如果只允许使用 GPL 许可的内核模块引用,使用宏“EXPORT_PER_CPU_SYMBOL_GPL(var)”把静态每处理器变量导出到符号表。
要在模块中访问这样的一个变量,应该这样声明:
DECLARE_PER_CPU(type,name);


静态每处理器变量的存储
以arch/i386/kernel/vmlinux.lds文件为例
……
/*will be free after init*/
.=ALIGN(4096);
__init_begin=.;
/*省略*/
.ALIGN(32);
__per_cpu_start=.;
.data.percpu:{*(.data.percpu)}
__per_cpu_end=.;
.=ALIGN(4096);
__init_end=.;
/*freed after init ends here*/
……

这说明__per_cpu_start和__per_cpu_end标识.data.percpu这个section的开头和结尾

并且,整个.data.percpu这个section都在__init_begin和__init_end之间,
也就是说,该section所占内存会在系统启动后释放(free)掉

因为有
#define DEFINE_PER_CPU(type, name) \
__attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name

所以
static DEFINE_PER_CPU(struct runqueue, runqueues);
会扩展成
__attribute__((__section__(".data.percpu"))) __typeof__(struct runqueue)per_cpu__runqueues;
也就是在.data.percpu这个section中定义了一个变量per_cpu__runqueues,
其类型是struct runqueue。事实上,这里所谓的变量per_cpu__runqueues,
其实就是一个偏移量,标识该变量的地址。

其次,系统启动后,在start_kernel()中会调用如下函数
unsigned long __per_cpu_offset[NR_CPUS];

static void __init setup_per_cpu_areas(void)
{

    unsigned long size, i;
    char *ptr;
    /* Created by linker magic */
    extern char __per_cpu_start[], __per_cpu_end[];


    /* Copy section for each CPU (we discard the original) */
    size = ALIGN(__per_cpu_end - __per_cpu_start, SMP_CACHE_BYTES);
#ifdef CONFIG_MODULES
    if (size < PERCPU_ENOUGH_ROOM)
        size = PERCPU_ENOUGH_ROOM;

#endif

    ptr = alloc_bootmem(size * NR_CPUS);

    for (i = 0; i < NR_CPUS; i++, ptr += size) {
        __per_cpu_offset[i] = ptr - __per_cpu_start;
        memcpy(ptr, __per_cpu_start, __per_cpu_end - __per_cpu_start);
    }
}
在该函数中,为每个CPU分配一段专有数据区,并将.data.percpu中的数据拷贝到其中,每个CPU各有一份。由于数据从__per_cpu_start处转移到各CPU自己的专有数据区中了,因此存取其中的变量就不能再用原先的值了,比如存取per_cpu__runqueues就不能再用per_cpu__runqueues了,需要做一个偏移量的调整,即需要加上各CPU自己的专有数据区首地址相对于__per_cpu_start的偏移量。在这里也就是__per_cpu_offset[i],其中CPU i的专有数据区相对于__per_cpu_start的偏移量为__per_cpu_offset[i]。这样,就可以方便地计算专有数据区中各变量的新地址,比如对于per_cpu_runqueues,其新地址即变成per_cpu_runqueues+__per_cpu_offset[i]。

经过这样的处理,.data.percpu这个section在系统初始化后就可以释放了。


动态每处理器变量

为动态每处理器变量分配内存的函数如下。
(1)使用函数__alloc_percpu_gfp 为动态每处理器变量分配内存。
void __percpu *__alloc_percpu_gfp(size_t size, size_t align, gfp_t gfp);
参数 size 是长度,参数 align 是对齐值,参数 gfp 是传给页分配器的分配标志位。

(2)宏 alloc_percpu_gfp(type, gfp)是函数__alloc_percpu_gfp 的简化形式,参数 size 取“sizeof(type)”,参数 align 取“__alignof__(type)”,即数据类型 type 的对齐值。

(3)函数__alloc_percpu 是函数__alloc_percpu_gfp 的简化形式,参数 gfp 取 GFP_KERNEL。
void __percpu *__alloc_percpu(size_t size, size_t align);

(4)宏 alloc_percpu(type)是函数__alloc_percpu 的简化形式,参数 size 取“sizeof(type)”,参数 align 取“ __alignof__(type)”。

最常用的是宏 alloc_percpu(type)。
为每处理器变量分配内存时,返回的虚拟地址是(chunk->base_addr + offset − delta),其中chunk->base_addr是块的基准地址,offset是单元内部的偏移,delta是(pcpu_base_addr − __per_cpu_start),__per_cpu_start是每处理器数据段的起始地址,内核把所有静态每处理器变量放在每处理器数据段,pcpu_base_addr是第一块的基准地址,每处理器内存分配器在初始化的时候把每处理器数据段复制到第一块的每个单元。
问:为每处理器变量分配内存时,返回的虚拟地址为什么要减去delta?
答:因为宏“this_cpu_ptr(ptr)”在计算变量副本的地址时加上了delta,所以分配内存时返回的虚拟地址要提前减去delta。宏“this_cpu_ptr(ptr)”为什么要加上delta?原因是要照顾内核的静态每处理器变量。
所以,使用alloc_percpu接口返回的地址不能直接使用赋值,需要使用per_cpu提供的接口。
使用函数 free_percpu 释放动态每处理器变量的内存。
void free_percpu(void __percpu *__pdata);


访问每处理器变量

宏“this_cpu_ptr(ptr)”用来得到当前处理器的变量副本的地址,宏“get_cpu_var(var)”用来得到当前处理器的变量副本的值。宏“this_cpu_ptr(ptr)”展开以后是:
unsigned long __ptr;
__ptr = (unsigned long) (ptr);
(typeof(ptr)) (__ptr + per_cpu_offset(raw_smp_processor_id()));

可以看出,当前处理器的变量副本的地址等于基准地址加上当前处理器的偏移。
宏“per_cpu_ptr(ptr, cpu)”用来得到指定处理器的变量副本的地址,宏“per_cpu(var, cpu)”用来得到指定处理器的变量副本的值。
宏“get_cpu_ptr(ptr)”禁止内核抢占并且返回当前处理器的变量副本的地址,宏“put_cpu_ptr(ptr)”开启内核抢占,这两个宏成对使用,确保当前进程在内核模式下访问当前处理器的变量副本的时候不会被其他进程抢占。
宏“get_cpu_var(var)”禁止内核抢占并且返回当前处理器的变量副本的值,宏“put_cpu_var(var)”开启内核抢占,这两个宏成对使用,确保当前进程在内核模式下访问当前处理器的变量副本的时候不会被其他进程抢占。

示例

#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/module.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <linux/list.h>
#include <net/inet_hashtables.h>
#include <net/protocol.h>
#include <linux/cpumask.h>

#define err(msg) printk(KERN_ALERT "%s\n", msg)

static int *percpu;

static int main_init(void)
{
    int *p;
    int i;

    percpu = alloc_percpu(int);
    if (!percpu) {
        err("alloc_percpu");
        goto err;
    }

    for_each_possible_cpu(i) {
        p = per_cpu_ptr(percpu, i);
        *p = i;
    }

    for_each_possible_cpu(i) {
        p = per_cpu_ptr(percpu, i);
        printk(KERN_INFO "%d = %d\n", i, *p);
    }

    return 0;
err:
    return -1;
}

static void main_exit(void)
{
    free_percpu(percpu);
}

module_init(main_init);
module_exit(main_exit);

MODULE_LICENSE("GPL");

每处理器计数器

通常使用原子变量作为计数器,在多处理器系统中,如果处理器很多,那么计数器可能成为瓶颈:每次只能有一个处理器修改计数器,其他处理器必须等待。如果访问计数器很频繁,将会严重降低系统性能。
有些计数器,我们不需要时刻知道它们的准确值,计数器的近似值和准确值对我们没有差别。针对这种情况,我们可以使用每处理器计数器加速多处理器系统中计数器的操作。每处理器计数器的设计思想是:计数器有一个总的计数值,每个处理器有一个临时计数值,每个处理器先把计数累加到自己的临时计数值,当临时计数值达到或超过阈值的时候,把临时计数值累加到总的计数值。
每处理器计数器的定义如下:
include/linux/percpu_counter.h
struct percpu_counter {
    raw_spinlock_t lock;
    s64 count;
#ifdef CONFIG_HOTPLUG_CPU
    struct list_head list;  /* All percpu_counters are on a list */
#endif
    s32 __percpu *counters;
};
成员 count 是总的计数值,成员 lock 用来保护总的计数值,成员 counters 指向每处理器变量,每个处理器对应一个临时计数值。
运行时动态初始化每处理器计数器的方法如下:
percpu_counter_init(fbc, value, gfp)
fbc 是每处理器计数器的地址, value 是初始值, gfp 是分配每处理器变量的标志位。
把计数累加到每处理器计数器的函数是:
void percpu_counter_add(struct percpu_counter *fbc, s64 amount)
读取近似计数值的函数如下。
(1) s64 percpu_counter_read(struct percpu_counter *fbc)
可能返回负数。
(2) s64 percpu_counter_read_positive(struct percpu_counter *fbc)
返回值大于或等于 0,如果是负数,返回 0。如果计数值必须大于或等于 0,那么应该使用这个函数。读取准确计数值的函数如下。
(1) s64 percpu_counter_sum(struct percpu_counter *fbc)
可能返回负数。
(2) s64 percpu_counter_sum_positive(struct percpu_counter *fbc)
返回值大于或等于 0,如果是负数,返回 0。如果计数值必须大于或等于 0,那么应该使用这个函数。

销毁每处理器计数器的函数是:
void percpu_counter_destroy(struct percpu_counter *fbc)
函数 percpu_counter_add 的功能是把计数累加到每处理器计数器,其代码如下:
lib/percpu_counter.c
1 static inline void percpu_counter_add(struct percpu_counter *fbc, s64 amount)
2 {
3   __percpu_counter_add(fbc, amount, percpu_counter_batch);
4 }
5
6 void __percpu_counter_add(struct percpu_counter *fbc, s64 amount, s32 batch)
7 {
8   s64 count;
9   
10  preempt_disable();
11  count = __this_cpu_read(*fbc->counters) + amount;
12  if (count >= batch || count <= -batch) {
13      unsigned long flags;
14      raw_spin_lock_irqsave(&fbc->lock, flags);
15      fbc->count += count;
16      __this_cpu_sub(*fbc->counters, count - amount);
17      raw_spin_unlock_irqrestore(&fbc->lock, flags);
18  } else {
19      this_cpu_add(*fbc->counters, amount);
20  }
21  preempt_enable();
22 }
第 10 行代码,禁止内核抢占。
第 11 行代码, count 等于本处理器的临时计数值加上 amount。
第 12 行代码,如果 count 大于或等于阈值,或者小于或等于阈值的相反数,处理如下。
(1)第 14 行代码,申请自旋锁并且禁止本处理器的硬中断。
(2)第 15 行代码,把 count 加到总的计数值。
(3)第 16 行代码,从本处理器的临时计数值减去( count − amount)。
(4)第 17 行代码,释放自旋锁并且恢复本处理器的硬中断状态。
第 18~20 行代码,如果 count 大于阈值的相反数,并且小于阈值,那么把 amount 加到本处理器的临时计数值。
第 21 行代码,开启内核抢占。
全局变量 percpu_counter_batch 是每处理器计数器的阈值,取值是 32 和(处理器数量× 2)的最大值。


 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter开发笔记 —— sqflite插件数据库应用

前言 今天在观阅掘金大佬文章的时候&#xff0c;了解到了该 sqflite 插件&#xff0c;结合官网教程和自己实践&#xff0c;由此总结出该文&#xff0c;希望对大家的学习有帮助&#xff01; 插件详情 Flutter的 SQLite 插件。支持 iOS、Android 和 MacOS。 支持事务和batch模式…

智能优化算法应用:基于入侵杂草算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于入侵杂草算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于入侵杂草算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.入侵杂草算法4.实验参数设定5.算法结果6.…

【LeetCode刷题笔记(8-3)】【Python】【接雨水】【双指针】【困难】

文章目录 引言接雨水题目描述提示 解决方案3&#xff1a;【双指针】结束语 接雨水 【LeetCode刷题笔记&#xff08;8-1&#xff09;】【Python】【接雨水】【动态规划】【困难】 【LeetCode刷题笔记&#xff08;8-2&#xff09;】【Python】【接雨水】【单调栈】【困难】 引言…

Arcgis新建矢量并手动绘制范围

新建一个shapefile&#xff0c;并选择面 得到了一个新shape 然后右击&#xff0c;开始编辑&#xff0c;打开编辑器

mysql 当前时间加3个工作日

1. 问题描述&#xff1a; 在日常工作中可能会遇到计算工作日的情况 2. 解决过程 (1) 首先制作一个假日表 holiday_config CREATE TABLE holiday_config (id int(10) NOT NULL AUTO_INCREMENT,holiday varchar(8) DEFAULT NULL,PRIMARY KEY (id) USING BTREE ) ENGINEInnoDB…

issue queue的实现方式

主要从一下几个点进行考虑&#xff1a; 集中式&#xff08;Centrallized&#xff09;或者分布式(Distributed)&#xff1b;压缩式&#xff08;Compressing&#xff09;或者非压缩式(Non-compressing)&#xff1b;数据捕捉的方式&#xff08;Data-capture&#xff09;或者非数据…

matlab中Signal Builder模块的用法总结

目录 前言方法一方法二参考文章 前言 今天在用matlab中Signal Builder的模块时&#xff0c;不知道怎么去得到想要的信号源&#xff0c;于是上网查了一下&#xff0c;并记录一下 方法一 如图所示&#xff0c;打开自定义 上面一行是横坐标&#xff0c;下面一行是纵坐标 [0,1…

什么是Symbol?在实际开发中怎么用?

Symbol 是 ECMAScript 6&#xff08;ES6&#xff09;引入的一种新的基本数据类型。Symbol 类型的值是唯一且不可变的。目的是确保对象属性使用唯一标识符&#xff0c;不会发生属性冲突的危险。 1. 使用Symbol() 创建 调用 Symbol()函数时&#xff0c;也可以传入一个字符串参数…

ubuntu docker镜像制作及常用命令

ubuntu docker镜像制作及常用命令 一、安装docker 1、安装 sudo apt install docker.io 2、拉取ubuntu镜像 sudo docker pull ubuntu:18.04 3、从Dockerfile构建镜像 3.1、 Dockerfile示例 FROM ubuntu:18.04MAINTAINER [user] [email]RUN apt-get update \&& …

.NET 自定义中间件 判断是否存在 AllowAnonymousAttribute 特性 来判断是否需要身份验证

public Task InvokeAsync(HttpContext context){// 获取终点路由特性var endpointFeature context.Features.Get<IEndpointFeature>();// 获取是否定义了特性var attribute endpointFeature?.Endpoint?.Metadata?.GetMetadata<AllowAnonymousAttribute>();if …

CentOS 7部署vsftpd

&#xff08;1&#xff09;概述 vsftpd是Linux上一个非常流行的FTP服务器软件。它使用简单&#xff0c;功能强大&#xff0c;安全性高。本文将介绍如何在CentOS 7上部署vsftpd服务器。 &#xff08;2&#xff09;安装vsftpd 使用yum命令安装vsftpd&#xff1a; yum install…

二叉树题目:二叉树着色游戏

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树着色游戏 出处&#xff1a;1145. 二叉树着色游戏 难度 6 级 题目描述 要求 两位玩家参与二叉树着色游戏。给定二叉树的根结点 root \textt…

【稳定检索|投稿优惠】2024年公共服务、健康与医药国际会议(ICPSHM 2024)

2024年公共服务、健康与医药国际会议(ICPSHM 2024) 2024 International Conference on Public Services, Health, and Medicine(ICPSHM) 一、【会议简介】 ​2024年公共服务、健康与医药国际会议&#xff08;ICPSHM 2024&#xff09;将于三亚这片美丽的海滨城市盛大召开。我们诚…

送货服务Grupo RÃO将在Moonbeam上通过DUX推出Web3支持的忠诚度计划

波卡上的首选多链开发平台Moonbeam宣布使用由Account Abstraction提供支持的DUX智能钱包为Grupo RO推出Web3忠诚度计划。此次合作为Grupo RO食品配送服务带来了新的奖励计划&#xff0c;其中包括折扣、产品、优惠券、竞赛等福利的订阅 — 所有这些都得益于Moonbeam的区块链技术…

UDP特性之组播(多播)

UDP特性之组播 1. 组播的特点2. 设置主播属性2.1 发送端2.2 接收端 3. 组播通信流程3.1 发送端3.2 接收端 4. 通信代码 原文链接 在公司测试广播和多播有一点问题。。。 1. 组播的特点 组播也可以称之为多播这也是UDP的特性之一。组播是主机间一对多的通讯模式&#xff0c;是…

URP管线下Shader中的SubShader Tags、Pass Tags、ShaderLab Commands总结

一、SubShader Tags SubShader Tags是提供SubShader信息的键值对&#xff0c;它们允许你指定特定的设置好让渲染管线知道如何处理SubShader。 下面分别介绍一下SubShader中的tag。 1、 RenderPipeline 这个tag用来指定SubShader的目标渲染管线&#xff0c;不同的渲染管线有特定…

Vue3源码梳理:运行时之基于h函数生成vnode的内部流程

VNode 节点类型 对于vnode而言&#xff0c;具备很多节点类型vue源码中patch函数switch处理包含了好几种类型&#xff0c;常见类型如下 Text&#xff1a;文本节点Comment&#xff1a;注释节点Static&#xff1a;静态dom节点Fragment&#xff1a;包含多个根节点的模板被表示为一…

SHT10温湿度传感器——STM32驱动

———————实验效果——————— &#x1f384;硬件外观 &#x1f384;接线 &#x1f388; 3.3V供电 &#x1f388; IIC通讯 &#x1f384; 代码获取 &#x1f388; 查看下方 ———————END———————

FB使用汇编模拟GoSub(子函数)功能

在FB里不支持GoSub功能&#xff0c;在面对函数内简单又重复的操作&#xff0c;而所涉及变量又比较多的时候&#xff0c;再在外边定义一个函数就显得累赘&#xff0c;此时如果可以有一个函数内部的子函数&#xff0c;就显得方便多了。 在汇编探索里发现&#xff0c;可以使用汇编…

20231218在微软官网下载WINDOWS10以及通过rufus-4.3p写入U盘作为安装盘

20231218在微软官网下载WINDOWS10以及通过rufus-4.3p写入U盘作为安装盘 2023/12/18 17:06 百度搜索&#xff1a;下载 windows10 https://www.microsoft.com/zh-cn/software-download/windows10 下载 Windows 10 更新之前&#xff0c;请参阅 Windows 版本信息状态中的已知问题&a…