进程间通信?
因为每一个进程都有一个虚拟地址空间, 保证了进程的独立性, 但也正是因此导致进程间无法通信. 所以需要操作系统提供进程间通信方式, 又因为通信的场景不同, 所以提供了多种不同的进程间通信方式
进程间通信的目的
- 数据传输
- 资源共享
- 进程控制
- 通知事件
进程间通信方式:
System V 标准的进程间通信方式:
- 管道: 用于进程间的数据传输
- 共享内存: 用于进程间的数据共享
- 消息队列: 用于进程间的数据传输
管道
内核中的一块缓冲区,通过让多个进程都能访问到同一块缓冲区, 来实现进程间通信(半双工通信)
管道分为 匿名管道和命名管道
匿名管道和命名管道区别在于这块内核中的缓冲区是否具有标识符(可见于文件系统的管道文件),其它的进程可以通过这个标识符,找到这块缓冲区(打开同一个管道文件, 进而访问到同一块缓冲区),实现通信.
管道特性
- 具有同步与互斥的特性
- 同步:对临界资源(公共资源)访问的合理性----通过条件判断当前进程能否访问,不能访问则等待,能访问在唤醒. 例如: 如果管道没有数据, 使用read()读取数据会阻塞, 如果管道写满了, 使用write()继续写入数据会阻塞
- 互斥:保证在同一时间, 只有一个进程能对临界资源进行访问, 保证临界资源的安全性; 对管道进行数据操作的大小不超过PIPE_BUF, 保证操作的原子性
- 如果管道所有的写端被关闭(表示当前没有进程继续写入数据了),read读完管道中的数据后, 直接返回0
- 如果管道所有的读端被关闭(表示没有进程读取数据了)使用write继续写入数据会触发异常, 程序退出
- 管道的生命周期随进程, 打开管道的所有进程退出, 管道也会被释放
- 管道提供字节流传输服务
- 命名管道
若命名管道以只读的方式打开, 则会阻塞, 直到这个命名管道被以写的方式打开
若命名管道以只写的方式打开, 则会阻塞, 直到这个命名管道被以读的方式打开
若命名管道以读写的方式打开, 则不会阻塞
匿名管道
这块内核中中的缓冲区没有标识符,(没有标识符,其它的进程就不能找到这块缓冲区).所以只能用于具有亲缘关系的进程间通信(比如:子进程通过赋值父进程的方式, 获取到管道的操作句柄进而实现访问同一个管道)
通过代码加深对管道的理解
代码示例1:
父进程向管道写入数据
子进程从管道中读取数据
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
// 匿名管道只能用于具有亲缘关系的进程间通信, 子进程通过复制父进程的方式获取操作句柄
// 创建管道的操作要在创建子进程之前, 否则 子进程复制父进程就复制不到这个管道了
int pipefd[2] = {-1};
// 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
// pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
// 成功 返回0 失败返回-1
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error!");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error!");
}
else if(pid == 0)
{
// 子进程
char buf[1024];
read(pipefd[0], buf, 1023);
printf("child:%s",buf);
}
else{
// 父进程
//sleep(3);
char *ptr = "我真的好菜啊!\n";
write(pipefd[1], ptr, strlen(ptr));
}
return 0;
}
结果:父进程先于子进程退出, 子进程成为孤儿进程运行在后台
子进程运行在后台时,shell 切换回前台打印提示信息, 然后子进程打印信息就会打印到shell提示信息的后边
代码示例2:
在代码示例1的基础上,休眠父进程3秒,
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {-1};
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error!");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error!");
}
else if(pid == 0)
{
// 子进程
char buf[1024];
read(pipefd[0], buf, 1023);
printf("child:%s",buf);
}
else{
// 父进程
sleep(3);
char *ptr = "我真的好菜啊!\n";
write(pipefd[1], ptr, strlen(ptr));
}
return 0;
}
观察结果验证管道的特性:若管道中没有数据,调用 read 读取数据 则会阻塞一段时间(父进程休眠的时间)
代码示例3:
不停向管道写入数据,写满后, 使用write继续写入数据会阻塞;
管道是一块缓冲区, 大小有限
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {-1};
// 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
// pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
// 成功 返回0 失败返回-1
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error!");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error!");
}
else if(pid == 0)
{
// 子进程
char buf[1024];
read(pipefd[0], buf, 1023);
printf("child:%s",buf);
}
else{
// 父进程
//sleep(3);
// 父进程
sleep(2);
char *ptr = "我真的好菜啊!\n";
int total_len = 0;
while(1)
{
int wlen = write(pipefd[1], ptr, strlen(ptr));
total_len += wlen;
printf("total_len = %d\n", total_len);
}
}
return 0;
}
代码示例4:
关闭管道所有的读端pipefd[0], 使用write继续写入数据 观察结果
注意: 父子进程都有读写端
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {-1};
// 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
// pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
// 成功 返回0 失败返回-1
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error!");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error!");
}
else if(pid == 0)
{
// 子进程
close(pipefd[0]); // 关闭读端
sleep(100); // 关闭了读端,休眠一段时间, 让代码不在向下运行
char buf[1024];
read(pipefd[0], buf, 1023);
printf("child:%s",buf);
}
else{
// 父进程
//sleep(3);
// 父进程
close(pipefd[0]); // 注意:父进程也有读端
sleep(2);
char *ptr = "我真的好菜啊!\n";
int total_len = 0;
while(1)
{
int wlen = write(pipefd[1], ptr, strlen(ptr));
total_len += wlen;
printf("total_len = %d\n", total_len);
}
}
return 0;
}
结果: 程序直接退出了(write触发了异常)
验证了特性--------若管道所有的读端被关闭, 继续写入数据会触发异常, 程序直接退出.
代码示例5:
关闭所有的写端pipefd[1], 继续使用 read 读数据.
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = {-1};
// 创建一个匿名管道, 通过参数pipefd返回管道的操作句柄
// pipefd[0] : 用于从管道读取数据 pipefd[1] : 用于向管道写入数据
// 成功 返回0 失败返回-1
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error!");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error!");
}
else if(pid == 0)
{
// 子进程
close(pipefd[1]); // 关闭写端
sleep(1);
char buf[1024];
int rlen = read(pipefd[0], buf, 1023);
printf("child:%d--%s", rlen, buf);
}
else{
// 父进程
//sleep(3);
// 父进程
close(pipefd[1]); // 注意:父进程也有写端
sleep(100);
char *ptr = "我真的好菜啊!\n";
int total_len = 0;
while(1)
{
int wlen = write(pipefd[1], ptr, strlen(ptr));
total_len += wlen;
printf("total_len = %d\n", total_len);
}
}
return 0;
}
结果: read没有读到数据, 返回0
若管道所有的写端被关闭, 使用read读完数据后会返回0
如果管道中没有写入数据
1. 所有写端被关闭—继续读数据 read, 会返回0,不再阻塞
2. 写端没有全部关闭—继续读数据 会阻塞
代码示例6:
通过匿名管道实现管道符的作用
举例命令: ls - l | grep a
grep 从标准输入读取数据进行过滤
- 通过重定向(dup2) 将标准输出写入到管道中
- 通过重定向使 grep 从管道读取数据
/**
* 通过匿名管道实现管道符的作用
* 实现: ls -l | grep a
* ls 运行结束后将进程信息输出到标准输出
* grep a从标准输入读取数据进行过滤
*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<wait.h>
int main()
{
int pipefd[2] = {-1};
if(pipe(pipefd) < 0){
perror("pipe error");
return -1;
}
pid_t ls_pid = fork();
if(ls_pid < 0){
perror("ls_fork error");
return -1;
}
else if(ls_pid == 0)
{
// ls子进程
// 将标准输出重定向到管道输入端, 向1写入数据就相当于向管道写入数据
// 关闭写端---ls进程一旦推出, 所有的写端被关闭, grep读完数据后返回0
close(pipefd[1]);
dup2(pipefd[1], 1);
execlp("ls", "ls","-l", NULL);
exit(0);
}
pid_t grep_pid = fork();
if(grep_pid == 0){
// grep 子进程
// 关闭写端---ls进程一旦推出, 所有的写端被关闭, grep读完数据后返回0
close(pipefd[1]);
// 将标准输入重定向到管道读取端,
dup2(pipefd[0], 0);
execlp("grep", "grep", "a", NULL);
exit(0);
}
// 父进程关闭读写端
close(pipefd[1]);
close(pipefd[0]);
waitpid(ls_pid, NULL, 0);
waitpid(grep_pid, NULL, 0);
return 0;
}
结果: