深入C语言底层系列23-信号与信号量
·
C语言中的信号(Signal)与信号量(Semaphore)详解
一、信号(Signal)
1. 概念与本质
信号是UNIX/Linux系统中一种异步事件通知机制,主要用于:
- 内核向用户进程通知系统事件
- 进程间通信(IPC)
- 进程自身处理异常情况
信号本质上是发送给进程的一个整数编号(1-31),表示特定事件的发生。当信号到达时,进程会中断当前执行流,转而处理信号。
2. 常见信号类型
| 信号名 | 值 | 描述 | 默认动作 |
|---|---|---|---|
| SIGHUP | 1 | 终端挂起或控制进程终止 | 终止 |
| SIGINT | 2 | 键盘中断(Ctrl+C) | 终止 |
| SIGQUIT | 3 | 键盘退出(Ctrl+\) | 终止+核心转储 |
| SIGILL | 4 | 非法指令 | 终止+核心转储 |
| SIGABRT | 6 | 来自abort()的终止信号 | 终止+核心转储 |
| SIGFPE | 8 | 浮点异常 | 终止+核心转储 |
| SIGKILL | 9 | 立即终止进程 | 终止 |
| SIGSEGV | 11 | 无效内存引用 | 终止+核心转储 |
| SIGPIPE | 13 | 管道破裂 | 终止 |
| SIGALRM | 14 | 定时器超时 | 终止 |
| SIGTERM | 15 | 终止信号 | 终止 |
| SIGUSR1 | 10 | 用户自定义信号1 | 终止 |
| SIGUSR2 | 12 | 用户自定义信号2 | 终止 |
| SIGCHLD | 17 | 子进程状态改变 | 忽略 |
3. 信号处理机制
进程可以通过三种方式处理信号:
- 默认处理:系统预定义的处理方式
- 忽略信号:完全忽略信号(SIGKILL和SIGSTOP除外)
- 自定义处理:注册信号处理函数
4. 核心API函数
a) signal() - 注册信号处理函数
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
// 简化理解:
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sigint_handler(int sig) {
printf("\nReceived SIGINT (Ctrl+C)\n");
// 重新注册处理函数
signal(SIGINT, sigint_handler);
}
int main() {
// 注册SIGINT处理函数
signal(SIGINT, sigint_handler);
printf("Press Ctrl+C to test...\n");
while(1) {
sleep(1);
}
return 0;
}
b) sigaction() - 更健壮的信号处理
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
};
示例:
#include <stdio.h>
#include <signal.h>
#include <string.h>
void handler(int sig, siginfo_t *info, void *ucontext) {
printf("Received signal %d from process %d\n", sig, info->si_pid);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler;
sa.sa_flags = SA_SIGINFO; // 使用扩展信号处理
sigaction(SIGINT, &sa, NULL);
printf("Waiting for signal...\n");
pause(); // 等待信号
return 0;
}
c) kill() - 发送信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
示例:
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <pid> <signal>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]);
int sig = atoi(argv[2]);
if (kill(pid, sig) == -1) {
perror("kill");
return 1;
}
printf("Sent signal %d to process %d\n", sig, pid);
return 0;
}
5. 信号集操作
#include <signal.h>
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 包含所有信号
int sigaddset(sigset_t *set, int signum); // 添加信号
int sigdelset(sigset_t *set, int signum); // 删除信号
int sigismember(const sigset_t *set, int signum); // 检查信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// how: SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
6. 信号处理注意事项
- 可重入性:信号处理函数应使用可重入函数(async-signal-safe)
- 原子操作:对共享数据的访问需要特殊处理
- volatile变量:使用volatile修饰在信号处理函数中访问的变量
- sig_atomic_t:使用此类型保证原子访问
二、信号量(Semaphore)
1. 概念与本质
信号量是一种同步原语,用于控制多个进程/线程对共享资源的访问。它包含:
- 一个整数计数器
- 等待队列
- 两个原子操作:P(wait)和 V(post)
2. 类型
- 无名信号量:用于线程间或相关进程间的同步
- 有名信号量:用于不相关进程间的同步
3. POSIX信号量API
a) 无名信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem); // P操作(阻塞)
int sem_trywait(sem_t *sem); // 非阻塞P操作
int sem_post(sem_t *sem); // V操作
int sem_destroy(sem_t *sem); // 销毁信号量
线程同步示例:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM_THREADS 5
sem_t semaphore;
int counter = 0;
void *thread_func(void *arg) {
sem_wait(&semaphore); // P操作
// 临界区开始
int temp = counter;
printf("Thread %ld: counter=%d\n", (long)arg, temp);
usleep(10000); // 模拟工作
counter = temp + 1;
// 临界区结束
sem_post(&semaphore); // V操作
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
// 初始化信号量(线程间共享,初始值1)
sem_init(&semaphore, 0, 1);
for (long i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, thread_func, (void *)i);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Final counter value: %d\n", counter);
sem_destroy(&semaphore);
return 0;
}
b) 有名信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
进程间同步示例:
// 进程1:创建并初始化信号量
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
int main() {
sem_t *sem = sem_open("/my_sem", O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
printf("Semaphore created. Waiting for other process...\n");
sem_wait(sem);
printf("Critical section in process 1\n");
sleep(2);
sem_post(sem);
sem_close(sem);
return 0;
}
// 进程2:打开并使用信号量
#include <stdio.h>
#include <fcntl.h>
#include <semaphore.h>
int main() {
sem_t *sem = sem_open("/my_sem", 0);
if (sem == SEM_FAILED) {
perror("sem_open");
return 1;
}
printf("Process 2 waiting for semaphore...\n");
sem_wait(sem);
printf("Critical section in process 2\n");
sleep(2);
sem_post(sem);
sem_close(sem);
sem_unlink("/my_sem"); // 删除信号量
return 0;
}
4. System V信号量
另一种较旧的信号量实现:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...);
int semop(int semid, struct sembuf *sops, size_t nsops);
5. 信号量使用模式
- 互斥锁(Mutex):信号量初始值为1
- 计数信号量:初始值>1,限制资源访问数量
- 屏障同步:多个进程/线程在特定点同步
- 生产者-消费者问题:协调生产者和消费者
三、信号与信号量的关键区别
| 特性 | 信号 (Signal) | 信号量 (Semaphore) |
|---|---|---|
| 本质 | 异步事件通知机制 | 同步原语 |
| 主要用途 | 进程间通信、异常处理 | 控制对共享资源的并发访问 |
| 操作 | 发送(kill)、接收(handler) | P操作(wait)、V操作(post) |
| 携带信息 | 仅信号编号 | 计数器值表示可用资源数量 |
| 处理位置 | 用户空间信号处理函数 | 内核空间或用户空间同步原语 |
| 阻塞行为 | 非阻塞(异步通知) | 可能阻塞(等待资源) |
| 原子性 | 信号处理可能中断正常执行流 | 操作是原子的 |
| 持久性 | 瞬时事件 | 持续存在的同步对象 |
| 标准 | POSIX标准 | POSIX和System V标准 |
| 典型应用场景 | Ctrl+C处理、进程终止通知、自定义进程通信 | 线程同步、进程同步、资源池管理 |
四、使用建议
信号使用建议:
- 保持信号处理函数简单且可重入
- 避免在信号处理函数中调用非异步安全函数
- 使用sigaction()代替signal()以获得更好控制
- 正确处理信号屏蔽和阻塞
- 注意信号可能丢失的问题
信号量使用建议:
- 明确信号量的用途(互斥还是计数)
- 确保正确的初始值
- 避免死锁(按固定顺序获取多个信号量)
- 使用超时机制防止永久阻塞
- 及时释放不再使用的信号量资源
五、总结
信号和信号量是C/C++编程中处理并发和通信的重要机制,尽管名称相似但功能完全不同:
- 信号是事件驱动的通知机制,主要用于进程间通信和异常处理
- 信号量是协调机制,用于控制对共享资源的并发访问
理解它们的区别和适用场景对于开发健壮的并发程序至关重要。在实际应用中:
- 使用信号处理外部事件和进程通信
- 使用信号量保护共享资源和协调并发访问
- 对于复杂的同步需求,可结合使用信号、信号量和其他同步机制

更多推荐


所有评论(0)