太原在线网站制作,赣州,廊坊企业网站服务,网站技术部做什么并发 是指在某一时间段内能够处理多个任务的能力#xff0c;而 并行 是指同一时间能够处理多个任务的能力。并发和并行看起来很像#xff0c;但实际上是有区别的#xff0c;如下图#xff08;图片来源于网络#xff09;#xff1a;concurrency-parallelism上图的意思是而 并行 是指同一时间能够处理多个任务的能力。并发和并行看起来很像但实际上是有区别的如下图图片来源于网络concurrency-parallelism上图的意思是有两条在排队买咖啡的队列并发只有一架咖啡机在处理而并行就有两架的咖啡机在处理。咖啡机的数量越多并行能力就越强。可以把上面的两条队列看成两个进程并发就是指只有单个CPU在处理而并行就有两个CPU在处理。为了让两个进程在单核CPU中也能得到执行一般的做法就是让每个进程交替执行一段时间比如让每个进程固定执行 100毫秒执行时间使用完后切换到其他进程执行。而并行就没有这种问题因为有两个CPU所以两个进程可以同时执行。如下图concurrency-parallelism原子操作上面介绍过并发有可能会打断当前执行的进程然后替切换成其他进程执行。如果有两个进程同时对一个共享变量 count 进行加一操作由于C语言的 count 操作会被翻译成如下指令mov eax, [count]
inc eax
mov [count], eax
那么在并发的情况下有可能出现如下问题concurrency-problem假设count变量初始值为0进程1执行完 mov eax, [count] 后寄存器eax内保存了count的值0。进程2被调度执行。进程2执行 count 的所有指令将累加后的count值1写回到内存。进程1再次被调度执行计算count的累加值仍为1写回到内存。虽然进程1和进程2执行了两次 count 操作但是count最后的值为1而不是2。要解决这个问题就需要使用 原子操作原子操作是指不能被打断的操作在单核CPU中一条指令就是原子操作。比如上面的问题可以把 count 语句翻译成指令 inc [count] 即可。Linux也提供了这样的原子操作如对整数加一操作的 atomic_inc()static __inline__ void atomic_inc(atomic_t *v)
{__asm__ __volatile__(LOCK incl %0:m (v-counter):m (v-counter));
}
在多核CPU中一条指令也不一定是原子操作比如 inc [count] 指令在多核CPU中需要进行如下过程从内存将count的数据读取到cpu。累加读取的值。将修改的值写回count内存。Intel x86 CPU 提供了 lock 前缀来锁住总线可以让指令保证不被其他CPU中断如下lock
inc [count]
锁原子操作 能够保证操作不被其他进程干扰但有时候一个复杂的操作需要由多条指令来实现那么就不能使用原子操作了这时候可以使用 锁 来实现。计算机科学中的 锁 与日常生活的 锁 有点类似举个例子比如要上公厕首先找到一个没有人的厕所然后把厕所门锁上。其他人要使用的话必须等待当前这人使用完毕并且把门锁打开才能使用。在计算机中要对某个公共资源进行操作时必须对公共资源进行上锁然后才能使用。如果不上锁那么就可能导致数据混乱的情况。在Linux内核中比较常用的锁有自旋锁、信号量、读写锁 等下面介绍一下自旋锁和信号量的实现。自旋锁自旋锁 只能在多核CPU系统中其核心原理是 原子操作原理如下图spinlock使用 自旋锁 时必须先对自旋锁进行初始化设置为1上锁过程如下对自旋锁 lock 进行减一操作判断结果是否等于0如果是表示上锁成功并返回。如果不等于0表示其他进程已经上锁此时必须不断比较自旋锁 lock 的值是否等于1表示已经解锁。如果自旋锁 lock 等于1跳转到第一步继续进行上锁操作。由于Linux的自旋锁使用汇编实现所以比较苦涩难懂这里使用C语言来模拟一下void spin_lock(amtoic_t *lock)
{
again:result --(*lock);if (result 0) {return;}while (true) {if (*lock 1) {goto again;}}
}
上面代码将 result --(*lock); 当成原子操作解锁过程只需要把 lock 设置为1即可。由于自旋锁会不断尝试上锁操作并不会对进程进行调度所以在单核CPU中可能会导致 100% 的CPU占用率。另外自旋锁只适合粒度比较小的操作如果操作粒度比较大就需要使用信号量这种可调度进程的锁。信号量与 自旋锁 不一样当当前进程对 信号量 进行上锁时如果其他进程已经对其进行上锁那么当前进程会进入睡眠状态等待其他进程对信号量进行解锁。过程如下图semaphore在Linux内核中信号量使用 struct semaphore 表示定义如下struct semaphore {raw_spinlock_t lock;unsigned int count;struct list_head wait_list;
};
各个字段的作用如下lock自旋锁用于对多核CPU平台进行同步。count信号量的计数器上锁时对其进行减一操作(count--)如果得到的结果为大于等于0表示成功上锁如果小于0表示已经被其他进程上锁。wait_list正在等待信号量解锁的进程队列。信号量 上锁通过 down() 函数实现代码如下void down(struct semaphore *sem)
{unsigned long flags;spin_lock_irqsave(sem-lock, flags);if (likely(sem-count 0))sem-count--;else__down(sem);spin_unlock_irqrestore(sem-lock, flags);
}
上面代码可以看出down() 函数首先对信号量进行自旋锁操作为了避免多核CPU竞争然后比较计数器是否大于0如果是对计数器进行减一操作并且返回否则调用 __down() 函数进行下一步操作。__down() 函数实现如下static noinline void __sched __down(struct semaphore *sem)
{__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}static inline int __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 0;for (;;) {...__set_task_state(task, state);spin_unlock_irq(sem-lock);timeout schedule_timeout(timeout);spin_lock_irq(sem-lock);if (waiter.up) // 当前进程是否获得信号量锁?return 0;}...
}
__down() 函数最终调用 __down_common() 函数而 __down_common() 函数的操作过程如下把当前进程添加到信号量的等待队列中。切换到其他进程运行直到被其他进程唤醒。如果当前进程获得信号量锁由解锁进程传递那么函数返回。接下来看看解锁过程解锁过程主要通过 up() 函数实现代码如下void up(struct semaphore *sem)
{unsigned long flags;raw_spin_lock_irqsave(sem-lock, flags);if (likely(list_empty(sem-wait_list))) // 如果没有等待的进程, 直接对计数器加一操作sem-count;else__up(sem); // 如果有等待进程, 那么调用 __up() 函数进行唤醒raw_spin_unlock_irqrestore(sem-lock, flags);
}static noinline void __sched __up(struct semaphore *sem)
{// 获取到等待队列的第一个进程struct semaphore_waiter *waiter list_first_entry(sem-wait_list, struct semaphore_waiter, list);list_del(waiter-list); // 把进程从等待队列中删除waiter-up 1; // 告诉进程已经获得信号量锁wake_up_process(waiter-task); // 唤醒进程
}
解锁过程如下判断当前信号量是否有等待的进程如果没有等待的进程, 直接对计数器加一操作如果有等待的进程那么获取到等待队列的第一个进程。把进程从等待队列中删除。告诉进程已经获得信号量锁唤醒进程。推荐阅读专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈关注公众号后台回复「1024」获取学习资料网盘链接。欢迎点赞关注转发在看您的每一次鼓励我都将铭记于心~嵌入式Linux微信扫描二维码关注我的公众号