进程间通信
-
进程的独立性–因为进程独立, 因此想要通信必须能够共同访问相同的媒介
-
进程间通信目的(数据传输,数据共享,进程间的访问控制
因为通信的目的不同,使用场景不同,因此操作系统提供了多种进程间通信方式:
- 管道 (命名管道/匿名管道)数据传输
- 共享内存 共享数据
- 消息队列 传输数据
- 信号量 进程间的访问控制
-
管道
进程间数据资源传输
原理:内核中的一块缓冲区
匿名管道:pipe
-
只能用于具有亲缘关系的进程间通信
/* 匿名管道的基本使用 * #include <unistd.h> * int pipe(int pipefd[2]); * pipefd 输出型参数 * pipefd[0] 用于从管道读数据 * pipefd[1] 用于向管道写数据 * 返回值:0 失败:-1 */ #include <stdio.h> #include <unistd.h> int main(){ int pipefd[2]; int ret = pipe(pipefd); if (ret < 0){ perror("pipe error!"); return -1; } pid_t pid = fork(); if (pid < 0){ return -1; } else if (pid == 0){ // 写入 int ret = write(pipefd[1], "hello", 5); printf("write %d\n", ret); close(pipefd[1]); } else { // 读取 sleep(3); char buf[1024] = {0}; int ret = read(pipefd[0], buf, 1023); printf("read: %d %s\n", ret, buf); close(pipefd[1]); } return 0; }
-
特性
-
读写特性:
如果管道没有数据,则read会阻塞,直到读到数据返回
如果管道中数据满了, 则write阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回
若管道所有读端被关闭,则写端会触发异常 SIGPIPE 导致进程退出
若管道所有写端被关闭, 则读端读完管道数据后返回0
-
管道自带同步与互斥特性
当读写大小小于PIPE_BUF时保证 操作原子性–操作不会被打断
-
管道是一个半双工通信(可选择方向的单向通信)
-
管道的生命周期跟随进程
-
-
-
命名管道
可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命令管道文件
因为其可见于文件系统,所以所有进程都能够通过打开管道文件,进而获取管道操作句柄,因此命名管道可以用于同一主机上任意通信
管道的原理依旧是内核中的缓存区, 只是通过文件向所有进程都提供了能够访问管道的方法
-
特性
若管道没有被以写打开, 只以只读打开则会阻塞,直到文件被以写的方式打开
若管道没有被以读的方式打开,只以只写打开则会阻塞, 直到文件被以读的方式打开
若管道以读写方式打开,则不会阻塞
-
管道提供流式服务
面向字节流数据传输 传输灵活,但是会造成数据粘连
-
实例:
// 读端 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> int main(){ char* file = "./pipe.fifo"; umask(0); int ret = mkfifo(file, 0664); if (ret < 0){ if (errno != EEXIST){ perror("mkfifo error!"); return -1; } } printf("open file!\n"); int fd = open(file, O_RDWR); if (fd < 0){ perror("open error!"); return -1; } printf("open success\n"); while(1){ char buf[1024] = {0}; int ret = read(fd, buf, 1023); if (ret > 0){ printf("read %s\n", buf); } else if (ret == 0) { printf("write closed \n"); } else { perror("read error"); } printf("-------\n"); } close(fd); return 0; } // 写端 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <string.h> int main(){ char* file = "./pipe.fifo"; umask(0); int ret = mkfifo(file, 0664); if (ret < 0){ if (errno != EEXIST){ perror("mkfile error"); return -1; } } printf("already open file\n"); int fd = open(file, O_WRONLY); if (fd < 0){ perror("open error"); return -1; } printf("open success!\n"); while(1){ char buf[1024] = {0}; scanf("%s", buf); write(fd, buf, strlen(buf)); } close(fd); return 0; }
-
-
共享内存
最快的进程间通信方式,因为相较于其它进程间通信方式(先将数据从用户态方式拷贝到内核态,用的时候,从内核态拷贝到用户态), 共享内存直接将一块内存映射到用户空间,用户可以直接通过地址对内存进行操作,并反馈到其它进程,少了两步数据拷贝的过程
-
通信原理
在Linux中, 每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
-
共享内存使用流程
流程 API 创建共享内存 shmget 将共享内存映射到虚拟地址空间 shmat 对共享内存进行基本的内存操作 memcpy 解除映射关系 shmdt 删除共享内存 shmctl -
共享内存的接口函数以及指令
- 查看系统中的共享存储段
ipcs -m
- 删除系统中的共享存储段
ipcrm -m [shmid]
- shmget ( ):创建共享内存
int shmget(key_t key, size_t size, int shmflg);
[参数key]:由ftok生成的key标识,标识系统的唯一IPC资源。 [参数size]:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页
是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
[参数shmflg]:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已
经存在的,可以使用IPC_CREAT或直接传0。
[返回值]:成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参
数。失败返回-1并设置错误码。
- shmat ( ):挂接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
[参数shmid]:共享存储段的标识符。 [参数*shmaddr]:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推
荐使用)。
[参数shmflg]:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连
接此段。
[返回值]:成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段
相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);出错返回-1。
-
shmdt ( ):去关联共享内存
当一个进程不需要共享内存的时候,就需要去关联。该函数并不删除所指定的共享内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程。
int shmdt(const void shmaddr);
[参数shmaddr]:连接以后返回的地址。 [返回值]:成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返
回-1。
- shmctl ( ):销毁共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
[参数shmid]:共享存储段标识符。 [参数cmd]:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存。
[参数*buf]:设置为NULL即可。
[返回值]:成功返回0,失败返回-1。
注意: 删除一块共享内存,并不会立即删除,而是判断映射连接数,若为0则删除,不为0则拒绝后续连接,直到为0删除
-
实例
// 写端 #include <sys/shm.h> #include <stdio.h> #include <unistd.h> #define IPC_KEY 0x12345678 #define PROJ_ID 12345 #define SHM_SIZE 4096 int main(){ int shmid; // 创建共享内存 shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666); if (shmid < 0){ perror("shmget error"); return -1; } // 映射共享内存 char* shm_start = (char*)shmat(shmid, NULL, 0); if (shm_start == (void*) - 1){ perror("shmat error"); return -1; } int i = 0; // 操作 while(1){ sprintf(shm_start, "i write something!", i++); sleep(1); } // 解除映射 shmdt(shm_start); // 删除共享内存 shmctl(shmid, IPC_RMID, NULL); return 0; } // 读端 #include <stdio.h> #include <unistd.h> #include <sys/shm.h> #define IPC_KEY 0x12345678 #define SHM_SIZE 4096 #define PROJ_ID 12345 int main(){ int shmid; // 创建共享内存 shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666); if (shmid < 0){ perror("shmget error!"); return -1; } // 映射虚拟地址空间 char* shm_start = (char*)shmat(shmid, NULL, 0); if (shm_start == (void*)- 1){ perror("shmat error"); return -1; } int i = 0; // 操作 while(1){ printf("%s\n", shm_start); sleep(1); } // 解除映射 shmdt(shm_start); // 删除虚拟空间 shmctl(shmid, IPC_RMID, NULL); return 0; }
-
消息队列
流程 API 创建 msgget 添加数据节点 msgsnd 获取数据节点 msgrcv 删除 msgctl 消息队列传输的是有类型的数据块,用户可以根据自己的需要选择性的获取某些类型的数据
-
信号量
内核中的计数器 具有等待队列(具有等待与唤醒的功能)
用于资源技术
若计数>0, 则表示有资源,则可以获取资源,然后计数-1
若计数<= 0, 则表示没有资源,没有资源则需等待
如果放置了资源,则计数+1, 并且唤醒等待的进程
实现进程间的同步与互斥
-