问题:为什么进程间无法直接通信?
因为进程之间具有独立性,每个进程访问的都是自己的独立的虚拟地址空间,使用的都是虚拟地址,通过页表映射到物理内存。因此就算将数据的内存地址交给其他进程,其他进程也无法访问(当然其实也无法直接给),只能通过操作系统提供的几种方式来进行。
本质上来说是操作系统为多个进程提供了一处公共的数据传输媒介(内存)。因为通信场景不同,因此提供了多种不同的方式: 管道,共享内存,消息队列,信号量
管道
管道:用于传输数据
本质:管道就是内核中的一块缓冲区(内存)
特性:
半双工通信–可以选择方向的单向通信
管道提字节流传输服务:有序的,基于连接的,可靠的传输方式。
基于连接:所有读端被关闭,则write触发异常,所有写端被关闭则read读完数据返回0,不再阻塞
分类:
匿名管道:只能用于具有亲缘关系的进程间通信
没有标识符,只能通过子进程复制父进程的方式获取操作句柄
命名管道:可以用于同一主机任意一个进程间通信
标识符是管道文件,通过打开同一个管道文件访问同一个缓冲区
管道自带同步与互斥:
互斥:通过保证同一事件对临界资源的唯一访问保证操作安全性
同步:通过某种条件判断,实现对资源访问获取的有序合理性。
互斥的具体体现:对管道进行写操作时写入数据大小不超过PIPE_BUF默认4096字节大小,则保证操作原子性
同步的具体体现:若管道中没有数据则read阻塞;若管道中数据满了则write阻塞
管道生命周期随进程
匿名管道
匿名管道:管道没有名字–缓冲区没有标识符;
只能用于具有亲缘关系的进程通信:因为管道缓冲区没有标识符,无法被其他进程找到,因此只能通过子进程复制父进程的方式获取到管道的操作句柄进行通信。
int pipe(int pipefd[2])
pipefd[0]—读
pipefd[1]—写
返回值:成功返回0;失败返回-1
匿名管道一定要在创建子进程之前创建
管道的读写特性:
管道中没有数据,则read会阻塞。管道中数据满了,则write会阻塞
若管道的所有读端被关闭,则继续write就会触发异常-导致进程退出
若管道的所有写端被关闭,read读完所有数据后,则不再阻塞,返回0;
实例
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main(int argc, char *argv[])
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
else if(pid == 0)
{
char *ptr = "hello\n";
write(pipefd[1], ptr, strlen(ptr));
printf("son fork write success\n");
}
else
{
char buf[1024] = {0};
read(pipefd[0], buf, 1023);
printf("father fork: %s\n", buf);
}
return 0;
}
管道:| 连接两个命令,将前边命令进程要输出到标准输出的结果,不再输出到标准输出,而是将这个数据传输到后边的命令进程。
ps -er | grep pipe
ps - ef:将所有进程信息写入到标准输出
grep pipe:不断循环从标准输入读取数据进行字符串匹配过滤
实现:----------------------
shell进程中创建两个进程,一个进程运行ps,一个进程运行grep
要将ps进程的数据,交给grep进程只能通过进程间通信完成–匿名管道
1.创建管道
2.创建两个进程,在各自进程中进行程序替换
//execlp(新的程序文件名称,运行参数, NULL)
3.在程序替换之前,ps进程应该标准输出重定向到管道写入端;grep进程应该标准输入重定向到管道读取端
4.父进程等待子进程退出。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main(int argc, char *argv[])
{
int pipefd[2];
int ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
if(fork() == 0)
{
dup2(pipefd[1], 1);
execlp("ps", "ps", "-ef", NULL);
exit(-1);
}
if(fork() == 0)
{
close(pipefd[1]);
dup2(pipefd[0], 0);
execlp("grep","grep","pipe",NULL);
exit(-1);
}
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);
return 0;
}
命名管道
命名管道:管道具有标识符;
可用于同一主机上的任意进程间通信
命名操作:mkfifo filename 创建一个管道文件
命名管道本质依然是内核中的一块缓冲区,但是命名管道有名字具有标识符,而这个标识符就是一个可见于文件系统的管道类型文件,多个进程可以通过打开同一个管道文件访问同一块内核中的缓冲区实现通信。
int mkfifo(char *path, int mode)
对管道的读写操作,依然是IO操作
打开特性:若以只读方式打开命名管道文件,则会阻塞,直到这个文件被以写的方式打开;若以只写方式打开命名管道文件,则会阻塞,直到这个文件被以读的方式打开。
实例
//fifo_read.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc, char * argv[])
{
umask(0);
char *fifo_name = "./test.fifo";
int ret = mkfifo(fifo_name, 0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
int fd = open(fifo_name, O_RDWR);
if(fd < 0)
{
perror("open error");
return -1;
}
printf("open fifo success\n");
while(1)
{
char buf[1024] = {0};
int ret = read(fd, buf, 1023);
if(ret < 0)
{
perror("read error");
return -1;
}
else if(ret == 0)
{
printf("all write closed\n");
return -1;
}
printf("buf:%s\n");
}
close(fd);
return 0;
}
//fifo_write.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
int main(int argc, char *argv[])
{
umask(0);
char *fifo_name = "./test.fifo";
int ret = mkfifo(fifo_name, 0664);
if(ret < 0 && errno != EEXIST)
{
perror("mkfifo error");
return -1;
}
int fd = open(fifo_name, O_WRONLY);
if(fd < 0)
{
perror("open error");
return -1;
}
while(1)
{
char buf[1024] = {0};
scanf("%s", buf);
int ret = write(fd, buf, strlen(buf));
if(ret < 0)
{
perror("write error");
return -1;
}
}
close(fd);
return 0;
}
共享内存
共享内存:用于进程间的数据共享
通信原理:开辟一块物理内存,多个进程将同一块内存映射到自己的虚拟地址空间,进行访问,进而实现数据共享
共享内存是最快的进程间通信方式,因为通过虚拟地址空间映射后,直接通过虚拟地址访问物理内存,相较于其他方式少了两步数据拷贝操作。
特性:
1.效率最高的进程间通信方式
2.生命周期随内核
注意事项:
各个进程对共享内存的操作都是不安全的操作
操作流程:
1.创建或打开共享内存
int shmget(key_t key, size_t size, int shmflg)
key:标识符–通过相同标识符,多个进程可以打开同一块共享内存
size:要创建的共享内存大小
shmflg:打开方式+权限;IPC_CREAT | IPC_EXCL | 0664
返回值:成功返回非负整数-操作句柄;失败返回-1
2.与进程建立映射关系
void *shmat(int shmid, const void *shmaddr, int shmflg)
shmid:shmget返回的操作句柄
shmaddr:映射首地址,通常置NULL
shmflag:SHM_RDONLY-只读;0-可读可写
返回值:成功返回映射后的首地址,失败返回(void*)-1;
3.对共享内存进行内存操作:
memcpy,strcpy,printf…
4.与进程间解除映射关系
int shmdt(void *shmaddr)
shmaddr:shmat返回的映射首地址
返回值:成功返回0;失败返回-1;
5.删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
shmid:shmget返回的操作句柄
cmd:要对共享内存进行的操作类型
IPC_RMID:标记要删除的共享内存
映射链接数为0时删除共享内存;禁止新的映射链接
buf:用于获取或设置共享内存属性的,简单实用置NULL即可
返回值:成功返回0;失败返回-1;
key_t ftok(const char *pathname, int proj_id)
用于产生一个IPC key–标识符
实例
//shm_read.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#define IPC_KEY 0x01234567
#define PROJ_ID 0x1234567
int main(int argc, char *argv[])
{
//key_t key = ftok("./", PROJ_ID);
//shmget()
int shmid = shmget(IPC_KEY, 32, IPC_CREAT | 0664);
if(shmid < 0)
{
perror("shmget error");
return -1;
}
void *shm_start = shmat(shmid, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
while(1)
{
printf("%s\n", (char*)shm_start);
sleep(1);
}
shmdt(shm_start);
shmctl(shmid, IC_RMID, NULL);
return 0;
}
//shm_write.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/shm.h>
#define IPC_KEY 0x01234567
int main(int argc, char *argv[])
{
int shmid = shmget(IPC_KEY, 32, IPC_CREAT | 0664);
if(shmid < 0)
{
perror("shmget error");
return -1;
}
void *shm_start = shmat(shmid, NULL, 0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
sprintf(shm_start, "newv+%d", i++);
sleep(1);
}
shmdt(shm_start);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
消息队列
消息队列:实际上就是一个内核中的优先级队列
通信原理:在内核中创建一个优先级队列,多个进程通过对同一个队列添加或者获取节点实现数据通信。
特性:
1.自带同步与互斥;
2.生命周期随内核;
信号量
信号量:
本质:就是一个内核中的计数器+pcb等待队列,用于是按进程间的同步与互斥
停车场–车位计数牌–对停车场中的空闲车位资源的计数
原理:对资源进行计数,在获取资源之前先通过计数判断获取是否合理,不合理则阻塞等待。
P操作:计数-1,判断访问是否合理,不合理则阻塞;合理则正确返回
V操作:计数+1,唤醒阻塞进程
同步的实现:对资源进行计数,通过计数实现合理访问;产生一个资源则进行一次V操作。
互斥的实现:计数最大为1,表示只有一个资源,访问前P操作,在访问期间其他进程不可访问,访问完毕之后,进行V操作
ipc资源管理命令
ipc资源管理命令:
ipcs:查看ipc资源;
ipcrm:删除ipc资源
-m 共享内存;-q 消息队列;-s信号量
ipcrm -m shmid