Linux进程之-进程间通信

摘要

1、pipe进行父子进程间通信

2、pipe进行兄弟进程间通信

3、fifo进行无血缘关系的进程间通信

4、mmap函数:如何使用、创建匿名映射区、进行有血缘关系的进程间通信和进行无血缘关系的进程间通信

概念

IPC: InterProcess Communication 进程间通信,通过内核提供的缓冲区进行数据交换的机制。

IPC通信的方式:

  • pipe 管道 --最简单
  • fifo 有名管道
  • mmap 文件映射共享IO --速度最快
  • 本地socket --最稳定
  • 信号 --携带信息量最小
  • 共享内存
  • 消息队列

1、pipe管道

管道函数:

#include <unistd.h>

       int pipe(int pipefd[2]);

参数:pipefd 读写文件描述符,0-代表读,1-代表写

返回值:失败返回-1,成功返回0

管道pipe的优劣:

  • 优点:简单,比信号、套接字实现进程间通信简单很多。
  • 缺点:(1)只能单向通信,双工需要建立两个管道(每个管道需要关闭一个端口形成单向);只能用于父子、兄弟进程(用共同祖先)之间通信,该问题可以使用fifo有名管道解决。

管道缓冲区大小:使用ulimit -a命令查看当前系统中创建管道文件所对应的内核缓冲区大小。pipe size            (512 bytes, -p) 8

也可以使用fpathconf函数,借助参数、选项来查看,使用该函数引入头文件<unistd.h>,long fpathconf(int fd, int name); ,成功返回管道大小,失败返回-1,设置errno。

(1)练习1:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
    int fd[2];
    pipe(fd);
    pid_t pid = fork();

    if(pid == 0)
    {
        //子进程 --写
        sleep(3);
        write(fd[1],"hello",5);
    }
    else if(pid > 0)
    {
        //父进程  --读
        char buf[12]={0};
        int ret = read(fd[0],buf,sizeof(buf));
        if(ret > 0)
        {
            write(STDOUT_FILENO,buf,ret);
        }
    }
    return 0;
}

输出:

~$ gcc pipe_.c
~$ ./a.out
hello

(2)练习2:父子进程实现pipe通信,实现ps aux|grep bash 功能

#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();
    if(pid == 0)
    {
        //子进程
        //子进程只用到pipe管道的写端,把读端关闭
        close(fd[0]);
        //重定向
        dup2(fd[1],STDOUT_FILENO); //标准输出重定向到管道的写端
        //使用execlp函数
        execlp("ps","ps","aux",NULL);
    }
    else if(pid > 0)
    {
        //父进程
        //父进程只用到pipe管道的读端,把写端关闭
        close(fd[1]);
        //先重定向,标准输入重定向到管道的读端
        dup2(fd[0],STDIN_FILENO);
        //使用execlp函数
        execlp("grep","grep","bash",NULL);
    }
    return 0;
}

输出:

~$ gcc pipe_ps.c 
~$ ./a.out 
chen      7559  0.0  0.0  25272  6552 pts/4    Ss   21:39   0:00 bash
chen     12009  0.0  0.0  15968  1080 pts/4    S+   22:09   0:00 grep bash
等价于:
~$ ps aux|grep bash
chen      7559  0.0  0.0  25272  6552 pts/4    Ss   21:39   0:00 bash
chen     12015  0.0  0.0  15968   968 pts/4    S+   22:09   0:00 grep --color=auto bash

注意:pipe管道是双向的,如果不关闭另一端,如父进程认为还有写端存在,就有可能还有人给它发数据,会继续等待。

(3)pipe 读写管道的几种情况

读管道

  • 写端全部关闭,read读到0,相当于读到文件末尾
#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();
    if(pid == 0)
    {
        //子进程
        sleep(3);
        //关闭读端
        close(fd[0]);
        write(fd[1],"hello",5);
        close(fd[1]);  //关闭写端
        while(1)
        {
            sleep(1);
        }
    }
    else if(pid > 0)
    {
        //父进程
        close(fd[1]); //关闭写端
        char buf[12]={0};
        while(1)
        {
            int ret = read(fd[0],buf,sizeof(buf));
            //如果读到0,认为读到文件末尾
            if(ret == 0)
            {
                printf("read over!\n");
                break;
            }
            if(ret > 0)
            {
                write(STDOUT_FILENO,buf,ret);
            }
        }
    }
    return 0;
}

输出:

helloread over!

  • 写端没有全部关闭。有数据--read读到数据;没有数据--read阻塞,fcntl函数可以更改为非阻塞。

写管道

  • 读端全部关闭----产生一个信号SIGPIPE,程序异常终止。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    int fd[2];
    pipe(fd);

    pid_t pid = fork();
    if(pid == 0)
    {
        //子进程
        sleep(3);
        close(fd[0]);  //关闭读端
        write(fd[1],"hello",5);
        close(fd[1]);  //关闭写端
        while(1)
        {
            sleep(1);
        }
    }
    else if(pid > 0)
    {
        //父进程
        close(fd[1]); //关闭写端
        close(fd[0]); //关闭读端
        int status;
        wait(&status);
        if(WIFSIGNALED(status))
        {
            printf("killed by %d\n",WTERMSIG(status));
        }

        //父进程只是关闭读写两端,但是并不退出
        while (1)
        {
            sleep(1);
        }

        char buf[12]={0};
        while(1)
        {
            int ret = read(fd[0],buf,sizeof(buf));
            //如果读到0,认为读到文件末尾
            if(ret == 0)
            {
                printf("read over!\n");
                break;
            }
            if(ret > 0)
            {
                write(STDOUT_FILENO,buf,ret);
            }
        }
    }
    return 0;
}

输出:

killed by 13
  • 读端未全部关闭。管道已满,--write阻塞,如果要显示现象,读端一直不读,写端狂写;管道未满,--write正常写入。

2、FIFO

        fifo常被称为命名管道,以区分管道pipe,管道(pipe)只能用于“有血缘关系”的进程间,但FIFO不相关的进程也能交换数据。

        FIFO是linux基础文件类型中的一种,但FIFO文件在磁盘上没有数据交换,仅仅用来标识内核中一条通道,各进程可以打开这个文件进行read\write,实际上是在读写内核通道,这样就实现了进程间通信。

       实现方式:

  1. 命令:mkfifo 管道名
  2. 库函数:int mkfifo(const char *pathname, mode_t mode); 成功返回0,失败-1。

      一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I\O函数都可以用于FIFO,如close、read、write、unlink等。

内核会针对fifo文件开辟一个缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信--实际上就是文件读写。fifo文件既可以直接“mkfifo+文件”名创建,也可以调用库函数mkfifo()创建。

练习:两个读写程序通过myfifo通信

  • 第一步:在当前目录下创建一个fifo文件用于标识“通道”
~/linux$ mkfifo myfifo
~/linux$ ls
myfifo
  • 第二步:创建一个写程序,并编译 gcc fifo_w.c -o fifo_w
/*fifo_w.c*/

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main(int agrc,char *agrv[])
{
    if(agrc != 2)
    {
        printf("./a.out fifoname\n");
        return -1;
    }

    //当前目录下已使用“mkfifo myfifo”创建了一个myfifo文件
    //打开myfifo文件
    int fd = open(agrv[1],O_WRONLY);
    //向fifo文件中写
    char buf[256];
    int num = 1;
    while (1)
    {
        memset(buf,0x00,sizeof(buf));
        sprintf(buf,"xiaoming%04d",num++);
        write(fd,buf,strlen(buf));
        sleep(1);
    }
    close(fd);
    return 0;
}
  • 第三步:创建一个读程序用于读,并编译 gcc fifo_r.c -o fifo_r
/*fifo_r.c*/

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main(int argc,char *agrv[])
{
    if(argc != 2)
    {
        printf("./a.out fifoname\n");
        return -1;
    }
    int fd = open(agrv[1],O_RDONLY);

    char buf[256];
    int ret;
    while (1)
    {
       //循环读
       ret = read(fd,buf,sizeof(buf));
       if(ret > 0)
       {
           printf("read:%s\n",buf);
       }
    }
    close(fd);
    return 0;
}
  • 第四步:运行程序
//运行读程序
./fifo_r myfifo

//运行写程序
./fifo_w myfifo

输出结果:

:~$ ./fifo_r myfifo
read:xiaoming0001
read:xiaoming0002
read:xiaoming0003
read:xiaoming0004
read:xiaoming0005
read:xiaoming0006
read:xiaoming0007
read:xiaoming0008
read:xiaoming0009

...

说明:读写程序通过fifo实现了通信,其中创建的myfifo只是一个标识“通道”。

FIFO的注意事项

打开FIFO文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另一端打开。对上面代码进行稍微改造,添加一些打印:

//fifo_w.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main(int agrc,char *agrv[])
{
    if(agrc != 2)
    {
        printf("./a.out fifoname\n");
        return -1;
    }

    //当前目录下已使用“mkfifo myfifo”创建了一个myfifo文件
    //打开myfifo文件
    printf("Begin open ...\n");
    int fd = open(agrv[1],O_WRONLY);
    printf("End open ...\n");
    //向fifo文件中写
    char buf[256];
    int num = 1;
    while (1)
    {
        memset(buf,0x00,sizeof(buf));
        sprintf(buf,"xiaoming%04d",num++);
        write(fd,buf,strlen(buf));
        sleep(1);
    }
    close(fd);
    return 0;
}

--------------------------------------------------------------------------

//fifo_r.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>

int main(int argc,char *agrv[])
{
    if(argc != 2)
    {
        printf("./a.out fifoname\n");
        return -1;
    }

    printf("Begin open read ...\n");
    int fd = open(agrv[1],O_RDONLY);
    printf("End open read ...\n");

    char buf[256];
    int ret;
    while (1)
    {
       //循环读
       ret = read(fd,buf,sizeof(buf));
       if(ret > 0)
       {
           printf("read:%s\n",buf);
       }
    }
    close(fd);
    return 0;
}

3、mmap 创建/释放映射区

(1)mmap函数原型--创建映射区

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

参数:

  • addr 传NULL
  • length 映射区的长度
  • prot:PROT_READ,可读;PROT_WRITE,可写。
  • flags:MAP_SHARED,共享的(对内存的修改会影响到源文件);MAP_PRIVATE,私有的。
  • fd 文件描述符,open打开一个文件
  • offset 偏移量

返回值:

  • 成功,返回可用的内存首地址
  • 失败,返回MAP_FAILED

(2)munmap函数原型--释放映射区

 int munmap(void *addr, size_t length);

参数:

  • addr 传mmap的返回值,即创建映射区内存首地址
  • length mmap创建的长度

返回值:

  •  On  success,  munmap() returns 0.  On failure, it returns -1, and errno

(3)练习

首先,在当前目录下创建 mem.txt 文件

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>
#include<stdio.h>
#include<unistd.h>

int main()
{
    int fd = open("mem.txt",O_RDWR);
    //int fd = open("mem.txt",O_RDWR|O_CREAT|O_TRUNC,0664);  //问题5:创建并且截断文件
    //ftruncate(fd,8);

    //int fd = open("mem.txt",O_WRONLY);  //问题6:open文件选择O_WRONLY

    //创建映射区
    char *mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    //char *mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,1000);  //问题3:文件偏移量

    if(mem == MAP_FAILED)
    {
        perror("mmap err");
        return -1;
    }

    //close(fd);  //问题4:提前关闭文件描述符

    //拷贝数据
    strcpy(mem,"hello");
    //strcpy(mem,"helloworld");  //问题2:对mem越界操作
    
    //mem++;  //问题1:更改mem变量地址
    //释放mmap
    munmap(mem,8);
    close(fd);
    return 0;
}

4、mmap相关问题

  • 问题1:如果更改mem变量的地址,释放的时候munmap传入mem还能成功吗?例如:在代码munmap前,mem++; 。

不能。

  • 问题2:如果对mem越界操作会怎样?

文件的大小对映射区有影响,应尽量避免。

  • 问题3:如果文件偏移量随便填个数会怎样?

offset必须是4k的整数倍。

  • 问题4:如果文件描述符先关闭,对mmap映射有没有影响?

没有影响。

  • 问题5:open的时候,可以新创建一个文件来创建映射区吗?

不可以用大小为0的文件。

  • 问题6:open文件选择O_WRONLY,可以吗?

不可以:permission denied

  • 问题7:当选择MAP_SHARED时,open文件选择O_RDONLY,port可以选择PROT_READ|PROT_WRITE吗?

Permission denied,SHARED的时候,映射区的权限小于等于open文件的权限。

int main()
{
    int fd = open("mem.txt",O_RDONLY);
    ftrunccate(fd,8);

    //创建映射区
    char *mem = mmap(NULL,20,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

    if(mem == MAP_FAILED)
    {
        perror("mmap err");
        return -1;
    }
    close(fd);
    //拷贝数据
    strcpy(mem,"helloworld");
    
    //释放mmap
    if(munmap(mem,20) < 0)
    {
        perror("munmap err");
    }

    return 0;
}
  • 问题8:mmap什么情况下会报错?多种情况下。
  • 问题9:如果不判断返回值会怎样?一定要判断。

5、父子进程mmap映射区共享

要求:父进程创建映射区,然后fork子进程,子进程修改映射区内容,然后,父进程读取映射区内容,查验是否共享。

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>

int main()
{
    //先创建映射区
    //int fd = open("mem.txt",O_RDWR);
    //int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);  //系统提供匿名映射区
    if(mem == MAP_FAILED)
    {
        perror("mmap err");
        return -1;
    }
    //fork子进程
    pid_t pid = fork();

    //父子进程交替修改数据
    if(pid == 0)
    {
        //子进程
        *mem = 100;
        printf("child, *mem=%d\n",*mem);
        sleep(3);
        printf("child, *mem=%d\n",*mem);
    }
    else if(pid > 0)
    {
        //父进程
        sleep(1);
        printf("parent, *mem=%d\n",*mem);
        *mem = 1001;
        printf("parent, *mem=%d\n",*mem);
        wait(NULL);  //进程回收,如果不想查看回收状态,传出参数NULL
    }

    munmap(mem,4);
    //close(fd);
    return 0;
}

输出:

child, *mem=100

child, *mem=1001  //子进程中也相应改变。说明父子进程共享映射区

parent, *mem=100

parent, *mem=1001  //父进程修改了映射区值

6、匿名映射

      通过使用可以发现,使用映射区来完成文件读写锁操作十分方便,父子进程间通信也较为容易。但是缺点是,每次创建映射区一定要依赖一个文件才能实现。(有时部分架构系统,如AMD可能会出错,Bus error (core dumped)

通常为了建立映射区,要open一个temp文件,创建好文件后再unlink、close掉,比较麻烦。可直接使用匿名映射区来代替,其实linux系统给提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flag指定:

使用 MAP_ANONYMOUS(或MAP_ANON),如

int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);  //系统提供匿名映射区

“4”随意举例,表示映射区大小,可依实际需要填写。

注意:MAP_ANONYMOUS(或MAP_ANON)这两个宏是linux操作系统特有的宏。

实际上,在linux系统中有一些文件可以作为temp文件使用,不用再使用open创建,/dev/zero 聚宝盆,可以随意映射;/dev/null 无底洞。

int fd = open("/dev/zero",O_RDWR);

7、mmap实现进程间通信

用mmap支持无血缘关系进程通信,flag必须设为MAP_SHARED。

两个程序,一个读,一个写,使用mmap进行通信。

~~小技巧:~/Linux_C$ head -7 mmap_w.c > mmap_r.c  使用该命令可以将mmap_w.c 文件中的7行头文件定向复制到 mmap_r.c 文件中。

//mmap_w.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>

typedef struct _Student
{
    int sid;
    char sname[20];
}Student;

int main(int agrc,char *agrv[])
{
    if(agrc != 2)
    {
        printf("./a.out filename\n");
        return -1;
    }

    //第一步:open file
    int fd = open(agrv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
    int length = sizeof(Student);
    ftruncate(fd,length);
    //第二步:创建mmap映射区
    Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(stu == MAP_FAILED)
    {
        perror("mmap err");
        return -1;
    }
    //第三步:修改内存数据
    int num = 1;
    while (1)
    {
        stu->sid = num;
        sprintf(stu->sname,"xiaoming-%03d",num++);
        sleep(1);  //相当于每隔1s修改一次映射区的内容
    }
    //第四步:释放映射区和关闭文件描述符
    munmap(stu,length);
    close(fd);

    return 0;
}


//mmap_r.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>

typedef struct _Student
{
    int sid;
    char sname[20];
}Student;

int main(int agrc,char *agrv[])
{
    //第一步:open file
    int fd = open(agrv[1],O_RDWR);
    //第二步:创建映射区
    int length = sizeof(Student);
    Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(stu == MAP_FAILED)
    {
        perror("mmap err");
        return -1;
    }
    //第三步:read data 读数据
    while (1)
    {
        printf("sid=%d,sname=%s\n",stu->sid,stu->sname);
        sleep(1);
    }
    //第四步:释放映射区和关闭文件描述符
    munmap(stu,length);
    close(fd);
    return 0;
}

~/Linux_C$ ./mmap_w xxx(xxx代表文件名)

打开另一个终端:

~/Linux_C$ ./mmap_r xxx
sid=9,sname=xiaoming-009
sid=10,sname=xiaoming-010
sid=11,sname=xiaoming-011
sid=12,sname=xiaoming-012
sid=13,sname=xiaoming-013
sid=14,sname=xiaoming-014
sid=15,sname=xiaoming-015
sid=16,sname=xiaoming-016
sid=17,sname=xiaoming-017
sid=18,sname=xiaoming-018

......

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值