Linux IPC进程通信几种方法

Linux IPC进程通信几种方法

线程管道(popen)

在 Linux 系统中,popen 和 pclose 函数用于创建一个管道,通过该管道可以在进程之间传递数据。popen 函数用于启动一个新进程,并可以通过管道与该进程进行通信,而 pclose 函数用于关闭管道并等待进程结束。

  • 头文件:

    • #include <stdio.h>
  • 函数原型:

    • FILE *popen(const char *command, const char *type);
      • command 参数是一个指向以 NULL 结束的 shell 命令字符串的指针。
      • type 参数只能是 "r" 或 "w" 中的一种,表示读或写模式。
    • int pclose(FILE *stream);
  • popen 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。如果 type 是 "r",则文件指针连接到 command 的标准输出;如果 type 是 "w",则文件指针连接到 command 的标准输入。

示例程序

读取外部程序的输出:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
   FILE *read_fp;
   char buf[1024];
   int chars_read;
   memset(buf, '\0', sizeof(buf));
   read_fp = popen("uname -a", "r");
   if (read_fp != NULL) {
       chars_read = fread(buf, sizeof(char), sizeof(buf), read_fp);
       if (chars_read > 0) {
           printf("Output:\n%s\n", buf);
       }
       pclose(read_fp);
   }
   return 0;
}

将输出送往外部程序

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
   FILE *write_fp;
   char buf[1024];
   memset(buf, '\0', sizeof(buf));
   sprintf(buf, "hello world...\n");
   write_fp = popen("od -c", "w");
   if (write_fp != NULL) {
       fwrite(buf, sizeof(char), sizeof(buf), write_fp);
       pclose(write_fp);
   }
   return 0;
}

匿名管道(pipe)

仅父子进程或兄弟进程(有血缘关系)

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main()
{
        int fd[2];
        pid_t pid;
        char buf[256];

        if (pipe(fd) == -1) {
                perror("pipe");
                return 1;
        }


        pid = fork();
        if (pid == 0) {

                // 子进程:读
                close(fd[1]); // 关闭写端
                read(fd[0], buf, sizeof(buf));
                printf("Child received: %s\n", buf);
                close(fd[0]);

        } else if (pid > 0) {
                // 父进程:写
                close(fd[0]); // 关闭读端

                const char* msg = "Hello from parent!";
                write(fd[1], msg, strlen(msg)+1);
                close(fd[1]);
                wait(NULL); // 等待子进程结束

        }

        return 0;
}

命名管道 (fifo)

FIFO(First In First Out),也称为命名管道(named pipe),是一种在 Linux/Unix 系统中实现进程间通信(IPC)的机制。与普通的匿名管道(pipe)不同,FIFO 有一个在文件系统中存在的名称,允许不相关的进程之间进行通信。

FIFO 的核心特性:

  1. 有名字:在文件系统中以特殊文件的形式存在
  2. 单向通信:数据只能单向流动(类似管道)
  3. 字节流:数据以字节流的形式传输
  4. 阻塞行为:默认情况下,读写操作是阻塞的
  5. 内核缓冲:数据在内核空间中缓冲,大小有限
  6. 持久性:只要文件系统存在,FIFO 就存在(除非被删除)

创建 FIFO 的方法:

命令行创建

bash 复制代码
mkfifo myfifo
# 或者指定权限
mkfifo -m 644 myfifo

程序内创建

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>

// 函数原型
int mkfifo(const char *pathname, mode_t mode);

// 示例
if (mkfifo("/tmp/myfifo", 0666) == -1) {
    perror("mkfifo");
    exit(1);
}

FIFO 的工作模式

1. 阻塞模式(默认)

读操作:如果没有进程写,读操作会阻塞

写操作:如果没有进程读,写操作会阻塞

2. 非阻塞模式

使用 O_NONBLOCK 标志打开

读操作:没有数据时立即返回

写操作:没有读进程时立即返回错误

FIFO 的通信流程

c 复制代码
进程A(写端)          FIFO文件          进程B(读端)
    |                    |                   |
    |-----> 写入数据 ---->|-----> 读取数据 --->|
    |                    |                   |
    |                    |(内核缓冲区)       |
    |<---- 读取完成 <-----|<---- 确认 <--------|

完整示例代码

示例1:简单的读写通信

写端程序(writer.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 256

int main() {
    int fd;
    char buffer[BUFFER_SIZE];
    
    // 检查FIFO是否存在,如果不存在则创建
    if (access(FIFO_PATH, F_OK) == -1) {
        if (mkfifo(FIFO_PATH, 0666) == -1) {
            perror("mkfifo failed");
            exit(EXIT_FAILURE);
        }
        printf("FIFO created: %s\n", FIFO_PATH);
    }
    
    printf("Writer process started (PID: %d)\n", getpid());
    printf("Waiting for reader to connect...\n");
    
    // 以只写方式打开FIFO(阻塞模式)
    fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }
    
    printf("Reader connected! Start typing messages (type 'quit' to exit):\n");
    
    while (1) {
        printf("> ");
        fflush(stdout);
        
        // 从标准输入读取
        if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
            break;
        }
        
        // 移除换行符
        buffer[strcspn(buffer, "\n")] = '\0';
        
        // 检查退出条件
        if (strcmp(buffer, "quit") == 0) {
            break;
        }
        
        // 写入FIFO
        if (write(fd, buffer, strlen(buffer) + 1) == -1) {
            perror("write failed");
            break;
        }
        
        printf("Sent: %s\n", buffer);
    }
    
    close(fd);
    printf("Writer exiting...\n");
    
    // 可选:删除FIFO文件
    // unlink(FIFO_PATH);
    
    return 0;
}

读端程序(reader.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

#define FIFO_PATH "/tmp/myfifo"
#define BUFFER_SIZE 256

int main() {
    int fd;
    char buffer[BUFFER_SIZE];
    
    // 检查FIFO是否存在
    if (access(FIFO_PATH, F_OK) == -1) {
        fprintf(stderr, "FIFO %s does not exist. Please start writer first.\n", FIFO_PATH);
        exit(EXIT_FAILURE);
    }
    
    printf("Reader process started (PID: %d)\n", getpid());
    printf("Waiting for messages...\n");
    
    // 以只读方式打开FIFO(阻塞模式)
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        exit(EXIT_FAILURE);
    }
    
    printf("Connected to writer! Receiving messages:\n");
    printf("----------------------------------------\n");
    
    while (1) {
        // 从FIFO读取数据
        ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
        
        if (bytes_read > 0) {
            printf("Received: %s\n", buffer);
            
            // 检查是否收到退出命令
            if (strcmp(buffer, "quit") == 0) {
                break;
            }
        } else if (bytes_read == 0) {
            // 写端关闭了FIFO
            printf("Writer closed the connection.\n");
            break;
        } else {
            perror("read failed");
            break;
        }
    }
    
    close(fd);
    printf("Reader exiting...\n");
    
    return 0;
}

示例2:双向通信(使用两个FIFO)

双向通信示例(fifo_chat.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <errno.h>

#define FIFO1 "/tmp/fifo_chat_1"
#define FIFO2 "/tmp/fifo_chat_2"
#define BUFFER_SIZE 256

// 线程函数:接收消息
void* receive_messages(void* arg) {
    char* fifo_path = (char*)arg;
    int fd;
    char buffer[BUFFER_SIZE];
    
    fd = open(fifo_path, O_RDONLY);
    if (fd == -1) {
        perror("open receive fifo failed");
        return NULL;
    }
    
    while (1) {
        ssize_t bytes = read(fd, buffer, BUFFER_SIZE);
        if (bytes > 0) {
            printf("\n[Peer]: %s\n> ", buffer);
            fflush(stdout);
            
            if (strcmp(buffer, "quit") == 0) {
                break;
            }
        } else if (bytes <= 0) {
            break;
        }
    }
    
    close(fd);
    return NULL;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s [1|2] (1 for first process, 2 for second)\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    int role = atoi(argv[1]);
    char* write_fifo;
    char* read_fifo;
    pthread_t receive_thread;
    
    // 根据角色分配FIFO
    if (role == 1) {
        write_fifo = FIFO2;  // 进程1写FIFO2
        read_fifo = FIFO1;   // 进程1读FIFO1
    } else {
        write_fifo = FIFO1;  // 进程2写FIFO1
        read_fifo = FIFO2;   // 进程2读FIFO2
    }
    
    // 创建两个FIFO
    if (access(FIFO1, F_OK) == -1) {
        mkfifo(FIFO1, 0666);
    }
    if (access(FIFO2, F_OK) == -1) {
        mkfifo(FIFO2, 0666);
    }
    
    printf("Chat process %d started (PID: %d)\n", role, getpid());
    printf("Waiting for peer to connect...\n");
    
    // 创建接收线程
    pthread_create(&receive_thread, NULL, receive_messages, (void*)read_fifo);
    
    // 主线程处理发送
    int write_fd = open(write_fifo, O_WRONLY);
    if (write_fd == -1) {
        perror("open write fifo failed");
        exit(EXIT_FAILURE);
    }
    
    printf("Peer connected! Start chatting (type 'quit' to exit):\n");
    
    char buffer[BUFFER_SIZE];
    while (1) {
        printf("> ");
        fflush(stdout);
        
        if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
            break;
        }
        
        buffer[strcspn(buffer, "\n")] = '\0';
        
        if (write(write_fd, buffer, strlen(buffer) + 1) == -1) {
            perror("write failed");
            break;
        }
        
        if (strcmp(buffer, "quit") == 0) {
            break;
        }
    }
    
    close(write_fd);
    pthread_cancel(receive_thread);
    pthread_join(receive_thread, NULL);
    
    // 清理
    if (role == 1) {
        unlink(FIFO1);
        unlink(FIFO2);
    }
    
    return 0;
}

示例3:非阻塞模式

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

#define FIFO_PATH "/tmp/nonblock_fifo"

int main() {
    // 创建FIFO
    mkfifo(FIFO_PATH, 0666);
    
    // 以非阻塞方式打开
    int fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    
    char buffer[100];
    ssize_t bytes = read(fd, buffer, sizeof(buffer));
    
    if (bytes == -1 && errno == EAGAIN) {
        printf("No data available (non-blocking mode)\n");
    }
    
    close(fd);
    unlink(FIFO_PATH);
    
    return 0;
}

FIFO 的优缺点

优点

✅ 简单易用,API直观

✅ 支持不相关的进程间通信

✅ 有文件名,可以长期存在

✅ 内核自动同步(阻塞机制)

✅ 支持多读多写(但数据会交错)

缺点

❌ 单向通信,双向需要两个FIFO

❌ 数据是字节流,没有消息边界

❌ 缓冲区大小有限(通常为64KB)

❌ 阻塞特性可能导致死锁

❌ 不支持随机访问(只能顺序读写)

注意事项

清理FIFO:程序退出后记得使用 unlink() 删除FIFO文件

错误处理:总是检查系统调用的返回值

信号处理:考虑处理SIGPIPE信号(当读端关闭时写操作会产生)

原子性:对于小于PIPE_BUF(通常为4096字节)的写操作是原子的

权限控制:使用适当的文件权限(mode参数)

实际应用场景

简单的客户端-服务器通信

日志收集系统

进程间数据传输

多媒体流处理

简单的消息队列替代方案

FIFO是Linux进程通信中最简单直接的机制之一,特别适合简单的单向数据流场景。对于更复杂的通信需求,可以考虑使用消息队列、共享内存或Socket。

消息队列

  • 消息队列是Linux内核中维护的一个对象链表,每个消息都是一个数据块,拥有自己的类型和优先级。它与管道的核心区别在于:

    面向消息:读进程可以根据消息的类型有选择地接收,而不是像管道那样只能被动地读取字节流。

    异步通信:发送方把消息丢进队列就可以去做别的事,接收方可以在它方便的时候再来取。双方的生命周期不需要严格匹配。

  • Linux支持两套主流的消息队列API:System V 和 POSIX。System V是经典的老牌IPC,接口功能强大但稍显复杂;POSIX则是更新的标准,接口设计更简洁,类似于文件操作。我们主要介绍更常见的System V消息队列。

核心函数(System V)

使用System V消息队列,通常涉及以下几个关键步骤和函数:

💡 特性与优势

  • 消息类型化:这是它最突出的特点。接收方可以设置只接收自己关心的消息类型,非常适合多服务、多客户的场景。
  • 异步与解耦:发送者和接收者不必同时在线,消息在内核中排队,实现了时间和空间上的解耦。
  • 生命周期由内核管理:消息队列一旦创建,就存在于内核中,除非显式删除或系统重启。这保证了数据在进程崩溃后不会丢失。

⚠️ 局限性

  • 性能开销:相比于共享内存这种零拷贝的方式,消息队列在内核态和用户态之间复制数据,会有一定的性能开销。
  • 大小限制:每个消息队列的总字节数和消息数都有系统级的上限(例如 msg_qbytes)。
  • 编程复杂度:相比管道,需要处理ftok生成键值、定义消息结构、手动清理队列等,步骤略繁琐。

演示代码

定义公共头文件 (msg_common.h)

c 复制代码
#ifndef MSG_COMMON_H
#define MSG_COMMON_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <errno.h>

#define PROJECT_PATH "/tmp" // ftok使用的路径
#define PROJECT_ID 233      // ftok使用的项目ID
#define MSG_SIZE 256        // 消息正文最大长度

// 定义消息结构体,第一个字段必须是long类型
struct msgbuf {
    long mtype;             // 消息类型,必须 > 0
    char mtext[MSG_SIZE];   // 消息正文
};

#endif

发送端程序 (sender.c)

c 复制代码
#include "msg_common.h"

int main() {
    key_t key;
    int msqid;
    struct msgbuf message;

    // 1. 获取键值
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 2. 创建消息队列(如果不存在)或获取已有队列
    msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid == -1) {
        perror("msgget");
        exit(EXIT_FAILURE);
    }
    printf("Sender: Message queue created/opened, ID = %d\n", msqid);

    // 3. 准备两条不同类型的数据
    // 消息类型为1
    message.mtype = 1;
    strcpy(message.mtext, "Hello, this is a type 1 message!");
    if (msgsnd(msqid, &message, strlen(message.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }
    printf("Sender: Sent type 1 message: %s\n", message.mtext);

    // 消息类型为2
    message.mtype = 2;
    strcpy(message.mtext, "Greetings from sender, this is type 2!");
    if (msgsnd(msqid, &message, strlen(message.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }
    printf("Sender: Sent type 2 message: %s\n", message.mtext);

    printf("Sender: Done. Messages are in the queue.\n");
    return 0;
}

接收端程序 (receiver.c)

c 复制代码
#include "msg_common.h"

int main() {
    key_t key;
    int msqid;
    struct msgbuf message;

    // 1. 获取键值(必须和发送端一致)
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 2. 获取已存在的消息队列(注意这里没有IPC_CREAT)
    msqid = msgget(key, 0666);
    if (msqid == -1) {
        perror("msgget");
        fprintf(stderr, "Receiver: Queue does not exist. Run sender first?\n");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Opened message queue, ID = %d\n", msqid);

    // 3. 接收类型为1的消息(第四个参数 msgtyp = 1)
    if (msgrcv(msqid, &message, MSG_SIZE, 1, 0) == -1) {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Got type 1 message: %s\n", message.mtext);

    // 4. 接收类型为2的消息(msgtyp = 2)
    if (msgrcv(msqid, &message, MSG_SIZE, 2, 0) == -1) {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Got type 2 message: %s\n", message.mtext);

    // 5. 清理消息队列(可选,实际应用中应由管理进程执行)
    // 这里为了演示,让接收端最后删除队列
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(EXIT_FAILURE);
    }
    printf("Receiver: Message queue removed.\n");

    return 0;
}

共享内存

  • 共享内存(Shared Memory)是Linux进程间通信(IPC)中效率最高的一种方式。它允许多个不相关的进程直接访问同一块物理内存区域,避免了数据在内核空间和用户空间之间的复制,实现了真正的零拷贝通信。

  • 共享内存是一块被多个进程共同映射的物理内存区域。多个进程通过将自己的虚拟地址空间映射到同一块物理内存,实现对这块内存的直接读写。

c 复制代码
进程A的虚拟内存          物理内存          进程B的虚拟内存
    +---------+           +---------+         +---------+
    |         |           |         |         |         |
    |  数据区  |<--------->| 共享内存 |<------->|  数据区  |
    |         |           |         |         |         |
    +---------+           +---------+         +---------+
    
    直接读写                   |               直接读写
                              |
                        所有进程都能访问

💡 核心特性

  1. 最高效的IPC:零拷贝,数据直接在进程间共享
  2. 生命周期随内核:显式删除或系统重启前一直存在
  3. 需要同步机制:多个进程同时读写需要额外同步(通常配合信号量)
  4. 没有内置边界:类似于管道,需要自己定义协议
  5. 灵活性高:可以存储任意复杂的数据结构

⚠️ 重要注意事项

  1. 同步问题:共享内存本身不提供任何同步机制,必须使用信号量、互斥锁等配合
  2. 指针问题:共享内存中不能存储绝对指针,因为不同进程的虚拟地址不同
  3. 内存对齐:需要注意数据结构的内存对齐问题
  4. 大小限制:受系统限制,可用 ipcs -l 查看
  5. 安全性:任何有权限的进程都可以访问

示例代码

示例1:基础读写(配合信号量同步)

公共头文件(shm_common.h)

c 复制代码
#ifndef SHM_COMMON_H
#define SHM_COMMON_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#include <errno.h>

#define PROJECT_PATH "/tmp"
#define PROJECT_ID 666
#define SHM_SIZE 4096  // 4KB共享内存

// 共享内存中存储的数据结构
struct shared_data {
    int written;        // 标志位:数据是否已写入
    char text[1024];    // 实际数据
    int counter;        // 计数器示例
};

// 信号量操作结构
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 信号量操作函数
void sem_p(int semid) {
    struct sembuf sb = {0, -1, 0};  // 等待(P操作)
    semop(semid, &sb, 1);
}

void sem_v(int semid) {
    struct sembuf sb = {0, 1, 0};   // 释放(V操作)
    semop(semid, &sb, 1);
}

#endif

写端程序(shm_writer.c)

c 复制代码
#include "shm_common.h"

int main() {
    key_t key;
    int shmid, semid;
    struct shared_data *data;
    union semun sem_union;
    
    // 1. 生成键值
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    
    // 2. 创建共享内存
    shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    printf("Writer: Shared memory created, ID = %d\n", shmid);
    
    // 3. 附加共享内存
    data = (struct shared_data *)shmat(shmid, NULL, 0);
    if (data == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    printf("Writer: Shared memory attached at %p\n", data);
    
    // 4. 创建信号量用于同步
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }
    
    // 初始化信号量为1
    sem_union.val = 1;
    if (semctl(semid, 0, SETVAL, sem_union) == -1) {
        perror("semctl");
        exit(EXIT_FAILURE);
    }
    printf("Writer: Semaphore initialized\n");
    
    // 5. 写入数据
    int count = 0;
    while (count < 5) {
        // 获取信号量(P操作)
        sem_p(semid);
        
        // 写入数据
        data->written = 1;
        sprintf(data->text, "Message from writer #%d", count + 1);
        data->counter = count + 1;
        printf("Writer: Wrote: %s\n", data->text);
        
        // 释放信号量(V操作)
        sem_v(semid);
        
        count++;
        sleep(1);  // 模拟工作间隔
    }
    
    // 6. 分离共享内存
    if (shmdt(data) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    printf("Writer: Shared memory detached\n");
    
    // 注意:不删除共享内存,让读端读取
    
    return 0;
}

读端程序(shm_reader.c)

c 复制代码
#include "shm_common.h"

int main() {
    key_t key;
    int shmid, semid;
    struct shared_data *data;
    
    // 1. 生成键值
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    
    // 2. 获取共享内存
    shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
        fprintf(stderr, "Reader: Shared memory not found. Run writer first?\n");
        exit(EXIT_FAILURE);
    }
    printf("Reader: Shared memory opened, ID = %d\n", shmid);
    
    // 3. 获取信号量
    semid = semget(key, 1, 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }
    
    // 4. 附加共享内存
    data = (struct shared_data *)shmat(shmid, NULL, 0);
    if (data == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    printf("Reader: Shared memory attached at %p\n", data);
    
    // 5. 读取数据
    int last_counter = 0;
    while (1) {
        // 获取信号量(P操作)
        sem_p(semid);
        
        // 检查是否有新数据
        if (data->counter > last_counter) {
            printf("Reader: Read: %s (counter=%d)\n", 
                   data->text, data->counter);
            last_counter = data->counter;
        }
        
        // 释放信号量(V操作)
        sem_v(semid);
        
        // 检查是否读完
        if (last_counter >= 5) {
            break;
        }
        
        usleep(500000);  // 500ms轮询间隔
    }
    
    // 6. 分离共享内存
    if (shmdt(data) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }
    printf("Reader: Shared memory detached\n");
    
    // 7. 删除共享内存和信号量(通常由最后一个进程做)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl RMID");
    } else {
        printf("Reader: Shared memory removed\n");
    }
    
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl RMID");
    } else {
        printf("Reader: Semaphore removed\n");
    }
    
    return 0;
}

示例2:POSIX共享内存(更现代的接口)

POSIX共享内存示例(posix_shm.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <semaphore.h>
#include <errno.h>

#define SHM_NAME "/my_shared_memory"
#define SEM_NAME "/my_semaphore"
#define SHM_SIZE 4096

struct shared_data {
    int written;
    char buffer[1024];
};

int main(int argc, char* argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s [writer|reader]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    if (strcmp(argv[1], "writer") == 0) {
        // 创建共享内存
        int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
        if (fd == -1) {
            perror("shm_open");
            exit(EXIT_FAILURE);
        }
        
        // 设置大小
        if (ftruncate(fd, SHM_SIZE) == -1) {
            perror("ftruncate");
            exit(EXIT_FAILURE);
        }
        
        // 映射共享内存
        struct shared_data *data = mmap(NULL, SHM_SIZE, 
                                        PROT_READ | PROT_WRITE,
                                        MAP_SHARED, fd, 0);
        if (data == MAP_FAILED) {
            perror("mmap");
            exit(EXIT_FAILURE);
        }
        
        // 创建信号量
        sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
        if (sem == SEM_FAILED) {
            perror("sem_open");
            exit(EXIT_FAILURE);
        }
        
        // 写入数据
        for (int i = 0; i < 5; i++) {
            sem_wait(sem);
            
            data->written = 1;
            sprintf(data->buffer, "POSIX message #%d", i + 1);
            printf("Writer: %s\n", data->buffer);
            
            sem_post(sem);
            sleep(1);
        }
        
        // 清理
        munmap(data, SHM_SIZE);
        close(fd);
        sem_close(sem);
        
    } else if (strcmp(argv[1], "reader") == 0) {
        // 打开共享内存
        int fd = shm_open(SHM_NAME, O_RDWR, 0666);
        if (fd == -1) {
            perror("shm_open");
            exit(EXIT_FAILURE);
        }
        
        // 映射共享内存
        struct shared_data *data = mmap(NULL, SHM_SIZE,
                                        PROT_READ | PROT_WRITE,
                                        MAP_SHARED, fd, 0);
        if (data == MAP_FAILED) {
            perror("mmap");
            exit(EXIT_FAILURE);
        }
        
        // 打开信号量
        sem_t *sem = sem_open(SEM_NAME, 0);
        if (sem == SEM_FAILED) {
            perror("sem_open");
            exit(EXIT_FAILURE);
        }
        
        // 读取数据
        int last = 0;
        while (last < 5) {
            sem_wait(sem);
            
            if (data->written > last) {
                printf("Reader: %s\n", data->buffer);
                last = data->written;
            }
            
            sem_post(sem);
            usleep(500000);
        }
        
        // 清理
        munmap(data, SHM_SIZE);
        close(fd);
        sem_close(sem);
        
        // 删除共享内存和信号量
        shm_unlink(SHM_NAME);
        sem_unlink(SEM_NAME);
        
    } else {
        fprintf(stderr, "Invalid argument: %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    
    return 0;
}

示例3:共享内存中的复杂数据结构

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>

#define MAX_CLIENTS 10
#define MAX_NAME_LEN 32

// 复杂的共享数据结构
struct client_info {
    int pid;
    char name[MAX_NAME_LEN];
    time_t login_time;
    int active;
};

struct shared_database {
    int server_pid;
    int client_count;
    struct client_info clients[MAX_CLIENTS];
    char server_status[64];
    int total_requests;
};

int main() {
    key_t key = ftok("/tmp", 888);
    int shmid = shmget(key, sizeof(struct shared_database), IPC_CREAT | 0666);
    
    struct shared_database *db = shmat(shmid, NULL, 0);
    
    // 初始化
    db->server_pid = getpid();
    db->client_count = 0;
    db->total_requests = 0;
    strcpy(db->server_status, "Running");
    
    printf("Server PID: %d\n", db->server_pid);
    printf("Shared database initialized\n");
    
    // ... 后续操作
    
    shmdt(db);
    return 0;
}

常用命令

shell 复制代码
# 查看所有IPC资源
ipcs

# 只查看共享内存
ipcs -m

# 查看详细信息
ipcs -m -i <shmid>

# 删除共享内存
ipcrm -m <shmid>

# 查看系统限制
ipcs -l

# 使用命令删除所有共享内存
ipcs -m | awk '{print $2}' | xargs -I {} ipcrm -m {}

🎯 实际应用场景

高性能计算:多个进程共享大型数据集

数据库系统:共享缓冲区缓存

Web服务器:共享缓存和会话数据

实时系统:高频数据交换

图形处理:共享帧缓冲区

⚠️ 常见问题和解决方案

同步问题:使用信号量或互斥锁

内存泄漏:确保正确删除不再使用的共享内存

指针问题:使用偏移量而不是绝对指针

权限问题:设置适当的权限位

死锁:小心处理多个锁的获取顺序

共享内存是最强大的IPC机制,但也是最复杂的。它特别适合需要高性能、大数据量传输的场景。如果需要更简单的通信方式,可以考虑消息队列或管道;如果追求极致性能,共享内存是不二之选。

套接字(socket)

  • 套接字(Socket)是Linux/Unix系统中最强大、最灵活的进程间通信(IPC)机制。它的独特之处在于,不仅支持同一台主机上的进程通信,还能实现不同主机之间的网络通信,真正做到了"本地与远程一统"。

  • 套接字是网络编程中实现进程间通信的关键机制,它提供了一种跨网络或在同一主机上不同进程之间进行数据交换的方式。从本质上讲,套接字是对TCP/IP协议栈的抽象封装,为应用程序提供了一组简单的接口,让开发者无需关注底层网络协议的复杂性。

  • 套接字的设计哲学与Unix/Linux的"一切皆文件"理念一脉相承------套接字也是一种特殊的文件,可以用类似文件操作的方式(打开、读写、关闭)来使用

🔑 核心概念

  1. 套接字描述符
    套接字描述符是一个整数,类似于文件描述符。当应用程序创建套接字时,操作系统返回一个小整数作为描述符,后续的所有操作都通过这个描述符来引用该套接字
c 复制代码
进程的文件描述符表
+----------------+
| 0: stdin       |
| 1: stdout      |
| 2: stderr      |
| 3: socket      | <---- 套接字描述符
+----------------+
  1. 地址族(协议族)
    套接字支持多种协议族
协议族 说明 应用场景
AF_INET IPv4网络协议 跨网络通信
AF_INET6 IPv6网络协议 跨网络通信
AF_UNIX(或AF_LOCAL) Unix域协议 同一主机进程通信
  1. 套接字类型
类型 说明 对应协议
SOCK_STREAM 流式套接字,面向连接,可靠 TCP
SOCK_DGRAM 数据报套接字,无连接,不可靠 UDP
SOCK_RAW 原始套接字 直接访问网络层

🎯 套接字的两种应用场景

场景一:网络通信(跨主机)

网络通信需要指定IP地址和端口号,用于标识网络中的主机和进程。三元组 (IP地址, 协议, 端口) 可以在全网唯一标识一个进程。

c 复制代码
主机A(192.168.1.10)              主机B(192.168.1.20)
   进程A(端口8888)  <---网络--->    进程B(端口9999)

场景二:本地进程通信(同一主机)

Unix域套接字(Unix Domain Socket)专门用于同一主机上的进程通信。它使用文件系统中的路径名作为标识,通信过程不经过网络协议栈,效率更高。

c 复制代码
进程A                 进程B
  |                    |
  |---- Unix域套接字---->|
  |   (/tmp/mysocket)   |

🛠️ 核心API函数

  1. socket() - 创建套接字
c 复制代码
int socket(int domain, int type, int protocol);
  • domain: 协议族(AF_INET、AF_UNIX等)
  • type: 套接字类型(SOCK_STREAM、SOCK_DGRAM)
  • protocol: 协议(通常设为0,自动选择默认协议)
  • 返回值: 成功返回套接字描述符,失败返回-1
  1. bind() - 绑定地址
c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 将套接字与特定的地址(IP+端口)绑定。对于服务器,这是必需步骤。
  1. listen() - 监听连接
c 复制代码
int listen(int sockfd, int backlog);
  • 将套接字设为监听模式,准备接受客户端连接
  1. accept() - 接受连接
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 接受客户端的连接请求,返回新的套接字用于与该客户端通信
  1. connect() - 发起连接
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 客户端主动连接服务器
  1. 发送/接收数据
函数 TCP/UDP 说明
send() / recv() TCP 面向连接的数据传输
write() / read() TCP 也可用于套接字
sendto() / recvfrom() UDP 无连接的数据传输
  1. close() - 关闭套接字
c 复制代码
int close(int fd);
  • 关闭套接字,释放资源。

完整代码示例

示例1:TCP网络通信(C语言)

TCP服务器端(tcp_server.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket created: %d\n", server_fd);
    
    // 2. 设置地址和端口复用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    // 3. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网卡
    server_addr.sin_port = htons(PORT);
    
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Bound to port %d\n", PORT);
    
    // 4. 监听
    if (listen(server_fd, 5) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Listening for connections...\n");
    
    // 5. 接受连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd == -1) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected: %s:%d\n", 
           inet_ntoa(client_addr.sin_addr), 
           ntohs(client_addr.sin_port));
    
    // 6. 通信
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int bytes_read = recv(client_fd, buffer, BUFFER_SIZE, 0);
        
        if (bytes_read <= 0) {
            printf("Client disconnected\n");
            break;
        }
        
        printf("Received: %s", buffer);
        
        // 发送响应
        send(client_fd, "ACK\n", 4, 0);
    }
    
    // 7. 关闭连接
    close(client_fd);
    close(server_fd);
    
    return 0;
}

TCP客户端(tcp_client.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sock;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket created\n");
    
    // 2. 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    // 3. 连接服务器
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Connected to server\n");
    
    // 4. 通信
    while (1) {
        printf("Enter message (or 'quit' to exit): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 发送数据
        send(sock, buffer, strlen(buffer), 0);
        
        if (strcmp(buffer, "quit\n") == 0) {
            break;
        }
        
        // 接收响应
        memset(buffer, 0, BUFFER_SIZE);
        recv(sock, buffer, BUFFER_SIZE, 0);
        printf("Server response: %s", buffer);
    }
    
    // 5. 关闭连接
    close(sock);
    
    return 0;
}

示例2:Unix域套接字(本地进程通信)

Unix域服务器(unix_server.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/unix_socket"
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    
    // 删除已存在的socket文件
    unlink(SOCKET_PATH);
    
    // 1. 创建Unix域套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Unix socket created\n");
    
    // 2. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
    
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Bound to %s\n", SOCKET_PATH);
    
    // 3. 监听
    if (listen(server_fd, 5) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Listening for connections...\n");
    
    // 4. 接受连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd == -1) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected\n");
    
    // 5. 通信
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int bytes_read = read(client_fd, buffer, BUFFER_SIZE);
        
        if (bytes_read <= 0) {
            printf("Client disconnected\n");
            break;
        }
        
        printf("Received: %s", buffer);
        
        // 发送响应
        write(client_fd, "ACK\n", 4);
    }
    
    // 6. 清理
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    
    return 0;
}

Unix域客户端(unix_client.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/unix_socket"
#define BUFFER_SIZE 1024

int main() {
    int sock;
    struct sockaddr_un server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建Unix域套接字
    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    printf("Unix socket created\n");
    
    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
    
    // 3. 连接服务器
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Connected to server\n");
    
    // 4. 通信
    while (1) {
        printf("Enter message (or 'quit' to exit): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        
        // 发送数据
        write(sock, buffer, strlen(buffer));
        
        if (strcmp(buffer, "quit\n") == 0) {
            break;
        }
        
        // 接收响应
        memset(buffer, 0, BUFFER_SIZE);
        read(sock, buffer, BUFFER_SIZE);
        printf("Server response: %s", buffer);
    }
    
    // 5. 关闭连接
    close(sock);
    
    return 0;
}

示例3:socketpair - 简便的本地双向通信

socketpair() 是一个特殊函数,直接创建一对相互连接的套接字,非常适合父子进程通信

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/wait.h>

int main() {
    int sv[2];  // 两个套接字描述符
    char buffer[1024];
    
    // 创建一对连接的套接字
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair failed");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        close(sv[0]);  // 关闭读端
        
        // 发送消息给父进程
        char *msg = "Hello from child!";
        write(sv[1], msg, strlen(msg) + 1);
        printf("Child sent message\n");
        
        // 接收父进程的回复
        read(sv[1], buffer, sizeof(buffer));
        printf("Child received: %s\n", buffer);
        
        close(sv[1]);
        
    } else if (pid > 0) {
        // 父进程
        close(sv[1]);  // 关闭写端
        
        // 接收子进程的消息
        read(sv[0], buffer, sizeof(buffer));
        printf("Parent received: %s\n", buffer);
        
        // 回复子进程
        char *reply = "Hello from parent!";
        write(sv[0], reply, strlen(reply) + 1);
        
        close(sv[0]);
        wait(NULL);
        
    } else {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }
    
    return 0;
}

🎯 特殊类型:Unix域套接字

Unix域套接字(UDS)是套接字家族中的"特长生",专门用于本地进程通信:

特点

  • 高效:不经过网络协议栈,性能接近管道
  • 可靠:基于Unix本地域的UDP也是可靠的
  • 双工:支持双向通信(不同于管道的单向)
  • 文件系统可见:通过ls命令可以看到socket文件

使用场景

  • 本地高性能服务(如数据库、Web服务器)
  • 容器间通信
  • 需要比管道更灵活的本地IPC

⚠️ 注意事项

  • 端口占用:服务端关闭后,端口可能处于TIME_WAIT状态,可用SO_REUSEADDR选项解决
  • 粘包问题:TCP是流式协议,没有消息边界,需自定义协议(固定长度头、分隔符等)
  • 阻塞与非阻塞:默认是阻塞模式,可用fcntl()或setsockopt()设置
  • 清理Unix域socket文件:程序退出后记得unlink()删除socket文件
  • 字节序:网络字节序使用大端模式,需用htonl()、ntohl()等函数转换

📚 总结

套接字是Linux进程间通信的"全能选手",既能用于同一台主机的进程通信,也能用于不同主机的网络通信。它的核心优势在于:

  • 统一API:本地和远程通信使用相同的接口
  • 灵活多样:支持TCP、UDP、Unix域等多种协议
  • 广泛应用:几乎所有网络应用都基于套接字

正如那句经典名言:"一切皆Socket"------掌握了套接字,就掌握了Linux下进程通信的精髓。

信号量(Semaphore)

  • 信号量(Semaphore)是Linux系统中用于进程同步和互斥的重要机制。它不同于之前介绍的管道、消息队列、共享内存等数据传递型IPC,信号量专注于控制多个进程对共享资源的访问,解决并发访问时的竞争条件。

  • 信号量本质上是一个计数器,记录可用资源的数量。当进程需要访问共享资源时,必须先获取信号量;访问结束后,再释放信号量。通过这种方式,信号量确保多个进程能够有序地访问共享资源,避免数据混乱。

c 复制代码
信号量就像一个停车场的管理员:
- 停车场有N个空位(信号量初值=N)
- 每来一辆车(进程请求资源),空位减1
- 每走一辆车(进程释放资源),空位加1
- 当空位为0时,后来的车必须等待

🔑 核心概念

  1. 二元信号量(Binary Semaphore)
  • 取值只有0和1
  • 相当于互斥锁(Mutex)
  • 用于保护单个共享资源
  1. 计数信号量(Counting Semaphore)
  • 取值可以为任意非负整数
  • 管理多个同类资源
  • 例如:5个打印机,信号量初值=5
  1. 原子操作
    信号量的两个核心操作都是原子的(不可中断):
  • P操作(Proberen,荷兰语"尝试"):资源减1,如果资源不够则等待
  • V操作(Verhogen,荷兰语"增加"):资源加1,唤醒等待的进程

两种信号量标准

Linux支持两套信号量API:System V信号量(经典)和POSIX信号量(现代)。我们先介绍更常用的System V信号量。

System V信号量核心函数

函数 描述 关键点
semget() 创建或获取信号量集 可以创建多个信号量
semop() 执行信号量操作 P/V操作通过这个函数
semctl() 信号量控制操作 初始化、删除、查询等

完整示例代码

示例1:使用信号量保护共享内存

这是一个完整的生产者-消费者示例,展示了信号量如何与共享内存配合使用。

公共头文件(sem_shm_common.h)

c 复制代码
#ifndef SEM_SHM_COMMON_H
#define SEM_SHM_COMMON_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <errno.h>
#include <time.h>

#define PROJECT_PATH "/tmp"
#define PROJECT_ID 999
#define SHM_SIZE 4096
#define BUFFER_SIZE 10
#define MAX_ITEM 20

// 共享内存中的环形缓冲区
struct shared_buffer {
    int buffer[BUFFER_SIZE];  // 环形缓冲区
    int in;                    // 生产者写入位置
    int out;                   // 消费者读取位置
    int count;                 // 当前元素个数
};

// 信号量索引
#define SEM_EMPTY 0  // 空闲缓冲区数量信号量
#define SEM_FULL  1  // 已占用缓冲区数量信号量
#define SEM_MUTEX 2  // 互斥信号量
#define SEM_COUNT 3  // 信号量总数

// 信号量操作联合体
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

// 信号量P/V操作封装
void sem_p(int semid, int sem_num) {
    struct sembuf sb = {sem_num, -1, 0};
    if (semop(semid, &sb, 1) == -1) {
        perror("semop P failed");
        exit(EXIT_FAILURE);
    }
}

void sem_v(int semid, int sem_num) {
    struct sembuf sb = {sem_num, 1, 0};
    if (semop(semid, &sb, 1) == -1) {
        perror("semop V failed");
        exit(EXIT_FAILURE);
    }
}

#endif

生产者程序(producer.c)

c 复制代码
#include "sem_shm_common.h"

int main() {
    key_t key;
    int shmid, semid;
    struct shared_buffer *buf;
    union semun sem_union;
    
    // 1. 生成键值
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    
    // 2. 创建共享内存
    shmid = shmget(key, sizeof(struct shared_buffer), IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }
    
    // 3. 附加共享内存
    buf = (struct shared_buffer *)shmat(shmid, NULL, 0);
    if (buf == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    
    // 4. 创建信号量集(3个信号量)
    semid = semget(key, SEM_COUNT, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }
    
    // 5. 初始化信号量
    // 空闲缓冲区数量 = BUFFER_SIZE
    sem_union.val = BUFFER_SIZE;
    if (semctl(semid, SEM_EMPTY, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL empty");
        exit(EXIT_FAILURE);
    }
    
    // 已占用缓冲区数量 = 0
    sem_union.val = 0;
    if (semctl(semid, SEM_FULL, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL full");
        exit(EXIT_FAILURE);
    }
    
    // 互斥信号量 = 1
    sem_union.val = 1;
    if (semctl(semid, SEM_MUTEX, SETVAL, sem_union) == -1) {
        perror("semctl SETVAL mutex");
        exit(EXIT_FAILURE);
    }
    
    // 初始化缓冲区
    buf->in = 0;
    buf->out = 0;
    buf->count = 0;
    
    printf("Producer started (PID: %d)\n", getpid());
    printf("Producing %d items...\n\n", MAX_ITEM);
    
    // 6. 生产数据
    srand(time(NULL));
    for (int i = 0; i < MAX_ITEM; i++) {
        int item = rand() % 1000;  // 生成随机数
        
        // 等待空闲缓冲区
        sem_p(semid, SEM_EMPTY);
        
        // 互斥访问缓冲区
        sem_p(semid, SEM_MUTEX);
        
        // 将数据放入缓冲区
        buf->buffer[buf->in] = item;
        buf->in = (buf->in + 1) % BUFFER_SIZE;
        buf->count++;
        
        printf("Producer: produced item %d (value: %d, buffer count: %d)\n", 
               i + 1, item, buf->count);
        
        // 释放互斥锁
        sem_v(semid, SEM_MUTEX);
        
        // 增加已占用缓冲区计数
        sem_v(semid, SEM_FULL);
        
        usleep(rand() % 500000);  // 随机延迟
    }
    
    printf("\nProducer finished\n");
    
    // 等待消费者完成(简单起见,sleep一会儿)
    sleep(5);
    
    // 清理
    shmdt(buf);
    
    return 0;
}

消费者程序(consumer.c)

c 复制代码
#include "sem_shm_common.h"

int main() {
    key_t key;
    int shmid, semid;
    struct shared_buffer *buf;
    
    // 1. 生成键值
    key = ftok(PROJECT_PATH, PROJECT_ID);
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }
    
    // 2. 获取共享内存
    shmid = shmget(key, sizeof(struct shared_buffer), 0666);
    if (shmid == -1) {
        perror("shmget");
        fprintf(stderr, "Run producer first to create shared memory\n");
        exit(EXIT_FAILURE);
    }
    
    // 3. 附加共享内存
    buf = (struct shared_buffer *)shmat(shmid, NULL, 0);
    if (buf == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }
    
    // 4. 获取信号量
    semid = semget(key, SEM_COUNT, 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }
    
    printf("Consumer started (PID: %d)\n", getpid());
    printf("Consuming items...\n\n");
    
    // 5. 消费数据
    int items_consumed = 0;
    while (items_consumed < MAX_ITEM) {
        // 等待有数据可读
        sem_p(semid, SEM_FULL);
        
        // 互斥访问缓冲区
        sem_p(semid, SEM_MUTEX);
        
        // 从缓冲区取出数据
        int item = buf->buffer[buf->out];
        buf->out = (buf->out + 1) % BUFFER_SIZE;
        buf->count--;
        items_consumed++;
        
        printf("Consumer: consumed item %d (value: %d, buffer count: %d)\n", 
               items_consumed, item, buf->count);
        
        // 释放互斥锁
        sem_v(semid, SEM_MUTEX);
        
        // 增加空闲缓冲区计数
        sem_v(semid, SEM_EMPTY);
        
        usleep(rand() % 800000);  // 随机延迟
    }
    
    printf("\nConsumer finished\n");
    
    // 6. 清理
    shmdt(buf);
    
    // 7. 删除共享内存和信号量(由最后一个消费者执行)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl RMID");
    } else {
        printf("Shared memory removed\n");
    }
    
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl RMID");
    } else {
        printf("Semaphore set removed\n");
    }
    
    return 0;
}

示例2:POSIX信号量(更现代)

POSIX信号量使用更简洁,分为命名信号量和匿名信号量两种。

POSIX命名信号量示例(posix_sem.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define SEM_NAME "/my_posix_sem"
#define SHM_NAME "/my_posix_shm"
#define LOOP_COUNT 5

int main() {
    sem_t *sem;
    int *shared_counter;
    int shm_fd;
    
    // 创建命名信号量
    sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        exit(EXIT_FAILURE);
    }
    
    // 创建共享内存作为计数器
    shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }
    
    // 设置大小
    ftruncate(shm_fd, sizeof(int));
    
    // 映射共享内存
    shared_counter = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, 
                          MAP_SHARED, shm_fd, 0);
    if (shared_counter == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    
    *shared_counter = 0;
    
    // 创建子进程
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        for (int i = 0; i < LOOP_COUNT; i++) {
            sem_wait(sem);  // P操作
            
            // 临界区
            int temp = *shared_counter;
            printf("Child: read counter = %d\n", temp);
            temp++;
            usleep(100000);  // 模拟工作
            *shared_counter = temp;
            printf("Child: set counter = %d\n", *shared_counter);
            
            sem_post(sem);  // V操作
            
            sleep(1);
        }
        
        munmap(shared_counter, sizeof(int));
        sem_close(sem);
        
    } else if (pid > 0) {
        // 父进程
        for (int i = 0; i < LOOP_COUNT; i++) {
            sem_wait(sem);  // P操作
            
            // 临界区
            int temp = *shared_counter;
            printf("Parent: read counter = %d\n", temp);
            temp++;
            usleep(100000);  // 模拟工作
            *shared_counter = temp;
            printf("Parent: set counter = %d\n", *shared_counter);
            
            sem_post(sem);  // V操作
            
            sleep(1);
        }
        
        wait(NULL);
        
        // 清理
        munmap(shared_counter, sizeof(int));
        sem_close(sem);
        sem_unlink(SEM_NAME);
        shm_unlink(SHM_NAME);
        
        printf("Final counter value: %d\n", *shared_counter);
        
    } else {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    return 0;
}

示例3:信号量实现互斥锁

简单的二元信号量使用示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

#define THREAD_NUM 5
#define LOOP_NUM 3

sem_t mutex;  // 用作互斥锁
int counter = 0;

void* thread_func(void* arg) {
    int id = *(int*)arg;
    
    for (int i = 0; i < LOOP_NUM; i++) {
        // P操作 - 获取锁
        sem_wait(&mutex);
        
        // 临界区
        int temp = counter;
        printf("Thread %d: read counter = %d\n", id, temp);
        temp++;
        usleep(100000);  // 模拟工作
        counter = temp;
        printf("Thread %d: set counter = %d\n", id, counter);
        
        // V操作 - 释放锁
        sem_post(&mutex);
        
        sleep(1);
    }
    
    return NULL;
}

int main() {
    pthread_t threads[THREAD_NUM];
    int ids[THREAD_NUM];
    
    // 初始化信号量(匿名信号量,初始值1,用于线程间共享)
    sem_init(&mutex, 0, 1);
    
    // 创建线程
    for (int i = 0; i < THREAD_NUM; i++) {
        ids[i] = i + 1;
        pthread_create(&threads[i], NULL, thread_func, &ids[i]);
    }
    
    // 等待线程结束
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(threads[i], NULL);
    }
    
    // 销毁信号量
    sem_destroy(&mutex);
    
    printf("Final counter value: %d\n", counter);
    
    return 0;
}

信号量的典型应用场景

  • 生产者-消费者问题:管理有界缓冲区
  • 读者-写者问题:控制读写访问权限
  • 哲学家就餐问题:避免死锁
  • 资源池管理:如数据库连接池
  • 任务队列:控制并发任务数量

常见问题和注意事项

  1. 死锁(Deadlock)
c 复制代码
// 可能导致死锁的代码
void process_A() {
    sem_wait(&sem1);  // 获取资源1
    sem_wait(&sem2);  // 等待资源2(被B占用)
    // 使用资源...
}

void process_B() {
    sem_wait(&sem2);  // 获取资源2
    sem_wait(&sem1);  // 等待资源1(被A占用)
    // 使用资源...
}

解决方案:固定获取资源的顺序,或使用sem_trywait()。

  1. 优先级反转

    高优先级进程被低优先级进程阻塞,中间优先级进程抢占CPU。

    解决方案:优先级继承协议。

  2. 信号量泄漏

    System V信号量不会随进程退出自动删除,必须显式删除:

c 复制代码
semctl(semid, 0, IPC_RMID);
  1. 初始化问题

    必须正确初始化信号量,未初始化的信号量行为未定义。

  2. 信号中断

    sem_wait()可能被信号中断,需要处理EINTR错误:

c 复制代码
while ((r = sem_wait(&sem)) == -1 && errno == EINTR)
    continue;  // 重试

常用命令

c 复制代码
# 查看所有IPC资源(包括信号量)
ipcs -s

# 查看特定信号量集
ipcs -s -i <semid>

# 删除信号量集
ipcrm -s <semid>

# 查看系统限制
ipcs -l

# 查看POSIX命名信号量
ls /dev/shm/

总结

信号量是Linux进程同步的基石,它与之前介绍的IPC机制有本质区别:

  • 管道/消息队列/共享内存:负责数据传递
  • 信号量:负责访问控制,不传递数据

信号量的核心价值在于:

  • 解决竞争条件:确保共享资源的安全访问
  • 灵活的资源管理:计数信号量可管理多份资源
  • 进程/线程同步:协调多个执行单元的工作顺序

正如计算机科学家Dijkstra所说:"信号量是并发编程的万能钥匙"------掌握了信号量,就掌握了并发控制的核心。

信号(signal)

什么是信号

  • 信号(Signal)是Linux/Unix系统中一种异步的进程间通信机制,用于通知进程某个事件已经发生。它是软件层面上对中断的模拟,因此也被称为"软中断"。

  • 信号是一种异步通信机制,意味着进程无法预知信号何时到达。当信号发送给进程时,操作系统会中断进程的正常执行流程,转而执行信号处理函数(如果注册了的话),执行完毕后再恢复原来的执行流程。

c 复制代码
进程正常执行流程                 信号到达
    +-------------------+          
    |                   |          
    |   main()          |   SIGINT
    |   ...             | <─────── 用户按Ctrl+C
    |                   |          
    +-------------------+         ⬇
                                  ⬇ 中断
    +-------------------+         ⬇
    |                   | <─────── 转向执行
    |   信号处理函数     |          信号处理函数
    |   sig_handler()   |          
    |                   |          
    +-------------------+          
              ⬆                   
              ⬆ 返回继续执行        
    +-------------------+          
    |                   |          
    |   main()继续      |          
    |   ...             |          
    +-------------------+          

信号的核心特性

  • 异步性:信号可以在任何时间到达
  • 简单信息:信号本身只携带信号编号,不传递复杂数据
  • 软中断:类似于硬件中断的工作方式
  • 内核生成:信号由内核生成并递送给进程
  • 优先级:信号可以中断进程的阻塞系统调用

常见信号列表

信号 编号 默认动作 说明
SIGINT 2 终止进程 键盘中断(Ctrl+C)
SIGQUIT 3 终止+转储 键盘退出(Ctrl+\)
SIGKILL 9 终止进程 强制杀死进程(不能捕获/忽略)
SIGSEGV 11 终止+转储 段错误(非法内存访问)
SIGUSR1 10 终止进程 用户自定义信号1
SIGUSR2 12 终止进程 用户自定义信号2
SIGALRM 14 终止进程 定时器超时
SIGTERM 15 终止进程 软件终止信号(kill默认发送)
SIGCHLD 17 忽略 子进程状态改变
SIGCONT 18 继续执行 继续暂停的进程
SIGSTOP 19 暂停进程 暂停进程(不能捕获/忽略)
SIGTSTP 20 暂停进程 键盘暂停(Ctrl+Z)

信号相关API

  1. signal() - 简化版信号处理(不推荐)
c 复制代码
#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);
// 或者用typedef简化
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  1. sigaction() - 推荐使用的信号处理
c 复制代码
#include <signal.h>

int sigaction(int signum, 
              const struct sigaction *act,
              struct sigaction *oldact);

struct sigaction结构体:

c 复制代码
struct sigaction {
    void     (*sa_handler)(int);      // 信号处理函数
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数
    sigset_t   sa_mask;                // 信号屏蔽字
    int        sa_flags;                // 标志位
    void     (*sa_restorer)(void);     // 已废弃
};
  1. 信号发送函数
函数 描述 示例
kill() 向进程发送信号 kill(pid, SIGUSR1)
raise() 向自己发送信号 raise(SIGINT)
alarm() 设置定时器 alarm(5) 发送SIGALRM
pause() 等待信号 pause()
sigqueue() 发送信号并携带数据 sigqueue(pid, SIGUSR1, val)

完整示例代码

示例1:基本的信号处理

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

// SIGINT处理函数
void sigint_handler(int sig) {
    printf("\nCaught signal %d (SIGINT). Exiting gracefully...\n", sig);
    // 执行清理工作
    printf("Cleaning up resources...\n");
    exit(EXIT_SUCCESS);
}

// SIGUSR1处理函数
int usr1_count = 0;
void sigusr1_handler(int sig) {
    usr1_count++;
    printf("Received SIGUSR1 (count: %d)\n", usr1_count);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, sigint_handler);
    signal(SIGUSR1, sigusr1_handler);
    
    // 忽略SIGQUIT
    signal(SIGQUIT, SIG_IGN);
    
    printf("PID: %d\n", getpid());
    printf("Try:\n");
    printf("  - Ctrl+C to trigger SIGINT\n");
    printf("  - Ctrl+\\ is ignored\n");
    printf("  - kill -USR1 %d to send SIGUSR1\n", getpid());
    printf("  - kill -TERM %d to send SIGTERM (default action)\n\n", getpid());
    
    // 无限循环
    int count = 0;
    while (1) {
        printf("Working... (%d)\n", ++count);
        sleep(2);
    }
    
    return 0;
}

示例2:使用sigaction

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

// 信号处理函数
void signal_handler(int sig, siginfo_t *info, void *context) {
    printf("\n=== Signal Received ===\n");
    printf("Signal number: %d\n", sig);
    
    // 从siginfo_t获取详细信息
    if (info != NULL) {
        printf("Sender PID: %d\n", info->si_pid);
        printf("Sender UID: %d\n", info->si_uid);
        printf("Signal value: %d\n", info->si_value.sival_int);
        printf("Signal code: %d\n", info->si_code);
    }
    
    // 如果是SIGINT,退出
    if (sig == SIGINT) {
        printf("Exiting...\n");
        exit(EXIT_SUCCESS);
    }
}

int main() {
    struct sigaction sa;
    
    // 清空结构体
    memset(&sa, 0, sizeof(sa));
    
    // 设置信号处理函数(使用扩展版本)
    sa.sa_sigaction = signal_handler;
    
    // 设置标志位:使用扩展处理函数,并重启被中断的系统调用
    sa.sa_flags = SA_SIGINFO | SA_RESTART;
    
    // 初始化信号屏蔽字(在处理SIGINT时阻塞SIGUSR1)
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGUSR1);
    
    // 注册信号处理
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction SIGINT");
        exit(EXIT_FAILURE);
    }
    
    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction SIGUSR1");
        exit(EXIT_FAILURE);
    }
    
    printf("PID: %d\n", getpid());
    printf("Send signals with: kill -USR1 %d (with optional value)\n", getpid());
    printf("Or with: kill -INT %d\n", getpid());
    printf("Or with: kill -QUEUE %d <value>\n\n", getpid());
    
    // 使用sigqueue发送带数据的信号(需要在另一个终端执行)
    printf("Main program running...\n");
    
    while (1) {
        printf(".");
        fflush(stdout);
        sleep(1);
    }
    
    return 0;
}

示例3:信号集和信号屏蔽

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

void print_sigset(const char *prefix, sigset_t *sigset) {
    printf("%s: ", prefix);
    for (int i = 1; i < 32; i++) {
        if (sigismember(sigset, i)) {
            printf("%d ", i);
        }
    }
    printf("\n");
}

int main() {
    sigset_t new_set, old_set, pending_set;
    
    // 初始化信号集
    sigemptyset(&new_set);
    sigaddset(&new_set, SIGINT);
    sigaddset(&new_set, SIGQUIT);
    sigaddset(&new_set, SIGUSR1);
    
    print_sigset("Initial blocked set", &new_set);
    
    // 屏蔽这些信号
    if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }
    
    printf("\nSignals SIGINT, SIGQUIT, SIGUSR1 are now blocked\n");
    printf("Try sending these signals now (they will be pending)\n");
    printf("PID: %d\n", getpid());
    printf("Press Enter to check pending signals...\n");
    getchar();
    
    // 检查挂起的信号
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        exit(EXIT_FAILURE);
    }
    print_sigset("Pending signals", &pending_set);
    
    printf("\nUnblocking signals...\n");
    // 恢复原来的信号屏蔽字
    if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {
        perror("sigprocmask restore");
        exit(EXIT_FAILURE);
    }
    
    printf("Signals unblocked. Pending signals will be delivered now.\n");
    
    return 0;
}

示例4:alarm和定时器

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

int alarm_count = 0;
time_t start_time;

void alarm_handler(int sig) {
    alarm_count++;
    time_t current = time(NULL);
    printf("\nAlarm %d triggered at %ld seconds\n", 
           alarm_count, current - start_time);
    
    // 设置下一个定时器
    if (alarm_count < 5) {
        alarm(2);  // 2秒后再次触发
    }
}

void timeout_handler(int sig) {
    printf("\nOperation timed out after 5 seconds!\n");
    exit(EXIT_FAILURE);
}

int main() {
    start_time = time(NULL);
    
    // 示例1:重复定时器
    struct sigaction sa;
    sa.sa_handler = alarm_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, NULL);
    
    printf("Starting repetitive alarms (5 times, every 2 seconds)\n");
    alarm(2);  // 2秒后第一次触发
    
    // 模拟工作
    for (int i = 0; i < 15; i++) {
        printf("Working... (%d)\n", i + 1);
        sleep(1);
    }
    
    // 示例2:超时控制
    printf("\n=== Timeout Example ===\n");
    printf("Setting 5-second timeout for read operation...\n");
    
    struct sigaction timeout_sa;
    timeout_sa.sa_handler = timeout_handler;
    sigemptyset(&timeout_sa.sa_mask);
    timeout_sa.sa_flags = 0;
    sigaction(SIGALRM, &timeout_sa, NULL);  // 覆盖之前的处理函数
    
    alarm(5);  // 5秒超时
    
    char buffer[100];
    printf("Enter input (will timeout in 5 seconds): ");
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        // 取消定时器
        alarm(0);
        printf("You entered: %s", buffer);
    }
    
    return 0;
}

示例5:父子进程信号通信

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

volatile sig_atomic_t child_status = 0;
volatile sig_atomic_t got_signal = 0;

void child_handler(int sig) {
    got_signal = 1;
    child_status = sig;
}

void parent_handler(int sig) {
    printf("Parent received signal %d from child\n", sig);
}

int main() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        struct sigaction sa;
        sa.sa_handler = child_handler;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        sigaction(SIGUSR1, &sa, NULL);
        
        printf("Child (PID: %d) waiting for signal...\n", getpid());
        
        // 等待信号
        while (!got_signal) {
            pause();  // 等待信号
        }
        
        printf("Child got signal %d, doing work...\n", child_status);
        sleep(2);
        printf("Child work done, sending signal back\n");
        
        // 发送信号给父进程
        kill(getppid(), SIGUSR2);
        
    } else if (pid > 0) {
        // 父进程
        struct sigaction sa;
        sa.sa_handler = parent_handler;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        sigaction(SIGUSR2, &sa, NULL);
        
        printf("Parent (PID: %d) will send signal to child\n", getpid());
        sleep(1);  // 给子进程时间设置处理函数
        
        printf("Parent sending SIGUSR1 to child\n");
        kill(pid, SIGUSR1);
        
        // 等待子进程完成
        int status;
        wait(&status);
        printf("Parent: child finished\n");
        
    } else {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    return 0;
}

示例6:siglongjmp实现非局部跳转

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

sigjmp_buf env;
volatile int timeout_occurred = 0;

void timeout_handler(int sig) {
    timeout_occurred = 1;
    siglongjmp(env, 1);  // 跳回setjmp点
}

int main() {
    struct sigaction sa;
    sa.sa_handler = timeout_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGALRM, &sa, NULL);
    
    if (sigsetjmp(env, 1) == 0) {
        // 正常执行路径
        printf("Starting operation with 3-second timeout...\n");
        
        // 设置定时器
        alarm(3);
        
        // 模拟耗时操作
        printf("Performing long operation...\n");
        for (int i = 0; i < 10; i++) {
            printf("Working... %d/10\n", i + 1);
            sleep(1);
        }
        
        // 操作完成,取消定时器
        alarm(0);
        printf("Operation completed successfully\n");
        
    } else {
        // 超时跳转到这里
        printf("\nOperation timed out after 3 seconds!\n");
        if (timeout_occurred) {
            printf("Performing cleanup...\n");
            // 执行清理工作
        }
    }
    
    return 0;
}

🎯 信号的应用场景

  1. 进程控制:终止、暂停、继续进程
  2. 异常处理:段错误、非法指令等
  3. 定时器:alarm定时通知
  4. 父子进程通信:通知状态变化
  5. 用户交互:Ctrl+C、Ctrl+Z等
  6. 资源限制:CPU时间超限、文件大小超限

⚠️ 注意事项和最佳实践

  1. 可重入函数
    信号处理函数中只能调用异步信号安全的函数(async-signal-safe):
c 复制代码
// 安全的函数
void safe_handler(int sig) {
    write(1, "Signal received\n", 16);  // write是安全的
    // 不能调用printf、malloc等
}

// 不安全的函数
void unsafe_handler(int sig) {
    printf("Signal received\n");  // printf不是异步信号安全的
    malloc(100);                   // malloc不是异步信号安全的
}
  1. volatile sig_atomic_t
    用于在信号处理函数和主程序之间共享标志:
c 复制代码
volatile sig_atomic_t flag = 0;

void handler(int sig) {
    flag = 1;  // 原子操作
}

int main() {
    signal(SIGINT, handler);
    while (!flag) {
        // 等待信号
    }
    printf("Signal received\n");
    return 0;
}
  1. 避免在信号处理函数中做复杂操作
c 复制代码
// 错误示例:在信号处理函数中做复杂操作
void bad_handler(int sig) {
    // 复杂的处理逻辑
    open(...);    // 可能阻塞
    malloc(...);  // 不安全
    printf(...);  // 不安全
    longjmp(...); // 可能造成资源泄漏
}

// 正确做法:仅设置标志
void good_handler(int sig) {
    flag = 1;
}
  1. 处理EINTR
    被信号中断的系统调用需要重新启动:
c 复制代码
// 方式1:自动重启(设置SA_RESTART标志)
struct sigaction sa;
sa.sa_flags = SA_RESTART;

// 方式2:手动处理EINTR
ssize_t ret;
do {
    ret = read(fd, buf, sizeof(buf));
} while (ret == -1 && errno == EINTR);
  1. 信号丢失问题
    标准信号不排队,相同信号多次发送可能只接收一次:
c 复制代码
// 发送10次SIGUSR1
for (int i = 0; i < 10; i++) {
    kill(pid, SIGUSR1);
}
// 进程可能只收到一次SIGUSR1

// 使用实时信号(SIGRTMIN+)可以排队
for (int i = 0; i < 10; i++) {
    kill(pid, SIGRTMIN + 1);  // 可以排队
}

常用命令

shell 复制代码
# 查看所有信号
kill -l

# 发送信号
kill -SIGUSR1 1234      # 发送SIGUSR1给PID 1234
kill -9 1234            # 强制杀死进程
killall -TERM myapp      # 发送SIGTERM给所有myapp进程

# 查看进程信号相关信息
ps -l                   # 查看进程
cat /proc/[pid]/status  # 查看进程详细信息(包括信号掩码)

# 查看信号的默认行为
man 7 signal

信号的核心价值:

  1. 事件通知:告知进程发生了某个事件
  2. 异常处理:处理程序运行时的异常情况
  3. 进程控制:控制其他进程的执行状态
  4. 异步响应:响应外部异步事件

正如那句经典的话:"信号是Linux系统的神经末梢",它们传递着系统中最紧急、最重要的信息,确保系统能够及时响应各种事件。

相关推荐
像污秽一样2 小时前
算法设计与分析-习题6.1
数据结构·算法
C++ 老炮儿的技术栈2 小时前
Linux 文件系统目录架构全解析
linux·服务器·c语言·开发语言·c++
北京地铁1号线2 小时前
8.2 对比学习的损失函数
算法·机器学习·损失函数·对比学习
yiwenrong2 小时前
安全审计-Ubuntu-ufw防火墙
linux·运维·ubuntu
样例过了就是过了2 小时前
LeetCode热题100 分割回文串
数据结构·c++·算法·leetcode·深度优先·dfs
默|笙2 小时前
【Linux】进程间通信(3)_命令管道
linux
小比特_蓝光2 小时前
Linux:基本指令
linux·运维·服务器
hnlgzb2 小时前
如果获取deepseek的api key?
运维
带娃的IT创业者3 小时前
WeClaw 心跳与重连实战:指数退避算法如何让 WebSocket 在弱网环境下的连接成功率提升 67%?
python·websocket·网络协议·算法·fastapi·实时通信