IPC方法
Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。
任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间
不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据
从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种
机制称为进程间通信(IPC,InterProcess Communication)。
在进程间完成数据传递需要借助操作系统提供特殊的方法,
如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。
现今常用的进程间通信方式有:
① 管道 (使用最简单)
② 信号 (开销最小)
③ 共享映射区 (无血缘关系)
④ 本地套接字 (最稳定)
![image]()
管道
管道的概念:
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据自己读不能自己写。
② 数据一旦被读走,便不在管道中存在,不可反复读取。
③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
④ 只能在有公共祖先的进程间使用管道。
常见的通信方式有,单工通信、半双工通信、全双工通信。
pip父子进程通信示例
点击查看代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int fd[2];
pid_t pid;
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error:");
exit(1);
}
pid = fork();
if (pid == -1) {
perror("pipe error:"); //ls | wc -l
exit(1);
}else if (pid == 0) { // 子 读数据
close(fd[1]);
char buf[1024];
ret = read(fd[0],buf,sizeof(buf));
if(ret == 0){
printf("----------\n");
}
write(STDOUT_FILENO,buf,ret);
}else{ // 父 写数据
close(fd[0]);
write(fd[1],"hello pipe\n",strlen("hello pipe\n"));
}
return 0;
}
管道的深入进阶
暂略
mmap映射
引入:使用文件完成进程间通信
父子进程共享打开的文件描述符
/*
*父子进程共享打开的文件描述符------使用文件完成进程间通信.
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(void)
{
int fd1, fd2; pid_t pid;
char buf[1024];
char *str = "---------test for shared fd in parent child process-----\n";
pid = fork();
if (pid < 0) {
perror("fork error");
exit(1);
} else if (pid == 0) {
fd1 = open("test.txt", O_RDWR);
if (fd1 < 0) {
perror("open error");
exit(1);
}
write(fd1, str, strlen(str));
printf("child wrote over...\n");
} else {
fd2 = open("test.txt", O_RDWR);
if (fd2 < 0) {
perror("open error");
exit(1);
}
sleep(1); //保证子进程写入数据
int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
wait(NULL);
}
return 0;
}
mmap函数
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏
参数:
addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot:映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd:用来建立映射区的文件描述符
offset:映射文件的偏移(4k的整数倍)
最简单mmap函数
最简单mmap函数的使用
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
int main(void)
{
int fd = open("mytest.txt", O_RDWR|O_CREAT,0644);
char *p=NULL;
int ret,len;
if(fd<0){
perror("open error:");
exit(1);
}
int file_size=20;
//函数原型: int ftruncate(int fd, off_t length)
//函数说明: ftruncate()会将参数fd指定的文件大小改为参数length指定的大小
len = ftruncate(fd,file_size);
if(len==-1){
perror("ftruncate error:");
exit(1);
}
printf("len = %d\n", len);
//mmap映射区的创建
p = mmap(NULL, file_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}
//p指向创建mmap映射区的首地址,然后下面向其中写入数据,也就完成了共享内存的操作
strcpy(p, "hello,my001.c\n");
//关闭映射区
ret=munmap(p,file_size);
if(ret==-1){
perror(" munmap error:");
exit(1);
}
printf("ret = %d\n", ret);
close(fd);
return 0;
}
mmap函数进阶
点击查看代码
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(void)
{
char *mem;
int len = 0;
int fd = open("hello244", O_RDWR|O_CREAT, 0644);
//int fd = open("dict.txt", O_RDWR);
if (fd < 0)
sys_err("open error");
ftruncate(fd, 20);
/*
len = lseek(fd, 3, SEEK_SET); //获取文件大小,根据文件大小创建映射区
write(fd, "e", 1); //实质性完成文件拓展
*/
printf("The length of file = %d\n", len);
mem = mmap(NULL, 20, PROT_WRITE, MAP_SHARED, fd, 0);
if (mem == MAP_FAILED) //出错判断
sys_err("mmap err: ");
close(fd);
strcpy(mem, "xxx");
printf("%s\n", mem);
if (munmap(mem, 4) < 0)
sys_err("munmap");
return 0;
}
//思考:
//0. 可以open的时候O_CREAT一个新文件来创建映射区吗?
//1. 如果mem++,munmap可否成功?
//2. 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?
//3. 如果文件偏移量为1000会怎样?
//4. 如果不检测mmap的返回值,会怎样?
//5. mmap什么情况下会调用失败?
//6. 对mem越界操作会怎样?
//7. 文件描述符先关闭,对mmap映射有没有影响?
/*
解答:
0 : 可以,但文件必须要有实际大小,新O_CREAT出来的文件不能用来创建映射区
1 :不成功,但++之后进行--可以成功,根本原因在于映射区的首地址(addr)不能改变;使用过程杜绝++/--操作
2 :会产生权限不允许的错误,设置其他不匹配的权限可能会出现段错误或其他错误
权限总结:
创建映射区隐含着一次对文件的读操作,
映射区的权限<=打开文件的权限
3 : 偏移量offset必须是4K的整数倍:因为映射区是内核内部的mmu帮忙创建的,
而mmu映射的单位为4k,故而偏移量offset必须是4K的整数倍
4 :必须要检查,因为mmap的各个参数非常容易出错(出错概率很高)
5 :见前面所述
6 :mumap失败
7 :无影响,文件描述符是操作文件的句柄,
mmap映射区并不是用的句柄,mmap映射区用的是地址的方式将其当做数组看待的,没有用到句柄
映射区一旦创建成功,文件描述符相对于映射区而言就没有意义了
8 :文件大小必须比映射区空间大
*/
父子进程间使用mmap
mmap父子进程通信
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。
但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
MAP_SHARED: (共享映射) 父子进程共享映射区;
练习:父进程创建映射区,然后fork子进程,子进程修改映射区内容,
而后,父进程读取映射区内容,查验是否共享【fork_mmap.c】
结论:父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)
示例
仔细理解下面代码:
若①:p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
父子进程独享0-3G的进程地址空间,共享的只有打开的文件和mmap建立的映射区
全局变量处于0-3G空间中,故而不会共享
而指针p使用了mmap映射区赋值,会共享(语言再斟酌)
若②:p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
p不会被共享
父子进程间使用mmap
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
ftruncate(fd, 4);
// p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork(); //创建子进程
if(pid == 0){
*p = 2090;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
匿名映射
匿名映射的引入
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信
也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了
建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。
可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区
的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags
来指定。
匿名映射代码1 anon_1.c
宏MAP_ANON只能在Linux系统中使用,在类UNIX系统中不能使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_ANON|MAP_SHARED,-1, 0);
// p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
pid = fork(); //创建子进程
if(pid == 0){
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
//匿名映射只是没有借助文件,但依然需要释放
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
匿名映射代码2-通用型的anon_2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd = open("/dev/zero",O_RDWR);
p = (int *)mmap(NULL, 41, PROT_READ|PROT_WRITE,MAP_SHARED,fd, 0);
// p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd);
pid = fork(); //创建子进程
if(pid == 0){
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
非血缘关系之间通过mmap通信
写端mmap_w.c
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU {
int id;
char name[20];
char sex;
};
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student = {10, "xiaoming", 'm'};
char *mm;
if (argc < 2) {
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0664);
ftruncate(fd, sizeof(student));
mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap");
close(fd);
while (1) {
memcpy(mm, &student, sizeof(student));
student.id++;
sleep(1);
}
munmap(mm, sizeof(student));
return 0;
}
读端mmap_r.c
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU {
int id;
char name[20];
char sex;
};
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;
if (argc < 2) {
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDONLY);
if (fd == -1)
sys_err("open error");
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if (mm == MAP_FAILED)
sys_err("mmap error");
close(fd);
while (1) {
printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex);
sleep(2);
}
munmap(mm, sizeof(student));
return 0;
}
非血缘关系之间不通过mmap,而通过文件进行通信的示例
strace ./test2
上面一条命令能够追踪可执行程序test2执行过程中所使用的系统调用有哪些
通过该指令可以发现内部的底层实现也是调用了mmap函数,
说明mmap与read/write/open等相关函数有一定的对应关系
test1.c
/*
* 先执行,将数据写入文件test.txt
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#define N 5
int main(void)
{
char buf[1024];
char *str = "--------------secesuss-------------\n";
int ret;
int fd = open("test.txt", O_RDWR|O_TRUNC|O_CREAT, 0664);
//直接打开文件写入数据
write(fd, str, strlen(str));
printf("test1.c write into test.txt finish\n");
sleep(N);
lseek(fd, 0, SEEK_SET);
ret = read(fd, buf, sizeof(buf));
ret = write(STDOUT_FILENO, buf, ret);
if (ret == -1) {
perror("write second error");
exit(1);
}
close(fd);
return 0;
}
test2.c
/*
* 后执行,尝试读取另外一个进程写入文件的内容
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
char buf[1024];
char *str = "----------test2 write secesuss--------\n";
int ret;
sleep(2); //睡眠2秒,保证test1将数据写入test.txt文件
int fd = open("test.txt", O_RDWR);
//尝试读取test.txt文件中test1写入的数据
ret = read(fd, buf, sizeof(buf));
//将读到的数据打印至屏幕
write(STDOUT_FILENO, buf, ret);
//写入数据到文件test.txth中, 未修改读写位置
write(fd, str, strlen(str));
printf("test2 read/write finish\n");
close(fd);
return 0;
}
其他进阶课程以及课后作业-注意反复复习