进程间通信基本介绍
进程通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程通信的方式
1.匿名管道和命名管道
2.System V进程间通信(消息队列, 共享内存, 信号量)
3.POSIX IPC进程间通信(消息队列,共享内存,信号量,互斥量,条件变量,读写锁)
这篇博客主要对基础的进程通信方式进行讲解举例
进程间匿名管道通信
该方式主要利用了子进程继承了父进程储存fd数组的结构体,这样父子进程就可以同时看到相同的文件(公共资源),为进程间通信提供了条件
管道通信的性质:
1.管道是一个只能进行单向通信的方法
2.管道是面向字节流的
3.匿名管道仅限于父子进程间通信
4.管道自带同步机制 并且采用原子性写入
这里只要对性质留一点印象就可以了,后文会对每个性质做实验来具体描述这些性质
系统接口介绍
NAME
pipe, pipe2 - create pipe
SYNOPSIS
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd[2]是一个输出性参数,我们可以通过这个参数打开两个fd
若成功返回0,失败返回-1
pipe接口基本使用示例
[clx@VM-20-6-centos proc_pipe]$ ll
total 20
-rw-rw-r-- 1 clx clx 69 Oct 22 10:22 Makefile
-rwxrwxr-x 1 clx clx 8464 Oct 22 10:25 test
-rw-rw-r-- 1 clx clx 269 Oct 22 10:25 test.c
[clx@VM-20-6-centos proc_pipe]$ cat test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0){
perror("pipe failed\n");
return 1;
}
printf("pipefd[0] = %d\n", pipefd[0]);
printf("pipefd[1] = %d\n", pipefd[1]);
return 0;
}
//编译运行程序
[clx@VM-20-6-centos proc_pipe]$ ./test
pipefd[0] = 3
pipefd[1] = 4
可以看到pipefd[2] 数组经过pipe处理后,数组两个元素都被赋予了一个值,这两个值分别对应一个fd(文件描述符),因为0,1,2被标准输入/输出/错误占用,所以fd由3开始分配,这两个文件形成一个管道,供我们传输数据。
piepfd[0]对应文件的读取端
pipefd[1]对应文件的写入端
管道的通信原理结结构示意图
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
//情况1:写端关闭,读端读完pipe内数据后退出,read返回0表示读到结尾
//发送十条mag关闭写入端,读取端一直读
void test1()
{
int pipefd[2] = {0};
//pipe 成功返回0 失败返回-1
if (pipe(pipefd) != 0){
perror("pipe failed\n");
exit(1);
}
if (fork() == 0){
//child 进程
close(pipefd[0]); //关闭读取端
int count = 0;
const char* msg = "hello pipe\n";
while (count++ < 10){
write(pipefd[1], msg, strlen(msg)); // 向管道中打印十条信息
}
close(pipefd[1]);//关闭写入端
printf("chile end\n");
exit(0);
}
//father 进程
close(pipefd[1]); //关闭写入端
while (1){
sleep(1);
char buffer[64] = {0};
size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); //将文件读取到buffer中并打印,一次读取s个字节
if (s == 0){
printf("file end\n");
break;
}
else if (s > 0){
printf("child say: %s", buffer);
}
else {
break; //程序出错
}
}
int status = 0;
waitpid(-1, &status, 0); //父进程夯等待子进程
printf("child exit code = %d, exit single = %d\n", (status >> 8) & 0xff, status & 0x7f); //输出子进程的退出码和得到的信号
}
//现象:读取端虽然读的慢,但是将文件读完结束后退出
//情况2:读端关闭,写端收到SIGPIPE信号被杀死
//写端一直写,读端读了一下关闭了
void test2()
{
int pipefd[2] = {0};
//pipe 成功返回0 失败返回-1
if (pipe(pipefd) != 0){
perror("pipe failed\n");
exit(1);
}
if (fork() == 0){
//child 进程
close(pipefd[0]); //关闭读取端
int count = 0;
const char* msg = "hello pipe\n";
while (++count){
sleep(1);
write(pipefd[1], msg, strlen(msg));// 每个一秒向管道中输入一次msg
}
printf("chile end\n");
exit(0);
}
//father 进程
close(pipefd[1]); //关闭写入端
while (1){
char buffer[64] = {0};
size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); //将文件读取到buffer中并打印,一次读取s个字节
if (s == 0){
printf("file end\n");
break;
}
else if (s > 0){
printf("child say: %s", buffer);
close(pipefd[0]);//读一次后,关闭读端并退出循环
break;
}
else {
break; //程序出错
}
}
int status = 0;
waitpid(-1, &status, 0); //父进程夯等待子进程
printf("child exit code = %d, exit single = %d\n", (status >> 8) & 0xff, status & 0x7f); //输出子进程的退出码和得到的信号
}
//现象:读端读一次退出后,写端(子进程)被13信号杀死
//情况3:写端写的慢,读端需要等写端
void test3()
{
int pipefd[2] = {0};
//pipe 成功返回0 失败返回-1
if (pipe(pipefd) != 0){
perror("pipe failed\n");
exit(1);
}
if (fork() == 0){
//child 进程
close(pipefd[0]); //关闭读取端
int count = 0;
const char* msg = "hello pipe\n";
while (++count){
sleep(3);
write(pipefd[1], msg, strlen(msg)); // 向管道中打印十条信息
}
close(pipefd[1]);//关闭写入端
printf("chile end\n");
exit(0);
}
//father 进程
close(pipefd[1]); //关闭写入端
while (1){
char buffer[64] = {0};
//写端写的慢,读端进行读取操作也会进入等待
size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); //将文件读取到buffer中并打印,一次读取s个字节
if (s == 0){
printf("file end\n");
break;
}
else if (s > 0){
printf("child say: %s", buffer);
}
else {
break; //程序出错
}
}
int status = 0;
waitpid(-1, &status, 0); //父进程夯等待子进程
printf("child exit code = %d, exit single = %d\n", (status >> 8) & 0xff, status & 0x7f); //输出子进程的退出码和得到的信号
}
//情况4:读端读的慢,写端也需要等读端
void test4()
{
int pipefd[2] = {0};
//pipe 成功返回0 失败返回-1
if (pipe(pipefd) != 0){
perror("pipe failed\n");
exit(1);
}
if (fork() == 0){
//child 进程
close(pipefd[0]); //关闭读取端
int count = 0;
const char* msg = "hello pipe\n";
while (++count){
write(pipefd[1], msg, strlen(msg));
printf("%d\n", count);
}
printf("chile end\n");
exit(0);
}
//father 进程
close(pipefd[1]); //关闭写入端
while (1){
sleep(1);
char buffer[64] = {0};
size_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); //将文件读取到buffer中并打印,一次读取s个字节
if (s == 0){
printf("file end\n");
break;
}
else if (s > 0){
printf("child say: %s", buffer);
}
else {
break; //程序出错
}
}
int status = 0;
waitpid(-1, &status, 0); //父进程夯等待子进程
printf("child exit code = %d, exit single = %d\n", (status >> 8) & 0xff, status & 0x7f); //输出子进程的退出码和得到的信号
}
int main()
{
test1();
//test2();
//test3();
//test4();
return 0;
}
情况1:写端关闭,读端读完pipe内数据后退出,read返回0表示读到结尾
情况2:读端关闭,写端收到SIGPIPE信号被杀死
情况3:写端写的慢,读端需要等写端
情况4:读端读的慢,写端也需要等读端
各个情况的现象
情况1
[clx@VM-20-6-centos pipe_blog]$ ./test
chile end
child say: hello pipe
hello pipe
hello pipe
hello pipe
hello pipe
hello pichild say: pe
hello pipe
hello pipe
hello pipe
hello pipe
file end
child exit code = 0, exit single = 0
现象解析:子进程快速打完十条msg后退出,读端(父进程)慢慢的将管道中的数据读完后退出
情况2
[clx@VM-20-6-centos pipe_blog]$ ./test
child say: hello pipe
child exit code = 0, exit single = 13
[clx@VM-20-6-centos pipe_blog]$
现象解析:写端不断打印,但是读端只读一次后就关闭管道,操作系统直接向写端发送13号信号杀死进程
情况3
[clx@VM-20-6-centos pipe_blog]$ ./test
child say: hello pipe
child say: hello pipe
child say: hello pipe
child say: hello pipe
^C
现象解析:写端每隔三秒写一次,读端的read接口等待写端继续输入后再输出
情况4
5946
5947
5948
5949
5950
5951
5952
child say: hello pipe
hello pipe
hello pipe
hello pipe
hello pipe
hello pichild say: pe
hello pipe
现象解析:当写端快速写入,管道一下子就被占满(管道是有大小的),读端读的速度很慢,写端并未继续写入而是等待读端读取数据,
当管道中的数据减少到一定量的时候写端才开始继续写入(这一条这个test看不出来),可以再测试管道大小中测试
测试管道大小
[clx@VM-20-6-centos proc_pipe]$ cat test.c
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
void test5()
{
int pipefd[2] = {0};
if (pipe(pipefd) != 0){
perror("pipe failed\n");
exit(1);
}
if (fork() == 0){
//child 进程
close(pipefd[0]);
int count = 0;
while (1){
write(pipefd[1], "a", 1);
count++;
printf("%d\n", count);
}
exit(0);
}
//father 进程
close(pipefd[1]);
char buffer[32] = {0};
while (1){
sleep(5);
read(pipefd[0], buffer, sizeof(buffer) - 1);
}
waitpid(-1, NULL, 0);
}
int main()
{
test5();
return 0;
}
因为测试输出的结果很长,截取最后一小段进行说明
可以看到写入端写入的速度非常快,读取端还为读取就已经将管道写满了。此时管道中有65536个字符,也就是65536个字节,即64KB,这就是管道的大小。
注:当缓冲区被读取超过4KB大小的空间后才会继续写入,再读取4KB大小的空间前写入端会等待读取端读取数据
进程间命名管道通信
命名管道采用磁盘文件的标识方式,使得管道标识符具有唯一性,可以让不同的进程看到自己。并且命名管道并不会将数据刷新到磁盘上,提高通信效率
命名通信管道使用文件的标识方式,所以实现和普通磁盘文件的读写非常相似
接口介绍:mkfifo
mkfifo:有两个形参,前者代表路径名称,后者代表创建管道的权限(管道也是权限)
文件的权限由我们自定义的权限和掩码决定,当我们使用0666创建管道时会发现其权限实际为0644,这是0666和umask共同决定的,所以想要得到一个权限为0666的命名管道需要在开头调用umask(0),将掩码置零
在实际生活场景中:客户通过键盘(标准输入)向客户端发送数据,客户端将信息输出给服务器。接下来我们创建两个可执行程序,运行起来就是两个进程,分别代表客户端和服务器,来模拟进程间通信
test_code
[clx@VM-20-6-centos fifo_blog]$ ll
total 16
-rw-rw-r-- 1 clx clx 321 Oct 25 10:06 client.c
-rw-rw-r-- 1 clx clx 198 Oct 24 20:35 comm.h
-rw-rw-r-- 1 clx clx 163 Oct 24 20:22 Makefile
-rw-rw-r-- 1 clx clx 986 Oct 25 10:08 server.c
Makefile 的编写:
#创建一个伪目标all,生成all就必须拥有两个可执行程序
.PHONY:all
all:server client
server:server.c
gcc -o $@ $^ -std=c99
client:client.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f server client my_fifo
Makefile默认会生成其遇到的第一个可执行程序,若想同时生成多个则需要创建一个伪目标
comm.h
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#define FIFO_PATH "./my_fifo" //命名管道所在路径
#define FIFO_MODE 0644 //命名管道权限
server.c
#include "comm.h"
int main()
{
//若命名管道不存在则创建,成功返回0失败返回-1
printf("server running\n");
if (mkfifo(FIFO_PATH, FIFO_MODE) != 0){
perror("mkfifo failed\n");
exit(1);
}
//文件打开失败打印错误信息后退出,返回非0退出码
int fd = open(FIFO_PATH, O_RDONLY);
if (fd < 0){
perror("open failed\n");
exit(2);
}
//业务逻辑
while (1){
printf("client say:");
fflush(stdout);
char buffer[64] = {0};
int s = read(fd, buffer, sizeof(buffer) - 1); //ls\n
buffer[s - 1] = 0;//字符串以'\0'结尾时C语言的规则,系统调用无需遵守此规则,将有效信息提取即可
if (strcmp(buffer, "show") == 0){ //使用进程程序替换创建子进程执行指令,此处列举两条指令
if (fork() == 0){
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
exit(3);//程序替换失败,终止程序
}
waitpid(-1, NULL, 0);
}
else if (strcmp(buffer, "run") == 0){
if (fork() == 0){
execl("/usr/bin/sl", "sl", NULL);
}
waitpid(-1, NULL, 0);
}
else if (s == 0){
printf("fail end\n"); //当子进程退出,即写入端退出,管道读取结束,文件来到结尾的标志
break;
}
else {
printf("%s\n", buffer);
}
}
close(fd);
return 0;
}
client.c
#include "comm.h"
int main()
{
int fd = open(FIFO_PATH, O_WRONLY); //服务器先于客户运行,此时管道已经存在
printf("client running\n");
while (1){
printf("print:");
fflush(stdout);
char buffer[64] = {0};
int s = read(0, buffer, sizeof(buffer) - 1);//ls\n 从标准输入读取信息
buffer[s - 1] = 0;
write(fd, buffer, sizeof(buffer) - 1);//将信息输出到管道中
}
return 0;
}
现象展示:
可以向服务器的业务逻辑中添加更多else if 语句,就可以增加服务器的功能
匿名管道和命名管道主要区别:让不同进程看到同一份资源的方式不同,匿名管道是通过父子进程继承files_struct结构体来实现的。命名管道则通过磁盘文件的命名方式具有唯一性,通过路径+文件名的方式让不同进程找到同一份文件,以达到看到相同资源的目的
基于System V进程通信
命名管道和匿名管道是基于文件的通信方式
System V标准的进程通信方式是OS层面上专门为进程通信设计的一个方案(效率非常高)
用户想要使用System V通信必须使用系统调用接口(system call)
普遍应用场景:同一主机内部进程间通信
查看System V指令: ipcs
共享内存
共享内存的性质:共享内存的生命周期是随内核的,并不随进程结束而结束只能通过程序员显式释放或者重启操作系统
查看共享内存指令: ipcs -m
删除共享内存指令: ipcrm -m + shmid
接口介绍
1.shmget()
第一个参数 key
1.key是操作系统为实现标识唯一性通过ftok()函数生成的,具有唯一性
第二个参数 size
2. size 开辟共享内存的大小,OS会按照4KB的整数倍开辟, 若开辟4KB+1字节,则操作系统会开辟8KB的内存,并且合法的只有4097个字节,若越界很可能发生错误,甚至程序崩溃。有点像size(有效数据)和capacity(容量)的差别
第三个参数 shmflg
共享内存标识符,主要选项有IPC_CREAT(0)若这个共享内存不存在则创建,存在则进行获取.
IPE_EXCL选项不单独使用,和IPC_CREAT一起使用时,若共享内存存在就会返回-1报错
上述shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限
2.ftok(file to key)
第一个参数 pathname
自定义路径名(路径真实必须存在)
第二个参数 proj_id
自定义项目id
OS会通过这两个参数生成一个唯一标识的key值,key只是用来在用户层进行标识唯一性,不能用来管理share memrary
shmid是操作系统返还给用户的id,用来管理共享内存
3.shmat
第一个参数 shmid
shmget返回的具有唯一性的标识符
第二个参数shmaddr
如果 shmaddr 是NULL,系统将自动选择一个合适的地址! 如果shmaddr不是NULL 并且没有指定SHM_RND 则此段连接到addr所指定的地址上 如果shmaddr非0 并且指定了SHM_RND 则此段连接到shmaddr -(shmaddr mod SHMLAB)所表示的地址上.SHM_RND命令的意思是取整,SHMLAB的意思是低边界地址的倍数,它总是2的乘方。该算式是将地址向下取最近一个 SHMLAB的倍数
第三个参数shmflg
如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写的方式连接此段
shmat返回值是该段所连接的实际地址 如果出错返回-1
4.shmdt
第一个参数 shmid
shmget返回的具有唯一性的标识符
第二个参数 shmaddr
共享内存的起始地址
第三个参数 shmflg
指定(特定于Linux的)SHM_REMAP标志,以指示段的映射应替换范围内的任何现有映射从shmaddr开始,继续计算段的大小。(通常为如果此地址范围中已存在映射,则会导致EINVAL错误。)在里面在这种情况下,shmaddr不能为NULL。(一般置零即可)
成功返回0 失败返回(void*) -1。这
个接口并不是释放共享内存的,而是取消当前进程和共享内存的关系
5.shmctl
第一个参数 shmid
shmget返回的具有唯一性的标识符
第二个参数 cmd
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存
第三个参数 buf
共享内存管理结构体。具体说明参见共享内存内核结构定义部分
删除共享内存的时候,一般设置为NULL
成功返回0,失败返回-1
通过开辟共享内存的方式,进程之间可以通过系统调用接口拿到同一块共享内存的首地址对内存进行访问,达到不同进程看到同一份资源,进行进程 通信。
示例:
Makefile
[clx@VM-20-6-centos shar_memory]$ cat Makefile
.PHONY:all
all:server client
server:server.c
gcc -o $@ $^ -std=c99
client:client.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f server client
comm.h
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4096
server.c
#include "comm.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0){
perror("ftok error\n");
exit(1);
}
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0){
perror("shmget error\n");
exit(2);
}
char* mem = (char*)shmat(shmid , NULL, 0);
printf("attatchs shm success\n");
sleep(5);
//业务逻辑
while (1){
sleep(1);
printf("%s\n", mem);
}
shmdt(mem);;
printf("detaches shm success\n");
sleep(5);
printf("key = %u, shmget = %d\n", key, shmid);
sleep(10);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
上述shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限
client.c
#include "comm.h"
int main()
{
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key < 0){
perror("ftok error\n");
}
sleep(5);
//获取共享内存
int shmid = shmget(key, SIZE, IPC_CREAT);
if (shmid < 0){
perror("shmid error\n");
exit(1);
}
char* mem = (char*)shmat(shmid, NULL, 0);
printf("client process attaches success\n");
//sleep(5);
int count = 0;
while (1)
{
sleep(1);
char ch = 'a' + count;
mem[count] = ch;
count++;
}
shmdt(mem);
printf("client process detaches success\n");
//sleep(5);
return 0;
}
共享内存测评
优点:速度快,read和write的本质是将数据从内核拷贝到用户,或者从用户拷贝到内核中。而共享内存一旦建立好并映射进自己进程的地址空间,该进程就可以直接看到共享内存,就如同malloc出来的空间一样,不需要任何系统调用接口,所以共享内存是所有进程中速度最快的
缺点:当client没有写入,甚至还没有启动时,server端根本不会等待client端写入,并且共享内存不提供任何同步或者互斥机制,需要程序员自行保证数据的安全
进程通信名词解释
1.临界资源:凡是被多个执行流同时能够访问的资源就是临界资源!同时向显示器打印,进程间通信的时候,管道,共享内存,消息队列等都是临界资源
2.临界区:进程的代码可是有很多的,其中,用来访问临界资源的代码就是临界区
3.原子性:一件事要么不做,要么做完没有中间态就叫原子性
4.信号量:管道,共享内存、消息队列都是以传输数据为目的的!但是信号量并非以传输数据为目的!而是通过共享资源的方式来达到多个进程同步或者互斥的目的。
信号量的本质是一个计数器,用来衡量临界资源中的资源数目。就像一份数据它规定有100个进程可以提取,操作系统会提早发好门票,允许100个进程对其进行提取以创建对临界资源的预定机制