我们先来个宏观上的认识
首先我们要理解并发控制之所以要控制,是因为不同进程或线程对同一资源想要使用的时候会出现资源的抢占问题。对于同一资源的抢占,我们必须要实现不同程序的互斥,这个时候我们就可以借助各种所来进行并发控制,其中锁是我们的一个工具。
为什么要使用锁呢?其实在C语言层面,我们可以在软件层面进行定义,用一个变量来防止一个资源的被重复使用。但是在内核态中,我们是依靠时间片轮转的方式来进行不同现成的任务并行处理。在内核态中,每个调度的单元是任务,每一个任务可能只有10毫秒给你,当你时间用完之后内核就会把CPU的资源从你的任务中切出到另外一个任务。
我们都知道C语言的更底层是汇编语言,如果在我们用正常语言的过程中,CPU通过时间片轮转把我们所在的任务切出去了,而它正在执行C语言转到汇编语言中的某一条,就会导致我们正常的任务目标不能完成,所以我们要使用原子操作,这是无法被打断的单元来进行保护,我们后面的各种锁也是基于原子操作这一基础来进行的、
而在应用层中具有明确的线程跟进程的关系了,线程被包含在进程中的,不同线程共用资源所以更容易出现资源的抢占,而不同进程,由于他们的地址空间是独立的,所以一般不存在抢占资源的问题,更多的是同步的问题。
下面是在内核态中常见的处理资源抢占的工具
-
原子操作(atomic):通过硬件指令保证对整型变量的增减、读写等操作不可分割,不会被中断或其他 CPU 干扰,用于高效地保护简单计数或标志位。
-
自旋锁(spinlock):在获取不到锁时不断忙等(spin),不允许睡眠,适用于临界区极短、不能阻塞的场景;持有者释放锁后,等待者立即重试进入。
-
互斥锁(mutex):获取不到时会让调用者睡眠并加入等待队列,直到锁可用再被唤醒;适合较长临界区,但会有上下文切换开销。
-
信号量(semaphore):维护一个计数器,
down()(P 操作)失败时睡眠等待,up()(V 操作)唤醒;可用于控制对多个同类资源的并发访问。 -
读写锁(rwlock):区分读者和写者,允许多个读者并行进入,只在写者持锁时阻止其他读者/写者,适合读多写少的场景。
-
顺序锁(seqlock):写者独占且会更新版本号,读者乐观读取并在版本号改变时重试,读侧几乎无锁开销但需可能多次重试。
-
RCU(Read‑Copy‑Update):读者在临界区内几乎零开销(无需加锁),写者复制数据后更新指针,并在所有当前读者退出后再回收旧版本;非常适合读多写少。
-
中断屏蔽(local_irq_disable/enable):在本 CPU 上禁用/恢复本地中断,用于保护中断上下文中的临界区,确保不会被中断处理程序打断。
重点介绍自旋锁和互斥锁
自旋锁是忙等待的锁,它的开关锁之间的代码要尽可能的短,同时可以搭配中断进行配合使用,如spin_lock_irqsave函数
互斥锁是可以在遇到已锁情况下进行睡眠的锁,也就是可以切出去让CPU忙别的事,故也不能用于中断中,下面是基础操作。
#include <linux/mutex.h
static struct mutex lock; //定义互斥锁
mutex_init(&lock); // 初始化互斥锁
mutex_lock(&lock); //上锁 (无法获得,则阻塞睡眠)
mutex_unlock(&lock); //解锁
//睡眠(block):如果拿锁失败,当前进程会被挂起(blocked),调度器会切换到其他可运行进程。直到锁可用时,再被唤醒并重新竞争。因此,持有互斥锁的临界区里是允许做阻塞操作(比如 I/O、等待队列、耗时计算)的
多进程与多线程

多进程(multi-process)是一种并发执行机制,指的是一个程序可以创建多个独立的进程,这些进程彼此独立、互不共享内存空间,并可以同时在多个 CPU 或内核上运行,从而实现并发或并行处理
|
特性 |
多进程 |
多线程 |
|
地址空间 |
各进程独立 |
所有线程共享同一进程空间 |
|
崩溃隔离 |
一个进程崩溃不会影响另一个 |
一个线程崩溃可能导致整个进程崩溃 |
|
通信复杂度 |
高,需专门的 IPC 机制 |
低,直接访问共享变量 |
|
创建销毁开销 |
大 |
小 |
|
编程复杂度 |
相对简单 |
更复杂,需考虑线程安全、锁等问题 |
在 Linux 内核里,“时间片轮转”(time‐slice round robin)的调度单位是 任务(task),也就是 task_struct 结构体,通俗地说就是 线程(lightweight process),而不是“整个进程”这个更高的概念。具体来说:
Linux 把进程和线程都视作“任务(task)”
在 Linux 中,每个执行流(包括传统意义上的“线程”以及“进程”)都由一个 task_struct 描述。你启动一个多线程程序,fork() 出来的进程、clone() 出来的线程,都各自对应一个 task_struct。时间片分配给每个 task_struct
调度器给每个就绪的任务分配一个时间片(quantum),例如 10 ms。当一个任务的时间片用完,调度器就会在就绪队列里挑下一个任务来运行。这个“下一个任务”既可能是同一进程的另一个线程,也可能是完全不同进程的线程。
5万+

被折叠的 条评论
为什么被折叠?



