内核同步问题

linux内核同步问题

Linux内核设计与实现 十、内核同步方法

[手把手教Linux驱动5-自旋锁、信号量、互斥体概述](https://www.cnblogs.com/yikoulinux/p/13558924.html)

基础概念:

并发:多个执行单元同时进行或多个执行单元微观串行执行,宏观并行执行

竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量)的访问而导致的竟态状态。

临界资源:多个进程访问的资源

临界区:多个进程访问的代码段

并发场合:

1、单CPU之间进程间的并发:时间片轮转,调度进程。 A进程访问打印机,时间片用完,OS调度B进程访问打印机。

2、单cpu上进程和中断之间并发:CPU必须停止当前进程的执行中断;

3、多cpu之间

4、单CPU上中断之间的并发

使用偏向:

需求 建议加锁方式 低开销、短期加锁 优先自旋锁 长期锁定 优先互斥锁 中断上下文加锁 自旋锁 需要睡眠的持有锁(单线程) 互斥锁 需要睡眠的持有锁(多线程) 信号量

1、信号量(semaphore)

信号量用于进程之间的同步,进程在信号量保护的临界区代码里面是可以睡眠的(需要进行进程调度),这是与自旋锁最大的区别。

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它负责协调各个进程,以保证他们能够正确、合理的使用公共资源。它和spin lock最大的不同之处就是:无法获取信号量的进程可以睡眠,因此会导致系统调度。

1.1、特点

1、用于进程与进程之间的同步

2、允许多个进程进入临界区代码执行,临界区代码允许睡眠;

3、信号量本质是基于调度器的,在UP和SMP下没有区别;进程获取不到信号量将陷入休眠,并让出CPU;

4、不支持进程和中断之间的同步

5、进程调度也是会消耗系统资源的,如果一个int型共享变量就需要使用信号量,将极大的浪费系统资源

6、信号量可以用于多个线程,用于资源的计数(有多种状态)

1.2、常用函数

信号量加锁以及解锁过程:

sema_init(&sp->dead_sem, 0); / 初始化/

down(&sema);

临界区代码

up(&sema);

信号量定义:

struct semaphore {
    raw_spinlock_t      lock;
    unsigned int        count;
    struct list_head    wait_list;
};

信号量初始化:

static inline void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

dowm函数实现:

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct task_struct *task = current;/*当前进程代表的结构体*/
    struct semaphore_waiter waiter;

    list_add_tail(&waiter.list, &sem->wait_list);
    waiter.task = task;
    waiter.up = false;

    for (;;) {
        if (signal_pending_state(state, task))
            goto interrupted;
        if (unlikely(timeout <= 0)) goto timed_out; __set_task_state(task, state); raw_spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);
        raw_spin_lock_irq(&sem->lock);
        if (waiter.up)
            return 0;
    }

 timed_out:
    list_del(&waiter.list);
    return -ETIME;

 interrupted:
    list_del(&waiter.list);
    return -EINTR;
}
static noinline void __sched __down(struct semaphore *sem)
{
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

void down(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);/*&#x81EA;&#x65CB;&#x9501;*/
    if (likely(sem->count > 0))
        sem->count--;
    else
        __down(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);

}
</=>

up函数实现:

void up(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);/*&#x81EA;&#x65CB;&#x9501;*/
    if (likely(list_empty(&sem->wait_list)))
        sem->count++;
    else
        __up(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);

}

1.3、实现原理

信号量一般可以用来标记可用资源的个数。

举2个生活中的例子:

  1. 我们要坐火车从南京到新疆,这个’任务’特别的耗时,只能在车上等着车到站,但是我们没有必要一直睁着眼睛等着车到站,最好的情况就是我们上车就直接睡觉,醒来就到站,这样从人(用户)的角度来说,体验是最好的,对比于进程,程序在等待一个耗时的任务的时候,没有必须要占用CPU,可以暂停当前任务使其进入休眠状态,当等待的事件发生之后再由其他任务唤醒,这种场景采用信号量比较合适。
  2. 我们在等待电梯、等待洗手间,这种场景需要等待的事件并不是很多,如果我们还要找个地方睡一觉,然后等电梯到了或者洗手间可以用了再醒来,那很显然这也没有必要,我们只需要排好队,刷一刷抖音就可以了,对比于计算机程序,比如驱动在进入中断例程,在等待某个寄存器被置位,这种场景需要等待的时间很短暂,系统开销远小于进入休眠的开销,所以这种场景采用自旋锁比较合适。

dowm函数实现原理解析:

(1)down

判断sem->count是否 > 0,大于0则说明系统资源够用,分配一个给该进程,否则进入__down(sem);

(2)__down

调用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);其中TASK_UNINTERRUPTIBLE=2代表进入睡眠,且不可以打断;MAX_SCHEDULE_TIMEOUT休眠最长LONG_MAX时间;

(3)list_add_tail(&waiter.list, &sem->wait_list);

把当前进程加入到sem->wait_list中;

(3)先解锁后加锁;

进入__down_common前已经加锁了,先把解锁,调用schedule_timeout(timeout),当waiter.up=1后跳出for循环;退出函数之前再加锁;

2、原子变量(atomic)

Linux内核ARM构架中原子变量的底层实现研究

rk3288 原子操作和原子位操作

原子变量适用于只共享一个int型变量;

2.1、特点

1、原子操作是指不被打断的操作,即它是最小的执行单位。

2、最简单的原子操作就是一条条的汇编指令(不包括一些伪指令,伪指令会被汇编器解释成多条汇编指令)

2.2、常用函数

常见函数:

#define ATOMIC_INIT(i)  { (i) }    /*&#x521D;&#x59CB;&#x5316;&#x539F;&#x5B50;&#x53D8;&#x91CF;*/
#define atomic_inc(v)       atomic_add(1, v)   /*&#x539F;&#x5B50;&#x53D8;&#x91CF;&#x52A0;1*/
#define atomic_dec(v)       atomic_sub(1, v)   /*&#x539F;&#x5B50;&#x53D8;&#x91CF;&#x51CF;1*/

#define atomic_inc_and_test(v)  (atomic_add_return(1, v) == 0)   /*&#x539F;&#x5B50;&#x53D8;&#x91CF;&#x52A0;1&#x5E76;&#x6D4B;&#x8BD5;&#x662F;&#x5426;&#x7B49;&#x4E8E;0*/
#define atomic_dec_and_test(v)  (atomic_sub_return(1, v) == 0)   /*&#x539F;&#x5B50;&#x53D8;&#x91CF;&#x51CF;1&#x5E76;&#x6D4B;&#x8BD5;&#x662F;&#x5426;&#x7B49;&#x4E8E;0*/

2.3、实现原理

以atomic_inc为例介绍实现过程

Linux内核文件arch\arm\include\asm\atomic.h中。
执行 atomic_read&#x3001;atomic_set这些操作都只需要一条汇编指令,所以它们本身就是不可打断的。
需要特别研究的是 atomic_inc&#x3001;atomic_dec这类读出、修改、写回的函数。

但是atomic_add在内核中是很难找到的,因为没有这个直接的声明。而是一种宏实现。

所以atomic_add的原型是下面这个宏:

#define ATOMIC_OPS(op, c_op, asm_op)                    \
    ATOMIC_OP(op, c_op, asm_op)                 \
    ATOMIC_OP_RETURN(op, c_op, asm_op)              \
    ATOMIC_FETCH_OP(op, c_op, asm_op)

ATOMIC_OPS(add, +=, add)
#define ATOMIC_OP(op, c_op, asm_op)                 \
static inline void atomic_##op(int i, atomic_t *v)          \
{                                   \
    unsigned long tmp;                      \
    int result;                         \
                                    \
    prefetchw(&v->counter);                     \
    __asm__ __volatile__("@ atomic_" #op "\n"           \
"1: ldrex   %0, [%3]\n"                     \
"   " #asm_op " %0, %0, %4\n"                   \
"   strex   %1, %0, [%3]\n"                     \
"   teq %1, #0\n"                       \
"   bne 1b"                         \
    : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)       \
    : "r" (&v->counter), "Ir" (i)                   \
    : "cc");                            \
}

atomic_add等效于:

static inline void atomic_add(int i, atomic_t *v)           \
{                                   \
    unsigned long tmp;                      \
    int result;                         \
                                    \
    prefetchw(&v->counter);                     \
    __asm__ __volatile__("@ atomic_" #op "\n"           \
"1: ldrex   %0, [%3]\n"                     \
"   " #asm_op " %0, %0, %4\n"                   \
"   strex   %1, %0, [%3]\n"                     \
"   teq %1, #0\n"                       \
"   bne 1b"                         \
    : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)       \
    : "r" (&v->counter), "Ir" (i)                   \
    : "cc");                            \
}

result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)

注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。

(1)ldrex %0, [%3]

意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %0, %0, %4

result = result + i

(3)strex %1, %0, [%3]

意思是将result保存到&v->counter指向的内存中, 此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。

(4) teq %1, #0

测试strex是否成功(tmp == 0 ??)

(5)bne 1b

如果发现strex失败,从(1)再次执行。

3、自旋锁(spinlock)

Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是”原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地”打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源),一般应用在中断上下文。

3.1、特点

1、spinlock是一种死等机制

2、信号量可以允许多个执行单元进入,spinlock不行,一次只能允许一个执行单元获取锁,并且进入临界区,其他执行单元都是在门口不断的死等

3、由于不休眠,因此spinlock可以应用在中断上下文中;

4、由于spinlock死等的特性,因此临界区执行代码尽可能的短;

3.2、常用函数

spinlock加锁以及解锁过程:

​ spin_lock(&devices_lock);

​ 临界区代码

​ spin_unlock(&devices_lock);

spinlock初始化

#define spin_lock_init(_lock)               \
do {                            \
    spinlock_check(_lock);              \
    raw_spin_lock_init(&(_lock)->rlock);        \
} while (0)

进程和进程之间同步

static __always_inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}

本地软中断之间同步

static __always_inline void spin_lock_bh(spinlock_t *lock)
{
    raw_spin_lock_bh(&lock->rlock);
}

本地硬中断之间同步

static __always_inline void spin_lock_irq(spinlock_t *lock)
{
    raw_spin_lock_irq(&lock->rlock);
}

本地硬中断之间同步并且保存本地中断状态

#define spin_lock_irqsave(lock, flags)              \
do {                                \
    raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)

尝试获取锁

static __always_inline int spin_trylock(spinlock_t *lock)
{
    return raw_spin_trylock(&lock->rlock);
}

3.3、实现原理

内核同步问题

arch_spinlock_t结构体定义如下:

#define TICKET_SHIFT    16

typedef struct {
    union {
        u32 slock;/*union*/
        struct __raw_tickets {
#ifdef __ARMEB__  /*&#x5927;&#x7AEF;&#x6A21;&#x5F0F;*/
            u16 next;
            u16 owner;
#else/*&#x5C0F;&#x7AEF;&#x6A21;&#x5F0F;*/
            u16 owner;
            u16 next;
#endif
        } tickets;
    };
} arch_spinlock_t;

arch_spin_lock的实现如下:

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    unsigned long tmp;
    u32 newval;
    arch_spinlock_t lockval;

    prefetchw(&lock->slock);/*&#x4ECE;lock&#x4E2D;&#x53D6;&#x51FA;slock*/
    __asm__ __volatile__(
"1: ldrex   %0, [%3]\n"
"   add %1, %0, %4\n"
"   strex   %2, %1, [%3]\n"
"   teq %2, #0\n"
"   bne 1b"
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
    : "cc");

    while (lockval.tickets.next != lockval.tickets.owner) {
        wfe();/*&#x7B49;&#x5F85;&#xFF0C;&#x7CFB;&#x7EDF;&#x5F00;&#x9500;&#x5F88;&#x5927;*/
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
    }

    smp_mb();
}

lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 << TICKET_SHIFT(%4)

(1)ldrex %0, [%3]

把lock->slock的值赋值给lockval;并且(分别在Local monitor和Global monitor中)设置独占标志。

(2)add %1, %0, %4

newval =lockval +(1<

Original: https://www.cnblogs.com/agui125/p/16132406.html
Author: 风御之举
Title: 内核同步问题

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/607301/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • C语言练习:hackerrank十五关

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Linux 2023年6月11日
    0114
  • Linux 逻辑卷&精简卷报错问题解决

    一、 故障 描述 现象1 :oraclelog 目录提示坏道信息,进行修复后执行删除文件操作,目录不可使用。 现象2 :lsblk 看到目录出现重复,并且有tmeta,tdata …

    Linux 2023年6月13日
    0128
  • 一文让你明白Redis持久化

    网上虽然已经有很多类似的介绍了,但我还是自己总结归纳了一下,自认为内容和细节都是比较齐全的。 文章篇幅有 4k 多字,货有点干,断断续续写了好几天,希望对大家有帮助。不出意外地话,…

    Linux 2023年5月28日
    0112
  • redis实战

    转载于:https://blog.csdn.net/piaoslowly/article/details/81563579 redis简介 Redis 是一个开源的 使用 ANSI…

    Linux 2023年5月28日
    0115
  • jmeter 常用函数总结

    继上节课学习的_Randomstring函数,今天来学习全部的函数,进行函数总结。 1、_counter 函数—计数器 第一行值 true \ false :选择true,表示发起…

    Linux 2023年6月8日
    0118
  • 安卓逆向从0到1学习总结

    PS:该文已经首发于公众号信安之路!!! 初识安卓逆向是在2019年的暑假,到现在也快一年了,这一年来有刚从web渗透转来的迷茫,有成功破解了第一个app的喜悦,也有通宵熬夜逆向的…

    Linux 2023年6月8日
    0143
  • 继承、封装、多态的实现原理

    欢迎来到Java学习之继承、封装、多态的实现原理 目录 从JVM结构开始谈多态 JVM 的结构 Java 的方法调用方式 常量池(constant pool) 图 2. 常量池各表…

    Linux 2023年6月13日
    0149
  • 配置管理docker对象和守护进程

    使用 Docker 的主要工作是创建和使用各类对象:镜像、容器、网络、卷等。 1、Docker对象的标记 标记(Label):是一种将元数据应用于Docker对象(镜像、容器、网络…

    Linux 2023年6月8日
    0105
  • 002 Linux 文件与目录命令的必会姿势!

    文件及目录的路径切换、显示、创建、复制、移动和删除操作的常用姿势,必会!因为这些命令是使用 Linux 系统进行工作的基础,是摆脱小白的第一步,是构建大厦的基石!发现锅锅真是个话痨…

    Linux 2023年5月27日
    0118
  • 嵌入式软件架构设计-函数调用

    1 前言 函数调用很好理解,即使刚学没多久的朋友也知道函数调用是怎么实现的,即调用一个已经封装好的函数,实现某个特定的功能。 把一个或者多个功能通过函数的方式封装起来,对外只提供一…

    Linux 2023年6月7日
    0111
  • 备用链接

    win10 stick note 地址 https://www.partitionwizard.com/partitionmanager/where-are-sticky-note…

    Linux 2023年6月7日
    0104
  • cpp创建对象的多种形式

    1 使用非默认构造函数来创建对象的几种形式 Person person = Person("binny1", 26); 这种方式创建对象,C++标准允许编译器使…

    Linux 2023年6月13日
    0198
  • Spring 进入Controller前参数校验

    在进入Controller前完成参数的校验,针对对象参数 分为两个验证方式 (1)直接使用已定义的校验方式 1、在需要进行校验的属性上增加校验类型注解 import java.ut…

    Linux 2023年6月7日
    0172
  • 如何使用yum来下载RPM包而不进行安装

    yum是基于Red Hat的系统(如CentOS、Fedora、RHEl)上的默认包管理器。使用yum,你可以安装或者更新一个RPM包,并且他会自动解决包依赖关系。但是如果你只想将…

    Linux 2023年6月6日
    0184
  • mit 6.824 lab2 C,raft持久化(lab2D中有关于此处大量代码修改找出了很多错误)

    lab2 C 实现的就是持久化非常简单,在mit提供的框架中,持久化是存储在内存中。 首先看论文 需要持久化的元素。 根据lab2C的描述中我们可以知道需要实现的函数: persi…

    Linux 2023年6月7日
    0127
  • 使用Visual Studio 2019将ASP.NET Core发布为linux-arm64程序

    前言 前段时间入手了一台树莓派4B,一直闲置未使用,最近工作需要,要在上面跑下.NET Core程序,由于树莓派4B使用的是ARM架构,并且支持64位操作系统,为了充分发挥树莓派性…

    Linux 2023年6月8日
    0142
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球