PS:要转载请注明出处,本人版权所有。

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明

  无

前言


  最近为了实现在android linux kernel上,是的bionic c和glibc的sem_相关的信号量接口能够相互调用的功能(例如:用bionic c wait,用glibc awake),需要去深度阅读相关c库关于sem_ posix api的实现。

  最终的最终,发现主要要解决futex的问题就行。因此将其相关的概念进行了总结。

进程同步与互斥的一些基本概念


  进程:

  • 是操作系统调度的基本单位。

  进程状态:

  • 三态模型:运行、就绪、等待(阻塞/睡眠)
  • 五态模型:运行、就绪、等待(阻塞/睡眠) + 新建、终止

  进程调度:

  • 因某些原因(io等待,自己睡眠,调度公平等等),一个或者多个进程需要进行状态切换。

  进程工作特性分类:

  • 计算密集型:进程的主要工作是计算某些事情。
  • IO密集型:进程的主要工作是加载IO并进行处理。

  并发:

  • 是指多个进程同时运行,并完成一定任务。

  临界区:

  • 是指多个进程同时运行时,同一时刻访问相同的代码和数据。

  同步:

  • 强调的是进程间的执行需要按照某种先后顺序,即进程运行的时间是有序的。

  互斥:

  • 强调的是对于某些共享资源的访问不能同时进行,同一时间只能有一定数量的进程访问这些共享资源。

  进程常见的同步机制:

  • 信号量:解决了同步问题。
  • 互斥量:解决了互斥问题。是信号量的一种特例,只具备0,1两种值。


  锁:

  • 既可以解决同步问题,也可以解决互斥问题。

  死锁:

  • 指多个进程同时获取锁,且由于某些原因,此锁永远不会被空闲。

  两种基本锁:

  • 互斥锁:加锁失败后,线程会释放 CPU ,给其他线程;
  • 自旋锁:加锁失败后,线程会忙等待,直到它拿到锁;

  读写锁:

  • 读锁:当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
  • 写锁:一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞
  • 特征:读写锁在读多写少的场景,能发挥出优势。读写锁可以分为「读优先锁」和「写优先锁」

  乐观锁:

  • 如果多线程同时修改共享资源的概率比较低

  悲观锁:

  • 认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

CAS(Compare And Swap)


  CAS是一种无锁同步算法,主要用于多线程环境下面,高效的对多个线程进行同步。

  首先CAS有3个重要的参数:目标地址(P)、期望值(E)、新值(N),然后其工作原理是:

  • 读取目标地址(P)的值
  • 对比目标地址(P)的值与期望值(E)
  • 如果P==E,则写入新值(N),如果P!=E,则不做任何操作并返回。

  可以乍一看,这里有3步操作,会让我们对这个算法难以理解,所以这里还有一个重要的概念:CAS的这几步操作是原子的,一次性完成的,此外,这些特性是硬件提供的。因此,在实际使用上面,这些原子操作被编译器封装成相关的接口来使用这些硬件特性。例如gcc里面:

// https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html
// 注意,原来的__sync_*系列函数的__atomic系列替代了
bool __atomic_compare_exchange (type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder)

futex 系统调用


  futex是linux下用来实现各种同步机制的一个系统调用,我们先来学习看看其api:

#include <linux/futex.h>
#include <sys/time.h> int futex(int *uaddr, int futex_op, int val, const struct timespec *timeout, /* or: uint32_t val2 */
int *uaddr2, int val3);

  注意这里的看到这个api有许多的参数,但是我们常用的也是前面4个,一般来说,我们会将futex分为常用的两类,是futex_op参数指定的,分别是:FUTEX_WAIT、FUTEX_WAKE(注意,还有其他很多op类型),其他的参数根据不同的类型,有不同的一些含义:

  • 对于FUTEX_WAIT来说:如果uaddr的值等于期待值(val),则将线程挂起。timeout如果是NULL,则无限等待,或者等待timeout时间。
  • 对于FUTEX_WAKE来说:指定唤醒uaddr关联的并被挂起的val个线程。timeout参数忽略。

  上面我们简单说明了这个系统调用的一些用法,现在我们来看看这个系统调用的内核简单实现,然后我们就基本理解了这个系统调用的工作原理:

首先来看看FUTEX_WAIT的内核部分源码,如下:

//linux kernel v4.6 kernel/futex.c

static inline void queue_me(struct futex_q *q, struct futex_hash_bucket *hb)
__releases(&hb->lock)
{
int prio; // ... ... 省略 plist_node_init(&q->list, prio);
plist_add(&q->list, &hb->chain);
q->task = current;
spin_unlock(&hb->lock);
} static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q,
struct hrtimer_sleeper *timeout)
{
// ... ... 省略
set_current_state(TASK_INTERRUPTIBLE);//挂起
queue_me(q, hb); // ... ... 省略
}
static int futex_wait_setup(u32 __user *uaddr, u32 val, unsigned int flags,
struct futex_q *q, struct futex_hash_bucket **hb)
{
u32 uval;
int ret; // ... ... 省略 retry:
// ... ... 省略
ret = get_futex_value_locked(&uval, uaddr);
// ... ... 省略 ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &q->key, VERIFY_READ);
if (unlikely(ret != 0))
return ret; // ... ... 省略 if (uval != val) {//判断值是否是期望值,并做后续操作
queue_unlock(*hb);
ret = -EWOULDBLOCK;
}
// ... ... 省略
} static int futex_wait(u32 __user *uaddr, unsigned int flags, u32 val,
ktime_t *abs_time, u32 bitset)
{
struct hrtimer_sleeper timeout, *to = NULL;
struct restart_block *restart;
struct futex_hash_bucket *hb;
struct futex_q q = futex_q_init;
int ret; // ... ... 省略 retry:
/*
* Prepare to wait on uaddr. On success, holds hb lock and increments
* q.key refs.
*/
ret = futex_wait_setup(uaddr, val, flags, &q, &hb);
if (ret)
goto out; /* queue_me and wait for wakeup, timeout, or a signal. */
futex_wait_queue_me(hb, &q, to); // ... ... 省略 out:
// ... ... 省略
}

  其实我们这里可以看到,关键是通过get_futex_key来根据传入的uaddr获取一个key,然后根据key,来构造一个hash list(注意,这时这个hash list被一个hash数据结构维护了,可通过key查询),并将当前线程插入到这个list,并将线程/进程挂起。在这个过程中,还会检查*uaddr 是否等于val,否则做相关操作。

  现在,其实我们基本上也可以猜到FUTEX_WAKE的实现是什么样子,现在我们先来看看其源码节选:

//linux kernel v4.6 kernel/futex.c
static inline int match_futex(union futex_key *key1, union futex_key *key2)
{
return (key1 && key2
&& key1->both.word == key2->both.word
&& key1->both.ptr == key2->both.ptr
&& key1->both.offset == key2->both.offset);
} static int
futex_wake_op(u32 __user *uaddr1, unsigned int flags, u32 __user *uaddr2,
int nr_wake, int nr_wake2, int op)
{
union futex_key key1 = FUTEX_KEY_INIT, key2 = FUTEX_KEY_INIT;
struct futex_hash_bucket *hb1, *hb2;
struct futex_q *this, *next;
int ret, op_ret;
WAKE_Q(wake_q); retry:
ret = get_futex_key(uaddr1, flags & FLAGS_SHARED, &key1, VERIFY_READ);//根据uaddr获取key
if (unlikely(ret != 0))
goto out;
// ... ... 省略 hb1 = hash_futex(&key1);//根据key获取hash对象 // ... ... 省略 retry_private:
// ... ... 省略 plist_for_each_entry_safe(this, next, &hb1->chain, list) {
if (match_futex (&this->key, &key1)) {//匹配符合条件的key
if (this->pi_state || this->rt_waiter) {
ret = -EINVAL;
goto out_unlock;
}
mark_wake_futex(&wake_q, this);//将符合条件的对象放入到队列wake_q
if (++ret >= nr_wake)
break;
}
} // ... ... 省略 out_unlock:
double_unlock_hb(hb1, hb2);
wake_up_q(&wake_q); //唤醒线程/进程,wake_up_q在kernel/sched/core.c中定义
out_put_keys:
put_futex_key(&key2);
out_put_key1:
put_futex_key(&key1);
out:
return ret;
}

  从这里我们可以看到,首先我们根据uaddr获取了key,然后通过hash_futex获取key对象,这个时候我们就获取了和uaddr相关的线程/进程list。然后我们遍历list,将符合条件的线程/进程放到wake_q队列中去,最后通过wake_up_q来设置TASK_WAKING,并唤醒线程/进程。

后记


  从这里我们可以看到,这些概念是为了解决前置问题逐步引入的:

  • 进程具备一定的状态(运行、就绪、等待(阻塞/睡眠) + 新建、终止)。
  • 进程是OS调度的基本单位,调度就是调整进程的状态。
  • 为了高效完成一个任务,我们需要并发,这时我们需要同步,需要信号量。
  • 因为有了并发,我们需要注意临界区。
  • 有了临界区,我们需要互斥量(锁)。
  • 最后,CAS和futex可以实现各种信号量和锁。

  完结散花。

参考文献


打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

关于进程同步与互斥的一些概念(锁、cas、futex)的更多相关文章

  1. HTTP协议漫谈 C#实现图(Graph) C#实现二叉查找树 浅谈进程同步和互斥的概念 C#实现平衡多路查找树(B树)

    HTTP协议漫谈   简介 园子里已经有不少介绍HTTP的的好文章.对HTTP的一些细节介绍的比较好,所以本篇文章不会对HTTP的细节进行深究,而是从够高和更结构化的角度将HTTP协议的元素进行分类讲 ...

  2. 【APUE】信号量、互斥体和自旋锁

    http://www.cnblogs.com/biyeymyhjob/archive/2012/07/21/2602015.html http://blog.chinaunix.net/uid-205 ...

  3. 看完了进程同步与互斥机制,我终于彻底理解了 PV 操作

    尽人事,听天命.博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 CS-Wiki(Gitee 官 ...

  4. 并发系列2:Java并发的基石,volatile关键字、synchronized关键字、乐观锁CAS操作

    由并发大师Doug Lea操刀的并发包Concurrent是并发编程的重要包,而并发包的基石又是volatile关键字.synchronized关键字.乐观锁CAS操作这些基础.因此了解他们的原理对我 ...

  5. JUC原子操作类与乐观锁CAS

    JUC原子操作类与乐观锁CAS ​ 硬件中存在并发操作的原语,从而在硬件层面提升效率.在intel的CPU中,使用cmpxchg指令.在Java发展初期,java语言是不能够利用硬件提供的这些便利来提 ...

  6. 【Python下进程同步之互斥锁、信号量、事件机制】

    " 一.锁机制:  multiprocess.Lock 上篇博客中,我们千方百计实现了程序的异步,让多个任务同时在几个进程中并发处理,但它们之间的运行没有顺序.尽管并发编程让我们能更加充分的 ...

  7. 互斥体与互锁 <第五篇>

    互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex)).互斥体禁止多个线程同时进入受保护的代码“临界区”.因此,在任意时刻,只有一个线程被允许进入这 ...

  8. 转载 互斥体与互锁 <第五篇>

    互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex)).互斥体禁止多个线程同时进入受保护的代码“临界区”.因此,在任意时刻,只有一个线程被允许进入这 ...

  9. python-Lock进程同步解决互斥

    #!/usr/bin/python from multiprocessing import Process,Lock import time,sys def A(lock): with lock: f ...

  10. 乐观锁--CAS

    悲观锁与乐观锁的区别 悲观锁会把整个对象加锁占为已有后才去做操作,Java中的Synchronized属于悲观锁.悲观锁有一个明显的缺点就是:它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时 ...

随机推荐

  1. 安装Electron时卡在install.js不动的解决方案

    问题来源,发现即使 源切换成淘宝的之后,安装 electron的时候还是慢死,郁闷,后来百度才发现,原来,还需要设置一个地方!!! 经过试验,果然快了 爽.... 之前在安装Electron的时候,经 ...

  2. 迟来的HIT2024和realworld2024体验赛WP

    目录 前言碎语 2024.2.14 中午 rwctf2024 体验赛 vision 哈工大青训营2024 结营赛 计算器 小技巧 神奇玩意 gdb! 再也不用苦哈哈往回翻 跟踪fork 赛后复现rw ...

  3. NC16856 [NOI1999]钉子和小球.md

    题目链接 题目 题目描述 有一个三角形木板,竖直立放,上面钉着n(n+1)/2颗钉子,还有(n+1)个格子(当n=5时如图1).每颗钉子和周围的钉子的距离都等于d,每个格子的宽度也都等于d,且除了最左 ...

  4. js加css实现div展示更多隐藏内容

    说明 在设计博客首页文章分类等栏目时,有时候列表内容太多往往不是一次性展示出来.此时需要添加更多功能,当点击更多标签时再展示剩余隐藏的项目. 效果 代码 <!DOCTYPE html> & ...

  5. cronet 的简单学习

    官方的解释 "Cronet is the networking stack of Chromium put into a library for use on mobile. This is ...

  6. win32- 窗口模板

    主要用于日常的win32窗口的测试 #include <Windows.h> #include <stdio.h> #include <iostream> usin ...

  7. [Android逆向]Exposed 破解 jwxdxnx02.apk

    使用exposed 遇到了一些坑,这里记录一下 源码: package com.example.exposedlesson01; import de.robv.android.xposed.IXpos ...

  8. InSAR处理软件——Gamma 安装教程

    Gamma是由瑞士 GAMMA Remote Sensing 公司开发SAR数据处理软件,支持SAR数据全流程处理,是最InSAR最常用的软件.下面介绍该软件的安装流程,安装环境为Ubuntu16.0 ...

  9. python列表操作的大O效率

  10. Linux开端---Centos

    Linux-Centos 虚拟化所需工具:https://pan.baidu.com/s/1643-kYcx9oPGnGEZM1pLOw?pwd=g0v5 提取码:g0v5 问题解决 正常注册网络适配 ...