1. 共享内存
现代Linux有两种共享内存机制:
POSIX共享内存(shm_open()、shm_unlink()) System V共享内存(shmget()、shmat()、shmdt()) 其中,System V共享内存历史悠久,一般的UNIX系统上都有这套机制;而POSIX共享内存机制接口更加方便易使用,一般是结合内存映射mmap用。
mmap和System V共享内存的主要区别在于: sysv shm是持久化的,除非被一个进程明确的删除,否则它始终存在于内存里,直到系统关机; mmap映射的内存在不是持久化的,假如进程关闭,映射随即失效,除非事前已经映射到了一个文件上。 内存映射机制mmap是POSIX标准的系统调使用,有匿名映射和文件映射两种。
匿名映射用进程的虚拟内存空间,它和malloc(3)相似,实际上有些malloc实现会用mmap匿名映射分配内存,不过匿名映射不是POSIX标准中规定的。 文件映射有MAP_PRIVATE和MAP_SHARED两种。前者用COW的方式,把文件映射到当前的进程空间,修改操作不会改动源文件。后者直接把文件映射到当前的进程空间,所有的修改会直接反应到文件的page cache,而后由内核自动同步到映射文件上。
相比于IO函数调使用,基于文件的mmap的一大优点是把文件映射到进程的地址空间,避免了数据从使用户缓冲区到内核page cache缓冲区的复制过程;当然还有一个优点就是不需要频繁的read/write系统调使用。
因为接口易使用,且可以方便的persist到文件,避免主机shutdown丢失数据的情况,所以在现代操作系统上一般偏向于用mmap而不是传统的System V的共享内存机制。
建议仅把mmap使用于需要大量内存数据操作的场景,而不使用于IPC。由于IPC总是在多个进程之间通信,而通信则涉及到同步问题,假如自己手工在mmap之上实现同步,容易滋生bug。推荐用socket之类的机制做IPC,基于socket的通信机制相对健全很多,有很多成熟的机制和模式,比方epoll, reactor等。
sysv shm的实现可以参考glibc源码,shm_open(3) 打开一个名为abc的共享内存,等价于open(“/dev/shm/abc”, ..),其中 /dev/shm 是Linux下sysv共享内存的默认挂载点。shm_open调使用返回一个文件形容符,其实也可以给 mmap(2) 用,作为named share memory用。
第一种:mmap方式,适用场景:非持久化, 父子进程之间,创建的内存非常大时 第二种:shmget方式,适用场景:持久化, 同一台电脑上不同进程之间,创建的内存相对较小时
下面表格详细列举 System V 共享内存 和 POSIX 共享内存 的差异。
System V 共享内存 与 POSIX 共享内存 差异比较
比较项目 | System V 共享内存 | POSIX 共享内存 |
---|---|---|
创建与打开 | 使用 shmget 创建,通过键值(key_t )标识 |
使用 shm_open 创建,通过名称(字符串)标识 |
标识方式 | 整数键值,需要使用 ftok 生成,可能冲突 |
以 / 开头的字符串名称,类似文件路径,冲突概率小 |
连接与断开 | 使用 shmat 连接,shmdt 断开 |
使用 mmap 映射,munmap 解除映射 |
删除共享内存 | 使用 shmctl 的 IPC_RMID 命令显式删除 |
使用 shm_unlink 删除共享内存对象 |
权限控制 | 基于 UNIX 权限位,使用 shmctl 进行权限管理 |
基于文件系统权限,使用标准的文件权限管理 |
命名空间 | 系统范围内,全局共享,键值易冲突 | 基于文件系统的命名空间,名称管理更直观 |
API 复杂度 | API 较旧,函数多且复杂,使用较繁琐 | API 简洁明了,类似于标准文件操作 |
跨平台兼容性 | 某些非 UNIX 系统可能不完全支持,兼容性较差 | 遵循 POSIX 标准,跨平台兼容性更好 |
持久性 | 重启后共享内存段可能仍存在,需要手动删除,易泄漏 | 重启后共享内存对象不存在,生命周期受文件系统管理 |
适用场景 | 需要兼容旧系统或特定系统需求的场景 | 新的应用程序开发,推荐使用,便于维护和移植 |
资源清理 | 需要显式地删除共享内存段,否则可能占用内核资源 | 所有引用关闭且调用 shm_unlink 后自动清理 |
编程范式 | 与传统的 System V IPC 机制一致,学习成本高 | 更符合现代编程习惯,易于上手 |
性能 | 性能高,但管理复杂度高,适合大型、复杂的 IPC | 性能高,且管理方便,适合大多数 IPC 场景 |
详细说明
- 创建与打开:
- System V:使用
shmget
创建共享内存段,需要提供一个键值(key_t
类型),该键值需要全局唯一,通常使用ftok
函数生成。 - POSIX:使用
shm_open
创建共享内存对象,通过名称(字符串)标识,名称以斜杠开头,类似于文件路径,更直观。
- System V:使用
- 标识方式:
- System V:使用整数键值标识,可能出现键值冲突,需要小心管理。
- POSIX:使用字符串名称标识,命名更加灵活,冲突概率较小。
- 连接与断开:
- System V:使用
shmat
将共享内存段附加到进程地址空间,使用shmdt
断开。 - POSIX:使用
mmap
将共享内存对象映射到进程地址空间,使用munmap
解除映射。
- System V:使用
- 删除共享内存:
- System V:需要使用
shmctl
的IPC_RMID
命令显式删除,否则共享内存段会一直存在于内核中,可能导致资源泄漏。 - POSIX:使用
shm_unlink
删除共享内存对象,当所有打开的文件描述符关闭后,内存会自动释放。
- System V:需要使用
- 权限控制:
- System V:基于 UNIX 权限位(读、写、执行),权限管理较为原始,需要通过
shmctl
进行复杂的权限设置。 - POSIX:基于文件系统权限,使用标准的
chmod
、fchmod
等函数,权限管理更灵活方便。
- System V:基于 UNIX 权限位(读、写、执行),权限管理较为原始,需要通过
- 命名空间:
- System V:共享内存键值在整个系统范围内,易发生冲突,且管理不便。
- POSIX:共享内存对象存在于文件系统命名空间下,名称管理直观,冲突概率低。
- API 复杂度:
- System V:API 较为繁琐,函数多且复杂,学习和使用成本高。
- POSIX:API 简洁,函数少,类似于标准文件操作,易于学习和使用。
- 跨平台兼容性:
- System V:某些非 UNIX 系统可能不支持或支持不完全,跨平台兼容性较差。
- POSIX:遵循 POSIX 标准,跨平台兼容性好,适用于大多数 UNIX 系统。
- 持久性:
- System V:共享内存段在系统重启后可能仍然存在,需要手动删除,容易造成内存泄漏。
- POSIX:共享内存对象在系统重启后不存在,其生命周期受文件系统管理,更安全。
- 适用场景:
- System V:适用于需要兼容旧系统、特定系统需求,或者已有大量基于 System V IPC 的代码库的场景。
- POSIX:推荐用于新应用程序开发,API 更现代化,易于维护和移植。
- 资源清理:
- System V:需要程序员手动删除共享内存段,否则会一直占用系统资源。
- POSIX:当所有引用关闭并调用
shm_unlink
后,系统会自动清理共享内存对象。
- 编程范式:
- System V:与早期 UNIX 系统的 IPC 机制一致,概念较为陈旧,可能不符合现代编程习惯。
- POSIX:API 设计更符合现代编程理念,使用体验更好。
- 性能:
- 两者在性能上差别不大,但 POSIX 共享内存的管理更方便,代码更简洁,更易于维护。
案例说明
System V 共享内存示例
// 创建共享内存段
key_t key = ftok("shmfile", 65);
int shmid = shmget(key, 1024, 0666|IPC_CREAT);
// 连接共享内存段
char *str = (char*) shmat(shmid, NULL, 0);
// 使用共享内存...
// 断开共享内存段
shmdt(str);
// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
POSIX 共享内存示例
// 创建共享内存对象
int fd = shm_open("/shmfile", O_CREAT | O_RDWR, 0666);
// 设置共享内存大小
ftruncate(fd, 1024);
// 映射共享内存对象
char *ptr = mmap(0, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 使用共享内存...
// 解除映射
munmap(ptr, 1024);
// 关闭文件描述符
close(fd);
// 删除共享内存对象
shm_unlink("/shmfile");
总结
- System V 共享内存:适合需要兼容旧系统或已有大量 System V IPC 代码的项目,但需要注意手动管理共享内存的创建和删除,防止资源泄漏。
- POSIX 共享内存:更现代化的共享内存机制,API 简洁明了,建议在新的项目中使用,具备更好的跨平台兼容性和易用性。
希望以上表格和说明能够清晰地展示 System V 共享内存 和 POSIX 共享内存 的差异。如有任何疑问,请随时提问!
2. 管道 (PIPE)
管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。 管道的特点: 1、管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道; 2、只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。 3、单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。 4、数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。 管道的实现机制:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
管道只能在本地计算机中使用,而不可用于网络间的通信。
3. 信号 (signal)
信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或几个进程之间传递异步信号。信号可以有各种异步事件产生,比如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。
4. 消息队列(Message queues)
消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用IPC标识符唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个独立的链表。 消息队列克服了信号承载信息量少,管道只能承载无格式字符流。
5. 信号量(Semaphore)
信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。 信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号) p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行 V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1 简单理解就是P相当于申请资源,V相当于释放资源
6. 套接字(socket)
套接字机制不但可以单机的不同进程通信,而且使得跨网机器间进程可以通信。 套接字的创建和使用与管道是有区别的,套接字明确地将客户端与服务器区分开来,可以实现多个客户端连到同一服务器。 服务器套接字连接过程描述: 首先,服务器应用程序用socket创建一个套接字,它是系统分配服务器进程的类似文件描述符的资源。 接着,服务器调用bind给套接字命名。这个名字是一个标示符,它允许linux将进入的针对特定端口的连接转到正确的服务器进程。 然后,系统调用listen函数开始接听,等待客户端连接。listen创建一个队列并将其用于存放来自客户端的进入连接。 当客户端调用connect请求连接时,服务器调用accept接受客户端连接,accept此时会创建一个新套接字,用于与这个客户端进行通信。 客户端套接字连接过程描述: 客户端首先调用socket创建一个未命名套接字,让后将服务器的命名套接字作为地址来调用connect与服务器建立连接。 只要双方连接建立成功,我们就可以像操作底层文件一样来操作socket套接字实现通信。