信号量和信号
2025/12/21大约 7 分钟
信号量和信号
- 信号量和信号的区别
基本定义
信号(Signal)
- 信号是Unix/Linux系统中用于进程间通信的一种机制
- 是一种软件中断,用于通知进程某个事件已经发生
- 信号是异步的,进程无法预知信号何时到达
- 本质上是一个整数编号,代表特定的事件类型
信号量(Semaphore)
- 信号量是一种用于进程/线程同步的计数器
- 用于控制对共享资源的访问,实现进程/线程间的互斥和同步
- 包含一个整数值和两个原子操作:P操作(wait/down)和V操作(signal/up)
- 由荷兰计算机科学家Dijkstra提出
核心区别
1. 用途不同
- 信号:用于进程间的通知机制,告知进程某个事件发生了
- 例如:进程被杀死(SIGKILL)、定时器到期(SIGALRM)、子进程终止(SIGCHLD)
- 信号量:用于进程/线程间的同步和互斥,控制对共享资源的访问
- 例如:保护临界区、控制资源数量、实现生产者-消费者模型
2. 工作机制不同
- 信号:
- 发送方通过系统调用(如kill)发送信号
- 接收方通过信号处理函数响应信号
- 异步处理,不阻塞发送方
- 信号可能丢失(多次发送同一信号可能只收到一次)
- 信号量:
- P操作:信号量-1,如果<0则阻塞等待
- V操作:信号量+1,如果有进程在等待则唤醒一个
- 同步操作,可能阻塞调用者
- 不会丢失,精确计数
3. 携带信息不同
- 信号:只能携带信号类型(一个整数编号),信息量极少
- 标准信号:1-31(如SIGINT、SIGTERM)
- 实时信号:32-64(可携带少量额外数据)
- 信号量:只包含一个计数值,不携带其他信息
- 计数值表示可用资源数量或同步状态
4. 实现层次不同
- 信号:
- 由操作系统内核实现
- 涉及进程控制块(PCB)中的信号位图
- 需要内核参与信号的发送、接收和处理
- 信号量:
- 由操作系统提供的同步原语
- System V信号量或POSIX信号量
- 可以在用户态实现(如Go的channel内部使用信号量)
5. 适用范围不同
- 信号:
- 主要用于进程间通信
- 不适合线程间通信(一个进程的所有线程共享信号处理函数)
- 信号量:
- 可用于进程间同步(命名信号量)
- 可用于线程间同步(无名信号量)
- 应用更广泛
常见信号类型
| 信号名 | 编号 | 默认动作 | 说明 |
|---|---|---|---|
| SIGINT | 2 | 终止 | 中断信号(Ctrl+C) |
| SIGQUIT | 3 | 终止+core dump | 退出信号(Ctrl+\) |
| SIGKILL | 9 | 终止 | 强制杀死(不能捕获) |
| SIGSEGV | 11 | 终止+core dump | 段错误(非法内存访问) |
| SIGTERM | 15 | 终止 | 终止信号(优雅退出) |
| SIGCHLD | 17 | 忽略 | 子进程状态改变 |
| SIGSTOP | 19 | 停止 | 停止进程(不能捕获) |
| SIGALRM | 14 | 终止 | 定时器到期 |
信号量类型
1. 二值信号量(Binary Semaphore)
- 值只能为0或1
- 类似互斥锁,用于实现互斥访问
- 保护临界区
2. 计数信号量(Counting Semaphore)
- 值可以为任意非负整数
- 用于控制有限资源的访问
- 例如:数据库连接池(最多N个连接)
3. POSIX信号量
- 无名信号量:用于同一进程内的线程间同步
- 命名信号量:用于不同进程间同步
4. System V信号量
- 信号量集合,可以包含多个信号量
- 功能更强大但使用更复杂
Golang代码示例
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// ============ 信号示例 ============
// 信号处理示例
func signalExample() {
// 创建信号接收通道
sigChan := make(chan os.Signal, 1)
// 注册要接收的信号
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("等待信号... (按 Ctrl+C 发送 SIGINT)")
// 启动一个goroutine模拟工作
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
default:
fmt.Println("工作中...")
time.Sleep(1 * time.Second)
}
}
}()
// 等待信号
sig := <-sigChan
fmt.Printf("\n收到信号: %v\n", sig)
// 清理工作
close(done)
fmt.Println("优雅退出...")
}
// 发送信号给其他进程
func sendSignalExample() {
// 获取目标进程
targetPID := os.Getpid() // 这里用自己的PID做演示
process, err := os.FindProcess(targetPID)
if err != nil {
fmt.Println("找不到进程:", err)
return
}
// 发送信号
err = process.Signal(syscall.SIGTERM)
if err != nil {
fmt.Println("发送信号失败:", err)
}
}
// ============ 信号量示例 ============
// 使用channel实现信号量
type Semaphore struct {
ch chan struct{}
}
func NewSemaphore(n int) *Semaphore {
return &Semaphore{
ch: make(chan struct{}, n),
}
}
// P操作(获取资源)
func (s *Semaphore) Acquire() {
s.ch <- struct{}{}
}
// V操作(释放资源)
func (s *Semaphore) Release() {
<-s.ch
}
// 信号量使用示例 - 限制并发数
func semaphoreExample() {
// 创建一个容量为3的信号量,限制最多3个并发
sem := NewSemaphore(3)
var wg sync.WaitGroup
// 模拟10个任务
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 获取信号量(P操作)
sem.Acquire()
fmt.Printf("任务 %d 开始执行\n", id)
// 模拟工作
time.Sleep(1 * time.Second)
fmt.Printf("任务 %d 完成\n", id)
// 释放信号量(V操作)
sem.Release()
}(i)
}
wg.Wait()
fmt.Println("所有任务完成")
}
// 使用信号量实现互斥锁
type BinarySemaphore struct {
sem *Semaphore
}
func NewBinarySemaphore() *BinarySemaphore {
return &BinarySemaphore{
sem: NewSemaphore(1),
}
}
func (b *BinarySemaphore) Lock() {
b.sem.Acquire()
}
func (b *BinarySemaphore) Unlock() {
b.sem.Release()
}
// 互斥访问示例
func mutexExample() {
mutex := NewBinarySemaphore()
counter := 0
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mutex.Lock()
// 临界区
temp := counter
time.Sleep(10 * time.Millisecond) // 模拟操作
counter = temp + 1
fmt.Printf("Goroutine %d: counter = %d\n", id, counter)
mutex.Unlock()
}(i)
}
wg.Wait()
fmt.Printf("最终计数: %d\n", counter)
}
// 生产者-消费者模型(使用信号量)
func producerConsumerExample() {
bufferSize := 5
empty := NewSemaphore(bufferSize) // 空闲位置数
full := NewSemaphore(0) // 已用位置数
mutex := NewBinarySemaphore() // 保护缓冲区
buffer := make([]int, 0, bufferSize)
var wg sync.WaitGroup
// 生产者
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
item := id*10 + j
empty.Acquire() // 等待空闲位置
mutex.Lock() // 加锁
buffer = append(buffer, item)
fmt.Printf("生产者 %d 生产: %d (缓冲区大小: %d)\n", id, item, len(buffer))
mutex.Unlock() // 解锁
full.Release() // 增加已用位置
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 消费者
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 7; j++ {
full.Acquire() // 等待已用位置
mutex.Lock() // 加锁
item := buffer[0]
buffer = buffer[1:]
fmt.Printf("消费者 %d 消费: %d (缓冲区大小: %d)\n", id, item, len(buffer))
mutex.Unlock() // 解锁
empty.Release() // 增加空闲位置
time.Sleep(150 * time.Millisecond)
}
}(i)
}
wg.Wait()
}
func main() {
fmt.Println("=== 信号示例 ===")
// signalExample() // 需要交互,注释掉
fmt.Println("\n=== 信号量示例 - 限制并发 ===")
semaphoreExample()
fmt.Println("\n=== 二值信号量 - 互斥锁 ===")
mutexExample()
fmt.Println("\n=== 生产者-消费者模型 ===")
producerConsumerExample()
}对比总结表
| 特性 | 信号(Signal) | 信号量(Semaphore) |
|---|---|---|
| 主要用途 | 进程间通知/事件通知 | 进程/线程同步与互斥 |
| 通信方式 | 异步通知 | 同步等待 |
| 是否阻塞 | 不阻塞发送方 | 可能阻塞(P操作时) |
| 携带信息 | 信号类型(整数编号) | 计数值 |
| 可靠性 | 可能丢失 | 不丢失,精确计数 |
| 实现层次 | 内核实现 | 内核提供原语 |
| 适用对象 | 主要用于进程 | 进程和线程 |
| 典型场景 | 中断处理、异常通知 | 资源控制、临界区保护 |
| 操作 | 发送、捕获、处理 | P操作、V操作 |
| 可捕获性 | 部分可捕获(SIGKILL不可) | N/A |
关键点总结
- 信号是通知机制,信号量是同步机制
- 信号用于告知事件,信号量用于控制资源
- 信号是异步的,信号量操作可能同步阻塞
- 信号主要用于进程间,信号量可用于进程和线程
- 信号可能丢失,信号量精确计数不丢失
- 典型应用:信号用于优雅退出、定时器;信号量用于限流、互斥锁
实际应用场景
信号的应用:
- 优雅关闭服务(捕获SIGTERM/SIGINT)
- 子进程管理(SIGCHLD)
- 定时任务(SIGALRM)
- 热重载配置(SIGHUP)
- 调试工具(SIGTRAP)
信号量的应用:
- 数据库连接池(限制最大连接数)
- 线程池(控制工作线程数量)
- 限流器(控制请求并发数)
- 生产者-消费者队列
- 读者-写者问题
- 哲学家就餐问题