进程间可能会存在特定的协同工作的场景,即一个进程要把自己的数据交付给另一个进程,让其处理,这就是进程间通信。因为在进程之间,所以就需要OS来设计通信方式。
因为进程具有独立性,交互数据成本很高。一个进程看不到另一个进程的资源,所以就必须有一份公共资源,能够被多个进程看到,通过它进行通信。
进程间通信的前提和本质:其实是由OS参与,提供一份所有通信进程能看到的公共资源。 提供的方式可能以文件方式提供,也可能以队列的方式,也可能提供的就是原始的内存块。这也就是通信方式有很多种的原因!
下面先来介绍一下最基础的文件方式的通信,管道。
一、管道
管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道” 比如常用的命令: 管道通信分为两种:匿名管道和命名管道
1.1 匿名管道
1.1.1 匿名管道的原理
创建子进程的时候会继承父进程struct files_struct (管理文件的结构体)里面有文件描述,利用这个特性,父进程分别以读和写打开一个文件,这样就有两个文件描述符分别可以读和写,然后创建子进程,子进程写,父进程读,这样就实现了两个进程的通信。
我们知道在OS里面有文件的内核缓冲区,由OS控制,这就是一块公共资源,可被进程共享,这个就是管道。
OS提供一个系统调用接口,用来创建这样一个缓冲区。 其中pipefd[2]是一个输出型参数,它存放的就是读和写的文件描述符。 成功返回0, 失败返回-1。
1.1.2 匿名管道的实现
先来看一下输出型参数的值 不出意外的是3和4,0,1,2已被占用。规定pipeid[0]是读的文件描述符,pipeid[1]是写的文件描述符。
成功读到了子进程写的内容。
内核缓冲器有没有大小呢?是多大呢? 写个程序来验证一下。
我们可以让子进程不断的写,每写一次打印一次,看最后写到多少停止,父进程不读。
可以看到写了65536个字符,也即65536字节,这是216,也就是64KB大小。所以管道是有大小的,但是写满了它也没有退出程序,而是在等另一个进程来读。 通过man 7 pipe 也可以看到在Linux下该缓存区的大小。
当我们的读端关闭,写端还在写入,此时站在OS的层面,严重不合理。本质就是在浪费OS的资源,OS会直接终止写入进程! 此时OS给目标进程发送信号SIGPIPE
1.1.3 匿名管道总结
可总结为4种情况,5个特点
4种情况:
- 读端不读或者读的慢,写端要等读端
- 读端关闭,写端收到SIGPIPE信号直接终止
- 写端不写或者写的慢,读端要等写端
- 写端关闭,读端读完pipe内部的数据然后再读,会读到0,表明读到文件结尾!
5个特点:
- 管道是一个只能单向通信的通信信道,即只能一个读一个写,否则会产生冲突
- 管道是面向字节流的,按字节读写。
- 仅限于父子通信—待补充具有血缘关系的进程进行进程间通信管道自带同步机制原子性写入
- 管道的生命周期是随进程的! 管道本质是文件,由进程创建,在OS内核中,相关进程退出后,文件的引用计数减1,当减为0时即吗,没有进程使用文件,文件关闭。
1.2 命名管道
1.2.1 认识命名管道(name pipes)
为什么区分为匿名和命名呢?
- 因为在上面匿名管道,是子进程继承父进程的,我们不需要知道名字。而且只能在父子这类有关系的进程中通信。
- 命名管道就是,自己可以创建一个文件管道,有专属的名字,而且可以实现不相关的两个进程通信。
Linux下就有一个创建命名管道的命令mkfifo
1.2.2 命名管道的实现
命名管道为什么又称做fifo 呢? 因为管道面向字节流,本来就是先进先出。
实现两个进程通信的前提是,让两个进程看到同一份资源。命名管道是怎么解决的呢? 它是通过一个头文件,里面包含一个文件地址,文件地址具有唯一性,然后不同的进程分别引用这个头文件,就可以看到同一个文件了,实现通信。
先写一个头文件,存放需要的库头文件,最主要的是存放一个管道文件,这样才能实现通信。 再定义一个服务端 int mkfifo(const char *filename,mode_t mode); 在程序内部创建管道文件。 成功返回0, 失败返回-1
命名管道的使用很简单,创建对应管道后,只需要向对文件一样对管道即可实现通信
comm.h文件
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#define MY_FIFO "./fifo"
server.c文件 创建管道并读取管道内容
#include "comm.h"
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
umask(0);
if(mkfifo(MY_FIFO , 0664) < 0){
perror("mkfifo");
return 1;
}
int fd = open(MY_FIFO , O_RDONLY );
if(fd < 0){
perror("open");
return 2;
}
while(1){
char buffer[128] = {0};
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if(s > 0) {
buffer[s-1] = 0;
if(strcmp(buffer, "ls") == 0){
if(fork() == 0) {
execl("/usr/bin/ls","ls",NULL);
}
waitpid(-1, NULL, 0);
} else if(strcmp(buffer, "ll") == 0){
if(fork() == 0){
execl("/usr/bin/ls","ls", "-l",NULL);
}
waitpid(-1, NULL, 0);
} else {
printf("client # %s\n", buffer);
}
} else if (s == 0){
printf("client quit ...\n");
break;
} else {
perror("read");
break;
}
}
close(fd);
return 0;
}
客户端, 打开管道并写入信息
#include "comm.h"
int main()
{
int fd = open(MY_FIFO , O_WRONLY);
if(fd < 0){
perror("open");
return 2;
}
while(1){
printf("请输入# ");
fflush(stdout);
char buffer[128] = {0};
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if(s > 0){
buffer[s-1] = 0;
write(fd, buffer, s);
}
}
close(fd);
return 0;
}
二、System V
System V标准下,有三种利用内存通信的方式
2.1 共享内存
共享内存也是进程通信常用的方式。进程能够通信的本质就是能够看到一块公共资源,管道通信是文件资源,共享内存是内存资源。
2.1.1 共享内存使用步骤
共享内存是在内存中开辟一段空间,然后让不同的进程用一定的标准与这个内存建立映射,然后即可通信。
- 通过某种调用,在内存创建一份空间
- 通过某种调用,让参与通信的进程 “挂接” 到这份新开辟的内存空间上。
- 去关联(去挂接)
- 释放共享内存
共享内存在内存中可能存在多份,系统也为此产生了管理的数据结构。 为了保证不同进程看到的是同一份共享内存,每个共享内存都有属于自己的唯一标识符。
常用的共享内存相关指令 查看当前有的共享内存ipcs -m 删除指定shmid的共享内存ipcrm -m shmid
共享内存不会自动释放需要程序员自己释放,若程序内释放函数没有执行到,内存还存在,下次以同样的key创建会显示文件已存在。
2.1.2 共享内存相关函数
对于上面的步骤,有对应的函数接口。
首先需要一个唯一标识共享内存的标识符,可以用ftok 函数获得一个系统用于标识key值。
函数作用:将路径名和项目标识符转换为SystemV IPC密钥。 指定路径名,加上给定的项目ID,用指定的算法可以获得唯一的Key值,作为系统识别的标识符。 返回值:该key值。
创建共享内存
功能:用来创建共享内存 参数 : key: 这个共享内存段名字 size: 共享内存大小 (建议为4096Byte的整数倍) shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1 该返回值是作为用户层面标识的标识符。
shmflg参数
如果单独使用IPC_CREAT,或者flg为0:创建一个共享内存,如果创建的共享内存已经存在,则直接返回当前已经存在的共享内存。不存在则创建。(基本不会空手而归) IPC_EXCL(单独使用没有意义) IPC_CREAT | IPC_EXCL:如果不存在共享内存,则创建之。如果已经有了共享内存,则返回出错!(意义:如果我调用成功,得到的一定是一个最新的,没有没别人使用的共享内存!)
功能:将共享内存段连接到进程地址空间 参数 shmid: 共享内存标识 shmaddr: 指定连接的地址 shmflg: 它的两个可能取值是SHM_RND和SHM_RDONLY 返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
参数说明
shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离 原型 参数 shmaddr: 由shmat所返回的指针 返回值:成功返回0;失败返回-1 注意:将共享内存段与当前进程脱离不等于删除共享内存段
功能:用于控制共享内存 参数 shmid:由shmget返回的共享内存标识码 cmd:将要采取的动作(有三个可取值) buf:指向一个保存着共享内存的模式状态和访问权限的数据结构 返回值:成功返回0;失败返回-1
cmd的取值 工PC_STAT:把shmid ds结构中的数据设置为共享内存的当前关联值 IPC_SET:在进程有足够权限的前提下,把共享内存的当前天联值攻直为shmid ds数据结构中给出的值 IPC_RMID:删除共享内存段
2.1.3 共享内存创建
同命名通道,需要创建一个头文件,两个源文件进行通信。
头文件包含用于创建key值的路径和项目ID,以及共享内存的大小,这样就可以识别相同的共享内存。
服务端创建共享内存。
#include "comm.h"
#include <unistd.h>
int main()
{
key_t key = ftok(PATH__NAME, PROJ_ID);
if(key < 0){
perror("ftok");
return 1;
}
int shmid = shmget(key, SIZE, IPC_CREAT|IPC_EXCL|0666);
if(shmid < 0) {
perror("shmget");
return 2;
}
printf("key: 0x%x, shmid: %d\n", key, shmid);
char *mem = (char*)shmat(shmid, NULL, 0);
printf("attaches shm success\n");
int cnt = 9;
while(cnt--){
sleep(1);
printf("%s\n", mem);
}
shmdt(mem);
printf("detaches shm success\n");
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d -> shm delete success\n", key,shmid);
return 0;
}
客户端
#include "comm.h"
#include <unistd.h>
int main()
{
key_t key = ftok(PATH__NAME, PROJ_ID);
if(key < 0) {
perror("ftok");
return 1;
}
printf("key:0x%x\n", key);
int shmid = shmget(key, SIZE, 0);
if(shmid < 0){
perror("shmget");
return 1;
}
char* mem = (char*)shmat(shmid, NULL, 0);
printf("client process attachs success\n");
char i;
for(i = 'A'; i <= 'H'; i ++ ){
mem[i - 'A'] = i;
sleep(1);
}
shmdt(mem);
printf("client process detaches success\n");
return 0;
}
2.2 了解信号量
上面共享内存的shmid是0,1,2,3 …这样生成的,这样数组下标。所有的SystemV标准的IPC资源是通过一个数组织起来的。
进程通信相关知识
- 什么是临界资源:凡是核多个执行流同时能够访何的资源就是临界资源! 例如:同时向显示器打印。进程间通信的时候,管道,共享内在,消息队列等都是临界资源
- 什么是临界区:进程中用来访问临界资源的代码,就叫做临界区
- 什么是原子性:一件事情要么不做,要做就做完,没有中间态,就叫做原子性!
- 什么是互斥:在任意一个时刻,只能允许一个执行流进入临界资源,执行他自己的临界区
什么是信号量 匿名/命名管道,共享内存,消息队列:都是以传输数据为目的的! 信号量不是以传输数据为目的的!通过共享“资源”的方式,来达到多个进程的同步和互斥的目的。 信号量的本质,是一个计数器,是用来衡量临界资源中资源数目的。
凡是要进程间通信,必定要引入被多个进程看到的资源(通信需要),同时,也造就了引入一个新的问题,临界资源的问题。
临界资源,可以被任何进程访问,信号量就是用来衡量资源数目,如果资源全被占用或被预定了,其他想要资源的进程就只有等待了,也避免了使用临界资源的冲突。
但是信号量要被不同的进程感知到,那么它本身也是一种临界资源,它如何保证自己的安全,不被使用呢?就是保持自己的原子性。
|