文章

进程间通信

进程间通信

概念

进程是操作系统中资源分配的基本单位,每个进程拥有独立的地址空间。进程间通信(Inter Process Communication,IPC), 是将这些独立进程联系起来的桥梁。让进程能够交换数据、协调行为,是实现多进程协作的核心技术。[2]

由于每个进程的地址空间是独立的,进程之间想要进行信息交换就必须通过内核。因此进程间通信会带来内核态用户态之间切换的一些性能开销。

常见的几种进程间通信方式如下: 管道(Pipe) 消息队列(Message Queue) 共享内存(Shared Memory) 信号量(Semaphore) 信号(Signal) 套接字(Socket)

进程间通信常用于模块化设计,资源共享,负载均衡,故障隔离等。

场景例子典型 IPC
流水线协作shell 里ps \| grep ssh,前一个输出当后一个输入管道 pipe
客户端–服务端浏览器连本地代理、数据库客户端连 mysqldsocket、命名管道
生产者–消费者下载进程写文件,播放器读;日志写入、分析进程读管道、消息队列、共享内存
共享状态/高性能传大数据多进程游戏引擎、科学计算、视频处理共享帧缓冲共享内存 + 信号量/互斥
事件通知子进程退出通知父进程、定时器到了、配置热更新信号 signal
微服务/多进程架构Chrome 多进程(渲染、GPU、插件隔离)、Nginx workersocket、共享内存等
权限隔离一个进程高权限做敏感操作,普通进程通过接口请求Unix domain socket、RPC

管道

管道分为匿名管道和命令管道。匿名管道通常给有亲缘关系的进程用;命名管道FIFO有路径名,无关进程也能打开。

 匿名管道命名管道
创建pipe()mkfifo()
标识只有 fd,无路径有路径,/tmp/demo_fifo
谁能用一般是父子等有亲缘关系的进程任意进程只要知道路径就能打开

匿名管道(Anonymous Pipe)是一条可用于进程间通信的单向数据通道。 并约定: pipefd[0]:读端 pipefd[1]:写端 写入写端的数据由内核缓冲,直到从读端读出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    int pipefd[2];
    char buf[20];
  
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }
  
    pid_t pid = fork();
    if (pid == 0) { // 子进程
        close(pipefd[0]); // 关闭读端
        write(pipefd[1], "Hello, Parent!", 14);
        close(pipefd[1]);
    } else { // 父进程
        close(pipefd[1]); // 关闭写端
        read(pipefd[0], buf, sizeof(buf));
        printf("Parent received: %s\n", buf);
        close(pipefd[0]);
        wait(NULL);
    }
    return 0;
}

输出

1
Parent received: Hello, Parent!

这个程序的本质是:子写父读,用匿名管道做一次简单的 IPC。fork() 会复制当前进程,成功时在两个进程里各返回一次:

进程fork() 的返回值
父进程子进程的真实 PID(正整数,如 12345)
子进程0
失败-1(只在父进程,没有子进程)

pid == 0 表示“当前代码跑在子进程里”,因为 fork() 规定子进程拿到的返回值是 0;子进程的真实 PID 要用 getpid() 获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
主程序
  │
  ├─ pipe() 创建管道
  │
  ├─ fork() ──────────────┐
  │                       │
  ▼                       ▼
父进程                  子进程
关写端                  关读端
读管道 ←─────────────── 写 "Hello, Parent!"
打印结果                退出
wait 等子进程
退出

命名管道(FIFO/Named Pipe)提供单向进程间通信通道;有读端和写端;写到写端的数据可以从读端读出。 管道是字节流,没有消息边界。

  • mkfifo 只在文件系统里创建一个特殊文件,真正传数据仍在内核缓冲区。
  • 两端都要用 open 打开同一路径;一端 open 会等另一端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/wait.h>

// 命名管道在文件系统中有路径,无关进程也能通过这个路径通信
static const char *FIFO_PATH = "/tmp/demo_fifo";

static int create_fifo() {
    // 已存在则先删掉,方便反复演示
    unlink(FIFO_PATH);
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        perror("mkfifo");
        return -1;
    }
    printf("Created named pipe: %s\n", FIFO_PATH);
    return 0;
}

// 写端:打开 FIFO 写入数据后关闭
static int run_writer() {
    const char *msg = "Hello via named pipe!";
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open (writer)");
        return 1;
    }
    printf("[Writer] pid=%d, sending: %s\n", getpid(), msg);
    write(fd, msg, strlen(msg) + 1); // +1 带上 '\0',方便对方按字符串打印
    close(fd);
    printf("[Writer] done\n");
    return 0;
}

// 读端:打开 FIFO 读出数据后关闭
static int run_reader() {
    char buf[64] = {0};
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open (reader)");
        return 1;
    }
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        printf("[Reader] pid=%d, received: %s\n", getpid(), buf);
    }
    close(fd);
    printf("[Reader] done\n");
    return 0;
}

static void usage(const char *prog) {
    printf("Usage:\n");
    printf("  %s              # 自动演示:创建 FIFO + fork 读写\n", prog);
    printf("  %s create       # 只创建命名管道\n", prog);
    printf("  %s write        # 作为写端(另开终端配合 read)\n", prog);
    printf("  %s read         # 作为读端(另开终端配合 write)\n", prog);
    printf("  %s clean        # 删除 /tmp/demo_fifo\n", prog);
}

int main(int argc, char *argv[]) {
    // ---------- 双终端演示模式 ----------
    if (argc >= 2) {
        if (strcmp(argv[1], "create") == 0) {
            return create_fifo() == 0 ? 0 : 1;
        }
        if (strcmp(argv[1], "write") == 0) {
            return run_writer();
        }
        if (strcmp(argv[1], "read") == 0) {
            return run_reader();
        }
        if (strcmp(argv[1], "clean") == 0) {
            if (unlink(FIFO_PATH) == -1 && errno != ENOENT) {
                perror("unlink");
                return 1;
            }
            printf("Removed %s\n", FIFO_PATH);
            return 0;
        }
        usage(argv[0]);
        return 1;
    }

    // ---------- 一键自动演示(结构类似 ipc.cpp)----------
    if (create_fifo() == -1) {
        return 1;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        unlink(FIFO_PATH);
        return 1;
    }

    if (pid == 0) { // 子进程:写端
        // 稍等一下,让父进程先去 open 读端,避免双方都卡在 open 上
        // (FIFO:第一个 open 会阻塞,直到另一端也被打开)
        usleep(100 * 1000);
        int rc = run_writer();
        _exit(rc);
    }

    // 父进程:读端
    int rc = run_reader();
    wait(NULL);
    unlink(FIFO_PATH); // 演示结束清理
    printf("Cleaned up %s\n", FIFO_PATH);
    return rc;
}

输出,单终端:

1
2
3
4
5
6
7
 ./named_pipe 
Created named pipe: /tmp/demo_fifo
[Writer] pid=40123, sending: Hello via named pipe!
[Writer] done
[Reader] pid=40122, received: Hello via named pipe!
[Reader] done
Cleaned up /tmp/demo_fifo

输出,双终端:

1
2
3
4
5
6
7
8
9
10
11
# Terminal 1
./named_pipe create
Created named pipe: /tmp/demo_fifo
./named_pipe read ## 等待 write
[Reader] pid=40285, received: Hello via named pipe!
[Reader] done

# Terminal 2
./named_pipe write
[Writer] pid=40312, sending: Hello via named pipe!
[Writer] done

消息队列

消息队列(Message Queue):进程把数据封装成一条条消息放进内核里的队列,另一个进程按规则取走。

和管道比,更像“邮箱”而不是“水管”:

 管道消息队列
数据形态字节流,无边界一条条消息,有边界
读取方式按字节数读通常一次读整条消息
选择能力基本先进先出常可按类型/优先级
生命周期随进程/两端关闭结束可独立于进程存在(直到显式删除)

内核视角:

  1. 创建/打开队列 内核里建一个队列对象(System V 用 msgget 的 key;POSIX 用名字如 myqueue)。
  2. 发送 msgsnd / mq_send 用户态把消息拷到内核缓冲区,挂到队列链表上。队列满则阻塞(或非阻塞返回错误)。
  3. 接收 msgrcv / mq_receive 从内核取出一条消息,再拷回用户缓冲区。队列空则阻塞等待。
  4. 解耦 发送方和接收方不必同时在线(队列还在就能继续塞/取),也不必有亲缘关系。
1
2
3
4
5
6
进程 A                    内核消息队列                 进程 B
  │                            │                         │
  │  msgsnd(类型=1, 数据) ───►  │ [msg1][msg2][msg3]...   │
  │                            │                         │
  │                            │ ◄── msgrcv(要类型=1) ──  │
  │                            │      取出匹配的一条       │

消息队列 = 内核(或中间件)里的带类型的邮箱,进程往里投信、从里取信,彼此不必同步握手。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>

struct queue_msg {
    long mtype;       // 消息类型,必须 > 0
    char mtext[100];  // 消息正文
};

static key_t make_key() {
    key_t key = ftok("/tmp", 'B');
    if (key == -1) {
        perror("ftok");
    }
    return key;
}

static int get_queue(int create) {
    key_t key = make_key();
    if (key == -1) {
        return -1;
    }
    int flags = create ? (IPC_CREAT | 0666) : 0666;
    int msgid = msgget(key, flags);
    if (msgid == -1) {
        perror("msgget");
    }
    return msgid;
}

static int run_sender(int msgid) {
    struct queue_msg msg;
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, Message Queue!");

    printf("[Sender] pid=%d, sending (mtype=%ld): %s\n",
           getpid(), msg.mtype, msg.mtext);

    if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd");
        return 1;
    }
    printf("[Sender] done\n");
    return 0;
}

static int run_receiver(int msgid) {
    struct queue_msg msg;
    // 第 4 个参数 1:只接收 mtype == 1 的消息
    ssize_t n = msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
    if (n == -1) {
        perror("msgrcv");
        return 1;
    }
    printf("[Receiver] pid=%d, received (mtype=%ld): %s\n",
           getpid(), msg.mtype, msg.mtext);
    printf("[Receiver] done\n");
    return 0;
}

static void usage(const char *prog) {
    printf("Usage:\n");
    printf("  %s              # 自动演示:创建队列 + fork 收发\n", prog);
    printf("  %s send         # 只发送一条消息(另开终端配合 recv)\n", prog);
    printf("  %s recv         # 只接收一条消息(另开终端配合 send)\n", prog);
    printf("  %s clean        # 删除消息队列\n", prog);
}

int main(int argc, char *argv[]) {
    setvbuf(stdout, NULL, _IONBF, 0); // 演示时立刻打印,避免 fork 后缓冲丢失

    // ---------- 双终端演示 ----------
    if (argc >= 2) {
        if (strcmp(argv[1], "send") == 0) {
            int msgid = get_queue(1);
            if (msgid == -1) {
                return 1;
            }
            return run_sender(msgid);
        }
        if (strcmp(argv[1], "recv") == 0) {
            int msgid = get_queue(1);
            if (msgid == -1) {
                return 1;
            }
            return run_receiver(msgid);
        }
        if (strcmp(argv[1], "clean") == 0) {
            int msgid = get_queue(0);
            if (msgid == -1) {
                // 队列本来就不存在
                if (errno == ENOENT) {
                    printf("Queue not found, nothing to clean\n");
                    return 0;
                }
                return 1;
            }
            if (msgctl(msgid, IPC_RMID, NULL) == -1) {
                perror("msgctl");
                return 1;
            }
            printf("Message queue removed\n");
            return 0;
        }
        usage(argv[0]);
        return 1;
    }

    // ---------- 一键自动演示(父子进程)----------
    int msgid = get_queue(1);
    if (msgid == -1) {
        return 1;
    }
    printf("Message queue ready, msgid=%d\n", msgid);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        msgctl(msgid, IPC_RMID, NULL);
        return 1;
    }

    if (pid == 0) { // 子进程:发送
        int rc = run_sender(msgid);
        fflush(stdout);
        _exit(rc);
    }

    // 父进程:接收
    int rc = run_receiver(msgid);
    wait(NULL);

    // 演示结束删除队列(否则会一直留在内核里)
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        return 1;
    }
    printf("Cleaned up message queue\n");
    return rc;
}

输出,单终端

1
2
3
4
5
6
7
./ipc_queue 
Message queue ready, msgid=4
[Sender] pid=40782, sending (mtype=1): Hello, Message Queue!
[Sender] done[Receiver] pid=40781, received (mtype=1): Hello, Message Queue!

[Receiver] done
Cleaned up message queue

输出,双终端

1
2
3
4
5
6
7
8
9
# Terminal 1
./ipc_queue send 
[Sender] pid=40886, sending (mtype=1): Hello, Message Queue!
[Sender] done

# Terminal 2
./ipc_queue recv 
[Receiver] pid=40931, received (mtype=1): Hello, Message Queue!
[Receiver] done

消息队列适合少量数据交换,不必处理共享冲突。但收发都要在用户态与内核态之间拷贝,数据一大就会频繁陷入内核,开销明显。

共享内存

共享内存让多个进程映射同一块物理内存,是最快的IPC:建好后按普通内存读写,无需每次系统调用,也避免用户态与内核态来回拷贝。 原理上,各进程有独立的PCB(进程控制块)、虚拟地址空间和页表;MMU(内存管理单元)通过页表把不同进程的虚拟地址映射到同一物理页,这块物理页就是共享内存。一端写入,另一端立即可见。 注意:共享内存本身不提供同步机制,需配合信号量(Semaphore)或互斥锁(Mutex)防止 “并发冲突”(如两个进程同时写同一块内存)。[1]

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

#define SHM_SIZE 1024

// 共享内存布局:用 ready 做简单同步(1=有数据可读)
struct shm_block {
    volatile int ready;
    char text[SHM_SIZE - sizeof(int)];
};

static key_t make_key() {
    key_t key = ftok("/tmp", 'A');
    if (key == -1) {
        perror("ftok");
    }
    return key;
}

static int get_shm(int create, int *created) {
    key_t key = make_key();
    if (key == -1) {
        return -1;
    }
    if (created) {
        *created = 0;
    }
    if (!create) {
        int shmid = shmget(key, SHM_SIZE, 0666);
        if (shmid == -1) {
            perror("shmget");
        }
        return shmid;
    }

    // 优先尝试新建,以便初始化 ready
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid != -1) {
        if (created) {
            *created = 1;
        }
        return shmid;
    }
    if (errno != EEXIST) {
        perror("shmget");
        return -1;
    }
    shmid = shmget(key, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget");
    }
    return shmid;
}

static struct shm_block *attach(int shmid) {
    void *p = shmat(shmid, NULL, 0);
    if (p == (void *)-1) {
        perror("shmat");
        return NULL;
    }
    return (struct shm_block *)p;
}

static void init_if_new(int shmid, int created) {
    if (!created) {
        return;
    }
    struct shm_block *shm = attach(shmid);
    if (!shm) {
        return;
    }
    shm->ready = 0;
    shm->text[0] = '\0';
    shmdt(shm);
}

static int run_writer(int shmid) {
    struct shm_block *shm = attach(shmid);
    if (!shm) {
        return 1;
    }

    shm->ready = 0;
    strcpy(shm->text, "Hello, Shared Memory!");
    // 先写完数据,再置 ready,让读端看到完整内容
    shm->ready = 1;

    printf("[Writer] pid=%d, wrote: %s\n", getpid(), shm->text);
    shmdt(shm);
    printf("[Writer] done\n");
    return 0;
}

static int run_reader(int shmid) {
    struct shm_block *shm = attach(shmid);
    if (!shm) {
        return 1;
    }

    // 等待写端把 ready 置为 1(演示用忙等;正式代码可用信号量)
    printf("[Reader] pid=%d, waiting...\n", getpid());
    while (shm->ready != 1) {
        usleep(10 * 1000);
    }

    printf("[Reader] pid=%d, read: %s\n", getpid(), shm->text);
    shm->ready = 0;
    shmdt(shm);
    printf("[Reader] done\n");
    return 0;
}

static void usage(const char *prog) {
    printf("Usage:\n");
    printf("  %s              # 自动演示:创建共享内存 + fork 读写\n", prog);
    printf("  %s write        # 写入共享内存(另开终端配合 read)\n", prog);
    printf("  %s read         # 读取共享内存(另开终端配合 write)\n", prog);
    printf("  %s clean        # 删除共享内存段\n", prog);
}

int main(int argc, char *argv[]) {
    setvbuf(stdout, NULL, _IONBF, 0);

    if (argc >= 2) {
        if (strcmp(argv[1], "write") == 0) {
            int created = 0;
            int shmid = get_shm(1, &created);
            if (shmid == -1) {
                return 1;
            }
            init_if_new(shmid, created);
            return run_writer(shmid);
        }
        if (strcmp(argv[1], "read") == 0) {
            int created = 0;
            int shmid = get_shm(1, &created);
            if (shmid == -1) {
                return 1;
            }
            init_if_new(shmid, created);
            return run_reader(shmid);
        }
        if (strcmp(argv[1], "clean") == 0) {
            int shmid = get_shm(0, NULL);
            if (shmid == -1) {
                if (errno == ENOENT) {
                    printf("Shared memory not found, nothing to clean\n");
                    return 0;
                }
                return 1;
            }
            if (shmctl(shmid, IPC_RMID, NULL) == -1) {
                perror("shmctl");
                return 1;
            }
            printf("Shared memory removed\n");
            return 0;
        }
        usage(argv[0]);
        return 1;
    }

    // ---------- 一键自动演示 ----------
    int created = 0;
    int shmid = get_shm(1, &created);
    if (shmid == -1) {
        return 1;
    }
    init_if_new(shmid, created);

    // 确保本轮演示从“未就绪”开始
    struct shm_block *shm = attach(shmid);
    if (!shm) {
        return 1;
    }
    shm->ready = 0;
    shmdt(shm);
    printf("Shared memory ready, shmid=%d\n", shmid);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        shmctl(shmid, IPC_RMID, NULL);
        return 1;
    }

    if (pid == 0) { // 子进程:写
        // 稍等,让父进程先进入等待,演示更清晰
        usleep(50 * 1000);
        int rc = run_writer(shmid);
        fflush(stdout);
        _exit(rc);
    }

    // 父进程:读
    int rc = run_reader(shmid);
    wait(NULL);

    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        return 1;
    }
    printf("Cleaned up shared memory\n");
    return rc;
}

输出,单终端:

1
2
3
4
5
6
7
8
./ipc_sharememo 
Shared memory ready, shmid=2
[Reader] pid=41183, waiting...
[Writer] pid=41184, wrote: Hello, Shared Memory!
[Writer] done
[Reader] pid=41183, read: Hello, Shared Memory!
[Reader] done
Cleaned up shared memory

输出,双终端:

1
2
3
4
5
6
7
8
9
10
# Terminal 1
./ipc_sharememo read
[Reader] pid=41224, waiting...
[Reader] pid=41224, read: Hello, Shared Memory!
[Reader] done

# Terminal 2
./ipc_sharememo write
[Writer] pid=41265, wrote: Hello, Shared Memory!
[Writer] done

信号量

信号量,可以理解为进程间同步的“红绿灯”。[2] 本质是通过P()减1,V()加1操作,控制对共享资源的访问。

信号量 S 表示可用资源数。P、V 必须是原子操作。

P(申请 / wait)

  1. S = S - 1
  2. S >= 0:申请成功,进程继续跑
  3. S < 0:没有资源,进程阻塞,进入等待队列

|S| 在小于 0 时,可理解为正在等待的进程数。)

V(释放 / signal)

  1. S = S + 1
  2. S > 0:没有人在等,只是资源变多
  3. S <= 0:说明有进程在等,唤醒一个等待进程

P:占资源;没资源就睡

V:还资源;有人在等就叫醒一个

互斥时初值常为 1(二值信号量);同步/计数时初值可为 N(资源个数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/wait.h>

// Linux 要求用户自行定义 semun
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

static key_t make_key() {
    key_t key = ftok("/tmp", 'C');
    if (key == -1) {
        perror("ftok");
    }
    return key;
}

static int get_sem(int create) {
    key_t key = make_key();
    if (key == -1) {
        return -1;
    }
    int flags = create ? (IPC_CREAT | 0666) : 0666;
    int semid = semget(key, 1, flags);
    if (semid == -1) {
        perror("semget");
    }
    return semid;
}

static int sem_init_binary(int semid) {
    union semun arg;
    arg.val = 1; // 二值信号量:1 表示资源空闲
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl SETVAL");
        return -1;
    }
    return 0;
}

// P 操作:S = S - 1,若 S < 0 则阻塞
static int sem_P(int semid) {
    struct sembuf op = {0, -1, SEM_UNDO};
    if (semop(semid, &op, 1) == -1) {
        perror("semop P");
        return -1;
    }
    return 0;
}

// V 操作:S = S + 1,若有进程在等则唤醒一个
static int sem_V(int semid) {
    struct sembuf op = {0, 1, SEM_UNDO};
    if (semop(semid, &op, 1) == -1) {
        perror("semop V");
        return -1;
    }
    return 0;
}

// 演示:用信号量保护临界区,同一时刻只允许一个进程进入
static int enter_critical(const char *name) {
    int semid = get_sem(1);
    if (semid == -1) {
        return 1;
    }

    printf("[%s] pid=%d, 准备 P(申请资源)...\n", name, getpid());
    if (sem_P(semid) == -1) {
        return 1;
    }

    printf("[%s] pid=%d, ★ 进入临界区\n", name, getpid());
    sleep(2); // 模拟占用资源;此时另一进程应阻塞在 P 上
    printf("[%s] pid=%d, ★ 离开临界区,执行 V(释放)\n", name, getpid());

    if (sem_V(semid) == -1) {
        return 1;
    }
    printf("[%s] pid=%d, 完成\n", name, getpid());
    return 0;
}

static void usage(const char *prog) {
    printf("Usage:\n");
    printf("  %s              # 自动演示:两进程竞争临界区\n", prog);
    printf("  %s init         # 创建信号量并设初值为 1\n", prog);
    printf("  %s worker A     # 进程 A 进入临界区(可开两终端)\n", prog);
    printf("  %s worker B     # 进程 B 进入临界区\n", prog);
    printf("  %s clean        # 删除信号量\n", prog);
}

int main(int argc, char *argv[]) {
    setvbuf(stdout, NULL, _IONBF, 0);

    if (argc >= 2) {
        if (strcmp(argv[1], "init") == 0) {
            int semid = get_sem(1);
            if (semid == -1) {
                return 1;
            }
            if (sem_init_binary(semid) == -1) {
                return 1;
            }
            printf("Semaphore ready, semid=%d, value=1\n", semid);
            return 0;
        }
        if (strcmp(argv[1], "worker") == 0) {
            const char *name = (argc >= 3) ? argv[2] : "worker";
            return enter_critical(name);
        }
        if (strcmp(argv[1], "clean") == 0) {
            int semid = get_sem(0);
            if (semid == -1) {
                if (errno == ENOENT) {
                    printf("Semaphore not found, nothing to clean\n");
                    return 0;
                }
                return 1;
            }
            if (semctl(semid, 0, IPC_RMID) == -1) {
                perror("semctl IPC_RMID");
                return 1;
            }
            printf("Semaphore removed\n");
            return 0;
        }
        usage(argv[0]);
        return 1;
    }

    // ---------- 一键自动演示:父子进程互斥访问临界区 ----------
    int semid = get_sem(1);
    if (semid == -1) {
        return 1;
    }
    if (sem_init_binary(semid) == -1) {
        return 1;
    }
    printf("Semaphore ready, semid=%d, initial value=1\n", semid);
    printf("观察:同一时刻只有一个进程在临界区内\n\n");

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        semctl(semid, 0, IPC_RMID);
        return 1;
    }

    if (pid == 0) {
        int rc = enter_critical("Child ");
        fflush(stdout);
        _exit(rc);
    }

    // 父进程稍晚一点启动,更容易看出“等待”
    usleep(200 * 1000);
    int rc = enter_critical("Parent");
    wait(NULL);

    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        return 1;
    }
    printf("\nCleaned up semaphore\n");
    return rc;
}

输出,单终端: 后到的进程会卡在「准备 P」,等先进入的进程做完 V 才能进临界区——说明同一时刻只有一个进程占用资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
./ipc_Semaphore 
Semaphore ready, semid=3, initial value=1
观察:同一时刻只有一个进程在临界区内

[Child ] pid=42275, 准备 P(申请资源)...
[Child ] pid=42275, ★ 进入临界区
[Parent] pid=42274, 准备 P(申请资源)...
[Child ] pid=42275, ★ 离开临界区,执行 V(释放)
[Child ] pid=42275, 完成
[Parent] pid=42274, ★ 进入临界区
[Parent] pid=42274, ★ 离开临界区,执行 V(释放)
[Parent] pid=42274, 完成

Cleaned up semaphore

输出,双终端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Terminal 1
./ipc_Semaphore init
Semaphore ready, semid=4, value=1
./ipc_Semaphore worker A
[A] pid=42387, 准备 P(申请资源)...
[A] pid=42387, ★ 进入临界区
[A] pid=42387, ★ 离开临界区,执行 V(释放)
[A] pid=42387, 完成

# Terminal 2
./ipc_Semaphore worker B 
[B] pid=42466, 准备 P(申请资源)...
[B] pid=42466, ★ 进入临界区
[B] pid=42466, ★ 离开临界区,执行 V(释放)
[B] pid=42466, 完成

信号

信号是异步通信机制,用于通知进程发生了某个事件。 信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说我们可以通过键盘输入某些组合键给进程发送信号,比如常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是通过 kill 系列的命令给进程发送信号,比如 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其立即结束。[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 $ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 2) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
1)  SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
2)  SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
3)  SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
4)  SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
5)  SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
6)  SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
7)  SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
8)  SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
9)  SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
10) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
11) SIGRTMAX-1  64) SIGRTMAX

示例[3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void handler(int sig) {
    printf("Received signal: %d\n", sig);
}

int main() {
    signal(SIGINT, handler); // 注册SIGINT处理函数
    signal(SIGALRM, handler); // 注册SIGALRM处理函数
  
    alarm(3); // 3秒后发送SIGALRM
  
    while(1) {
        pause(); // 等待信号
    }
    return 0;
}

输出

1
2
3
4
5
6
7
./icp_signal 
Received signal: 14 # 卡住
^CReceived signal: 2 # 键盘 ctrl + c
^CReceived signal: 2 # 键盘 ctrl + c
^CReceived signal: 2 # 键盘 ctrl + c
^Z                   # 键盘 ctrl + zs
[1]+  Stopped                 ./icp_signal

套接字

Socket(套接字) 是操作系统提供的一种 通信端点:进程通过它发数据、收数据,既可以在同一台机器上通信,也可以在网络上跨机器通信。

可以把它想成电话听筒:一端绑定地址/端口,另一端连过来,两边就能说话。

机制范围特点
管道 / 消息队列 / 共享内存主要本机快,但不方便跨机器
Socket本机或网络统一接口,可跨主机

本机也可用 Unix Domain Socket(路径像 /tmp/xxx.sock),语义类似网络 socket,但不走网卡。

常见类型

  • TCP(流式):可靠、有序、面向连接(先 connect/accept 再收发)
  • UDP(数据报):无连接、按包发送,不保证到达和顺序

典型流程(TCP)

1
2
服务端: socket → bind → listen → accept → recv/send → close
客户端: socket → connect → send/recv → close

Socket = 进程间/网络间通信的 标准接口;IPC 里它负责“能连上、能收发”,尤其适合客户端–服务端和跨机器场景。

Socket 套接字是网络通信的基石 ,是支持 TCP/IP 协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息。[1]也叫五元组。 (连接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址, 远地进程的协议端口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>

// Unix Domain Socket:本机进程间通信常用;路径像文件,但不走网卡
static const char *SOCK_PATH = "/tmp/demo_ipc.sock";

static void remove_sock_file() {
    unlink(SOCK_PATH); // 不存在也无所谓
}

static int run_server() {
    int listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket");
        return 1;
    }

    remove_sock_file();

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
        close(listen_fd);
        return 1;
    }
    if (listen(listen_fd, 1) == -1) {
        perror("listen");
        close(listen_fd);
        return 1;
    }

    printf("[Server] pid=%d, listening on %s\n", getpid(), SOCK_PATH);

    int conn_fd = accept(listen_fd, NULL, NULL);
    if (conn_fd == -1) {
        perror("accept");
        close(listen_fd);
        return 1;
    }
    printf("[Server] client connected\n");

    char buf[128] = {0};
    ssize_t n = read(conn_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        printf("[Server] received: %s\n", buf);
        const char *reply = "Hello from Server via Socket!";
        write(conn_fd, reply, strlen(reply) + 1);
        printf("[Server] replied: %s\n", reply);
    }

    close(conn_fd);
    close(listen_fd);
    remove_sock_file();
    printf("[Server] done\n");
    return 0;
}

static int run_client() {
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1) {
        perror("socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    // 服务端可能还没 listen,稍作重试
    int ok = 0;
    for (int i = 0; i < 50; i++) {
        if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
            ok = 1;
            break;
        }
        if (errno != ENOENT && errno != ECONNREFUSED) {
            perror("connect");
            close(fd);
            return 1;
        }
        usleep(50 * 1000);
    }
    if (!ok) {
        fprintf(stderr, "connect: server not ready\n");
        close(fd);
        return 1;
    }

    printf("[Client] pid=%d, connected\n", getpid());

    const char *msg = "Hello from Client via Socket!";
    write(fd, msg, strlen(msg) + 1);
    printf("[Client] sent: %s\n", msg);

    char buf[128] = {0};
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        printf("[Client] received: %s\n", buf);
    }

    close(fd);
    printf("[Client] done\n");
    return 0;
}

static void usage(const char *prog) {
    printf("Usage:\n");
    printf("  %s              # 自动演示:fork 出服务端 + 客户端\n", prog);
    printf("  %s server       # 启动服务端(另开终端配合 client)\n", prog);
    printf("  %s client       # 启动客户端\n", prog);
    printf("  %s clean        # 删除 %s\n", prog, SOCK_PATH);
}

int main(int argc, char *argv[]) {
    setvbuf(stdout, NULL, _IONBF, 0);

    if (argc >= 2) {
        if (strcmp(argv[1], "server") == 0) {
            return run_server();
        }
        if (strcmp(argv[1], "client") == 0) {
            return run_client();
        }
        if (strcmp(argv[1], "clean") == 0) {
            remove_sock_file();
            printf("Removed %s (if existed)\n", SOCK_PATH);
            return 0;
        }
        usage(argv[0]);
        return 1;
    }

    // ---------- 一键自动演示 ----------
    printf("Unix Domain Socket demo: %s\n\n", SOCK_PATH);

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        // 子进程:客户端(稍等服务端 bind/listen)
        usleep(100 * 1000);
        int rc = run_client();
        fflush(stdout);
        _exit(rc);
    }

    // 父进程:服务端
    int rc = run_server();
    wait(NULL);
    return rc;
}

输出,单终端:

1
2
3
4
5
6
7
8
9
10
11
12
./ipc_socket 
Unix Domain Socket demo: /tmp/demo_ipc.sock

[Server] pid=42753, listening on /tmp/demo_ipc.sock
[Server] client connected[Client] pid=42754, connected

[Client] sent: Hello from Client via Socket!
[Server] received: Hello from Client via Socket!
[Server] replied: Hello from Server via Socket!
[Client] received: Hello from Server via Socket!
[Client] done
[Server] done

输出,多终端:

1
2
3
4
5
6
7
8
9
10
# Terminal 1
./ipc_socket server
[Server] pid=42769, listening on /tmp/demo_ipc.sock

# Terminal 2
./ipc_socket client
[Client] pid=42801, connected
[Client] sent: Hello from Client via Socket!
[Client] received: Hello from Server via Socket!
[Client] done

以上程序用 Unix Domain Socket 演示本机进程通信。跨网络时接口几乎一样,只是地址从 sockaddr_un 换成 sockaddr_in。

说明
路径/tmp/demo_ipc.sock(像文件,但是套接字)
范围本机 IPC;换成AF_INET + IP/端口即可跨主机
模型面向连接的流式通信(类似 TCP)

小结

机制适用场景要点
管道(Pipe)流式数据传输内核缓冲区,表现为特殊文件。匿名管道通常限有亲缘关系的进程;命名管道(FIFO)有路径,无关进程也可用。无格式字节流,简单传输合适,频繁交换效率一般。
消息队列(Message Queue)结构化消息传递内核中的消息链表,可带自定义结构/类型。读走即删除,有消息边界。收发需用户态 ↔ 内核态拷贝,大数据量开销大。
共享内存(Shared Memory)高性能大数据传输不同进程虚拟地址经页表映射到同一物理内存。仅创建/映射时陷入内核,之后普通读写,速度最快。本身无同步,并发写易冲突。
信号量(Semaphore)进程同步和资源保护用 P(申请)/ V(释放)控制资源计数,实现互斥与同步。常与共享内存配合。注意:信号量 ≠ 信号。
信号(Signal)事件通知和简单控制唯一的异步 IPC,随时通知“发生了某事”。可自定义处理函数;不适合传大量数据。用户、内核、进程都可发送。
Socket本机 + 跨网络通信本机或跨主机均可(尤其跨网络)。同机也可用(含 Unix Domain Socket)。

以上六类常用的除外,还有RPC(远程过程调用),MQ(消息队列中间件)等微服务和分布式系统常用的一些IPC技术。

扩展阅读

[1] https://zhuanlan.zhihu.com/p/465574868 [2] https://blog.csdn.net/macowl/article/details/155109379 [3] https://www.cnblogs.com/Rare-30/p/19014020

本文由作者按照 CC BY 4.0 进行授权