Fork me on GitHub
0%

IPC进程间通讯

简介

IPC:Inter-Process Communication,进程间通信。描述的是运行在某个操作系统之上的不同进程间各个消息传递的方式,进程间通信类型通常包含以下几种:无名管道、有名管道、信号、共享内存、消息队列、信号量、套接字等

目的

  • 数据传输。一个进程数据传输给另外一个进程
  • 共享数据。一个进程对共享区的数据进行更改,其它进程可以看到
  • 通知事件。一个进程向另一个进程发送消息,通知某个事件(进程终止通知父进程)
  • 资源共享。多个进程间共享同样的资源,需要内核提供锁和同步机制
  • 进程控制。有些进程希望完全控制另一个进程的执行(Debug进程),此时控制进程希望能够拦截另一个进程的所有的Trap陷入和Exception异常,并能够及时知道状态e

无名管道pipe

当输入输出数据量比较大的时候,管道这种IPC机制非常有用。但是管道这种通讯方式有两种限制,第一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。

要想创建一个简单的管道,可以使用系统调用pipe()函数。这个函数将返回两个文件描述符,一个读端、一个写端。如果使用pipe进行双方的通信,需要建立两个通道。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void parent_process(int pipe_fd[2]) {
// 关闭管道的写入端,因为父进程将从管道中读取数据
close(pipe_fd[1]);
// 从管道中读取数据
char buffer[100];
read(pipe_fd[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
// 关闭读取端
close(pipe_fd[0]);
}

void child_process(int pipe_fd[2]) {
// 关闭管道的读取端,因为子进程将向管道中写入数据
close(pipe_fd[0]);
// 向管道中写入数据
const char *message = "Hello from child";
write(pipe_fd[1], message, strlen(message) + 1);
// 关闭写入端
close(pipe_fd[1]);
}

int main() {
// 创建管道
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程执行的代码
child_process(pipe_fd);
} else {
// 父进程执行的代码
parent_process(pipe_fd);
}
return 0;
}

命名管道FIFO

命名管道是一种具有持久性的管道,允许无关的进程之间进行通信。命名管道是建立在实际的磁盘介质上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。
实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

使用mkfifo命令或mkfifo系统调用创建有名管道,它会在文件系统中生成一个特殊的文件。进程1通过打开这个文件获取写入文件描述符,进程2通过打开同一个文件获取读取文件描述符。数据通过管道的缓冲区传递,和无名管道类似,当缓冲区满或为空时,写入或读取进程可能会被阻塞

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FIFO_FILE "myfifo"

void write_to_fifo() {
int fd;
// 打开FIFO文件,如果不存在则创建
mkfifo(FIFO_FILE, 0666);
// 以只写方式打开FIFO文件
fd = open(FIFO_FILE, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 向FIFO写入数据
const char *message = "Hello from writer";
write(fd, message, strlen(message) + 1);
// 关闭文件描述符
close(fd);
}

void read_from_fifo() {
int fd;
char buffer[100];
// 以只读方式打开FIFO文件
fd = open(FIFO_FILE, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 从FIFO读取数据
read(fd, buffer, sizeof(buffer));
printf("Reader received: %s\n", buffer);
// 关闭文件描述符
close(fd);
}

int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程执行写入操作
write_to_fifo();
} else {
// 父进程执行读取操作
read_from_fifo();
// 删除FIFO文件
unlink(FIFO_FILE);
}
return 0;
}

信号

信号是一种异步通信方式,用于通知进程发生了某个事件。可以用于处理外部事件、错误等。常见的信号包括SIGINT(中断)、SIGTERM(终止)等。

信号同时又是一种软中断,当某进程接收到信号时会终止当前程序的执行去处理信号的注册函数,然后回到断点程序继续往下执行
。信号事件的发生通常由两类原因引起,一种是硬件原因引起,比如敲击键盘等;另外一种是软件原因引起,比如调用kill、alarm函数等,同时系统异常(除0)等问题也会引发信号的产生。

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

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function
void signal_handler(int signum) {
if (signum == SIGUSR1) {
printf("Received SIGUSR1 signal\n");
}
}

int main() {
// Register signal handler for SIGUSR1
if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}
printf("Waiting for SIGUSR1 signal...\n");
pid_t pid = getpid();
kill(pid, SIGUSR1);
printf("Exiting the program\n");
return 0;
}

共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024
#define SHM_KEY 1234

int main() {
// 创建共享内存段
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 将共享内存段附加到进程的地址空间
char *shared_memory = (char *)shmat(shmid, NULL, 0);
if (shared_memory == (char *)(-1)) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 写入数据到共享内存
const char *message = "Hello from writer";
strncpy(shared_memory, message, strlen(message));
// 分离共享内存段
if (shmdt(shared_memory) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
// 创建另一个进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程执行的代码
// 将共享内存段附加到子进程的地址空间
char *child_shared_memory = (char *)shmat(shmid, NULL, 0);
if (child_shared_memory == (char *)(-1)) {
perror("shmat in child process");
exit(EXIT_FAILURE);
}
// 从共享内存读取数据
printf("Reader received: %s\n", child_shared_memory);
// 分离共享内存段
if (shmdt(child_shared_memory) == -1) {
perror("shmdt in child process");
exit(EXIT_FAILURE);
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(EXIT_FAILURE);
}
} else {
// 父进程执行的代码
// 等待子进程执行完毕
wait(NULL);
}
return 0;
}

消息队列( message queue )

消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

消息队列是消息的链接表,包括 Posix 消息队列 system V 消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

信号量( semophore )

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身

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
// 编译 gcc your_program.c -o your_program -pthread
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>

// 全局变量,作为信号量
sem_t semaphore;

// 线程函数,用于等待并增加信号量的值
void *thread_function(void *arg) {
printf("Thread is waiting...\n");
// 等待信号量
sem_wait(&semaphore);
// 临界区代码
printf("Thread is in the critical section\n");
// 离开临界区后增加信号量的值
sem_post(&semaphore);
return NULL;
}

int main() {
// 初始化信号量
if (sem_init(&semaphore, 0, 1) == -1) {
perror("sem_init");
exit(EXIT_FAILURE);
}
// 创建线程
pthread_t thread;
if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}
printf("Main thread is waiting...\n");
// 等待信号量
sem_wait(&semaphore);
// 临界区代码
printf("Main thread is in the critical section\n");
// 离开临界区后增加信号量的值
sem_post(&semaphore);
// 等待线程执行完毕
if (pthread_join(thread, NULL) != 0) {
perror("pthread_join");
exit(EXIT_FAILURE);
}
// 销毁信号量
if (sem_destroy(&semaphore) == -1) {
perror("sem_destroy");
exit(EXIT_FAILURE);
}
return 0;
}

套接字( socket )

Socket(套接字)是一种提供不同进程间通信的接口,通常用于网络编程。它是一个抽象层,通过它,程序可以通过网络进行数据交换,也可以在同一台计算机内的不同进程之间进行通信。Socket 提供了一种通用的编程模型,允许两个不同的进程在网络上或本地进行通信。通信可以是单向的(单工)、双向的(全双工或半双工)。

  • 服务器端

    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <arpa/inet.h>

    #define PORT 8080
    #define BUFFER_SIZE 1024

    int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_address, client_address;
    char buffer[BUFFER_SIZE] = {0};
    // 创建服务器套接字
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
    perror("socket failed");
    exit(EXIT_FAILURE);
    }
    // 设置服务器地址结构
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(PORT);
    // 绑定服务器套接字到指定地址和端口
    if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
    perror("bind failed");
    exit(EXIT_FAILURE);
    }
    // 监听客户端连接
    if (listen(server_socket, 3) < 0) {
    perror("listen failed");
    exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 接受客户端连接
    int addrlen = sizeof(client_address);
    if ((client_socket = accept(server_socket, (struct sockaddr *)&client_address, (socklen_t*)&addrlen)) < 0) {
    perror("accept failed");
    exit(EXIT_FAILURE);
    }
    // 从客户端接收消息
    read(client_socket, buffer, BUFFER_SIZE);
    printf("Server received: %s\n", buffer);
    // 发送回复给客户端
    const char *reply_message = "Hello from server";
    send(client_socket, reply_message, strlen(reply_message), 0);
    printf("Server sent: %s\n", reply_message);
    // 关闭套接字
    close(server_socket);
    close(client_socket);
    return 0;
    }
  • 客户端

    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
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <arpa/inet.h>

    #define PORT 8080
    #define BUFFER_SIZE 1024

    int main() {
    int client_socket;
    struct sockaddr_in server_address;
    char buffer[BUFFER_SIZE] = {0};
    // 创建客户端套接字
    if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
    perror("socket failed");
    exit(EXIT_FAILURE);
    }
    // 设置服务器地址结构
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(PORT);
    // 将IPv4地址从文本转换为二进制
    if (inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr) <= 0) {
    perror("inet_pton failed");
    exit(EXIT_FAILURE);
    }
    // 连接到服务器
    if (connect(client_socket, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
    perror("connect failed");
    exit(EXIT_FAILURE);
    }
    // 发送消息给服务器
    const char *message = "Hello from client";
    send(client_socket, message, strlen(message), 0);
    printf("Client sent: %s\n", message);
    // 接收服务器的回复
    read(client_socket, buffer, BUFFER_SIZE);
    printf("Client received: %s\n", buffer);
    // 关闭套接字
    close(client_socket);
    return 0;
    }