进程间通信IPC,管道、共享内存、消息队列、信号量原理介绍

目录

前言知识点

System V IPC机制

POSIX标准

操作系统的原语

同步机制

互斥锁

读写锁

条件变量

信号量

原子性与互斥性

半双工通信机制

全双工通信机制

内存级文件

ftok()

介绍

用例:

为什么

是什么

管道

命令

mknod

mkfifo

函数

pipe()

mkfifo()

是什么

匿名管道

原理(匿名管道)

管道文件

管道文件的文件描述符

原理(不同进程看到同一份资源)

问题:

0.管道文件会在进程中创建页表项嘛?

1.管道也是文件,他有FCB嘛?

2.创建管道时候生成的管道数据结构是什么?

3.管道文件会在进程的页表中形成嘛,这个过程的细节?

4.两个文件描述符映射的是同一个struct file嘛?

5.进程怎么区分两个描述符的?他们中的struct file是相同的不是吗?

6.读写操作是怎么进行的呢?

7.如果读端正常读取,写段却提前关闭了

8.如果写端正常写入,读端却提前关闭了

9.环形缓存中 的一些问题:

匿名管道的创建和使用

特点(匿名管道)

实验 (匿名管道)

1.创建匿名管道

2.管道缓冲区为空读取,读取阻塞

3.管道缓冲区是有大小的,写入阻塞:

4.如果写端正常写入,读端却提前关闭了

命名管道

原理(命名管道)

FIFO命名管道文件:

原理(不同进程看到同一份资源)

问题

1.如果两个进程同时打开一个普通文件,对其进行写入会发生什么

2.管道文件的文件描述符中实现了特殊的锁机制,保证写入和读取不会冲突?

3.命名管道文件的缓冲区

4.命名管道也有缓冲区,他和匿名管道一样吗?

5.既然如此,他的读写的阻塞规则和匿名管道一样吗?

命名管道的创建和使用

命名管道的创建

命名管道的使用

命名管道的删除

注意事项

特点(命名管道)

实验(命名管道)

1.使用命名管道文件在两个进程间实现通信

2.读写的阻塞?细节。

//原因:?

共享内存

命令

ipcs -m 中的几个名词

函数

shmget()

函数原型:

参数:

返回值:

key参数:

shmflg参数:

使用例子:

shmat()

函数原型:

参数:

返回值:

使用例子:

shmdt()

函数原型:

shmaddr参数:

返回值:

使用例子:

shmctl()

函数原型:

参数:

返回值:

cmd参数:

buf参数:

使用例子:

是什么

原理(不同的进程看到同一份资源)

问题:

1.注意点

2.共享内存读写两个指针,是不是也是环形缓冲区啊?

3.共享内存访问好像没有锁机制啊,不会产生空读(读到空)的情况吗?

4.共享内存和管道的优势点结合起来不是更好吗?

特点

实验

1.基础实验实现共享内存

comm.hpp

pa.cpp

pb.cpp

输出::

注意:

2.共享内存和管道结合

发送进程的伪代码示例:

接收进程的伪代码示例:

3.获取共享内存数据块的属性shmctl()

可能的输出结果:

消息队列

命令

ipcs中的名词:

函数

msgget()

原型:

参数说明:

返回值:

注意:

msgctl()

原型:

参数说明:

返回值:

示例

msgsnd()

原型:

参数说明:

返回值:

注意:

示例:

msgrcv()

原型:

参数:

msgtyp参数:

msgflag参数:

返回值:

注意点:

另一种版本:

mq_ 系列函数的使用

mq_attr结构体

原理(不同进程看到同一份资源)

消息队列的数据结构

特点

实验:

1.使用msg系列函数实现消息队列

2.使用mg_系列函数实现消息队列

信号量(在多线程中重点讲)

命令

ipcs中的名词:

函数

semget()

原型:

参数:

semflg参数:

返回值:

注意点:

例子:

semctl()

原型:

cmd :

semnum:

例子:

semop()

原型:

参数:

返回值:

sembuf结构体:

例子:

注意点:

信号量的介绍

类型:

主要操作:

应用场景:

信号量与互斥锁、条件变量的关系:

特点:

原理

实验


本质!不同的进程看到同一份东西

前言知识点

System V IPC机制

System V IPC(Inter-Process Communication)机制是一种在Unix/Linux系统中实现进程间通信的机制。它提供了几种不同的通信方式,包括共享内存(Shared Memory)、消息队列(Message Queue)和信号量(Semaphore)等。

System V IPC机制的主要特点是:

  1. 核心组件:System V IPC机制的核心组件包括IPC对象(如共享内存、消息队列、信号量等)和IPC标识符(用于标识IPC对象的唯一数字)。

  2. 唯一性:每个IPC对象都有一个唯一的标识符(IPC标识符),用于在进程间标识和访问该对象。

  3. 统一接口:System V IPC提供了一组统一的函数接口,使进程可以创建、访问和操作IPC对象。

  4. 高效通信:System V IPC机制的实现是在内核中进行的,因此通信效率较高。

  5. 高灵活性:System V IPC提供多种不同的通信方式,可以根据应用程序的需求选择合适的方式。

使用System V IPC机制进行进程间通信可以实现进程之间的数据共享和同步。每个IPC对象都有自己的特点和适用场景,共享内存适用于高性能的数据共享,消息队列适用于异步通信,信号量适用于进程间的同步和互斥。

需要注意的是,由于System V IPC机制是一种较为低级的通信方式,使用时需要谨慎处理,避免资源泄漏和竞争等问题。另外,从Linux 2.6开始,也引入了更现代的IPC机制,如POSIX消息队列、共享内存和信号量,它们提供了更简洁和便于使用的API。

POSIX标准

POSIX(可移植操作系统接口)是一种针对不同操作系统定义的接口标准,旨在为应用开发人员和系统管理员提供一种可移植的编程接口。POSIX标准定义了一组通用的API,使开发人员可以在多个不同的操作系统平台上编写可移植的应用程序。这些API包括进程管理、文件操作、网络编程、内存管理等。

POSIX标准是由IEEE和ISO联合制定的,它基于UNIX操作系统,但并不局限于UNIX系统。许多其他操作系统也实现了POSIX标准,例如Linux、FreeBSD、OpenBSD、Windows NT、macOS等。

POSIX标准定义了许多函数库,这些库提供了各种功能,例如:

  • 进程管理:包括进程创建、进程控制、进程同步等。

  • 文件操作:包括打开、文件读写、文件管理等。

  • 网络编程:包括套接字编程、网络协议等。

  • 内存管理:包括内存分配、内存释放等。

POSIX标准还定义了一些特殊的API,这些API用于实现与特定系统相关的功能。例如,gethostname()sethostname()函数用于获取和设置主机名。

POSIX标准提供了许多优点,包括:

  • 提高应用程序的可移植性:由于POSIX标准在多个操作系统上实现,因此应用程序可以在这些操作系统上运行,而无需进行大量的修改。

  • 简化应用程序的开发:由于POSIX标准提供了一组通用的API,因此开发人员可以使用这些API在不同的操作系统上编写应用程序,而不必了解每个操作系统的特定细节

  • 提高系统的安全性:POSIX标准提供了一些安全功能,例如访问控制、内存保护等,这些功能有助于提高系统的安全性。

POSIX标准还定义了一些与进程间通信(IPC)相关的API,包括:

  • 消息队列(Message Queues):允许进程通过发送和接收消息进行通信。POSIX消息队列具有异步通信、数据一致性、灵活的消息大小等特点。

  • 信号量(Semaphores):允许进程之间同步操作,例如互斥锁和条件变量。信号量可以用于实现进程间的同步和互斥。

  • 共享内存(Shared Memory):允许进程之间访问同一块内存区域。共享内存可以提高进程间通信的效率,但需要确保内存访问的安全性和一致性。

除了这些IPC机制外,POSIX标准还定义了一些与网络编程相关的API,例如套接字编程、网络协议等。这些API使得进程可以进行跨网络通信,并支持各种网络协议,例如TCP/IP、UDP等。

总之,POSIX标准为开发人员和系统管理员提供了一组通用的编程接口,使他们可以在多个操作系统平台上编写可移植的应用程序,并实现进程间通信和网络编程等功能。POSIX标准已经成为许多操作系统的基础,并且被广泛应用于服务器、嵌入式系统和移动设备等领域。

操作系统的原语

操作系统的原语(Primitive)是一种基本的操作或指令,是操作系统提供给程序员和开发人员使用的最基本的功能接口。原语是一种具有原子性(不可分割)和互斥性(排他性)的操作,用于实现并发控制、同步和互斥操作等。

常见的操作系统原语包括:

  1. 原子操作:原子操作是一种不可分割的操作,要么全部完成,要么全部不完成。在多线程环境下,原子操作可以保证对共享资源的原子性访问,从而避免竞态条件(Race Condition)。

  2. 互斥锁(Mutex):互斥锁是一种用于实现互斥访问的机制,它可以确保同一时间只有一个线程能够访问共享资源。互斥锁可以防止多个线程同时对共享资源进行写操作,避免数据不一致的问题。

  3. 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制,它可以控制对临界资源的访问。信号量可以用于进程之间的通信,通过等待和发送信号量来进行进程的同步和互斥操作。

  4. 条件变量(Condition Variable):条件变量用于实现线程之间的条件同步,通常与互斥锁配合使用。线程可以通过条件变量在特定条件下等待,并在条件满足时被唤醒。

  5. 自旋锁(Spinlock):自旋锁是一种在等待期间一直占用CPU资源的锁,它不会导致线程进入阻塞状态。线程会一直尝试获取自旋锁,直到成功获取为止。

这些原语是操作系统提供的基本工具,用于支持多线程和多进程的并发控制和同步操作。通过正确使用这些原语,可以实现对共享资源的安全访问,避免竞争条件和提高系统的可靠性和性能。

同步机制

同步机制是一种用于控制多进程或多线程之间访问共享资源的技术,以避免数据竞争和不一致的问题。在多进程或多线程环境中,由于不同进程或线程之间操作的独立性,如果没有同步机制,可能会导致数据的不一致性和混乱。(这些同步机制我们在以后还会再提重点讲

常见的同步机制包括:

  1. 互斥锁(Mutex Lock):互斥锁用于保护共享资源,防止多个进程或线程同时访问资源,从而避免数据竞争和不一致的问题。

  2. 读写锁(Read-Write Lock):读写锁允许多个读线程并发访问共享资源但在写线程访问资源时,其他读写线程将被阻塞。这种方式可以提高读操作的并发性能,同时保证写操作的互斥访问。

  3. 条件变量(Condition Variable):条件变量允许一个线程在特定条件下挂起执行,等待其他线程发送信号表示条件满足时,被挂起的线程才会继续执行。这种方式可以实现多线程之间的协作和同步。

  4. 信号量(Semaphore):信号量是一种计数同步原语,用于控制多个进程或线程对共享资源的访问。信号量的值表示可用的资源数量,当一个进程或线程请求访问资源时,会检查信号量的值,如果值大于0,则允许访问资源,并减1信号量值;否则,线程将被阻塞,直到有其他线程释放资源并增加信号量值。

机制的使用可以有效地控制多进程或多线程之间的访问冲突,保证共享数据的一致性和正确性。但是,同步机制的使用也需要谨慎,因为不当的同步可能会导致性能问题和死锁等问题。因此,在使用同步机制时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的同步机制来实现。

互斥锁

互斥锁(Mutex Lock)是一种用于控制多进程或多线程之间访问共享资源的技术,可以避免数据竞争和不一致的问题。互斥锁的实现通常使用原子操作或信号量等机制,确保锁的原子性和互斥性

互斥锁的基本原理是,当一个进程或线程需要访问共享资源时,首先尝试获取互斥锁,如果锁已经被其他进程或线程占用,则当前进程或线程会被阻塞,直到锁被释放。当锁被释放时,阻塞的进程或线程会被唤醒,并重新尝试获取锁。如果当前没有其他进程或线程竞争锁,则当前进程或线程可以访问共享资源。

互斥锁可以有效地保护共享资源,防止多个进程或线程同时访问资源,从而避免数据竞争和不一致的问题。但是,互斥锁也会带来性能问题,因为多个进程或线程需要竞争锁,可能会导致严重的性能瓶颈。因此,在选择使用互斥锁时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的锁粒度和实现方式。

读写锁

读写锁(Read-Write Lock)是一种用于控制多进程或多线程之间访问共享资源的技术,可以提高读操作的并发性能,同时保证写操作的互斥访问。读写锁通常使用读写锁互斥量(Read-Write Locks Recursive Semaphore)或信号量等机制实现。

读写锁的基本原理是,当一个进程或线程需要读取共享资源时,尝试获取读锁,如果有其他进程或线程正在读取资源,则当前进程或线程会被阻塞,直到读锁被释放。如果当前没有其他进程或线程竞争读锁,则当前进程或线程可以读取共享资源。当一个进程或线程需要写入共享资源时,尝试获取写锁,如果有其他进程或线程正在写入资源,则当前进程或线程会被阻塞,直到写锁被释放。如果当前没有其他进程或线程竞争写锁,则当前进程或线程可以写入共享资源。

读写锁可以有效地提高读操作的并发性能,因为多个读进程或线程可以并发地读取共享资源,而不会互相阻塞。同时,读写锁也可以保证写操作的互斥访问,因为只有一个写进程或线程可以访问共享资源,从而避免了数据竞争和不一致的问题。但是,读写锁也需要仔细考虑共享资源的访问模式和并发需求,并选择合适的锁粒度和实现方式,以避免性能问题和死锁等问题。

条件变量

条件变量(Condition Variable)是一种用于控制多进程或多线程之间协作和同步的技术。它允许一个线程在特定条件下挂起执行,等待其他线程发送信号表示条件满足时,被挂起的线程才会继续执行。条件变量通常使用信号量或互斥锁等机制实现。

条件变量的基本原理是,当一个线程需要等待某个条件满足时,它会释放一个信号量或互斥锁,并阻塞在条件变量上。当条件满足时,另一个线程会发送信号表示条件已经满足,被挂起的线程会收到信号并继续执行。

条件变量可以用于实现多线程之间的协作和同步,例如,当一个线程需要等待某个条件满足时,可以使用条件变量来实现等待效果,从而避免了线程之间的死锁和资源竞争。但是,条件变量也需要仔细考虑使用场景和线程之间的协作关系,并选择合适的实现方式,以避免性能问题和死锁等问题。

信号量

信号量(Semaphore)是一种用于控制多进程或多线程之间访问共享资源的技术,可以实现进程之间的同步和互斥。信号量通常使用一个计数器来实现,可用的资源数量。当一个进程或线程需要访问共享资源时,会检查信号量的值,如果值大于0,则允许访问资源,并减1信号量值;否则,进程或线程将被阻塞,直到有其他进程或线程释放资源并增加信号量值。

信号量可以有效地控制多进程或多线程之间的访问冲突,保证共享数据的一致性和正确性。但是,信号量也需要仔细考虑共享资源的访问模式和并发需求,并选择合适的信号量值和实现方式,以避免性能问题和死锁等问题。

在Linux系统中信号量通常使用sem_t数据类型来表示,并提供了一系列的API函数,如sem_initsem_waitsem_post等,用于实现信号量的创建、等待和释放等操作。此外,信号量还可以与其他同步机制,如互斥锁和条件变量等一起使用,以实现更复杂的同步和互斥效果。

原子性与互斥性

原子性(atomicity)是指一个操作在执行过程中不会被中断或打断的特性。原子性操作通常被视为不可分割的基本操作,即整个操作要么完全执行,要么完全不执行。

在计算机科学中,原子性通常用于实现并发控制和同步机制,例如,在Linux系统中,mq_openmq_sendmq_receive等函数都具有原子性,以确保在多进程或多线程环境中,消息队列的操作不会被中断或打断。

原子性可以确保共享数据的一致性和正确性,避免多进程或多线程之间的数据竞争和不一致的问题。但是,原子性操作也可能会影响性能,因为原子操作通常需要使用硬件原子操作指令或软件实现方式,从而导致性能下降。因此,在选择使用原子性操作时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的实现方式和同步机制,以实现良好的性能和一致性。


互斥性(mutual exclusion)是指多个进程或线程在访问共享资源时,同一时刻只能有一个进程或线程访问该资源,以避免数据竞争和不一致的问题。互斥性通常使用互斥锁(mutex lock)或信号量(semaphore)等机制来实现。

互斥性可以确保共享数据的一致性和正确性,避免多个进程或线程同时访问共享资源而导致的数据不一致和不一致的问题。但是,互斥性也会影响性能,因为多个进程或线程需要等待互斥锁或信号量的释放,从而导致性能下降。因此,在选择使用互斥性时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的实现方式和同步机制,以实现良好的性能和一致性。

此外,互斥性通常需要与其他同步机制,如条件变量(condition variable)和读写锁(read-write lock)等一起使用,以实现更复杂的同步和互斥效果。在使用互斥性时,需要确保正确使用同步机制,以避免死锁和资源竞争等问题。

半双工通信机制

半双工通信允许数据在通信双方之间单向传输,但不能同时进行双向传输。这意味着在半双工通信中,通信的两个实体可以轮流发送和接收数据,但不能同时进行发送和接收操作。

在半双工通信中,数据的流动只能在一个方向上,而不能同时进行双向的数据传输。这是因为在通信系统中,数据传输需要使用共享的通信通道,如管道、电缆等。半双工通信机制通过在时间上分割发送和接收操作来实现单向的数据传输。

半双工通信通常用于一些场景,其中只有一个实体可以发送数据,而另一个实体则负责接收数据。典型的半双工通信场景包括:

  1. 对讲机:在对讲机中,用户通过按下按钮启动发送模式,然后放开按钮切换到接收模式,从而实现在不同时间点的发送和接收操作。

  2. 管道通信:管道是一种半双工通信的方式,其中数据只能沿一个方向进行传输。一个进程可以将数据写入管道,而另一个进程则可以从管道中读取数据。

  3. 消息队列:消息队列是一种半双工的通信方式,允许一个实体将消息放入队列,并由另一个实体按顺序接收这些消息。

在半双工通信中,需要注意的是发送方和接收方在时间上的协调和同步,以确保数据的完整和正确性。因为半双工通信只能单向传输,所以需要设计合适的机制来处理发送者和接收者之间的交互和协作。

全双工通信机制

全双工通信机制是一种允许双方同时进行双向通信的通信方式。在全双工通信中,发送方可以同时发送数据给接收方,同时接收方也可以发送数据给发送方,实现了双方之间的并行通信。

全双工通信可以在同一物理通道上进行,如一根双绞线、光纤或无线信道。发送方和接收方使用不同的频率、时间槽或编码方案来避免冲突,从而同时进行双向通信。

常见的全双工通信机制包括:

  1. 网络中的TCP套接字:在网络通信中,使用TCP协议的套接字可以实现全双工通信。一个套接字可以同时接收和发送数据,在连接建立后,双方可以同时进行双向的数据传输。

  2. 电话通信:在传统的电话通信中,通信双方可以同时听到对方的声音,实现了全双工通信。通过双方独立的语音信道,每个人可以同时说话和听对方说话。

  3. 双向无线电通信:无线电通信中的全双工通信可以通过不同的频段进行,允许发射器和接收器在不同的频率上同时发送和接收数据。

全双工通信提供了更高的通信容量和吞吐量,允许双方同时进行双向数据传输。这对于需要实时交互、高效数据传输和多方协作的应用非常重要。

内存级文件

内存级文件(Memory-mapped files)是一种将磁盘文件映射到进程的虚拟内存空间中的技术。通过内存映射文件,可以将文件的内容直接映射到内存中,并通过内存访问操作来读写文件。

内存映射文件的过程如下:

  1. 打开文件:首先需要打开待映射的文件,这通常使用操作系统提供的文件I/O函数来完成。

  2. 创建映射:进程通过调用操作系统提供的函数(如mmap())来创建一个映射,并将文件映射到进程的虚拟内存空间中。

  3. 访问文件:一旦映射创建成功,进程可以像访问内存一样访问文件。进程可以直接使用指针进行文件的读写操作,而无需使用繁琐的读写函数。

  4. 同步数据:进程对映射的文件进行读写操作时,对内存中的数据的更改也会反映到磁盘文件上。此外,进程还可以通过msync()函数将内存中的数据同步回磁盘,以保证数据的持久化。

内存映射文件的优点包括:

  • 性能:通过内存映射文件可以避免频繁的读写操作,提高文件的访问性能。

  • 简便性:使用内存映射文件可以直接在内存中操作文件,无需繁琐的读写函数。

  • 共享和协作:多个进程可以同时映射同一个文件,并共享文件的内容,方便实现进程间的协作和共享数据。

然而,需要注意以下一些问题:

  • 内存限制:内存映射文件会消耗系统的内存资源,因此需要注意文件大小和系统的内存限制。

  • 文件同步:对映射文件的更改需要进行适时的同步操作,以确保数据的持久性和一致性。

  • 安全性:对内存映射文件的读写操作需要谨慎,需要注意数据的正确处理和保护。

内存级文件是一种高效的文件访问方式,特别适用于大文件读写、共享和并发访问的场景。它在许多应用领域中得到了广泛的应用,如数据库系统、高性能计算和缓存系统等。

ftok()

介绍

在Linux中,ftok函数用于根据文件的路径名和项目标识生成一个键值(key),用于创建或访问共享内存、消息队列和信号量等IPC资源。

ftok函数的原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
​
key_t ftok(const char *pathname, int proj_id);

pathname参数是一个指向文件的路径名的字符串,用于确定文件的唯一性。通常情况下,可以使用一个已存在的文件作为路径名,如/tmp/file.txt

proj_id参数是一个项目标识符的整数值,用于区分不同的IPC资源。在不同的应用程序或进程间,使用不同的项目标识来保证生成的键值不同。

ftok函数将pathname的最后一个字符与proj_id相结合,并通过一定的算法生成一个32位的键值。该键值将被用于创建或访问共享内存、消息队列和信号量等IPC资源。

需要注意的是,proj_id的范围是0到255之间的整数,因此选择合适的项目标识符很重要,以避免重复的键值。

另外,使用ftok函数生成的键值,还可以通过IPC_PRIVATE与权限标志进行或运算,生成私有的键值,用于在同一个进程内部使用。

用例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
​
int main() {
    const char *pathname = "/tmp/file.txt";  // 文件路径名
    int proj_id = 1;  // 项目标识
​
    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok");
        return 1;
    }
​
    // 使用生成的键值进行共享内存、消息队列或信号量的操作
    // ...
​
    return 0;
}

为什么

  1. 资源共享:现代计算机系统中,多个进程通常需要访问相同的资源,如文件、数据库、网络连接等。进程间通信使得这些资源可以被有效地共享和管理,避免了每个进程都有自己独立副本的冗余。

  2. 分工合作:复杂的应用程序通常由多个进程组成,每个进程负责不同的任务。通过IPC(进程间通信),这些进程可以协调工作,交换数据,共同完成任务。例如,一个进程可能负责数据处理,而另一个进程负责显示结果。

  3. 模块化设计:在软件开发中,将系统划分为多个进程模块可以提高可维护性和可扩展性。每个模块可以独立运行,通过IPC与其他模块通信,这样修改一个模块不会影响到整个系统。

  4. 响应性和性能:在交互式系统中,如图形用户界面,需要快速响应用户操作。通过IPC,用户界面进程可以与后台处理进程通信,确保用户操作能够得到及时处理,同时避免了用户界面冻结的情况。

  5. 分布式系统:在分布式计算中,多个进程可能运行在不同的机器上。IPC技术如套接字和消息队列使得这些进程能够跨越网络进行通信,共同完成计算任务。

  6. 进程控制:IPC允许一个进程请求另一个进程的服务,或者通知另一个进程某个事件已经发生。这种控制流的管理对于复杂的系统来说是必不可少的。

  7. 错误处理和恢复:当一个进程发生错误或者需要停止时,通过IPC可以通知其他进程进行相应的错误处理或恢复操作。

是什么

进程间通信(Inter-process Communication,IPC)是计算机程序之间进行通信的方法和技术。它允许不同的进程在计算机系统中的不同地址空间进行交互。首先进程是具有独立性的,所以通信的本质是让不同的进程看到同一份资源。

当涉及到进程间通信时,以下是一些常见的进程间通信方法:

  1. 管道(Pipe):管道是一种单向的进程间通信机制,将一个进程的输出连接到另一个进程的输入。有两种类型的管道:匿名管道和命名管道(FIFO)(文件系统中的特殊文件)。

  • 管道是一种最基本的IPC机制,它允许一个进程将数据发送到另一个进程。

    • 管道可以是匿名管道(无需预先创建)或命名管道(需要在系统中预先创建)。

    • 管道是一种单向通信机制,数据只能从一个进程流向另一个进程。

  1. 共享内存(Shared Memory):共享内存允许多个进程共享同一块内存区域。通过在进程之间共享共享内存块,进程可以直接读写共享数据,从而实现高效的进程间通信。

    • 共享内存允许多个进程访问同一块内存区域,这意味着它们可以读写相同的内存地址。

    • 这是一种高速的IPC方式,因为数据不需要在客户端和服务器之间复制。

    • 共享内存需要同步机制(如互斥锁)来避免并发时的数据竞争。

  1. 消息队列(Message Queue):消息队列是一个消息的列表,可以在不同进程之间传递。每个消息都具有一个特定的类型,接收方可以选择接收相应类型的消息。

  • 消息队列允许进程以消息为单位进行通信,这些消息存储在队列中。

    • 发送进程将消息添加到队列中,而接收进程从队列中读取消息。

    • 消息队列提供了更复杂的数据结构和访问控制。

  1. .信号量(Semaphore):信号量是一种用于同步进程和控制进程访问共享资源的方法。它可以用来解决进程间的互斥和同步问题。

    • 信号量是一种用于同步的IPC机制,它可以用来控制对共享资源的访问。

    • 信号量可以是二进制的(只有0和1两个状态),也可以是计数信号量,允许一定数量的进程访问资源。

    • 信号量通常与互斥锁一起使用,以避免死锁和资源竞争。

//下边的都是模块知识 这个模块我们只讲前五点

  1. 信号(Signals)

  • 信号是一种简单的IPC机制,用于通知接收进程某个事件已经发生。

  • 发送进程通过发送信号来通知接收进程,而接收进程可以处理这个信号。

  • 信号处理可以是非阻塞的,也可以是阻塞的,取决于信号的性质。

  1. 套接字(Socket):套接字是一种在网络上实现进程间通信的方法。通过套接字,不同主机上的进程可以通过网络进行通信。

    • 套接字是一种通用的IPC机制,它支持网络通信,也支持同一台机器上的进程间通信。

    • 套接字使用TCP或UDP协议进行通信,提供了面向连接和无连接的通信方式。

    • 套接字允许进程通过网络进行交互,也可以用于本地进程间通信。

  2. 远程过程调用 RPC(Remote Procedure Call):RPC允许在网络上的不同计算机上的进程之间进行通信,使其感觉像是在本地执行过程调用一样。

管道

命令

在 Linux 中,管道相关的命令主要涉及创建、操作和管理管道文件。以下是一些常用的管道相关命令:

  1. mknod 命令:

    • 用于创建一个命名管道FIFO)或设备文件。

    • 例如,mknod mypipe p 创建一个命名管道文件 mypipe

  2. mkfifo 命令:

    • 用于创建一个命名管道文件。

    • 例如,mkfifo /path/to/myfifo 创建一个名为 myfifo 的命名管道文件。

  3. rmdir 命令:

    • 用于删除空的目录。

    • 例如,rmdir mypipe 删除一个名为 mypipe 的空目录。

  4. rm 命令:

    • 用于删除文件和目录。

    • 例如,rm -rf /path/to/myfifo 删除一个名为 myfifo 的命名管道文件。

  5. ln 命令:

    • 用于创建文件的链接。

    • 例如,ln -s /path/to/myfifo /path/to/alink 创建一个指向 myfifo 的符号链接。

  6. cat 命令:

    • 用于查看文件内容。

    • 例如,cat /path/to/myfifo 查看命名管道文件 myfifo 的内容。

  7. headtail 命令:

    • 用于查看文件的开头几行(head)或结尾几行(tail)。

    • 例如,head -n 10 /path/to/myfifo 查看 myfifo 的前 10 行。

  8. echo 命令:

    • 用于输出文本到标准输出。

    • 例如,echo "Hello, World!" > /path/to/myfifo 将文本写入命名管道文件。

  9. truncate 命令:

    • 用于截断文件,使其大小变为指定的字节数。

    • 例如,truncate -s 1024 /path/to/myfifomyfifo 的大小截断为 1024 字节。

  10. fcntl 命令:

    • 用于控制文件描述符。

    • 例如,fcntl(myfifo, F_GETFL) 获取 myfifo 的文件状态标志。

这些命令涵盖了命名管道的创建、查看、删除和操作的基本需求。

mknod

在 Linux 中,mknod 命令用于创建一个新的文件节点,可以用于创建设备文件,管道文件等。

mknod 的基本语法如下:

mknod [options] 文件名 类型 [主设备号 次设备号]
  • [主设备号 次设备号(major minor)]:这两个参数用于指定设备的主次设备号。对于字符设备(例如串行端口),主设备号通常为 100 以下的数字,对于块设备(例如硬盘),主设备号通常为 80 以上的数字。

  • 文件名:这是你想要创建的特殊文件的名称。

常见的选项包括:

  • -m:设置文件的权限模式。例如,mknod -m 660 /dev/mydevice c 234 0 表示创建一个名为 /dev/mydevice 的字符设备文件,并为其设置读写权限为 660。

  • -p:自动选择合适的权限模式和主次设备号。例如,mknod -p /tmp/mypipe p 表示创建一个名为 /tmp/mypipe 的命名管道文件,并自动选择权限模式和主次设备号。

  • -t--type:指定要创建的文件类型。例如mknod -t fifo /tmp/mypipe 将创建一个命名管道文件,mknod -t c /dev/mydevice c 10 1/dev/mydevice 设置为字符设备。

  • -s--socket:用于创建套接字文件。

  • -n:在创建文件时跳过已存在的文件。例如,mknod -n /dev/mydevice c 10 1 将不会创建已存在的 /dev/mydevice 文件。

  • -f--force:强制创建文件,即使文件已经存在。

mkfifo

在 Linux 中,mkfifo 命令用于创建一个命名管道(Named Pipe),也被称为 FIFO(First-In-First-Out)。命名管道是一种特殊的文件类型,它允许不相关的进程通过文件进行通信。

mkfifo 的基本语法如下:

mkfifo [options] 文件名
  • 文件名:指定要创建的命名管道的名称。

  • [options]:包含一些可选的标志,例如:

    • -m:文件的权限模式。

常用的选项包括:

  • -m:设置文件的权限模式。例如,mkfifo -m 660 mypipe 表示创建一个名为 mypipe 的命名管道,并为其设置读写权限为 660。

  • -p:为管道指定主设备号和次设备号。例如,mkfifo -p 80 90 mypipe 表示创建一个名为 mypipe 的命名管道,并为其指定主设备号为 80,次设备号为 90。

  • -m:设置文件的权限模式。例如,mkfifo - 660 mypipe 表示创建一个名为mypipe` 的命名管道,并为其设置读写权限为 660。

  • 有一些特殊的选项可用于特定的目的,如创建带区号的命名管道(-b)或限制命名管道的大小(-s)。

举个例子,如果你想要创建一个命名管道文件,你可以在命令行中输入:

mkfifo mypipe

这将在当前目录下创建一个名为 mypipe 的命名管道文件。

创建命名管道后,你可以在不同的进程之间使用该文件进行通信。一个进程可以将数据写入命名管道,而另一个进程可以从管道中读取这些数据。

需要注意的是,默认情况下,管道是阻塞的。这意味着当管道为空时,读取进程将被阻塞,直到有数据可供读取。同样,当管道已满时,写入进程将被阻塞,直到有空间可供写入。

使用命名管道时,你需要确保读取和写入进程以正确的顺序进行操作,以避免死锁或其他问题。

函数

  1. pipe() 函数:

    • int pipe(int pipefd[2]);

    • 用于创建一个匿名管道,并返回两个文件描述符,pipefd[0] 用于从管道读取数据,pipefd[1] 用于向管道写入数据。

  2. mkfifo() 函数:

    • int mkfifo(const char *pathname, mode_t mode);

    • 用于创建一个命名管道,指定路径名和权限模式。

  3. open() 函数:

    • int open(const char *pathname, int flags);

    • 在读写操作之前,使用此函数打开已存在的命名管道。

  4. read()write() 函数:

    • ssize_t read(int fd, void *buf, size_t count);

    • ssize_t write(int fd, const void *buf, size_t count);

    • 用于从管道读取数据和向管道写入数据。

  5. close() 函数:

    • int close(int fd);

    • 用于关闭管道的文件描述符。

pipe()

pipe函数是一个系统调用,用于创建一个管道(pipe)。它的原型如下:

#include <unistd.h>
int pipe(int pipefd[2]);

pipe函数接受一个整型数组pipefd作为参数,用于存储管道的两个文件描述符。

  • pipefd[0]用于从管道读取数据。通常情况下,它被关联到管道的读取端。

  • pipefd[1]用于向管道写入数据。通常情况下,它被关联到管道的写入端。

pipe函数成功创建管道时返回0,如果发生错误,则返回-1并设置errno错误代码。

下面是一个简单的示例,展示了如何使用pipe函数创建管道:

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

int main() {
    int pipefd[2];

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    printf("Read end of pipe: %d\n", pipefd[0]);
    printf("Write end of pipe: %d\n", pipefd[1]);

    return 0;
}

在上述示例中,我们调用pipe函数创建了一个管道,并将其文件描述符存储在pipefd数组中。然后,我们打印出管道的读取端和写入端的文件描述符。

注意,pipe函数创建的管道是双向的,即可以在两个方向上进行读写。但通常情况下,一个进程只使用其中一个方向来进行读写操作。另外,由于管道是内核中的一个缓冲区,当管道被读取完毕后,再次读取将会阻塞,直到有新的数据写入管道。

mkfifo()

mkfifo()函数是一个系统调用,用于根据指定的路径名创建一个FIFO(First-In-First-Out)文件,也称为命名管道文件。

mkfifo()函数的原型如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

参数说明:

  • pathname:要创建的FIFO文件的路径名。

  • mode:FIFO文件的权限模式。在创建FIFO文件时,它的权限默认是根据进程的umask值和文件创建者的权限来确定的。通常情况下,应该使用合适的mode参数对FIFO文件的权限进行显式设置。

函数返回值:

  • 如果成功创建FIFO文件,则返回0。

  • 如果发生错误,则返回-1,并设置相应的错误码。

mkfifo()函数的工作原理如下:

  1. mkfifo()函数尝试在文件系统中创建一个具有指定路径名的FIFO文件。

  2. 如果路径名已存在,并且不是一个FIFO文件,则创建失败,返回错误。

  3. 如果路径名已存在并且已经是一个FIFO文件,则创建成功,返回0。

  4. 如果路径名不存在,则根据指定的权限模式创建一个FIFO文件,并返回0。

一旦使用mkfifo()函数成功创建了一个FIFO文件,应用程序就可以使用该文件进行进程间通信,通过读写FIFO文件进行数据交换。需要注意的是,FIFO文件是一个特殊类型的文件,不同于普通的文本文件或二进制文件,要正确地使用FIFO文件进行通信,需要遵循FIFO文件的读写规则。

是什么

在进程间通信中,管道(Pipe)是一种常用的通信方式之一。管道是一种半双工通信机制,通常用于具有亲缘关系的进程之间进行通信,如父子进程。这种半双工是支持数据的反向流动,但是这种反向流动是通过创建另一个管道来实现的。

在操作系统中,管道可以分为两种类型:匿名管道和命名管道。

  1. 匿名管道:匿名管道是一种无名的管道,只能在具有亲缘关系的进程之间使用(如父子进程,兄弟进程)。它在创建时会自动分配一个文件描述符,并存储在进程表中。匿名管道是内存中的一个缓冲区,其大小是固定的。其中一个进程将数据写入管道,另一个进程则从管道中读取数据。(’|‘操作是一种匿名管道)

  2. 命名管道(FIFO):命名管道是一种有名字的管道,可以在任何两个进程之间进行通信。与匿名管道不同,命名管道是通过文件系统中的特殊文件进行通信。命名管道使用mkfifo命令或mkfifo()系统调用创建,它允许多个进程以类似于读写文件的方式进行管道通信。

管道在进程间通信中有许多应用场景。例如,在一个父进程和多个子进程之间,父进程可以使用管道向子进程发送命令或数据,子进程则可以通过管道将处理结果返回给父进程。另一个例子是在一个生产者-消费者模型中,生产者进程可以将数据写入管道,而消费者进程则从管道中读取数据进行处理。

函数:pipe() read() write() mkfifo() mknod()

命令:mknod mkfifo

匿名管道

原理(匿名管道)
管道文件

在操作系统中,管道文件通常指的是命名管道的文件表示。命名管道是一种特殊的文件,它允许不同进程通过文件系统中的这个文件进行通信。这种文件类型通常用于实现进程间的数据传输。

当一个进程向命名管道写入数据时,它实际上是向这个特殊文件写入数据。另一个进程可以从命名管道读取数据,它是通过读取这个特殊文件来获取数据的。因此,命名管道文件是管道通信机制在文件系统中的一个表现形式。

需要注意的是,管道文件本身并不是内存中的一个结构,而是文件系统中的一个实体。它提供了进程间通信的接口,但不同于普通的文件,它的目的是为了支持进程间的数据传输,而不是为了持久化存储数据,可以说管道文件类似一种内存级文件,但是绝对不等于,管道文件是操作系统在程序运行时候创建的,不是从磁盘上加载的,他在磁盘中没有实体

虽然管道本身没有对应的实体文件,但是它可以与其他文件系统实体(如命名管道或消息队列)进行组合,以实现更复杂的持久化通信机制。

管道文件的文件描述符

管道文件通过文件描述符来进行操作。文件描述符(fd)是一个非负整数,它是由系统调用分配给每个打开的文件(或管道)的。当一个进程打开一个管道时,它会获得两个文件描述符,一个用于读取(通常是0或标准输入),另一个用于写入(通常是1或标准输出)。

原理(不同进程看到同一份资源)

两者看到的是相同的文件描述符,也就是相同的内存缓冲区。

管道文件(Pipe file)是一种特殊的文件类型,它在文件系统中有对应的路径名,并且可以通过文件描述符来进行访问和操作。

pipe()函数会在进程中新增两个文件描述符,在实验一中我们可以明白。

当父进程创建管道时,操作系统会在内核中创建对应的管道数据结构并在文件系统中为其分配路径名,从而创建了管道文件。父进程获得的文件描述符可以用来访问和操作该管道文件。这个管道文件实际上是一个普通的文件,但对于读写管道的操作,它会通过特殊的文件系统操作来处理。

不会创建新的页表项。

创建子进程,子进程会获得这个文件描述符表的拷贝,通过开关父子进程的管道文件的读写接口,实现读取和写入。

问题:
0.管道文件会在进程中创建页表项嘛?

不会。管道的缓冲区在内核中,由操作系统内核维护,而不是在物理内存中。内核负责管理缓冲区的分配、释放和读写操作,以确保数据的安全传输和正确处理。进程中的文件描述符可以找到这片缓冲区,通过系统调用完成读写操作。

页表是进程模块和物理内存模块的联系。与内核无关。

1.管道也是文件,他有FCB嘛?

管道文件(Pipe)并没有对应的FCB(文件控制块)。管道是一种特殊的文件类型,用于实现进程间通信。它通过在内核中创建一个缓冲区,将一个进程的输出直接连接到另一个进程的输入,从而实现两个进程之间的数据传输。

管道文件属于进程通信模块,不属于文件系统。文件系统主要负责文件的存储、检索和管理,进程通信模块负责处理管道文件的创建、读写、关闭等操作。管道文件直接由操作系统管理,不需要FCB,不需要文件系统参与,操作系统会维护一些专门的数据结构来管理管道文件的读取和写入操作,如文件描述符、读写位置等。

与普通文件不同,匿名管道文件没有对应的磁盘上的存储空间,而是存在于内存中。因此,管道的创建和销毁都是在内核中进行的,并不需要对应的FCB。

值得注意的是,虽然管道文件没有FCB,但操作系统在内核中可能会为管道维护一些其他相关信息,例如读取和写入的位置等。但这些信息并不被称为FCB,而是保存在其他数据结构中(文件描述符表,文件表)

2.创建管道时候生成的管道数据结构是什么?

创建管道时会在内核中一个管道数据结构,它通常使用一个特殊的结构体 pipe_inode_info 来表示。(也就是struct_file)

pipe_inode_info 结构体包含了以下关键属性:

  1. 读取端(Reader End):记录了读取端相关的信息,如读取偏移量、等待读取的进程列表等。

  2. 写入端(Writer End):记录了写入端相关的信息,如写入偏移量、等待写入的进程列表等。

  3. 缓冲区(Buffer):用于存放从写入端到读取端传输的数据的中间缓冲区。通常是一个环形缓冲区,数据从写入端进入缓冲区,然后通过读取端被取出。

  4. 管道状态(Pipe Status):记录了管道的状态,如是否为阻塞模式、是否已关闭等。

这个管道数据结构是在内核中使用的,用户程序无法直接访问。用户程序通过使用管道的文件描述符,从而与管道数据结构进行交互和进行进程间通信。当一个进程向管道写入数据时,数据被写入到管道的缓冲区;而另一个进程从管道读取数据时,数据则从缓冲区读取。这个缓冲区一个特殊的环形缓冲区。

进程创建匿名管道,会把读写端口分开成两个文件描述符,实现读写分离。

3.管道文件会在进程的页表中形成嘛,这个过程的细节?

管道文件在父进程的页表中并没有直接的映射,因为它不是存储在进程的虚拟地址空间中的,而是位于内核地址空间。相反,通过文件描述符(file struct)和内核提供的系统调用(如read、write)、接口来进行对管道文件的访问和操作。

这与一般的文件不同,对于普通的文件,当进程打开文件时,操作系统会将文件映射到进程的虚拟地址空间中,以便进程可以通过虚拟地址来访问文件数据。这个过程涉及到页表(page table)的更新,。当一个进程向管道写入数据时,数据会被暂存在内核缓冲区中,然后从缓冲区传递给等待读取的进程。读写操作不是直接在虚拟地址空间中进行的,而是在内核缓冲区中进行的

4.两个文件描述符映射的是同一个struct file嘛?

文件描述符中有一个fd_type成员,指示了文件描述符的类型。

一个管道文件的两个文件描述符会映射到同一个 struct file 实例。

当一个进程通过调用 pipe() 系统调用创建一个管道时,会返回两个文件描述符,一个用于读取数据,一个用于写入数据。

这两个文件描述符共享同一个 struct file 数据结构,而这个数据结构表示整个管道。这意味着对于同一个管道,一个进程使用一个文件描述符写入数据到管道,另一个进程使用另一个文件描述符从管道读取数据。

5.进程怎么区分两个描述符的?他们中的struct file是相同的不是吗?

当一个进程通过 pipe 系统调用创建一个管道时,它会收到两个文件符,通常称为 pipefd[0]pipefd[1]。这两个文件描述符分别对应于管道的读端和写端。进程可以通过这两个文件描述符来区分管道的两个方向,进行相应的读写操作。

6.读写操作是怎么进行的呢?

管道映射的两个文件描述符分别对应着管道读端和写端,可以通过不同的偏移量(属于文件描述符的属性而非struct_file的属性)来区分。由于管道缓冲区是环形的,因此可以通过偏移量来区分读端和写端。写端偏移量从缓冲区起始地址开始,而读端偏移量从缓冲区末尾地址开始。

最开始,两个指针地址相差一个单位(不一定是一个字节),这时候可以开始写操作,写指针向前运动。写入完毕后,如果进行读操作,读指针会向前运动进行读取,直到读取到写指针的位置的前一位。这时候两个指针地址还是相差一个单位,准备接受新一轮的读写。形成了一个闭环。

7.如果读端正常读取,写段却提前关闭了
  1. 读取操作返回0:当写入端关闭时,读取操作将读取到缓冲区中的已存在的数据,并当没有更多数据可供读取时,返回0。这表示已经读取到了管道的末尾。

  2. 后续的读取操作将返回0:一旦读取端读取了管道中的所有数据,后续的读取操作将返回0,表示已经读取到了管道的末尾。

  3. 读取端的进程可以继续执行:读取端的进程可以继续执行其余的操作,而不会受到写入端的关闭影响。

8.如果写端正常写入,读端却提前关闭了

操作系统通过信号杀死这个写入的进程

  1. 读取端读取操作返回0:当读取端关闭时,读取操作将返回0,表示已到达文件结束或管道结束。写入端仍然可以继续写入数据,但没有读取端可以接收它们。

  2. 未读取的数据被丢弃:操作系统会丢弃写入端已经写入但尚未被读取端读取的数据。没有读取端时,写入端的数据将无法传递给任何进程使用,并且会被废弃。

  3. 管道文件描述符关闭:当读取端关闭时,操作系统将关闭管道的文件描述符。这意味着无法再使用这些文件描述符进行读取或写入操作。

  4. 管道的资源被释放:一旦读取端关闭,操作系统会释放与管道相关的资源,包括内存和其他相关的系统资源。

9.环形缓存中 的一些问题:

原本没有数据的读取会发生什么?</

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值