引言
API预备知识(POSIX):https://zhangjiaju.blog.csdn.net/article/details/128673451
知道了什么是管道,什么是消息队列之后,我们来看一下什么是信号量。
在写这篇文章的时候,提到了线程,但我又没有系统性的讲解过线程,关于线程,我打算讲解完进程之后给大家补一下这方面的知识点。
线程的概念和进程的概念是不一样的,但使用上却极其的相似,包括他们所占用的内存空间等等,笔者在后面介绍线程时候给大家拆解。
在这里线程扫盲的话,可以先看一下老版本笔者写的文章,当遇到看不懂的API时候,可以参考一下。⭐️"传送门"⭐️
我总能找到回家的路
——家驹(StrangeHead)
我们常说:信号量,信号量,什么是信号呢?
信号是计算机的词语,在我们日常生活当中,信号我们换一个叫法,可以叫通知。
我们举一个例子:小驹是一家公司的苦逼程序员,为了项目能够成功上线,老板每天催催催,催催催。 有些事情当然是老板莫名其妙发牢骚,有些时候事情确实比较紧急,不断的打你工位的电话。…。
而小驹只能回复,好的,明白。。。
对于老板的 催催催(通知),
小驹可以有几种做法
- 处理
- 忽略
- 将任务排后
- 甚至冲进办公室将老板的电话砸掉,让他无法打电话
将小驹收到通知的行为,可以换成信号的三种处理方式等:忽略,捕捉,默认动作。
信号的三种常用操作:忽略,捕捉,默认动作。
1.忽略信号:多少信号都可以通过这个来处理,但是有俩种信号不能忽略,SIGKILL,SIGSTOP因为他们向超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人管的进程,显然Linux内核设计者不希望看到这个场景。
2.捕捉信号:需要告诉内核,用户如何处理信号,就是写一个信号处理函数来捕捉信号,然后处理函数,当信号产生时由Linux内核来调用你这个函数来执行信号发生的处理,这就是信号的捕捉及使用。
3.默认动作:对于每个信号,系统都会默认执行一些动作,当发送信号,系统就会自动执行,多数来说,都是直接杀死进程的粗暴方法。
使用kill -7 signal
来查看系统具体的定义。
信号就是类似单片机中断,单片机是硬件中断,而这里的信号则是软中断,比如你在看电视,快递到了你得去开门拿快递,这时快递员敲门被你听到了,这就是信号,这时你可以去开门去处理这个信号,也可以忽略,我就不收你能这么办,这就是信号的处理,如果此时快递员听到你不理他,他强行进来把你门打烂把你的快递给你,这就是忽略不了的信号,如kill -9 xxxx的信号;
kill是Linux的信号发送
String head小贴士:使用方法kill -l查看所有kill信号
平常退出进程使用的Ctrl+c就是kill方法里面的一种
信号的种类有这么多:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
平常使用的kill -9 xxxx ,这个-9就是上面的杀死信号、xxxx是进程id,也可以写成kill -SIGKILL xxxx
如Ctrl+c也是信号调用了上面序列号为3的功能给当前进程类似于kill -2 当前进程id
kill实质是指定干什么,及作用到哪些进程里面。
只不过平时音译过来kill是杀死的意思,所以听到kill就说他是杀死进程的指令。实则是给进程发信号执行。
捕捉信号
入门函数 :
Signal
andkill
重点在动作没有其他消息
高级函数:可以传递信息
初级函数Signal的使用方法,初级函数
接受信号函数原型(入门)
指针函数
typedef void (*sighandler_t)(int);
俩个参数代表,检测的指令,处理函数
sighandler_t signal(int signum, sighandler_t handler);
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
void handler(int signal) //处理函数
{
printf("signal = %d\n",signal); //打印触发的信号指令代码
printf("never quit\n"); //处理内容
}
int main()
{
printf("pid = %d\n",getpid()); //打印当前进程pid
signal(SIGINT,handler); //捕捉Ctrl+C的信号
while(1); //防止程序退出
return 0;
}
执行结果
信号发送(入门)
俩个参数代表,目标pid,指令
发送信号函数原型int kill(pid_t pid, int sig);
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
// int kill(pid_t pid, int sig);
int main(int argc,int **argv) //运行代码时接受俩个值
{
int pid;
int signum;
pid = atoi(argv[2]); //ascii转int
signum = atoi(argv[1]);
printf("pid = %d\n",pid);
printf("signum = %d\n",signum);
kill(pid,signum); //发送指令
printf("send signal ok\n");
return 0;
}
执行结果
Linux 信号量是什么(快速扫盲)
在 Linux 系统编程中,信号量(Semaphore)是一种重要的同步机制,主要用于解决并发访问的互斥和同步问题。
在多线程、多进程的编程环境中,信号量能够控制对共享资源的访问,确保操作的安全性。
大家可能对这句话不是很敏感,现在笔者在项目中,可是吃了不少这样的并发问题,两个线程或者进程,甚至多个,如果在某一个瞬间,同时操作了一个变量,那么悲催的事情就要来了。你的程序会直接死机!!
提示什么Double free
或者奇怪的一些问题。。。
所以要么用线程锁保护起来,要么用信号量严格把控你的线程或者进程的逻辑。
本文将详细介绍信号量的基本概念、常见类型、以及在 Linux 环境下如何使用信号量进行编程。以及使用POSIX信号量
的使用的实例代码。帮助学员们理解这个东东。
Linux 提供了两种信号量实现
信号量有两种实现,一个是System V信号量
,另一个是POSIX信号量
。本节围绕POSIX信号量
作为实例讲解,下节我们将使用并学习System V信号量
.
System V 信号量
较为传统,使用较多,但控制复杂。
POSIX 信号量
较新且更为简单,推荐在多线程编程中使用。
这里仅仅让学员们知道,Linux系统当中是有俩套API的,千万别学混咯,这篇文章的代码实例主要是基于POSIX信号量开始,他比较简单,可以快速上手,帮助学员们理解信号量。
System V 信号量 和 POSIX 信号量 都可以用于进程间同步;然而,POSIX 信号量 由于设计上的优势,也可以直接用于线程间同步。以下是两者的详细比较与用途解析:
System V 信号量与 POSIX 信号量的主要区别
特性 | System V 信号量 | POSIX 信号量 |
---|---|---|
主要用途 | 进程间同步 | 线程间或进程间同步 |
信号量类型 | 支持信号量集合 | 单个信号量 |
初始化方式 | 使用 semget 创建信号量集合,操作较复杂 | 可直接通过 sem_init 或 sem_open 初始化 |
API | System V 专有 API (semget , semop , semctl ) | POSIX 标准 API (sem_init , sem_wait ) |
跨进程支持 | 需要配合 IPC 键值 (ftok ) 实现跨进程共享 | 可通过共享内存或命名信号量 (sem_open ) 实现 |
线程支持 | 不直接支持线程同步 | 支持线程和进程同步,线程同步更高效 |
灵活性 | 支持信号量集合操作,适合复杂的进程间通信 | 操作简单,适合轻量级同步场景 |
删除方式 | 显式删除 (IPC_RMID ) | 命名信号量需显式删除 (sem_unlink ),未命名无需显式销毁 |
System V 信号量:进程间同步的经典方案
System V 信号量诞生于早期 UNIX 系统,设计初衷是为 进程间同步 提供服务。其信号量集合的概念,使其非常适合复杂的进程间通信场景。
- 信号量集合:一次可以管理多个信号量(多个共享资源)。
- 跨进程共享:通过唯一的键值 (
key_t
) 实现多个进程间共享同一信号量。
尽管 System V 信号量也可以用于线程同步,但由于操作复杂且性能较低,在现代编程中并不常用于线程间同步。
POSIX 信号量:线程间和进程间同步的现代选择
POSIX 信号量更轻量化,API 简洁,能够同时满足线程和进程同步需求:
-
线程间同步:
sem_init
创建未命名信号量,适合线程同步。- 信号量存在于进程的地址空间中,线程可以直接访问。
-
进程间同步:
- 通过
sem_open
创建命名信号量,并将其映射到系统范围内的命名空间。 - 多个进程可以通过相同的名称共享该信号量。
- 通过
POSIX 信号量因其灵活性和高效性,通常在多线程编程中被优先选择。
使用场景总结
-
System V 信号量:
适用于复杂的进程间同步,尤其是需要使用信号量集合来管理多资源的场景。示例:- 数据库进程池管理。
- IPC 缓冲区管理。
-
POSIX 信号量:
更通用,适合线程同步或简单的进程间同步。示例:- 多线程间的资源互斥。
- 生产者-消费者模型的线程或进程实现。
推荐使用场景
-
如果仅涉及线程同步:
首选 POSIX 信号量。它与线程的内存模型更匹配,操作简单,性能优越。 -
如果涉及复杂的进程间通信(多个资源共享):
使用 System V 信号量,其集合特性能够更好地处理复杂的同步需求。 -
如果是跨平台开发:
POSIX 信号量通常是更现代和标准的选择,具备更好的可移植性。
信号量的种类
信号量是一种特殊的计数器,用于控制多线程或多进程对共享资源的访问。最早由计算机科学家 Edsger Dijkstra 引入,信号量有两种类型:
- 计数信号量(Counting Semaphore):用于表示可以并发访问资源的数量。
- 二值信号量(Binary Semaphore):只取值 0 或 1,常用于互斥量(Mutex)的实现。
计数信号量(Counting Semaphore)
计数信号量(Counting Semaphore)是信号量的一种类型,用于控制多个线程或进程对共享资源的并发访问。它是一个具有整数值的变量,通过两种操作(通常称为 P
和 V
或 wait
和 signal
)对其进行修改。计数信号量的值可以是非负整数,表示可用资源的数量。
计数信号量的特点
-
整数值表示资源数量:
- 初始值通常设定为可用资源的总数。
- 值为正数时,表示有多少个资源可以被访问。
- 值为 0 时,表示没有可用资源,进程或线程需要等待。
- 值不能为负数。
-
同步控制:
- 减少信号量(P 操作或 wait):表示一个进程/线程请求使用资源。
- 如果信号量值 > 0,则资源可用,减少信号量值。
- 如果信号量值 = 0,则阻塞进程/线程直到资源可用。
- 增加信号量(V 操作或 signal):表示一个进程/线程释放资源。
- 信号量值增加,可能唤醒等待的进程/线程。
- 减少信号量(P 操作或 wait):表示一个进程/线程请求使用资源。
-
适用于多资源共享:
- 计数信号量允许多个线程同时访问有限的资源,例如线程池、数据库连接池等。
计数信号量的工作原理
假设一个资源池中有 5 个资源,计数信号量的初始值设为 5
。多个线程需要访问这些资源:
- 初始信号量值为 5,表示有 5 个资源可用。
- 第一个线程请求资源:
- 执行 P 操作,信号量值减为 4。
- 线程获得资源。
- 第二个线程请求资源:
- 执行 P 操作,信号量值减为 3。
- 线程获得资源。
- 一个线程释放资源:
- 执行 V 操作,信号量值加为 4。
- 表示有一个资源可用。
- 如果所有资源都被占用,信号量值变为 0。
- 请求资源的线程将被阻塞,直到有资源被释放。
计数信号量的实现
使用 POSIX 信号量实现:
以下代码展示了如何使用计数信号量控制对有限资源的访问:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define NUM_RESOURCES 3 // 资源数量
#define NUM_THREADS 5 // 线程数量
sem_t semaphore; // 定义计数信号量
void* thread_func(void* arg) {
int id = (int)(long)arg;
printf("Thread %d: Waiting for resource...\n", id);
sem_wait(&semaphore); // P 操作,减少信号量
printf("Thread %d: Acquired resource!\n", id);
sleep(2); // 模拟资源使用
printf("Thread %d: Releasing resource...\n", id);
sem_post(&semaphore); // V 操作,增加信号量
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
// 初始化信号量,初始值为 NUM_RESOURCES
sem_init(&semaphore, 0, NUM_RESOURCES);
// 创建线程
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, thread_func, (void*)(long)i);
}
// 等待线程完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// 销毁信号量
sem_destroy(&semaphore);
return 0;
}
运行结果(输出示例):
Thread 0: Waiting for resource...
Thread 0: Acquired resource!
Thread 1: Waiting for resource...
Thread 1: Acquired resource!
Thread 2: Waiting for resource...
Thread 2: Acquired resource!
Thread 3: Waiting for resource...
Thread 0: Releasing resource...
Thread 3: Acquired resource!
Thread 4: Waiting for resource...
Thread 1: Releasing resource...
Thread 4: Acquired resource!
Thread 2: Releasing resource...
Thread 3: Releasing resource...
Thread 4: Releasing resource...
计数信号量的应用场景
- 线程池:
- 控制同时运行的线程数量。
- 数据库连接池:
- 限制同时访问数据库的连接数。
- 资源分配:
- 管理有限资源的访问,例如磁盘块、内存缓冲区。
- 网络服务器:
- 控制同时连接的客户端数量。
与二值信号量的对比
特点 | 计数信号量 | 二值信号量 |
---|---|---|
值的范围 | 非负整数 | 0 或 1 |
应用场景 | 控制多个资源的访问 | 控制对单个资源的互斥访问 |
线程/进程的数量 | 支持多个线程/进程同时访问 | 只允许一个线程/进程访问资源 |
总结
计数信号量是强大的同步工具,适用于控制对有限资源的并发访问。通过学习其概念、API 和编程实践,可以灵活地应用于多线程编程和进程间通信中。
通俗易懂的来讲,信号量就是为了保护某一个东西,相当于给某个物品上锁,锁是公共的,每个人都可以看到,那么既然有锁一定有钥匙去开这把锁,然后拿到里面的东西,去使用。
所谓的P操作就是用钥匙开这把锁(开完锁之后钥匙自动销毁),V操作就是制造一把钥匙给下一个开锁的人(钥匙加一)。
二值信号量(Binary Semaphore)
二值信号量(Binary Semaphore)是一种特殊类型的信号量,其值仅能是 0
或 1
。二值信号量通常用于解决互斥(Mutex)问题,即确保同一时间只有一个线程或进程访问某个关键资源或执行临界区代码。
二值信号量的特点
-
值范围:
- 只有两种状态:
0
(不可用)和1
(可用)。 - 初始值通常设为
1
,表示资源可用。
- 只有两种状态:
-
P(wait)操作:
- 当信号量值为
1
时,P 操作将其值减为0
,表示资源被占用。 - 如果信号量值为
0
,执行 P 操作的线程或进程会阻塞,直到信号量值恢复为1
。
- 当信号量值为
-
V(signal)操作:
- 将信号量值从
0
置为1
,表示资源被释放,可以被其他线程或进程使用。
- 将信号量值从
-
应用场景:
- 用于实现互斥(类似于 Mutex)。
- 用于简单的线程同步。
二值信号量的工作原理
假设一个线程需要访问一个临界资源:
- 信号量初始值为
1
,表示资源可用。 - 线程 A 执行 P 操作:
- 检查信号量值是否为
1
。 - 如果是,减为
0
并进入临界区。 - 如果信号量值为
0
,线程 A 被阻塞。
- 检查信号量值是否为
- 线程 A 释放资源:
- 执行 V 操作,将信号量值恢复为
1
。 - 如果有其他线程在等待,则唤醒它们。
- 执行 V 操作,将信号量值恢复为
二值信号量的实现
POSIX 信号量实现
以下是一个使用 POSIX 信号量实现二值信号量的示例:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t binary_semaphore; // 定义二值信号量
void* thread_func(void* arg) {
int id = (int)(long)arg;
printf("Thread %d: Waiting to enter critical section...\n", id);
sem_wait(&binary_semaphore); // P 操作,尝试进入临界区
printf("Thread %d: Entered critical section.\n", id);
sleep(2); // 模拟临界区操作
printf("Thread %d: Exiting critical section.\n", id);
sem_post(&binary_semaphore); // V 操作,释放临界区
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化二值信号量,初始值为 1
sem_init(&binary_semaphore, 0, 1);
// 创建两个线程
pthread_create(&t1, NULL, thread_func, (void*)1);
pthread_create(&t2, NULL, thread_func, (void*)2);
// 等待两个线程完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁信号量
sem_destroy(&binary_semaphore);
return 0;
}
运行结果(输出示例):
Thread 1: Waiting to enter critical section...
Thread 1: Entered critical section.
Thread 2: Waiting to enter critical section...
Thread 1: Exiting critical section.
Thread 2: Entered critical section.
Thread 2: Exiting critical section.
二值信号量 vs 互斥锁
比较项 | 二值信号量 | 互斥锁(Mutex) |
---|---|---|
值范围 | 0 或 1 | 锁定或解锁 |
用途 | 通常用于同步或简单互斥 | 专门用于互斥 |
拥有者概念 | 无特定的拥有者,任何线程可以释放信号量 | 有明确的拥有者,只有拥有锁的线程可以解锁 |
使用场景 | 线程同步和互斥 | 仅用于互斥 |
二值信号量的应用场景
- 线程同步:
- 控制线程的执行顺序,例如一个线程需要等待另一个线程完成后再执行。
- 临界区保护:
- 确保同一时间只有一个线程可以访问共享资源。
- 简单的生产者-消费者模型:
- 限制生产者和消费者对共享缓冲区的访问。
二值信号量示例:线程同步
以下示例展示如何使用二值信号量实现线程间的同步:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sync_semaphore; // 定义二值信号量
void* thread_func1(void* arg) {
printf("Thread 1: Performing task...\n");
sleep(2); // 模拟任务
printf("Thread 1: Task complete, signaling thread 2.\n");
sem_post(&sync_semaphore); // V 操作,通知线程 2
return NULL;
}
void* thread_func2(void* arg) {
printf("Thread 2: Waiting for thread 1 to complete...\n");
sem_wait(&sync_semaphore); // P 操作,等待线程 1
printf("Thread 2: Received signal from thread 1, continuing task.\n");
return NULL;
}
int main() {
pthread_t t1, t2;
// 初始化二值信号量,初始值为 0
sem_init(&sync_semaphore, 0, 0);
// 创建线程
pthread_create(&t1, NULL, thread_func1, NULL);
pthread_create(&t2, NULL, thread_func2, NULL);
// 等待线程完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁信号量
sem_destroy(&sync_semaphore);
return 0;
}
运行结果(输出示例):
Thread 1: Performing task...
Thread 2: Waiting for thread 1 to complete...
Thread 1: Task complete, signaling thread 2.
Thread 2: Received signal from thread 1, continuing task.
总结
- 二值信号量适合用于互斥和简单的线程同步任务。
- 如果你的需求是对共享资源的访问控制,且仅允许一个线程访问,二值信号量是一个不错的选择。
- 需要更高级的互斥功能(如递归锁或超时机制)时,可以考虑使用互斥锁。
结束
持续更新,分享优质内容,加油!.