1. 操作系统为什么提供通信方式?
因为进程的独立性导致进程间是无法直接进行通信的。
2. 进程间通信的基本介绍
作用:进程间进行交流
因为进程间的独立性,因此通信需要双方拥有公共的媒介才能通信,而这个媒介由操作系统提供;并且因为通信的场景不同,操作系统提供了多种不同的进程间通信方式。
3. 进程间同i性能方式(ipc)
管道:匿名管道/命名管道
共享内存:进程间通信方式最快的一种方式
消息队列:内核创建的优先级队列
信号量:具有等待队列的计数器
4. 管道
管道:半双工通信:可选方向的单通信
特性:半双工通信,生命周期随进程,自带同步与互斥(< PIPE_BUF);管道提供字节流服务:管道中传输的时字节流。
管道原理:操作系统在内核中创建一块缓冲区,并且为用户提供了管道的操作句柄(两个文件描述符),用户通过IO接口进行进程间通信。
进程创建管道,有两个文件描述符int fd[2]—>fd[0]–读取,fd[1]–写入
匿名管道的使用场景:因为匿名管道没有名字,所以其他进程的内核中是无法找到这个管道的,因此匿名管道实现进程间通信,只能用于具有亲缘关系间的进程(子进程通过复制父进程pcb,得到管道的操作句柄)
匿名管道的实现:
int pipe(int pipefd[2]);
返回值:失败:-1; 成功:传出参数pipefd[2]
参数:拥有两个整形的数组,用来接受成功后返回的两个操作句柄
#include
#include
#include
#include
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error:");
exit(-1);
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error:");
exit(-1);
}
else if(pid == 0)
{
// 子进程写数据
close(pipefd[0]);
int len = 0;
while(1)
{
char* ptr = "child write!";
printf("write lenth: %d\n", len);
int ret = write(pipefd[1], ptr, strlen(ptr));
len += ret;
printf("write lenth: %d\n", len);
sleep(2);
}
}
else
{
// 父进程读数据
char buf[1024];
int ret = read(pipefd[0], buf, 1023);
printf("child read buf:[%d - %s]", ret, buf);
sleep(1);
}
while(1)
{
printf("------parent\n");
sleep(1);
}
return 0;
}
程序中使用了fork创建了一个子进程,让子进程向管道中写入了“child write”,然后父进程将管道中的数据读取。
匿名管道的使用:
连接两个命令,将第一个命令的输出结果作为第二个命令的输入数据进行处理。
ls | grep pipe*
实现思路:
主进程创建两个子进程,分别进行程序替换执行ls与grep程序。
ls程序是浏览目录,将结果写入到标准输出
grep程序是从标准输入读取数据(循环读取数据),进行过滤;
程序替换:
因为进程1,执行ls程序将结果写入到了标准输出,而不是管道,并没有将数据交给其他进程,因此需要对进程1进行标准输出重定向,将ls的结果不是写入到标准输出,而是写入到管道中。
因为进程2,执行grep程序是从标准输入读取数据进行处理,而不是管道,因此无法获取前边命令的结果,所以需要对输入进行重定向,将本身从标准输入读取改为从管道中读取数据。
需要注意的地方:
因为进程2不知道具体前面的命令写入了多少数据,因此只能循环处理,读取多少处理多少。这里就出现了一个问题:加入管道的数据已经被读取完了,读不到数据,read会阻塞,进程2一直不退出,程序无法退出。
解决方法:在数据全部写入后,需要将所有的写端关闭,这样read在读取不到数据后返回0,认为数据没有了,进程退出,程序结束。
#include
#include
#include
#include
#include
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error:");
exit(0);
}
pid_t pid1 = fork();
if(pid1 < 0)
{
perror("fork error:");
exit(0);
}
else if(pid1 == 0)
{
close(pipefd[0]);
dup2(pipefd[1], 1);
execlp("ls", "ls", NULL);
exit(0);
}
pid_t pid2 = fork();
if(pid2 < 0)
{
perror("fork error:");
exit(0);
}
else if(pid2 == 0)
{
close(pipefd[1]);
dup2(pipefd[0], 0);
execlp("grep", "grep", "pipe*", NULL);
exit(0);
}
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);
return 0;
}
程序中创建了两个子进程,分别执行不同的命令,子进程1执行的结果输入到管道中,子进程2从管道中获取数据进行过滤。需要注意重定向与程序替换的问题。在关闭所有的写端时注意主进程也拥有管道的两个操作句柄,所以关闭主进程的两个句柄。并且需要主进程等待两个子进程退出。
命名管道:
命名管道: 具有名字的管道
体现在文件可见性,管道文件的标识为p
创建命名管道会在文件系统中创建一个管道文件,本机上的所有进程都可以通过打开这个管道文件进而访问管道是心啊进程间通信
命名管道可以实现同一主机上的任意进程间通信
命名管道的创建:
int mkfifo(const char* pathname, mode_t mode);
返回值:成功:0 失败:<0 EEXST 管道文件已存在的错误,使用时排除这个错误(代码中体现)
pathname:管道文件创建路径
flags:管道文件的权限(创建的文件需要具有可读可写的权限,例:0664)
//命名管道的创建,写入
#include
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
char* fifo = "./test.fifo";
int ret = mkfifo(fifo, 0664);
if(ret < 0)
{
if(errno != EEXIST)
{
perror("fifo error");
exit(-1);
}
}
printf("make success\n");
int fd = open(fifo, O_WRONLY);
if(fd < 0)
{
perror("open error");
exit(-1);
}
printf("open success\n");
while(1)
{
char buf[1024];
scanf("%s",buf);
write(fd, buf, strlen(buf));
printf("[%s-%d]\n", buf, strlen(buf));
}
close(fd);
return 0;
}
命名管道的创建,需要注意管道创建的权限;在返回值为<0的时候,有可能是管道文件已存在,我们可以直接打开文件,所以在错误为EEXIST的时候,继续向下运行,而不是报错退出。
文件IO中write的用法
// 命名管道中读取数据,与write搭配使用
#include
#include
#include
#include
#include
#include
#include
int main()
{
char* fifo = "./test.fifo";
int fd = open(fifo,O_RDONLY);
if(fd < 0)
{
perror("open error");
exit(-1);
}
printf("open success\n");
while(1)
{
char buf[1024] = {0};
int ret = read(fd, buf,1023);
if(ret > 0)
{
printf("client say:[%s-%d]\n",buf, strlen(buf));
}
else if(ret ==0)
{
printf("write close\n");
exit(0);
}
else
{
perror("read error");
exit(-1);
}
}
close(fd);
return 0;
}
在命名管道中读取数据,注意打开的管道文件与写端创建的文件相同
文件IO中open的用法
需要注意两个程序不在进行读写操作的时候,关闭文件描述符,养成良好的编程习惯,避免文件描述符泄漏
命名管道的打开特性:
若文件以只读打开,但是当前这个管道又没有其他进程以写的方式打开,则阻塞;阻塞到其他进程以只写方式打开;
若文件以只写方式打开,但是当前这个管道没有被其他进程以只读方式打开,则阻塞;阻塞到其他进程以只读方式打开;
若管道文件以读写方式打开,则不会阻塞
命名管道的读写特性与匿名管道相同,参考匿名管道的读写特性
命名管道与匿名管道的区别:
命名管道可用于同一主机上的任意进程,匿名管道只能用于具有亲缘关系的进程间通信
原因:命名管道具有名字,也就是说有文件可见于文件系统中
5. 共享内存:进程间通信最快的方式
共享内存为什么是进程间通信方式中最快的一种?
这里应该先回答其他进程间通信的原理,然后回答共享内存的通信原理
其他进程间通信的原理:将数据拷贝到内核态,使用的时候从内核态拷贝到用户态
共享内存的通信原理:在物理内存上开辟一块空间,并且将空间映射到各个进程的虚拟地址空间,这时候进程就可以通过虚拟内存直接对内存进行操作;
相较于其他通信方式,少了两步用户态/内核态直接的数据拷贝的过程,所以是进程间通信中最快的
共享内存的操作步骤:
- 创建/打开共享内存:指定标识符、大小、权限 shmget
- 将共享内存映射到虚拟地址空间:通过句柄,映射到进程虚拟地址首地址 shmat
- 进行内存操作:memcpy/printf
- 解除映射关系:shmdt
- 删除共享内存:shmctl
需要注意:删除共享内存不是直接删除的,而是判断连接数;若为0,则删除,若不为0,则拒绝后续连接,直到连接数为0,删除共享内存
// shm_write向共享内存中写入数据
#include
#include
#include
#include
#include
#define IPC_KEY 0x12345678
#define PROJ_ID 0x12345678
#define SHM_SIZE 4096
int main()
{
// 1. 创建或打开共享内存
int shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget error");
exit(-1);
}
// 2. 将共享内存映射到虚拟内存
char* shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
exit(-1);
}
// 3. 进行内存操作
int i = 0;
while(1)
{
sprintf(shm_start,"共享内存通信++++%d",i++);
sleep(1);
}
// 4. 解除映射关系
shmdt(shm_start);
// 5. 删除共享内存
// 不是立即删除的,而是判断连接数是否为0;若不为0,连接数减一,拒绝后续的连接请求,为0,删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
// shm_read从共享内存中读取数据
#include
#include
#include
#include
#include
#define IPC_KEY 0x12345678
#define PROJ_ID 0x12345678
#define SHM_SIZE 4096
int main()
{
// 1. 创建或打开共享内存
int shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0664);
if(shmid < 0)
{
perror("shmget error");
exit(-1);
}
// 2. 将共享内存映射到虚拟内存
char* shm_start = (char*)shmat(shmid, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
exit(-1);
}
// 3. 进行内存操作
while(1)
{
printf("%s\n",shm_start);
sleep(1);
}
// 4. 解除映射关系
shmdt(shm_start);
// 5. 删除共享内存
// 不是立即删除的,而是判断连接数是否为0;若不为0,连接数减一,拒绝后续的连接请求,为0,删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
注:我编写的代码中名字写反了,在上边标注的是正确的
6. 消息队列
一个内核创建的(优先级)队列
传输的是有类型的数据块
// TODO
等待后续补充吧,会的也不多
7. 信号量
具有等待队列的计数器
本质是一个计数器,用于资源计数,实现等待与唤醒
可实现进程间的同步与互斥
// TODO