Linux-内核、线程、进程同步互斥方法及IPC方法的总结
2024年10月 · 预计阅读时间: 12 分钟
内核 | 线程 | 进程 | |
---|---|---|---|
机制 | 原子操作、自旋锁、信号量、互斥量 | 互斥量、读写锁、条件变量、信号量(有些书把信号量放在 IPC 通信里面,因为它可以被用于进程间通信同步) | IPC:管道(无名管道)、FIFO(有名管道)、消息队列、信号(Signals)、共享内存、套接字 (Sockets) |
特点 | 提供了多种用于进程间通信和线程同步的基础设施 | 协调同一进程中不同线程对共享数据的访问 | 在不同的进程之间传递数据 |
#
1.内核解决并发与竞态的方法这个部分在 Linux 内核学习笔记中,已经做过关于内部实现的笔记。
Linux 驱动-同步互斥与原子变量 - 认真学习的小诚同学
Linux 驱动-内核锁的介绍与使用 - 认真学习的小诚同学
Linux 驱动-内核自旋锁 spinlock 的实现 - 认真学习的小诚同学
Linux 驱动-内核信号量 semaphore 的实现 - 认真学习的小诚同学
Linux 驱动-内核互斥量 mutex 的实现 - 认真学习的小诚同学
这里做个总结:
内核锁分为
- 自旋锁(无法获得锁时,当前线程原地等待):原始自旋锁(raw_spinlock_t)、位自旋锁(bit spinlocks)
- 睡眠锁(无法获得锁时,当前线程就会休眠):互斥量、实时互斥锁、信号量、读写信号量等。
机制 | 实现方式 |
---|---|
自旋锁 | UP(单 CPU):支持抢占的系统,关闭调度器;不支持抢占的系统,本身就独占了 SMP(多核):使用原子操作 ldrex 和 strex 完成互斥 |
信号量 | 通过自旋锁 lock 和计数量 count,以及一个等待队列(阻塞的线程放在其中) |
互斥量 | 同样通过自旋锁 lock 和二值计数量 count,以及一个等待队列(阻塞的线程放在其中) 实现了一个快速路径 fastpath-在大部分情况下都可以直接在此获得或释放锁,所以互斥量比信号量的效率高 |
#
2.线程间同步线程同步(synchronization)是指在一定的时间内只允许某一个线程访问某个共享资源。而在此时间内,不允许其它的线程访问该资源。
#
2.1 互斥量从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
#
2.1.1 API功能 | API | 描述 |
---|---|---|
创建 | int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_t restrict attr); | 返回:若成功返回 0,否则返回错误编号 |
销毁 | int pthread_mutex_destroy(pthread_mutex_t mutex); | 返回:若成功返回 0,否则返回错误编号 |
加锁 | int pthread_mutex_lock(pthread_mutex_t mutex); int pthread_mutex_trylock(pthread_mutex_t mutex); | 返回:若成功返回 0,否则返回错误编号; 调用 pthread_mutex_lock,如果互斥量已经上锁, 调用线程阻塞直到互斥量被解锁。 如果不希望被阻塞,可以使用 pthread_mutex_trylock 尝试加锁,无论成功都会返回。 |
解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); |
#
2.1.2 死锁问题【并发 bug 和应对 (死锁/数据竞争/原子性违反;防御性编程和动态分析) 南京大学 2022 操作系统-P8】 【精准空降到 24:22】
死锁只有同时满足以下四个条件才会发生:
- 互斥:一个资源每次只能被一个进程使用;
- 持有并等待:一个进程请求资源阻塞时,不释放已获得的资源;
- 不可剥夺:进程已获得的资源不能被强行剥夺;
- 环路等待:若干进程之间形成头尾相接的循环等待资源关系。
解决方法就是经典的银行家算法。
#
2.1.3 示例#
2.2 读写锁与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
适用场景:读写锁非常适合于对数据结构读的次数远大于写的情况。
#
2.2.1 API功能 | API | 描述 |
---|---|---|
创建 | int pthread_rwlock_init(pthread_rwlock_t restrict rwlock, const pthread_rwlockattr_t restrict attr); | 返回:若成功返回 0,否则返回错误编号 |
销毁 | int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); | 返回:若成功返回 0,否则返回错误编号 |
加锁 | int pthread_rwlock_rdlock(pthread_rwlock_t rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t rwlock); | 返回:若成功返回 0,否则返回错误编号; |
解锁 | int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
#
2.2.2 示例#
2.3 信号量可以理解为一个计数器,表示当前可用共享资源的数量。
信号量基于操作系统的 PV 操作,对信号量的操作都是原子操作;
信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
支持信号量组;
有两套信号量:System V 和 Semaphore
#
2.3.1 API<sys/sem.h>
- System V 信号量
特性:
属于 System V IPC(进程间通信)。
提供了一组复杂的功能,适合复杂的进程间同步控制。
典型操作包括
semget()
(获取/创建信号量)、semop()
(操作信号量)和semctl()
(控制信号量集)。支持信号量集的概念,可以一次性操作多个信号量,适用于高级同步需求。
int semget(key_t key, int num_sems, int sem_flags);
// 创建或获取一个信号量组:若成功返回信号量集 ID,失败返回-1int semctl(int semid, int sem_num, int cmd, ...);
// 控制信号量的相关信息int semop(int semid, struct sembuf semoparray[], size_t numops);
// 对信号量组进行操作,改变信号量的值:成功返回 0,失败返回-1
使用场景:
- 适用于需要细粒度、复杂同步的多进程应用程序。
- 在需要跨越不同应用程序的持久性 IPC 时(System V 信号量可以在系统重启后仍保持存在,直到显式删除)。
复杂性:
- 相对复杂,使用起来需要理解其数据结构和操作指标。
- 相关数据结构和操作通常需要更复杂的设置和管理。需要使用
union semun
进行一些配置,这是标准的一个例外之处。
<semaphore.h>
- POSIX 信号量
- 特性:
- 属于 POSIX 标准。
- 提供了更简单易用的信号量 API。典型操作包括
sem_init()
(初始化信号量)、sem_wait()
(等待信号量)、sem_post()
(释放信号量)和sem_destroy()
(销毁信号量)。 - 可以是命名信号量或无名信号量。命名信号量可以用于进程间同步,而无名信号量通常用于线程间同步。
- 使用场景:
- 适用于进程间或线程间的简单同步。
- 易于使用,集成到标准 C 库中,大多数现代系统都支持。
- 复杂性:
- 易于使用和管理,适合需要简单同步机制的应用。
- 不支持信号量集,但更适合于一般的多线程和多进程同步场景。
#
2.3.2 示例#
2.4 条件变量条件变量是一种同步机制,允许线程挂起,直到共享数据上的某些条件得到满足。
主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使“条件成立”.
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
#
2.4.1 API#
2.4.2 示例#
3.进程间通信#
3.1 管道通常指无名管道。
- 数据只能在一个方向上流动, 具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
#
3.1.1 API返回值:若成功则返回 0,若出错则返回-1; 说明:由参数 filedes 返回两个文件描述符:filedes[0]为读而打开;filedes[1]为写而打开。
#
3.1.2 示例#
3.2 FIFO命名管道,它是一种文件类型。
- FIFO 可以在无关的进程之间交换数据,与无名管道不同。
- FIFO 有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
返回值:成功返回 0,出错返回-1;
其中的 mode 参数与 open 函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件 I/O 函数操作它。
示例:
#
3.3 消息队列消息队列就是消息的链表,存放在内核中。一个消息队列由一个标识符(即队列 ID)来标识。
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
#
3.3.1 API在以下两种情况下,msgget 将创建一个新的消息队列:
- 如果没有与键值 key 相对应的消息队列,并且 flag 中包含了 IPC_CREATE
- key 参数为 IPC_PRIVATE
函数 msgrcv 在读取消息队列时,type 参数有下面几种情况:
- type == 0,返回队列中的第一个消息;
- type > 0,返回队列中消息类型为 type 的第一个消息;
- type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来
#
3.3.2 示例服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来。
#
3.4 信号信号和信号量是完全不同的两个概念
#
3.4.1 有哪些信号Linux 系统中有许多信号,其中前面 31 个信号都有一个特殊的名字,对应一个特殊的事件,这些信号都是从 Unix 系统继承下来的,他们还有个名称叫“不可靠信号”,他们有如下的特点:
- 非实时信号不排队,信号的响应会相互嵌套。
- 如果目标进程没有及时响应非实时信号,那么随后到达的该信号将会被丢弃。
- 每一个非实时信号都对应一个系统事件,当这个事件发生时,将产生这个信号。
- 如果进程的挂起信号中含有实时和非实时信号,那么进程优先响应实时信号并且会从大到小依此响应,而非实时信号没有固定的次序。
后面的 31 个信号(从 SIGRTMIN[34]
到 SIGRTMAX[64]
)是 Linux 系统新增的实时信号,也被称为“可靠信号”,这些信号的特征是:
- 实时信号的响应次序按接收顺序排队,不嵌套。
- 即使相同的实时信号被同时发送多次,也不会被丢弃,而会依次挨个响应。
- 实时信号没有特殊的系统事件与之对应
对以上信号,需要着重注意的是:
1,上表中罗列出来的信号的“值”,在 x86、PowerPC 和 ARM
平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
2,“备注”中注明的事件发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用函数 kill()
来产生任何信号。
3,信号 SIGKILL
和 SIGSTOP
是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
换句话说,除了这两个信号之外的其他信号,接收信号的目标进程按照如下顺序来做出反应:
A) 如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。否则进入 B。
B) 如果该信号被捕捉,那么进一步判断捕捉的类型:
B1) 如果设置了响应函数,那么执行该响应函数。
B2) 如果设置为忽略,那么直接丢弃该信号。
否则进入 C。
C) 执行该信号的缺省动作
#
3.4.2 APIsignal
函数:- 用于设置一个简单的信号处理器。
- 原型:
void (*signal(int sig, void (*func)(int)))(int);
- 使用时,指定要捕获的信号以及处理信号的函数。
sigaction
函数:- 提供了更强大和灵活的信号处理设置。
- 原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 可以将
struct sigaction
的sa_handler
设置为SIG_IGN
来忽略信号或SIG_DFL
恢复默认处理。 - 提供了一种可靠的方法来设置信号处理程序,并支持更多功能,如信号阻塞集。
raise
函数:- 发送信号给调用进程本身。
- 原型:
int raise(int sig);
- 实际上相当于调用
kill(getpid(), sig);
。
kill
函数:- 发送信号给指定进程。
- 原型:
int kill(pid_t pid, int sig);
- 可以用来向一个或多个进程发送信号。
……
#
3.5 共享内存指两个或多个进程共享一个给定的存储区.
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取
- 因为多个进程可以同时操作,所以需要进行同步
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
#
3.5.1 API当用 shmget 函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,将 size 指定为 0。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用 shmat 函数连接该共享内存到当前进程的地址空间,随后可以访问。
shmdt 函数是用来断开 shmat 建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl 函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
#
3.5.2 示例【共享内存+信号量+消息队列】:
- 消息队列将用于初始化通信。在这种情况下,客户端可以请求服务,同时也可以用来通知服务器有新的请求需要处理。
- 共享内存将作为数据交换的主要手段,因为它允许两个进程共享和访问同一内存段。
- 信号量用于同步,以便控制对共享内存的访问,防止竞态条件。
#
3.6 套接字Socket 支持不同主机上的两个进程 IPC。这里不详细介绍了,可以参考网络编程相关知识。