目录
1. 进程间通信目的
进程间通信(Inter-Process Communication, IPC)的目的是为了在操作系统中不同进程之间共享数据、协调操作和同步执行。
具体来说,进程间通信的主要目的包括以下几方面:
1. 数据交换
- 目的:实现多个进程之间的数据共享和传递。
- 场景:
- 客户端与服务器之间的数据交互(如 Web 服务器与浏览器)。
- 多个子进程需要共享计算结果或中间数据。
- 示例:
- 一个进程从传感器读取数据,另一个进程处理和存储这些数据。
2. 资源共享
- 目的:协调多个进程访问同一资源(如文件、共享内存、数据库等),避免冲突和资源竞争。
- 场景:
- 数据库系统中,多个进程访问同一数据表。
- 多个进程共享同一个打印机资源。
- 示例:
- 使用信号量控制对共享文件的并发写操作。
3. 进程同步
- 目的:确保多个进程按照正确的顺序执行,以避免数据不一致或竞争条件。
- 场景:
- 一个进程的输出需要作为另一个进程的输入。
- 父进程需要等待子进程完成某个任务后再继续执行。
- 示例:
- 使用信号量或条件变量实现生产者-消费者模型。
4. 事件通知
- 目的:通知进程某个事件的发生,使其采取相应的操作。
- 场景:
- 文件系统通知应用程序某个文件被修改。
- 父进程收到子进程的退出信号。
- 示例:
- 使用信号(Signal)通知进程接收到外部中断。
5. 任务协作
- 目的:实现多个进程协同工作,完成复杂任务。
- 场景:
- 在分布式系统中,不同进程分布在多个节点上,需要协作完成任务。
- 数据处理流水线中,不同进程负责不同处理阶段。
- 示例:
- 使用管道或消息队列在不同进程之间传递任务。
6. 提高系统效率
- 目的:通过进程间通信优化资源利用率,提高系统的整体效率。
- 场景:
- 在多核系统中,将任务分配到多个进程并行处理。
- 数据密集型应用中,一个进程负责 I/O,另一个进程负责计算。
- 示例:
- 使用共享内存提高进程间大数据量传输的效率。
7. 隔离与安全
- 目的:通过进程间通信在进程之间建立明确的边界,同时提供数据共享的机制。
- 场景:
- 沙箱环境中,限制进程的直接交互,仅允许通过 IPC 通道通信。
- 系统进程与用户进程之间通过安全通道通信。
- 示例:
- 使用消息队列在高安全性环境中传递数据。
2. 进程间通信的方式
下面我们来着重介绍linux中进程间通信的两种方式:
2.1 管道
1. 匿名管道
特点:
- 只能用于父子进程之间通信:匿名管道只能在具有亲缘关系的进程(如父进程与子进程)之间使用,因为管道由文件描述符标识,而文件描述符只能在亲缘进程间继承。
- 没有名字:管道在内核中存在,但没有名字,仅通过文件描述符访问。
- 全双工限制:单个匿名管道通常是单向的(写端向读端发送数据)。如果需要双向通信,需要创建两个管道。
- 自动关闭:当所有进程都关闭了管道的文件描述符时,管道会被自动回收。
管道其实是一个很形象的名称,用示意图来理解:
这种通信方式就像在进程之间装了管道一样,可以实现数据的‘‘单向流动’’。
pipe函数
- 功能:创建一个未命名管道(匿名管道)。
- 原型:
int pipe(int pipefd[2]);
- 参数:
pipefd
:包含两个文件描述符的数组,pipefd[0]
用于读取,pipefd[1]
用于写入。
- 返回值:
- 成功:返回 0;
- 失败:返回 -1,并设置
errno
。
- 使用限制:
- 匿名管道只能用于具有亲缘关系的进程(父子、兄弟进程等)。
示例:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main() {
int pipefd[2];
char buf[1024];
pipe(pipefd); // 创建管道
if (fork() == 0) { // 子进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello from child", 17); // 写数据
close(pipefd[1]); // 关闭写端
} else { // 父进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buf, sizeof(buf)); // 读取数据
printf("Parent received: %s\n", buf);
close(pipefd[0]); // 关闭读端
}
return 0;
}
注意:管道要在子进程创建前创建,这样才能继承到管道。
2. 匿名管道原理
为什么管道要在子进程创建前创建呢?我们来看看匿名管道的原理:
由上图可知,管道也是文件,由进程的文件描述符表管理,这也符合linux一切皆文件的特性。
同时,管道并不是普通的文件,而是由内核级的文件缓冲区实现的
尽管管道使用了文件描述符,但它与常规文件有所不同:
-
管道是临时的:
- 管道数据存储在内存中,而普通文件的数据存储在磁盘上。
- 管道缓冲区在进程关闭或系统重启时会被释放。
-
管道是流式的:
- 管道类似于数据流,没有文件指针和随机访问功能。
- 写入的数据必须按顺序读取,不能像普通文件一样定位(
seek
)。
-
管道大小有限:
- 管道缓冲区的大小固定,超出缓冲区的写入操作会阻塞,直到读端读取部分数据释放空间。
管道关闭情形:
情况 1:写端关闭,读端未关闭
当读端调用 read()
:
- 如果管道中还有数据,
read()
会继续读取这些数据。 - 如果管道中没有数据,
read()
返回0
,表示管道已到达 EOF(End of File)。
#include <unistd.h>
#include <stdio.h>
int main() {
int pipefd[2];
char buf[1024];
pipe(pipefd);
if (fork() == 0) { // 子进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello", 5); // 写入数据
close(pipefd[1]); // 关闭写端
} else { // 父进程
close(pipefd[1]); // 关闭写端
int n = read(pipefd[0], buf, sizeof(buf));
while (n > 0) {
write(STDOUT_FILENO, buf, n); // 输出数据
n = read(pipefd[0], buf, sizeof(buf));
}
// 检测到 EOF
if (n == 0) {
printf("\nWrite end closed.\n");
}
close(pipefd[0]);
}
return 0;
}
情况2:读端关闭,写端未关闭
当写端调用 write()
:
- 如果缓冲区中还有未读取的数据,写端仍可以继续写入,直到缓冲区满为止。
- 一旦缓冲区满,
write()
调用会触发一个 SIGPIPE 信号,并终止进程(默认行为)。 - 如果进程捕获了
SIGPIPE
,write()
会返回-1
,并设置errno
为EPIPE
。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>
void handle_sigpipe(int sig) { //捕捉信号
printf("SIGPIPE caught: %d\n", sig);
}
int main() {
int pipefd[2];
pipe(pipefd);
signal(SIGPIPE,handle_sigpipe);//设置信号处理方法
if (fork() == 0) { // 子进程
close(pipefd[0]); // 关闭读端
sleep(1); // 等待父进程关闭读端
if (write(pipefd[1], "Hello", 5) == -1) {
perror("Write error");
}
close(pipefd[1]); // 关闭写端
} else { // 父进程
close(pipefd[1]); // 关闭写端
close(pipefd[0]); // 关闭读端
sleep(2); // 等待子进程写入
}
return 0;
}
3. 命名管道
管道应用的⼀个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是⼀种特殊类型的文件,和管道类似。
命名管道的特点:
- 文件系统中的实体: 命名管道是一个存在于文件系统中的文件,可以通过
ls
查看。 - 双向通信: 数据按先进先出的规则流动,可以实现双向通信,但通常是单向使用。
- 阻塞特性: 如果一端没有准备好读取/写入,另一端的操作会阻塞,直到对应的操作准备好。
- 跨进程通信: 支持任意两个进程之间的数据交换,不要求进程具有亲缘关系。
创建命名管道:
使用 mkfifo
命令:
mkfifo mypipe
使用系统调用:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数说明:
pathname
是命名管道的路径。mode
指定权限,比如0666
允许所有用户读写。
移除命名管道:
使用unlink命令:
unlink mypipe
使用系统调用:
#include <unistd.h>
int unlink(const char *pathname);
参数
pathname
:需要删除的文件的路径(绝对路径或相对路径)。
返回值
- 返回 0:表示成功删除文件。
- 返回 -1:表示删除失败,并设置
errno
来说明错误原因。
使用命名管道通信示例:
进程A写数据:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char *fifo = "/tmp/mypipe";
// 打开命名管道,写入数据
fd = open(fifo, O_WRONLY);
write(fd, "Hello from A\n", 13);
close(fd);
return 0;
}
进程B读数据:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char *fifo = "/tmp/mypipe";
char buffer[128];
// 打开命名管道,读取数据
fd = open(fifo, O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("Received: %s", buffer);
close(fd);
return 0;
}
4. 管道读写规则
-
当没有数据可读时
- O_NONBLOCK 禁用:
read
调用会阻塞,即进程暂停执行,一直等到有数据到来为止。 - O_NONBLOCK 启用:
read
调用返回-1
,errno
的值为EAGAIN
。
- O_NONBLOCK 禁用:
-
当管道满的时候
- O_NONBLOCK 禁用:
write
调用会阻塞,直到有进程读取数据。 - O_NONBLOCK 启用:
write
调用返回-1
,errno
的值为EAGAIN
。
- O_NONBLOCK 禁用:
-
特殊情况
- 如果所有管道写端对应的文件描述符被关闭,则
read
返回0
。 - 如果所有管道读端对应的文件描述符被关闭,则
write
操作会产生信号SIGPIPE
,进而可能导致write
进程退出。
- 如果所有管道写端对应的文件描述符被关闭,则
-
写入数据的原子性
- 当要写入的数据量不大于
PIPE_BUF
时,Linux 将保证写入操作的原子性。 - 当要写入的数据量大于
PIPE_BUF
时,Linux 将不再保证写入操作的原子性。
- 当要写入的数据量不大于
补充说明:
O_NONBLOCK
是文件描述符的一个标志,当启用时,read
和write
操作不会阻塞进程。PIPE_BUF
是管道的最大原子写入缓冲区大小(通常为 4096 字节,具体取决于系统实现)。
2.2 System V IPC
1.System V的背景
-
来源与发展:
- System V 是基于更早的 UNIX 版本(如 UNIX V7 和 UNIX System III)开发的,首次发布于 1983 年。
- 作为 UNIX 的一个商用分支,System V 在企业和服务器领域得到了广泛应用。
- 它与 BSD UNIX(由加州大学伯克利分校开发)是 UNIX 生态系统的两个主要分支。
-
主要版本:
- System V Release 1 (1983):最初版本。
- System V Release 2 (1984):增加了虚拟内存支持。
- System V Release 3 (1987):引入了更广泛的网络功能和 IPC(进程间通信)。
- System V Release 4 (1989):结合了 BSD、System V 和 SunOS 的特性,成为 UNIX 系统的重要里程碑。
2. System V的特性
System V 在 UNIX 系统中引入了一系列新功能和设计理念,这些特性后来成为许多 UNIX 和类 UNIX 系统的标准:
1. System V IPC(进程间通信)
System V 提供了一套强大的 IPC 机制,包括:
- 消息队列:用于进程之间的消息传递。
- 共享内存:允许进程共享内存区域,提供高效的数据交换。
- 信号量:用于进程同步和资源访问控制。
这些 IPC 特性广泛应用于需要进程协作的系统中。
2. 文件系统和目录结构
System V 引入了一些标准化的文件系统和目录结构:
- 目录标准:
/etc
:系统配置文件。/var
:可变数据存储(如日志文件)。/usr
:用户级工具和库。
- 支持 文件锁定,以避免多个进程同时修改文件内容。
3. 启动和服务管理
System V 引入了 init 系统,这是 UNIX 系统中的服务管理框架:
- 使用
/etc/inittab
文件定义系统的运行级别(runlevel)。 - 系统启动时按照运行级别加载不同的服务和脚本。
现代影响:System V 的 init 系统成为后来的服务管理系统(如 Linux 的 SysVinit 和 systemd)的基础。
4. 进程管理
System V 提供了强大的进程管理工具和功能:
- ps:查看当前系统中的进程。
- kill:向进程发送信号以终止或控制进程。
- wait 和 fork:支持父子进程管理和创建。
5. 网络和通信
System V Release 4 引入了支持 TCP/IP 协议的网络栈,扩展了网络功能,使其能够适应现代网络环境。
3. System V IPC(进程间通信)
System V 提供了多种进程间通信(Inter-Process Communication,IPC)方式,这些方式可以用于在不同的进程间交换数据、同步操作等。以下是 System V 支持的主要 IPC 方式及其特点
3.1 消息队列(Message Queue)
特点:
- 消息队列允许多个进程通过消息形式交换数据。
- 消息以 消息类型 和 消息内容 的形式存储在内核中,接收者可以根据消息类型选择性地读取消息。
- 适合异步通信,因为发送和接收操作之间不需要直接关联。
3.2 共享内存(Shared Memory)
特点:
- 共享内存允许多个进程共享一块内存区域,实现高速通信。
- 是 System V IPC 中速度最快的方式。
- 通常需要借助信号量来进行同步,防止多个进程同时访问共享内存导致数据不一致。
下面主要来说一下共享内存实现通信:
关键函数
1. shmget
shmget
用于创建或获取一个共享内存段。
函数原型:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key
:共享内存段的标识符,类似文件的路径名。多个进程通过相同的key
访问同一块内存。
(常用ftok指定文件路径以及项目号生成,生成的key值唯一)
size
:共享内存段的大小(以字节为单位)。若段已存在,size
应小于或等于已存在段的大小。shmflg
:IPC_CREAT
:如果共享内存段不存在,则创建。IPC_EXCL
:与IPC_CREAT
结合使用,如果共享内存段已存在,则返回错误。- 权限标志:与文件权限类似,例如
0666
(可读写)。
返回值:
- 成功:返回共享内存段的标识符(
shmid
)。 - 失败:返回
-1
,并设置errno
。
示例:
key_t key = ftok("path", 65);
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget failed");
}
2. shmat
shmat
用于将共享内存段附加到当前进程的地址空间。
函数原型:
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid
:共享内存段的标识符,由shmget
返回。shmaddr
:NULL
:由系统决定附加的地址。- 非
NULL
:尝试附加到指定地址(一般不推荐)。
shmflg
:0
:默认读写。SHM_RDONLY
:只读模式。
返回值:
- 成功:返回共享内存段的首地址。
- 失败:返回
(void *) -1
,并设置errno
。
示例:
void *shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
perror("shmat failed");
}
3. shmdt
shmdt
用于将共享内存段从当前进程的地址空间分离。
函数原型:
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明:
shmaddr
:共享内存段的首地址,由shmat
返回。
返回值:
- 成功:返回
0
。 - 失败:返回
-1
,并设置errno
。
示例:
if (shmdt(shared_memory) == -1) {
perror("shmdt failed");
}
4. shmctl
shmctl
用于控制共享内存段的行为,比如删除、获取信息或修改权限。
函数原型:
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
shmid
:共享内存段的标识符。cmd
:IPC_STAT
:获取共享内存段信息,填充到buf
。IPC_SET
:设置共享内存段的权限,取自buf
。IPC_RMID
:删除共享内存段。
buf
:- 用于存储或提供共享内存段的信息,结构为
struct shmid_ds
。
- 用于存储或提供共享内存段的信息,结构为
返回值:
- 成功:返回
0
。 - 失败:返回
-1
,并设置errno
。
示例:
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) == -1) {
perror("shmctl failed");
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl failed");
}
完整代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
#define SHM_SIZE 1024 // 共享内存大小
int main() {
// 创建一个键值
key_t key = ftok("shmfile", 65);
if (key == -1) {
perror("ftok failed");
exit(EXIT_FAILURE);
}
// 创建共享内存段
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget failed");
exit(EXIT_FAILURE);
}
// 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程:读取共享内存中的数据
char *shared_memory = (char *)shmat(shmid, NULL, 0);
if (shared_memory == (char *)-1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
printf("Child process reading from shared memory: %s\n", shared_memory);
// 分离共享内存
if (shmdt(shared_memory) == -1) {
perror("shmdt failed");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
} else {
// 父进程:写入共享内存
char *shared_memory = (char *)shmat(shmid, NULL, 0);
if (shared_memory == (char *)-1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
// 写入数据到共享内存
const char *message = "Hello from parent process!";
strncpy(shared_memory, message, SHM_SIZE);
printf("Parent process wrote to shared memory: %s\n", message);
// 分离共享内存
if (shmdt(shared_memory) == -1) {
perror("shmdt failed");
exit(EXIT_FAILURE);
}
// 等待子进程完成
wait(NULL);
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl failed");
exit(EXIT_FAILURE);
}
printf("Parent process deleted shared memory.\n");
}
return 0;
}
3.3 信号量(Semaphore)
特点
- 信号量是一种计数器机制,用于管理对共享资源的访问。
- 典型用途是同步进程或线程,避免多个进程同时访问共享资源(如共享内存)。
- System V 的信号量支持一组信号量操作,而不是单个信号量。
3.4 信号(Signals)
特点
- 信号是一种简单的异步通知机制,用于通知进程发生了某些事件。
- 信号用于进程间通信的应用范围有限,但常用于中断处理或通知某个特定事件(如
SIGCHLD
、SIGINT
)。