linux-ipc

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 解除映射
删除共享内存 使用 shmctlIPC_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:使用整数键值标识,可能出现键值冲突,需要小心管理。
    • POSIX:使用字符串名称标识,命名更加灵活,冲突概率较小。
  • 连接与断开
    • System V:使用 shmat 将共享内存段附加到进程地址空间,使用 shmdt 断开。
    • POSIX:使用 mmap 将共享内存对象映射到进程地址空间,使用 munmap 解除映射。
  • 删除共享内存
    • System V:需要使用 shmctlIPC_RMID 命令显式删除,否则共享内存段会一直存在于内核中,可能导致资源泄漏。
    • POSIX:使用 shm_unlink 删除共享内存对象,当所有打开的文件描述符关闭后,内存会自动释放。
  • 权限控制
    • System V:基于 UNIX 权限位(读、写、执行),权限管理较为原始,需要通过 shmctl 进行复杂的权限设置。
    • POSIX:基于文件系统权限,使用标准的 chmodfchmod 等函数,权限管理更灵活方便。
  • 命名空间
    • 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套接字实现通信。