IPC-Linux进程间通信

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;
}

其他进阶课程以及课后作业-注意反复复习

posted @ 2022-10-29 10:24  mnst  阅读(113)  评论(0)    收藏  举报