进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC的方式通常有管道(包括匿名管道和命名管道)、消息队列、信号量、共享内存等。
在学习之前我们需要了解以下内容:
1.因为进程具有独立性,如果两个或者多个进程需要相互通信,就必须要看到同一份资源:就是一段内存!这个内存可能以文件的方式提供,也可能以队列的方式,也可能提供的就是原始的内存块!
2.这个公共资源应该属于谁?这个公共资源肯定不属于任何进程,如果这个资源属于进程,这个资源就不应该再让其它进程看到,要不然进程的独立性怎么保证呢?所以这个资源只能属于操作系统!
3.进程间通信的前提:是有OS参与,提供一份所有通信进程能看到的公共资源!
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"。
管道是一个只能单向通信的通信管道,如果想要双向通信,那么就建立两个管道。
管道的本质是内核中的一块缓冲区
参数:是一个输出型参数,通过这个参数读取到两个打开的fd
返回值:若创建成功则返回0,否则返回-1
pipedf[0] 和 pipedf[1] 哪一个是读,哪一个是写呢?
0:读取端 1:写入端。 我们可以把0想象成嘴巴, 把1想象成笔,这样就很容易记住了
#include
#include
int main()
{
//pipefd[2]:是一个输出型参数!
int pipefd[2] = {0};
if(pipe(pipefd) != 0)
{
perror("pipe error!\n");
return -1;
}
//pipedf[0], pipedf[1] 哪一个是读,哪一个是写呢?
//0(嘴):读取端 1(笔):写入端
printf("pipefd[0]: %d\n", pipefd[0]);//打印3,因为0,1,2已经被占用
printf("pipefd[1]: %d\n", pipefd[1]);//打印4,因为0,1,2,3已经被占用
return 0;
}
看着样一段代码:
#include
#include
#include
#include
int main()
{
//pipefd[2]:是一个输出型参数!
int pipefd[2] = {0};
if(pipe(pipefd) != 0)
{
perror("pipe error!\n");
return -1;
}
//pipedf[0], pipedf[1] 哪一个是读,哪一个是写呢?
//0(嘴):读取端 1(笔):写入端
printf("pipefd[0]: %d\n", pipefd[0]);//打印3,因为0,1,2已经被占用
printf("pipefd[1]: %d\n", pipefd[1]);//打印4,因为0,1,2,4已经被占用
//我们让父进程读取,子进程写入
if(fork() == 0)
{
//子进程
close(pipefd[0]);//关闭读端,保留写端
const char* msg = "hello fl";
while(1)
{
write(pipefd[1], msg, strlen(msg));//strlen 不需要加1,因为'\0'是语言层面的结束标志符,不是系统的
sleep(1);
}
eixt(0);
}
//父进程
close(pipefd[1]);//关闭写端,保留读端
while(1)
{
//我们没有让父进程sleep
char buffer[64] = {0};
ssize_t s = read(pipefd[0], buffer, sizeof(buffer)-1);//zero indicates end of file, 如果read的返回值是0,意味着子进程关闭了文件描述符
if(s == 0)
{
printf("read quit...\n");
break;
}
else if(s > 0)
{
buffer[s] = 0;
printf("child say to father# %s\n", buffer);
}
else
{
printf("read error...\n");
break;
}
}
return 0;
}
上面这段代码的意思就是创建一个匿名管道,再创建一个子进程,让子进程每隔1秒就往管道里面写数据,父进程不挺从管道里面读数据
运行代码:
如果我们让子进程不要sleep,一直往管道里面写数据,父进程每隔一秒从管道里读取一次数据,buffer的大小还是64
运行代码后:
不是应该一句一句的读取吗?为什么一次读取这么多呢?
对于写端来说,pipe里面只要有缓冲区,就会一直写入。对于读端来说,缓冲区只要有数据,就会一直读取(每次都读63个字节)。
对于这种特性,就叫做字节流
如果子进程写入,而父进程就是不读
//子进程
int count = 0;
while(1)
{
write(pipefd[1], "a", 1);
count++;
printf("count: %d\n", count);//写一个字符(一字节),就打印一下
}
//父进程
while(1)
{
sleep(1);
}
运行代码:
最终我们发现写满了65536字节就不会写入了。65536字节也就是64KB,所以管道的大小是64KB。
当写端写满的时候,为什么不写了?
因为要让读端来读。此时可能会有人问,为什么要让读端来读?如果管道满了,不等读端读取的话,再写入,也就相当于把之前写入的数据给覆盖了,那对于写端来讲,之前做的工作就白做了。进程间通信也是为了相互合作的,写入的数据就应该给读端来读,这才是相互合作的表现。
//父进程
while(1)
{
sleep(5);
char c[64] = {0};
ssize_t s = read(pipefd[0], &c, 63);
c[s] = '\0';
printf("father take 63 byte\n");
}
如果我们让父进程一次读取63个字节,每次读完后就打印一次数据
运行代码:
虽然父进程一次读63个字节,但是子进程依旧没有写入
那就让父进程每次读取2KB
//父进程
while(1)
{
//我们没有让父进程sleep
sleep(5);
char c[1024*2+1] = {0};
ssize_t s = read(pipefd[0], &c, sizeof(c));
c[s] = '\0';
printf("father take 2KB \n");
}
运行代码:
我们发现每次读取4KB之后,写端才会写入。为什么会这样呢?原因就是要保证读写的原子性!
如果让子进程每隔4秒,写一个字符串
//子进程
while(1)
{
write(pipefd[1], msg, strlen(msg));
sleep(4);
}
//父进程
while(1)
{
char c[64] = {0};
ssize_t s = read(pipefd[0], &c, sizeof(c));
c[s] = '\0';
printf("father take %s\n", c);
}
运行代码:
虽然读端一直在读,但是写端每隔4秒才写入一次数据,就相当于读端一直在等写端写入数据
子进程写入一条消息后,间隔7秒就退出,再关闭写端的文件描述符,而父进程一直读取
//子进程
while(1)
{
write(pipefd[1], msg, strlen(msg));
sleep(7);
break;
}
close(pipefd[1]);
//父进程
while(1)
{
//我们没有让父进程sleep
//sleep(5);
char c[64] = {0};
ssize_t s = read(pipefd[0], &c, sizeof(c));
if(s > 0)
{
c[s] = '\0';
printf("father take %s\n", c);
}
else if(s == 0)
{
printf("writer quit...\n");
break;
}
else
break;
运行代码:
经过测试我们发现,写端(子进程)退出后,紧接着读端(父进程)也退出了。
如果子进程不断地写入数据,而父进程先sleep10秒,然后只读一次数据,就关闭读端
//子进程
const char* msg = "hello fl";
while(1)
{
write(pipefd[1], msg, strlen(msg));
}
//父进程
while(1)
{
sleep(10);
char c[64] = {0};
ssize_t s = read(pipefd[0], &c, sizeof(c));
break;
}
close(pipefd[0]);
我们可以用这段命令来监控一下while :; do ps axj | grep pipe_process | grep -v grep; sleep 1; echo "#################################";done
运行代码:
此时我们发现读端关闭,写端也会关闭。为什么呢?
当我们的读端关闭,写端还在写入,此时站在OS的层面,合理吗?
比如老师在讲课,学生都走了,都已经没人听老师讲课了
这种情况是严重不合理的,是没有任何价值的,站在OS层面,就是在浪费OS的资源,所以OS会直接终止写入进程!OS会给目标进程发送SIGPIPE信号
小总结:
匿名管道的4种情况
匿名管道的5个特点:
为了解决匿名管道只能具有血缘关系的进程进行通信,引入的命名管道。
命名管道与匿名管道的特点基本一致。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。
第一个参数是创建管道的名字
第二个参数是创建管道的权限
#include
#include
#include
#define MY_FIFO "./fifo"
int main()
{
if(mkfifo(MY_FIFO, 0666) < 0)
{
perror("mkfifo");
return 1;
}
return 0;
}
运行代码:
我们看到确实创建了一个管道,但是给的权限是666,创建出来的管道为什么是664呢?
其实原因就是创建的时候是要受系统umask的影响的,所以只需要在创建管道的时候,把umask设置为0即可。
一旦我们具有了一个命名管道,此时,我们只需要让通信双方按文件操作即可!
看以下代码:
comm.h
#include
#include
#include
#include
#include
#define MY_FIFO "./fifo"
server.c
#include "comm.h"
int main()
{
umask(0);
if(mkfifo(MY_FIFO, 0666) < 0)
{
perror("mkfifo");
return 1;
}
//只需要文件操作即可,语言层面的文件操作也行,但是可能会受到缓冲区的影响
int fd = open(MY_FIFO, O_RDONLY);
if(fd < 0)
{
perror("open");
return 2;
}
//业务逻辑,可以进行对应的读写
while(1)
{
char buffer[64] = {0};
ssize_t s = read(fd, buffer, sizeof(buffer)-1);
if(s > 0)
{
//success
buffer[s] = 0;
printf("client## %s\n", buffer);
}
else if(s == 0)
{
//perr close
printf("client quit...\n");
break;
}
else
{
//error
perror("read\n");
break;
}
}
close(fd);
return 0;
}
client.c
#include "comm.h"
#include
int main()
{
//如果管道已经存在,就不需要创建管道
int fd = open(MY_FIFO, O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
//业务逻辑
while(1)
{
printf("请输入# ");
fflush(stdout);
char buffer[64] = {0};
//先把数据从标准输入拿到我们的client进程内部
ssize_t s = read(0, buffer, sizeof(buffer)-1);//键盘输入的时候,/n也是输入字符的一部分
if(s > 0)
{
buffer[s-1] = 0;
printf("%s\n", buffer);
//拿到了数据
write(fd, buffer, strlen(buffer));
}
}
close(fd);
return 0;
}
运行代码:
一个server.c进程,一个client.c进程,两个进程没有任何关系,通过命名管道就能彼此通信
我们把server.c的代码稍作修改
if(s > 0)
{
//success
buffer[s] = 0;
if(strcmp(buffer, "show") == 0)
{
if(fork() == 0)
{
execl("usr/bin/ls", "ls", "-1", NULL);
exit(1);
}
waitpid(-1, NULL, 0);
}
else if(strcmp(buffer, "run") == 0)
{
if(fork() == 0)
{
execl("/usr/bin/sl", "sl", NULL);
exit(1);
}
waitpid(-1, NULL, 0);
}
else
printf("client# %s\n", buffer);
}
运行代码:
此时就通过cilent就能控制server的操作了
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
1.通过某种调用,在内存中创建一份内存空间!
2.通过某种调用,让进程"挂接"到这份新开辟的内存空降上!
通过上述这两步操作后,我们就可以使不同的进程看到了同一份资源,就能使不同的进程进行通信了,这种通信方案就叫做共享内存
对于shmflg,在这里只说两个IPC_CREAT 和 IPC_EXCL
IPC_CREAT:单独使用,或者shmflg为0,则表示如果不存在为key的共享内存,就会直接创建,如果存在了,则直接返回当前已经存在的共享内存(基本不会空手而归)。
IPC_EXCL:单独使用没有意义
IPC_CREAT | IPC_EXCL:如果不存在为key的共享内存,则创建。反之则报错。(意义:如果我调用成功,得到的一定是一个最新的,没有被别人使用过的共享内存!)
对于key:
相当于唯一标识符ID,需要用户自己填入。理论来讲,用户可以随便填什么值,具体是几并不重要,重要的是它和其他key不一样。但难免会填写的值与其他的key冲突,所以我们一般使用ftok()函数获取key
参数
返回值:如果是-1,则表示失败
server.c
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
printf("%u\n", key);
return 0;
}
此时就形成了一个key为1711342188的共享内存,此时这段代码在client.c中也要有一份,因为要保证两个进程看到的是同一份共享内存,才能进行后续的通信
再看这样一段代码:
comm.h
#pragma once
#include
#include
#include
#define PATH_NAME "./"
#define PROJ_ID 0X6666
#define SIZE 4097
server.c
#include "comm.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);
if(shmid < 0)
{
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
return 0;
}
运行代码:
第一次执行server之后,成功打印了key和shimd,我们发现shimd默认是从0开始的
除了第一次执行server之外,后面执行的server为什么会打印"shmget:file exists"呢?
此时说明共享内存已经被创建出来了。
通过指令ipcs -m
可以查看被创建出来的共享内存
通过这个例子我们可以知道,对于该进程曾经创建的共享内存在进程结束的时候被释放了吗?答案是没有。原因是:对于systemV的IPC资源,它不属于任何一个进程,生命周期是随内核的,如果程序员不显示的释放,那么内核存在多长时间,它就存在多长时间
key vs shimd
key:只是用来在系统层面上进行标识唯一性的,不能用来管理shm
shmid:是OS给用户返回的id,用来在用户层进行shm管理
buf的类型是一个描述共享内存的数据结构,我们可以看一看这个结构里面有什么
返回值:成功返回0;失败返回-1
comm.c
#pragma once
#include
#include
#include
#define PATH_NAME "./"
#define PROJ_ID 0X6666
#define SIZE 4097
server.c
#include "comm.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);
if(shmid < 0)
{
perror("shmget");
return 2;
}
printf("key: %u, shmid: %d\n", key, shmid);
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d->shm delete success\n", key, shmid);
sleep(5);
return 0;
}
运行代码:
返回值:成功返回一个指针,指向共享内存第一个字节,就是共享内存的起始地址(这个地址是虚拟地址);失败返回-1
注意:shmaddr为NULL,OS自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
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: %u, shmid: %d\n", key, shmid);
sleep(5);
char* mem = (char*)shmat(shmid, NULL, 0);
printf("attaches shm success\n");
sleep(5);
//这里就是我们的通信逻辑
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d->shm delete success\n", key, shmid);
return 0;
}
运行代码:
我们发现nattch(有多少个进程与之相关联)最终有0变为了1,表明已经将该进程挂接到了shmid为4的共享内存上了
shmaddr: 由shmat所返回的指针(共享内存的起始地址)
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
#include "comm.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: %u, shmid: %d\n", key, shmid);
sleep(5);
char* mem = (char*)shmat(shmid, NULL, 0);
printf("attaches shm success\n");
sleep(5);
//这里就是我们的通信逻辑
shmdt(mem);
printf("detaches shm success\n");
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d->shm delete success\n", key, shmid);
return 0;
}
运行代码:
我们发现nattch先是由0变为了1,再由1变为了0,说明该进程先挂接到了共享内存上,然后又取消了挂接
再看这样一段代码:
comm.h
#pragma once
#include
#include
#include
#include
#define PATH_NAME "./"
#define PROJ_ID 0X6666
#define SIZE 4097
client.c
#include "comm.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
printf("%u\n", key);
//client这里只需要获取共享内存即可,因为它在server端已经被创建了
int shmid = shmget(key, SIZE, IPC_CREAT);
if(shmid < 0)
{
perror("shmget");
return 1;
}
char* mem = (char*)shmat(shmid, NULL, 0);
sleep(5);
printf("client process attaches success!\n");
//这个地方就是我们要通信的区域
shmdt(mem);
sleep(5);
printf("client process detaches success!\n");
//client要不要删除共享内存呢? 不需要
return 0;
}
srerver.c
#include "comm.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: %u, shmid: %d\n", key, shmid);
sleep(1);
char* mem = (char*)shmat(shmid, NULL, 0);
printf("attaches shm success\n");
sleep(15);
//这里就是我们的通信逻辑
shmdt(mem);
printf("detaches shm success\n");
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
printf("key: 0x%x, shmid: %d->shm delete success\n", key, shmid);
return 0;
}
改代码的意思就是先让server端创建一个共享内存,自己再挂接到共享内存上,client端再挂接到共享内存上,然后client端取消挂接,server端再取消挂接。我们将看到nattch的变化过程为0->1->2->1->0
重点:
1.共享内存一旦建立好并映射进自己的进程地址空间,该进程就可以直接看到该共享内存,就如同malloc的空间一样,不需要任何系统调用接口
2.共享内存不提供任何同步或者互斥机制,需要程序员自行保证数据的安全性
3.共享内存是进程之间通信最快的方式,因为它省略了若干次数据拷贝,例如从用户到内核和内核到用户
消息队列是消息的链表,是存放在内核中并由消息队列标识符标识。因此是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区受限等特点。
允许不同进程将格式化的数据流以消息队列形式发送给任意进程,对消息队列具有操作权限的进程都可以使用msgget完成对消息队列的操作控制,通过使用消息类型,进程可以按顺序读信息,或为消息安排优先级顺序。
与信号量相比,都以内核对象确保多进程访问同一消息队列。但消息队列发送实际数据,信号量进行进程同步控制。
与管道相比,管道发送的数据没有类型,读取数据端无差别从管道中按照前后顺序读取;消息队列有类型,读端可以根据数据类型读取特定的数据。
操作:创建或获取消息队列, int msgget((key_tkey, int flag);//若存在获取,否则创建它
发送消息:int msgsnd(int msgid, void *ptr, size_t size, int flag); ptr指向一个结构体存放类型和数据 size 数据的大小
接受消息:int msgrcv(int msgid, void *ptr, size_t size, long type, int flag);
删除消息队列: int msgctl(int msgid, int cmd, struct msgid_ds*buff);
管道,共享内存,消息队列,它们都是以传输数据为目的的!
信号量不是以传输数据为目的!它是通过共享"资源"的方式,来达到多个进程的同步和互斥的目的!
信号量的本质:是一个计数器,类似int count。衡量临界资源中的资源数
1.什么是临界资源?
凡是被多个执行流同时能够访问的资源就是临界资源,例如进程间通信时,使用的管道、共享内存、消息队列都是临界资源。凡是需要进程通信,必定要引入多个进程看到的资源(通信需要),同时,也引入了一个新的问题,那就是临界资源
2.什么是临界区?
用来访问临街资源的代码就叫做临界区
3.什么是原子性?
任何情况下不能被打断的操作,也就是说要么都执行,要么不执行,没有中间态
4.什么是互斥?
在任意一个时刻,只能允许一个执行流进入临界资源,执行它自己的临界区
信号量的先关操作:
int semget((key_t)key, int nsems, int flag);//创建或获取信号量
int semop(int semid, stuct sembuf*buf, int length);//加一操作(V操作):释放资源;减一操作(P操作):获取资源
int semct(int semid, int pos, int cmd);//初始化和删除