C语言中的信号(Signal)与信号量(Semaphore)详解

一、信号(Signal)

1. 概念与本质

信号是UNIX/Linux系统中一种​​异步事件通知机制​​,主要用于:

  • 内核向用户进程通知系统事件
  • 进程间通信(IPC)
  • 进程自身处理异常情况

信号本质上是发送给进程的一个​​整数编号​​(1-31),表示特定事件的发生。当信号到达时,进程会中断当前执行流,转而处理信号。

2. 常见信号类型

信号名值描述默认动作
SIGHUP1终端挂起或控制进程终止终止
SIGINT2键盘中断(Ctrl+C)终止
SIGQUIT3键盘退出(Ctrl+\)终止+核心转储
SIGILL4非法指令终止+核心转储
SIGABRT6来自abort()的终止信号终止+核心转储
SIGFPE8浮点异常终止+核心转储
SIGKILL9立即终止进程终止
SIGSEGV11无效内存引用终止+核心转储
SIGPIPE13管道破裂终止
SIGALRM14定时器超时终止
SIGTERM15终止信号终止
SIGUSR110用户自定义信号1终止
SIGUSR212用户自定义信号2终止
SIGCHLD17子进程状态改变忽略

3. 信号处理机制

进程可以通过三种方式处理信号:

  1. ​​默认处理​​:系统预定义的处理方式
  2. ​​忽略信号​​:完全忽略信号(SIGKILL和SIGSTOP除外)
  3. ​​自定义处理​​:注册信号处理函数

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. 信号处理注意事项

  1. ​​可重入性​​:信号处理函数应使用可重入函数(async-signal-safe)
  2. ​​原子操作​​:对共享数据的访问需要特殊处理
  3. ​​volatile变量​​:使用volatile修饰在信号处理函数中访问的变量
  4. ​​sig_atomic_t​​:使用此类型保证原子访问

二、信号量(Semaphore)

1. 概念与本质

信号量是一种​​同步原语​​,用于控制多个进程/线程对共享资源的访问。它包含:

  • 一个整数计数器
  • 等待队列
  • 两个原子操作:P(wait)和 V(post)

2. 类型

  1. ​​无名信号量​​:用于线程间或相关进程间的同步
  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. 信号量使用模式

  1. ​​互斥锁(Mutex)​​:信号量初始值为1
  2. ​​计数信号量​​:初始值>1,限制资源访问数量
  3. ​​屏障同步​​:多个进程/线程在特定点同步
  4. ​​生产者-消费者问题​​:协调生产者和消费者

三、信号与信号量的关键区别

特性信号 (Signal)信号量 (Semaphore)
​​本质​​异步事件通知机制同步原语
​​主要用途​​进程间通信、异常处理控制对共享资源的并发访问
​​操作​​发送(kill)、接收(handler)P操作(wait)、V操作(post)
​​携带信息​​仅信号编号计数器值表示可用资源数量
​​处理位置​​用户空间信号处理函数内核空间或用户空间同步原语
​​阻塞行为​​非阻塞(异步通知)可能阻塞(等待资源)
​​原子性​​信号处理可能中断正常执行流操作是原子的
​​持久性​​瞬时事件持续存在的同步对象
​​标准​​POSIX标准POSIX和System V标准
​​典型应用场景​​Ctrl+C处理、进程终止通知、自定义进程通信线程同步、进程同步、资源池管理

四、使用建议

信号使用建议:

  1. 保持信号处理函数简单且可重入
  2. 避免在信号处理函数中调用非异步安全函数
  3. 使用sigaction()代替signal()以获得更好控制
  4. 正确处理信号屏蔽和阻塞
  5. 注意信号可能丢失的问题

信号量使用建议:

  1. 明确信号量的用途(互斥还是计数)
  2. 确保正确的初始值
  3. 避免死锁(按固定顺序获取多个信号量)
  4. 使用超时机制防止永久阻塞
  5. 及时释放不再使用的信号量资源

五、总结

信号和信号量是C/C++编程中处理并发和通信的重要机制,尽管名称相似但功能完全不同:

  • ​​信号​​是事件驱动的通知机制,主要用于进程间通信和异常处理
  • ​​信号量​​是协调机制,用于控制对共享资源的并发访问

理解它们的区别和适用场景对于开发健壮的并发程序至关重要。在实际应用中:

  • 使用信号处理外部事件和进程通信
  • 使用信号量保护共享资源和协调并发访问
  • 对于复杂的同步需求,可结合使用信号、信号量和其他同步机制
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐