Linux-进程间通信-管道-有名管道-匿名管道(父子进程popen,pipe)-基于阻塞的同步-共享内存(system V(命令:)(函数:shmget)(shmat)(shmdt)(ipcrm))

进程间通信常用参数:

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通道的场景,如网络编程和文件操作。

异步模型详解

异步模型是一种高效的编程模式,它允许程序在执行某个任务时,不会阻塞当前线程或其他任务的执行。以下是四种常见的异步模型:

  1. 回调函数(Callback)
    • 特点:将函数作为参数传递给另一个函数,并在异步操作完成时调用该函数来处理结果。
    • 优点:提高程序的响应性和并发性能,灵活性和可扩展性高。
    • 缺点:可能导致回调地狱,难以处理异步操作之间的依赖关系和协同操作,可读性和可维护性较差。
  2. Promise/Future
    • 特点:Promise表示一个尚未完成但预期将来会完成的异步操作的结果。Future用于从另一个执行线程中获取结果。
    • 优点:提供了一种更清晰的链式调用方式,避免了回调地狱,易于理解和使用。
    • 缺点:在某些情况下可能会增加代码的复杂度。
  3. 异步IO
    • 特点:执行IO操作时,不会阻塞当前线程,而是由操作系统在IO操作完成时通知程序。
    • 优点:真正实现了非阻塞IO,提高了程序的并发性能和响应速度。
    • 缺点:对编程模型的要求较高,需要合理地设计和处理事件驱动的逻辑。
  4. 协程(Coroutine)
    • 特点:协程是一种用户态的轻量级线程,可以在单个线程中执行多个协程,协程之间的切换由用户控制。
    • 优点:相比线程具有更低的开销和更高的并发性能,适用于IO密集型任务。
    • 缺点:需要编程语言的支持,并且需要合理地设计协程的调度和同步机制。

1、管道:

有名管道(Named Pipe,也称作FIFO)和匿名管道(Anonymous Pipe)都是进程间通信(IPC)的重要机制,但它们在多个方面存在显著区别,这些区别决定了它们各自的使用场景。以下是两者的主要区别及使用场景:

主要区别

  1. 名称与持久性
    • 有名管道:具有唯一名称,以文件的形式存在于文件系统中,因此是持久的,除非显式删除,否则会一直存在。
    • 匿名管道:没有名称,仅存在于内存中,是临时的,随着创建它的进程的终止而消失。
  2. 通信范围
    • 有名管道:可以在任意两个进程之间使用,只要它们都知道管道的名称,并且可以访问该文件系统。
    • 匿名管道:仅限于具有亲缘关系的进程间使用,如父子进程或兄弟进程,因为它们共享相同的资源信息。
  3. 创建方式
    • 有名管道:通过mkfifo()系统调用或mkfifo命令创建。
    • 匿名管道:通过pipe()系统调用创建。
  4. 操作方式
    • 两者都使用read()write()等系统调用来进行数据的读写操作。

使用场景

  1. 有名管道
    • 不相关进程间通信:由于有名管道可以在任意两个进程间使用,因此它特别适用于那些没有直接亲缘关系的进程间的通信。
    • 跨网络通信:虽然传统的有名管道主要用于同一台机器上的进程间通信,但在一些网络环境中,通过特定的协议或封装,有名管道的概念也可以被扩展到跨网络通信中。
    • 需要持久性通信的场景:当需要在进程间保持长期的通信通道时,有名管道因其持久性而成为一个合适的选择。
  2. 匿名管道
    • 父子进程或兄弟进程间通信:由于匿名管道的限制,它最适合用于父子进程或兄弟进程间的简单数据交换。
    • 临时通信需求:当进程间的通信需求是临时的,且通信双方具有亲缘关系时,匿名管道是一个高效且简便的选择。

匿名管道:针对父子进程

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);

这里的参数详细解释如下:

  1. key (key_t key):
    • 这是一个键值,用于唯一标识一个共享内存段。如果你想要创建一个新的共享内存段,并且希望它在系统中是唯一的,你可以使用 IPC_PRIVATE 作为键值。这将导致 shmget 创建一个新的、只对你当前进程可见的共享内存段。
    • 如果你想要访问一个已经存在的共享内存段,你需要知道该段的键值。这通常是通过某种形式的进程间通信(IPC)来传递的,比如通过命令行参数、环境变量或文件等。
  2. size (size_t size):
    • 这是共享内存段的大小,以字节为单位。你需要确保这个大小足够大,以存储你计划在共享内存段中存储的所有数据。如果指定的大小小于系统允许的最小大小,系统可能会自动将大小调整为最小大小。
  3. shmflg (int shmflg):
    • 这是一个标志位掩码,用于控制 shmget 的行为。它可以包含以下一个或多个标志:
      • IPC_CREAT: 如果指定的共享内存段不存在,则创建一个新的共享内存段。如果共享内存段已经存在,并且没有指定 IPC_EXCL 标志,则 shmget 会返回该共享内存段的标识符而不会出错。
      • IPC_EXCL: 与 IPC_CREAT 一起使用时,如果指定的共享内存段已经存在,则 shmget 会失败并返回错误。这通常用于确保你正在创建一个新的、唯一的共享内存段。
      • 权限位(如 06000666 等): 这些位指定了共享内存段的权限。它们与文件系统的权限类似,指定了谁可以读、写或执行该共享内存段。例如,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,但这仍然是一个竞态条件)。

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
fork-popen和system都是用来创建进程函数。 fork函数是用于创建一个新的进程,新进程完全复制了父进程的资源。子进程可以通过返回值来区分自己是子进程还是父进程。fork函数相当于同时调用了fork、exec和waitpid这三个系统调用,会等待子进程执行完成后再继续执行。而popen函数则是通过管道来启动一个进程,并返回一个文件指针,可以用来读取或写入子进程的输入输出。popen函数不需要等待子进程执行完成就可以返回。 系统提供的system函数是用于执行shell命令的。它的原型是`int system(const char* command)`,可以通过传入shell命令作为参数来执行命令。system函数会一直等待shell命令执行完成(waitpid),然后返回。而popen函数则可以并行执行shell命令,不需要等待命令执行完成。 在使用popen函数后,需要调用pclose来对所创建的子进程进行回收,否则可能会导致僵尸进程的情况。 总结起来,fork-popen和system都是用来创建进程函数,但是fork-popen是并行执行的,而system是串行执行的。同时,在使用popen函数后,需要调用pclose进行子进程回收。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Linux---popen、system函数](https://blog.csdn.net/y6_xiamo/article/details/80156598)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [重新实现popen和system函数](https://download.csdn.net/download/u013105439/10441160)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值