2022/6/5
2022/6/8 更新共享内存
2022/7/1 更新多线程
文章目录
前言
今天在看野火的Linux基础与应用开发实战指南时 产生了一个Posix IPC信号量和System-V IPC信号量的问题 ,找了不少资料,于是就有了下文。
补充资料:
IPC(Inter-Process Communication,进程间通信),包括消息队列(msg)、信号量(sem)、共享内存(shm)。
转载自:https://www.cnblogs.com/Zoran-/p/5819256.html https://blog.csdn.net/paradox_1_0/article/details/105739690
一、Posix IPC和System-V IPC简介
-
System-V IPC 接口:来自较早的Unix操作系统,是Unix众多分支中的一员,他有很多经典的用例,例如 ”SysV 初始化脚本“ (/etc/init.d),笔者之前用过这个脚本设置过韦东山的imx6ull上电自动连接WiFi功能。
-
Posix IPC 接口:则是来自IEEE所开发的一簇标准,基于Unix的经验和实践所实现的一堆调用服务接口,致力于在多种操作系统之间源代码级别移植。
-
总结:Posix IPC和System-V IPC都是应用于系统级的接口,不仅适用于多进程间通信,也适用于多线程间通信。Posix相对于System-V可以说是比较新的标准,语法相对简单。
二.应用场景
- System-V IPC 因为使用年代久远,因此有许多系统支持,使用得广泛,但由于没有固定的标准,所以不同操作系统System V存在一些差异。
- 有小部分操作系统没有实现POSIX标准,但POSIX的可移植性,必然是后续的发展趋势。
不同通信方式两者的优缺点:
信号量
- POSIX在无竞争条件下是不会陷入内核的,而System-V IPC则是无论何时都要陷入内核,因此性能稍差。
- POSIX的sem_wait函数成功获取信号量后,进程如果意外终止,将无法释放信号量,而System V则提供了SEM_UNDO选项来解决这个问题。因此,相比而言,后者更加可靠。
消息队列
(有待考究)进程间的消息传递和同步上,似乎POSIX用得较普遍。
共享内存
POSIX实现尚未完善,system V仍为主流。
2022/6/5勘误
上述画线部分的原话来自Unix网络编程第二卷,
实际Linux 内核从 **2.4** 起开始就支持Posix共享内存。
更何况现在Linux内核版本已经来到5.19(雾)。
在多线程与多进程的用途
- 多线程中使用的基本是Posix IPC,原因是因为System-V IPC容易陷入内核,于线程所追求轻量级化的目的相反。
- 多进程则多是基于System-V IPC。
三、Posix和System-V消息队列编程
四、Posix和System-V信号量编程
五、Posix和System-V共享内存编程
https://blog.csdn.net/modi000/article/details/125040478
-
补充知识:内存映射(mmap)
在读写一个普通文件的时候,我们通常会用到Linux的系统调用open()、write()、read(),有时也会用到C标准库所带的fwrite()、fread()、fflush()等。
而Linux在执行系统调用时,需要从用户态切换到内核态,执行完毕后再返回用户代码,这会浪费大量时间用于切换状态。
虽说C标准库所带的这几个库函数自带缓冲区,但本质上也是调用了Linux的系统调用,即write()等,那就不可避免会影响系统性能。因此,(重点来了)Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改,mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以向访问内存的方式对文件进行访问,不需要其他系统调用(read,write)去操作。
内存映射机制mmap是POSIX标准的系统调用,有匿名映射和文件映射两种。匿名映射用进程的虚拟内存空间,它和malloc(3)相似,实际上有些malloc实现会用mmap匿名映射分配内存,不过匿名映射不是POSIX标准中规定的。
文件映射有MAP_PRIVATE和MAP_SHARED两种。前者用COW(写时复制)的方式,把文件映射到当前的进程空间,修改操作不会改动源文件。后者直接把文件映射到当前的进程空间,所有的修改会直接反应到文件的page cache,而后由内核自动同步到映射文件上。
笔者一开始接触到这个内存映射的概念 也是一脸懵(大雾),暂且先放着 介绍我们的主角共享内存。
共享内存作为IPC效率最高的一种,具有许多好处(略),但需要注意的是,共享内存本身是没有同步、互斥机制的,如果没有处理好相应的关系,会导致牛头不对马嘴。这就需要我们在编写相关代码时,要万分注意。而现代Linux有两种共享内存机制:
- POSIX共享内存 (shm_open()、shm_unlink())
- System-V共享内存 (shmget()、shmat()、shmdt())
其中,System-V共享内存历史悠久,一般的UNIX系统上都有这套机制;
而POSIX共享内存机制接口更加方便易使用,一般是结合内存映射mmap结合使用。而mmap和System-V共享内存的主要区别在于:
- System-V是持久化的,除非被一个进程明确的删除,否则它始终存在于内存里,直到系统关机;
- mmap映射的内存在不是持久化的,假如进程关闭,映射随即失效,除非事前已经映射到了一个文件上。
介绍完毕,开始简单的编程。
1.父子进程共享内存
首先考虑简单的共享内存场景,父子进程共享内存。
这种情况通常不需要用到上述介绍的两种机制,因为父进程通过fork()函数产生子进程后,虽然各有各的共享内存区,但实际上fork会对内存映射文件进行特殊处理(虚拟内存),所以一般通过内存映射文件的方式在父子进程间共享内存区。
使用该方法有几个地方需要注意
- 父进程在调用mmap函数的使用需要指定MAP_SHARED标记。
- 父子进程读写共享内存需要使用某种方式同步
- 通常可以使用匿名内存映射的方式来简化父子进程共享内存区的使用。
- BSD提供了匿名内存映射标志MAP_ANON,SVR4提供/dev/zero设备可以用于匿名内存映射。
https://www.freesion.com/article/7292862090/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
void *shm_mmap_alloc(int size) //父子进程映射虚拟内存。
{
void *addr = NULL;
/* void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); */
addr = mmap(NULL,size,PROT_READ|PROT_WRITE,MAP_ANON|MAP_SHARED,-1,0);
if(addr == MAP_FAILED)
{
return NULL;
}
return addr;
}
int shm_mmap_free(void *addr,int size)
{
printf("free!\r\n");
return munmap(addr,size);
}
int main(int argc,const char *argv[])
{
pid_t pid;
sem_t *sem;
int running;
//POSIX 信号量
const char *sem_name = "my_sem";
char *addr = NULL;
//申请内存 一页=4096 Byte
addr = (char *)shm_mmap_alloc(1024);
if((pid = fork()) == -1)
{
perror("Fail to fork");
exit(-1);
}
if(pid == 0)
{
//打开信号量,否则创建一个新的。
sem = sem_open(sem_name,O_CREAT,0666,0);
if(sem == SEM_FAILED)
{
//careful
printf("POSIX SEM ERROR!\r\n");
//释放信号量。
sem_unlink(sem_name);
exit(-1);
}
while(running)
{
//等待信号量到达
if(sem_wait(sem) == 0)
{
printf("You wrote:%s\r\n",(char *)addr);
if(strncmp((char *)addr,"exit",4) == 0)
{
running = 0;
}
}
}
exit(EXIT_SUCCESS);
}
else
{
sem = sem_open(sem_name,O_CREAT,0666,0);
if(sem == SEM_FAILED)
{
printf("POSIX SEM ERROR!\r\n");
sem_unlink(sem_name);
exit(-1);
}
while(running)
{
fgets(addr,1024,stdin);
sem_post(sem);
if(strncmp(addr,"exit",4) == 0)
{
running = 0;
}
}
sem_close(sem);
sem_unlink(sem_name);
//内存映射释放
shm_mmap_free(addr,1024);
exit(EXIT_SUCCESS);
}
return 0;
}
效果图如下所示:(本代码所使用的makefile已经贴在了最底下 请自行查阅~)
2.POSIX共享内存
在POSIX中,共享内存可以用于没有亲缘关系的进程之间共享内存。挂载于 /dev/shm 目录下的专用 tmpfs 文件系统,这个文件系统具有内核持久性——它所包含的共享内存对象会一直持久,即使当前不存在任何进程打开它,但这些对象会在系统关闭后丢失。主要有以下两种方式
- 1.内存映射文件
通过将磁盘的文件映射成内存,实现效果与上文父子进程相似,但需要注意的是,上面的实现使用的是内存映射中的匿名映射,若要在POSIX实现,就得更改mmap函数的参数。 - 2.共享内存区对象
使用shm_open()函数打开一个POSIX共享内存区对象,返回的描述符再由mmap函数映射到当前进程的地址空间。传递给shm_open的名字随后由希望共享该内存区的任何其他进程使用。
相关函数:
- shm_open,创建或打开共享内存区
- shm_unlink,从系统中删除共享内存区对象
- ftruncate,设置共享内存区对象的大小
- fstat,获取共享内存区对象的相关信息
- getopt.h 用于解析命令行输入的参数。
POSIX share mem
// sudo gcc shm_create.c -o shm_create -lrt
//-lrt 编译过程需要链接lrt
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <getopt.h>
#include <fcntl.h>
#include <string.h>
static void usageError(char *error)
{
fprintf(stderr,"error in %s\n",error);
exit(EXIT_FAILURE);
}
int main(int argc,char* argv[])
{
int flags,fd;
//文件状态结构体
struct stat sb;
//
mode_t perms;
//size_t size;
long long int size;
void *addr;
flags = O_RDWR|O_CREAT;
size = 1000;
perms = S_IRUSR | S_IWUSR;
fd = shm_open("/shm_demo",flags,perms);
if(fd == -1)
exit(EXIT_FAILURE);
//设置文件空间大小
if(ftruncate(fd,size) == -1)
exit(EXIT_FAILURE);
//地址映射函数
addr = mmap(NULL,size,PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
exit(EXIT_FAILURE);
//write
memcpy(addr,"hello!\r\n",strlen("hello!\r\n"));
//read
//get file status
if(fstat(fd,&sb) == -1)
{
usageError("fstat");
}
if(sb.st_size > 0)
{
write(STDOUT_FILENO, addr, sb.st_size);
}
close(fd);
if(shm_unlink(argv[1]) == -1)
exit(EXIT_FAILURE);
exit(EXIT_SUCCESS);
}
本文所使用的makefile如下:
#对应平台
ARCH ?= x86
#编译所产生的文件夹
BUILD_DIR = build_$(ARCH)
#输出的文件
TARGET = demo
PTHREAD ?= -lpthread
#源文件头文件均为根目录
SRC_DIR = .
INC_DIR = .
SRCS = $(foreach dir,$(SRC_DIR),$(wildcard $(dir)/*.c))
DEPS = $(foreach dir,$(INC_DIR),$(wildcard $(dir)/*.h))
CFLAGS = $(patsubst %,-Wall -I %,$(INC_DIR))
OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SRCS)))
#编译平台 ARM or x86
ifeq ($(ARCH),x86)
cc=gcc
else
cc = arm-linux-gnueabihf-gcc
endif
$(BUILD_DIR)/$(TARGET):$(OBJS)
$(cc) $^ -o $@ $(CFLAGS) $(PTHREAD)
$(BUILD_DIR)/%.o:$(SRC_DIR)/%.c $(DEPS)
@mkdir -p $(BUILD_DIR)
$(cc) $< -c -o $@ $(CFLAGS) $(PTHREAD)
.PHONY:clean
clean:
rm -rf $(BUILD_DIR)
cleanall:
rm -rf build_*