当前位置: 首页 > article >正文

嵌入式八股RTOS与Linux---进程间的通信与同步篇

前言

  1. 同步异步、阻塞/非阻塞是什么?
  • 同步:在执行某个操作时,调用者必须等待该操作完成并返回结果后,才能继续执行后续的操作
  • 异步:在执行某个操作时,调用者发起操作后不必等待其完成,可以立即继续执行后续的操作。操作完成后,通常通过回调函数、事件或通知的方式告知调用者
    同步异步通常是是两个进程/线程之间的关系
  • 阻塞: 在执行某个操作时,调用者会被挂起(暂停执行),直到该操作完成并返回结果
  • 非阻塞: 在执行某个操作时,调用者不会被挂起,无论操作是否立即完成,调用者都可以立即获得一个结果(可能是部分结果或状态信息),并继续执行后续的任务。
    阻塞和非阻塞更是强调线程当前的状态
    同步不一定阻塞,阻塞也不一定同步,四种状态
  1. 线程的同步与互斥是什么?
  • 线程同步:是指在多线程编程中,协调多个线程对共享资源的访问。从而保证线程的执行顺序,才能让线程协作去完成大事.
  • 线程互斥: 是指若干线程访问同一块资源时, 规定同一时间只有一个线程可以得到访问权

FreeRTOS的IPC

  1. 事件标志组
  2. 信号量与互斥信号量
    • 信号量(二值/计数)与互斥信号量的区别?

      • 一个是释放的问题 二值可以是A线程获取 B线程释放 但是互斥不行
      • 二值不存在优先级继承问题
      • 二值信号量支持排队,互斥不会排队的
      • 互斥 != 二值 + 优先级继承
        在这里插入图片描述
    • 优先级翻转问题
      高优先级的任务想要某个资源,但是低优先级的任务先在使用,此时高优先级就得等了
      在这里插入图片描述

      假设Thread1和Thread3需要相同的资源,Thread3被Thread2抢占了结果Thead3还没释放资源,Thread1就算抢占了Thread2也咩用,就必须等

      • 解决方案
        当Thread3想要访问资源时,暂时把Thread1的优先级提升到和3相同
    • 为什么FreeRTOS里的互斥信号量是中断不安全的?

      • FreeRTOS里的信号量不支持递归调用,要是中断正好是在在获得了互斥锁的当前线程运行时发生,就死锁了
      • 在中断中是不能阻塞的
      • 互斥信号量的特点就是优先级继承, 那中断的优先级咋被继承呢?
      • 对于中断 要用也是二值信号量这种
    • 互斥信号量和递归互斥信号量的区别?
      递归互斥信号量是支持本任务多次获得该信号量
      但是就算递归互斥信号量也是中断不安全的!

  3. 消息队列
    就是利用队列的先入先出机制,进行消息的传递
  4. 任务通知
    • 任务通知和消息队列的区别,各自的优缺点
      • 任务通知是点对点的,比如A任务对B任务,但是消息队列不是,谁都可以往消息队列加消息,同样谁也都可以取出来
      • 任务通知发送的信息格式固定(unit32_t),消息队列更加灵活可以自定义
      • 任务通知不会存储未处理的消息 会直接覆盖
      • 任务通知开销小, 消息队列开销大
  5. 死锁的概念与解决
      就是逻辑上获取一个已经被占用且逻辑上不可能被释放的锁而阻塞,永久阻塞。
  • 避免死锁需要遵循的规则:
    对共享资源操作前一定要获得锁;
    完成操作后一定要释放锁;
    尽量短时间占用锁;
    如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC
  1. 大概实现原理
      我们首先定义一个tEvent作为父类,剩下的都可以通过这个父类去实现
  • tEvent的结构
typedef struct _tEvent {
	
	tEventType type;
	tList waitList;   // 双向列表 记录了所有在等待该事件的任务
	
}tEvent;
  • tEventWait()
    当我们的任务需要被挂起的时候,通过这个函数将任务挂起
//同一时间某个任务只能进入一种等待状态--要么等事件1 要么等事件2
void tEventWait (tEvent * event, tTask * task, void * msg, uint32_t state, uint32_t timeout)
{
   	// 进入临界区
   uint32_t status = tTaskEnterCritical();
   	//1. 更新task的一些信息--注意低16位不能被用 被我们占用了只能用高16位
   	task->state |= state << 16;
   	task->waitEvent = event;	// 设置任务等待的事件结构
   	task->eventMsg = msg;	// 设置任务等待事件的消息存储位置(不一定用的上)
   	
   	tTaskSchedUnRdy(task);	//此时任务处于等待状态--只要等待一个就被调走了
   	tListAddLast(&event->waitList,&task->linkNode);
   	if(timeout)
   		tTimeTaskWait(task, timeout); //如果有延时还要加入延时队列中 在sysTickHandler会处理好的
   	// 退出临界区
   tTaskExitCritical(status);
}
  • tEventWakeUp()
    唤醒的方式有很多种–延时时间到了/等待的事件发生了
tTask * tEventWakeUp (tEvent * event, void * msg, uint32_t result)
{
    tNode  * node;
		tTask 	*task = (tTask * )0;
		// 进入临界区
    uint32_t status = tTaskEnterCritical();
		    // 取出等待队列中的第一个结点
    if((node = tListRemoveFirst(&event->waitList)) != (tNode *)0)
    {      
		task = (tTask*) tNodeParent(node, tTask, linkNode);
		// 设置收到的消息、结构,清除相应的等待标志位
        task->waitEvent = (tEvent *)0;
        task->eventMsg = msg;
        task->waitEventResult = result;
        task->state &= ~TINYOS_TASK_WAIT_MASK;
		}	
       // 任务申请了超时等待,这里检查下,将其从延时队列中移除
     if (task->delayTicks != 0)
     { 
         tTimeTaskWakeUp(task);
     }		
		 // 将任务加入就绪队列
     tTaskSchedRdy(task);  
		// 退出临界区
    tTaskExitCritical(status); 
    return task;
}
  1. tSem和tMutex的区别
  • tMutex

        // 互斥信号量类型
        typedef struct  _tMutex
        {
            // 事件控制块
            tEvent event;
            // 已被锁定的次数
            uint32_t lockedCount;
            // 拥有者
            tTask * owner;
            // 拥有者原始的优先级
            uint32_t ownerOriginalPrio;
        }tMutex;
    
    • 获得/释放互斥量过程
    uint32_t tMutexWait (tMutex * mutex, uint32_t waitTicks)
    {
    	uint32_t status = tTaskEnterCritical();
    	//为啥有01数值信号量不信还得有互斥信号量的点就体现在这里了
    	// 第一就是允许递归调用 第二就是拥有者是谁是知道的
    	if (mutex->lockedCount <= 0)
        {
    	    // 如果没有锁定,则使用当前任务锁定
            mutex->owner = currentTask;
            mutex->ownerOriginalPrio = currentTask->prio;
            mutex->lockedCount++;
            tTaskExitCritical(status);
            return tErrorNoError;		
    	}
    	else
    	{
    		// 允许多次递归调用
    		if(mutex->owner == currentTask)
    		{
    		    mutex->lockedCount++;
    			tTaskExitCritical(status);
                return tErrorNoError;
    		}
    		//在有人用的情况下 那就得等咯
    		// 等的时候分优先级继承与否
    		else
    		{
    			if(currentTask->prio < mutex->owner->prio)
    			{
    				tTask * owner = mutex->owner;
    				// 如果当前正在调度的话就得先改一下
    				//提升原拥有者的优先
    				if (owner->state == TINYOS_TASK_STATE_RDY)
                    {
                        // 任务处于就绪状态时,更改任务在就绪表中的位置
                        tTaskSchedUnRdy(owner);
                        owner->prio = currentTask->prio;
                        tTaskSchedRdy(owner);
                    }
    				else
                    {
                        // 其它状态,只需要修改优先级
                        owner->prio = currentTask->prio;
                    }
    			}
    			// 当前任务进入等待队列中
                tEventWait(&mutex->event, currentTask, (void *)0, tEventTypeMutex, waitTicks);
                tTaskExitCritical(status);
                // 执行调度, 切换至其它任务 return不会被调用
                tTaskSched();
    			//切换回来的时候就会return了 注意这里是被另一个修改的结果
                return currentTask->waitEventResult;				
    	    }
    	}
    }
    // 唤醒方式1--正常唤醒
    uint32_t tMutexNotify (tMutex * mutex)
    {
        uint32_t status = tTaskEnterCritical();
            //这是互斥 mutex->lockedCount理论只有0 1
        if (mutex->lockedCount <= 0)
        {
            // 锁定计数为0,信号量未被锁定,直接退出
            tTaskExitCritical(status);
            return tErrorNoError;
        }
            // 和 二值信号量不同的地方了
            if (mutex->owner != currentTask)
        {
            // 不是拥有者释放,认为是非法
            tTaskExitCritical(status);
            return tErrorOwner;
        }
        if (--mutex->lockedCount > 0)
        {
            // 减1后计数仍不为0, 直接退出,不需要唤醒等待的任务
            tTaskExitCritical(status);
            return tErrorNoError;
        }
        //能到这里肯定就是自己释放了
        //那就得看自己是不是被人优先级继承了
        if (mutex->ownerOriginalPrio != mutex->owner->prio)
        {
            // 有发生优先级继承,恢复拥有者的优先级
            if (mutex->owner->state == TINYOS_TASK_STATE_RDY)
            {
                // 任务处于就绪状态时,更改任务在就绪表中的位置
                //这么干的原因是很有可能原本该低优先级,不配被调度的
                // 但是咱们这么想 虽然优先级改回来了,但是假设此时原来等着的高优先级因为时间放弃了
                // 那岂不是这个任务还会"假装"高优先级 直到下个systickHandler被触发 才会任务切换
                tTaskSchedUnRdy(mutex->owner);
                currentTask->prio = mutex->ownerOriginalPrio;
                //会切换到更好的任务
                tTaskSchedRdy(mutex->owner);
            }
            else
            {
                // 其它状态,只需要修改优先级
                currentTask->prio = mutex->ownerOriginalPrio;
            }
        }
        // 检查是否有其他任务被阻塞了在等待互斥锁
        if (tEventWaitCount(&mutex->event) > 0)
        {
            // 如果有的话,则直接唤醒位于队列首部(最先等待)的任务
            tTask * task = tEventWakeUp(&mutex->event, (void *)0, tErrorNoError);
    
            mutex->owner = task;
            mutex->ownerOriginalPrio = task->prio;
            mutex->lockedCount++;
    
            // 如果这个任务的优先级更高,就执行调度,切换过去
            if (task->prio < currentTask->prio)  //这里就和改回原来优先级对应上了
            {
                tTaskSched();
            }
        }
        tTaskExitCritical(status);
        return tErrorNoError;
            
    }
    

    // 唤醒方式2—等待超时了 在sysTickHadnler中唤醒

  • tSem 比较简单 如果得不到就等待 如果得到了那就返回

uint32_t tSemWait (tSem * sem, uint32_t waitTicks)
{
		uint32_t status = tTaskEnterCritical();
		if(sem->count > 0)
		{
				--sem->count;
				tTaskExitCritical(status);
				return tErrorNoError;
		}
		else
		{
				// 得不到就加入等待列表
				tEventWait(&sem->event,currentTask,(void*)0, tEventTypeSem, waitTicks);        
				tTaskExitCritical(status);
				tTaskSched();
				// 因为sched了哥们所以这里不会返回 而是直接切换到下一个任务的栈和堆里面了
			  // 直到再切换到本任务的时候发现这句话还没执行完接着执行
				// 当由于等待超时或者计数可用时,执行会返回到这里,然后取出等待结构
        return currentTask->waitEventResult;
		}
}
void tSemNotify (tSem * sem)
{
	uint32_t status = tTaskEnterCritical();
	if(tEventWaitCount(&sem->event) > 0)
	{
		tTask * task = tEventWakeUp(&sem->event, (void *)0, tErrorNoError );
		// 如果这个任务的优先级更高,就执行调度,切换过去
        if (task->prio < currentTask->prio)
        {
            tTaskSched(); 
        }
	}
	else
    {
    	// 如果没有任务等待的话,增加计数
    	++sem->count;

    	// 如果这个计数超过了最大允许的计数,则递减
    	if ((sem->maxCount != 0) && (sem->count > sem->maxCount)) 
    	{	
    		sem->count = sem->maxCount;
    	}
    }
	tTaskExitCritical(status);
	
}

Linux的IPC

  1. 管道
    无名管道(匿名管道):主要用于有亲缘关系的进程之间的通信(例如,父子进程)。数据是单向流动的。
    命名管道(FIFO):允许无亲缘关系的进程之间通信,它在文件系统中有一个名字。
  2. 信号
  3. 信号量
  4. 共享内存–最快的方案
  5. Scoket
  6. 消息队列

线程同步与互斥的方法–用户态

  用户态的并发通常由线程或进程实现,线程之间的同步主要通过操作系统提供的同步原语

  1. 自旋锁
  2. 信号量
  • 自旋锁与信号量 为什么有了信号量还需要自旋锁?
    上下文切换也是需要开销的,所以有的时候自旋也许是性能损失更小的组合
  1. 条件变量
  2. 互斥锁
  3. 读写锁
  4. 内存屏障
  • 内存屏障与cache一致性
      内存屏障解决的只是顺序一致性的问题,不解决Cache一致性的问题
      Cache一致性协议只是保证了Cache一致性,但是不关注顺序一致性的问题
    • 缓存一致性问题
      来源: 每个CPU核心都有自己的Cache 当多个CPU核心访问内存同一变量且都想做修改的时候就有问题了
      在这里插入图片描述

      • 基本思路:那假如我CPU A改了 就广播给所有人更新就行 但是这远远不够 假设CPU A/B 同时广播 那其他CPU的值会怎么变式不知道的–事件串行化
        在这里插入图片描述

      • MESI协议: 通过有限状态机来实现–硬件
        定义 M(已修改) /E(独占) / S(已共享) / I(已失效) 四个状态 发生不同的事件会导致状态的切换 从而保证缓存一致性
        在这里插入图片描述

      • DMA与CPU一致性问题
        比如CPU把某块区域读到缓存cache中了 此时DMA修改了这块内存,但是CPU不知道啊,CPU接着用cache里的数据就出问题了
        解决方案:一致性DMA映射 或者流式DMA映射

    • 顺序一致性问题
      来源: 编译器可能会对指令的顺序做调整 CPU也可能乱序执行 从而导致了结果和预期结果不一致。
      比如
      线程A {
      a = 1;
      b = 2; //从人类的角度 b = 2的时候 a肯定等于1了
      }
      线程B {
      while(b != 2) continue;
      assert(a == 1); //你觉得有缓存一致性,讲道理assert不该被触发
      }
      但是你的编译器想优化,可能先执行了 b = 2这个操作 或者CPU先执行 b = 2了 那缓存一致性也不顶用
      解决方案:写内存屏障
      线程 A{
      a = 1;
      smp_mb(); // 这意味着 如果想执行 b = 1 就得等a = 1这个操作执行完了(所有CPU都看见了)才能执行b = 1;
      b = 1;
      }

      • 核心应用就是Linux中的KFIFO–无锁的环形缓冲区

内核同步的方法–内核态

  内核态的并发不仅包括线程,还包括中断处理程序、软中断、工作队列等,甚至还有多核等复杂的环境

  • 原子操作
  • 自旋锁
  • 信号量
  • 互斥体
  • 完成变量
  • BLK:大内核锁
  • 顺序锁
  • 禁止抢占
  • 顺序和屏障
原文地址:https://blog.csdn.net/qq_45731845/article/details/146484714
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/599583.html

相关文章:

  • Select多路转接
  • 【机器学习】什么是线性回归?
  • Go常见问题与回答(下)
  • 【HTTP 传输过程中的 cookie】
  • 详细Linux中级知识(不断完善)
  • 高级java每日一道面试题-2025年3月09日-微服务篇[Eureka篇]-说一说Eureka自我保护模式
  • AI日报 - 2025年3月25日
  • 2018扬州大学876农业机械学概论填空名词解释简答
  • leetcode1109. 航班预订统计-medium
  • 批量配置Linux ~/.bash_profile
  • 【蓝桥杯每日一题】3.20
  • 阅读li2019-DOT源码--逐步调试
  • 第八课:Python高级排序算法:分治策略的深度应用与实践
  • 生产部署与多框架支持
  • 从零开始实现 C++ TinyWebServer 项目总览
  • 常见框架漏洞—中间件IIS
  • Filnk并行度和算子链
  • Python前缀和(例题:异或和,求和)
  • Java中static final才是修饰常量的,单独的final并不能修饰常量这样理解对吗?
  • Codeforces Round 1012 (Div. 2)