简介
进程间通信(Inter-Process Communication,IPC)是指不同的进程之间进行数据交换和通信的机制。进程间通信允许多个独立运行的进程协同工作以完成某个任务或共享信息。在操作系统中,有多种方式可以实现进程间通信,以下是一些常见的 IPC 方法:
-
管道(Pipe):
- 管道是一种在父进程和子进程之间进行单向通信的方式,通常用于相关进程之间的通信。
- 在C语言中,可以使用
pipe
函数创建管道,并使用write
和read
函数来在进程之间发送和接收数据。
-
命名管道(Named Pipe,FIFO):
- 命名管道是一种有名字的管道,允许不相关的进程进行通信。
- 它们通常用于不同的进程之间进行数据交换,无论它们是否有关联。
-
消息队列(Message Queue):
- 消息队列允许进程通过将消息放入队列中来进行通信。这些消息可以按照顺序进行处理。
- 消息队列提供了更灵活的通信方式,可以传递不同类型的数据。
-
共享内存(Shared Memory):
- 共享内存允许多个进程在它们之间共享一块内存区域。这样的进程可以读写这块内存,以实现高速数据传输。
- 共享内存通常需要同步机制,以防止数据竞争。
-
信号量(Semaphore):
- 信号量是一种用于进程同步和互斥的机制,通常用于解决竞态条件和临界区问题。
- 进程可以使用信号量来获取对某个共享资源的访问权限。
-
套接字(Socket):
- 套接字是一种网络编程中常用的 IPC 方法,允许不同主机上的进程之间进行通信。
- 套接字可以用于本地进程间通信(本地套接字)或远程主机间通信(网络套接字)。
-
文件(File):
- 进程可以通过读写文件进行通信,通常用于进程之间需要长期共享数据的情况。
- 文件通信通常需要文件锁定机制,以避免竞争条件。
不同的 IPC 方法适用于不同的应用场景和需求。选择适当的 IPC 方法取决于进程之间的关系、通信频率、数据传输量以及其他特定要求。在设计和实现应用程序时,需要仔细考虑IPC机制以确保正确的数据传输和进程协作。
一、管道
(1)概念
管道(Pipe)是一种用于进程间通信的机制,它允许一个进程的输出成为另一个进程的输入。管道通常用于在父进程和子进程之间或不相关的进程之间创建一个单向的数据流。在Linux和Unix系统中,管道是一种非常常见的IPC方式。
(2)管道的分类
(a)半双工管道
半双工管道(Half-Duplex Pipe)是管道的一种类型,它允许数据在两个进程之间进行单向的通信,但通信方向只能是一个方向的任一方。这意味着在半双工管道中,数据只能在一个方向上流动,要实现双向通信,需要创建两个独立的半双工管道,一个用于每个通信方向。
半双工管道通常用于以下情况:
父子进程通信:在具有亲缘关系的父子进程之间,可以使用一个半双工管道用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。这种方式是双向通信的一个典型应用。
命令行管道(命令行管道):在命令行中,使用
|
操作符可以将一个命令的输出重定向到另一个命令的输入,这实际上是在后台创建了一个半双工管道,其中一个方向用于命令的输出,另一个方向用于命令的输入。半双工管道的特点:
- 只能支持单向通信,要实现双向通信需要创建两个独立的半双工管道。
- 数据只能在一个方向上流动,通常用于父子进程之间的通信或者命令行管道。
- 半双工管道在创建时可以使用
pipe
系统调用来创建,其中一个描述符用于读取,另一个描述符用于写入。在C语言中,可以使用
pipe
函数来创建半双工管道,使用read
和write
函数来进行读写操作。半双工管道通常是同步的,当没有数据可读或写时,进程的操作可能会被阻塞,直到数据可用或者有空间可用于写入。总之,半双工管道是一种用于单向通信的IPC机制,适用于父子进程之间的通信或者命令行管道等场景。要实现双向通信,需要创建两个独立的半双工管道,一个用于每个通信方向。
(b)全双工管道
全双工管道(Full-Duplex Pipe)是管道的一种类型,它允许数据在两个进程之间进行双向的通信,同时支持读取和写入数据。与半双工管道不同,全双工管道允许数据在两个方向上自由流动,因此可以实现双向通信。
全双工管道的特点包括:
双向通信:允许两个进程之间进行双向的数据通信,每个进程既可以读取数据也可以写入数据。
支持同时读写:每个进程可以同时进行读操作和写操作,而不会出现冲突。
数据流动自由:数据可以在管道中的两个方向上自由流动,不会出现阻塞。
用途广泛:全双工管道通常用于需要双向通信的进程之间,可以用于父子进程通信、进程间协作等。
在C语言中,可以使用系统调用
pipe
来创建全双工管道。pipe
函数将返回两个文件描述符,一个用于读取(通常标识为0),另一个用于写入(通常标识为1)。进程可以使用这些描述符来进行读取和写入操作。
(c)命名全双工管道(下面)
(3)用法
全双工代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
int parent_to_child_pipe[2]; // 父进程到子进程的管道,0为读端,1为写端
int child_to_parent_pipe[2]; // 子进程到父进程的管道,0为读端,1为写端
char buffer[100];
// 创建父进程到子进程的管道
if (pipe(parent_to_child_pipe) == -1) {
perror("pipe (parent_to_child)");
return 1;
}
// 创建子进程到父进程的管道
if (pipe(child_to_parent_pipe) == -1) {
perror("pipe (child_to_parent)");
return 1;
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
close(parent_to_child_pipe[1]); // 关闭父进程到子进程管道的写端
close(child_to_parent_pipe[0]); // 关闭子进程到父进程管道的读端
const char* message_from_parent = "Hello from the parent!";
// 子进程从父进程读取数据
read(parent_to_child_pipe[0], buffer, sizeof(buffer));
printf("Child received from parent: %s\n", buffer);
// 子进程向父进程写入数据
write(child_to_parent_pipe[1], message_from_parent, strlen(message_from_parent) + 1);
close(parent_to_child_pipe[0]); // 关闭父进程到子进程管道的读端
close(child_to_parent_pipe[1]); // 关闭子进程到父进程管道的写端
} else { // 父进程
close(parent_to_child_pipe[0]); // 关闭父进程到子进程管道的读端
close(child_to_parent_pipe[1]); // 关闭子进程到父进程管道的写端
const char* message_from_child = "Hello from the child!";
// 父进程向子进程写入数据
write(parent_to_child_pipe[1], message_from_child, strlen(message_from_child) + 1);
// 父进程从子进程读取数据
read(child_to_parent_pipe[0], buffer, sizeof(buffer));
printf("Parent received from child: %s\n", buffer);
close(parent_to_child_pipe[1]); // 关闭父进程到子进程管道的写端
close(child_to_parent_pipe[0]); // 关闭子进程到父进程管道的读端
}
return 0;
}
二、命名管道
(1)概念
命名管道(Named Pipe),又称为FIFO(First-In-First-Out),是一种特殊类型的管道,用于在进程间进行通信。与普通(匿名)管道不同,命名管道在文件系统中有一个唯一的名称,允许不相关的进程通过这个名称来访问同一个管道,从而实现进程之间的双向通信。
命名管道在文件系统中有一个唯一的名称,通常表示为一个文件路径。这个名称用于标识管道,不同的进程可以使用这个名称来打开和访问管道。虽然命名管道在文件系统中以文件的形式存在,但它们不实际存储文件数据。它们只是用于进程之间传输数据的通道。
(2)用法
(a)创建命名管道
使用mkfifo创建命名管道
格式:
mkfifo(文件路径,权限)
mkfifo("/xxx/xxx", 0666);
///xxx/xxx为管道名称;0666为文件权限
(b)删除命名管道
使用unlink删除管道文件
格式:
unlink(文件名)
(3)特点
1.与匿名管道一样,命名管道通常是阻塞的。当没有数据可读取或没有空间可写入时,进程的操作可能会被阻塞,直到有数据可用。
2.命名管道可以支持双向通信,任何进程都可以在需要时向管道写入数据或从管道读取数据。这使得它非常适用于进程间的双向通信。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
int main() {
char *fifo_name = "/tmp/my_fifo"; // 命名管道的路径
char buffer[100];
// 创建命名管道
if (mkfifo(fifo_name, 0666) == -1) {
perror("mkfifo");
return 1;
}
// 打开管道进行读取
int fd_read = open(fifo_name, O_RDONLY);
if (fd_read == -1) {
perror("open (read)");
return 1;
}
// 打开管道进行写入
int fd_write = open(fifo_name, O_WRONLY);
if (fd_write == -1) {
perror("open (write)");
return 1;
}
// 父进程向管道写入数据
const char* message_from_parent = "Hello from the parent!";
write(fd_write, message_from_parent, strlen(message_from_parent) + 1);
// 子进程从管道读取数据
read(fd_read, buffer, sizeof(buffer));
printf("Child received: %s\n", buffer);
// 关闭管道和删除命名管道文件
close(fd_write);
close(fd_read);
unlink(fifo_name);
return 0;
}
注意:
需要注意的是,命名管道文件不同于常规文件,它们的目的是用于进程间通信,而不是用于存储数据。通常情况下,当不再需要命名管道时,应及时删除管道文件,以便释放相关资源并确保不会引起不必要的资源占用。
三、消息队列
(1)概念
Linux消息队列是一种进程间通信(IPC)机制,允许不同的进程在同一台Linux系统上通过消息进行通信。这些消息可以是数据、控制信息或信号,它们被发送到消息队列,然后由接收者进程从队列中提取并处理。
(2)用法
(a)创建或者获取一个消息队列
int msgget(key_t key, int flag)
key : 标识符
flag : 用于指定创建或获取消息队列时的选项和权限
返回值 :若成功返回消息队列ID
flag参数:
IPC_CREAT:如果消息队列不存在,则创建一个新的消息队列。如果消息队列已存在,这个标志不起作用。
IPC_EXCL:与
IPC_CREAT
结合使用,用于确保只有创建者可以访问该消息队列。如果消息队列已存在,并且使用了IPC_EXCL
标志,那么msgget
将失败(返回 -1)。IPC_PRIVATE:用于创建一个私有的消息队列,通常不与
IPC_CREAT
一起使用。它会生成一个不公开的唯一键,只能由当前进程及其子进程访问。权限位:您可以使用权限位来设置消息队列的权限,以控制哪些进程可以访问它。权限位通常包括
IPC_OWNER_R
(所有者读取权限)、IPC_OWNER_W
(所有者写入权限)、IPC_GROUP_R
(组读取权限)、IPC_GROUP_W
(组写入权限)和IPC_OTHER_R
(其他用户读取权限)等。这些权限位可以与操作位或运算来组合。
key可以由函数ftok生成
小小知识:
客户端和服务端认同一个路径名和项目ID(项目ID为0~255之间的字符值),调用ftok函数将这两个值转为一个key
#include <sys/ipc.h>
key_t ftok(const char*path. int id)
例如:key_t key = ftok("tmp/data",'A');
(b)控制消息队列
int msgctl(int msgid, int cmd,struct msqid_ds *buf)
msgid: 消息队列ID
cmd: 指定msgid所需要执行的命令
buf:一个指向
struct msqid_ds
结构的指针,用于存储或提供消息队列的属性信息。返回值 :返回执行状态,0成功,-1失败
cmd参数:
IPC_STAT
:获取消息队列的状态信息,将结果存储在struct msqid_ds
结构中。IPC_SET
:设置消息队列的属性,需要提供struct msqid_ds
结构的数据以修改相应的属性。IPC_RMID
:删除消息队列,将消息队列从系统中移除。- 其他特定于操作系统的命令,如
IPC_INFO
、MSG_STAT
、MSG_INFO
等。获取消息队列状态信息:
struct msqid_ds queue_info; if (msgctl(msqid, IPC_STAT, &queue_info) == -1) { perror("msgctl IPC_STAT"); return -1; } // 使用 queue_info 结构获取消息队列的属性信息
设置消息队列的属性:
struct msqid_ds new_info; // 修改 new_info 结构的字段,以设置新的属性 if (msgctl(msqid, IPC_SET, &new_info) == -1) { perror("msgctl IPC_SET"); return -1; }
删除消息队列:
if (msgctl(msqid, IPC_RMID, NULL) == -1) { perror("msgctl IPC_RMID"); return -1; }
(c)发送信息到消息队列
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag)
msqid: 消息队列ID
ptr: 一个指向消息数据的指针,通常是一个用户定义的结构,用于存储消息的内容。
nbytes:消息的大小,以字节为单位。通常,这应该等于 ptr指向的数据结构的大小。
flag:发送消息的选项标志。
返回值 :0成功,1失败
flag参数:
IPC_NOWAIT
:如果消息队列已满,不要等待,立即返回错误。MSG_NOERROR
:如果消息太大而无法放入消息队列,不要截断消息,立即返回错误。
(d)接受消息队列消息
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag )
msqid: 消息队列ID
ptr: 一个指向接收消息的缓冲区的指针,通常是一个用户定义的结构,用于存储接收到的消息内容。
nbytes:接收缓冲区的大小,以字节为单位。通常,这应该等于
msgp
指向的数据结构的大小。type:消息类型,用于选择性地接收特定类型的消息。如果设置为0,将接收队列中的第一条消息。否则,只有
msgtyp
与消息类型相匹配的消息才会被接收。flag:接收消息的选项标志
返回值 :0成功,1失败
flag参数:
IPC_NOWAIT
:如果队列为空,不要等待,立即返回错误。MSG_NOERROR
:如果消息太大而无法放入接收缓冲区,不要截断消息,立即返回错误。
(3)特点
异步通信:消息队列支持异步通信,发送方和接收方之间的交互是非实时的。这意味着发送方可以将消息发送到队列中,然后继续执行其他任务,而不必等待接收方立即处理消息。
解耦应用程序组件:消息队列可以用于将不同的应用程序组件解耦。发送方和接收方之间不需要直接通信,而是通过队列进行通信,这使得系统更加灵活和可维护。
支持发布-订阅模式:消息队列通常支持发布-订阅模式,其中多个消费者可以订阅相同的消息主题或通道。这使得广播消息和事件通知变得容易。
缓冲和削峰:消息队列可以用作缓冲器,以平衡生产者和消费者之间的速率差异。它可以处理突发的高负载,防止系统过载,并确保消息的顺序传递。
持久性:消息队列通常具有持久性,这意味着消息在存储中可靠地保留,即使在系统故障或重启后也不会丢失。这对于重要的通信和数据不丢失的要求非常重要。
可靠性:消息队列通常提供高度可靠的消息传递机制,确保消息的交付和处理。这可以通过重试机制和确认机制来实现。
消息传递模式:消息队列支持多种消息传递模式,包括点对点和发布-订阅。点对点模式用于将消息从一个生产者传递给一个特定的消费者,而发布-订阅模式用于将消息广播给多个订阅者。
扩展性:消息队列通常是可扩展的,可以处理大量的消息和高并发的请求。您可以添加更多的消息代理或消费者来增加系统的吞吐量。
消息路由:消息队列通常支持灵活的消息路由和过滤,以便将消息传递给特定的接收方或处理程序。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
// 定义消息结构
struct message {
long mtype;
char mtext[256];
};
int main() {
int msgid;
key_t key;
struct message msg;
// 1. 创建消息队列
key = ftok("/tmp/myfile", 'A');
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
return 1;
}
// 2. 发送消息到队列
msg.mtype = 1; // 指定消息类型
strcpy(msg.mtext, "Hello, Message Queue!");
if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
return 1;
}
printf("Sent message to the queue.\n");
// 3. 接收消息从队列
struct message received_msg;
received_msg.mtype = 1; // 指定要接收的消息类型
if (msgrcv(msgid, &received_msg, sizeof(received_msg.mtext), received_msg.mtype, 0) == -1) {
perror("msgrcv");
return 1;
}
printf("Received message from the queue: %s\n", received_msg.mtext);
// 4. 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl IPC_RMID");
return 1;
}
printf("Message queue deleted.\n");
return 0;
}
注意:
权限和安全性:消息队列是一种共享资源,因此需要谨慎处理其权限。确保只有授权的进程可以发送和接收消息。使用
msgctl
函数来设置和检查消息队列的权限。消息类型:每条消息都有一个类型(
mtype
),在发送和接收消息时需要正确匹配。消息类型通常用于选择性地接收特定类型的消息。消息大小:消息队列有消息大小限制,确保发送的消息不超过消息队列的限制。如果消息太大,可能会导致消息截断或发送失败。
错误处理:在每个关键操作后,检查返回值以处理错误。System V IPC 函数通常将错误信息存储在
errno
中,可用perror
函数打印错误消息。队列清理:确保在不再需要消息队列时进行清理。使用
msgctl
函数删除队列以释放系统资源。消息队列的持久性:消息队列通常是持久的,即使发送进程退出,消息仍然在队列中等待接收。因此,接收进程应该处理队列中可能积累的消息。
进程同步:消息队列可以用于进程间同步,但要小心避免死锁和竞态条件。确保进程在使用消息队列时采取适当的同步措施,如互斥锁。
错误处理和异常情况:考虑错误处理和异常情况,例如队列已满或队列为空。根据需要使用合适的标志位,如
IPC_NOWAIT
。消息队列命名冲突:使用唯一的键(key)来标识消息队列,以避免与其他应用程序冲突。
内存管理:消息队列使用系统内存,因此请确保不会耗尽可用内存。及时删除不再需要的消息队列。
跨平台兼容性:请注意,System V IPC 在不同的Unix/Linux系统上可能有轻微的差异,因此在不同平台上运行时可能需要进行适度的调整。
五、信号量
(1)概念
信号量(Semaphore)是一种用于进程间同步和互斥的同步原语,通常用于协调多个进程或线程对共享资源的访问。信号量是一个整数变量,可以进行原子操作,用于表示可用资源的数量或用于进行进程之间的通信。
信号量的主要目的是解决多个进程之间的竞争条件问题,确保多个进程可以有序地访问共享资源,而不会导致数据损坏或不一致。信号量的核心思想是通过控制资源的访问来实现同步和互斥。
主要有两种类型的信号量:
二进制信号量:也称为互斥信号量(Mutex),它只有两个可能的值,通常是0和1。二进制信号量通常用于实现互斥锁(Mutex Lock),用于控制对共享资源的互斥访问。一个进程或线程可以尝试获取互斥锁,如果锁已被占用,它将被阻塞等待,直到锁被释放。
计数信号量:计数信号量可以拥有多个可能的值,通常是一个非负整数。计数信号量通常用于控制资源的数量或限制并发访问。一个进程或线程可以通过执行P(等待)和V(释放)操作来请求和释放资源,每次P操作会减少信号量的值,而V操作会增加信号量的值。当信号量的值为0时,进程将被阻塞等待资源变为可用。
信号量的基本操作包括两种:
P 操作(等待操作):用于请求资源或锁定资源。如果信号量的值大于0,P操作将减少信号量的值,并允许进程继续执行。如果信号量的值为0或负数,P操作将进程阻塞,直到资源变为可用。
V 操作(释放操作):用于释放资源或解锁资源。V操作增加信号量的值,并通知等待的进程资源已经可用。
(2)用法
(a)创建信号量集
semget
函数用于创建一个新的信号量集或获取现有信号量集的标识符。它返回一个信号量标识符,该标识符可用于后续信号量操作。int semget(key_t key, int nsems, int semflg);
key
:用于唯一标识信号量集的键值。(同消息队列key获取方式一样)nsems
:信号量集中的信号量数量。semflg
:用于设置信号量集的标志,通常是创建标志(IPC_CREAT)和权限标志(如 0666)的组合。nsems是该集合的信号量数,如果创建新的集合(一般在服务器进程中),则必须指定nsems。如果引用现有集合(一个客户端进程),则将nsems指定为0。
(b)控制信号量的行为
semctl
函数用于执行不同的控制操作,如获取/设置信号量集的属性、获取/设置信号量的值、删除信号量集等。它可以执行多种操作,具体操作由参数cmd
决定。int semctl(int semid, int semnum, int cmd, ...);
semid
:信号量集的标识符,通常是由semget
返回的。semnum
:信号量在信号量集中的索引,从0开始。cmd
:控制命令,指定要执行的操作。可以是 IPC_STAT(获取信息)、IPC_SET(设置信息)、IPC_RMID(删除信号量集)等。...
:根据cmd
参数的不同,可能需要提供额外的参数,如struct semid_ds
结构或整数值。
cmd参数 :
IPC_STAT:获取信号量集的状态信息。
- 使用时需要提供一个
struct semid_ds
结构的指针作为可变参数(第四个参数),该结构将用于存储获取到的状态信息。IPC_SET:设置信号量集的属性。
- 使用时需要提供一个
struct semid_ds
结构的指针作为可变参数(第四个参数),该结构包含要设置的新属性信息。IPC_RMID:删除信号量集。
- 不需要提供可变参数,该命令将删除指定的信号量集。
GETALL:获取所有信号量的值。
- 使用时需要提供一个整数数组的指针作为可变参数(第四个参数),该数组将用于存储获取到的所有信号量的值。
SETALL:设置所有信号量的值。
- 使用时需要提供一个整数数组的指针作为可变参数(第四个参数),该数组包含要设置的所有信号量的新值。
GETVAL:获取单个信号量的值。
- 使用时需要提供一个整数的指针作为可变参数(第四个参数),该整数将用于存储获取到的单个信号量的值。
SETVAL:设置单个信号量的值。
- 使用时需要提供一个整数作为可变参数(第四个参数),该整数表示要设置的单个信号量的新值。
GETNCNT:获取等待信号量值变为非零的进程数。
- 使用时需要提供一个整数的指针作为可变参数(第四个参数),该整数将用于存储等待信号量变为非零的进程数。
GETZCNT:获取等待信号量值变为零的进程数。
- 使用时需要提供一个整数的指针作为可变参数(第四个参数),该整数将用于存储等待信号量变为零的进程数。
GETPID:获取上次执行 V(释放) 操作的进程的PID。
- 使用时需要提供一个整数的指针作为可变参数(第四个参数),该整数将用于存储上次执行 V 操作的进程的PID。
(c)执行一组P(等待)和V(释放)操作
semop
函数用于执行一系列P(等待)和V(释放)操作,以改变信号量的值。这个函数通常用于原子地执行多个操作,以确保进程之间的同步和互斥。int semop(int semid, struct sembuf *sops, unsigned int nsops);
semid
:信号量集的标识符。sops
:一个指向struct sembuf
结构数组的指针,每个结构描述一个P(等待)或V(释放)操作。nsops
:sops
数组中的操作数。
(d)初始化一个计数信号量
sem_init
函数用于在内存中创建和初始化一个计数信号量。它通常用于线程间通信,而不是进程间通信。int sem_init(sem_t *sem, int pshared, unsigned int value);
sem
:一个指向sem_t
类型的信号量变量的指针,用于存储新创建的信号量。pshared
:指示信号量是进程间共享(非零)还是线程间共享(零)。value
:信号量的初始值。
(e)销毁一个计数信号量
sem_destroy
函数用于销毁一个通过sem_init
创建的计数信号量,释放相关的资源。int sem_destroy(sem_t *sem);
sem
:一个指向sem_t
类型的信号量变量的指针,要销毁的信号量。
(f)执行P(等待)操作
sem_wait
函数用于请求资源或锁定资源。如果信号量的值大于0,则将其减一,并允许进程继续执行。如果信号量的值为0,进程将被阻塞等待资源变为可用。int sem_wait(sem_t *sem);
sem
:一个指向sem_t
类型的信号量变量的指针,用于执行P(等待)操作。
(g)执行V(释放)操作
sem_post
函数用于释放资源或解锁资源。它增加信号量的值,并通知等待的进程资源已经可用。int sem_post(sem_t *sem);
sem
:一个指向sem_t
类型的信号量变量的指针,用于执行V(释放)操作。
(h)获取信号量的当前值
sem_getvalue
函数用于获取信号量的当前值,通常用于调试和监视。int sem_getvalue(sem_t *sem, int *sval);
sem
:一个指向sem_t
类型的信号量变量的指针,要获取其值的信号量。sval
:一个指向整数的指针,用于存储信号量的当前值。
(3)特点
计数器形式:信号量是一个整数计数器,通常用于表示可用资源的数量。它可以是任意非负整数,允许多个进程或线程同时访问或占用共享资源,或者用于控制并发访问的数量。
原子操作:信号量操作通常是原子操作,确保操作的不可分割性。这意味着多个进程或线程可以同时尝试执行 P(等待)和 V(释放) 操作,但只有一个会成功,其他的会被阻塞。
用于同步和互斥:信号量可以用于实现同步,确保多个进程或线程按照特定的顺序执行,也可以用于实现互斥,防止多个进程或线程同时访问共享资源,从而避免竞态条件。
等待和通知机制:信号量提供了等待和通知机制,允许进程或线程等待某个条件满足后才能继续执行。等待时,进程或线程可以被阻塞,直到信号量的值满足某个条件。
跨进程或线程使用:信号量可用于进程间通信(Inter-Process Communication,IPC)或线程间通信(Inter-Thread Communication,ITC),使得不同进程或线程之间可以进行同步和互斥操作。
多种类型:信号量有两种主要类型,包括二进制信号量和计数信号量。二进制信号量只有两个可能的值(0和1),通常用于互斥操作。计数信号量可以拥有多个可能的值,通常用于资源控制或限制并发访问。
持久性:信号量通常是持久的,即使进程退出,信号量的状态仍然保持。这使得它们可以在多个进程或线程之间共享和传递信息。
(4)exit时的信号量调整
正如前面提到的,如果在进程终止时,它占用了由信号量分配的资源,那么就会成为一个问题。无论何时只要信号量操作指定了SEM_UNDO标志,然后分配资源,那么内核就会记住对于该特定信号量,分配给调用进程多少资源。当改进程终止时,不论自愿或者不自愿,内核都会将检验该进程是否还有尚未处理的信号量调整值,如果有,则按调整值对应信号量值进行处理。
如果带有SETVAL或SETALL命令的semctl设置的信号量的值,则在所有的进程中,该信号量的调整值都将设置为0.
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main() {
// 创建并初始化信号量集
key_t key = ftok("/tmp/myfile", 'A'); // 创建一个唯一的键
int semid = semget(key, 1, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
exit(1);
}
// 设置信号量的初始值
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
} arg;
arg.val = 5; // 设置信号量的初始值为 5
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL");
exit(1);
}
// 获取信号量的状态信息
struct semid_ds sem_info;
if (semctl(semid, 0, IPC_STAT, &sem_info) == -1) {
perror("semctl IPC_STAT");
exit(1);
}
printf("Semaphore ID: %d\n", semid);
printf("Semaphore Value: %d\n", semctl(semid, 0, GETVAL));
// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(1);
}
return 0;
}
四、共享内存
(1)概念
Linux进程间通信(IPC)的一种方式是使用共享内存(Shared Memory)。共享内存是一块可以被多个进程共同访问的内存区域,它允许进程之间高效地共享数据,而无需通过复制数据来传递信息。是由操作系统维护的一块内存区域,可以通过键(key)来唯一标识。多个进程可以通过相同的键来访问同一个共享内存段。通常,创建共享内存段需要提供键和内存大小等信息。主要目的是在多个进程之间共享数据。这些进程可以独立运行,彼此无需知道对方的存在,但通过访问共享内存,它们可以实现数据的传递和共享,从而协同工作。
(2)用法
(a)创建一个新的共享内存段或获取现有共享内存段的标识符
通常在进程间通信开始时由一个进程调用,用于创建或获取共享内存区域。
int shmget(key_t key, size_t size, int shmflg);
key
:用于唯一标识共享内存段的键值。(同消息队列获取key方式相同)size
:共享内存段的大小(以字节为单位)。shmflg
:用于设置共享内存段的标志,通常包括创建标志(IPC_CREAT)和权限标志(如 0666)的组合。- 返回值:成功时返回共享内存标识符,失败时返回-1。
(a)将共享内存段连接到当前进程的地址空间
在进程需要访问共享内存数据时调用,以便能够读写共享内存中的数据。
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:共享内存标识符,由shmget
返回。shmaddr
:指定将共享内存连接到进程地址空间的地址,通常设置为NULL以由系统自动选择适当的地址。shmflg
:标志参数,通常设置为0。- 返回值:成功时返回指向共享内存的指针,失败时返回-1。
(a)分离共享内存段,使其不再与当前进程相关联
在进程不再需要访问共享内存时调用,以释放对共享内存的连接。
(当对共享存储段的操作已经结束时,调用shmdt与该段分离,注意,并不是冲系统中删除其标识符以及相关的数据结构,该表示符仍然存在,知道某个进程(一般为服务器)带IPC_RMID命令的调用shmctl特地删除位置)
int shmdt(const void *shmaddr);
shmaddr
:指向共享内存的指针,由shmat
返回。- 返回值:成功时返回0,失败时返回-1。
(a)控制共享内存段,执行不同的控制操作
用于管理共享内存,例如获取共享内存状态、设置权限、删除共享内存等。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存标识符,由shmget
返回。cmd
:控制命令,用于执行不同的操作,如获取共享内存信息、设置共享内存权限、删除共享内存等。buf
:一个指向struct shmid_ds
结构的指针,用于存储共享内存的状态信息。- 返回值:成功时返回0,失败时返回-1。
(3)特点
高效性:共享内存是一种高效的IPC机制,因为数据直接存储在共享内存段中,而不需要复制或传递数据。这使得共享内存在大量数据传输和访问的场景中非常高效。
快速访问:由于数据存储在共享内存中,进程可以直接访问数据,而无需像管道或消息队列那样进行读取和写入操作。这导致共享内存通常比其他IPC方式更快速。
实时性:共享内存允许多个进程并发地访问相同的数据,这对于需要实时共享和更新数据的应用程序非常有用。
大容量:共享内存支持大容量的数据传输,因为它可以利用系统中可用的物理内存。这对于需要处理大量数据的应用程序非常重要。
多种数据结构支持:共享内存不限制您存储的数据类型或数据结构,可以用于传递任何类型的数据,包括简单的原始数据类型、复杂的数据结构和对象。
跨平台:共享内存机制通常在不同的操作系统上都有支持,因此可以用于实现跨平台的进程间通信。
持久性:共享内存段通常会持续存在,即使创建它的进程退出,除非显式地删除共享内存段。这允许不同进程在不同时间访问相同的共享内存。
独立性:共享内存允许不同进程独立地读写共享内存段,而无需了解其他进程的存在或状态。这提供了进程之间的松散耦合。
注意:
- 同步问题:多个进程访问共享内存时需要适当的同步机制,以防止竞态条件和数据不一致性。
- 复杂性:使用共享内存需要小心处理内存分配和释放,以及数据的完整性和安全性。
- 不适用于远程通信:共享内存通常局限于同一台计算机上的进程之间,不适用于远程通信。
(4)示例代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
key_t key = ftok("/tmp/myfile", 'A'); // 生成唯一的键
int shm_size = 1024; // 共享内存大小,以字节为单位
// 创建共享内存段
int shmid = shmget(key, shm_size, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
printf("Shared Memory ID: %d\n", shmid);
// 连接共享内存段
void *shm_ptr = shmat(shmid, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat");
exit(1);
}
// 在这里可以访问共享内存中的数据
// 分离共享内存段
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
exit(1);
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(1);
}
printf("Shared Memory Segment removed.\n");
return 0;
}
六、套接字
(1)概念
套接字(Socket)是一种用于实现进程间通信的通用机制,不仅可以用于不同计算机之间的网络通信,还可以用于同一台计算机上的进程间通信(Inter-Process Communication,IPC)。
(2)用法
(a)创建套接字
用于创建一个套接字,指定通信域(例如AF_INET表示IPv4)、套接字类型(例如SOCK_STREAM表示TCP套接字)、以及使用的协议(通常为0表示自动选择协议)。
int socket(int domain, int type, int protocol);
domain
:指定套接字的通信域,例如AF_INET
表示IPv4,AF_INET6
表示IPv6。type
:指定套接字的类型,例如SOCK_STREAM
表示流套接字(TCP),SOCK_DGRAM
表示数据报套接字(UDP)。protocol
:指定使用的协议,通常为0,表示根据domain
和type
自动选择协议。
(b)绑定地址
用于服务器端,在创建套接字后,将其绑定到指定的IP地址和端口号上。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:套接字描述符,表示要绑定的套接字。addr
:指向struct sockaddr
结构的指针,包含要绑定的IP地址和端口号信息。addrlen
:addr
结构的大小。
(c)监听连接
用于服务器端,在绑定套接字后,开始监听客户端的连接请求,
backlog
参数指定待处理连接请求的最大数量。int listen(int sockfd, int backlog);
sockfd
:套接字描述符,表示要监听的套接字。backlog
:等待连接队列的最大长度,表示待处理连接请求的最大数量。
(d)建立连接
用于服务器端,当有客户端请求连接时,
accept
函数返回一个新的套接字,用于与客户端进行通信。(服务端)int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
:服务器套接字描述符,表示要接受连接请求的套接字。addr
:指向struct sockaddr
结构的指针,用于存储客户端的地址信息。addrlen
:addr
结构的大小。
用于客户端,尝试连接到服务器的IP地址和端口号。(客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:套接字描述符,表示要建立连接的套接字。addr
:指向struct sockaddr
结构的指针,包含要连接的目标服务器的地址信息。addrlen
:addr
结构的大小。
(e)数据交换
TCP:
用于在连接的套接字之间发送和接收数据。
send
用于发送数据,recv
用于接收数据。ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd
:套接字描述符,表示要发送或接收数据的套接字。buf
:指向要发送或接收数据的缓冲区。len
:要发送或接收的数据的长度。flags
:传输标志,通常为0,可以用于指定操作的特定选项。UDP:
与
send
和recv
类似,但可以指定目标地址信息,适用于UDP等无连接协议。ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
:套接字描述符,表示要发送或接收数据的套接字。buf
:指向要发送或接收数据的缓冲区。len
:要发送或接收的数据的长度。flags
:传输标志,通常为0,可以用于指定操作的特定选项。dest_addr
:目标地址结构指针,用于sendto
指定目标地址,recvfrom
用于返回发送方的地址信息。addrlen
:dest_addr
结构的大小,recvfrom
中用于返回发送方地址信息的大小。
(f)关闭连接
用于释放套接字资源,不再使用套接字时应调用此函数。
int close(int sockfd);
sockfd
:套接字描述符,表示要关闭的套接字。
(3)特点
全双工通信:套接字提供了全双工通信的能力,这意味着两个进程可以同时进行读取和写入操作。
支持多种协议:套接字支持多种通信协议,包括TCP(面向连接的协议)和UDP(无连接的协议),可以根据需要选择合适的协议。
网络和本地通信:套接字可以用于网络通信,也可以用于本地通信。对于本地通信,通常使用Unix域套接字(Unix Domain Socket)。
灵活性:套接字通信非常灵活,可以传递任何类型的数据,包括文本、二进制数据、文件等。
广泛支持:套接字通信被支持在几乎所有主流操作系统上,包括Linux、Windows、macOS等。