进程间通信常用参数:
IPC(Inter-Process Communication,进程间通信)是操作系统中多个进程或线程间进行数据交换或通信的机制。IPC的参数宏主要用于控制IPC对象的创建、访问、删除等操作。下面是对一些常见的IPC参数宏的详细解释:
1. 共享内存(Shared Memory)
在共享内存相关的系统调用中,常用的参数宏包括:
- IPC_CREAT:创建一个新的IPC对象(如共享内存段)。如果指定的键(key)已经存在,此选项通常与IPC_EXCL一起使用来决定是创建新对象还是打开现有对象。
- IPC_EXCL:与IPC_CREAT一起使用,确保创建一个新的IPC对象。如果键已经存在,则调用失败。
- SHM_RDONLY:在调用
shmat()
函数时,此标志表示以只读方式映射共享内存。- SHM_RND:在调用
shmat()
函数时,此标志表示将映射地址向下舍入到最近的页面边界。- IPC_RMID:在调用
shmctl()
函数时,此命令用于删除IPC对象(如共享内存段)。2. 消息队列(Message Queues)
消息队列相关的参数宏包括:
- IPC_CREAT:与共享内存相同,用于创建新的消息队列。
- IPC_EXCL:与IPC_CREAT一起使用,确保创建一个新的消息队列。
- IPC_NOWAIT:在调用
msgsnd()
或msgrcv()
时,此标志表示非阻塞模式。如果消息队列已满或为空,则调用立即返回而不是等待。- MSG_NOERROR:在调用
msgrcv()
时,如果缓冲区太小,无法存放消息正文,则忽略消息正文部分,只返回消息类型。3. 信号量(Semaphores)
信号量相关的参数宏包括:
- IPC_CREAT:创建新的信号量集。
- IPC_EXCL:与IPC_CREAT一起使用,确保创建一个新的信号量集。
- GETVAL:在调用
semctl()
时,此命令用于获取信号量的值。- SETVAL:在调用
semctl()
时,此命令用于设置信号量的值。- GETALL:在调用
semctl()
时,此命令用于获取信号量集中所有信号量的值。- SETALL:在调用
semctl()
时,此命令用于设置信号量集中所有信号量的值。- IPC_RMID:在调用
semctl()
时,此命令用于删除信号量集。4. 管道(Pipes)和命名管道(FIFOs)
虽然管道和命名管道不直接涉及复杂的参数宏,但它们的创建和访问也涉及一些标志和模式:
- O_RDONLY:以只读方式打开管道或FIFO。
- O_WRONLY:以只写方式打开管道或FIFO。
- O_NONBLOCK:非阻塞模式,用于管道或FIFO的打开操作。
注意事项
- 这些参数宏通常与特定的IPC系统调用一起使用,如
shmget()
、msgget()
、semget()
等。- 不同的操作系统和库实现可能会有所不同,但上述参数宏在大多数UNIX和类UNIX系统中是通用的。
- 在使用这些参数宏时,需要包含相应的头文件,如
<sys/ipc.h>
、<sys/shm.h>
、<sys/msg.h>
和<sys/sem.h>
(对于信号量,可能需要根据具体系统查找相应的头文件)。
同步阻塞
同步阻塞是一种通信机制,在这种机制中,客户端发送请求给服务端后,如果服务端处理任务时间较长,客户端会一直等待服务端的响应,且在此期间客户端不能进行其他操作,同时服务端也不接受其他客户端的请求。这种机制简单直观,但效率不高,因为它会导致资源的浪费和客户端的长时间等待。
同步非阻塞
同步非阻塞模式与同步阻塞模式的不同之处在于,虽然客户端发送请求后仍需等待服务端的响应,但服务端在处理一个客户端请求的同时,能够处理其他客户端的请求。这意味着服务端不会被单个长时间的任务所阻塞,从而提高了服务端的处理效率和并发能力。然而,客户端在等待响应期间仍然不能进行其他操作。
IO多路复用:管理多个阻塞
IO多路复用(Input/Output Multiplexing)是一种在单个线程中管理多个输入/输出通道的技术。它允许一个线程同时监听多个输入流(如网络套接字、文件描述符等),并在有数据可读或可写时进行相应的处理,而不需要为每个通道创建一个独立的线程。常见的IO多路复用机制包括select、poll和epoll。
- select:是最古老的IO多路复用机制,使用一个文件描述符集合来监听多个IO事件的就绪状态。
- poll:是select的改进版本,使用pollfd结构体数组来传递需要监听的文件描述符和事件类型。
- epoll:是Linux特有的IO多路复用机制,使用内核事件表来管理和监听多个IO事件的就绪状态,相比select和poll具有更高的效率。
IO多路复用通过减少线程的创建和销毁开销,提高了系统的并发性能,适用于需要同时管理和处理多个IO通道的场景,如网络编程和文件操作。
异步模型详解
异步模型是一种高效的编程模式,它允许程序在执行某个任务时,不会阻塞当前线程或其他任务的执行。以下是四种常见的异步模型:
- 回调函数(Callback)
- 特点:将函数作为参数传递给另一个函数,并在异步操作完成时调用该函数来处理结果。
- 优点:提高程序的响应性和并发性能,灵活性和可扩展性高。
- 缺点:可能导致回调地狱,难以处理异步操作之间的依赖关系和协同操作,可读性和可维护性较差。
- Promise/Future
- 特点:Promise表示一个尚未完成但预期将来会完成的异步操作的结果。Future用于从另一个执行线程中获取结果。
- 优点:提供了一种更清晰的链式调用方式,避免了回调地狱,易于理解和使用。
- 缺点:在某些情况下可能会增加代码的复杂度。
- 异步IO
- 特点:执行IO操作时,不会阻塞当前线程,而是由操作系统在IO操作完成时通知程序。
- 优点:真正实现了非阻塞IO,提高了程序的并发性能和响应速度。
- 缺点:对编程模型的要求较高,需要合理地设计和处理事件驱动的逻辑。
- 协程(Coroutine)
- 特点:协程是一种用户态的轻量级线程,可以在单个线程中执行多个协程,协程之间的切换由用户控制。
- 优点:相比线程具有更低的开销和更高的并发性能,适用于IO密集型任务。
- 缺点:需要编程语言的支持,并且需要合理地设计协程的调度和同步机制。
1、管道:
有名管道(Named Pipe,也称作FIFO)和匿名管道(Anonymous Pipe)都是进程间通信(IPC)的重要机制,但它们在多个方面存在显著区别,这些区别决定了它们各自的使用场景。以下是两者的主要区别及使用场景:
主要区别
- 名称与持久性:
- 有名管道:具有唯一名称,以文件的形式存在于文件系统中,因此是持久的,除非显式删除,否则会一直存在。
- 匿名管道:没有名称,仅存在于内存中,是临时的,随着创建它的进程的终止而消失。
- 通信范围:
- 有名管道:可以在任意两个进程之间使用,只要它们都知道管道的名称,并且可以访问该文件系统。
- 匿名管道:仅限于具有亲缘关系的进程间使用,如父子进程或兄弟进程,因为它们共享相同的资源信息。
- 创建方式:
- 有名管道:通过
mkfifo()
系统调用或mkfifo
命令创建。 - 匿名管道:通过
pipe()
系统调用创建。
- 有名管道:通过
- 操作方式:
- 两者都使用
read()
和write()
等系统调用来进行数据的读写操作。
- 两者都使用
使用场景
- 有名管道:
- 不相关进程间通信:由于有名管道可以在任意两个进程间使用,因此它特别适用于那些没有直接亲缘关系的进程间的通信。
- 跨网络通信:虽然传统的有名管道主要用于同一台机器上的进程间通信,但在一些网络环境中,通过特定的协议或封装,有名管道的概念也可以被扩展到跨网络通信中。
- 需要持久性通信的场景:当需要在进程间保持长期的通信通道时,有名管道因其持久性而成为一个合适的选择。
- 匿名管道:
- 父子进程或兄弟进程间通信:由于匿名管道的限制,它最适合用于父子进程或兄弟进程间的简单数据交换。
- 临时通信需求:当进程间的通信需求是临时的,且通信双方具有亲缘关系时,匿名管道是一个高效且简便的选择。
匿名管道:针对父子进程
pipe:
Unix/Linux 系统中的 pipe 系统调用
在 Unix/Linux 系统中,pipe
函数是通过系统调用实现的,其原型定义在 <unistd.h>
头文件中。该函数用于创建一个管道,使得数据可以在一个进程(或进程组)的不同部分之间流动。
#include <unistd.h> | |
int pipe(int pipefd[2]); |
pipefd
是一个包含两个整数的数组,pipefd[0]
是管道的读端,pipefd[1]
是管道的写端。- 调用成功时,
pipe
返回 0;失败时,返回 -1 并设置 errno 以指示错误。
int pipe(int pipefd[2]):参数是一个长度为2的int数组数组名;
先pipe再fork:
文件对象在父子进程之间是共享的,
单功通信:
全双功通信:
#include <43func.h>
int main(){
int pipeFd1[2];
int pipeFd2[2];
pipe(pipeFd1);//父写,子读
pipe(pipeFd2);//父读,子写
if (fork() == 0)
{
close(pipeFd1[1]);//关闭父写1
close(pipeFd2[0]);//关闭父读2
write(pipeFd2[1],"msg from child",14);
char buf[20] = {0};
read(pipeFd1[0],buf,sizeof(buf));
puts(buf);
}else{
close(pipeFd1[0]);//
close(pipeFd2[1]);
char buf[20] = {0};
read(pipeFd2[0],buf,sizeof(buf));
puts(buf);
write(pipeFd1[1],"msg from parent!",16);
wait(NULL);
}
}
有名管道的函数:
mkfifo:创建有名管道
unlink:删除文件(删除文件是从文件系统取消挂载,磁盘中有可能还保存了该文件)
rename:改变位置类似mv;
link:创建一个新的硬链接;
共享内存:效率最高的进程间通信机制
让不同进程的虚拟内存页对应同一个物理页(页框);
虚拟内存映射:
让虚拟内存共享同一片物理内存:
P2根据P1的物理页来生成虚拟内存;
库文件经常使用共享
lsof:查看进程打开的文件信息
System V的共享内存机制:
Unix:
AT&T, System V版本,posix
伯克利大学,BSD版本;
ftok:生成key(key值将作为共享内存的地址)
shmget:创建共享内存段,在创建的时候把内存全部初始化,置零;
shmget
函数用于在 UNIX 和类 UNIX 系统(如 Linux)中创建或访问一个共享内存段。其原型通常如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
这里的参数详细解释如下:
key
(key_t key
):
- 这是一个键值,用于唯一标识一个共享内存段。如果你想要创建一个新的共享内存段,并且希望它在系统中是唯一的,你可以使用
IPC_PRIVATE
作为键值。这将导致shmget
创建一个新的、只对你当前进程可见的共享内存段。- 如果你想要访问一个已经存在的共享内存段,你需要知道该段的键值。这通常是通过某种形式的进程间通信(IPC)来传递的,比如通过命令行参数、环境变量或文件等。
size
(size_t size
):
- 这是共享内存段的大小,以字节为单位。你需要确保这个大小足够大,以存储你计划在共享内存段中存储的所有数据。如果指定的大小小于系统允许的最小大小,系统可能会自动将大小调整为最小大小。
shmflg
(int shmflg
):
- 这是一个标志位掩码,用于控制
shmget
的行为。它可以包含以下一个或多个标志:
IPC_CREAT
: 如果指定的共享内存段不存在,则创建一个新的共享内存段。如果共享内存段已经存在,并且没有指定IPC_EXCL
标志,则shmget
会返回该共享内存段的标识符而不会出错。IPC_EXCL
: 与IPC_CREAT
一起使用时,如果指定的共享内存段已经存在,则shmget
会失败并返回错误。这通常用于确保你正在创建一个新的、唯一的共享内存段。- 权限位(如
0600
、0666
等): 这些位指定了共享内存段的权限。它们与文件系统的权限类似,指定了谁可以读、写或执行该共享内存段。例如,0600
意味着所有者可以读写,但组用户和其他用户没有任何权限。因此,一个典型的
shmget
调用可能如下所示:
// 创建一个新的共享内存段,大小为 4096 字节,对所有者有读写权限
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
// 检查 shmget 是否成功
if (shmid == -1) {
// 错误处理
perror("shmget failed");
// ...
}
// 现在可以使用 shmid 来访问或修改共享内存段
如果你想要访问一个已经存在的共享内存段,你需要知道它的键值,并且不需要
IPC_CREAT
标志(除非你还想在共享内存段不存在时创建它)。如果你想要确保你正在访问的是唯一的共享内存段,并且它不存在于系统中,那么你可以同时使用IPC_CREAT
和IPC_EXCL
标志。如果共享内存段已经存在,这将导致shmget
调用失败。
size:页的大小;
shmfg:描述生成的属性;IPC_CREAT:
查看共享内存ipcs:
shmget创建的没有权限;
设置权限和固定地址;
shmat():share memary attach
return:
shmdt :回收虚拟内存;
ipcrm -M shmid:删除共享内存对象(共享内存段)
私有共享内存:
. IPC_PRIVATE的作用
- 创建新的IPC对象:当使用IPC_PRIVATE作为key值创建IPC对象时,系统会为该对象分配一个新的、唯一的标识符(ID),这个ID在系统中是唯一的,用于区分不同的IPC对象。
- 隔离性:由于IPC_PRIVATE创建的IPC对象具有唯一的ID,因此它提供了进程间通信的隔离性。只有知道该ID的进程才能访问该IPC对象,从而避免了不相关进程之间的干扰。
2. 使用场景
- 父子进程通信:在父子进程间通信的场景中,父进程可以使用IPC_PRIVATE创建IPC对象,并将该对象的ID传递给子进程。这样,父子进程就可以通过该IPC对象进行通信。
- 临时IPC对象:当需要创建一个临时的IPC对象,用于一次性通信或短时间的数据交换时,可以使用IPC_PRIVATE来创建。通信结束后,可以删除该IPC对象,以避免占用系统资源。
3. 注意事项
- ID的唯一性:虽然IPC_PRIVATE创建的IPC对象具有唯一的ID,但每次调用创建函数时都会生成一个新的ID。因此,如果需要在多个进程间共享同一个IPC对象,就不能使用IPC_PRIVATE来创建。
- 资源清理:使用IPC_PRIVATE创建的IPC对象在不再需要时应该被删除,以释放系统资源。这通常通过调用相应的删除函数(如shmctl()函数配合IPC_RMID命令)来完成。
只能由父子进程访问;
竞争条件:
两个并发的进程,访问同一个资源;
一个·进程在运行的过程中时间片用完,切到另一个进程;
进程的切换,导致寄存器和内存的值不同步,存在数据操作丢失的可能性;
#include<43func.h>
#define NUM 1000000
int main(){
int shmid = shmget(IPC_PRIVATE,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
int *p = (int *)shmat(shmid,NULL,0);
ERROR_CHECK(p,(void *)-1,"shmat");
p[0] = 0;
if (fork() == 0)
{
for (size_t i = 0; i < NUM; i++)
{
++p[0];
}
}else{
for (size_t i = 0; i < NUM; i++)
{
++p[0];
}
wait(NULL);
printf("p[0] = %d\n",p[0]);
}
shmdt(p);
}
由于父子进程几乎同时开始执行相同的操作(增加
p[0]
的值),且这两个操作之间没有同步机制(如互斥锁、信号量等),因此存在竞态条件。
竞态条件:在这种情况下,两个进程几乎同时读取
p[0]
的值(初始为0),然后各自在自己的上下文中增加这个值,并将结果写回p[0]
。但由于CPU调度的不确定性,可能父进程先读取了值(0),然后子进程也读取了值(仍然是0,因为父进程尚未更新),接着它们都各自加1并写回。这样,p[0]
的最终值可能低于预期,因为它只被“有效”增加了NUM
次(理想情况下应为2*NUM
次)。实际结果:由于竞态条件,
p[0]
的最终值将是一个不确定的值,但一定小于或等于2*NUM
(尽管在这个特定例子中,由于每次只有一个进程能实际增加p[0]
的值,它可能更接近NUM
,但这仍然是一个竞态条件)。