目录
前言知识点
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(Inter-Process Communication)机制是一种在Unix/Linux系统中实现进程间通信的机制。它提供了几种不同的通信方式,包括共享内存(Shared Memory)、消息队列(Message Queue)和信号量(Semaphore)等。
System V IPC机制的主要特点是:
核心组件:System V IPC机制的核心组件包括IPC对象(如共享内存、消息队列、信号量等)和IPC标识符(用于标识IPC对象的唯一数字)。
唯一性:每个IPC对象都有一个唯一的标识符(IPC标识符),用于在进程间标识和访问该对象。
统一接口:System V IPC提供了一组统一的函数接口,使进程可以创建、访问和操作IPC对象。
高效通信:System V IPC机制的实现是在内核中进行的,因此通信效率较高。
高灵活性:System V IPC提供多种不同的通信方式,可以根据应用程序的需求选择合适的方式。
使用System V IPC机制进行进程间通信可以实现进程之间的数据共享和同步。每个IPC对象都有自己的特点和适用场景,共享内存适用于高性能的数据共享,消息队列适用于异步通信,信号量适用于进程间的同步和互斥。
需要注意的是,由于System V IPC机制是一种较为低级的通信方式,使用时需要谨慎处理,避免资源泄漏和竞争等问题。另外,从Linux 2.6开始,也引入了更现代的IPC机制,如POSIX消息队列、共享内存和信号量,它们提供了更简洁和便于使用的API。
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)是一种基本的操作或指令,是操作系统提供给程序员和开发人员使用的最基本的功能接口。原语是一种具有原子性(不可分割)和互斥性(排他性)的操作,用于实现并发控制、同步和互斥操作等。
常见的操作系统原语包括:
原子操作:原子操作是一种不可分割的操作,要么全部完成,要么全部不完成。在多线程环境下,原子操作可以保证对共享资源的原子性访问,从而避免竞态条件(Race Condition)。
互斥锁(Mutex):互斥锁是一种用于实现互斥访问的机制,它可以确保同一时间只有一个线程能够访问共享资源。互斥锁可以防止多个线程同时对共享资源进行写操作,避免数据不一致的问题。
信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制,它可以控制对临界资源的访问。信号量可以用于进程之间的通信,通过等待和发送信号量来进行进程的同步和互斥操作。
条件变量(Condition Variable):条件变量用于实现线程之间的条件同步,通常与互斥锁配合使用。线程可以通过条件变量在特定条件下等待,并在条件满足时被唤醒。
自旋锁(Spinlock):自旋锁是一种在等待期间一直占用CPU资源的锁,它不会导致线程进入阻塞状态。线程会一直尝试获取自旋锁,直到成功获取为止。
这些原语是操作系统提供的基本工具,用于支持多线程和多进程的并发控制和同步操作。通过正确使用这些原语,可以实现对共享资源的安全访问,避免竞争条件和提高系统的可靠性和性能。
同步机制是一种用于控制多进程或多线程之间访问共享资源的技术,以避免数据竞争和不一致的问题。在多进程或多线程环境中,由于不同进程或线程之间操作的独立性,如果没有同步机制,可能会导致数据的不一致性和混乱。(这些同步机制我们在以后还会再提重点讲)
常见的同步机制包括:
互斥锁(Mutex Lock):互斥锁用于保护共享资源,防止多个进程或线程同时访问资源,从而避免数据竞争和不一致的问题。
读写锁(Read-Write Lock):读写锁允许多个读线程并发访问共享资源但在写线程访问资源时,其他读写线程将被阻塞。这种方式可以提高读操作的并发性能,同时保证写操作的互斥访问。
条件变量(Condition Variable):条件变量允许一个线程在特定条件下挂起执行,等待其他线程发送信号表示条件满足时,被挂起的线程才会继续执行。这种方式可以实现多线程之间的协作和同步。
信号量(Semaphore):信号量是一种计数同步原语,用于控制多个进程或线程对共享资源的访问。信号量的值表示可用的资源数量,当一个进程或线程请求访问资源时,会检查信号量的值,如果值大于0,则允许访问资源,并减1信号量值;否则,线程将被阻塞,直到有其他线程释放资源并增加信号量值。
机制的使用可以有效地控制多进程或多线程之间的访问冲突,保证共享数据的一致性和正确性。但是,同步机制的使用也需要谨慎,因为不当的同步可能会导致性能问题和死锁等问题。因此,在使用同步机制时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的同步机制来实现。
互斥锁(Mutex Lock)是一种用于控制多进程或多线程之间访问共享资源的技术,可以避免数据竞争和不一致的问题。互斥锁的实现通常使用原子操作或信号量等机制,确保锁的原子性和互斥性。
互斥锁的基本原理是,当一个进程或线程需要访问共享资源时,首先尝试获取互斥锁,如果锁已经被其他进程或线程占用,则当前进程或线程会被阻塞,直到锁被释放。当锁被释放时,阻塞的进程或线程会被唤醒,并重新尝试获取锁。如果当前没有其他进程或线程竞争锁,则当前进程或线程可以访问共享资源。
互斥锁可以有效地保护共享资源,防止多个进程或线程同时访问资源,从而避免数据竞争和不一致的问题。但是,互斥锁也会带来性能问题,因为多个进程或线程需要竞争锁,可能会导致严重的性能瓶颈。因此,在选择使用互斥锁时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的锁粒度和实现方式。
读写锁(Read-Write Lock)是一种用于控制多进程或多线程之间访问共享资源的技术,可以提高读操作的并发性能,同时保证写操作的互斥访问。读写锁通常使用读写锁互斥量(Read-Write Locks Recursive Semaphore)或信号量等机制实现。
读写锁的基本原理是,当一个进程或线程需要读取共享资源时,尝试获取读锁,如果有其他进程或线程正在读取资源,则当前进程或线程会被阻塞,直到读锁被释放。如果当前没有其他进程或线程竞争读锁,则当前进程或线程可以读取共享资源。当一个进程或线程需要写入共享资源时,尝试获取写锁,如果有其他进程或线程正在写入资源,则当前进程或线程会被阻塞,直到写锁被释放。如果当前没有其他进程或线程竞争写锁,则当前进程或线程可以写入共享资源。
读写锁可以有效地提高读操作的并发性能,因为多个读进程或线程可以并发地读取共享资源,而不会互相阻塞。同时,读写锁也可以保证写操作的互斥访问,因为只有一个写进程或线程可以访问共享资源,从而避免了数据竞争和不一致的问题。但是,读写锁也需要仔细考虑共享资源的访问模式和并发需求,并选择合适的锁粒度和实现方式,以避免性能问题和死锁等问题。
条件变量(Condition Variable)是一种用于控制多进程或多线程之间协作和同步的技术。它允许一个线程在特定条件下挂起执行,等待其他线程发送信号表示条件满足时,被挂起的线程才会继续执行。条件变量通常使用信号量或互斥锁等机制实现。
条件变量的基本原理是,当一个线程需要等待某个条件满足时,它会释放一个信号量或互斥锁,并阻塞在条件变量上。当条件满足时,另一个线程会发送信号表示条件已经满足,被挂起的线程会收到信号并继续执行。
条件变量可以用于实现多线程之间的协作和同步,例如,当一个线程需要等待某个条件满足时,可以使用条件变量来实现等待效果,从而避免了线程之间的死锁和资源竞争。但是,条件变量也需要仔细考虑使用场景和线程之间的协作关系,并选择合适的实现方式,以避免性能问题和死锁等问题。
信号量(Semaphore)是一种用于控制多进程或多线程之间访问共享资源的技术,可以实现进程之间的同步和互斥。信号量通常使用一个计数器来实现,可用的资源数量。当一个进程或线程需要访问共享资源时,会检查信号量的值,如果值大于0,则允许访问资源,并减1信号量值;否则,进程或线程将被阻塞,直到有其他进程或线程释放资源并增加信号量值。
信号量可以有效地控制多进程或多线程之间的访问冲突,保证共享数据的一致性和正确性。但是,信号量也需要仔细考虑共享资源的访问模式和并发需求,并选择合适的信号量值和实现方式,以避免性能问题和死锁等问题。
在Linux系统中信号量通常使用sem_t
数据类型来表示,并提供了一系列的API函数,如sem_init
、sem_wait
、sem_post
等,用于实现信号量的创建、等待和释放等操作。此外,信号量还可以与其他同步机制,如互斥锁和条件变量等一起使用,以实现更复杂的同步和互斥效果。
原子性(atomicity)是指一个操作在执行过程中不会被中断或打断的特性。原子性操作通常被视为不可分割的基本操作,即整个操作要么完全执行,要么完全不执行。
在计算机科学中,原子性通常用于实现并发控制和同步机制,例如,在Linux系统中,mq_open
、mq_send
和mq_receive
等函数都具有原子性,以确保在多进程或多线程环境中,消息队列的操作不会被中断或打断。
原子性可以确保共享数据的一致性和正确性,避免多进程或多线程之间的数据竞争和不一致的问题。但是,原子性操作也可能会影响性能,因为原子操作通常需要使用硬件原子操作指令或软件实现方式,从而导致性能下降。因此,在选择使用原子性操作时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的实现方式和同步机制,以实现良好的性能和一致性。
互斥性(mutual exclusion)是指多个进程或线程在访问共享资源时,同一时刻只能有一个进程或线程访问该资源,以避免数据竞争和不一致的问题。互斥性通常使用互斥锁(mutex lock)或信号量(semaphore)等机制来实现。
互斥性可以确保共享数据的一致性和正确性,避免多个进程或线程同时访问共享资源而导致的数据不一致和不一致的问题。但是,互斥性也会影响性能,因为多个进程或线程需要等待互斥锁或信号量的释放,从而导致性能下降。因此,在选择使用互斥性时,需要仔细考虑共享资源的访问模式和并发需求,并选择合适的实现方式和同步机制,以实现良好的性能和一致性。
此外,互斥性通常需要与其他同步机制,如条件变量(condition variable)和读写锁(read-write lock)等一起使用,以实现更复杂的同步和互斥效果。在使用互斥性时,需要确保正确使用同步机制,以避免死锁和资源竞争等问题。
半双工通信允许数据在通信双方之间单向传输,但不能同时进行双向传输。这意味着在半双工通信中,通信的两个实体可以轮流发送和接收数据,但不能同时进行发送和接收操作。
在半双工通信中,数据的流动只能在一个方向上,而不能同时进行双向的数据传输。这是因为在通信系统中,数据传输需要使用共享的通信通道,如管道、电缆等。半双工通信机制通过在时间上分割发送和接收操作来实现单向的数据传输。
半双工通信通常用于一些场景,其中只有一个实体可以发送数据,而另一个实体则负责接收数据。典型的半双工通信场景包括:
对讲机:在对讲机中,用户通过按下按钮启动发送模式,然后放开按钮切换到接收模式,从而实现在不同时间点的发送和接收操作。
管道通信:管道是一种半双工通信的方式,其中数据只能沿一个方向进行传输。一个进程可以将数据写入管道,而另一个进程则可以从管道中读取数据。
消息队列:消息队列是一种半双工的通信方式,允许一个实体将消息放入队列,并由另一个实体按顺序接收这些消息。
在半双工通信中,需要注意的是发送方和接收方在时间上的协调和同步,以确保数据的完整和正确性。因为半双工通信只能单向传输,所以需要设计合适的机制来处理发送者和接收者之间的交互和协作。
全双工通信机制是一种允许双方同时进行双向通信的通信方式。在全双工通信中,发送方可以同时发送数据给接收方,同时接收方也可以发送数据给发送方,实现了双方之间的并行通信。
全双工通信可以在同一物理通道上进行,如一根双绞线、光纤或无线信道。发送方和接收方使用不同的频率、时间槽或编码方案来避免冲突,从而同时进行双向通信。
常见的全双工通信机制包括:
网络中的TCP套接字:在网络通信中,使用TCP协议的套接字可以实现全双工通信。一个套接字可以同时接收和发送数据,在连接建立后,双方可以同时进行双向的数据传输。
电话通信:在传统的电话通信中,通信双方可以同时听到对方的声音,实现了全双工通信。通过双方独立的语音信道,每个人可以同时说话和听对方说话。
双向无线电通信:无线电通信中的全双工通信可以通过不同的频段进行,允许发射器和接收器在不同的频率上同时发送和接收数据。
全双工通信提供了更高的通信容量和吞吐量,允许双方同时进行双向数据传输。这对于需要实时交互、高效数据传输和多方协作的应用非常重要。
内存级文件(Memory-mapped files)是一种将磁盘文件映射到进程的虚拟内存空间中的技术。通过内存映射文件,可以将文件的内容直接映射到内存中,并通过内存访问操作来读写文件。
内存映射文件的过程如下:
打开文件:首先需要打开待映射的文件,这通常使用操作系统提供的文件I/O函数来完成。
创建映射:进程通过调用操作系统提供的函数(如mmap()
)来创建一个映射,并将文件映射到进程的虚拟内存空间中。
访问文件:一旦映射创建成功,进程可以像访问内存一样访问文件。进程可以直接使用指针进行文件的读写操作,而无需使用繁琐的读写函数。
同步数据:进程对映射的文件进行读写操作时,对内存中的数据的更改也会反映到磁盘文件上。此外,进程还可以通过msync()
函数将内存中的数据同步回磁盘,以保证数据的持久化。
内存映射文件的优点包括:
性能:通过内存映射文件可以避免频繁的读写操作,提高文件的访问性能。
简便性:使用内存映射文件可以直接在内存中操作文件,无需繁琐的读写函数。
共享和协作:多个进程可以同时映射同一个文件,并共享文件的内容,方便实现进程间的协作和共享数据。
然而,需要注意以下一些问题:
内存限制:内存映射文件会消耗系统的内存资源,因此需要注意文件大小和系统的内存限制。
文件同步:对映射文件的更改需要进行适时的同步操作,以确保数据的持久性和一致性。
安全性:对内存映射文件的读写操作需要谨慎,需要注意数据的正确处理和保护。
内存级文件是一种高效的文件访问方式,特别适用于大文件读写、共享和并发访问的场景。它在许多应用领域中得到了广泛的应用,如数据库系统、高性能计算和缓存系统等。
在Linux中,ftok
函数用于根据文件的路径名和项目标识生成一个键值(key),用于创建或访问共享内存、消息队列和信号量等IPC资源。
ftok
函数的原型如下:
#include#include 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
#include
#include
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;
}
资源共享:现代计算机系统中,多个进程通常需要访问相同的资源,如文件、数据库、网络连接等。进程间通信使得这些资源可以被有效地共享和管理,避免了每个进程都有自己独立副本的冗余。
分工合作:复杂的应用程序通常由多个进程组成,每个进程负责不同的任务。通过IPC(进程间通信),这些进程可以协调工作,交换数据,共同完成任务。例如,一个进程可能负责数据处理,而另一个进程负责显示结果。
模块化设计:在软件开发中,将系统划分为多个进程模块可以提高可维护性和可扩展性。每个模块可以独立运行,通过IPC与其他模块通信,这样修改一个模块不会影响到整个系统。
响应性和性能:在交互式系统中,如图形用户界面,需要快速响应用户操作。通过IPC,用户界面进程可以与后台处理进程通信,确保用户操作能够得到及时处理,同时避免了用户界面冻结的情况。
分布式系统:在分布式计算中,多个进程可能运行在不同的机器上。IPC技术如套接字和消息队列使得这些进程能够跨越网络进行通信,共同完成计算任务。
进程控制:IPC允许一个进程请求另一个进程的服务,或者通知另一个进程某个事件已经发生。这种控制流的管理对于复杂的系统来说是必不可少的。
错误处理和恢复:当一个进程发生错误或者需要停止时,通过IPC可以通知其他进程进行相应的错误处理或恢复操作。
进程间通信(Inter-process Communication,IPC)是计算机程序之间进行通信的方法和技术。它允许不同的进程在计算机系统中的不同地址空间进行交互。首先进程是具有独立性的,所以通信的本质是让不同的进程看到同一份资源。
当涉及到进程间通信时,以下是一些常见的进程间通信方法:
管道(Pipe):管道是一种单向的进程间通信机制,将一个进程的输出连接到另一个进程的输入。有两种类型的管道:匿名管道和命名管道(FIFO)(文件系统中的特殊文件)。
管道是一种最基本的IPC机制,它允许一个进程将数据发送到另一个进程。
管道可以是匿名管道(无需预先创建)或命名管道(需要在系统中预先创建)。
管道是一种单向通信机制,数据只能从一个进程流向另一个进程。
共享内存(Shared Memory):共享内存允许多个进程共享同一块内存区域。通过在进程之间共享共享内存块,进程可以直接读写共享数据,从而实现高效的进程间通信。
共享内存允许多个进程访问同一块内存区域,这意味着它们可以读写相同的内存地址。
这是一种高速的IPC方式,因为数据不需要在客户端和服务器之间复制。
共享内存需要同步机制(如互斥锁)来避免并发时的数据竞争。
消息队列(Message Queue):消息队列是一个消息的列表,可以在不同进程之间传递。每个消息都具有一个特定的类型,接收方可以选择接收相应类型的消息。
消息队列允许进程以消息为单位进行通信,这些消息存储在队列中。
发送进程将消息添加到队列中,而接收进程从队列中读取消息。
消息队列提供了更复杂的数据结构和访问控制。
.信号量(Semaphore):信号量是一种用于同步进程和控制进程访问共享资源的方法。它可以用来解决进程间的互斥和同步问题。
信号量是一种用于同步的IPC机制,它可以用来控制对共享资源的访问。
信号量可以是二进制的(只有0和1两个状态),也可以是计数信号量,允许一定数量的进程访问资源。
信号量通常与互斥锁一起使用,以避免死锁和资源竞争。
//下边的都是模块知识 这个模块我们只讲前五点
信号(Signals):
信号是一种简单的IPC机制,用于通知接收进程某个事件已经发生。
发送进程通过发送信号来通知接收进程,而接收进程可以处理这个信号。
信号处理可以是非阻塞的,也可以是阻塞的,取决于信号的性质。
套接字(Socket):套接字是一种在网络上实现进程间通信的方法。通过套接字,不同主机上的进程可以通过网络进行通信。
套接字是一种通用的IPC机制,它支持网络通信,也支持同一台机器上的进程间通信。
套接字使用TCP或UDP协议进行通信,提供了面向连接和无连接的通信方式。
套接字允许进程通过网络进行交互,也可以用于本地进程间通信。
远程过程调用 RPC(Remote Procedure Call):RPC允许在网络上的不同计算机上的进程之间进行通信,使其感觉像是在本地执行过程调用一样。
在 Linux 中,管道相关的命令主要涉及创建、操作和管理管道文件。以下是一些常用的管道相关命令:
mknod
命令:
用于创建一个命名管道FIFO)或设备文件。
例如,mknod mypipe p
创建一个命名管道文件 mypipe
。
mkfifo
命令:
用于创建一个命名管道文件。
例如,mkfifo /path/to/myfifo
创建一个名为 myfifo
的命名管道文件。
rmdir
命令:
用于删除空的目录。
例如,rmdir mypipe
删除一个名为 mypipe
的空目录。
rm
命令:
用于删除文件和目录。
例如,rm -rf /path/to/myfifo
删除一个名为 myfifo
的命名管道文件。
ln
命令:
用于创建文件的链接。
例如,ln -s /path/to/myfifo /path/to/alink
创建一个指向 myfifo
的符号链接。
cat
命令:
用于查看文件内容。
例如,cat /path/to/myfifo
查看命名管道文件 myfifo
的内容。
head
和 tail
命令:
用于查看文件的开头几行(head
)或结尾几行(tail
)。
例如,head -n 10 /path/to/myfifo
查看 myfifo
的前 10 行。
echo
命令:
用于输出文本到标准输出。
例如,echo "Hello, World!" > /path/to/myfifo
将文本写入命名管道文件。
truncate
命令:
用于截断文件,使其大小变为指定的字节数。
例如,truncate -s 1024 /path/to/myfifo
将 myfifo
的大小截断为 1024 字节。
fcntl
命令:
用于控制文件描述符。
例如,fcntl(myfifo, F_GETFL)
获取 myfifo
的文件状态标志。
这些命令涵盖了命名管道的创建、查看、删除和操作的基本需求。
在 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
:强制创建文件,即使文件已经存在。
在 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
的命名管道文件。
创建命名管道后,你可以在不同的进程之间使用该文件进行通信。一个进程可以将数据写入命名管道,而另一个进程可以从管道中读取这些数据。
需要注意的是,默认情况下,管道是阻塞的。这意味着当管道为空时,读取进程将被阻塞,直到有数据可供读取。同样,当管道已满时,写入进程将被阻塞,直到有空间可供写入。
使用命名管道时,你需要确保读取和写入进程以正确的顺序进行操作,以避免死锁或其他问题。
pipe()
函数:
int pipe(int pipefd[2]);
用于创建一个匿名管道,并返回两个文件描述符,pipefd[0]
用于从管道读取数据,pipefd[1]
用于向管道写入数据。
mkfifo()
函数:
int mkfifo(const char *pathname, mode_t mode);
用于创建一个命名管道,指定路径名和权限模式。
open()
函数:
int open(const char *pathname, int flags);
在读写操作之前,使用此函数打开已存在的命名管道。
read()
和 write()
函数:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
用于从管道读取数据和向管道写入数据。
close()
函数:
int close(int fd);
用于关闭管道的文件描述符。
pipe
函数是一个系统调用,用于创建一个管道(pipe)。它的原型如下:
#includeint pipe(int pipefd[2]);
pipe
函数接受一个整型数组pipefd
作为参数,用于存储管道的两个文件描述符。
pipefd[0]
用于从管道读取数据。通常情况下,它被关联到管道的读取端。
pipefd[1]
用于向管道写入数据。通常情况下,它被关联到管道的写入端。
pipe
函数成功创建管道时返回0,如果发生错误,则返回-1并设置errno错误代码。
下面是一个简单的示例,展示了如何使用pipe
函数创建管道:
#include
#include
#include
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()
函数是一个系统调用,用于根据指定的路径名创建一个FIFO(First-In-First-Out)文件,也称为命名管道文件。
mkfifo()
函数的原型如下:
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数说明:
pathname
:要创建的FIFO文件的路径名。
mode
:FIFO文件的权限模式。在创建FIFO文件时,它的权限默认是根据进程的umask
值和文件创建者的权限来确定的。通常情况下,应该使用合适的mode
参数对FIFO文件的权限进行显式设置。
函数返回值:
如果成功创建FIFO文件,则返回0。
如果发生错误,则返回-1,并设置相应的错误码。
mkfifo()
函数的工作原理如下:
mkfifo()
函数尝试在文件系统中创建一个具有指定路径名的FIFO文件。
如果路径名已存在,并且不是一个FIFO文件,则创建失败,返回错误。
如果路径名已存在并且已经是一个FIFO文件,则创建成功,返回0。
如果路径名不存在,则根据指定的权限模式创建一个FIFO文件,并返回0。
一旦使用mkfifo()
函数成功创建了一个FIFO文件,应用程序就可以使用该文件进行进程间通信,通过读写FIFO文件进行数据交换。需要注意的是,FIFO文件是一个特殊类型的文件,不同于普通的文本文件或二进制文件,要正确地使用FIFO文件进行通信,需要遵循FIFO文件的读写规则。
在进程间通信中,管道(Pipe)是一种常用的通信方式之一。管道是一种半双工通信机制,通常用于具有亲缘关系的进程之间进行通信,如父子进程。这种半双工是支持数据的反向流动,但是这种反向流动是通过创建另一个管道来实现的。
在操作系统中,管道可以分为两种类型:匿名管道和命名管道。
匿名管道:匿名管道是一种无名的管道,只能在具有亲缘关系的进程之间使用(如父子进程,兄弟进程)。它在创建时会自动分配一个文件描述符,并存储在进程表中。匿名管道是内存中的一个缓冲区,其大小是固定的。其中一个进程将数据写入管道,另一个进程则从管道中读取数据。(’|‘操作是一种匿名管道)
命名管道(FIFO):命名管道是一种有名字的管道,可以在任何两个进程之间进行通信。与匿名管道不同,命名管道是通过文件系统中的特殊文件进行通信。命名管道使用mkfifo
命令或mkfifo()
系统调用创建,它允许多个进程以类似于读写文件的方式进行管道通信。
管道在进程间通信中有许多应用场景。例如,在一个父进程和多个子进程之间,父进程可以使用管道向子进程发送命令或数据,子进程则可以通过管道将处理结果返回给父进程。另一个例子是在一个生产者-消费者模型中,生产者进程可以将数据写入管道,而消费者进程则从管道中读取数据进行处理。
函数:pipe() read() write() mkfifo() mknod()
命令:mknod mkfifo
在操作系统中,管道文件通常指的是命名管道的文件表示。命名管道是一种特殊的文件,它允许不同进程通过文件系统中的这个文件进行通信。这种文件类型通常用于实现进程间的数据传输。
当一个进程向命名管道写入数据时,它实际上是向这个特殊文件写入数据。另一个进程可以从命名管道读取数据,它是通过读取这个特殊文件来获取数据的。因此,命名管道文件是管道通信机制在文件系统中的一个表现形式。
需要注意的是,管道文件本身并不是内存中的一个结构,而是文件系统中的一个实体。它提供了进程间通信的接口,但不同于普通的文件,它的目的是为了支持进程间的数据传输,而不是为了持久化存储数据,可以说管道文件类似一种内存级文件,但是绝对不等于,管道文件是操作系统在程序运行时候创建的,不是从磁盘上加载的,他在磁盘中没有实体
虽然管道本身没有对应的实体文件,但是它可以与其他文件系统实体(如命名管道或消息队列)进行组合,以实现更复杂的持久化通信机制。
管道文件通过文件描述符来进行操作。文件描述符(fd)是一个非负整数,它是由系统调用分配给每个打开的文件(或管道)的。当一个进程打开一个管道时,它会获得两个文件描述符,一个用于读取(通常是0或标准输入),另一个用于写入(通常是1或标准输出)。
两者看到的是相同的文件描述符,也就是相同的内存缓冲区。
管道文件(Pipe file)是一种特殊的文件类型,它在文件系统中有对应的路径名,并且可以通过文件描述符来进行访问和操作。
pipe()函数会在进程中新增两个文件描述符,在实验一中我们可以明白。
当父进程创建管道时,操作系统会在内核中创建对应的管道数据结构,并在文件系统中为其分配路径名,从而创建了管道文件。父进程获得的文件描述符可以用来访问和操作该管道文件。这个管道文件实际上是一个普通的文件,但对于读写管道的操作,它会通过特殊的文件系统操作来处理。
不会创建新的页表项。
创建子进程,子进程会获得这个文件描述符表的拷贝,通过开关父子进程的管道文件的读写接口,实现读取和写入。
不会。管道的缓冲区在内核中,由操作系统内核维护,而不是在物理内存中。内核负责管理缓冲区的分配、释放和读写操作,以确保数据的安全传输和正确处理。进程中的文件描述符可以找到这片缓冲区,通过系统调用完成读写操作。
页表是进程模块和物理内存模块的联系。与内核无关。
管道文件(Pipe)并没有对应的FCB(文件控制块)。管道是一种特殊的文件类型,用于实现进程间通信。它通过在内核中创建一个缓冲区,将一个进程的输出直接连接到另一个进程的输入,从而实现两个进程之间的数据传输。
管道文件属于进程通信模块,不属于文件系统。文件系统主要负责文件的存储、检索和管理,进程通信模块负责处理管道文件的创建、读写、关闭等操作。管道文件直接由操作系统管理,不需要FCB,不需要文件系统参与,操作系统会维护一些专门的数据结构来管理管道文件的读取和写入操作,如文件描述符、读写位置等。
与普通文件不同,匿名管道文件没有对应的磁盘上的存储空间,而是存在于内存中。因此,管道的创建和销毁都是在内核中进行的,并不需要对应的FCB。
值得注意的是,虽然管道文件没有FCB,但操作系统在内核中可能会为管道维护一些其他相关信息,例如读取和写入的位置等。但这些信息并不被称为FCB,而是保存在其他数据结构中(文件描述符表,文件表)。
创建管道时会在内核中一个管道数据结构,它通常使用一个特殊的结构体 pipe_inode_info
来表示。(也就是struct_file)
pipe_inode_info
结构体包含了以下关键属性:
读取端(Reader End):记录了读取端相关的信息,如读取偏移量、等待读取的进程列表等。
写入端(Writer End):记录了写入端相关的信息,如写入偏移量、等待写入的进程列表等。
缓冲区(Buffer):用于存放从写入端到读取端传输的数据的中间缓冲区。通常是一个环形缓冲区,数据从写入端进入缓冲区,然后通过读取端被取出。
管道状态(Pipe Status):记录了管道的状态,如是否为阻塞模式、是否已关闭等。
这个管道数据结构是在内核中使用的,用户程序无法直接访问。用户程序通过使用管道的文件描述符,从而与管道数据结构进行交互和进行进程间通信。当一个进程向管道写入数据时,数据被写入到管道的缓冲区;而另一个进程从管道读取数据时,数据则从缓冲区读取。这个缓冲区一个特殊的环形缓冲区。
进程创建匿名管道,会把读写端口分开成两个文件描述符,实现读写分离。
管道文件在父进程的页表中并没有直接的映射,因为它不是存储在进程的虚拟地址空间中的,而是位于内核地址空间。相反,通过文件描述符(file struct)和内核提供的系统调用(如read、write)、接口来进行对管道文件的访问和操作。
这与一般的文件不同,对于普通的文件,当进程打开文件时,操作系统会将文件映射到进程的虚拟地址空间中,以便进程可以通过虚拟地址来访问文件数据。这个过程涉及到页表(page table)的更新,。当一个进程向管道写入数据时,数据会被暂存在内核缓冲区中,然后从缓冲区传递给等待读取的进程。读写操作不是直接在虚拟地址空间中进行的,而是在内核缓冲区中进行的。
文件描述符中有一个fd_type
成员,指示了文件描述符的类型。
一个管道文件的两个文件描述符会映射到同一个 struct file
实例。
当一个进程通过调用 pipe()
系统调用创建一个管道时,会返回两个文件描述符,一个用于读取数据,一个用于写入数据。
这两个文件描述符共享同一个 struct file
数据结构,而这个数据结构表示整个管道。这意味着对于同一个管道,一个进程使用一个文件描述符写入数据到管道,另一个进程使用另一个文件描述符从管道读取数据。
当一个进程通过 pipe
系统调用创建一个管道时,它会收到两个文件符,通常称为 pipefd[0]
和 pipefd[1]
。这两个文件描述符分别对应于管道的读端和写端。进程可以通过这两个文件描述符来区分管道的两个方向,进行相应的读写操作。
管道映射的两个文件描述符分别对应着管道读端和写端,可以通过不同的偏移量(属于文件描述符的属性而非struct_file的属性)来区分。由于管道缓冲区是环形的,因此可以通过偏移量来区分读端和写端。写端偏移量从缓冲区起始地址开始,而读端偏移量从缓冲区末尾地址开始。
最开始,两个指针地址相差一个单位(不一定是一个字节),这时候可以开始写操作,写指针向前运动。写入完毕后,如果进行读操作,读指针会向前运动进行读取,直到读取到写指针的位置的前一位。这时候两个指针地址还是相差一个单位,准备接受新一轮的读写。形成了一个闭环。
读取操作返回0:当写入端关闭时,读取操作将读取到缓冲区中的已存在的数据,并当没有更多数据可供读取时,返回0。这表示已经读取到了管道的末尾。
后续的读取操作将返回0:一旦读取端读取了管道中的所有数据,后续的读取操作将返回0,表示已经读取到了管道的末尾。
读取端的进程可以继续执行:读取端的进程可以继续执行其余的操作,而不会受到写入端的关闭影响。
操作系统通过信号杀死这个写入的进程
读取端读取操作返回0:当读取端关闭时,读取操作将返回0,表示已到达文件结束或管道结束。写入端仍然可以继续写入数据,但没有读取端可以接收它们。
未读取的数据被丢弃:操作系统会丢弃写入端已经写入但尚未被读取端读取的数据。没有读取端时,写入端的数据将无法传递给任何进程使用,并且会被废弃。
管道文件描述符关闭:当读取端关闭时,操作系统将关闭管道的文件描述符。这意味着无法再使用这些文件描述符进行读取或写入操作。
管道的资源被释放:一旦读取端关闭,操作系统会释放与管道相关的资源,包括内存和其他相关的系统资源。
原本没有数据的读取会发生什么?
原本没有数据的读取,读指针检测到下一位就是写指针,发生读取堵塞。直到写入了数据。
一部分数据的读取会发生什么?
一部分数据的读取,读指针向前运动正常读取,直到将缓存全部读取完毕(读取到写指针的位置的前一位)。读取到的数据放到用户定义的缓冲区域中。一般是数组buffer。
原本有数据的写入会发生什么?
如果管道中已经有数据,写指针会尝试将数据写入到缓冲区中。如果缓冲区已满,这时候会发生写入阻塞,直到有足够的空间可供写入。因此,如果没有读取操作,写入操作将无法继续,直到有其他进程准备好读取管道中的数据。
环形的缓存区可不可能发生覆盖问题?
对于匿名管道,它们通常具有固定大小的缓冲区,因此管道空间不会扩充。写满后会进入阻塞状态。
进程可以通过以下几种方式解决阻塞问题:
等待一段时间后再次尝试写入数据。在阻塞期间,进程可以等待一段时间,例如几秒钟,然后再次尝试写入数据。
使用其他机制来分批次写入数据。如果进程需要频繁地与管道进行通信,可以使用其他机制来分批次写入数据,例如将数据分为多个批次写入,或者使用循环写入的策略。
使用临时文件作为缓冲区。如果进程需要频繁地与管道进行通信,并且管道空间有限,可以考虑使用临时文件作为缓冲区。进程可以将数据写入临时文件中,然后在管道中有可用空间时再将其写入管道中。
有的操作系统会扩充
当写指针在写入时候转了一圈遇到了读指针(这里的缓存区是内核中的缓存区),这时候会重新分配一个更大的空间,发生拷贝,将两个指针放到合适的位置。(拷贝:两个指针在同一起点,运动其中一个指针遍历整个缓冲区,将整个缓冲区复制到更大的缓冲区,同时在新的缓冲区中建立两个指针变量,指向起始位置,其中一个指针随着数据的拷贝在运动)这个内核操作,称为“球仓算法”(ring buffer rotation)
在实验一中介绍
亲缘关系通信:匿名管道通常用于具有亲缘关系的进程之间,如父子进程。命名管道(也称为FIFO)可以用于没有亲缘关系的进程之间,但它们必须以某种方式知道对方的管道名。
数据流:管道是半双工的,管道是一种单向通信机制,数据只能从一个进程流向另一个进程。为了实现双向通信,可以创建两个管道,一个用于读,一个用于写。一旦确定方向就不能改变方向。
创建和删除:管道在创建时需要指定是创建匿名管道还是命名管道。匿名管道在进程终止时会自动删除,而管道则会一直存在,直到显式删除。
阻塞操作:进程可以在管道文件上进行阻塞操作。如果读取进程没有可用数据,它会阻塞直到有数据可读;如果写入进程的管道已满,它会阻塞直到有空间可写。
同步和互斥:管道是不安全的:如果多个进程同时读写管道,会导致数据竞争和不确定的结果。因此,在多进程场景中,需要适当的同步机制(如互斥锁、信号量)来保证数据的一致性和正确性。
限制:管道的大小有限(我的系统大小是1MB),因此传送大数据量时可能会受到限制。此外,管道通信不支持复杂的通信协议,如错误检查和流控制。可以使用操作系统提供的sysctl变量(例如/proc/sys/fs/pipe-max-size
)来查看或修改管道缓冲区的大小。
[root@MYCAT fs]# cat pipe-max-size 1048576
管道是基于字节流的,没有边界,因此需要适当的协议来解析和处理数据。
后边的几点在后续学习中会理解。
展示了如何在父子进程中使用管道文件描述符:
#include
#include
#include
#include
#include
int main() {
int pipefd[2];
//创建匿名管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 在子进程中写入数据到管道
close(pipefd[0]); // 关闭读取描述符
char message[] = "Hello, parent!";
write(pipefd[1], message, strlen(message));
close(pipefd[1]); // 关闭写入描述符
exit(EXIT_SUCCESS);
} else {
// 在父进程中读取管道中的数据
close(pipefd[1]); // 关闭写入描述符
char buffer[20];
read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[sizeof(buffer)-1] = '\0';
close(pipefd[0]); // 关闭读取描述符
printf("Message from child: %s\n", buffer);
int status;
waitpid(pid, &status, 0);
exit(EXIT_SUCCESS);
}
}
输出:
[lzh@MYCAT pipe]$ ./myexe Message from child: Hello, parent!
#include
#include
#include
#include
#include
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 在子进程中写入数据到管道
close(pipefd[0]); // 关闭读取描述符
int cnt =0;
//多次写入
while (cnt!=500000)
{
char message[] = "Hello, parent!";
write(pipefd[1], message, strlen(message));
sleep(10);
}
close(pipefd[1]); // 关闭写入描述符
exit(EXIT_SUCCESS);
} else {
// 在父进程中读取管道中的数据
int cnt =50;
close(pipefd[1]); // 关闭写入描述符
while(cnt--)
{
char buffer[1024];
read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[sizeof(buffer)-1] = '\0';
printf("Message from child: %s\n", buffer);
printf("%d\n",cnt);
}
close(pipefd[0]); // 关闭读取描述符
int status;
waitpid(pid, &status, 0);
exit(EXIT_SUCCESS);
}
}
输出:
[lzh@MYCAT pipe]$ ./myexe
Message from child: Hello, parent!
49
Message from child: Hello, parent!
48
Message from child: Hello, parent!
47
Message from child: Hello, parent!
46
Message from child: Hello, parent!
45
Message from child: Hello, parent!
44
Message from child: Hello, parent!
43
Message from child: Hello, parent!
42
Message from child: Hello, parent!
41
Message from child: Hello, parent!
40
Message from child: Hello, parent!
39
只要有了数据就会马上读取,没有数据发生读取阻塞。
#include
#include
#include
#include
#include
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 在子进程中写入数据到管道
close(pipefd[0]); // 关闭读取描述符
int cnt =0;
//多次写入
while (cnt!=500000)
{
cnt++;
char message[] = "Hello, parent!";
printf("%d\n",cnt);
write(pipefd[1], message, strlen(message));
}
close(pipefd[1]); // 关闭写入描述符
exit(EXIT_SUCCESS);
} else {
// 在父进程中读取管道中的数据
int cnt =50;
close(pipefd[1]); // 关闭写入描述符
while(cnt--)
{
char buffer[1024];
read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[sizeof(buffer)-1] = '\0';
printf("Message from child: %s\n", buffer);
sleep(10);
}
close(pipefd[0]); // 关闭读取描述符
int status;
waitpid(pid, &status, 0);
exit(EXIT_SUCCESS);
}
}
输出:
[lzh@MYCAT pipe]$ ./myexe 1 2 3 4 5 6 7 ........ ........ 46 47 Message from child: Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent! 48 49 ....... ....... 4702 4703 4704 ///在这里卡了将近十秒钟 Message from child: Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!H
#include
#include
#include
#include
#include
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 在子进程中写入数据到管道
close(pipefd[0]); // 关闭读取描述符
int cnt =0;
//多次写入
while (cnt!=500000)
{
char message[] = "Hello, parent!";
write(pipefd[1], message, strlen(message));
cnt++;
printf("child write: %d\n",cnt);
sleep(1);
}
close(pipefd[1]); // 关闭写入描述符
exit(EXIT_SUCCESS);
} else {
// 在父进程中读取管道中的数据
int cnt =5;
close(pipefd[1]); // 关闭写入描述符
while(cnt--)
{
char buffer[1024];
read(pipefd[0], buffer, sizeof(buffer) - 1);
buffer[sizeof(buffer)-1] = '\0';
printf("Message from child: %s\n", buffer);
sleep(1);
}
int status;
close(pipefd[0]); // 关闭读取描述符
printf("father close read,waite 5 seconds\n");
sleep(5);
pid_t ret = waitpid(pid,&status,0);
if(ret==pid){
printf("father kill child,exit code:%d, exit signal:%d\n"
,status&(0xff),status&(0x7f));}
exit(EXIT_SUCCESS);
}
}
输出:
[lzh@MYCAT pipe]$ ./myexe child write: 1 Message from child: Hello, parent! child write: 2 Message from child: Hello, parent! child write: 3 Message from child: Hello, parent! child write: 4 Message from child: Hello, parent! child write: 5 Message from child: Hello, parent! child write: 6 father close read,waite 5 seconds father kill child,exit code:13, exit signal:13 [lzh@MYCAT pipe]$ kill -l | grep "13)" 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
发现:
子进程是被信号杀死的,是操作系统杀死了它。
命名管道(Named Pipe)是一种特殊类型的管道,用于进程间通信。与匿名管道不同,命名管道在文件系统中有一个唯一的名称,适用于不共享亲缘关系的进程之间的通信。有了命名管道,进程可以通过共享一个指定的名称进行通信,而不需要直接使用网络套接字或其他复杂的通信机制。
命名管道在Unix和类Unix系统中作为文件存在于文件系统中,而在Windows中,它们作为对象存在于命名管道命名空间中。
命名管道的原理可以总结如下:
创建命名管道:首先,要创建一个命名管道,应用程序需要调用系统调用或API来创建一个具有唯一名称的FIFO文件(在Unix中)或命名管道对象(在Windows中)。)(路径具有唯一性)
打开管道:一旦创建了命名管道,应用程序就可以使用系统调用或API打开管道并获取一个文件描述符(在Unix中)或一个句柄(在Windows中)。
读/写数据:通过文件描述符或句柄,应用程序可以使用类似读取和写入文件的操作来从管道中读取数据或向管道中写入数据。读取和写入的操作可以是阻塞的或非阻塞的,取决于应用程序的设计。
进程间通信:通过共享管道的名称,不同的进程可以打开同一个命名管道,并通过读取和写入数据来进行进程间通信。数据会从一个进程的写入端流到另一个进程的读取端。
关闭管道:当应用程序完成对命名管道的使用时,应该调用适当的系统调用或API来关闭文件描述符或句柄,以释放相关资源。同时,对于FIFO文件,还应该删除该文件。
唯一名称:FIFO命名管道文件在文件系统中有一个唯一的名称。它可以通过文件路径来标识,通常以一个特定的文件名出现在文件系统的某个位置。
半双工通信:FIFO文件是一种半双工通信方式,只允许单向数据流。因此,要实现双向通信,需要创建两个FIFO文件,每个文件在不同的方向上进行数据传输。
阻塞与非阻塞操作:在对FIFO文件进行读取和写入操作时,可以选择是阻塞操作还是非阻塞操作。阻塞操作会使进程在没有数据可读或没有空间可写入时暂停等待,而非阻塞操作会立即返回,并返回适当的错误码。
读写规则:FIFO文件的读写操作有一些特定的规则。当一个进程打开一个FIFO文件进行读取时,它会等待另一个进程打开同一FIFO文件进行写入。当一个进程写入FIFO文件时,它会等待另一个进程打开同一FIFO文件进行读取。这确保了在读取和写入之间的同步。
两者看到的是相同的文件,不同进程打开同一个文件,操作系统只会打开同一个文件。它们所获得的文件描述符指向的是同一个文件。
在匿名管道中,磁盘中不会生成文件,匿名管道文件只会在内存中存在,随着进程的销毁而销毁,没有FCB,没有inode,在文件描述符中指示了他的类型是匿名管道,将他的读写端口分成两个fd。
同样的,在命名管道中,操作系统会在磁盘上创建这个特殊的文件(为了实现持久化),但是不会创建FCB(不涉及文件管理,这个文件由操作系统管理),会创建出文件描述符,然后直接通过文件描述符(通过特殊读写机制(锁)实现了他的读写端口)进行读写操作(在内核缓冲区中)。直接使用read write等系统调用接口来管理。
注意:这里创建或者打开文件,但是不会创建页表项,这点和匿名管道是相同的。因为命名管道的数据传输是在内核缓冲区中进行的,并不涉及虚拟地址空间和内存的映射。
当两个进程同时打开一个普通文件并对其进行写入操作时,会发生以下情况之一:
写冲突:如果两个进程同时试图向文件中的相同位置写入数据,那么可能会发生数据的覆盖和混乱。结果取决于操作系统和文件系统的实现方式,可能会导致数据的丢失或不一致。
同步写入:某些操作系统和文件系统可能会提供同步写入的机制,确保多个进程按顺序写入数据。这意味着第一个进程完成写入操作后,第二个进程才能开始写入。通过这种机制,可以避免数据的覆盖和混乱,但会导致效率低下,因为进程需要等待其他进程完成写入操作。
并发写入:某些操作系统和文件系统允许多个进程同时写入文件,但在内部会使用锁机制来处理冲突。锁机制确保每个进程只能同时访问文件的某个部分,从而避免数据的冲突和损坏。这种并发写入可能会提高效率,但需要操作系统和文件系统提供相应的支持。
综上所述,对于普通文件的并发写入,具体的行为取决于操作系统和文件系统的实现方式。在多个进程同时写入同一个文件时,可能会发生数据冲突、同步写入或并发写入等情况。为避免数据冲突,可以使用锁机制或其他同步机制来协调进程间的写入操作。
管道文件的文件描述符中通常会实现一种特殊的锁机制,以避免写入和读取的冲突。这种机制通常被称为互斥锁或文件锁,它允许一个进程在写入管道文件时阻止其他进程同时读取或写入该文件,从而确保数据的一致性和完整性。
当一个进程向管道写入数据时,它会将数据写入到管道文件的写入端,并将数据保留在文件的缓冲区中。此时,如果另一个进程尝试从管道文件的读取端读取数据,它会阻塞,直到写入进程释放了对管道文件的锁定。
这样,多个进程可以同时打开同一个管道文件进行读写操作,但是它们之间的访问是互斥的,即一个进程在写入数据时,其他进程必须等待直到写入进程释放了对管道文件的锁定。这种机制可以确保数据在多个进程之间的正确传输和同步,避免数据冲突和竞争条件的发生。
命名管道的通信机制在底层是通过操作系统的文件系统来实现的。当一个进程向命名管道写入数据时,数据首先被写入到操作系统的内核缓冲区(而不是内存缓冲区中)中,然后根据管道的状态和另一个进程的读取请求,数据会被从内核缓冲区传输到另一个进程的缓冲区中。注意不会再磁盘中写,因为没有FCB。
读取进程的工作方式类似,它通过文件描述符向操作系统请求数据。操作系统从管道的读取端(即另一个进程写入数据的端口)读取数据,并将其放入内核缓冲区。然后,数据会被传输到读取进程的缓冲区中,供进程使用。
这个过程是同步的,也就是说,当一个进程正在写入数据时,其他进程必须等待,直到写入操作完成(通过锁机制实现)。同样,当一个进程正在读取数据时,其他进程必须等待,直到读取操作完成。这种同步机制确保了数据的完整性和一致性。
是的,命名管道也有自己的缓冲区,也是环形的。
是的,特点与她一样,你可以看看匿名管道中的“问题模块”。
我们以在命令行中创建文件为例:
可以使用 mknod
命令或者mkfifo
命令创建一个命名管道。例如:
mknod mypipe p 或者 mkfifo mypipe
这将在当前目录下创建一个名为 mypipe
的命名管道。
要使用命名管道,必须在管道两端的进程之间进行适当的安排。以下是一个简单的使用示例:
# 假设我们有两个进程,一个生产者(producer)和一个消费者(consumer) # 生产者进程 echo "Hello, World!" > mypipe # 消费者进程 cat mypipe
在这段代码中,生产者进程将文本 Hello, World!
写入命名管道 mypipe
,而消费者进程从同一管道中读取该文本。
当不再需要命名管道时,可以使用 unlink
命令删除它:
unlink mypipe
命名管道的路径必须是绝对路径,否则可能会导致进程无法找到对方。
命名管道的使用受限于进程之间的文件系统访问权限。
如果在使用命名管道的过程中,任何一方进程终止,另一方可能会遇到问题,除非有适当的错误处理机制。
持久性:命名管道在创建后将持续存在,直到进程使用 unlink
删除它。
跨进程通信:命名管道允许不同进程间的通信,只要它们能够访问同一个文件系统路径。
同步机制:命名管道提供了基本的同步机制,确保数据在管道中的正确传递。
文本和二进制数据:命名管道可以传输文本数据和二进制数据。
缓冲区大小固定:命名管道中的缓冲区大小是固定的,无法动态调整。这意味着管道中的数据量是有限的。如果数据量超过了缓冲区大小,数据将会丢失或被丢弃。
[lzh@MYCAT pipe]$ cat named\ pipe.cpp
#include
#include
#include
#include
#include
#include
int main() {
const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
// 创建命名管道文件
mkfifo(fifoFile, 0666);
int fd = open(fifoFile, O_WRONLY); // 打开命名管道文件
// 写入数据到命名管道文件
const char* data = "Hello, named pipe!";
write(fd, data, strlen(data) + 1);
return 0;
}
[lzh@MYCAT pipe]$ cat named\ pipe1.cpp
#include
#include
#include
#include
#include
#include
int main() {
const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
int fd = open(fifoFile, O_RDONLY); // 打开命名管道文件
char buffer[25];
// 从命名管道文件中读取数据
read(fd, buffer, sizeof(buffer));
std::cout << buffer << std::endl;
close(fd); // 关闭命名管道文件
// 删除命名管道文件
// unlink(fifoFile);
return 0;
}
[lzh@MYCAT pipe]$ g++ named\ pipe.cpp -o myexewrite
[lzh@MYCAT pipe]$ g++ named\ pipe1.cpp -o myexeread
//接下来是分开的终端窗口
window1
[lzh@MYCAT pipe]$ ./myexewrite
//发现页面卡住,发生阻塞。
window2
[lzh@MYCAT pipe]$ ./myexeread
//此时window1突然输出语句
[lzh@MYCAT pipe]$ ./myexewrite
Read from named pipe: Hello, named pipe!
我们首先定义了一个命名管道文件的名称("myfifo")。然后使用mkfifo
函数创建命名管道文件。接下来,使用open
函数打开命名管道文件,并获得文件描述符。我们将一段数据写入命名管道文件,然后使用read
函数从命名管道文件中读取数据到缓冲区。最后,通过close
函数关闭文件描述符,并使用unlink
函数删除命名管道文件。
我们可以创建管道文件的类,在析构函数中Unlink掉它,这样可以实现更简便的操作(直接在进程中创建一个类就行,不用管理自动析构)。
mywrite.cpp
#include
#include
#include
#include
#include
#include
int main() {
const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
// 创建命名管道文件
// mkfifo(fifoFile, 0666);
int fd = open(fifoFile, O_WRONLY); // 打开命名管道文件
std::cout<< "write open file"<
myread.cpp
#include
#include
#include
#include
#include
#include
int main()
{
const char *fifoFile = "myfifo"; // 定义命名管道文件的名称
int fd = open(fifoFile, O_RDONLY); // 打开命名管道文件
std::cout<< "read open file"<
以下按照时间顺序进行
窗口一(read) | 窗口二(write) |
---|---|
[lzh@MYCAT pipe]$ ./myread read open file | |
(阻塞) | [lzh@MYCAT pipe]$ ./mywrite write open file |
Hello, named pipe! | |
[lzh@MYCAT pipe]$ ./mywritewrite open file | |
[lzh@MYCAT pipe]$ ./mywritewrite open file | |
[lzh@MYCAT pipe]$ ./mywritewrite open file | |
[lzh@MYCAT pipe]$ ./myread read open file Hello, named pipe! | |
[lzh@MYCAT pipe]$ ./myread read open file named pipe! | |
[lzh@MYCAT pipe]$ ./myread read open file pipe! |
[lzh@MYCAT pipe]$ ./myread read open file | |
(阻塞) | [lzh@MYCAT pipe]$ ./mywrite write open file |
Hello, named pipe! | |
这是因为你的read中的缓冲区大小问题,’Hello, named pipe!‘ 的长度是19,你每次读取25个字节,向缓冲区写了3遍’Hello, named pipe!‘ .
但是:write默认情况下不是覆盖写文件嘛?这里就体现出了文件的特殊性质,这里的写是向文件内核缓冲区写的,指针一直在前进。
read指针没有运动,所以在读取 时候,每次读取25个字符。第一次读到buffer中的其实是
’Hello, named pipe!\0Hello,'
第二次读的是
' named pipe!\0Hello, named'
第三次
‘ pipe!\0’
所以第二次第三次前都有一个空格。
在Linux中,共享内存相关的命令包括:
ipcs:用于列出系统当前的共享内存、消息队列及信号量的状态。
ipcrm:用于删除系统中的共享内存、消息队列及信号量。
shmget:用于创建共享内存段。
shmat:将共享内存段连接到当前进程的地址空间。
shmdt:断开当前进程与共享内存段的连接。
shmctl:用于对共享内存段进行控制操作,如获取、设置或删除共享内存段的权限和属性。
ipcs -m:**与ipcs命令一起使用,仅列出共享内存的状态信息。
ipcrm -m:删除指定的共享内存,注意-m后要加shmid而不是key。
这些命令可以帮助创建、连接、控制和删除共享内存段,以及获取共享内存的状态信息。
[root@MYCAT ~]# ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 2 gdm 777 16384 1 dest 0x00000000 5 gdm 777 14417920 2 dest
这些值的含义?
key
:共享内存的键值,用于唯一标识共享内存段。
shmid
:共享内存的标识符,由shmget
函数返回。它是共享内存段的一个唯一标识。
owner
:共享内存段的所有者,即创建共享内存段的进程的用户ID。
perms
:共享内存段的权限,用八进制表示。它定义了共享内存段的访问权限,包括读、写和执行的权限。
bytes
:共享内存段的大小,以字节为单位。
nattch
:当前挂接(或连接)到共享内存段的进程数。
status
:共享内存段的状态信息,包括一些附加的标志和元数据。
这些术语用于描述和了解共享内存段的基本信息,可以通过使用shmctl
函数中的IPC_STAT
命令来获取这些信息。使用shmctl
函数的IPC_STAT
命令,将共享内存的状态信息填充到一个struct shmid_ds
结构体中,然后可以通过访问结构体成员来获取这些信息。
当涉及到共享内存的操作,还有一些其他的函数可用:
shmget():用于获取一个共享内存段的标识符。如果共享内存段不存在,则创建一个新的共享内存段。
shmat():将指定的共享内存段连接到当前进程的地址空间中,并返回该共享内存段的内存地址。
shmdt():将指定的共享内存段与当前进程断开连接。
shmctl():用于对共享内存段进行控制操作,如获取、修改或删除共享内存段。
semget():用于获取一个信号量的标识符。如果信号量不存在,则创建一个新的信号量。
semop():对指定的信号量进行操作,如锁定、解锁或修改信号量的值。
semctl():用于对信号量进行控制操作,如获取、修改或删除信号量。
mmap():用于将文件或设备映射到进程的地址空间中,包括共享内存段。
shm_open():用于打开或创建一个共享内存对象。
shm_map():将共享内存段的内存映射到当前进程的地址空间中。
shm_unlink():用于删除指定的共享内存对象。
这些函数一起提供了在Linux中进行共享内存操作所需的完整功能。
在Linux中,shmget
函数是用于创建或打开共享内存段的系统调用函数。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
key
:共享内存的键值,是一个整数值,用于标识共享内存段。可以使用ftok函数生成键值,也可以使用事先约定的键值。
size
:需要创建的共享内存段的大小,以字节为单位。
shmflg
:用于指定共享内存段的权限和状态标志,可以使用IPC_CREAT
与权限值的或操作,表示创建共享内存段并设置权限。还可以使用其他标志,如IPC_EXCL
表示如果已存在相同的键值则创建失败,IPC_NOWAIT
表示以非阻塞方式创建。
shmget
函数的返回值为共享内存段的标识符(shmid),如果创建或打开失败,则返回-1,并设置相应的错误码。
在Linux中的shmget
函数中,key
参数是共享内存的键值。它是一个整数值,用于唯一标识共享内存段。多个进程可以使用相同的键值来访问同一个共享内存段。有若干方法:
1.ftok函数
使用不同的key
值,可以创建或访问不同的共享内存段。常见的获取key
值的方法是使用ftok
函数,其原型如下:
#include
#include
key_t ftok(const char *pathname, int proj_id);
ftok
函数根据文件路径名和项目标识生成一个key
值。路径名用于确定文件,项目标识用于区分不同的共享内存段。通常,多个进程通过使用相同的路径名和项目标识来获得相同的key
值,从而可以访问到同一个共享内存段。
注意,key
值有一定的限制,在32位系统中通常为32位,而在64位系统中通常为64位。因此,key
值的选择应该在范围内合理,避免冲突。
除了使用ftok
函数生成key
值外,也可以直接使用预定义的key
值或通过其他方式协商得到。在多进程间进行共享内存通信时,确保使用相同的key
值非常重要。
2.IPC_PRIVATE宏
在Linux中,IPC_PRIVATE
是一个宏定义,用于生成私有的键值(key)用于创建或访问IPC资源,如共享内存、消息队列和信号量等。
IPC_PRIVATE
宏在头文件
中定义如下:
#define IPC_PRIVATE ((key_t) 0) //私有键值
IPC_PRIVATE
宏的值为0。使用IPC_PRIVATE
作为键值时,shmget
、msgget
和semget
等函数将会创建一个新的标识符,用于唯一标识相应的IPC资源,并将它们设为私有。这意味着只有创建该IPC资源的进程可以访问和操作该资源,其他进程无法直接访问。
需要注意的是,使用IPC_PRIVATE
创建的IPC资源,像共享内存段、消息队列或信号量,在不同的进程之间是隔离和独立的。即使两个进程都使用相同的IPC_PRIVATE
值调用相同的创建函数,它们也会分别创建各自的私有资源,并且这些资源是互相独立的。
使用getpid
函数:
对于某些情况,可以使用当前进程的进程ID(PID)作为键值的一部分。这通常用于创建与当前进程相关的IP资源。
key_t key = getpid(); // 使用当前进程ID作为键值的一部分
在Linux的shmget
函数中,shmflg
参数是用于指定共享内存段的权限和状态标志的参数。
shmflg
参数可以是以下几种组合方式的标志之一或多个:
IPC_CREAT
:如果指定的共享内存不存在,则创建一个新的共享内存段。如果共享内存已存在,则改变访问权限为指定的权限。如果省略了此标志,则表示仅打开现有的共享内存段。
IPC_EXCL
:与IPC_CREAT
标志一起使用(不单独使用),用于保证只有一个进程能够创建共享内存段,如果指定的key
已经存在,则返回错误。
IPC_NOWAIT
:指定以非阻塞方式运行,如果没有可用的共享内存段,则立即返回错误。
权限标志:可以使用IPC_PRIVATE
与权限值的或操作,表示使用私有的key
值,并使用权限值对共享内存进行权限设置。
shmflg
参数可以根据需要任意组合,使用IPC_CREAT | IPC_EXCL
表示创建一个新的共享内存段并确保唯一性。
shmflg
参数在创建或打开共享内存段时起关键作用,因此在使用时需仔细考虑各个标志位的组合,以确保正确的操作。
int shmid;
char *shmaddr;
// 创建共享内存段
if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
perror("shmget");
return 1;
}
在Linux中,shmat
函数用于将共享内存映射到进程的地址空间,使得进程可以访问共享内存中的数据。
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:共享内存标识符,由shmget
函数返回。
shmaddr
:可选参数,指向进程中的内存地址,用于指定共享内存映射。如果省略该参数或将其设为NULL(nullptr),则系统将选择一个合适的地址进行映射。
shmflg
:可选参数,用于控制共享内存的访问权限和内存保护等。常见的包括:
SHM_R
:读取共享内存的权限。
SHM_W
:写入共享内存的权限。
SHM_X
:执行共享内存的权限。
SHM_U
:允许进程访问共享内存,即使该进程没有写入权限。
SHM_P
:禁止其他进程访问共享内存,即使它们拥有相应的权限。
0
:默认原本的权限。
shmat
函数的返回值是一个指向共享内存中数据类型的指针,通过该指针可以访问共享内存中的数据。如果返回值为NULL,则表示映射失败,可能是因为共享内存已被删除或访问权限不足等原因。
需要注意的是,在使用完共享内存后,应使用shmdt
函数将其从进程的地址空间中解除映射,以释放系统资源。同时,如果共享内存中的数据已被修改,则需要将其同步回原始存储介质,以数据的完整性和一致性。
int shmid;
char *shmaddr;
// 创建共享内存段
if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
perror("shmget");
return 1;
}
// 将共享内存段连接到当前进程的地址空间
shmaddr = (char *) shmat(shmid, NULL, 0);
if (shmaddr == (char *) -1) {
perror("shmat");
return 1;
}
在Linux中,shmdt
函数是用于断开进程与共享内存段之间的连接的函数。它的作用是将共享内存从当前进程的地址空间中分离,使得进程无法再访问共享内存中的数据。
#include
int shmdt(const void *shmaddr);
shmaddr
参数是一个共享内存段的映射地址,即之前使用shmat
函数返回的指针。该参数指定要断开连接的共享内存段。
shmdt
函数将共享内存段从进程的地址空间中分离,并返回成功与否的状态。如果断开连接成功,则返回0;如果失败,则返回-1,并设置相应的错误码。
需要注意的是,断开共享内存段的连接并不会导致共享内存段本身被删除。共享内存段仍然存在于系统中,其他连接到该共享内存段的进程仍然可以访问和操作共享内存。
在使用完共享内存后,为了释放资源,确保调用shmdt
函数断开与共享内存段的连接。同时,还需要注意及时删除共享内存段,使用shmctl
函数进行清理和释放。
int shmid;
char *shmaddr;
// 创建共享内存段
if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
perror("shmget");
return 1;
}
// 将共享内存段连接到当前进程的地址空间
shmaddr = (char *) shmat(shmid, NULL, 0);
if (shmaddr == (char *) -1) {
perror("shmat");
return 1;
}
// 在共享内存中写入数据
*shmaddr = 'H';
*(shmaddr + 1) = 'i';
// 从共享内存中读取数据并输出
printf("Shared memory content: %c%c\n", shmaddr[0], shmaddr[1]);
// 断开共享内存连接并释放资源
if (shmdt(shmaddr) == -1) {
perror("shmdt");
return 1;
}
在Linux中,shmctl
函数用于对共享内存段进行控制操作,比如获取/设置共享内存段的状态信息、修改共享内存段的权限、删除共享内存段等。
#include
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存段标识符,由shmget
函数返回。
cmd
:控制命令,用于指定要执行的操作。常用的控制命令包括:
IPC_STAT
:获取共享内存段的状态信息,该信息将被存储在buf
参数指向的结构体中。
IPC_SET
:修改共享内存段的权限和其他属性,修改的信息为buf
参数指向的结构体中的内容。
IPC_RMID
:删除共享内存段,释放系统资源。
buf
:指向shmid_ds
结构体的指针,用于存储获取到的状态信息或修改的属性信息。一般置为NULL(在删除共享空间时)
shmctl
函数的返回值取决于执行的控制命令。一般来说,如果操作成功,则返回0;如果发生错误,则返回-1,并设置相应的错误码。
需要注意的是,使用shmctl
函数修改共享内存段的属性、权限等操作可能需要管理员权限。同时,删除共享内存段后,其他进程无法再访问此共享内存段。使用shmctl
函数时要小心谨慎,确保操作的正确性和安全性。
cmd
参数用于指定要执行的操作类型。cmd
的取值如下:
IPC_STAT
:获取共享内存的状态信息,将共享内存的信息存储在struct shmid_ds
结构体中。通过该结构体可以获取共享内存的大小、创建时间、最后连接时间等信息。
IPC_SET
:修改共享内存的属性,需要提供一个struct shmid_ds
结构体的指针作为buf
参数,通过修改该结构体的成员来改变共享内存的权限、最后连接时间等属性。
IPC_RMID
:删除共享内存,将释放占用的系统资源,并使其他进程无法再访问该共享内存。
例如,可以使用以下代码删除一个共享内存段:
#include
#include
int main() {
int shmid = shmget(key, size, IPC_CREAT | 0666); // 假设已创建一个共享内存段
if (shmid == -1) {
perror("shmget");
return 1;
}
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
printf("Shared memory removed successfully.\n");
return 0;
}
在上述示例中,首先通过shmget
函数获取共享内存段的标识符,然后使用shmctl
函数以IPC_RMID
命令删除该共享内存。如果删除成功,将输出"Shared memory removed successfully."。 需要注意的是,删除共享内存只是断开了与该共享内存的连接,并释放了共享内存使用的系统资源,但实际的共享内存段可能在文件系统中保留。
buf
参数是一个指向struct shmid_ds
类型的指针,通过该指针传递共享内存段的信息和修改共享内存段属性的值。struct shmid_ds
结构体定义如:
struct shmid_ds {
uid_t shm_perm.uid; // 所有者ID
gid_t shm_perm.gid; // 组ID
mode_t shm_perm.mode; // 访问权限
int shm_perm.__key; // IPC键值
struct ipc_pid shm_perm.cuid;
struct ipc_pid shm_perm.uid;
unsigned short shm_perm.mode;
unsigned short shm_perm._seq;
time_t shm_ctime; //创建时间
time_t shm_atime; //最后连接时间
time_t shm_dtime; //最后断开连接时间
size_t shm_segsz; //内存段大小
pid_t shm_cpid; //创建进程号
pid_t shm_lpid; //最后连接进程号
short shm_nattch; //当前挂接进程数
... // 其他成员
}
删除共享内存端
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl");
return 1;
}
下面是使用shmctl
函数获取共享内存状态信息的示例代码:
#include
#include
int main() {
int shmid = shmget(key, size,\
IPC_CREAT | 0666); // 假设已创建一个共享内存
if (shmid == -1) {
perror("shmget");
return 1;
}
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) == -1) {
perror("shmctl");
return 1;
}
printf("Shared memory size: %lu bytes
", buf.shm_segsz);
printf("Shared memory owner UID: %d
", buf.shm_perm.uid);
printf("Shared memory owner GID: %d
", buf.shm_perm.gid);
return 0;
}
在该示例代码中,使用shmctl
函数获取共享内存段的状态信息,并将信息存储在struct shmid_ds
结构体中。然后,通过结构体成员访问共享内的大小、所有者UID、所有者GID等信息。类似地,可以使用shmctl
函数来修改共享内存段的属性信息。
共享内存是在多进程或多线程环境下,不同进程或线程之间共享相同的一段内存区域的技术。共享内存是一种高效的IPC(进程间通信)方式,因为它可以避免不必要的数据复制和序列化,从而提高通信效率。
在Linux中,可以使用shmget()
、shmat()
、shmdt()
和shmctl()
等系统调用创建和管理共享内存。其中,shmget()
用于创建共享内存对象并分配内存空间,shmat()
用于将共享内存映射到进程的地址空间中,shmdt()
用于将共享内存从进程的地址空间中解除映射,shmctl()
用于对共享内存控制和管理。
通俗来说:系统在物理内存中创建一片共享区域,这个区域(结构体)由系统维护,通过系统的接口,可以让进程获得这片区域的物理地址,从而在页表中形成映射。
共享内存实现的原理主要涉及以下几个方面:
内核操作:共享内存由内核维护和管理,它通过系统调用(例如shmget,shmat,shmdt,shmctl)来提供对共享内存的创建、映射、解除映射和控制等操作。
分配和映射内存:在创建共享内存时,内核会分配一块连续的内存空间,并为该内存块生成一个唯一的标识符(即共享内存id)。通过映射操作,将这块共享内存映射到进程的地址空间中,使得不同进程之间可以通过相同的地址来访问共享内存。
共享内存访问:不同进程可以通过相同的地址访问共享内存中的数据,因为它们映射的是同一块物理内存。读取和写入共享内存的操作是直接的,无需涉及数据的拷贝或序列化,因此具有高效的性能。
同步机制:由于共享内存可以被多个进程同时访问,因此需要使用适当的同步机制来保证数据的一致性和完整性。常用的同步机制包括信号量和互斥锁,用于防止数据竞争和访问冲突。
内存管理:共享内存的生命周期由创建它的进程控制。当所有使用共享内存的进程都解除了映射关系(使用shmdt)并且不再需要这块共享内存时,可以通过shmctl系统调用来删除共享内存。
总的来说,共享内存是一种多进程间共享内存区域的机制,通过内核提供的系统调用实现分配、映射和控制等操作。不同进程通过映射共享内存到自己的地址空间,实现共享数据的读写操作,并通过适当的同步机制保证数据的一致性。
使用共享内存需要注意以下几点:
共享内存中的数据对所有共享该内存的进程都是可见的,因此需要采取同步措施来避免数据竞争和一致性问题。
共享内存的大小在创建时需要进行配置,如果创建时未指定大小,系统将使用默认值。
共享内存中数据的访问速度比其他IPC方式更快,因此适用于需要快速通信的场景。
共享内存适用于多个进程之间需要频繁通信的场景,因为它避免了不必要的进程间通信的开销
不一定
当设计共享内存缓冲区时,可以考虑以下几个方面:
缓冲区的结构:确定缓冲区的数据组织方式,可以是线性缓冲区、环形缓冲区或其他自定义结构。根据需求选择合适的数据结构,考虑到数据读写的方式和效率。
数据同步:多个进程或线程同时读写共享内存缓冲区时,需要有效地进行数据同步以避免数据冲突。可以使用互斥锁或其他同步机制来确保并发访问的正确性。
读写指针:在环形缓冲区中,可以使用读写指针来标记当前读取和写入的位置。需要确保正确地更新读写指针,并处理边界条件,以避免指针越界或数据覆盖问题。
缓冲区大小:确定缓冲区的大小,以容纳期望的数据量。根据应用程序的需求和性能要求,选择适当的缓冲区大小。
锁粒度:考虑并发访问时锁的粒度,即锁定整个缓冲区还是仅保护特定部分的读写操作。需要权衡锁的开销和并发性能,合理选择锁的粒度。
共享内存缓冲区的设计和实现是复杂的,需要仔细考虑并发访问和数据同步等问题。在使用共享内存缓冲区时,应该注意保证数据的一致性、避免死锁和数据竞争等并发问题,并进行充分的测试和验证。
确实可能出现空读(读到空)的情况,因为其他进程可能已经更新了共享内存中的数据。
为了避免这种情况,使用锁机制来确保只有一个进程在共享内存中写入数据,其他进程在读取数据之前必须先获取锁,以确保读取到最新的数据。 常见的锁机制包括互斥锁和读写锁。(在以后学习中逐步介绍) 总之,共享内存访问需要使用适当的锁机制来确保数据的一致性和完整性,避免空读和其他数据竞争问题。
是滴!!
共享内存和管道结合可以实现高效的并发处理和数据传输。共享内存可以提供快速的数据交换,而管道可以提供进程间通信的机制。
共享内存可以被多个进程访问,因此可以用来实现多个进程之间的数据交换。
管道可以被用来实现进程间通信,将数据从发送进程发送到接收进程。
将共享内存和管道结合使用,可以将数据快速地从发送进程传输到接收进程,从而实现高效的并发处理。
(在实验二中我们模拟了一个实例)
主要特点如下:(可以结合原理和注意点来看待)
不需要亲缘关系;
没有同步互斥之类的保护机制。
易产生死锁:共享内存区允许多个进程同时访问,但是如果对内存区的访问控制没有做好同步,就可能会产生死锁问题。
速度最快:共享内存是最快的共享数据方法,在进程间复制数据时效率很高。
有效减少内存消耗:因为所有的内存管理都由操作系统内核负责,所以应用进程间无需再交换数据。
可直接访问:通过共享内存,进程可以直接访问其他进程的数据,无需通过交换区,这样可以提高数据访问效率。
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include
#include
#include
#include
#include
using namespace std;
const string pathname = "/home/lzh/tmp";
const int proj_id = 1;
// 大小最好分配4096整数倍
#define SHM_SIZE 4096
// 获得key
key_t Get_key()
{
key_t k = ftok(pathname.c_str(), proj_id);
if (k == -1)
{
perror("ftok");
// 后续中可以把自己的log管理加进来
exit(1);
}
return k;
}
// 创建共享内存段的接口
int Get_share_mem(int flag)
{
key_t k = Get_key();
int shmid = shmget(k, SHM_SIZE, flag);
// int shmid = shmget(k,SHM_SIZE,IPC_CREAT|0666);
if (shmid == -1)
{
perror("shmget");
exit(2);
}
return shmid;
}
// 创建
int Creat_shm()
{
return Get_share_mem(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取
int Get_shm()
{
return Get_share_mem(IPC_CREAT);
}
// 将共享内存段连接到当前进程的地址空间
char *Set_share_mem(int shmid)
{
char *shmaddr;
shmaddr = (char *)shmat(shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
exit(3);
}
cout<<"get shmaddr: " << shmaddr <
#include "comm.hpp"
#include
int main()
{
int shmid = Creat_shm();
char *shmaddr = Set_share_mem(shmid);
int cnt=5;
while (cnt--)
{
cout<<"pa:"<
#include "comm.hpp"
#include
#include
#include
int main()
{
int shmid = Get_shm();
char *shmaddr = Set_share_mem(shmid);
int cnt=5;
while (cnt--)
{
char buffer[1024];
cout << "pb write:" ; fgets(buffer,sizeof(buffer),stdin); memcpy(shmaddr,buffer,strlen(buffer));
}
Disconnect_shm(shmid, shmaddr);
return 0;
}
在A窗口中:
[lzh@MYCAT shared_mem]$ ./pa
get shmaddr,shmid: 7
pa:
pa:12345
pa:123456789
pa:adafsdfdv1
pa:1234565555
disconnect shmid: 7
delete shmid: 7
在B窗口中:
[lzh@MYCAT shared_mem]$ ./pb
get shmaddr,shmid: 7
pb write:12345
pb write:123456789
pb write:adafsdfdv1
pb write:1234565555
pb write:你好
disconnect shmid: 7
分配内存大小最好分配4096整数倍。(好理解)
pb直接用得到的共享区就可以啦
while (cnt--) { cout << "pb write:" ; fgets(shmaddr,SHM_SIZE,stdin); }
当将共享内存与管道结合使用时,可以实现进程间的高效数据传输和并发处理。实现方式:利用管道的特点做了一个类似命令发送的过程,如果写端没有发送读取的命令,那么读取端就会一直阻塞下去。
以下是一个示例:
假设有两个进程,一个是发送进程,一个是接收进程:
发送进程:将数据写入共享内存中,并通过管道通知接收进程。
接收进程:从管道接收通知,然后从共享内存中读取数据进行处理。
#include
#include
#include
#include
#define SHM_SIZE 1024
int main() {
int shmid;
char *shmaddr;
int pipefd[2];
// 创建共享内存段
shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
shmaddr = (char *)shmat(shmid, NULL, 0);
// 创建管道
pipe(pipefd);
// 写入数据到共享内存
strcpy(shmaddr, "Hello, shared memory!");
// 通过管道发送消息通知接收进程
write(pipefd[1], "1", 1);
// 等待接收进程处理完毕
char buf;
read(pipefd[0], &buf, 1);
// 断开共享内存连接
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
#include
#include
#include
#include
#define SHM_SIZE 1024
int main() {
int shmid;
char *shmaddr;
int pipefd[2];
// 创建共享内存段
shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
shmaddr = (char *)shmat(shmid, NULL, 0);
// 创建管道
pipe(pipefd);
// 等待发送进程的通知
char buf;
read(pipefd[0], &buf, 1);
// 读取共享内存中的数据进行处理
printf("Received data: %s\n", shmaddr);
// 通知发送进程数据处理完毕
write(pipefd[1], "1", 1);
// 断开共享内存连接
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
这只是一个示例,实际使用时还需要考虑数据同步、错误处理等方面的实现。
#include
#include
int main() {
int shmid = shmget(key, size,\
IPC_CREAT | 0666); // 假设已创建一个共享内存
if (shmid == -1) {
perror("shmget");
return 1;
}
//创建结构体接受
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) == -1) {
//IPVC_STAT获取共享内存的状态信息
perror("shmctl");
return 1;
}
printf("Shared memory size: %lu bytes
", buf.shm_segsz);
printf("Shared memory owner UID: %d
", buf.shm_perm.uid);
printf("Shared memory owner GID: %d
", buf.shm_perm.gid);
return 0;
}
Shared memory size: 4096 bytes Shared memory owner UID:Shared memory owner GID:
ipcs
:显示系统当前的消息队列的状态。使用ipcs -q
命令只显示消息队列的相关信息。(可以看看共享内存中的命令中的介绍)
ipcrm
:用于删除消息队列。使用ipcrm -q
命令,其中
是消息队列的标识符。
msgget
:用于创建或打开一个消息队列。具体用法是使用 C 语言的 msgget
函数或在命令行中使用 msgget
命令。
msgctl
:用于控制消息队列的属性。可以使用该命令来获取、设置或删除消息队列的属性。具体用法是使用 C 语言的 msgctl
函数或在命令行中使用 msgctl
命令。
msgsnd
:用于发送消息到指定的消息队列中。一般通过 C 语言的 msgsnd
函数来实现。
msgrcv
:用于接收消息从指定的消息队列中。一般通过 C 语言的 msgrcv
函数来实现。
这些命令可以在命令行界面上查看和操作消息队列。
[root@MYCAT ~]# ipcs -q
------ Message Queues -------- key msqid owner perms used-bytes messages
key:消息队列的键,用于在系统中唯一标识消息队列。当一个进程想要访问消息队列时,它需要使用这个键来查找队列。
msqid:消息队列的ID,是在创建队列时系统分配的。进程通过这个ID来对队列进行操作。
owner:队列的所有者,通常是一个用户或进程的ID。只有所有者或者超级用户(root)可以改变队列的权限。
perms:消息队列的权限,决定了哪些用户或组可以访问队列,以及他们可以执行的操作(如读取、写入或改变权限)。
used-bytes:当前队列中已使用的字节数,表示队列中消息的总大小。
messages:队列中消息的数量。每个消息队列都有一个最大消息数量的限制。
在Linux中,使用
头文件提供的函数来操作消息队列。以下是一些常用的消息队列函数:
msgget()
:创建或打开一个消息队列。
int msgget(key_t key, int msgflg);
msgctl()
:控制和管理消息队列,例如获取、设置和删除消息队列的属性。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgsnd()
:将消息发送到消息队列中。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgrcv()
:从消息队列中接收消息。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);
mq_open
函数用于打开一个已经存在的消息队列,或者创建一个新消息队列。
#include#include int mq_open(const char *name, int oflags, ... /* mode_t mode */);
参数name
是消息队列的名称,oflags
是指定打开模式,可以包含O_RDONLY
、O_WRONLY
或O_RDWR
等标志。该函数返回一个消息队列描述符,如果失败则返回-1
。
mq_send
函数用于向消息队列中发送消息
#include int mq_send(mqd_t mqdes, const char *msg_ptr size_t msg_len, unsigned int msg_prio);
参数mqdes
是消息队列描述符,msg_ptr
是指向消息的指针,msg_len
是指消息的长度,msg_prio
是指消息的优先级。该函数返回0
表示成功,如果失败则返回-1
。
mq_receive
函数用于从消息队列中接收消息。
#include ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
参数mqdes
是消息队列描述符,msg_ptr
是指向消息缓冲区的指针,msg_len
是指缓冲区的大小,msg_prio
是指接收消息的优先级。该函数返回接收到的消息长度,如果失败则返回-1
。
mq_close
函数用于关闭一个打开的消息队列。
#include int mq_close(mqd_t mqdes);
参数mqdes
是消息队列描述符。该函数返回0
表示成功,如果失败则返回-1
。
mq_unlink
函数用于删除一个消息队列。
#include int mq_unlink(const char *name);
参数name
是消息队列的名称。该函数返回0
表示成功,如果失败则返回-1
。
msgget()
函数用于获取一个消息队列的标识符(ID)。该函数允许进程访问和操作共享内存中的消息队列,从而实现进程间通信(IPC)。
#include int msgget(key_t key, int msgflg);
key
:一个键值(key value),用于在系统中唯一标识消息队列。当一个进程想要访问消息队列时,它需要使用这个键来查找队列。键值可以是任何有效的键值,例如,一个字符串、数字或文件的路径等。
msgflg
:一个标志位,用于控制msgget()
函数的行为。可能的标志值包括:
IPC_CREAT
:如果消息队列不存在,则创建一个新的队列。如果队列已经存在,则不执行任何操作。
IPC_EXCL
:与IPC_CREAT
标志一起使用,表示如果消息队列不存在,则创建一个新队列。如果队列已经存在,则尝试获取队列的独占访问权限,如果无法获取则返回错误。
IPC_NOWAIT
:如果消息队列已满,则不等待,立即返回错误。
IPC_WAIT
:如果消息队列已满,则等待新的消息被写入队列。
IPC_STATUS
:返回队列的状态信息,如队列标识符、所有者、权限等。
如果成功,则返回一个非负整数,表示消息队列的标识符(ID)。
如果失败,则返回-1,并设置errno
码以表示错误原因。可能的错误原因包括:EACCESS
(权限 denied)、EEXIST
(队列已存在,无法创建)、EIDRM
(队列已被删除)等。
在使用msgget()
函数之前,需要确保消息队列已经存在或打算创建。如果消息队列不存在且不打算创建,则可以使用key_t
类型的键值作为key
参数,以避免系统分配一个已存在的队列。
key 与之前共享内存中的key是一样的
在Linux系统中,msgctl()
函数是一个系统调用,它用于控制消息队列的属性。这个函数是POSIX消息队列接口的一部分,允许进程间通过共享内存进行通信。
#include int msgctl(int msqid, int cmd, struct msqid_ds *msg_buf);
msqid
:这是消息队列的标识符(ID),由msgget()
函数返回。
cmd
:这是一个命令字,用于指定要执行的操作。可能的命令包括:
IPC_STAT
:获取消息队列的状态。
IPC_SET
:设置消息队列的权限。
IPC_RMID
:删除消息。
msg_buf
:这是一个指向struct msqid_ds
类型的指针,用于存储消息队列的属性。这个结构体的定义如下:
struct msqid_ds { int msqid; /* 消息队列ID */ pid_t pid /* 拥有者进程ID */ uid_t uid; /* 拥有者用户ID */ gid_t gid; /* 拥有者组ID */ mode_t perms; /* 队列权限 */ struct timespec msg_stime; /* 最近一次发送消息的时间 */ struct timespec msg_rtime; /* 最近一次接收消息的时间 */ struct timespec msg_ctime; /* 最近一次更改消息队列属性时间 */ unsigned long long msg_qnum;/* 消息队列中的消息数量 */ unsigned long long msg_qbytes;/* 消息队列的最大消息大小 */ key_t key; /* 消息的键 */ bool msg_flg; /* 消息队列的标志位 */ };
msgctl()
函数允许进程查询和修改消息队列的属性,如权限和所有权。例如,进程可以使用msgctl()
来改变消息队列的访问权限,或者删除不再消息队列。
msgctl()
函数的返回值:
如果成功,则返回0。
如果失败,则返回-1,并设置errno
以表示错误原因。常见的错误原因包括权限问题(EACCES
)、队列已满(EAGAIN
)或发送的数据长度不正确(EINVAL
)。
展示了如何使用msgctl()
函数来获取消息队列的状态:
#include#include #include int main() { key_t key = ftokprogname", ''); int msq = msgget(key, 0600); // 创建或获取消息队列 if (msqid == -1) { perror("msgget"); exit(1); } struct msqid_ds msg_buf; if (msgctl(msqid, IPC_STAT, &msg_buf) == -1) { perror("msgctl IPC_STAT"); exit(); } printf("Message queue status:\n"); printf("msqid: %d\n", msg_buf.msqid); printf("Owner PID: %ld\n", msg_buf.pid); // 输出更多队列属性... // 清理 msgctl(msqid, IPC_RMID, NULL); // 删除消息队列 exit(0); }
在这个示例中,我们首先使用ftok()
生成一个键,然后使用msgget()
获取或创建一个消息队列。接着,我们使用msgctl()
获取消息队列的状态,并打印出来。最后,我们使用msgctl()
删除消息队列。
在Linux系统中,msgsnd()
函数是一个系统调用,它用于向消息队列发送消息。这个函数是POSIX消息队列接口的一部分,允许进程间通过共享内存进行通信。
#include int msgsnd(int msqid, const void *msg_buf, size_t msg_len, int msg_flags);
msqid
:这是消息队列的标识符(ID),由`msgget()函数返回。
msg_buf
:这是一个指向void
类型的指针,用于存储要发送的消息。
msg_len
:这是要发送消息的长度。
msg_flags
:这是一个整数参数,用于指定消息发送的选项。可能的标志值包括:
IPC_NOWAIT
:如果消息队列已满,则不等待,立即返回错误。
IPC_WAIT
:如果消息队列已满,则等待直到有空间可用。
MSG_DONTWAIT
:如果消息队列已满,则发送方不会阻塞等待接收方处理消息。
如果成功,则返回0。
如果失败,则返回-1,并设置errno
以表示错误原因。常见的错误原因包括权限问题(EACCES
)、队列已满(EAGAIN
)或发送的数据长度不正确(EINVAL
)。
在使用msgsnd()
函数之前,通常需要使用msgget()
函数创建一个消息队列。在发送消息时,需要将消息存储在void
类型的缓冲区中,并将其长度设置为要发送消息的长度。发送方可以将消息发送到接收方,接收方使用msgrcv()
从队列中接收消息。
msgsnd()
函数发送的消息长度不能超过系统定义的最大消息长度的限制。此外,msgsnd()
函数发送的消息应该与接收方期望接收的消息长度相匹配。如果发送方发送的消息长度与接收方期望接收的消息长度不匹配,则接收方可能会出现缓冲区溢出或其他问题。
#include#include #include int main() { key_t key = ftokprogname", ''); int msqid = msgget(key, 0600); // 创建或获取消息队列 if (msqid == -1) { perror("msgget"); exit(1); } void *msg_buf = malloc(1024); // 分配一个缓冲区用于存储消息 if (_buf == NULL) { perror("malloc"); exit(1); } // 初始化消息缓冲区 sprintf(msg_buf, "Hello, world!"); // 发送消息到队列 if (msgsnd(msqid, msg_buf, strlen(msg_buf), 0) == -1) { perror("msgsnd"); exit(1); } free(msg_buf); // 释放内存 // 删除队列 msgctl(msqid, IPC_RMID, NULL); exit(0); }
在这个示例中,我们首先使用ftok()
生成一个键,然后使用msgget()
获取或创建一个消息队列。然后我们分配一个缓冲区,用于存储要发送的消息,并将其发送到队列中。最后,我们使用msgctl()
删除队列。
msgrcv
函数是用于从消息队列中接收消息的系统调用
#include#include ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msqid
:消息队列的标识符,该标识符是通过调用msgget
函数获得的。
msgp
:指向消息缓冲区的指针,该缓冲区用于存放从队列中接收到的消息。
msgsz
:消息缓冲区的大小,即可以接收的最大消息长度。
msgtyp
:消息类型。如果设置为MSG_ANY
,则接收任何类型的消息;否则,只接收类型与msgtyp
相等的消息。
msgflg
:控制接收行为的标志。它可以是0
,表示只接收非阻塞方式;也可以是IPC_NOWAIT
,表示如果队列中没有指定类型的消息,则立即返回错误。
msgrcv
函数会从指定的消息队列中接收一条消息,并将该消息存入由msgp
指向的缓冲区中。如果msgtyp
参数设置为MSG_ANY
,则函数会接收队列中第一个可用的消息,不论其类型。如果msgtyp
为一个具体的值,则只有当队列中存在该类型的消息时,函数才会接收。
msgtyp
是一个整型值,它指定了自己希望接收的消息类型。msgtyp
的值可以是任意整数,但是通常它与消息队列中消息类型的标识符相对应。
消息队列中的每个消息都有一个类型标识符,这个标识符是在向消息队列中发送消息时由msgsnd
函数指定的。当一个进程调用msgrcv
函数时,它可以指定msgtyp
参数为一个特定的类型标识符,这样它就只会接收类型匹配的消息。如果msgtyp
被设置为MSG_ANY
,则msgrcv
会接收任何类型的消息。
msgtyp
的使用场景举例:
如果你知道消息队列中只有两种类型的消息,分别为类型1和类型2,你可以这样调用msgrcv
来接收特定类型的消息:
msgrcv(msqid, mbufp, sizeof(mbufp->mtext), 1, 0); // 接收类型1的消息 msgrcv(msqid, mbufp, sizeof(mbufp->mtext), 2, 0); // 接收类型2的消息
如果你想要接收任何类型的消息,你可以使用MSG_ANY
:
msgrcv(msqid, mbufp, sizeof(mbufp->mtext), MSG_ANY, 0); // 接收任何类型的消息
如果你想要接收最高优先级的消息,你可以将msgtyp
设置为IPC_PRIVATE
,然后在发送消息时使用msg->mtype
来指定优先级:
msgrcv(msqid, mbufp, sizeof(mbufp->mtext), IPC_PRIVATE, 0); // 接收优先级最高的消息
在实际应用中,消息类型的使用取决于应用程序的具体需求。例如,在某些应用程序中,消息类型可能用来区分不同的数据类型或者指示不同的操作。在多进程通信中,正确使用msgtyp
可以确保消息的有序接收,避免错误类型的消息被错误地处理。
在Linux中,msgrcv
函数用于从消息队列中接收消息。该函数的第四个参数msgflag
是一个整型值,用于控制函数的行为。msgflag
可以是以下几个标志的组合:
0
:值,表示阻塞式接收。如果消息队列中没有消息,函数会一直等待直到有消息可用为止。
IPC_WAIT
(只适用于msgflag
):如果队列中没有消息可接收,msgrcv
会阻塞,直到有消息可接收或者超时。这个标志与msgflg
的其他标志不兼容。
MSG_NOERROR
:如果消息队列已经被删除,但调用进程仍然尝试接收消息,则返回一个错误而不是死亡。默认情况下,如果消息队列不存在,msgrcv
会导致进程终止。
MSG_EXPUNGED
:如果消息队列中的消息在接收过程中被删除(例如,队列的拥有者销毁了队列),则将MSG_EXPUNGED
标志设置在返回的msgrcv
结果中。接收进程可以通过检查这个标志来确定消息是否已被删除。
IPC_NOWAIT
(只适用于msgflag
):如果队列中没有消息可接收,则msgrcv
不会阻塞,而是立即返回-1
,并且设置errno
为EAGAIN
。
IPC_NOWAIT
和MSG_NOERROR
的组合:如果队列中没有消息可接收,则不阻塞,并且如果队列已经被销毁,则返回错误而不是进程终止。
成功调用msgrcv
后,它会返回接收到的消息的长度。如果返回值是-1
,则表示出错,错误原因可以通过errno
来获取。
如果消息队列已被删除,任何对它的操作都会失败。
接收消息时,如果有多个消息类型相同,则按照它们在队列中的顺序来接收。
消息队列的存取权限需要通过msgflg
参数与适当的存取控制位进行按位与操作来设置。
只有消息队列的所有者(即拥有该队列的进程)才能从队列中接收消息。
在没有可用的消息时,调用 msgrcv()
的进程可能会被挂起,直到有消息可用或信号中断进程。这是通过 IPC_NOWAIT
标志实现的。
调用 msgrcv()
的进程需要拥有读权限以访问相关的消息队列。
int msgrcv(int msgid, struct msgbuf *mbufp, int msgflg, int msgprio);
msgbuf
结构通常如下定义:
struct msgbuf { long mtype; /* 消息类型 */ char mtext[]; /* 消息文本(可变长度数组) */ };
在这个结构中,mtype
字段用来标识消息的类型,而mtext
字段则用来存储消息的实际内容。mtext
字段是一个可变长度的数组,这意味着消息的长度是不固定的,可以根据需要传递不同长度的数据。
在msgrcv
函数中,mbufp
指向的msgbuf
结构会被用来接收消息队列中的消息。函数会将从队列中读取的消息类型存储在msgbuf
的mtype
字段中,将从队列中读取的消息内容存储在mtext
字段中。
其他参数的含义与之前描述的一致:
msgid
是消息队列的标识符。
msgflg
是控制接收行为的标志,可以是0
或者IPC_NOWAIT
。
msgprio
是消息的优先级,如果消息队列是按照优先级来存储消息的,这个参数就会被用来指定想要接收的最低优先级的消息。
当msgrcv
函数成功执行时,它返回接收到的消息的长度。如果返回-1
,则表示出错,可以通过errno
来获取错误代码。
在这里我使用一个例子来解释这个现象:
在Linux系统中,消息队列使用mq_open
、mq_send
、mq_receive
和mq_close
等系统调用进行操作
#include#include #include #include #include #include #include #include #include #define QUEUE_NAME "/my_mq" #define QUEUE_SIZE 1024 int main() { // 创建消息队列 mqd_t mq; struct mq_attr attr; memset(&attr, 0, sizeof(attr)); // 创建消息缓冲区 char message[QUEUE_SIZE]; ssize_t read_count; // 设置消息队列的属性 attr.mq_maxmsg = 10; attr.mq_msgsize = QUEUE_SIZE; mq = mq_open(QUEUE_NAME, O_AT | O_RD, 0666 & attr); if (mqdes == -1) { perror("mq_open"); exit(EXIT_FAILURE); } printf("消息队列已创建,描述符为:%d\n", mq); // 发送消息 sprintf(message, "Hello, World!"); if (mq_send(mq, message, strlen(message), 0) == -1) { perror("mq_send"); exit(EXIT_FAILURE); } // 接收消息 read_count = mq_receive(mq, message, QUEUE_SIZE, NULL); if (read_count == -1) { perror("mq_receive"); exit(EXIT_FAILURE); } printf("Received message: %s\n", message); // 关闭消息队列 if (mq_close(mq) == -1) { perror("mq_close"); exit(EXIT_FAILURE); } printf("消息队列已关闭\n"); if (mq_unlink(QUEUE_NAME) == -1) { perror("mq_unlink"); exit(EXIT_FAILURE); } printf("消息队列已sunlink\n"); return 0; }
在这个示例中,我们首先定义了一个消息队列的名称QUEUE_NAME
和消息队列的大小QUEUE_SIZE
。然后,我们使用mq_open
函数创建一个消息队列,并设置了队列的最大消息数量和每个消息的最大大小。如果创建成功,mq_open
函数会返回一个消息队列描述符,我们将其存储在mqdes
变量中。
接下来,我们使用mq_close
函数关闭消息队列。如果关闭成功,函数会返回0
,否则返回-1
。
请注意,这个示例假设您已经有了相应的权限来创建和访问消息队列。在实际使用中,您可能需要根据实际情况调整消息队列的权限和属性。此外,如果消息队列已经存在,mq_open
函数会打开现有的队列,而不是创建一个新的队列。
注意,这个示例没有处理错误情况,实际应用中应该添加适当的错误处理逻辑。
mq_attr
结构体是mq_open
函数的第二个参数,用于指定消息队列的属性。该结构体的定义如下:
#include typedef struct mq_attr { int mq_flags; /* 打开模式,如O_RDONLYO_WRONLY、O_RDWR等 */ mode_t mq_mode; /* 权限掩码 */ size_t mq_msgsize; /* 每个消息的最大长度 */ int mq_maxmsg; /* 队列中允许的最大消息数量 */ int mq_msgmax; /* 每个消息的最大优先级 */ int mq_curmsgs; /* 当前消息数量 */ int mq_maxattr; /* 最大属性数量 */ struct mq_attr *mq_attrs; /* 指向mq_attr结构的指针,用于指定消息队列的属性 */ } mqattr;
其中,各个成员的含义如下:
mq_flags
:打开模式,用于指定消息队列的访问模式,如O_RDONLY
、O_WRONLY
、O_RDWR
等。
mq_mode
:权限掩码,用于指定消息队列的访问权限。
mq_size
:每个消息的最大长度,用于指定每个消息的最大长度。
mq_maxmsg
:队列中允许的最大消息数量,用于指定队列中允许的最大消息数量。
mq_msgmax
:每个消息的最大优先级,用于指定每个消息的最大优先级。
mq_curmsgs
:当前消息数量,用于指定当前消息数量。
mq_maxattr
:最大属性数量,用于指定最大属性数量。
mq_attrs
:指向mq_attr
结构的指针,用于指定消息队列的属性。
在mq_open
函数中,可以指定mq_attr
结构体的值来指定消息队列的属性。如果不指定任何属性,则使用默认值。如果指定了mq_attr
结构体,则该结构体的值将被用于创建消息队列,并将其存储在队列的属性中。在创建消息队列后,可以使用mq_attr
结构体的成员来获取或设置队列的属性。
消息队列(Message Queue)是一种进程间通信(IPC)机制,它允许一个或多个生产者进程向队列中插入消息,同时也允许一个或多个消费者进程从队列中消息。消息队列提供了一种异步、有序的通信方式,适用于不同进程之间传递数据的情景。
消息队列的原理可以概括为以下几个关键点:
数据结构(先描述在组织):消息队列是通过内核维护的一个数据结构来实现的。这个数据结构通常包含一个或多个消息缓冲区,每个缓冲区都包含了一定长度的数据。
生产者-消费者模型:消息队列遵循生产者-消费者模型,其中生产者是向队列中添加消息的进程,消费者是从队列中取出消息的进程。
同步机制:消息队列通常包含了同步机制,如互斥锁(mutex)、条件变量(condition variables)等,以保证多个消费者不会同时访问队列中的同一个消息,也不会多个生产者同时向队列中添加消息。(在之后的学习中介绍)
消息传递:生产者进程将消息放入队列时,消息会被放置在队列的末尾。消费者进程从队列中取出消息时,会从队列的头部取出消息。如果队列是循环的,则当队列满时,新来的生产者会覆盖队列头部的消息。
消息类型和优先级:消息队列中的每个消息都可以有一个类型标识符和优先级。这允许消费者根据类型或优先级来选择性地接收消息。
阻塞与非阻塞:消息队列操作可以是阻塞的,也可以是非阻塞的。阻塞操作会在没有可用消息时等待,直到有消息可取或可放。非阻塞操作则会立即返回,如果队列满或空,则根据操作的具体定义可能会返回错误或者将消息放入队列中(取决于msgsnd
和msgrcv
函数的标志位)。
权限控制:消息队列的访问通常受到权限控制的限制,以确保只有授权的进程才能读取或写入队列中的消息。
队列(Queue):队列是一种先进先出(FIFO)的数据结构,用于存储消息。消息按照它们到达的顺序被添加到队列中,并按照它们被提取的顺序被处理。队列常用于异步消息传递,因为它能够确保消息的传递顺序。
堆栈(Stack):堆栈是一种后进先出(LIFO)的数据结构,它用于存储消息的临时存储区。当一个消息被添加到堆栈时,它会被放置在顶部。当需要处理消息时,通常会从堆栈的顶部弹出消息。堆栈通常用于快速处理少量消息,因为它可以提供高效的查找和删除操作。
优先级队列(Priority Queue):优先级队列是一种具有优先级标记的消息队列,它可以根据消息的优先级对消息进行排序和检索。这种数据结构允许对不同重要性的消息进行区分处理,因此对于需要优先处理的消息具有更高的处理效率。
除了以上三种常见的数据结构外,消息队列还可能使用其他数据结构,如链表、哈希表等,具体取决于实现和应用场景的需求。
异步通信:生产者进程可以将消息放入队列中,而消费者进程可以独立地从队列中读取,两者不必同时进行。这种方式可以提高进程通信效率,同时也可以使得进程之间的通信更加灵活。
数据序列化、顺序性:消息队列中的消息是有序的,按照消息被发送的顺序来接收。这种方式可以使得进程之间的通信更加可靠,因为每个进程都可以按照预期的顺序来接收和处理消息,确保了数据的序列化。
可靠性:消息队列通常提供消息的持久化能力,即使发送进程或接收进程发生故障,消息也不会丢失。
灵活性:消息队列可以支持多种类型的数据,包括文本信息和二进制数据,还可以设置消息的优先级。
可伸缩性:消息队列可以支持多个生产者和多个消费者,这使得它们在分布式系统和并发处理中非常有用。
独立性:消息队列中的消息是独立的,每个消息都有一个独立的主题和内容。这种方式可以使得进程之间的通信更加灵活,因为每个进程都可以根据自己的需要来接收和处理消息。
安全性:消息队列提供了访问控制和消息认证等功能,可以保证只有授权的进程才能访问队列中的消息,同时也可以防止非法篡改和伪造消息。
总之,消息队列是一种非常灵活和可靠的IPC机制,它可以使得不同的进程之间更加方便地进行通信和同步。
#include#include #include int main() { key_t key = ftokprogname", ''); int msqid = msgget(key, 0600); // 创建或获取消息队列 if (msqid == -1) { perror("msgget"); exit(1); } void *msg_buf = malloc(1024); // 分配一个缓冲区用于存储消息 if (_buf == NULL) { perror("malloc"); exit(1); } // 初始化消息缓冲区 sprintf(msg_buf, "Hello, world!"); // 发送消息到队列 if (msgsnd(msqid, msg_buf, strlen(msg_buf), 0) == -1) { perror("msgsnd"); exit(1); } free(msg_buf); // 释放内存 // 删除队列 msgctl(msqid, IPC_RMID, NULL); exit(0); }
#include#include #include #include #include #include #include #include #include #define QUEUE_NAME "/my_mq" #define QUEUE_SIZE 1024 int main() { // 创建消息队列 mqd_t mq; struct mq_attr attr; memset(&attr, 0, sizeof(attr)); // 创建消息缓冲区 char message[QUEUE_SIZE]; ssize_t read_count; // 设置消息队列的属性 attr.mq_maxmsg = 10; attr.mq_msgsize = QUEUE_SIZE; mq = mq_open(QUEUE_NAME, O_AT | O_RD, 0666 & attr); if (mqdes == -1) { perror("mq_open"); exit(EXIT_FAILURE); } printf("消息队列已创建,描述符为:%d\n", mq); // 发送消息 sprintf(message, "Hello, World!"); if (mq_send(mq, message, strlen(message), 0) == -1) { perror("mq_send"); exit(EXIT_FAILURE); } // 接收消息 read_count = mq_receive(mq, message, QUEUE_SIZE, NULL); if (read_count == -1) { perror("mq_receive"); exit(EXIT_FAILURE); } printf("Received message: %s\n", message); // 关闭消息队列 if (mq_close(mq) == -1) { perror("mq_close"); exit(EXIT_FAILURE); } printf("消息队列已关闭\n"); if (mq_unlink(QUEUE_NAME) == -1) { perror("mq_unlink"); exit(EXIT_FAILURE); } printf("消息队列已sunlink\n"); return 0; }
为什么信号量是进程间通信(IPC)的一种?
前边说过信号量本质是一个计数器,她确实不直接能够促使进程间实现通信
通信不仅仅是通信数据,互相协同也是通信的一部分
要实现协同,本质也是通信
ip
这个命令用于查看系统中的信号量、消息队列和共享内存的信息。使用ipcs
命令可以快速查看当前系统中的信号量列表,包括它们的ID、名称、归属进程ID、当前值等信息。
ipcs -s
ipcrm
这个命令用于删除系统中的信号量、消息队列和共享内存。如果你需要删除一个信号量,可以使用ipcrm
命令 followed by the semaphore ID。
ipcrm -s 1234
semget
这个命令用于创建一个新的信号量或者获取一个已存在的信号量的句柄。它可以设置信号量的初始值,并且指定信号量的所有权。
semget -m 10000 -p 8080 semname
semop
这个命令用于对信号量执行P(等待和V(信号)操作。它通过操作数组来执行一系列的操作,每个操作指定一个信号量ID和操作类型。
semop -S 1234 0 1
semctl
这个命令用于控制信号量。它可以设置信号量的值,获取信号量的值,或者启动、停止信号量。
semctl -u semname -1 1
在使用这些命令时,需要具备相应的权限,通常需要root权限。
信号量的操作需要遵循一定的协议,以确保并发程序的正确性和一致性。在使用信号量时,应当注意避免死锁和资源饥饿等问题。
------ Semaphore Arrays -------- key semid owner perms nsems
在Semaphore Arrays(信号量数组)中,以下是常用的字段含义:
key:表示信号量数组的关键字(Key),用于标识和访问信号量数组。它是一个唯一的标识符,可以通过它来创建和获取信号量数组。
semid:表示信号量数组的标识符(Semaphore Identifier),是操作系统为该信号量数组分配的一个唯一数字。
owner:表示当前拥有该信号量数组的进程或线程的标识符。通常是一个进程或线程的ID。
perms:表示信号量数组的访问权限(Permissions),指定了其他进程或线程可以访问该信号量数组的权限级别。
nsems:表示信号量数组中的信号量数量(Number of Semaphores),即数组中包含的信号量个数。
Semaphore Arrays是一种用于同步和互斥的数据结构,由操作系统提供并在内核中维护。它可以用于多个进程或线程之间的同步和通信,通过使用信号量数组中的不同信号量来实现不同的操作和控制。每个信号量具有一个整型的值,用于控制各种操作和资源的访问。
在Linux操作系统中,信号量相关的函数主要涉及信号量的创建、操作和控制等方面。他们的头文件是sys/sem.h
semget
这个函数用于创建一个新的信号量或者获取一个已存在的信号量的句柄。它可以设置信号量的初始值,并且指定信号量的所有权。
int semget(key_t key, int nsems, int semflg);
semctl
这个函数用于控制信号量。它可以设置信号量的值,获取信号量的值,或者启动、停止信号量。
int semctl(int semid, int semnum, int cmd, ...);
semop
这个函数用于对信号量执行P(等待)和V(信号)操作。它通过操作数组来执行一系列的操作,每个操作指定一个信号量ID和操作类型。
int semop(int semid, struct sembuf *sops, size_t nsops);
semget函数是Linux中用于获取或创建信号量集(Semaphore Set)的函数,它是System V IPC机制中的一部分。信号量集是一个包含多个信号量的集合,信号量可以用于实现进程间的同步和互斥。
#include#include #include int semget(key_t key, int nsems, int semflg);
其中,参数key是信号量集的键值,用于唯一标识信号量集。nsems是信号量集中的信号量数量,用于控制信号量集的容量。flags是信号量集的属性,用于控制信号量集的行为。
它用于指定信号量集的创建标志和权限模式。semflg
是一个整型值,可以是由多个标志位组成的组合。以下常用的标志位:
IPC_CREAT
:如果该标志位被设置,并且指定的 key
不存在,semget
函数将创建一个新的信号量集。如果 key
已存在,则返回现有信号量集的标识符。
IPC_EXCL
:与 IPC_CREAT
标志位结合使用时,如果指定的 key
已存在,semget
函数将失败,而不是创建一个新的信号量集。
0666
(或 0644
):这是文件的默认权限模式用于设置创建的信号量集的权限。这里有 rw-rw-rw-
(0666
)或 rw-rw-r--
(0644
),分别对应读写权限。
其他权限相关的标志位:可以根据需要设置更多的权限标志位,例如 S_IRUSR
(读权限)、S_IWUSR
(写权限)、S_IXUSR
(执行权限)等。
semget函数的作用是根据键值key来获取或创建一个信号量集,如果键值key不存在,则函数会创建一个新的信号量集并返回其标识符;如果键值key已经存在,则函数会返回该信号量集的标识符。
键值key必须符合一定的规则,通常是由系统生成的或由应用程序定义的。
信号量集的容量nsems必须大于0,否则函数会返回-1。
flags参数可以用于控制信号量集的行为,比如是否为二值信号量、是否支持信号量集操作等。
semget函数返回的信号量集标识符必须妥善保存,后续的信号量操作需要使用该标识符作为参数。
总之,semget函数是Linux中用于获取或创建信号量集的函数,它为进程间的同步和互斥提供了一种高效的机制。在使用时需要注意键值key的定义、信号量集容量和属性的设置,以及信号量集标识符的保存和使用。
key_t key = ftok("path/to/file", PROJ_ID); // 一个唯一的key int semflg = IPC_CREAT | 0666; // 创建标志位和权限模式 int semid = semget(key, 1, semflg); // 创建或获取信号量集
在上面的代码中,ftok
函数用于生成一个唯一的键值,PROJID
是一个项目ID,通常是一个整数,用于确保键值的唯一性。semget
函数的第三个参数 semflg
设置了创建标志位和权限模式。在这里,IPC_CREAT
用于创建新的信号量集,而 0666
用于设置创建的信号量集的权限,使得所有用户都有读写权限。
在 Linux 中,semctl
函数用于控制和操纵信号量集(Semaphore Set)。信号量集是由 semget
函数创建的,包含一组相关的信号量(Semaphore)。semctl
函数允许用户对信号量集各种控制操作,例如获取和设置信号量值、删除信号量、查询信号量属性等。
#include#include #include int semctl(int semid, int semnum, int cmd, ...);
其中,semid
是信号量集的标识符,由semget
函数返回;semnum
是要进行控制的信号量编号,从0开始;cmd
是控制操作的类型;...
是可变参数列表,用于指定控制操作的参数。
SETVAL
:设置指定信号量的值。
GETVAL
:获取指定信号量的值。
SETALL
:设置信号量集中所有信号量的值。
GETALL
:获取信号量集中所有信号量的值。
IPC_RMID
:删除指定的信号量集。
IPC_STAT
:获取关于信号量集当前状态的信息。
IPC_SET
:设置信号量集的权限和属性。
其他命令根据具体需求和IPC对象的类型而定。
semctl
函数的最后一个参数是可选参数,用于传递与 cmd
命令相关的数据。例如,对于 SETVAL
命令,最后一个参数传递的是要设置的信号量的新值。
union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
union semun
是一个用于semctl
函数的联合体数据类型,用于传递不同类型的参数给semctl
函数的arg
参数。它包含以下成员:
val
:用于设置单个信号量的初始值,或者获取单个信号量的当前值。
buf
:用于获取或设置信号量集的状态(struct semid_ds
结构体类型)。
array
:用于获取或设置信号量集的所有信号量的值,其类型为unsigned short
数组。
一般情况下:
在Linux中,semctl
函数的semnum
参数用于指定要操作的信号量在信号量集中的索引或编号。索引从0开始,代表第一个信号量,以此类推。
semnum
参数可以指定单个信号量进行操作,也可以指定一个范围。以下是几种常见的情况:
如果semnum
为0,表示操作第一个信号量。
如果semnum
为1,表示操作第二个信号量。
如果semnum
为n,表示操作第n+1个信号量。
如果semnum
为SEM_UNDO,表示对信号量的操作将在进程结束时自动撤销(比如进程异常终止)。
可以根据实际需求选择具体的semnum
值来操作特定的信号量。
例如,以下示例代码使用union semun
来初始化一个信号量的值:
#include union semun arg; int semid = semget(key, 1, IPC_CREAT | 0666); // 创建或获取信号量集 arg.val = 5; // 设置初始值为5 semctl(semid, 0, SETVAL, arg); // 设置信号量的值为5
在上面的代码中,通过给arg
联合体的val
成员赋值5,将初始值设置为5,并使用semctl
函数的SETVAL
命令来将信号量的值设置为5。
需要注意的是,semnum
参数的有效范围是0到信号量集中信号量的数量减1。在使用semctl
函数时,应确保指定的semnum
值在合法范围内,否则可能会导致错误或未定义行为。
下面是一个示例,演示如何使用 semctl
函数来设置一个信号量的值:
#include#include #include #include int main() { // 获取或创建信号量集 key_t key = ftok("path/to/file", 'A'); int semid = semget(key, 1, IPC_CREAT | 0666); // 设置信号量的值为5 union semun { int val; struct semid_ds *buf; unsigned short *array; } arg; arg.val = 5; semctl(semid, 0, SETVAL, arg); // 获取信号量的值 int value = semctl(semid, 0, GETVAL); printf("Semaphore value: %d\n", value); return 0; }
上述示例中,首先通过 ftok
函数生成一个唯一的键值,然后使用 semget
函数创建信号量集并获取标识符。接下来,使用 semctl
函数将信号量的值设置为5,并使用 semctl
函数再次获取信号量的值并打印出来。
semop
函数用于对指定的信号量集执行一系列的操作。每个操作由struct sembuf
结构体描述,可以指定要操作的信号量编号、操作数(增加或减少)以及操作标志。
#include#include #include int semop(int semid, struct sembuf *sops, unsigned nsops);
semid
:信号量集的标识符,由semget
函数返回。
sops
:指向一个sembuf
结构体的指针,其中包含了要执行的操作列表。
nsops
:表示要执行的操作数量。
成功执行所有操作时,返回0。
如果有操作失败(例如,因为信号量值不允许减少到负数或者因为信号量集已被),则返回-1,并且设置errno来指示错误类型。
如果调用进程的进程组ID与信号量集的创建进程组ID不同,并且信号量集的权限设置了SEM_UNDO
,则返回-1,并且设置errno为EACCES
。
在异常情况下,semop
函数也可能导致进程终止。例如,如果信号量操作导致进程等待,而进程在被信号中断时没有正确处理,那么可能导致进程终止。
需要注意的是,semop
函数的操作是原子性的,这意味着在一次调用中对多个信号量的操作是顺序执行的,不会被其他进程的操作中断。这保证了信号量的安全性,但也可能导致性能问题,特别是在频繁操作信号量时。
sembuf
结构体:struct sembuf { unsigned short sem_num; // 要操作的信号量编号 short sem_op; // 操作操作数(增加或减少) short sem_flg; // 操作标志 };
sem_num
字段用于指定要操作的信号量在信号量集中的索引或编号。索引从0开始,代表第一个信号量。
sem_op
字段用于指定对信号量的操作数。正数表示增加(V操作)信号量的值,负数表示减少(P操作)信号量的值。通常将 sem_op
设置为 -1
表示执行 P 操作,将 sem_op
设置为 1
表示执行 V 操作。
sem_flg
字段用于指定操作标志,用于控制信号量操作的行为。常见的标志包括:
SEM_UNDO
:与操作一起设置,表示系统在进程异常结束后会回滚信号量操作。
IPC_NOWAIT
:表示在不能立即进行操作时不进行等待,而是立即返回错误。
struct sembuf sops[2]; sops[0].sem_num = 0; // 操作第一个信号量 sops[0].sem_op = -1; // 执行 P 操作 sops[0].sem_flg = SEM_UNDO; // 设置操作标志,表示操作失败时撤销操作 sops[1].sem_num = 1; // 操作第二个信号量 sops[1].sem_op = 1; // 执行 V 操作 sops[1].sem_flg = SEM_UNDO;// 设置操作标志,表示操作失败时撤销操作 int semid = semget(key, 2, IPC_CREAT | 0666); // 创建或获取信号量集 int result = semop(semid, sops, 2); // 执行两个操作 if (result == -1) { // 处理错误 // 设置 errno 来了解具体的错误原因 } else { // 操作成功执行 }
在这个例子中,semop
函数被调用来对信号量集中的两个信号量执行操作。第一个信号量(编号为0)的值被减少1,第二个信号量(编号为1)的值被增加1。SEM_UNDO
标志被设置,这意味着如果在操作过程中发生错误(例如,信号量值不允许减少到负数),系统将尝试撤销这些操作
在使用semop
函数时,需要确保传入的操作序列合法且正确,以避免出现竞争条件和死锁等问题。此外,确保在操作完成后对信号量集进行适当处理,如释放、删除等
信号量操作是原子的,这意味着 semop
函数中的所有操作要么全部成功,要么全部失败。这与信号量操作的顺序有关,因为 semop
函数按照 sops
数组中的顺序执行操作。
信号量(semaphore)是一种用于多线程或多进程同步的机制,旨在控制对共享资源的访问,以避免竞态条件、死锁等并发问题。信号量确保了共享资源的正确使用和一致性。
信号量的概念最早由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)提出,并广泛应用于各种操作系统中。信号量可以用于实现互斥(mutual exclusion)和同步(synchronization)。
二进制信号量(Binary Semaphore):其值只能是0或1。它通常用于互斥,确保同一时刻只有一个线程可以进入某个临界区。
计数信号量(Counting Semaphore):其值可以是任何非负整数。它用于控制同时访问资源的线程或进程的数量。
P(wait、decrease):如果信号量的值大于0,线程将其减1并继续执行。如果信号量的值为0,则(阻塞模式下)线程阻塞,直到信号量变为大于0。
V(signal、increase):线程信号量的值加1。如果有其他线程因信号量值为0而阻塞,那么这些线程中的一个将被唤醒。
操作系统:用于进程同步和互斥锁。
嵌入式系统:用于控制对共享资源的访问。
多线程编程:用于临界区保护。
互斥锁(Mutex):用于保护共享资源,确保同一时刻只有一个线程可以访问。信号量可以用来实现互斥锁。
条件变量(Condition Variable):用于线程间的通信。信号量常与条件变量一起使用,实现更复杂的同步模式。
信号量的实现可以基于操作系统提供的原语操作,如P操作和V操作。这些原语操作保证了信号量的原子性,即在并发环境下,信号量的操作不会被其他线程中断。
互斥、独占:实现互斥访问,在同一时刻只允许一个执行流访问共享资源。信号量默认为独占模式,即同一时刻只有一个线程可以获得该信号量。
临界资源、临界区:任何时刻只允许一个流访问一片区域的资源叫做临界资源,这块访问资源的代码叫做临界区。
同步:为了保护临界资源,需要用到同步机制,如互斥锁、信号量。
可重入:信号量支持可重入,即一个线程在获得信号后,如果需要再次进入临界区,不需要再次获得信号量。
非阻塞:信号量提供了非阻塞模式,不会阻塞线程,即一个线程在申请信号量失败时,不会进入阻塞状态,而是继续其他任务。
轮询:在非阻塞模式下,线程在申请信号量失败后,可以通过轮询(Polling)的方式来继续检查信号量是否变为可用状态,一旦信号量可用,线程可以再次尝试申请信号量。
计数器:信号量把一块临界资源分成了若干份,用一个计数器资源来管理,0表示没有进程使用,数字表示由n个进程在访问统一资源,但是访问位置是不同的。二元信号量:只有0和1。等于把资源你当成一个整体来看待,整体分配,整体使用。
原子性:俗点说就是:要么不做,要么做完,他没有正在做的过程,计算寄必须保证这个操作(PV操作)在一个时间分片中完成。多个线程同时对信号量进行操作时,不会互相干扰。
信号量的底层本质是一个计数器。(我们在后面的学习还会重点讲)
信号量的底层原理是基于操作系统的原语和同步机制来实现的。具体来说,信号量是一个计数器,用来控制多个线程的并发访问共享资源。
在操作系统中,信号量是由一个整型变量和一组对该变量进行操作的原子操作组成的。这个整型变量表示资源的可用数量,原子操作用来保证信号量操作的原子性和互斥性。
当一个线程需要访问共享资源时,它首先检查信号量的值。如果信号量大于0,则表示有可用资源,线程可以继续执行,并将信号量减1。如果信号量等于0,则表示没有可用资源,线程进入阻塞状态,直到有其他线程释放资源并将信号量增加。
当一个线程释放共享资源时,它将信号量增加1,这样等待资源的线程就可以继续执行。
通过使用信号量,可以实现多线程的同步和互斥。线程可以通过信号量来控制资源的访问顺序,并确保同一时间只有一个线程能访问共享资源,从而避免数据竞争和不一致性。
需要注意的是,信号量是一种低层的同步机制,通常需要在编程中进行手动管理,以确保信号量的正确使用和释放。
当线程对信号量的管理出现问题时,可能会导致死锁、资源浪费等问题。因此,良好的编程习惯是定期检查信号量的值,并正确地使用和释放它。此外,随着线程数量的增加和系统负载的变化,信号量的值也会发生变化,因此需要定期检查和更新信号量的值,以确保系统的正确运行。
除了信号量之外,操作系统还提供了其他一些同步机制,如互斥锁、条件变量、屏障等,这些机制可以与信号量配合使用,以实现更复杂的同步和协调任务。
总之,信号量的底层原理是基于操作系统的原语和同步机制来实现的,它是一种用于控制多线程访问共享资源的低层机制。在使用信号量时,需要仔细考虑其管理方式,并确保正确地使用和释放它,以避免出现死锁、资源浪费等问题。
省略,可以在函数部分查看有关代码